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,
+)