diff --git a/lib/Gestures/Gesture.vala b/lib/Gestures/Gesture.vala index 2e2a5bcde..7869ac333 100644 --- a/lib/Gestures/Gesture.vala +++ b/lib/Gestures/Gesture.vala @@ -41,6 +41,7 @@ namespace Gala { MULTITASKING_VIEW, ZOOM, CUSTOM, + CUSTOM_2, N_ACTIONS } diff --git a/lib/Gestures/WorkspaceHideTracker.vala b/lib/Gestures/WorkspaceHideTracker.vala new file mode 100644 index 000000000..dc72fe8b6 --- /dev/null +++ b/lib/Gestures/WorkspaceHideTracker.vala @@ -0,0 +1,134 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + */ + +public class Gala.WorkspaceHideTracker : Object, GestureTarget { + public signal double compute_progress (Meta.Workspace workspace); + public signal void switching_workspace_progress_updated (double new_progress); + public signal void window_state_changed_progress_updated (double new_progress); + + //we don't want to hold a strong reference to the actor because we might've been added to it which would form a reference cycle + private weak Clutter.Actor? _actor; + public Clutter.Actor? actor { get { return _actor; }} + public Meta.Display display { private get; construct; } + + private double switch_workspace_progress = 0.0; + private double[] workspace_hide_progress_cache = {}; + + public WorkspaceHideTracker (Meta.Display display, Clutter.Actor actor) { + Object (display: display); + _actor = actor; + } + + construct { + display.list_all_windows ().foreach (setup_window); + display.window_created.connect (setup_window); + + unowned var monitor_manager = display.get_context ().get_backend ().get_monitor_manager (); + monitor_manager.monitors_changed.connect (recalculate_all_workspaces); + + unowned var workspace_manager = display.get_workspace_manager (); + workspace_manager.workspace_added.connect (recalculate_all_workspaces); + workspace_manager.workspace_removed.connect (recalculate_all_workspaces); + + recalculate_all_workspaces (); + } + + private void setup_window (Meta.Window window) { + window.notify["window-type"].connect (on_window_type_changed); + + if (!Utils.get_window_is_normal (window)) { + return; + } + + if (window.on_all_workspaces) { + recalculate_all_workspaces (); + } else { + recalculate_workspace (window); + } + + window.position_changed.connect (recalculate_workspace); + window.size_changed.connect (recalculate_workspace); + window.workspace_changed.connect (recalculate_all_workspaces); + window.focused.connect (recalculate_workspace); + window.notify["on-all-workspaces"].connect (recalculate_all_workspaces); + window.notify["fullscreen"].connect (recalculate_workspace_pspec); + window.notify["minimized"].connect (recalculate_workspace_pspec); + window.notify["above"].connect (recalculate_workspace_pspec); + window.unmanaged.connect (recalculate_workspace); + } + + private void on_window_type_changed (Object obj, ParamSpec pspec) { + var window = (Meta.Window) obj; + + window.notify["window-type"].disconnect (on_window_type_changed); + window.position_changed.disconnect (recalculate_workspace); + window.size_changed.disconnect (recalculate_workspace); + window.workspace_changed.disconnect (recalculate_all_workspaces); + window.focused.disconnect (recalculate_workspace); + window.notify["on-all-workspaces"].disconnect (recalculate_all_workspaces); + window.notify["fullscreen"].disconnect (recalculate_workspace_pspec); + window.notify["minimized"].disconnect (recalculate_workspace_pspec); + window.notify["above"].disconnect (recalculate_workspace_pspec); + window.unmanaged.disconnect (recalculate_workspace); + + setup_window (window); + } + + public override void propagate (UpdateType update_type, GestureAction action, double progress) { + if (action != SWITCH_WORKSPACE || update_type == COMMIT) { + return; + } + + switch_workspace_progress = progress.abs (); + switching_workspace_progress_updated (get_hidden_progress ()); + } + + private double get_hidden_progress () { + var n_workspaces = workspace_hide_progress_cache.length; + + var left_workspace = int.max ((int) Math.floor (switch_workspace_progress), 0); + var right_workspace = int.min ((int) Math.ceil (switch_workspace_progress), n_workspaces - 1); + + var relative_progress = switch_workspace_progress - left_workspace; + + return ( + workspace_hide_progress_cache[left_workspace] * (1 - relative_progress) + + workspace_hide_progress_cache[right_workspace] * relative_progress + ); + } + + /** + * Trigger recalculation of all workspaces + */ + public void recalculate_all_workspaces () { + unowned var workspace_manager = display.get_workspace_manager (); + workspace_hide_progress_cache = new double[workspace_manager.n_workspaces]; + foreach (unowned var workspace in workspace_manager.get_workspaces ()) { + internal_recalculate_workspace (workspace, false); + } + + window_state_changed_progress_updated (get_hidden_progress ()); + } + + private void internal_recalculate_workspace (Meta.Workspace? workspace, bool send_signal) { + if (workspace == null || workspace.workspace_index >= workspace_hide_progress_cache.length) { + return; + } + + workspace_hide_progress_cache[workspace.workspace_index] = compute_progress (workspace); + + if (send_signal) { + window_state_changed_progress_updated (get_hidden_progress ()); + } + } + + private void recalculate_workspace (Meta.Window window) { + internal_recalculate_workspace (window.get_workspace (), true); + } + + private void recalculate_workspace_pspec (Object obj, ParamSpec pspec) { + internal_recalculate_workspace (((Meta.Window) obj).get_workspace (), true); + } +} diff --git a/lib/meson.build b/lib/meson.build index 058829832..94885b458 100644 --- a/lib/meson.build +++ b/lib/meson.build @@ -36,7 +36,8 @@ gala_lib_sources = files( 'Gestures/ScrollBackend.vala', 'Gestures/SpringTimeline.vala', 'Gestures/ToucheggBackend.vala', - 'Gestures/TouchpadBackend.vala' + 'Gestures/TouchpadBackend.vala', + 'Gestures/WorkspaceHideTracker.vala' ) + gala_common_enums gala_resources = gnome.compile_resources( diff --git a/src/ShellClients/HideTracker.vala b/src/ShellClients/HideTracker.vala index 7ab79b375..6fe97a604 100644 --- a/src/ShellClients/HideTracker.vala +++ b/src/ShellClients/HideTracker.vala @@ -1,5 +1,5 @@ /* - * Copyright 2024 elementary, Inc. (https://elementary.io) + * Copyright 2024-2025 elementary, Inc. (https://elementary.io) * SPDX-License-Identifier: GPL-3.0-or-later * * Authored by: Leonhard Kargl @@ -7,15 +7,13 @@ public class Gala.HideTracker : Object { private const int BARRIER_OFFSET = 50; // Allow hot corner trigger - private const int UPDATE_TIMEOUT = 200; private const int HIDE_DELAY = 500; public signal void hide (); public signal void show (); public Meta.Display display { get; construct; } - public unowned PanelWindow panel { get; construct; } - public Pantheon.Desktop.HideMode hide_mode { get; set; } + public unowned ShellWindow panel { get; construct; } private static GLib.Settings behavior_settings; @@ -23,19 +21,14 @@ public class Gala.HideTracker : Object { private bool hovered = false; - private bool overlap = false; - private bool focus_overlap = false; - private bool focus_maximized_overlap = false; - private bool fullscreen_overlap = false; - - private Meta.Window current_focus_window; + private uint num_transients = 0; + private bool has_transients { get { return num_transients > 0; } } private Barrier? barrier; private uint hide_timeout_id = 0; - private uint update_timeout_id = 0; - public HideTracker (Meta.Display display, PanelWindow panel) { + public HideTracker (Meta.Display display, ShellWindow panel) { Object (display: display, panel: panel); } @@ -49,22 +42,6 @@ public class Gala.HideTracker : Object { // access the panel which was already freed. To prevent that make sure we reset // the timeouts so that we get freed immediately reset_hide_timeout (); - reset_update_timeout (); - }); - - // Can't be local otherwise we get a memory leak :( - // See https://gitlab.gnome.org/GNOME/vala/-/issues/1548 - current_focus_window = display.focus_window; - track_focus_window (current_focus_window); - display.notify["focus-window"].connect (() => { - untrack_focus_window (current_focus_window); - current_focus_window = display.focus_window; - track_focus_window (current_focus_window); - }); - - display.window_created.connect ((window) => { - schedule_update (); - window.unmanaged.connect (schedule_update); }); #if HAS_MUTTER48 @@ -77,11 +54,23 @@ public class Gala.HideTracker : Object { if (hovered != has_pointer) { hovered = has_pointer; - schedule_update (); + check_trigger_conditions (); } }); - display.get_workspace_manager ().active_workspace_changed.connect (schedule_update); + display.window_created.connect ((new_window) => { + InternalUtils.wait_for_window_actor (new_window, (new_window_actor) => { + if (panel.window.is_ancestor_of_transient (new_window_actor.meta_window)) { + num_transients++; + check_trigger_conditions (); + + new_window_actor.meta_window.unmanaged.connect (() => { + num_transients = uint.max (num_transients - 1, 0); + check_trigger_conditions (); + }); + } + }); + }); pan_action = new Clutter.PanAction () { n_touch_points = 1, @@ -101,146 +90,21 @@ public class Gala.HideTracker : Object { var monitor_manager = display.get_context ().get_backend ().get_monitor_manager (); monitor_manager.monitors_changed.connect (() => { setup_barrier (); //Make sure barriers are still on the primary monitor - schedule_update (); }); setup_barrier (); } - - private void track_focus_window (Meta.Window? window) { - if (window == null) { - return; - } - - window.position_changed.connect (schedule_update); - window.size_changed.connect (schedule_update); - schedule_update (); - } - - private void untrack_focus_window (Meta.Window? window) { - if (window == null) { - return; - } - - window.position_changed.disconnect (schedule_update); - window.size_changed.disconnect (schedule_update); - schedule_update (); - } - - public void schedule_update () { - if (update_timeout_id != 0) { - return; - } - - update_timeout_id = Timeout.add (UPDATE_TIMEOUT, () => { - update_overlap (); - update_timeout_id = 0; - return Source.REMOVE; - }); - } - - private void reset_update_timeout () { - if (update_timeout_id != 0) { - Source.remove (update_timeout_id); - update_timeout_id = 0; - } - } - - public void update_overlap () { - overlap = false; - focus_overlap = false; - focus_maximized_overlap = false; - fullscreen_overlap = display.get_monitor_in_fullscreen (panel.window.get_monitor ()); - - unowned var active_workspace = display.get_workspace_manager ().get_active_workspace (); - - Meta.Window? normal_mru_window, any_mru_window; - normal_mru_window = InternalUtils.get_mru_window (active_workspace, out any_mru_window); - - foreach (var window in active_workspace.list_windows ()) { - if (window == panel.window) { - continue; - } - - if (window.minimized) { - continue; - } - - var type = window.get_window_type (); - if (type == DESKTOP || type == DOCK || type == MENU || type == SPLASHSCREEN) { - continue; - } - - if (!panel.get_custom_window_rect ().overlap (window.get_frame_rect ())) { - continue; - } - - overlap = true; - - if (window != normal_mru_window && window != any_mru_window) { - continue; - } - - focus_overlap = true; - focus_maximized_overlap = window.maximized_vertically; - } - - update_hidden (); - } - - private void update_hidden () { - switch (hide_mode) { - case MAXIMIZED_FOCUS_WINDOW: - toggle_display (focus_maximized_overlap); - break; - - case OVERLAPPING_FOCUS_WINDOW: - toggle_display (focus_overlap); - break; - - case OVERLAPPING_WINDOW: - toggle_display (overlap); - break; - - case ALWAYS: - toggle_display (true); - break; - - case NEVER: - toggle_display (fullscreen_overlap); - break; - } - } - - private void toggle_display (bool should_hide) { - hovered = panel.window.has_pointer (); - - // Showing panels in fullscreen is broken in X11 - if (should_hide && !hovered && !panel.window.has_focus () || InternalUtils.get_x11_in_fullscreen (display)) { - trigger_hide (); - } else { + private void check_trigger_conditions () { + if (hovered || has_transients) { trigger_show (); + } else { + trigger_hide (); } } private void trigger_hide () { - if (hide_timeout_id != 0) { - return; - } - - // Don't hide if we have transients, e.g. an open popover, dialog, etc. - var has_transients = false; - panel.window.foreach_transient (() => { - has_transients = true; - return false; - }); - - if (has_transients) { - reset_hide_timeout (); - - return; - } + reset_hide_timeout (); hide_timeout_id = Timeout.add_once (HIDE_DELAY, () => { hide (); @@ -349,9 +213,10 @@ public class Gala.HideTracker : Object { return; } - if (hide_mode != NEVER || behavior_settings.get_boolean ("enable-hotcorners-in-fullscreen")) { + if (!display.get_monitor_in_fullscreen (panel.window.get_monitor ()) || + behavior_settings.get_boolean ("enable-hotcorners-in-fullscreen") + ) { trigger_show (); - schedule_update (); } } } diff --git a/src/ShellClients/PanelWindow.vala b/src/ShellClients/PanelWindow.vala index 4235ea05b..3d30ef9fa 100644 --- a/src/ShellClients/PanelWindow.vala +++ b/src/ShellClients/PanelWindow.vala @@ -1,5 +1,5 @@ /* - * Copyright 2024 elementary, Inc. (https://elementary.io) + * Copyright 2024-2025 elementary, Inc. (https://elementary.io) * SPDX-License-Identifier: GPL-3.0-or-later * * Authored by: Leonhard Kargl @@ -37,12 +37,6 @@ public class Gala.PanelWindow : ShellWindow, RootTarget { } construct { - window.unmanaging.connect (() => { - if (window_struts.remove (window)) { - update_struts (); - } - }); - notify["anchor"].connect (() => position = Position.from_anchor (anchor)); unowned var workspace_manager = window.display.get_workspace_manager (); @@ -53,13 +47,6 @@ public class Gala.PanelWindow : ShellWindow, RootTarget { window.position_changed.connect (update_strut); notify["width"].connect (update_strut); notify["height"].connect (update_strut); - - gesture_controller = new GestureController (CUSTOM, wm); - add_gesture_controller (gesture_controller); - - hide_tracker = new HideTracker (wm.get_display (), this); - hide_tracker.hide.connect (hide); - hide_tracker.show.connect (show); } public void request_visible_in_multitasking_view () { diff --git a/src/ShellClients/ShellWindow.vala b/src/ShellClients/ShellWindow.vala index 5d5d7d2b1..269f66149 100644 --- a/src/ShellClients/ShellWindow.vala +++ b/src/ShellClients/ShellWindow.vala @@ -14,6 +14,11 @@ public class Gala.ShellWindow : PositionedWindow, GestureTarget { private PropertyTarget property_target; + private GestureController custom_gesture_controller; + private GestureController workspace_gesture_controller; + private HideTracker hide_tracker; + private WorkspaceHideTracker workspace_hide_tracker; + public ShellWindow (Meta.Window window, Position position, Variant? position_data = null) { base (window, position, position_data); } @@ -21,11 +26,34 @@ public class Gala.ShellWindow : PositionedWindow, GestureTarget { construct { window_actor = (Meta.WindowActor) window.get_compositor_private (); + custom_gesture_controller = new GestureController (CUSTOM, wm) { + progress = 1.0 + }; + add_gesture_controller (custom_gesture_controller); + + workspace_gesture_controller = new GestureController (CUSTOM_2, wm); + add_gesture_controller (workspace_gesture_controller); + + hide_tracker = new HideTracker (window.display, this); + hide_tracker.hide.connect (() => custom_gesture_controller.goto (1)); + hide_tracker.show.connect (() => custom_gesture_controller.goto (0)); + + workspace_hide_tracker = new WorkspaceHideTracker (window.display, actor); + workspace_hide_tracker.compute_progress.connect (update_overlap); + workspace_hide_tracker.switching_workspace_progress_updated.connect ((value) => workspace_gesture_controller.progress = value); + workspace_hide_tracker.window_state_changed_progress_updated.connect (workspace_gesture_controller.goto); + window_actor.notify["width"].connect (update_clip); window_actor.notify["height"].connect (update_clip); window_actor.notify["translation-y"].connect (update_clip); notify["position"].connect (update_clip); + window.unmanaging.connect (() => { + if (window_struts.remove (window)) { + update_struts (); + } + }); + window.size_changed.connect (update_target); notify["position"].connect (update_target); update_target (); @@ -39,9 +67,13 @@ public class Gala.ShellWindow : PositionedWindow, GestureTarget { calculate_value (false), calculate_value (true) ); + + workspace_hide_tracker.recalculate_all_workspaces (); } public override void propagate (UpdateType update_type, GestureAction action, double progress) { + workspace_hide_tracker.propagate (update_type, action, progress); + switch (update_type) { case START: animations_ongoing++; @@ -163,4 +195,117 @@ public class Gala.ShellWindow : PositionedWindow, GestureTarget { window_actor.remove_clip (); } } + + private double update_overlap (Meta.Workspace workspace) { + var overlap = false; + var focus_overlap = false; + var focus_maximized_overlap = false; + var fullscreen_overlap = window.display.get_monitor_in_fullscreen (window.get_monitor ()); + + Meta.Window? normal_mru_window, any_mru_window; + normal_mru_window = InternalUtils.get_mru_window (workspace, out any_mru_window); + + foreach (var window in workspace.list_windows ()) { + if (window == this.window) { + continue; + } + + if (window.minimized) { + continue; + } + + var type = window.get_window_type (); + if (type == DESKTOP || type == DOCK || type == MENU || type == SPLASHSCREEN) { + continue; + } + + if (!get_custom_window_rect ().overlap (window.get_frame_rect ())) { + continue; + } + + overlap = true; + + if (window != normal_mru_window && window != any_mru_window) { + continue; + } + + focus_overlap = true; + focus_maximized_overlap = window.maximized_vertically; + } + + switch (hide_mode) { + case MAXIMIZED_FOCUS_WINDOW: + return focus_maximized_overlap ? 1.0 : 0.0; + + case OVERLAPPING_FOCUS_WINDOW: + return focus_overlap ? 1.0 : 0.0; + + case OVERLAPPING_WINDOW: + return overlap ? 1.0 : 0.0; + + case ALWAYS: + return 1.0; + + case NEVER: + return fullscreen_overlap ? 1.0 : 0.0; + } + + return 0.0; + } + + private void make_exclusive () { + update_strut (); + } + + internal void update_strut () { + if (hide_mode != NEVER) { + return; + } + + var rect = get_custom_window_rect (); + + Meta.Strut strut = { + rect, + side_from_anchor (anchor) + }; + + window_struts[window] = strut; + + update_struts (); + } + + internal void update_struts () { + var list = new SList (); + + foreach (var window_strut in window_struts.get_values ()) { + list.append (window_strut); + } + + foreach (var workspace in wm.get_display ().get_workspace_manager ().get_workspaces ()) { + workspace.set_builtin_struts (list); + } + } + + private void unmake_exclusive () { + if (window in window_struts) { + window_struts.remove (window); + update_struts (); + } + } + + private Meta.Side side_from_anchor (Pantheon.Desktop.Anchor anchor) { + switch (anchor) { + case BOTTOM: + return BOTTOM; + + case LEFT: + return LEFT; + + case RIGHT: + return RIGHT; + + default: + return TOP; + } + } }