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 dba31632c..4e4967ed5 100644 --- a/src/Widgets/MultitaskingView/WindowClone.vala +++ b/src/Widgets/MultitaskingView/WindowClone.vala @@ -440,8 +440,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 2f23d06da..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; } @@ -18,6 +17,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,78 +29,54 @@ 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) { + // 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); - 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 (); + // Don't reflow if only the sorting changed + if (to_remove.size () > 0 || added != removed) { + reflow (false); } } @@ -120,10 +96,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 (); } } @@ -142,34 +118,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 () { - 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. */ @@ -350,35 +298,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/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 dc78017d7..5f2f09e4e 100644 --- a/src/Widgets/WindowOverview.vala +++ b/src/Widgets/WindowOverview.vala @@ -20,6 +20,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); } @@ -67,10 +69,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) { @@ -104,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); @@ -123,7 +115,11 @@ 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 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) { padding_top = TOP_GAP, padding_left = BORDER, padding_right = BORDER, @@ -135,7 +131,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); } @@ -145,13 +140,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); @@ -178,58 +166,17 @@ 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 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); } - 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; + 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 (); } - - container.remove_window (window); } private void thumb_selected (Meta.Window window) { @@ -255,12 +202,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/WindowListModel.vala b/src/WindowListModel.vala new file mode 100644 index 000000000..a4c55a343 --- /dev/null +++ b/src/WindowListModel.vala @@ -0,0 +1,173 @@ +/* + * Copyright 2025 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * 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, + 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 Utils.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; } + + public Gtk.Filter? custom_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, + Gtk.Filter? custom_filter = null + ) { + Object ( + display: display, sort_mode: sort_mode, normal_filter: normal_filter, + monitor_filter: monitor_filter, workspace_filter: workspace_filter, + custom_filter: custom_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.unmanaging.connect (on_window_unmanaging); + InternalUtils.wait_for_window_actor (window, (actor) => check_window (actor.meta_window)); + } + + private void on_window_unmanaging (Meta.Window window) { + var pos = windows.index_of (window); + if (pos >= 0) { + windows.remove_at (pos); + items_changed (pos, 1, 0); + } + } + + 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; + } + + if (custom_filter != null) { + return custom_filter.match (window); + } + + return true; + } + + public void sort () { + if (sort_mode == STACKING) { + int i = 0; + foreach (var window in get_sorted_windows ()) { + windows[i++] = window; + } + + items_changed (0, windows.size, windows.size); + } + } + + public Object? get_item (uint position) { + return windows[(int) position]; + } + + public uint get_n_items () { + return windows.size; + } + + 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_other_workspaces_sorted); + sorted_windows.concat ((owned) windows_on_active_workspace_sorted); + + return sorted_windows; + } +} 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 724bff590..f3a35a6c8 100644 --- a/src/meson.build +++ b/src/meson.build @@ -19,6 +19,7 @@ gala_bin_sources = files( 'WindowAttentionTracker.vala', 'WindowDragProvider.vala', 'WindowListener.vala', + 'WindowListModel.vala', 'WindowManager.vala', 'WindowStateSaver.vala', 'WindowTracker.vala',