diff --git a/lib/Gestures/Backends/TouchpadBackend.vala b/lib/Gestures/Backends/TouchpadBackend.vala index c1219cc9b..2eefeea93 100644 --- a/lib/Gestures/Backends/TouchpadBackend.vala +++ b/lib/Gestures/Backends/TouchpadBackend.vala @@ -74,6 +74,14 @@ private class Gala.TouchpadBackend : Object, GestureBackend { event.get_gesture_motion_delta_unaccelerated (out delta_x, out delta_y); if (state != ONGOING) { + foreach (var instance in instances) { + if (instance != this && (instance.group == NONE || instance.group != group) && instance.state == ONGOING) { + /* Another instance is already recognizing this gesture, make sure we don't steal it */ + state = IGNORED; + return Clutter.EVENT_PROPAGATE; + } + } + distance_x += delta_x; distance_y += delta_y; diff --git a/lib/Gestures/Gesture.vala b/lib/Gestures/Gesture.vala index 828975ac2..3f316a063 100644 --- a/lib/Gestures/Gesture.vala +++ b/lib/Gestures/Gesture.vala @@ -40,6 +40,7 @@ namespace Gala { SWITCH_WINDOWS, MULTITASKING_VIEW, ZOOM, + TOGGLE_MAXIMIZED, CUSTOM, N_ACTIONS; diff --git a/lib/Gestures/GestureSettings.vala b/lib/Gestures/GestureSettings.vala index a6a5c367c..162ebd66e 100644 --- a/lib/Gestures/GestureSettings.vala +++ b/lib/Gestures/GestureSettings.vala @@ -72,6 +72,11 @@ private class Gala.GestureSettings : Object { fingers == 4 && four_finger_swipe_up == "multitasking-view") { return MULTITASKING_VIEW; } + + if (fingers == 3 && three_finger_swipe_up == "toggle-maximized" || + fingers == 4 && four_finger_swipe_up == "toggle-maximized") { + return TOGGLE_MAXIMIZED; + } } break; diff --git a/src/Widgets/WindowMaximizer.vala b/src/Widgets/WindowMaximizer.vala new file mode 100644 index 000000000..664881d62 --- /dev/null +++ b/src/Widgets/WindowMaximizer.vala @@ -0,0 +1,94 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public class Gala.WindowMaximizer : ActorTarget, RootTarget { + public WindowManager wm { get; construct; } + + public Clutter.Actor? actor { get { return tile; } } + + private Clutter.Actor tile; + private GestureController controller; + + private Meta.Window? current_window; + + public WindowMaximizer (WindowManager wm) { + Object (wm: wm); + } + + construct { + tile = new Clutter.Actor () { + opacity = 200 + }; + Drawing.StyleManager.get_instance ().bind_property ("theme-accent-color", tile, "background-color", SYNC_CREATE); + + add_child (tile); + + controller = new GestureController (TOGGLE_MAXIMIZED); + controller.add_trigger (new GlobalTrigger (TOGGLE_MAXIMIZED, wm)); + add_gesture_controller (controller); + + } + + public override void start_progress (GestureAction action) requires (action == TOGGLE_MAXIMIZED) { + current_window = wm.get_display ().focus_window; + + if (current_window == null || !current_window.can_maximize ()) { + return; + } + + visible = true; + controller.progress = (double) current_window.maximized_horizontally; + + var workarea = wm.get_display ().get_workspace_manager ().get_active_workspace ().get_work_area_for_monitor (current_window.get_monitor ()); + var initial_rect = get_initial_rect (workarea); + + add_target (new PropertyTarget (TOGGLE_MAXIMIZED, tile, "x", typeof (float), (float) initial_rect.x, (float) workarea.x)); + add_target (new PropertyTarget (TOGGLE_MAXIMIZED, tile, "y", typeof (float), (float) initial_rect.y, (float) workarea.y)); + add_target (new PropertyTarget (TOGGLE_MAXIMIZED, tile, "width", typeof (float), (float) initial_rect.width, (float) workarea.width)); + add_target (new PropertyTarget (TOGGLE_MAXIMIZED, tile, "height", typeof (float), (float) initial_rect.height, (float) workarea.height)); + + var window_actor = (Meta.WindowActor) current_window.get_compositor_private (); + wm.window_group.set_child_above_sibling (this, window_actor); + } + + private Mtk.Rectangle get_initial_rect (Mtk.Rectangle workarea) requires (current_window != null) { + if (!current_window.maximized_horizontally) { + return current_window.get_frame_rect (); + } + + var unmaximized_geometry = WindowListener.get_default ().get_unmaximized_state_geometry (current_window); + if (unmaximized_geometry != null) { + return unmaximized_geometry.inner; + } + + return { workarea.x + (workarea.width / 2), workarea.y + (workarea.height / 2), 0, 0 }; + } + + public override void commit_progress (GestureAction action, double commit) requires (action == TOGGLE_MAXIMIZED) { + if (current_window == null) { + return; + } + + if (commit == 0 && current_window.maximized_horizontally) { +#if HAS_MUTTER49 + current_window.unmaximize (); +#else + current_window.unmaximize (BOTH); +#endif + } else if (commit == 1 && !current_window.maximized_horizontally) { +#if HAS_MUTTER49 + current_window.maximize (); +#else + current_window.maximize (BOTH); +#endif + } + + remove_all_targets (); + visible = false; + current_window = null; + } +} diff --git a/src/WindowManager.vala b/src/WindowManager.vala index 6a0ec38d4..97b1faf85 100644 --- a/src/WindowManager.vala +++ b/src/WindowManager.vala @@ -260,6 +260,7 @@ namespace Gala { background_group = new BackgroundContainer (display); ((BackgroundContainer)background_group).show_background_menu.connect (daemon_manager.show_background_menu); window_group.add_child (background_group); + window_group.add_child (new WindowMaximizer (this)); window_group.set_child_below_sibling (background_group, null); #if HAS_MUTTER48 diff --git a/src/meson.build b/src/meson.build index aa42ee15e..5ab294fbc 100644 --- a/src/meson.build +++ b/src/meson.build @@ -67,6 +67,7 @@ gala_bin_sources = files( 'Widgets/PointerLocator.vala', 'Widgets/SessionLocker.vala', 'Widgets/SelectionArea.vala', + 'Widgets/WindowMaximizer.vala', 'Widgets/WindowOverview.vala', 'Widgets/WindowSwitcher/WindowSwitcher.vala', 'Widgets/WindowSwitcher/WindowSwitcherIcon.vala',