From 7739c7a11e7152174d8d19f9450b503fdacabf18 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Fri, 12 Sep 2025 22:54:53 +0200 Subject: [PATCH 01/13] Implement and use a WindowListModel --- .../MultitaskingView/MonitorClone.vala | 42 +-------- src/Widgets/MultitaskingView/WindowClone.vala | 2 - .../WindowCloneContainer.vala | 92 +++++++------------ .../MultitaskingView/WorkspaceClone.vala | 69 ++------------ src/Widgets/WindowOverview.vala | 78 +--------------- src/WindowListener.vala | 2 + src/meson.build | 1 + 7 files changed, 50 insertions(+), 236 deletions(-) diff --git a/src/Widgets/MultitaskingView/MonitorClone.vala b/src/Widgets/MultitaskingView/MonitorClone.vala index eec6d59a4..0d562ae15 100644 --- a/src/Widgets/MultitaskingView/MonitorClone.vala +++ b/src/Widgets/MultitaskingView/MonitorClone.vala @@ -33,29 +33,13 @@ public class Gala.MonitorClone : ActorTarget { background = new BackgroundManager (display, monitor, false); - window_container = new WindowCloneContainer (wm, monitor_scale); + var windows = new WindowListModel (display, STACKING, true, monitor); + + window_container = new WindowCloneContainer (wm, windows, monitor_scale); window_container.add_constraint (new Clutter.BindConstraint (this, SIZE, 0.0f)); window_container.window_selected.connect ((w) => { window_selected (w); }); bind_property ("monitor-scale", window_container, "monitor-scale"); - display.window_entered_monitor.connect (window_entered); - display.window_left_monitor.connect (window_left); - -#if HAS_MUTTER48 - unowned GLib.List window_actors = display.get_compositor ().get_window_actors (); -#else - unowned GLib.List window_actors = display.get_window_actors (); -#endif - foreach (unowned Meta.WindowActor window_actor in window_actors) { - if (window_actor.is_destroyed ()) - continue; - - unowned Meta.Window window = window_actor.get_meta_window (); - if (window.get_monitor () == monitor) { - window_entered (monitor, window); - } - } - add_child (background); add_child (window_container); @@ -63,12 +47,6 @@ public class Gala.MonitorClone : ActorTarget { add_action (drop); } - ~MonitorClone () { - unowned var display = wm.get_display (); - display.window_entered_monitor.disconnect (window_entered); - display.window_left_monitor.disconnect (window_left); - } - /** * Make sure the MonitorClone is at the location of the monitor on the stage */ @@ -82,18 +60,4 @@ public class Gala.MonitorClone : ActorTarget { monitor_scale = display.get_monitor_scale (monitor); } - - private void window_left (int window_monitor, Meta.Window window) { - if (window_monitor != monitor) - return; - - window_container.remove_window (window); - } - - private void window_entered (int window_monitor, Meta.Window window) { - if (window_monitor != monitor || window.window_type != Meta.WindowType.NORMAL) - return; - - window_container.add_window (window); - } } diff --git a/src/Widgets/MultitaskingView/WindowClone.vala b/src/Widgets/MultitaskingView/WindowClone.vala index 421d7c103..0f77fadb5 100644 --- a/src/Widgets/MultitaskingView/WindowClone.vala +++ b/src/Widgets/MultitaskingView/WindowClone.vala @@ -437,8 +437,6 @@ public class Gala.WindowClone : ActorTarget, RootTarget { SignalHandler.disconnect (window.get_display (), check_confirm_dialog_cb); check_confirm_dialog_cb = 0; } - - destroy (); } private void actor_clicked (uint32 button, Clutter.InputDeviceType device_type = POINTER_DEVICE) { diff --git a/src/Widgets/MultitaskingView/WindowCloneContainer.vala b/src/Widgets/MultitaskingView/WindowCloneContainer.vala index 8adac200a..0b5f5f199 100644 --- a/src/Widgets/MultitaskingView/WindowCloneContainer.vala +++ b/src/Widgets/MultitaskingView/WindowCloneContainer.vala @@ -18,6 +18,7 @@ public class Gala.WindowCloneContainer : ActorTarget { public int padding_bottom { get; set; default = 12; } public WindowManager wm { get; construct; } + public WindowListModel windows { get; construct; } public float monitor_scale { get; construct set; } public bool overview_mode { get; construct; } @@ -29,79 +30,50 @@ public class Gala.WindowCloneContainer : ActorTarget { */ private unowned WindowClone? current_window = null; - public WindowCloneContainer (WindowManager wm, float monitor_scale, bool overview_mode = false) { - Object (wm: wm, monitor_scale: monitor_scale, overview_mode: overview_mode); + public WindowCloneContainer (WindowManager wm, WindowListModel windows, float monitor_scale, bool overview_mode = false) { + Object (wm: wm, windows: windows, monitor_scale: monitor_scale, overview_mode: overview_mode); } - /** - * Create a WindowClone for a Meta.Window and add it to the group - * - * @param window The window for which to create the WindowClone for - */ - public void add_window (Meta.Window window) { - var windows = new List (); - windows.append (window); - foreach (unowned var clone in (GLib.List) get_children ()) { - windows.append (clone.window); - } - - var new_window = new WindowClone (wm, window, monitor_scale, overview_mode); - new_window.selected.connect ((_new_window) => window_selected (_new_window.window)); - new_window.request_reposition.connect (() => reflow (false)); - new_window.destroy.connect ((_new_window) => { - // make sure to release reference if the window is selected - if (_new_window == current_window) { - select_next_window (Meta.MotionDirection.RIGHT, false); - } - - // if window is still selected, reset the selection - if (_new_window == current_window) { - current_window = null; - } + construct { + on_items_changed (0, 0, windows.get_n_items ()); + windows.items_changed.connect (on_items_changed); + } - reflow (false); - }); - bind_property ("monitor-scale", new_window, "monitor-scale"); + private void on_items_changed (uint position, uint removed, uint added) { + var to_remove = new HashTable (null, null); - unowned Meta.Window? target = null; - foreach (unowned var w in sort_windows (windows)) { - if (w != window) { - target = w; - continue; - } - break; + for (uint i = 0; i < removed; i++) { + var window_clone = (WindowClone) get_child_at_index ((int) position); + to_remove[window_clone.window] = window_clone; + remove_child (window_clone); } - // top most or no other children - if (target == null) { - add_child (new_window); - } + for (int i = (int) position; i < position + added; i++) { + var window = (Meta.Window) windows.get_item (i); - foreach (unowned var clone in (GLib.List) get_children ()) { - if (target == clone.window) { - insert_child_below (new_window, clone); - break; + WindowClone? clone = to_remove.take (window); + + if (clone == null) { + clone = new WindowClone (wm, window, monitor_scale, overview_mode); + clone.selected.connect ((_clone) => window_selected (_clone.window)); + clone.request_reposition.connect (() => reflow (false)); + bind_property ("monitor-scale", clone, "monitor-scale"); } + + insert_child_at_index (clone, i); } - reflow (false); - } + // Make sure we release the reference on the window + if (current_window != null && current_window.window in to_remove) { + select_next_window (RIGHT, false); - /** - * Find and remove the WindowClone for a MetaWindow - */ - public void remove_window (Meta.Window window) { - foreach (unowned var clone in (GLib.List) get_children ()) { - if (clone.window == window) { - remove_child (clone); - reflow (false); - break; + // There is no next window so select nothing + if (current_window.window in to_remove) { + current_window = null; } } - if (get_n_children () == 0) { - last_window_closed (); - } + reflow (false); } public override void start_progress (GestureAction action) { @@ -147,6 +119,8 @@ public class Gala.WindowCloneContainer : ActorTarget { * during animations correct. */ private void restack_windows () { + windows.sort (); + return; var children = (GLib.List) get_children (); var windows = new GLib.List (); diff --git a/src/Widgets/MultitaskingView/WorkspaceClone.vala b/src/Widgets/MultitaskingView/WorkspaceClone.vala index 40c986e12..b6563b6b9 100644 --- a/src/Widgets/MultitaskingView/WorkspaceClone.vala +++ b/src/Widgets/MultitaskingView/WorkspaceClone.vala @@ -121,6 +121,7 @@ public class Gala.WorkspaceClone : ActorTarget { public WindowCloneContainer window_container { get; private set; } private BackgroundManager background; + private WindowListModel windows; private uint hover_activate_timeout = 0; public WorkspaceClone (WindowManager wm, Meta.Workspace workspace, float monitor_scale) { @@ -136,7 +137,9 @@ public class Gala.WorkspaceClone : ActorTarget { background = new FramedBackground (display); background.add_action (background_click_action); - window_container = new WindowCloneContainer (wm, monitor_scale) { + windows = new WindowListModel (display, STACKING, true, display.get_primary_monitor (), workspace); + + window_container = new WindowCloneContainer (wm, windows, monitor_scale) { width = monitor_geometry.width, height = monitor_geometry.height, }; @@ -164,22 +167,9 @@ public class Gala.WorkspaceClone : ActorTarget { } }); - display.window_entered_monitor.connect (window_entered_monitor); - display.window_left_monitor.connect (window_left_monitor); - workspace.window_added.connect (add_window); - workspace.window_removed.connect (window_container.remove_window); - add_child (background); add_child (window_container); - // add existing windows - foreach (var window in workspace.list_windows ()) { - add_window (window); - } - - var static_windows = StaticWindowContainer.get_instance (display); - static_windows.window_changed.connect (on_window_static_changed); - unowned var monitor_manager = display.get_context ().get_backend ().get_monitor_manager (); monitor_manager.monitors_changed.connect (update_targets); notify["monitor-scale"].connect (update_targets); @@ -187,56 +177,10 @@ public class Gala.WorkspaceClone : ActorTarget { } ~WorkspaceClone () { - unowned var display = workspace.get_display (); - - display.window_entered_monitor.disconnect (window_entered_monitor); - display.window_left_monitor.disconnect (window_left_monitor); - workspace.window_added.disconnect (add_window); - workspace.window_removed.disconnect (window_container.remove_window); - background.destroy (); window_container.destroy (); } - /** - * Add a window to the WindowCloneContainer if it belongs to this workspace and this monitor. - */ - private void add_window (Meta.Window window) { - if (window.window_type != NORMAL || - window.get_workspace () != workspace || - StaticWindowContainer.get_instance (workspace.get_display ()).is_static (window) || - !window.is_on_primary_monitor () - ) { - return; - } - - foreach (var child in (GLib.List) window_container.get_children ()) { - if (child.window == window) { - return; - } - } - - window_container.add_window (window); - } - - private void window_entered_monitor (Meta.Display display, int monitor, Meta.Window window) { - add_window (window); - } - - private void window_left_monitor (Meta.Display display, int monitor, Meta.Window window) { - if (monitor == display.get_primary_monitor ()) { - window_container.remove_window (window); - } - } - - private void on_window_static_changed (Meta.Window window, bool is_static) { - if (is_static) { - window_container.remove_window (window); - } else { - add_window (window); - } - } - public void update_size (Mtk.Rectangle monitor_geometry) { if (window_container.width != monitor_geometry.width || window_container.height != monitor_geometry.height) { window_container.set_size (monitor_geometry.width, monitor_geometry.height); @@ -248,8 +192,11 @@ public class Gala.WorkspaceClone : ActorTarget { remove_all_targets (); unowned var display = workspace.get_display (); + var primary = display.get_primary_monitor (); + + windows.monitor_filter = primary; - var monitor = display.get_monitor_geometry (display.get_primary_monitor ()); + var monitor = display.get_monitor_geometry (primary); var scale = (float)(monitor.height - Utils.scale_to_int (TOP_OFFSET + BOTTOM_OFFSET, monitor_scale)) / monitor.height; var pivot_y = Utils.scale_to_int (TOP_OFFSET, monitor_scale) / (monitor.height - monitor.height * scale); diff --git a/src/Widgets/WindowOverview.vala b/src/Widgets/WindowOverview.vala index 78ead9884..a76a83fc2 100644 --- a/src/Widgets/WindowOverview.vala +++ b/src/Widgets/WindowOverview.vala @@ -103,13 +103,6 @@ public class Gala.WindowOverview : ActorTarget, RootTarget, ActivatableComponent return; } - foreach (var workspace in workspaces) { - workspace.window_added.connect (add_window); - workspace.window_removed.connect (remove_window); - } - - wm.get_display ().window_left_monitor.connect (window_left_monitor); - grab_key_focus (); modal_proxy = wm.push_modal (this, true); @@ -122,7 +115,9 @@ public class Gala.WindowOverview : ActorTarget, RootTarget, ActivatableComponent var geometry = display.get_monitor_geometry (i); var scale = display.get_monitor_scale (i); - window_clone_container = new WindowCloneContainer (wm, scale, true) { + var model = new WindowListModel (display, STACKING, true, i); + + window_clone_container = new WindowCloneContainer (wm, model, scale, true) { padding_top = TOP_GAP, padding_left = BORDER, padding_right = BORDER, @@ -144,13 +139,6 @@ public class Gala.WindowOverview : ActorTarget, RootTarget, ActivatableComponent foreach (unowned var window in windows) { unowned var actor = (Meta.WindowActor) window.get_compositor_private (); actor.hide (); - - unowned var container = (WindowCloneContainer) get_child_at_index (window.get_monitor ()); - if (container == null) { - continue; - } - - container.add_window (window); } gesture_controller.goto (1); @@ -177,60 +165,6 @@ public class Gala.WindowOverview : ActorTarget, RootTarget, ActivatableComponent return true; } - private void window_left_monitor (int num, Meta.Window window) { - unowned var container = (WindowCloneContainer) get_child_at_index (num); - if (container == null) { - return; - } - - // make sure the window belongs to one of our workspaces - foreach (var workspace in workspaces) { - if (window.located_on_workspace (workspace)) { - container.remove_window (window); - break; - } - } - } - - private void add_window (Meta.Window window) { - if (!visible) { - return; - } - if (window.window_type == Meta.WindowType.DOCK || NotificationStack.is_notification (window)) { - return; - } - if (window.window_type != Meta.WindowType.NORMAL && - window.window_type != Meta.WindowType.DIALOG || - window.is_attached_dialog ()) { - unowned var actor = (Meta.WindowActor) window.get_compositor_private (); - actor.hide (); - - return; - } - - unowned var container = (WindowCloneContainer) get_child_at_index (window.get_monitor ()); - if (container == null) { - return; - } - - // make sure the window belongs to one of our workspaces - foreach (var workspace in workspaces) { - if (window.located_on_workspace (workspace)) { - container.add_window (window); - break; - } - } - } - - private void remove_window (Meta.Window window) { - unowned var container = (WindowCloneContainer) get_child_at_index (window.get_monitor ()); - if (container == null) { - return; - } - - container.remove_window (window); - } - private void thumb_selected (Meta.Window window) { if (window.get_workspace () == wm.get_display ().get_workspace_manager ().get_active_workspace ()) { window.activate (window.get_display ().get_current_time ()); @@ -254,12 +188,6 @@ public class Gala.WindowOverview : ActorTarget, RootTarget, ActivatableComponent return; } - foreach (var workspace in workspaces) { - workspace.window_added.disconnect (add_window); - workspace.window_removed.disconnect (remove_window); - } - wm.get_display ().window_left_monitor.disconnect (window_left_monitor); - #if HAS_MUTTER48 GLib.Timeout.add (MultitaskingView.ANIMATION_DURATION, () => { #else diff --git a/src/WindowListener.vala b/src/WindowListener.vala index 28953faac..f822f7fbe 100644 --- a/src/WindowListener.vala +++ b/src/WindowListener.vala @@ -53,6 +53,7 @@ public class Gala.WindowListener : Object { return instance; } + public signal void window_workspace_changed (Meta.Window window); public signal void window_on_all_workspaces_changed (Meta.Window window); private Gee.HashMap unmaximized_state_geometry; @@ -63,6 +64,7 @@ public class Gala.WindowListener : Object { private void monitor_window (Meta.Window window) { window.notify.connect (window_notify); + window.workspace_changed.connect ((win) => window_workspace_changed (win)); window.unmanaged.connect (window_removed); window_maximized_changed (window); diff --git a/src/meson.build b/src/meson.build index e9bc57c69..e7bd3f4a3 100644 --- a/src/meson.build +++ b/src/meson.build @@ -18,6 +18,7 @@ gala_bin_sources = files( 'WindowAttentionTracker.vala', 'WindowDragProvider.vala', 'WindowListener.vala', + 'WindowListModel.vala', 'WindowManager.vala', 'WindowStateSaver.vala', 'WindowTracker.vala', From f31cf54b146af39173d126ba1cf7a2b73b522909 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Fri, 12 Sep 2025 22:55:07 +0200 Subject: [PATCH 02/13] Add WindowListModel --- src/WindowListModel.vala | 136 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 src/WindowListModel.vala diff --git a/src/WindowListModel.vala b/src/WindowListModel.vala new file mode 100644 index 000000000..5625b3e98 --- /dev/null +++ b/src/WindowListModel.vala @@ -0,0 +1,136 @@ +/* + * Copyright 2025 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + + public class Gala.WindowListModel : Object, ListModel { + public enum SortMode { + NONE, + STACKING + } + + public Meta.Display display { get; construct; } + + public SortMode sort_mode { get; construct; } + + /** + * If true only present windows that are normal as gotten by {@link InternalUtils.get_window_is_normal}. + */ + public bool normal_filter { get; construct set; } + + /** + * If >= 0 only present windows that are on this monitor. + */ + public int monitor_filter { get; construct set; } + + /** + * If not null only present windows that are on this workspace. + * This also excludes static windows as defined by {@link StaticWindowContainer.is_static}. + */ + public Meta.Workspace? workspace_filter { get; construct set; } + + private Gee.ArrayList windows; + + public WindowListModel ( + Meta.Display display, SortMode sort_mode = NONE, + bool normal_filter = false, int monitor_filter = -1, + Meta.Workspace? workspace_filter = null + ) { + Object ( + display: display, sort_mode: sort_mode, normal_filter: normal_filter, + monitor_filter: monitor_filter, workspace_filter: workspace_filter + ); + } + + construct { + windows = new Gee.ArrayList (); + + display.window_created.connect (on_window_created); + + WindowListener.get_default ().window_workspace_changed.connect (check_window); + + StaticWindowContainer.get_instance (display).window_changed.connect (check_window); + display.window_entered_monitor.connect ((monitor, win) => check_window (win)); + + notify.connect (check_all); + + check_all (); + } + + private void on_window_created (Meta.Window window) { + window.unmanaged.connect ((win) => windows.remove (win)); + check_window (window); + } + + private void check_all () { + foreach (var window in display.list_all_windows ()) { + check_window (window); + } + } + + private void check_window (Meta.Window window) { + var exists = window in windows; + var should_exist = should_present_window (window); + + if (!exists && should_exist) { + windows.add (window); + items_changed (windows.size - 1, 0, 1); + } else if (exists && !should_exist) { + var pos = windows.index_of (window); + windows.remove_at (pos); + items_changed (pos, 1, 0); + } + } + + private bool should_present_window (Meta.Window window) { + if (monitor_filter >= 0 && monitor_filter != window.get_monitor ()) { + return false; + } + + if (workspace_filter != null && + (StaticWindowContainer.get_instance (display).is_static (window) || + !window.located_on_workspace (workspace_filter)) + ) { + return false; + } + + if (normal_filter && !Utils.get_window_is_normal (window)) { + return false; + } + + return true; + } + + public void sort () { + if (sort_mode == STACKING) { + var to_sort = new GLib.SList (); + + foreach (var window in windows) { + to_sort.prepend (window); + } + + var sorted = display.sort_windows_by_stacking (to_sort); + + int i = 0; + foreach (var window in sorted) { + windows.set (i++, window); + } + + items_changed (0, windows.size, windows.size); + } + } + + public Object? get_item (uint position) { + return windows.get ((int) position); + } + + public uint get_n_items () { + return windows.size; + } + + public Type get_item_type () { + return typeof (Meta.Window); + } +} From f6d6daf036642d5ff067195159f85bd7431cd680 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Sun, 14 Sep 2025 12:52:43 +0200 Subject: [PATCH 03/13] Fix flickering --- src/Widgets/MultitaskingView/WindowCloneContainer.vala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Widgets/MultitaskingView/WindowCloneContainer.vala b/src/Widgets/MultitaskingView/WindowCloneContainer.vala index 0b5f5f199..15e4a45ed 100644 --- a/src/Widgets/MultitaskingView/WindowCloneContainer.vala +++ b/src/Widgets/MultitaskingView/WindowCloneContainer.vala @@ -73,7 +73,10 @@ public class Gala.WindowCloneContainer : ActorTarget { } } - reflow (false); + // Don't reflow if only the sorting changed + if (to_remove.size () > 0 || added != removed) { + reflow (false); + } } public override void start_progress (GestureAction action) { From 912e435f5844c961f46f16a3b7a808c8d5ac1d99 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Sun, 14 Sep 2025 13:04:09 +0200 Subject: [PATCH 04/13] fix no signal on unmanaged --- src/WindowListModel.vala | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/WindowListModel.vala b/src/WindowListModel.vala index 5625b3e98..901dccd64 100644 --- a/src/WindowListModel.vala +++ b/src/WindowListModel.vala @@ -60,10 +60,16 @@ } private void on_window_created (Meta.Window window) { - window.unmanaged.connect ((win) => windows.remove (win)); + window.unmanaged.connect (on_window_unmanaged); check_window (window); } + private void on_window_unmanaged (Meta.Window window) { + var pos = windows.index_of (window); + windows.remove_at (pos); + items_changed (pos, 1, 0); + } + private void check_all () { foreach (var window in display.list_all_windows ()) { check_window (window); From 81cf96c2ce65d71f5dfb6b90f4677f6db0c2d960 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Sun, 14 Sep 2025 13:40:24 +0200 Subject: [PATCH 05/13] fix assertion --- src/WindowListModel.vala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/WindowListModel.vala b/src/WindowListModel.vala index 901dccd64..afa52d3d3 100644 --- a/src/WindowListModel.vala +++ b/src/WindowListModel.vala @@ -66,8 +66,10 @@ private void on_window_unmanaged (Meta.Window window) { var pos = windows.index_of (window); - windows.remove_at (pos); - items_changed (pos, 1, 0); + if (pos >= 0) { + windows.remove_at (pos); + items_changed (pos, 1, 0); + } } private void check_all () { From e9e950c3a01117c8253acc775f6f2b62ef379ab6 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Sun, 14 Sep 2025 14:03:22 +0200 Subject: [PATCH 06/13] Cleanup + correct sort --- .../WindowCloneContainer.vala | 65 ++----------------- src/WindowListModel.vala | 42 ++++++++---- 2 files changed, 33 insertions(+), 74 deletions(-) diff --git a/src/Widgets/MultitaskingView/WindowCloneContainer.vala b/src/Widgets/MultitaskingView/WindowCloneContainer.vala index 15e4a45ed..1f6b7700c 100644 --- a/src/Widgets/MultitaskingView/WindowCloneContainer.vala +++ b/src/Widgets/MultitaskingView/WindowCloneContainer.vala @@ -40,6 +40,8 @@ public class Gala.WindowCloneContainer : ActorTarget { } private void on_items_changed (uint position, uint removed, uint added) { + // Used to make sure we only construct new window clones for windows that are really new + // and not when only the position changed (e.g. when sorted) var to_remove = new HashTable (null, null); for (uint i = 0; i < removed; i++) { @@ -95,10 +97,10 @@ public class Gala.WindowCloneContainer : ActorTarget { } } - restack_windows (); + windows.sort (); reflow (true); } else if (action == MULTITASKING_VIEW) { // If we are open we only want to restack when we close - restack_windows (); + windows.sort (); } } @@ -117,36 +119,6 @@ public class Gala.WindowCloneContainer : ActorTarget { } } - /** - * Sort the windows z-order by their actual stacking to make intersections - * during animations correct. - */ - private void restack_windows () { - windows.sort (); - return; - var children = (GLib.List) get_children (); - - var windows = new GLib.List (); - foreach (unowned var clone in children) { - windows.prepend (clone.window); - } - - var windows_ordered = sort_windows (windows); - windows_ordered.reverse (); - - var i = 0; - foreach (unowned var window in windows_ordered) { - foreach (unowned var clone in children) { - if (clone.window == window) { - set_child_at_index (clone, i); - children.remove (clone); - i++; - break; - } - } - } - } - /** * Recalculate the tiling positions of the windows and animate them to the resulting spots. */ @@ -327,35 +299,6 @@ public class Gala.WindowCloneContainer : ActorTarget { current_window = closest; } - /** - * Sorts the windows by stacking order so that the window on active workspaces come first. - */ - private GLib.SList sort_windows (GLib.List windows) { - unowned var display = wm.get_display (); - - var windows_on_active_workspace = new GLib.SList (); - var windows_on_other_workspaces = new GLib.SList (); - unowned var active_workspace = display.get_workspace_manager ().get_active_workspace (); - foreach (unowned var window in windows) { - if (window.get_workspace () == active_workspace) { - windows_on_active_workspace.append (window); - } else { - windows_on_other_workspaces.append (window); - } - } - - var sorted_windows = new GLib.SList (); - var windows_on_active_workspace_sorted = display.sort_windows_by_stacking (windows_on_active_workspace); - windows_on_active_workspace_sorted.reverse (); - var windows_on_other_workspaces_sorted = display.sort_windows_by_stacking (windows_on_other_workspaces); - windows_on_other_workspaces_sorted.reverse (); - sorted_windows.concat ((owned) windows_on_active_workspace_sorted); - sorted_windows.concat ((owned) windows_on_other_workspaces_sorted); - - return sorted_windows; - } - - // Code ported from KWin present windows effect // https://projects.kde.org/projects/kde/kde-workspace/repository/revisions/master/entry/kwin/effects/presentwindows/presentwindows.cpp diff --git a/src/WindowListModel.vala b/src/WindowListModel.vala index afa52d3d3..9bfacb42e 100644 --- a/src/WindowListModel.vala +++ b/src/WindowListModel.vala @@ -5,7 +5,7 @@ * Authored by: Leonhard Kargl */ - public class Gala.WindowListModel : Object, ListModel { +public class Gala.WindowListModel : Object, ListModel { public enum SortMode { NONE, STACKING @@ -16,7 +16,7 @@ public SortMode sort_mode { get; construct; } /** - * If true only present windows that are normal as gotten by {@link InternalUtils.get_window_is_normal}. + * If true only present windows that are normal as gotten by {@link Utils.get_window_is_normal}. */ public bool normal_filter { get; construct set; } @@ -113,17 +113,9 @@ public void sort () { if (sort_mode == STACKING) { - var to_sort = new GLib.SList (); - - foreach (var window in windows) { - to_sort.prepend (window); - } - - var sorted = display.sort_windows_by_stacking (to_sort); - int i = 0; - foreach (var window in sorted) { - windows.set (i++, window); + foreach (var window in get_sorted_windows ()) { + windows[i++] = window; } items_changed (0, windows.size, windows.size); @@ -131,7 +123,7 @@ } public Object? get_item (uint position) { - return windows.get ((int) position); + return windows[(int) position]; } public uint get_n_items () { @@ -141,4 +133,28 @@ public Type get_item_type () { return typeof (Meta.Window); } + + /** + * Sorts the windows by stacking order so that the window on active workspaces come first. + */ + private GLib.SList get_sorted_windows () { + var windows_on_active_workspace = new GLib.SList (); + var windows_on_other_workspaces = new GLib.SList (); + unowned var active_workspace = display.get_workspace_manager ().get_active_workspace (); + foreach (var window in windows) { + if (window.get_workspace () == active_workspace) { + windows_on_active_workspace.prepend (window); + } else { + windows_on_other_workspaces.prepend (window); + } + } + + var sorted_windows = new GLib.SList (); + var windows_on_active_workspace_sorted = display.sort_windows_by_stacking (windows_on_active_workspace); + var windows_on_other_workspaces_sorted = display.sort_windows_by_stacking (windows_on_other_workspaces); + sorted_windows.concat ((owned) windows_on_active_workspace_sorted); + sorted_windows.concat ((owned) windows_on_other_workspaces_sorted); + + return sorted_windows; + } } From ba51fc363d9fdc48486f50476b165a4507832d5c Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Fri, 19 Sep 2025 13:37:19 +0200 Subject: [PATCH 07/13] Sorting fixes + introduce custom filter --- src/Widgets/WindowOverview.vala | 3 +++ src/WindowListModel.vala | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Widgets/WindowOverview.vala b/src/Widgets/WindowOverview.vala index a76a83fc2..e035ae961 100644 --- a/src/Widgets/WindowOverview.vala +++ b/src/Widgets/WindowOverview.vala @@ -116,6 +116,9 @@ public class Gala.WindowOverview : ActorTarget, RootTarget, ActivatableComponent var scale = display.get_monitor_scale (i); var model = new WindowListModel (display, STACKING, true, i); + model.set_custom_filter ((window) => { + return window_ids == null || (window.get_id () in window_ids); + }); window_clone_container = new WindowCloneContainer (wm, model, scale, true) { padding_top = TOP_GAP, diff --git a/src/WindowListModel.vala b/src/WindowListModel.vala index 9bfacb42e..c3a2b982f 100644 --- a/src/WindowListModel.vala +++ b/src/WindowListModel.vala @@ -11,6 +11,8 @@ public class Gala.WindowListModel : Object, ListModel { STACKING } + public delegate bool WindowFilter (Meta.Window window); + public Meta.Display display { get; construct; } public SortMode sort_mode { get; construct; } @@ -33,6 +35,8 @@ public class Gala.WindowListModel : Object, ListModel { private Gee.ArrayList windows; + private WindowFilter? custom_filter = null; + public WindowListModel ( Meta.Display display, SortMode sort_mode = NONE, bool normal_filter = false, int monitor_filter = -1, @@ -59,6 +63,11 @@ public class Gala.WindowListModel : Object, ListModel { check_all (); } + public void set_custom_filter (WindowFilter? filter) { + custom_filter = filter; + check_all (); + } + private void on_window_created (Meta.Window window) { window.unmanaged.connect (on_window_unmanaged); check_window (window); @@ -108,6 +117,10 @@ public class Gala.WindowListModel : Object, ListModel { return false; } + if (custom_filter != null) { + return custom_filter (window); + } + return true; } @@ -152,8 +165,8 @@ public class Gala.WindowListModel : Object, ListModel { var sorted_windows = new GLib.SList (); var windows_on_active_workspace_sorted = display.sort_windows_by_stacking (windows_on_active_workspace); var windows_on_other_workspaces_sorted = display.sort_windows_by_stacking (windows_on_other_workspaces); - sorted_windows.concat ((owned) windows_on_active_workspace_sorted); sorted_windows.concat ((owned) windows_on_other_workspaces_sorted); + sorted_windows.concat ((owned) windows_on_active_workspace_sorted); return sorted_windows; } From 0300bd6c8e2bf5abcead14011607af810d2692af Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Thu, 9 Oct 2025 14:34:16 +0200 Subject: [PATCH 08/13] Guarantee window actor while window is in the model --- src/WindowListModel.vala | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/WindowListModel.vala b/src/WindowListModel.vala index c3a2b982f..6d4f83d61 100644 --- a/src/WindowListModel.vala +++ b/src/WindowListModel.vala @@ -5,6 +5,11 @@ * Authored by: Leonhard Kargl */ +/** + * A list model that provides all current windows optionally filtered and sorted. + * While a window is in the model it is guaranteed to have an associated actor, i.e. + * {@link Meta.Window.get_compositor_private} will not return null. + */ public class Gala.WindowListModel : Object, ListModel { public enum SortMode { NONE, @@ -69,11 +74,11 @@ public class Gala.WindowListModel : Object, ListModel { } private void on_window_created (Meta.Window window) { - window.unmanaged.connect (on_window_unmanaged); - check_window (window); + window.unmanaging.connect (on_window_unmanaging); + InternalUtils.wait_for_window_actor (window, (actor) => check_window (actor.meta_window)); } - private void on_window_unmanaged (Meta.Window window) { + private void on_window_unmanaging (Meta.Window window) { var pos = windows.index_of (window); if (pos >= 0) { windows.remove_at (pos); From d5cc5e138452165ddacf1b042c12d19abfa91951 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Fri, 10 Oct 2025 16:26:12 +0200 Subject: [PATCH 09/13] Replace last_window_closed signal --- src/Widgets/MultitaskingView/WindowCloneContainer.vala | 1 - src/Widgets/WindowOverview.vala | 6 +++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Widgets/MultitaskingView/WindowCloneContainer.vala b/src/Widgets/MultitaskingView/WindowCloneContainer.vala index e984874ca..708534a41 100644 --- a/src/Widgets/MultitaskingView/WindowCloneContainer.vala +++ b/src/Widgets/MultitaskingView/WindowCloneContainer.vala @@ -10,7 +10,6 @@ public class Gala.WindowCloneContainer : ActorTarget { public signal void window_selected (Meta.Window window); public signal void requested_close (); - public signal void last_window_closed (); public int padding_top { get; set; default = 12; } public int padding_left { get; set; default = 12; } diff --git a/src/Widgets/WindowOverview.vala b/src/Widgets/WindowOverview.vala index e035ae961..7df65b992 100644 --- a/src/Widgets/WindowOverview.vala +++ b/src/Widgets/WindowOverview.vala @@ -119,6 +119,11 @@ public class Gala.WindowOverview : ActorTarget, RootTarget, ActivatableComponent model.set_custom_filter ((window) => { return window_ids == null || (window.get_id () in window_ids); }); + model.items_changed.connect ((_model, pos, removed, added) => { + if (_model.get_n_items () == 0) { + close (); + } + }); window_clone_container = new WindowCloneContainer (wm, model, scale, true) { padding_top = TOP_GAP, @@ -132,7 +137,6 @@ public class Gala.WindowOverview : ActorTarget, RootTarget, ActivatableComponent }; window_clone_container.window_selected.connect (thumb_selected); window_clone_container.requested_close.connect (() => close ()); - window_clone_container.last_window_closed.connect (() => close ()); add_child (window_clone_container); } From 450dc8d485728306518d58c688a6202da5c79788 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Fri, 10 Oct 2025 17:27:10 +0200 Subject: [PATCH 10/13] Fix infinite loop --- src/Widgets/WindowOverview.vala | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/Widgets/WindowOverview.vala b/src/Widgets/WindowOverview.vala index 7df65b992..b83b2c34f 100644 --- a/src/Widgets/WindowOverview.vala +++ b/src/Widgets/WindowOverview.vala @@ -19,6 +19,8 @@ public class Gala.WindowOverview : ActorTarget, RootTarget, ActivatableComponent private List workspaces; private WindowCloneContainer window_clone_container; + private uint64[]? window_ids = null; + public WindowOverview (WindowManager wm) { Object (wm : wm); } @@ -66,10 +68,7 @@ public class Gala.WindowOverview : ActorTarget, RootTarget, ActivatableComponent workspaces.append (workspace); } - uint64[]? window_ids = null; - if (hints != null && "windows" in hints) { - window_ids = (uint64[]) hints["windows"]; - } + window_ids = hints != null && "windows" in hints ? (uint64[]) hints["windows"] : null; var windows = new List (); foreach (var workspace in workspaces) { @@ -116,14 +115,8 @@ public class Gala.WindowOverview : ActorTarget, RootTarget, ActivatableComponent var scale = display.get_monitor_scale (i); var model = new WindowListModel (display, STACKING, true, i); - model.set_custom_filter ((window) => { - return window_ids == null || (window.get_id () in window_ids); - }); - model.items_changed.connect ((_model, pos, removed, added) => { - if (_model.get_n_items () == 0) { - close (); - } - }); + model.set_custom_filter (window_filter_func); + model.items_changed.connect (on_items_changed); window_clone_container = new WindowCloneContainer (wm, model, scale, true) { padding_top = TOP_GAP, @@ -172,6 +165,16 @@ public class Gala.WindowOverview : ActorTarget, RootTarget, ActivatableComponent return true; } + private bool window_filter_func (Meta.Window window) { + return window_ids == null || (window.get_id () in window_ids); + } + + private void on_items_changed (ListModel model, uint pos, uint removed, uint added) { + if (is_opened () && removed > added && model.get_n_items () == 0) { + close (); + } + } + private void thumb_selected (Meta.Window window) { if (window.get_workspace () == wm.get_display ().get_workspace_manager ().get_active_workspace ()) { window.activate (window.get_display ().get_current_time ()); From 7612038eaa5aa6b40eac5cd42ad693f4e594df02 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Sun, 12 Oct 2025 14:58:37 +0200 Subject: [PATCH 11/13] Add a comment explaining removed > added --- src/Widgets/WindowOverview.vala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Widgets/WindowOverview.vala b/src/Widgets/WindowOverview.vala index 3ab19788f..3444d0fa7 100644 --- a/src/Widgets/WindowOverview.vala +++ b/src/Widgets/WindowOverview.vala @@ -171,6 +171,8 @@ public class Gala.WindowOverview : ActorTarget, RootTarget, ActivatableComponent } private void on_items_changed (ListModel model, uint pos, uint removed, uint added) { + // Check removed > added to make sure we only close once when the last window is removed + // This avoids an inifinite loop since closing will sort the windows which also triggers this signal if (is_opened () && removed > added && model.get_n_items () == 0) { close (); } From f25da8fd9fd89c66a301a86a00dc1708d4847554 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Mon, 13 Oct 2025 16:31:39 +0200 Subject: [PATCH 12/13] Use Gtk Filter for custom filters --- src/Widgets/WindowOverview.vala | 7 ++++--- src/WindowListModel.vala | 19 +++++++------------ 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/Widgets/WindowOverview.vala b/src/Widgets/WindowOverview.vala index 3444d0fa7..a62a8d2ab 100644 --- a/src/Widgets/WindowOverview.vala +++ b/src/Widgets/WindowOverview.vala @@ -115,8 +115,8 @@ public class Gala.WindowOverview : ActorTarget, RootTarget, ActivatableComponent var geometry = display.get_monitor_geometry (i); var scale = display.get_monitor_scale (i); - var model = new WindowListModel (display, STACKING, true, i); - model.set_custom_filter (window_filter_func); + var custom_filter = new Gtk.CustomFilter (window_filter_func); + var model = new WindowListModel (display, STACKING, true, i, null, custom_filter); model.items_changed.connect (on_items_changed); window_clone_container = new WindowCloneContainer (wm, model, scale, true) { @@ -166,7 +166,8 @@ public class Gala.WindowOverview : ActorTarget, RootTarget, ActivatableComponent return true; } - private bool window_filter_func (Meta.Window window) { + private bool window_filter_func (Object obj) { + var window = (Meta.Window) obj; return window_ids == null || (window.get_id () in window_ids); } diff --git a/src/WindowListModel.vala b/src/WindowListModel.vala index 6d4f83d61..a4c55a343 100644 --- a/src/WindowListModel.vala +++ b/src/WindowListModel.vala @@ -16,8 +16,6 @@ public class Gala.WindowListModel : Object, ListModel { STACKING } - public delegate bool WindowFilter (Meta.Window window); - public Meta.Display display { get; construct; } public SortMode sort_mode { get; construct; } @@ -38,18 +36,20 @@ public class Gala.WindowListModel : Object, ListModel { */ public Meta.Workspace? workspace_filter { get; construct set; } - private Gee.ArrayList windows; + public Gtk.Filter? custom_filter { get; construct set; } - private WindowFilter? custom_filter = null; + private Gee.ArrayList windows; public WindowListModel ( Meta.Display display, SortMode sort_mode = NONE, bool normal_filter = false, int monitor_filter = -1, - Meta.Workspace? workspace_filter = null + Meta.Workspace? workspace_filter = null, + Gtk.Filter? custom_filter = null ) { Object ( display: display, sort_mode: sort_mode, normal_filter: normal_filter, - monitor_filter: monitor_filter, workspace_filter: workspace_filter + monitor_filter: monitor_filter, workspace_filter: workspace_filter, + custom_filter: custom_filter ); } @@ -68,11 +68,6 @@ public class Gala.WindowListModel : Object, ListModel { check_all (); } - public void set_custom_filter (WindowFilter? filter) { - custom_filter = filter; - check_all (); - } - private void on_window_created (Meta.Window window) { window.unmanaging.connect (on_window_unmanaging); InternalUtils.wait_for_window_actor (window, (actor) => check_window (actor.meta_window)); @@ -123,7 +118,7 @@ public class Gala.WindowListModel : Object, ListModel { } if (custom_filter != null) { - return custom_filter (window); + return custom_filter.match (window); } return true; From 041114b7bed8bc69fc3a58d85d91880e2219a69b Mon Sep 17 00:00:00 2001 From: Leo Date: Tue, 14 Oct 2025 03:16:16 +0900 Subject: [PATCH 13/13] window_filter_func: add requires --- src/Widgets/WindowOverview.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Widgets/WindowOverview.vala b/src/Widgets/WindowOverview.vala index a62a8d2ab..5f2f09e4e 100644 --- a/src/Widgets/WindowOverview.vala +++ b/src/Widgets/WindowOverview.vala @@ -166,7 +166,7 @@ public class Gala.WindowOverview : ActorTarget, RootTarget, ActivatableComponent return true; } - private bool window_filter_func (Object obj) { + private bool window_filter_func (Object obj) requires (obj is Meta.Window) { var window = (Meta.Window) obj; return window_ids == null || (window.get_id () in window_ids); }