fix(wm): handle moving windows to/from floating workspaces

This commit fixes the issue related to moving windows to/from a floating
workspace to a tiled workspace.

Previously the start of the move would be ignored however when moving
back from a tiled workspace since it didn't know about the existance of
that window it would also "move" that workspace focused tiled window
without physically moving it, leaving it in a weird state that seemed
like it was unmanaged.

This commit changes the way this mouse moves are handled and now also
handles moving `floating_windows` and even monocle or maximized windows.
This commit is contained in:
alex-ds13
2024-10-17 19:21:18 +01:00
committed by LGUG2Z
parent aa9f50fd5c
commit 5503323695
3 changed files with 297 additions and 159 deletions
+166 -158
View File
@@ -279,166 +279,170 @@ impl WindowManager {
WindowManagerEvent::Show(_, window) WindowManagerEvent::Show(_, window)
| WindowManagerEvent::Manage(window) | WindowManagerEvent::Manage(window)
| WindowManagerEvent::Uncloak(_, window) => { | WindowManagerEvent::Uncloak(_, window) => {
let focused_monitor_idx = self.focused_monitor_idx(); if matches!(event, WindowManagerEvent::Uncloak(_, _))
let focused_workspace_idx = && self.uncloack_to_ignore >= 1
self.focused_workspace_idx_for_monitor_idx(focused_monitor_idx)?; {
tracing::info!("ignoring uncloak after monocle move by mouse across monitors");
self.uncloack_to_ignore = self.uncloack_to_ignore.saturating_sub(1);
} else {
let focused_monitor_idx = self.focused_monitor_idx();
let focused_workspace_idx =
self.focused_workspace_idx_for_monitor_idx(focused_monitor_idx)?;
let focused_pair = (focused_monitor_idx, focused_workspace_idx); let focused_pair = (focused_monitor_idx, focused_workspace_idx);
let mut needs_reconciliation = false; let mut needs_reconciliation = false;
for (i, monitors) in self.monitors().iter().enumerate() { for (i, monitors) in self.monitors().iter().enumerate() {
for (j, workspace) in monitors.workspaces().iter().enumerate() { for (j, workspace) in monitors.workspaces().iter().enumerate() {
if workspace.contains_window(window.hwnd) && focused_pair != (i, j) { if workspace.contains_window(window.hwnd) && focused_pair != (i, j) {
// At this point we know we are going to send a notification to the workspace reconciliator // At this point we know we are going to send a notification to the workspace reconciliator
// So we get the topmost window returned by EnumWindows, which is almost always the window // So we get the topmost window returned by EnumWindows, which is almost always the window
// that has been selected by alt-tab // that has been selected by alt-tab
if let Ok(alt_tab_windows) = WindowsApi::alt_tab_windows() { if let Ok(alt_tab_windows) = WindowsApi::alt_tab_windows() {
if let Some(first) = if let Some(first) =
alt_tab_windows.iter().find(|w| w.title().is_ok()) alt_tab_windows.iter().find(|w| w.title().is_ok())
{
// If our record of this HWND hasn't been updated in over a minute
let mut instant = ALT_TAB_HWND_INSTANT.lock();
if instant.elapsed().gt(&Duration::from_secs(1)) {
// Update our record with the HWND we just found
ALT_TAB_HWND.store(Some(first.hwnd));
// Update the timestamp of our record
*instant = Instant::now();
}
}
}
workspace_reconciliator::send_notification(i, j);
needs_reconciliation = true;
}
}
}
// There are some applications such as Firefox where, if they are focused when a
// workspace switch takes place, it will fire an additional Show event, which will
// result in them being associated with both the original workspace and the workspace
// being switched to. This loop is to try to ensure that we don't end up with
// duplicates across multiple workspaces, as it results in ghost layout tiles.
let mut proceed = true;
for (i, monitor) in self.monitors().iter().enumerate() {
for (j, workspace) in monitor.workspaces().iter().enumerate() {
if workspace.contains_window(window.hwnd)
&& i != self.focused_monitor_idx()
&& j != monitor.focused_workspace_idx()
{
tracing::debug!(
"ignoring show event for window already associated with another workspace"
);
window.hide();
proceed = false;
}
}
}
if proceed {
let mut behaviour = self.window_management_behaviour(
focused_monitor_idx,
focused_workspace_idx,
);
let workspace = self.focused_workspace_mut()?;
let workspace_contains_window = workspace.contains_window(window.hwnd);
let monocle_container = workspace.monocle_container().clone();
if !workspace_contains_window && !needs_reconciliation {
let floating_applications = FLOATING_APPLICATIONS.lock();
let regex_identifiers = REGEX_IDENTIFIERS.lock();
let mut should_float = false;
if !floating_applications.is_empty() {
if let (Ok(title), Ok(exe_name), Ok(class), Ok(path)) =
(window.title(), window.exe(), window.class(), window.path())
{ {
// If our record of this HWND hasn't been updated in over a minute should_float = should_act(
let mut instant = ALT_TAB_HWND_INSTANT.lock(); &title,
if instant.elapsed().gt(&Duration::from_secs(1)) { &exe_name,
// Update our record with the HWND we just found &class,
ALT_TAB_HWND.store(Some(first.hwnd)); &path,
// Update the timestamp of our record &floating_applications,
*instant = Instant::now(); &regex_identifiers,
)
.is_some();
}
}
behaviour.float_override = behaviour.float_override
|| (should_float
&& !matches!(event, WindowManagerEvent::Manage(_)));
if behaviour.float_override {
workspace.floating_windows_mut().push(window);
self.update_focused_workspace(false, false)?;
} else {
match behaviour.current_behaviour {
WindowContainerBehaviour::Create => {
workspace.new_container_for_window(window);
self.update_focused_workspace(false, false)?;
}
WindowContainerBehaviour::Append => {
workspace
.focused_container_mut()
.ok_or_else(|| {
anyhow!("there is no focused container")
})?
.add_window(window);
self.update_focused_workspace(true, false)?;
stackbar_manager::send_notification();
} }
} }
} }
workspace_reconciliator::send_notification(i, j); if (self.focused_workspace()?.containers().len() == 1
needs_reconciliation = true; && self.focused_workspace()?.floating_windows().is_empty())
} || (self.focused_workspace()?.containers().is_empty()
} && self.focused_workspace()?.floating_windows().len() == 1)
}
// There are some applications such as Firefox where, if they are focused when a
// workspace switch takes place, it will fire an additional Show event, which will
// result in them being associated with both the original workspace and the workspace
// being switched to. This loop is to try to ensure that we don't end up with
// duplicates across multiple workspaces, as it results in ghost layout tiles.
let mut proceed = true;
for (i, monitor) in self.monitors().iter().enumerate() {
for (j, workspace) in monitor.workspaces().iter().enumerate() {
if workspace.contains_window(window.hwnd)
&& i != self.focused_monitor_idx()
&& j != monitor.focused_workspace_idx()
{
tracing::debug!(
"ignoring show event for window already associated with another workspace"
);
window.hide();
proceed = false;
}
}
}
if proceed {
let mut behaviour = self
.window_management_behaviour(focused_monitor_idx, focused_workspace_idx);
let workspace = self.focused_workspace_mut()?;
let workspace_contains_window = workspace.contains_window(window.hwnd);
let monocle_container = workspace.monocle_container().clone();
if !workspace_contains_window && !needs_reconciliation {
let floating_applications = FLOATING_APPLICATIONS.lock();
let regex_identifiers = REGEX_IDENTIFIERS.lock();
let mut should_float = false;
if !floating_applications.is_empty() {
if let (Ok(title), Ok(exe_name), Ok(class), Ok(path)) =
(window.title(), window.exe(), window.class(), window.path())
{ {
should_float = should_act( // If after adding this window the workspace only contains 1 window, it
&title, // means it was previously empty and we focused the desktop to unfocus
&exe_name, // any previous window from other workspace, so now we need to focus
&class, // this window again. This is needed because sometimes some windows
&path, // first send the `FocusChange` event and only the `Show` event after
&floating_applications, // and we will be focusing the desktop on the `FocusChange` event since
&regex_identifiers, // it is still empty.
) window.focus(self.mouse_follows_focus)?;
.is_some();
} }
} }
behaviour.float_override = behaviour.float_override if workspace_contains_window {
|| (should_float && !matches!(event, WindowManagerEvent::Manage(_))); let mut monocle_window_event = false;
if let Some(ref monocle) = monocle_container {
if behaviour.float_override { if let Some(monocle_window) = monocle.focused_window() {
workspace.floating_windows_mut().push(window); if monocle_window.hwnd == window.hwnd {
self.update_focused_workspace(false, false)?; monocle_window_event = true;
} else { }
match behaviour.current_behaviour {
WindowContainerBehaviour::Create => {
workspace.new_container_for_window(window);
self.update_focused_workspace(false, false)?;
}
WindowContainerBehaviour::Append => {
workspace
.focused_container_mut()
.ok_or_else(|| anyhow!("there is no focused container"))?
.add_window(window);
self.update_focused_workspace(true, false)?;
stackbar_manager::send_notification();
} }
} }
}
if (self.focused_workspace()?.containers().len() == 1 if !monocle_window_event && monocle_container.is_some() {
&& self.focused_workspace()?.floating_windows().is_empty()) window.hide();
|| (self.focused_workspace()?.containers().is_empty()
&& self.focused_workspace()?.floating_windows().len() == 1)
{
// If after adding this window the workspace only contains 1 window, it
// means it was previously empty and we focused the desktop to unfocus
// any previous window from other workspace, so now we need to focus
// this window again. This is needed because sometimes some windows
// first send the `FocusChange` event and only the `Show` event after
// and we will be focusing the desktop on the `FocusChange` event since
// it is still empty.
window.focus(self.mouse_follows_focus)?;
}
}
if workspace_contains_window {
let mut monocle_window_event = false;
if let Some(ref monocle) = monocle_container {
if let Some(monocle_window) = monocle.focused_window() {
if monocle_window.hwnd == window.hwnd {
monocle_window_event = true;
}
} }
} }
if !monocle_window_event && monocle_container.is_some() {
window.hide();
}
} }
} }
} }
WindowManagerEvent::MoveResizeStart(_, window) => { WindowManagerEvent::MoveResizeStart(_, window) => {
if *self.focused_workspace()?.tile() { let monitor_idx = self.focused_monitor_idx();
let monitor_idx = self.focused_monitor_idx(); let workspace_idx = self
let workspace_idx = self .focused_monitor()
.focused_monitor() .ok_or_else(|| anyhow!("there is no monitor with this idx"))?
.ok_or_else(|| anyhow!("there is no monitor with this idx"))? .focused_workspace_idx();
.focused_workspace_idx();
let container_idx = self
.focused_monitor()
.ok_or_else(|| anyhow!("there is no monitor with this idx"))?
.focused_workspace()
.ok_or_else(|| anyhow!("there is no workspace with this idx"))?
.focused_container_idx();
WindowsApi::bring_window_to_top(window.hwnd)?; WindowsApi::bring_window_to_top(window.hwnd)?;
let pending_move_op = Arc::make_mut(&mut self.pending_move_op); let pending_move_op = Arc::make_mut(&mut self.pending_move_op);
*pending_move_op = Option::from((monitor_idx, workspace_idx, container_idx)); *pending_move_op = Option::from((monitor_idx, workspace_idx, window.hwnd));
}
} }
WindowManagerEvent::MoveResizeEnd(_, window) => { WindowManagerEvent::MoveResizeEnd(_, window) => {
// We need this because if the event ends on a different monitor, // We need this because if the event ends on a different monitor,
@@ -448,6 +452,18 @@ impl WindowManager {
let pending_move_op = Arc::make_mut(&mut self.pending_move_op); let pending_move_op = Arc::make_mut(&mut self.pending_move_op);
*pending_move_op = None; *pending_move_op = None;
// If the window handles don't match then something went wrong and the pending move
// is not related to this current move, if so abort this operation.
if let Some((_, _, w_hwnd)) = pending {
if w_hwnd != window.hwnd {
color_eyre::eyre::bail!(
"window handles for move operation don't match: {} != {}",
w_hwnd,
window.hwnd
);
}
}
let target_monitor_idx = self let target_monitor_idx = self
.monitor_idx_from_current_pos() .monitor_idx_from_current_pos()
.ok_or_else(|| anyhow!("cannot get monitor idx from current position"))?; .ok_or_else(|| anyhow!("cannot get monitor idx from current position"))?;
@@ -492,8 +508,7 @@ impl WindowManager {
.get(origin_workspace_idx) .get(origin_workspace_idx)
.ok_or_else(|| anyhow!("cannot get workspace idx"))?; .ok_or_else(|| anyhow!("cannot get workspace idx"))?;
let managed_window = let managed_window = origin_workspace.contains_window(window.hwnd);
origin_workspace.contains_managed_window(window.hwnd);
if !managed_window { if !managed_window {
moved_across_monitors = false; moved_across_monitors = false;
@@ -523,11 +538,8 @@ impl WindowManager {
tracing::info!("moving with mouse"); tracing::info!("moving with mouse");
if moved_across_monitors { if moved_across_monitors {
if let Some(( if let Some((origin_monitor_idx, origin_workspace_idx, w_hwnd)) =
origin_monitor_idx, pending
origin_workspace_idx,
origin_container_idx,
)) = pending
{ {
let target_workspace_idx = self let target_workspace_idx = self
.monitors() .monitors()
@@ -547,18 +559,13 @@ impl WindowManager {
// Default to 0 in the case of an empty workspace // Default to 0 in the case of an empty workspace
.unwrap_or(0); .unwrap_or(0);
self.transfer_container( let origin = (origin_monitor_idx, origin_workspace_idx, w_hwnd);
( let target = (
origin_monitor_idx, target_monitor_idx,
origin_workspace_idx, target_workspace_idx,
origin_container_idx, target_container_idx,
), );
( self.transfer_window(origin, target)?;
target_monitor_idx,
target_workspace_idx,
target_container_idx,
),
)?;
// We want to make sure both the origin and target monitors are updated, // We want to make sure both the origin and target monitors are updated,
// so that we don't have ghost tiles until we force an interaction on // so that we don't have ghost tiles until we force an interaction on
@@ -570,9 +577,10 @@ impl WindowManager {
self.focus_monitor(target_monitor_idx)?; self.focus_monitor(target_monitor_idx)?;
self.focus_workspace(target_workspace_idx)?; self.focus_workspace(target_workspace_idx)?;
self.update_focused_workspace(false, false)?; self.update_focused_workspace(false, false)?;
// Make sure to give focus to the moved window again
window.focus(self.mouse_follows_focus)?;
} }
// Here we handle a simple move on the same monitor which is treated as
// a container swap
} else if window_management_behaviour.float_override { } else if window_management_behaviour.float_override {
workspace.floating_windows_mut().push(window); workspace.floating_windows_mut().push(window);
self.update_focused_workspace(false, false)?; self.update_focused_workspace(false, false)?;
+1
View File
@@ -1051,6 +1051,7 @@ impl StaticConfig {
has_pending_raise_op: false, has_pending_raise_op: false,
pending_move_op: Arc::new(None), pending_move_op: Arc::new(None),
already_moved_window_handles: Arc::new(Mutex::new(HashSet::new())), already_moved_window_handles: Arc::new(Mutex::new(HashSet::new())),
uncloack_to_ignore: 0,
}; };
match value.focus_follows_mouse { match value.focus_follows_mouse {
+130 -1
View File
@@ -102,8 +102,9 @@ pub struct WindowManager {
pub hotwatch: Hotwatch, pub hotwatch: Hotwatch,
pub virtual_desktop_id: Option<Vec<u8>>, pub virtual_desktop_id: Option<Vec<u8>>,
pub has_pending_raise_op: bool, pub has_pending_raise_op: bool,
pub pending_move_op: Arc<Option<(usize, usize, usize)>>, pub pending_move_op: Arc<Option<(usize, usize, isize)>>,
pub already_moved_window_handles: Arc<Mutex<HashSet<isize>>>, pub already_moved_window_handles: Arc<Mutex<HashSet<isize>>>,
pub uncloack_to_ignore: usize,
} }
#[allow(clippy::struct_excessive_bools)] #[allow(clippy::struct_excessive_bools)]
@@ -341,6 +342,7 @@ impl WindowManager {
has_pending_raise_op: false, has_pending_raise_op: false,
pending_move_op: Arc::new(None), pending_move_op: Arc::new(None),
already_moved_window_handles: Arc::new(Mutex::new(HashSet::new())), already_moved_window_handles: Arc::new(Mutex::new(HashSet::new())),
uncloack_to_ignore: 0,
}) })
} }
@@ -819,6 +821,133 @@ impl WindowManager {
Ok(()) Ok(())
} }
#[tracing::instrument(skip(self))]
pub fn transfer_window(
&mut self,
origin: (usize, usize, isize),
target: (usize, usize, usize),
) -> Result<()> {
let (origin_monitor_idx, origin_workspace_idx, w_hwnd) = origin;
let (target_monitor_idx, target_workspace_idx, target_container_idx) = target;
let origin_workspace = self
.monitors_mut()
.get_mut(origin_monitor_idx)
.ok_or_else(|| anyhow!("cannot get monitor idx"))?
.workspaces_mut()
.get_mut(origin_workspace_idx)
.ok_or_else(|| anyhow!("cannot get workspace idx"))?;
let origin_container_idx = origin_workspace
.container_for_window(w_hwnd)
.and_then(|c| origin_workspace.containers().iter().position(|cc| cc == c));
if let Some(origin_container_idx) = origin_container_idx {
// Moving normal container window
self.transfer_container(
(
origin_monitor_idx,
origin_workspace_idx,
origin_container_idx,
),
(
target_monitor_idx,
target_workspace_idx,
target_container_idx,
),
)?;
} else if let Some(idx) = origin_workspace
.floating_windows()
.iter()
.position(|w| w.hwnd == w_hwnd)
{
// Moving floating window
// There is no need to physically move the floating window between areas with
// `move_to_area` because the user already did that, so we only need to transfer the
// window to the target `floating_windows`
let floating_window = origin_workspace.floating_windows_mut().remove(idx);
let target_workspace = self
.monitors_mut()
.get_mut(target_monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor at this idx"))?
.focused_workspace_mut()
.ok_or_else(|| anyhow!("there is no focused workspace for this monitor"))?;
target_workspace
.floating_windows_mut()
.push(floating_window);
} else if origin_workspace
.monocle_container()
.as_ref()
.and_then(|monocle| monocle.focused_window().map(|w| w.hwnd == w_hwnd))
.unwrap_or_default()
{
// Moving monocle container
if let Some(monocle_idx) = origin_workspace.monocle_container_restore_idx() {
let origin_workspace = self
.monitors_mut()
.get_mut(origin_monitor_idx)
.ok_or_else(|| anyhow!("there is no monitor at this idx"))?
.workspaces_mut()
.get_mut(origin_workspace_idx)
.ok_or_else(|| anyhow!("there is no workspace for this monitor"))?;
let mut uncloack_amount = 0;
for container in origin_workspace.containers_mut() {
container.restore();
uncloack_amount += 1;
}
origin_workspace.reintegrate_monocle_container()?;
self.transfer_container(
(origin_monitor_idx, origin_workspace_idx, monocle_idx),
(
target_monitor_idx,
target_workspace_idx,
target_container_idx,
),
)?;
// After we restore the origin workspace, some windows that were cloacked
// by the monocle might now be uncloacked which would trigger a workspace
// reconciliation since the focused monitor would be different from origin.
// That workspace reconciliation would focus the window on the origin monitor.
// So we need to ignore the uncloak events produced by the origin workspace
// restore to avoid that issue.
self.uncloack_to_ignore = uncloack_amount;
}
} else if origin_workspace
.maximized_window()
.as_ref()
.map(|max| max.hwnd == w_hwnd)
.unwrap_or_default()
{
// Moving maximized_window
if let Some(maximized_idx) = origin_workspace.maximized_window_restore_idx() {
self.focus_monitor(origin_monitor_idx)?;
let origin_monitor = self
.focused_monitor_mut()
.ok_or_else(|| anyhow!("there is no origin monitor"))?;
origin_monitor.focus_workspace(origin_workspace_idx)?;
self.unmaximize_window()?;
self.focus_monitor(target_monitor_idx)?;
let target_monitor = self
.focused_monitor_mut()
.ok_or_else(|| anyhow!("there is no target monitor"))?;
target_monitor.focus_workspace(target_workspace_idx)?;
self.transfer_container(
(origin_monitor_idx, origin_workspace_idx, maximized_idx),
(
target_monitor_idx,
target_workspace_idx,
target_container_idx,
),
)?;
}
}
Ok(())
}
#[tracing::instrument(skip(self))] #[tracing::instrument(skip(self))]
pub fn transfer_container( pub fn transfer_container(
&mut self, &mut self,