diff --git a/data/gala.gschema.xml b/data/gala.gschema.xml index 7cea18fd0..ce5949f28 100644 --- a/data/gala.gschema.xml +++ b/data/gala.gschema.xml @@ -271,4 +271,23 @@ + + + + Tab']]]> + Switch between windows. + + + Tab']]]> + Switch between windows backwards. + + + grave']]]> + Switch between windows of the current application. + + + grave']]]> + Switch between windows of the current application backwards. + + diff --git a/lib/Plugin.vala b/lib/Plugin.vala index 4bd011f0e..75f57349d 100644 --- a/lib/Plugin.vala +++ b/lib/Plugin.vala @@ -18,7 +18,6 @@ namespace Gala { public enum PluginFunction { ADDITION, - WINDOW_SWITCHER, WORKSPACE_VIEW, WINDOW_OVERVIEW } diff --git a/meson.build b/meson.build index 3bfa153df..91b6e1e13 100644 --- a/meson.build +++ b/meson.build @@ -189,6 +189,7 @@ if get_option('documentation') subdir('docs') endif subdir('po') +subdir('windowSwitcher') vapigen = find_program('vapigen', required: false) if vapigen.found() diff --git a/src/DBus.vala b/src/DBus.vala index 1034883b3..8fedcc9f1 100644 --- a/src/DBus.vala +++ b/src/DBus.vala @@ -30,6 +30,15 @@ public class Gala.DBus { () => {}, () => warning ("Could not acquire name\n") ); + Bus.own_name (BusType.SESSION, "io.elementary.desktop.wm", BusNameOwnerFlags.NONE, + (connection) => { + try { + connection.register_object ("/io/elementary/desktop/wm", new DBusGestureProvider ()); + } catch (Error e) { warning (e.message); } + }, + () => {}, + () => warning ("Could not acquire name") ); + Bus.own_name (BusType.SESSION, "org.gnome.Shell", BusNameOwnerFlags.NONE, (connection) => { try { diff --git a/src/Gestures/DBusGestureProvider.vala b/src/Gestures/DBusGestureProvider.vala new file mode 100644 index 000000000..d94023fa1 --- /dev/null +++ b/src/Gestures/DBusGestureProvider.vala @@ -0,0 +1,19 @@ +[DBus (name = "io.elementary.desktop.wm.GestureProvider")] +public class Gala.DBusGestureProvider : Object { + public signal void on_gesture_detected (Gesture gesture); + public signal void on_begin (double percentage); + public signal void on_update (double percentage); + public signal void on_end (double percentage, bool cancel_action, int calculated_duration); + + private GestureTracker gesture_tracker; + + construct { + gesture_tracker = new GestureTracker (0, 0); + gesture_tracker.enable_touchpad (); + + gesture_tracker.on_gesture_detected.connect ((gesture) => on_gesture_detected (gesture)); + gesture_tracker.on_begin.connect ((percentage) => on_begin (percentage)); + gesture_tracker.on_update.connect ((percentage) => on_update (percentage)); + gesture_tracker.on_end.connect ((percentage, cancel_action, calculated_duration) => on_end (percentage, cancel_action, calculated_duration)); + } +} diff --git a/src/Gestures/Gesture.vala b/src/Gestures/Gesture.vala index 9bccedaf7..dd7a59f31 100644 --- a/src/Gestures/Gesture.vala +++ b/src/Gestures/Gesture.vala @@ -34,7 +34,7 @@ namespace Gala { OUT = 6, } - public class Gesture { + public struct Gesture { public Clutter.EventType type; public GestureDirection direction; public int fingers; diff --git a/src/PluginManager.vala b/src/PluginManager.vala index 0af190e5b..b65a93b8d 100644 --- a/src/PluginManager.vala +++ b/src/PluginManager.vala @@ -36,7 +36,6 @@ namespace Gala { return _regions; } - public string? window_switcher_provider { get; private set; default = null; } public string? window_overview_provider { get; private set; default = null; } public string? workspace_view_provider { get; private set; default = null; } @@ -155,13 +154,6 @@ namespace Gala { } window_overview_provider = name; return true; - case PluginFunction.WINDOW_SWITCHER: - if (window_switcher_provider != null) { - warning (message, window_switcher_provider, name, "window switcher"); - return false; - } - window_switcher_provider = name; - return true; default: break; } diff --git a/src/Widgets/WindowSwitcher/WindowSwitcher.vala b/src/Widgets/WindowSwitcher/WindowSwitcher.vala deleted file mode 100644 index 4cf6d6890..000000000 --- a/src/Widgets/WindowSwitcher/WindowSwitcher.vala +++ /dev/null @@ -1,563 +0,0 @@ -/* - * Copyright 2021 Aral Balkan - * Copyright 2020 Mark Story - * Copyright 2017 Popye - * Copyright 2014 Tom Beckmann - * Copyright 2023 elementary, Inc. - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -public class Gala.WindowSwitcher : CanvasActor { - public const int ICON_SIZE = 64; - public const int WRAPPER_PADDING = 12; - - private const string CAPTION_FONT_NAME = "Inter"; - private const int MIN_OFFSET = 64; - private const int ANIMATION_DURATION = 200; - // https://github.com/elementary/gala/issues/1317#issuecomment-982484415 - private const int GESTURE_RANGE_LIMIT = 10; - - public Gala.WindowManager? wm { get; construct; } - public GestureTracker gesture_tracker { get; construct; } - public bool opened { get; private set; default = false; } - - private bool handling_gesture = false; - private int modifier_mask; - private Gala.ModalProxy modal_proxy = null; - private Drawing.StyleManager style_manager; - private Clutter.Actor container; - private Clutter.Text caption; - private ShadowEffect shadow_effect; - - private WindowSwitcherIcon? _current_icon = null; - private WindowSwitcherIcon? current_icon { - get { - return _current_icon; - } - set { - if (_current_icon != null) { - _current_icon.selected = false; - } - - _current_icon = value; - if (_current_icon != null) { - _current_icon.selected = true; - _current_icon.grab_key_focus (); - } - - update_caption_text (); - } - } - - private float scaling_factor = 1.0f; - - public WindowSwitcher (Gala.WindowManager wm, GestureTracker gesture_tracker) { - Object ( - wm: wm, - gesture_tracker: gesture_tracker - ); - } - - construct { - style_manager = Drawing.StyleManager.get_instance (); - - container = new Clutter.Actor () { - reactive = true, -#if HAS_MUTTER46 - layout_manager = new Clutter.FlowLayout (Clutter.Orientation.HORIZONTAL) -#else - layout_manager = new Clutter.FlowLayout (Clutter.FlowOrientation.HORIZONTAL) -#endif - }; - - get_accessible ().accessible_name = _("Window switcher"); - container.get_accessible ().accessible_role = LIST; - - caption = new Clutter.Text () { - font_name = CAPTION_FONT_NAME, - ellipsize = END, - line_alignment = CENTER - }; - - add_child (container); - add_child (caption); - - reactive = true; - visible = false; - opacity = 0; - layout_manager = new Clutter.BoxLayout () { - orientation = VERTICAL - }; - - shadow_effect = new ShadowEffect ("window-switcher") { - border_radius = InternalUtils.scale_to_int (9, scaling_factor), - shadow_opacity = 100 - }; - add_effect (shadow_effect); - - scale (); - - container.button_release_event.connect (container_mouse_release); - - // Redraw the components if the colour scheme changes. - style_manager.notify["prefers-color-scheme"].connect (content.invalidate); - - unowned var monitor_manager = wm.get_display ().get_context ().get_backend ().get_monitor_manager (); - monitor_manager.monitors_changed.connect (scale); - - notify["opacity"].connect (() => visible = opacity != 0); - } - - private void scale () { - scaling_factor = wm.get_display ().get_monitor_scale (wm.get_display ().get_current_monitor ()); - - shadow_effect.scale_factor = scaling_factor; - - var margin = InternalUtils.scale_to_int (WRAPPER_PADDING, scaling_factor); - - container.margin_left = margin; - container.margin_right = margin; - container.margin_bottom = margin; - container.margin_top = margin; - - caption.margin_left = margin; - caption.margin_right = margin; - caption.margin_bottom = margin; - } - - protected override void get_preferred_width (float for_height, out float min_width, out float natural_width) { - min_width = 0; - - float preferred_nat_width; - base.get_preferred_width (for_height, null, out preferred_nat_width); - - unowned var display = wm.get_display (); - var monitor = display.get_current_monitor (); - var geom = display.get_monitor_geometry (monitor); - - float container_nat_width; - container.get_preferred_size (null, null, out container_nat_width, null); - - var max_width = float.min ( - geom.width - InternalUtils.scale_to_int (MIN_OFFSET, scaling_factor) * 2, //Don't overflow the monitor - container_nat_width //Ellipsize the label if it's longer than the icons - ); - - natural_width = float.min (max_width, preferred_nat_width); - } - - protected override void draw (Cairo.Context ctx, int width, int height) { - var background_color = Drawing.Color.LIGHT_BACKGROUND; - var border_color = Drawing.Color.LIGHT_BORDER; - var caption_color = "#2e2e31"; - var highlight_color = Drawing.Color.LIGHT_HIGHLIGHT; - - if (style_manager.prefers_color_scheme == Drawing.StyleManager.ColorScheme.DARK) { - background_color = Drawing.Color.DARK_BACKGROUND; - border_color = Drawing.Color.DARK_BORDER; - caption_color = "#fafafa"; - highlight_color = Drawing.Color.DARK_HIGHLIGHT; - } - - var stroke_width = scaling_factor; - - caption.color = Clutter.Color.from_string (caption_color); - - ctx.save (); - ctx.set_operator (Cairo.Operator.CLEAR); - ctx.paint (); - ctx.clip (); - ctx.reset_clip (); - - ctx.set_operator (Cairo.Operator.SOURCE); - - // Offset by 0.5 so cairo draws a stroke on real pixels - Drawing.Utilities.cairo_rounded_rectangle ( - ctx, 0.5, 0.5, - width - stroke_width, - height - stroke_width, - InternalUtils.scale_to_int (9, scaling_factor) - ); - - ctx.set_source_rgba ( - background_color.red, - background_color.green, - background_color.blue, - background_color.alpha - ); - ctx.fill_preserve (); - - ctx.set_line_width (stroke_width); - ctx.set_source_rgba ( - border_color.red, - border_color.green, - border_color.blue, - border_color.alpha - ); - ctx.stroke (); - ctx.restore (); - - // Offset by 0.5 so cairo draws a stroke on real pixels - Drawing.Utilities.cairo_rounded_rectangle ( - ctx, stroke_width + 0.5, stroke_width + 0.5, - width - stroke_width * 2 - 1, - height - stroke_width * 2 - 1, - InternalUtils.scale_to_int (8, scaling_factor) - ); - - ctx.set_line_width (stroke_width); - ctx.set_source_rgba ( - highlight_color.red, - highlight_color.green, - highlight_color.blue, - highlight_color.alpha - ); - ctx.stroke (); - ctx.restore (); - } - - [CCode (instance_pos = -1)] - public void handle_switch_windows ( - Meta.Display display, Meta.Window? window, - Clutter.KeyEvent event, Meta.KeyBinding binding - ) { - if (handling_gesture) { - return; - } - - var workspace = display.get_workspace_manager ().get_active_workspace (); - - // copied from gnome-shell, finds the primary modifier in the mask - var mask = binding.get_mask (); - if (mask == 0) { - modifier_mask = 0; - } else { - modifier_mask = 1; - while (mask > 1) { - mask >>= 1; - modifier_mask <<= 1; - } - } - - if (!opened) { - bool windows_exist; - if (binding.get_name ().has_prefix ("switch-group")) { - windows_exist = collect_current_windows (display, workspace); - } else { - windows_exist = collect_all_windows (display, workspace); - } - - if (!windows_exist) { - return; - } - - open_switcher (); - } - - var binding_name = binding.get_name (); - var backward = binding_name.has_suffix ("-backward"); - - next_window (backward); - } - - public void handle_gesture (GestureDirection direction) { - handling_gesture = true; - - unowned var display = wm.get_display (); - unowned var workspace_manager = display.get_workspace_manager (); - unowned var active_workspace = workspace_manager.get_active_workspace (); - - var windows_exist = collect_all_windows (display, active_workspace); - if (!windows_exist) { - return; - } - open_switcher (); - - // if direction == LEFT we need to move to the end of the list first, thats why last_window_index is set to -1 - var last_window_index = direction == RIGHT ? 0 : -1; - GestureTracker.OnUpdate on_animation_update = (percentage) => { - var window_index = GestureTracker.animation_value (0, GESTURE_RANGE_LIMIT, percentage, true); - - if (window_index >= container.get_n_children ()) { - return; - } - - if (window_index > last_window_index) { - while (last_window_index < window_index) { - next_window (direction == LEFT); - last_window_index++; - } - } else if (window_index < last_window_index) { - while (last_window_index > window_index) { - next_window (direction == RIGHT); - last_window_index--; - } - } - }; - - GestureTracker.OnEnd on_animation_end = (percentage, cancel_action, calculated_duration) => { - handling_gesture = false; - close_switcher (wm.get_display ().get_current_time ()); - }; - - gesture_tracker.connect_handlers (null, (owned) on_animation_update, (owned) on_animation_end); - } - - private bool collect_all_windows (Meta.Display display, Meta.Workspace? workspace) { - var windows = display.get_tab_list (Meta.TabList.NORMAL, workspace); - if (windows == null) { - return false; - } - - unowned var current_window = display.get_tab_current (Meta.TabList.NORMAL, workspace); - if (current_window == null) { - current_icon = null; - } - - container.remove_all_children (); - - foreach (unowned var window in windows) { - var icon = new WindowSwitcherIcon (window, ICON_SIZE, scaling_factor); - if (window == current_window) { - current_icon = icon; - } - - add_icon (icon); - } - - return true; - } - - private bool collect_current_windows (Meta.Display display, Meta.Workspace? workspace) { - var windows = display.get_tab_list (Meta.TabList.NORMAL, workspace); - if (windows == null) { - return false; - } - - unowned var current_window = display.get_tab_current (Meta.TabList.NORMAL, workspace); - if (current_window == null) { - current_icon = null; - return false; - } - - container.remove_all_children (); - - unowned var window_tracker = ((WindowManagerGala) wm).window_tracker; - var app = window_tracker.get_app_for_window (current_window); - foreach (unowned var window in windows) { - if (window_tracker.get_app_for_window (window) == app) { - var icon = new WindowSwitcherIcon (window, ICON_SIZE, scaling_factor); - if (window == current_window) { - current_icon = icon; - } - - add_icon (icon); - } - } - - return true; - } - - private void add_icon (WindowSwitcherIcon icon) { - container.add_child (icon); - icon.get_accessible ().accessible_parent = container.get_accessible (); - - icon.motion_event.connect ((_icon, event) => { - if (current_icon != _icon && !handling_gesture) { - current_icon = (WindowSwitcherIcon) _icon; - } - - return Clutter.EVENT_PROPAGATE; - }); - } - - private void open_switcher () { - if (container.get_n_children () == 0) { - Clutter.get_default_backend ().get_default_seat ().bell_notify (); - return; - } - - if (opened) { - return; - } - - //Although we are setting visible via the opacity notify handler anyway - //we have to set it here manually otherwise the size gotten via get_preferred_size is wrong - visible = true; - - float width, height; - get_preferred_size (null, null, out width, out height); - - unowned var display = wm.get_display (); - var monitor = display.get_current_monitor (); - var geom = display.get_monitor_geometry (monitor); - - set_position ( - (int) (geom.x + (geom.width - width) / 2), - (int) (geom.y + (geom.height - height) / 2) - ); - - toggle_display (true); - } - - private void toggle_display (bool show) { - if (opened == show) { - return; - } - - opened = show; - if (show) { - push_modal (); - } else { - wm.pop_modal (modal_proxy); - get_stage ().set_key_focus (null); - } - - save_easing_state (); - set_easing_duration (wm.enable_animations ? ANIMATION_DURATION : 0); - opacity = show ? 255 : 0; - restore_easing_state (); - } - - private void push_modal () { - modal_proxy = wm.push_modal (this); - modal_proxy.set_keybinding_filter ((binding) => { - var action = Meta.Prefs.get_keybinding_action (binding.get_name ()); - - switch (action) { - case Meta.KeyBindingAction.NONE: - case Meta.KeyBindingAction.LOCATE_POINTER_KEY: - case Meta.KeyBindingAction.SWITCH_APPLICATIONS: - case Meta.KeyBindingAction.SWITCH_APPLICATIONS_BACKWARD: - case Meta.KeyBindingAction.SWITCH_WINDOWS: - case Meta.KeyBindingAction.SWITCH_WINDOWS_BACKWARD: - case Meta.KeyBindingAction.SWITCH_GROUP: - case Meta.KeyBindingAction.SWITCH_GROUP_BACKWARD: - return false; - default: - break; - } - - return true; - }); - - } - - private void close_switcher (uint32 time, bool cancel = false) { - if (!opened) { - return; - } - - var window = current_icon.window; - if (window == null) { - return; - } - - if (!cancel) { - unowned var workspace = window.get_workspace (); - if (workspace != wm.get_display ().get_workspace_manager ().get_active_workspace ()) { - workspace.activate_with_focus (window, time); - } else { - window.activate (time); - } - } - - toggle_display (false); - } - - private void next_window (bool backward) { - Clutter.Actor actor; - - if (container.get_n_children () == 1 && current_icon != null) { - Clutter.get_default_backend ().get_default_seat ().bell_notify (); - return; - } - - if (current_icon == null) { - actor = container.get_first_child (); - } else if (!backward) { - actor = current_icon.get_next_sibling (); - if (actor == null) { - actor = container.get_first_child (); - } - } else { - actor = current_icon.get_previous_sibling (); - if (actor == null) { - actor = container.get_last_child (); - } - } - - current_icon = (WindowSwitcherIcon) actor; - } - - private void update_caption_text () { - var current_window = current_icon != null ? current_icon.window : null; - var current_caption = current_window != null ? current_window.title : "n/a"; - caption.set_text (current_caption); - } - - public override void key_focus_out () { - if (!handling_gesture) { - close_switcher (wm.get_display ().get_current_time ()); - } - } - -#if HAS_MUTTER45 - private bool container_mouse_release (Clutter.Event event) { -#else - private bool container_mouse_release (Clutter.ButtonEvent event) { -#endif - if (opened && event.get_button () == Clutter.Button.PRIMARY && !handling_gesture) { - close_switcher (event.get_time ()); - } - - return true; - } - -#if HAS_MUTTER45 - public override bool key_release_event (Clutter.Event event) { -#else - public override bool key_release_event (Clutter.KeyEvent event) { -#endif - if ((get_current_modifiers () & modifier_mask) == 0 && !handling_gesture) { - close_switcher (event.get_time ()); - } - - return Clutter.EVENT_PROPAGATE; - } - -#if HAS_MUTTER45 - public override bool key_press_event (Clutter.Event event) { -#else - public override bool key_press_event (Clutter.KeyEvent event) { -#endif - switch (event.get_key_symbol ()) { - case Clutter.Key.Right: - if (!handling_gesture) { - next_window (false); - } - return Clutter.EVENT_STOP; - case Clutter.Key.Left: - if (!handling_gesture) { - next_window (true); - } - return Clutter.EVENT_STOP; - case Clutter.Key.Escape: - close_switcher (event.get_time (), true); - return Clutter.EVENT_PROPAGATE; - case Clutter.Key.Return: - close_switcher (event.get_time (), false); - return Clutter.EVENT_PROPAGATE; - } - - return Clutter.EVENT_PROPAGATE; - } - - - private inline Clutter.ModifierType get_current_modifiers () { - Clutter.ModifierType modifiers; - wm.get_display ().get_cursor_tracker ().get_pointer (null, out modifiers); - - return modifiers & Clutter.ModifierType.MODIFIER_MASK; - } -} diff --git a/src/Widgets/WindowSwitcher/WindowSwitcherIcon.vala b/src/Widgets/WindowSwitcher/WindowSwitcherIcon.vala deleted file mode 100644 index c85a12cda..000000000 --- a/src/Widgets/WindowSwitcher/WindowSwitcherIcon.vala +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2023 elementary, Inc. - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -public class Gala.WindowSwitcherIcon : CanvasActor { - private const int WRAPPER_BORDER_RADIUS = 3; - - public Meta.Window window { get; construct; } - - private WindowIcon icon; - - private bool _selected = false; - public bool selected { - get { - return _selected; - } - set { - _selected = value; - content.invalidate (); - } - } - - private float _scale_factor = 1.0f; - public float scale_factor { - get { - return _scale_factor; - } - set { - _scale_factor = value; - - update_size (); - } - } - - public WindowSwitcherIcon (Meta.Window window, int icon_size, float scale_factor) { - Object (window: window); - - icon = new WindowIcon (window, InternalUtils.scale_to_int (icon_size, scale_factor)); - icon.add_constraint (new Clutter.AlignConstraint (this, Clutter.AlignAxis.BOTH, 0.5f)); - add_child (icon); - - get_accessible ().accessible_name = window.title; - get_accessible ().accessible_role = LIST_ITEM; - get_accessible ().notify_state_change (Atk.StateType.FOCUSABLE, true); - - reactive = true; - - this.scale_factor = scale_factor; - } - - private void update_size () { - var indicator_size = InternalUtils.scale_to_int ( - (WindowSwitcher.ICON_SIZE + WindowSwitcher.WRAPPER_PADDING * 2), - scale_factor - ); - set_size (indicator_size, indicator_size); - } - - protected override void draw (Cairo.Context ctx, int width, int height) { - ctx.save (); - ctx.set_operator (Cairo.Operator.CLEAR); - ctx.paint (); - ctx.clip (); - ctx.reset_clip (); - - if (selected) { - // draw rect - var rgba = Drawing.StyleManager.get_instance ().theme_accent_color; - ctx.set_source_rgba ( - rgba.red, - rgba.green, - rgba.blue, - rgba.alpha - ); - var rect_radius = InternalUtils.scale_to_int (WRAPPER_BORDER_RADIUS, scale_factor); - Drawing.Utilities.cairo_rounded_rectangle (ctx, 0, 0, width, height, rect_radius); - ctx.set_operator (Cairo.Operator.SOURCE); - ctx.fill (); - - ctx.restore (); - } - - get_accessible ().notify_state_change (Atk.StateType.SELECTED, selected); - get_accessible ().notify_state_change (Atk.StateType.FOCUSED, selected); - } -} diff --git a/src/WindowManager.vala b/src/WindowManager.vala index 8d1122132..96c7c00da 100644 --- a/src/WindowManager.vala +++ b/src/WindowManager.vala @@ -62,8 +62,6 @@ namespace Gala { private Meta.PluginInfo info; - private WindowSwitcher? window_switcher = null; - public ActivatableComponent? window_overview { get; private set; } public ScreenSaverManager? screensaver { get; private set; } @@ -325,18 +323,6 @@ namespace Gala { workspace_view.open (); }); - if (plugin_manager.window_switcher_provider == null) { - window_switcher = new WindowSwitcher (this, gesture_tracker); - ui_group.add_child (window_switcher); - - Meta.KeyBinding.set_custom_handler ("switch-applications", (Meta.KeyHandlerFunc) window_switcher.handle_switch_windows); - Meta.KeyBinding.set_custom_handler ("switch-applications-backward", (Meta.KeyHandlerFunc) window_switcher.handle_switch_windows); - Meta.KeyBinding.set_custom_handler ("switch-windows", (Meta.KeyHandlerFunc) window_switcher.handle_switch_windows); - Meta.KeyBinding.set_custom_handler ("switch-windows-backward", (Meta.KeyHandlerFunc) window_switcher.handle_switch_windows); - Meta.KeyBinding.set_custom_handler ("switch-group", (Meta.KeyHandlerFunc) window_switcher.handle_switch_windows); - Meta.KeyBinding.set_custom_handler ("switch-group-backward", (Meta.KeyHandlerFunc) window_switcher.handle_switch_windows); - } - if (plugin_manager.window_overview_provider == null || (window_overview = (plugin_manager.get_plugin (plugin_manager.window_overview_provider) as ActivatableComponent)) == null) { window_overview = new WindowOverview (this); @@ -562,9 +548,6 @@ namespace Gala { var three_fingers_move_to_workspace = fingers == 3 && three_finger_swipe_horizontal == "move-to-workspace"; var four_fingers_move_to_workspace = fingers == 4 && four_finger_swipe_horizontal == "move-to-workspace"; - var three_fingers_switch_windows = fingers == 3 && three_finger_swipe_horizontal == "switch-windows"; - var four_fingers_switch_windows = fingers == 4 && four_finger_swipe_horizontal == "switch-windows"; - switch_workspace_with_gesture = three_fingers_switch_to_workspace || four_fingers_switch_to_workspace; if (switch_workspace_with_gesture) { var direction = gesture_tracker.settings.get_natural_scroll_direction (gesture); @@ -586,11 +569,6 @@ namespace Gala { switch_to_next_workspace (direction, display.get_current_time ()); return; } - - var switch_windows = three_fingers_switch_windows || four_fingers_switch_windows; - if (switch_windows && !window_switcher.opened) { - window_switcher.handle_gesture (gesture.direction); - } } /** diff --git a/src/meson.build b/src/meson.build index e2e61807b..348380635 100644 --- a/src/meson.build +++ b/src/meson.build @@ -33,6 +33,7 @@ gala_bin_sources = files( 'ColorFilters/ColorblindnessCorrectionEffect.vala', 'ColorFilters/FilterManager.vala', 'ColorFilters/MonochromeEffect.vala', + 'Gestures/DBusGestureProvider.vala', 'Gestures/Gesture.vala', 'Gestures/GestureSettings.vala', 'Gestures/GestureTracker.vala', @@ -63,8 +64,6 @@ gala_bin_sources = files( 'Widgets/WindowCloneContainer.vala', 'Widgets/WindowIconActor.vala', 'Widgets/WindowOverview.vala', - 'Widgets/WindowSwitcher/WindowSwitcher.vala', - 'Widgets/WindowSwitcher/WindowSwitcherIcon.vala', 'Widgets/WorkspaceClone.vala', 'Widgets/WorkspaceInsertThumb.vala', ) diff --git a/windowSwitcher/Application.vala b/windowSwitcher/Application.vala new file mode 100644 index 000000000..921a04cf4 --- /dev/null +++ b/windowSwitcher/Application.vala @@ -0,0 +1,79 @@ +/* + * Copyright 2024 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +public class Gala.WindowSwitcher.Application : Gtk.Application { + public const string ACTION_PREFIX = "app."; + public const string CYCLE_FORWARD_ACTION = "switch-windows"; + public const string CYCLE_BACKWARD_ACTION = "switch-windows-backward"; + public const string CYCLE_CURRENT_FORWARD_ACTION = "switch-group"; + public const string CYCLE_CURRENT_BACKWARD_ACTION = "switch-group-backward"; + + private const ActionEntry[] ACTIONS = { + {CYCLE_FORWARD_ACTION, cycle, null, null, null}, + {CYCLE_BACKWARD_ACTION, cycle_backward, null, null, null}, + {CYCLE_CURRENT_FORWARD_ACTION, cycle_current, null, null, null}, + {CYCLE_CURRENT_BACKWARD_ACTION, cycle_current_backward, null, null, null} + }; + + private static Settings settings; + + private WindowSwitcher window_switcher; + + public Application () { + Object (application_id: "io.elementary.window-switcher"); + } + + public override void startup () { + base.startup (); + + settings = new Settings ("io.elementary.desktop.window-switcher"); + settings.changed.connect (setup_accels); + setup_accels (); + + add_action_entries (ACTIONS, this); + + Granite.init (); + + window_switcher = new WindowSwitcher (this); + + ShellKeyGrabber.init ({CYCLE_FORWARD_ACTION, CYCLE_BACKWARD_ACTION, + CYCLE_CURRENT_FORWARD_ACTION, CYCLE_CURRENT_BACKWARD_ACTION}, settings); + } + + private void setup_accels () { + set_accels_for_action (ACTION_PREFIX + CYCLE_FORWARD_ACTION, settings.get_strv (CYCLE_FORWARD_ACTION)); + set_accels_for_action (ACTION_PREFIX + CYCLE_BACKWARD_ACTION, settings.get_strv (CYCLE_BACKWARD_ACTION)); + set_accels_for_action (ACTION_PREFIX + CYCLE_CURRENT_FORWARD_ACTION, settings.get_strv (CYCLE_CURRENT_FORWARD_ACTION)); + set_accels_for_action (ACTION_PREFIX + CYCLE_CURRENT_BACKWARD_ACTION, settings.get_strv (CYCLE_CURRENT_BACKWARD_ACTION)); + } + + private void cycle () { + window_switcher.cycle (false, false); + } + + private void cycle_backward () { + window_switcher.cycle (false, true); + } + + private void cycle_current () { + window_switcher.cycle (true, false); + } + + private void cycle_current_backward () { + window_switcher.cycle (true, true); + } + + public override void activate () { } +} + +public static int main (string[] args) { + GLib.Intl.setlocale (LocaleCategory.ALL, ""); + GLib.Intl.bindtextdomain (Config.GETTEXT_PACKAGE, Config.LOCALEDIR); + GLib.Intl.bind_textdomain_codeset (Config.GETTEXT_PACKAGE, "UTF-8"); + GLib.Intl.textdomain (Config.GETTEXT_PACKAGE); + + var app = new Gala.WindowSwitcher.Application (); + return app.run (); +} diff --git a/windowSwitcher/ShellKeyGrabber.vala b/windowSwitcher/ShellKeyGrabber.vala new file mode 100644 index 000000000..1a76c9eac --- /dev/null +++ b/windowSwitcher/ShellKeyGrabber.vala @@ -0,0 +1,92 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * SPDX-FileCopyrightText: 2024 elementary, Inc. (https://elementary.io) + */ + +public struct Accelerator { + public string name; + public uint mode_flags; + public uint grab_flags; +} + +[DBus (name = "org.gnome.Shell")] +public interface Gala.WindowSwitcher.ShellKeyGrabber : GLib.Object { + public abstract signal void accelerator_activated (uint action, GLib.HashTable parameters_dict); + + public abstract uint grab_accelerator (string accelerator, uint mode_flags, uint grab_flags) throws GLib.DBusError, GLib.IOError; + public abstract uint[] grab_accelerators (Accelerator[] accelerators) throws GLib.DBusError, GLib.IOError; + public abstract bool ungrab_accelerators (uint[] actions) throws GLib.DBusError, GLib.IOError; + + private static string[] actions; + private static Settings settings; + + private static Gee.HashMap saved_action_ids; + + private static ShellKeyGrabber? instance; + + public static void init (string[] _actions, Settings _settings) { + actions = _actions; + settings = _settings; + + saved_action_ids = new Gee.HashMap (); + + settings.changed.connect (() => { + ungrab_keybindings (); + setup_grabs (); + }); + + Bus.watch_name (SESSION, "org.gnome.Shell", NONE, () => on_watch.begin (), () => instance = null); + } + + private static async void on_watch () { + try { + instance = yield Bus.get_proxy (SESSION, "org.gnome.Shell", "/org/gnome/Shell"); + + setup_grabs (); + instance.accelerator_activated.connect (on_accelerator_activated); + } catch (Error e) { + warning ("Failed to connect to bus for keyboard shortcut grabs: %s", e.message); + } + } + + private static void setup_grabs () requires (instance != null) { + foreach (var action in actions) { + Accelerator[] accelerators = {}; + + foreach (var keybinding in settings.get_strv (action)) { + accelerators += Accelerator () { + name = keybinding, + mode_flags = 0, + grab_flags = 0 + }; + } + + try { + foreach (var id in instance.grab_accelerators (accelerators)) { + saved_action_ids.set (id, action); + } + } catch (Error e) { + critical ("Couldn't grab accelerators: %s", e.message); + } + } + } + + private static void on_accelerator_activated (uint action, GLib.HashTable parameters_dict) { + if (!saved_action_ids.has_key (action)) { + return; + } + + ((Gtk.Application) GLib.Application.get_default ()).activate_action ( + saved_action_ids[action], + null + ); + } + + private static void ungrab_keybindings () requires (instance != null) { + try { + instance.ungrab_accelerators (saved_action_ids.keys.to_array ()); + } catch (Error e) { + critical ("Couldn't ungrab accelerators: %s", e.message); + } + } +} diff --git a/windowSwitcher/WindowSwitcher.vala b/windowSwitcher/WindowSwitcher.vala new file mode 100644 index 000000000..51d737e77 --- /dev/null +++ b/windowSwitcher/WindowSwitcher.vala @@ -0,0 +1,233 @@ +[DBus (name="org.pantheon.gala.DesktopIntegration")] +public interface DesktopIntegration : Object { + public struct Window { + uint64 uid; + GLib.HashTable properties; + } + + public abstract Window[] get_windows () throws IOError, DBusError; + public abstract void focus_window (uint64 uid) throws GLib.DBusError, GLib.IOError; +} + +public class Gala.WindowSwitcher.WindowSwitcher : Gtk.ApplicationWindow, PantheonWayland.ExtendedBehavior { + private DesktopIntegration? desktop_integration; + + private Gtk.FlowBox flow_box; + private Gtk.Label title_label; + + private int n_windows = 0; + + private bool active = false; + private bool only_current = false; + + public WindowSwitcher (Application application) { + Object ( + application: application + ); + } + + construct { + flow_box = new Gtk.FlowBox () { + homogeneous = true, + selection_mode = NONE, + column_spacing = 3, + row_spacing = 3, + activate_on_single_click = true + }; + + title_label = new Gtk.Label (null) { + ellipsize = END + }; + + var box = new Gtk.Box (VERTICAL, 6) { + margin_top = 12, + margin_bottom = 12, + margin_end = 12, + margin_start = 12 + }; + box.append (flow_box); + box.append (title_label); + + titlebar = new Gtk.Grid () { visible = false }; + child = box; + + child.realize.connect (connect_to_shell); + + /* + * Because we hide, our surface doesn't get destroyed. + * But Gala "forgets" about us so every time we present we have to keep above and center again. + */ + child.map.connect (() => { + set_keep_above (); + make_centered (); + + var surface = get_surface (); + if (surface is Gdk.Toplevel) { + ((Gdk.Toplevel) surface).inhibit_system_shortcuts (null); + } + + update_default_size (); + }); + + var key_controller = new Gtk.EventControllerKey () { + propagation_phase = CAPTURE + }; + + key_controller.key_released.connect ((val) => { + if (val == Gdk.Key.Alt_L) { + close_switcher (); + } + }); + + key_controller.key_pressed.connect ((val, code, modifier_state) => { + if (val == Gdk.Key.Right) { + cycle (only_current, false); + return Gdk.EVENT_STOP; + } + + if (val == Gdk.Key.Left) { + cycle (only_current, true); + return Gdk.EVENT_STOP; + } + + return Gdk.EVENT_PROPAGATE; + }); + + ((Gtk.Widget) this).add_controller (key_controller); + + try { + desktop_integration = Bus.get_proxy_sync (SESSION, "org.pantheon.gala", "/org/pantheon/gala/DesktopInterface"); + } catch (Error e) { + warning ("Failed to get the desktop integration: %s", e.message); + } + + flow_box.child_activated.connect (() => close_switcher ()); + } + + public void activate_switcher (bool only_current) { + active = true; + this.only_current = only_current; + + n_windows = 0; + + flow_box.remove_all (); + + try { + var windows = desktop_integration.get_windows (); + var current_app_id = only_current ? get_current_app_id (windows) : null; + foreach (var window in windows) { + if (is_eligible_window (window, current_app_id)) { + var icon = new WindowSwitcherIcon (window.uid, (string) window.properties["title"], (string) window.properties["app-id"]); + flow_box.append (icon); + + if (++n_windows == 2) { + flow_box.set_focus_child (icon); + } + } + } + } catch (Error e) { + warning ("Failed to get windows: %s", e.message); + } + + if (n_windows == 0) { + get_surface ().beep (); + return; + } + + if (n_windows == 1) { + flow_box.set_focus_child (flow_box.get_first_child ()); + } + + update_title (); + present (); + } + + private void update_default_size () { + Gtk.Requisition natural_size; + flow_box.get_first_child ().get_preferred_size (null, out natural_size); + + var display_width = Gdk.Display.get_default ().get_monitor_at_surface (get_surface ()).get_geometry ().width - 50; + + var max_children = (int) display_width / (natural_size.width + 3); + var min_children = (int) Math.fmin (n_windows, max_children); + + flow_box.min_children_per_line = min_children; + flow_box.max_children_per_line = max_children; + + default_width = 1; + default_height = 1; + } + + public void close_switcher () { + hide (); + + var icon = (WindowSwitcherIcon) flow_box.get_focus_child (); + + try { + desktop_integration.focus_window (icon.uid); + } catch (Error e) { + warning ("Failed to focus window"); + } + + active = false; + } + + public void cycle (bool only_current, bool backwards) { + if (!active) { + activate_switcher (only_current); + return; + } + + if (this.only_current != only_current) { + //todo: gdk beep? + return; + } + + if (backwards) { + if (!(flow_box.get_focus_child ().get_prev_sibling () is WindowSwitcherIcon)) { + flow_box.set_focus_child (flow_box.get_last_child ()); + } + + flow_box.child_focus (TAB_BACKWARD); + } else { + if (!(flow_box.get_focus_child ().get_next_sibling () is WindowSwitcherIcon)) { + flow_box.set_focus_child (flow_box.get_first_child ()); + } + + flow_box.child_focus (TAB_FORWARD); + } + + update_title (); + } + + private void update_title () { + var focus_child = flow_box.get_focus_child (); + if (focus_child != null && focus_child is WindowSwitcherIcon) { + title_label.label = ((WindowSwitcherIcon) focus_child).title; + } else { + title_label.label = null; + } + } + + private bool is_eligible_window (DesktopIntegration.Window window, string? current_app_id) { + if (!(bool) window.properties["on-active-workspace"]) { + return false; + } + + if (current_app_id != null && (string) window.properties["app-id"] != current_app_id) { + return false; + } + + return true; + } + + private string? get_current_app_id (DesktopIntegration.Window[] windows) { + foreach (var window in windows) { + if ((bool) window.properties["has-focus"]) { + return (string) window.properties["app-id"]; + } + } + + return null; + } +} diff --git a/windowSwitcher/WindowSwitcherIcon.vala b/windowSwitcher/WindowSwitcherIcon.vala new file mode 100644 index 000000000..20b432143 --- /dev/null +++ b/windowSwitcher/WindowSwitcherIcon.vala @@ -0,0 +1,31 @@ +public class WindowSwitcherIcon : Gtk.FlowBoxChild { + public uint64 uid { get; construct; } + public string title { get; construct; } + public string app_id { get; construct; } + + public WindowSwitcherIcon (uint64 uid, string title, string app_id) { + Object ( + uid: uid, + title: title, + app_id: app_id + ); + } + + construct { + var desktop_app_info = new DesktopAppInfo (app_id); + + var image = new Gtk.Image.from_gicon (desktop_app_info.get_icon ()) { + pixel_size = 64, + margin_top = 12, + margin_bottom = 12, + margin_start = 12, + margin_end = 12 + }; + + child = image; + + var hover_controller = new Gtk.EventControllerMotion (); + hover_controller.enter.connect (() => grab_focus ()); + add_controller (hover_controller); + } +} diff --git a/windowSwitcher/meson.build b/windowSwitcher/meson.build new file mode 100644 index 000000000..5173fed2c --- /dev/null +++ b/windowSwitcher/meson.build @@ -0,0 +1,15 @@ +window_switcher_sources = files( + 'Application.vala', + 'ShellKeyGrabber.vala', + 'WindowSwitcher.vala', + 'WindowSwitcherIcon.vala' +) + +window_switcher_bin = executable( + 'io.elementary.window-switcher', + window_switcher_sources, + gala_resources, + dependencies: [config_dep, granite_dep, gtk4_dep, dependency('pantheon-wayland-1'), m_dep], + include_directories: config_inc_dir, + install: true, +)