From 76e39eff721c0ee5dfcf8177fc2772410c3cb536 Mon Sep 17 00:00:00 2001 From: lenemter Date: Sun, 16 Aug 2026 20:47:29 +0200 Subject: [PATCH 1/3] Keyboard navigation --- data/Application.css | 5 ++ src/AppSystem/Background/BackgroundItem.vala | 2 +- src/AppSystem/Launcher.vala | 26 +++--- src/BaseItem.vala | 20 ++--- src/Button.vala | 46 ++++++++++ src/ContainerItem.vala | 2 +- src/ItemGroup.vala | 2 +- src/ItemManager.vala | 83 ++++++++++++++++++- src/WorkspaceSystem/DynamicWorkspaceItem.vala | 4 +- src/WorkspaceSystem/WorkspaceIconGroup.vala | 4 +- src/meson.build | 1 + 11 files changed, 166 insertions(+), 29 deletions(-) create mode 100644 src/Button.vala diff --git a/data/Application.css b/data/Application.css index 379e8968..4a7772e6 100644 --- a/data/Application.css +++ b/data/Application.css @@ -172,3 +172,8 @@ backgrounditem .close-button { .running-indicator:disabled { color: @fg_color; } + +dock-button:focus-visible { + background: alpha(@accent_color, 0.5); + border-radius: 6px; +} diff --git a/src/AppSystem/Background/BackgroundItem.vala b/src/AppSystem/Background/BackgroundItem.vala index d547b964..be6b5a35 100644 --- a/src/AppSystem/Background/BackgroundItem.vala +++ b/src/AppSystem/Background/BackgroundItem.vala @@ -75,7 +75,7 @@ public class Dock.BackgroundItem : BaseIconGroup { } }); - gesture_click.released.connect (popover_menu.popup); + button.clicked.connect (popover_menu.popup); } private Gtk.Widget create_widget_func (Object obj) { diff --git a/src/AppSystem/Launcher.vala b/src/AppSystem/Launcher.vala index 05c93ae3..0ffb1396 100644 --- a/src/AppSystem/Launcher.vala +++ b/src/AppSystem/Launcher.vala @@ -126,10 +126,14 @@ public class Dock.Launcher : BaseItem { transition_type = CROSSFADE }; - overlay.child = image; + var overlay = new Gtk.Overlay () { + child = image + }; overlay.add_overlay (badge_container); overlay.add_overlay (progress_revealer); + button.child = overlay; + var running_indicator = new Gtk.Image.from_icon_name ("pager-checked-symbolic"); running_indicator.add_css_class ("running-indicator"); @@ -240,8 +244,8 @@ public class Dock.Launcher : BaseItem { easing = EASE_IN_OUT_QUAD }; - gesture_click.button = 0; - gesture_click.released.connect (on_click_released); + button.mouse_button = 0; + button.clicked.connect (on_clicked); var long_press = new Gtk.GestureLongPress () { touch_only = true @@ -320,12 +324,12 @@ public class Dock.Launcher : BaseItem { remove_dnd_cycle (); } - private void on_click_released (int n_press, double x, double y) { - var event_display = gesture_click.get_current_event ().get_display (); - var context = event_display.get_app_launch_context (); - context.set_timestamp (gesture_click.get_current_event_time ()); + private void on_clicked (uint button, uint32 timestamp) { + unowned var display = Gdk.Display.get_default (); + var context = display.get_app_launch_context (); + context.set_timestamp (timestamp); - switch (gesture_click.get_current_button ()) { + switch (button) { case Gdk.BUTTON_PRIMARY: app.launch (context); break; @@ -334,7 +338,7 @@ public class Dock.Launcher : BaseItem { animate_launch (); } else { animate_shake (); - event_display.beep (); + display.beep (); } break; case Gdk.BUTTON_SECONDARY: @@ -349,7 +353,7 @@ public class Dock.Launcher : BaseItem { return; } - bounce_up.value_to = -0.5 * overlay.get_height (); + bounce_up.value_to = -0.5 * button.get_height (); bounce_down.value_from = bounce_up.value_to; bounce_up.play (); @@ -360,7 +364,7 @@ public class Dock.Launcher : BaseItem { return; } - shake.value_to = -0.1 * overlay.get_width (); + shake.value_to = -0.1 * button.get_width (); shake.play (); int repeat_count = 0; diff --git a/src/BaseItem.vala b/src/BaseItem.vala index 95437092..118aeae1 100644 --- a/src/BaseItem.vala +++ b/src/BaseItem.vala @@ -48,7 +48,7 @@ public class Dock.BaseItem : Gtk.Box { bin.height_request = -1; } - overlay.visible = !value; + button.visible = !value; } } @@ -70,8 +70,7 @@ public class Dock.BaseItem : Gtk.Box { * It's needed because top margin messes with dnd offsets and gsk transform. */ protected Gtk.Box actionable_box; - protected Gtk.Overlay overlay; - protected Gtk.GestureClick gesture_click; + protected Button button; protected Granite.Bin bin { get; private set; } @@ -92,11 +91,11 @@ public class Dock.BaseItem : Gtk.Box { construct { orientation = VERTICAL; - overlay = new Gtk.Overlay (); + button = new Button (); - // We need the bin because we need the animation to run even if the overlay is not visible + // We need the bin because we need the animation to run even if the button is not visible bin = new Granite.Bin () { - child = overlay + child = button }; actionable_box = new Gtk.Box (VERTICAL, 0); @@ -185,9 +184,6 @@ public class Dock.BaseItem : Gtk.Box { add_controller (motion_controller); - gesture_click = new Gtk.GestureClick (); - add_controller (gesture_click); - if (group == NONE) { return; } @@ -214,6 +210,10 @@ public class Dock.BaseItem : Gtk.Box { popover_tooltip.dispose (); } + public override bool grab_focus () { + return button.grab_focus (); + } + public void set_revealed (bool revealed) { fade.skip (); reveal.skip (); @@ -281,7 +281,7 @@ public class Dock.BaseItem : Gtk.Box { } private void on_drag_begin (Gtk.DragSource drag_source, Gdk.Drag drag) { - var paintable = new Gtk.WidgetPaintable (overlay); + var paintable = new Gtk.WidgetPaintable (button); drag_source.set_icon (paintable.get_current_image (), drag_offset_x, drag_offset_y); moving = true; diff --git a/src/Button.vala b/src/Button.vala new file mode 100644 index 00000000..e5bf6026 --- /dev/null +++ b/src/Button.vala @@ -0,0 +1,46 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * SPDX-FileCopyrightText: 2026 elementary, Inc. (https://elementary.io) + */ + +public class Dock.Button : Granite.Bin { + public signal void clicked (uint button, uint32 timestamp); + + public uint mouse_button { get; set; default = 1; } + + class construct { + set_accessible_role (BUTTON); + set_css_name ("dock-button"); + } + + construct { + focusable = true; + + var gesture_click = new Gtk.GestureClick (); + bind_property ("mouse-button", gesture_click, "button", SYNC_CREATE); + gesture_click.released.connect (on_released); + add_controller (gesture_click); + + var key_controller = new Gtk.EventControllerKey (); + key_controller.key_pressed.connect (on_key_pressed); + add_controller (key_controller); + } + + private void on_released (Gtk.GestureClick gesture_click, int n_press, double x, double y) { + clicked (gesture_click.get_current_button (), gesture_click.get_current_event_time ()); + } + + private bool on_key_pressed ( + Gtk.EventControllerKey key_controller, + uint keyval, + uint keycode, + Gdk.ModifierType state + ) { + if (keyval == Gdk.Key.space || keyval == Gdk.Key.Return || keyval == Gdk.Key.KP_Enter) { + clicked (Gdk.BUTTON_PRIMARY, key_controller.get_current_event_time ()); + return true; + } + + return false; + } +} diff --git a/src/ContainerItem.vala b/src/ContainerItem.vala index 4442ea74..08469538 100644 --- a/src/ContainerItem.vala +++ b/src/ContainerItem.vala @@ -18,7 +18,7 @@ public abstract class Dock.ContainerItem : BaseItem { bind_property ("icon-size", container, "width-request", SYNC_CREATE); bind_property ("icon-size", container, "height-request", SYNC_CREATE); - overlay.child = container; + button.child = container; notify["state"].connect (() => { if ((state != HIDDEN) && !moving) { diff --git a/src/ItemGroup.vala b/src/ItemGroup.vala index 1e3e5e86..a2b775e6 100644 --- a/src/ItemGroup.vala +++ b/src/ItemGroup.vala @@ -15,9 +15,9 @@ public ListModel items { get; construct; } public CreateBaseItemFunc create_item_func { get; construct; } + public ListStore current_children { get; private set; } private Sequence item_store; - private ListStore current_children; private Adw.TimedAnimation resize_animation; diff --git a/src/ItemManager.vala b/src/ItemManager.vala index f48074d4..97cd7351 100644 --- a/src/ItemManager.vala +++ b/src/ItemManager.vala @@ -12,6 +12,9 @@ private Gtk.Separator separator; private DynamicWorkspaceIcon dynamic_workspace_item; #endif + private ListStore all_item_groups; + private Gtk.FlattenListModel all_items; + private bool changed_focus = false; static construct { settings = new Settings ("io.elementary.dock"); @@ -33,6 +36,8 @@ separator_box.append (new TopMargin ()); separator_box.append (separator); + var workspaces_group = new ItemGroup (WorkspaceSystem.get_default ().workspaces, (obj) => new WorkspaceIconGroup ((Workspace) obj)); + dynamic_workspace_item = new DynamicWorkspaceIcon (); #endif @@ -40,7 +45,7 @@ append (background_group); #if WORKSPACE_SWITCHER append (separator_box); - append (new ItemGroup (WorkspaceSystem.get_default ().workspaces, (obj) => new WorkspaceIconGroup ((Workspace) obj))); + append (workspaces_group); append (dynamic_workspace_item); #endif overflow = VISIBLE; @@ -149,6 +154,82 @@ WorkspaceSystem.get_default ().load.begin (); #endif }); + + all_item_groups = new GLib.ListStore (typeof (GLib.ListModel)); + all_item_groups.append (app_group.current_children); + all_item_groups.append (background_group.current_children); +#if WORKSPACE_SWITCHER + all_item_groups.append (workspaces_group.current_children); + + var dynamic_workspace_item_list = new GLib.ListStore (typeof (DynamicWorkspaceIcon)); + dynamic_workspace_item_list.append (dynamic_workspace_item); + + all_item_groups.append (dynamic_workspace_item_list); +#endif + + all_items = new Gtk.FlattenListModel (all_item_groups); + all_items.items_changed.connect ((all_items, position, removed, added) => { + if (!changed_focus) { + ((BaseItem) all_items.get_item (0)).grab_focus (); + } + }); + + var key_controller = new Gtk.EventControllerKey (); + key_controller.key_pressed.connect (on_key_pressed); + add_controller (key_controller); + } + + private bool on_key_pressed (uint keyval, uint keycode, Gdk.ModifierType state) { + unowned var current_focus = ((Gtk.Window) root).get_focus (); + if (current_focus == null || + !(current_focus.is_ancestor (this)) + ) { + return false; + } + + unowned var current_item = current_focus.get_ancestor (typeof (BaseItem)); + if (current_item == null) { + return false; + } + + int current_position = -1; + var n_items = all_items.n_items; + for (var i = 0; i < n_items; i++) { + var item = (BaseItem) all_items.get_item (i); + if (item == current_item) { + current_position = i; + break; + } + } + + if (current_position == -1) { + return false; + } + + BaseItem? next_widget = null; + switch (keyval) { + case Gdk.Key.Left: + if (current_position != 0) { + next_widget = (BaseItem) all_items.get_item (current_position - 1); + } + break; + case Gdk.Key.Right: + if (current_position != all_items.n_items - 1) { + next_widget = (BaseItem) all_items.get_item (current_position + 1); + } + break; + default: + return false; + } + + if (next_widget != null) { + next_widget.grab_focus (); + changed_focus = true; + queue_draw (); + return true; + } + + return false; } public void move_launcher_after (BaseItem source, int target_index) { diff --git a/src/WorkspaceSystem/DynamicWorkspaceItem.vala b/src/WorkspaceSystem/DynamicWorkspaceItem.vala index 9cac739f..f9030693 100644 --- a/src/WorkspaceSystem/DynamicWorkspaceItem.vala +++ b/src/WorkspaceSystem/DynamicWorkspaceItem.vala @@ -43,8 +43,8 @@ public class Dock.DynamicWorkspaceIcon : ContainerItem, WorkspaceItem { null, null ); - gesture_click.button = Gdk.BUTTON_PRIMARY; - gesture_click.released.connect (switch_to_new_workspace); + button.mouse_button = Gdk.BUTTON_PRIMARY; + button.clicked.connect (switch_to_new_workspace); } private void update_active_state () { diff --git a/src/WorkspaceSystem/WorkspaceIconGroup.vala b/src/WorkspaceSystem/WorkspaceIconGroup.vala index 1243d060..ae9606b9 100644 --- a/src/WorkspaceSystem/WorkspaceIconGroup.vala +++ b/src/WorkspaceSystem/WorkspaceIconGroup.vala @@ -38,8 +38,8 @@ public class Dock.WorkspaceIconGroup : BaseIconGroup, WorkspaceItem { return true; }); - gesture_click.button = Gdk.BUTTON_PRIMARY; - gesture_click.released.connect (workspace.activate); + button.mouse_button = Gdk.BUTTON_PRIMARY; + button.clicked.connect (workspace.activate); } public void window_entered (Window window) { diff --git a/src/meson.build b/src/meson.build index 51ab51dd..6550c73d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -3,6 +3,7 @@ sources = [ 'BaseIconGroup.vala', 'BaseItem.vala', 'BottomMargin.vala', + 'Button.vala', 'ContainerItem.vala', 'ItemGroup.vala', 'ItemManager.vala', From 856e56ebc053671bb3e6655ac61bc5cc0a45c232 Mon Sep 17 00:00:00 2001 From: lenemter Date: Thu, 20 Aug 2026 21:24:55 +0300 Subject: [PATCH 2/3] Simplify --- data/Application.css | 1 + src/BaseIconGroup.vala | 1 + src/ItemManager.vala | 79 ------------------------------------------ 3 files changed, 2 insertions(+), 79 deletions(-) diff --git a/data/Application.css b/data/Application.css index 4a7772e6..01559df3 100644 --- a/data/Application.css +++ b/data/Application.css @@ -176,4 +176,5 @@ backgrounditem .close-button { dock-button:focus-visible { background: alpha(@accent_color, 0.5); border-radius: 6px; + outline: 1px solid alpha(@accent_color, 0.8); } diff --git a/src/BaseIconGroup.vala b/src/BaseIconGroup.vala index 6e086d17..89913284 100644 --- a/src/BaseIconGroup.vala +++ b/src/BaseIconGroup.vala @@ -18,6 +18,7 @@ public abstract class Dock.BaseIconGroup : ContainerItem { selection_mode = NONE, halign = CENTER, valign = CENTER, + can_focus = false, }; flow_box.bind_model (slice, create_flow_box_child); diff --git a/src/ItemManager.vala b/src/ItemManager.vala index 97cd7351..b26508aa 100644 --- a/src/ItemManager.vala +++ b/src/ItemManager.vala @@ -12,9 +12,6 @@ private Gtk.Separator separator; private DynamicWorkspaceIcon dynamic_workspace_item; #endif - private ListStore all_item_groups; - private Gtk.FlattenListModel all_items; - private bool changed_focus = false; static construct { settings = new Settings ("io.elementary.dock"); @@ -154,82 +151,6 @@ WorkspaceSystem.get_default ().load.begin (); #endif }); - - all_item_groups = new GLib.ListStore (typeof (GLib.ListModel)); - all_item_groups.append (app_group.current_children); - all_item_groups.append (background_group.current_children); -#if WORKSPACE_SWITCHER - all_item_groups.append (workspaces_group.current_children); - - var dynamic_workspace_item_list = new GLib.ListStore (typeof (DynamicWorkspaceIcon)); - dynamic_workspace_item_list.append (dynamic_workspace_item); - - all_item_groups.append (dynamic_workspace_item_list); -#endif - - all_items = new Gtk.FlattenListModel (all_item_groups); - all_items.items_changed.connect ((all_items, position, removed, added) => { - if (!changed_focus) { - ((BaseItem) all_items.get_item (0)).grab_focus (); - } - }); - - var key_controller = new Gtk.EventControllerKey (); - key_controller.key_pressed.connect (on_key_pressed); - add_controller (key_controller); - } - - private bool on_key_pressed (uint keyval, uint keycode, Gdk.ModifierType state) { - unowned var current_focus = ((Gtk.Window) root).get_focus (); - if (current_focus == null || - !(current_focus.is_ancestor (this)) - ) { - return false; - } - - unowned var current_item = current_focus.get_ancestor (typeof (BaseItem)); - if (current_item == null) { - return false; - } - - int current_position = -1; - var n_items = all_items.n_items; - for (var i = 0; i < n_items; i++) { - var item = (BaseItem) all_items.get_item (i); - if (item == current_item) { - current_position = i; - break; - } - } - - if (current_position == -1) { - return false; - } - - BaseItem? next_widget = null; - switch (keyval) { - case Gdk.Key.Left: - if (current_position != 0) { - next_widget = (BaseItem) all_items.get_item (current_position - 1); - } - break; - case Gdk.Key.Right: - if (current_position != all_items.n_items - 1) { - next_widget = (BaseItem) all_items.get_item (current_position + 1); - } - break; - default: - return false; - } - - if (next_widget != null) { - next_widget.grab_focus (); - changed_focus = true; - queue_draw (); - return true; - } - - return false; } public void move_launcher_after (BaseItem source, int target_index) { From a0d2be046b163909778bd270ef5c142c4fb3a6bc Mon Sep 17 00:00:00 2001 From: lenemter Date: Fri, 21 Aug 2026 00:21:54 +0300 Subject: [PATCH 3/3] Always focus the fist item on start --- src/ItemGroup.vala | 6 +++++- src/ItemManager.vala | 35 ++++++++++++++++++++++++++++++++--- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/ItemGroup.vala b/src/ItemGroup.vala index a2b775e6..ccfe9765 100644 --- a/src/ItemGroup.vala +++ b/src/ItemGroup.vala @@ -15,9 +15,9 @@ public ListModel items { get; construct; } public CreateBaseItemFunc create_item_func { get; construct; } - public ListStore current_children { get; private set; } private Sequence item_store; + private ListStore current_children; private Adw.TimedAnimation resize_animation; @@ -174,6 +174,10 @@ } } + public BaseItem? get_first_item () { + return (BaseItem?) current_children.get_item (0); + } + public uint get_index_for_item (BaseItem item) { uint index; if (current_children.find (item, out index)) { diff --git a/src/ItemManager.vala b/src/ItemManager.vala index b26508aa..d358ec86 100644 --- a/src/ItemManager.vala +++ b/src/ItemManager.vala @@ -8,20 +8,24 @@ public Launcher? added_launcher { get; set; default = null; } + private ItemGroup app_group; + private ItemGroup background_group; #if WORKSPACE_SWITCHER private Gtk.Separator separator; + private ItemGroup workspaces_group; private DynamicWorkspaceIcon dynamic_workspace_item; #endif + private bool focus_was_moved = false; static construct { settings = new Settings ("io.elementary.dock"); } construct { - var app_group = new ItemGroup (AppSystem.get_default ().apps, (obj) => new Launcher ((App) obj)); + app_group = new ItemGroup (AppSystem.get_default ().apps, (obj) => new Launcher ((App) obj)); var background_item = new BackgroundItem (); - var background_group = new ItemGroup (background_item.group_model, (obj) => (BackgroundItem) obj); + background_group = new ItemGroup (background_item.group_model, (obj) => (BackgroundItem) obj); #if WORKSPACE_SWITCHER separator = new Gtk.Separator (VERTICAL) { @@ -33,7 +37,7 @@ separator_box.append (new TopMargin ()); separator_box.append (separator); - var workspaces_group = new ItemGroup (WorkspaceSystem.get_default ().workspaces, (obj) => new WorkspaceIconGroup ((Workspace) obj)); + workspaces_group = new ItemGroup (WorkspaceSystem.get_default ().workspaces, (obj) => new WorkspaceIconGroup ((Workspace) obj)); dynamic_workspace_item = new DynamicWorkspaceIcon (); #endif @@ -47,6 +51,10 @@ #endif overflow = VISIBLE; + app_group.items.items_changed.connect (on_group_items_changed); + background_group.items.items_changed.connect (on_group_items_changed); + workspaces_group.items.items_changed.connect (on_group_items_changed); + var drop_target_file = new Gtk.DropTarget (typeof (File), COPY) { preload = true }; @@ -153,6 +161,27 @@ }); } + private void on_group_items_changed () { + if (focus_was_moved) { + return; + } + + var item_to_focus = (BaseItem) ( + app_group.get_first_item () ?? + background_group.get_first_item () ?? + workspaces_group.get_first_item () ?? + dynamic_workspace_item + ); + + item_to_focus.grab_focus (); + } + + public override bool focus (Gtk.DirectionType direction) { + focus_was_moved = true; + + return base.focus (direction); + } + public void move_launcher_after (BaseItem source, int target_index) { if (source is Launcher) { AppSystem.get_default ().reorder_app (source.app, target_index);