From b16f344ef49faafd459048878e5edf0e734d814c Mon Sep 17 00:00:00 2001 From: lenemter Date: Sat, 15 Aug 2026 23:30:01 +0200 Subject: [PATCH] WIP: keyboard nav & a11y --- data/Application.css | 5 ++ src/AppSystem/Background/BackgroundItem.vala | 4 +- src/AppSystem/Launcher.vala | 27 +++--- src/BaseItem.vala | 19 ++-- src/Button.vala | 46 ++++++++++ src/ContainerItem.vala | 2 +- src/ItemGroup.vala | 2 +- src/ItemManager.vala | 87 ++++++++++++++++++- src/MainWindow.vala | 3 +- src/WorkspaceSystem/DynamicWorkspaceItem.vala | 6 +- src/WorkspaceSystem/WorkspaceIconGroup.vala | 11 ++- src/meson.build | 1 + 12 files changed, 184 insertions(+), 29 deletions(-) create mode 100644 src/Button.vala diff --git a/data/Application.css b/data/Application.css index 62210c65..cd4fe8fe 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; +} \ No newline at end of file diff --git a/src/AppSystem/Background/BackgroundItem.vala b/src/AppSystem/Background/BackgroundItem.vala index d547b964..861ca027 100644 --- a/src/AppSystem/Background/BackgroundItem.vala +++ b/src/AppSystem/Background/BackgroundItem.vala @@ -75,7 +75,9 @@ public class Dock.BackgroundItem : BaseIconGroup { } }); - gesture_click.released.connect (popover_menu.popup); + button.clicked.connect (popover_menu.popup); + + button.update_property (Gtk.AccessibleProperty.LABEL, _("Background Apps")); } private Gtk.Widget create_widget_func (Object obj) { diff --git a/src/AppSystem/Launcher.vala b/src/AppSystem/Launcher.vala index 05c93ae3..78786df6 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,7 @@ public class Dock.Launcher : BaseItem { easing = EASE_IN_OUT_QUAD }; - gesture_click.button = 0; - gesture_click.released.connect (on_click_released); + button.clicked.connect (on_clicked); var long_press = new Gtk.GestureLongPress () { touch_only = true @@ -301,6 +304,8 @@ public class Dock.Launcher : BaseItem { add_controller (drop_controller_motion); drop_controller_motion.enter.connect (queue_dnd_cycle); drop_controller_motion.leave.connect (remove_dnd_cycle); + + button.update_property (Gtk.AccessibleProperty.LABEL, app.app_info.get_display_name ()); } ~Launcher () { @@ -320,12 +325,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 +339,7 @@ public class Dock.Launcher : BaseItem { animate_launch (); } else { animate_shake (); - event_display.beep (); + display.beep (); } break; case Gdk.BUTTON_SECONDARY: @@ -349,7 +354,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 +365,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..5fd8e8ce 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; } @@ -91,12 +90,13 @@ public class Dock.BaseItem : Gtk.Box { construct { orientation = VERTICAL; + focusable = true; - 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 bin = new Granite.Bin () { - child = overlay + child = button }; actionable_box = new Gtk.Box (VERTICAL, 0); @@ -185,9 +185,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 +211,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 +282,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..ee0505d7 --- /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 = 0; } + + 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..03122b66 100644 --- a/src/ItemManager.vala +++ b/src/ItemManager.vala @@ -12,6 +12,13 @@ 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; + + class construct { + set_accessible_role (LIST); + } static construct { settings = new Settings ("io.elementary.dock"); @@ -33,6 +40,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 +49,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 +158,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/MainWindow.vala b/src/MainWindow.vala index f54a6dcc..a0db9e61 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -32,7 +32,8 @@ public class Dock.MainWindow : Gtk.ApplicationWindow { construct { overflow = VISIBLE; resizable = false; - titlebar = new Gtk.Label ("") { visible = false }; + title = _("Dock"); + titlebar = new Gtk.Grid () { visible = false }; var dock_box = new Gtk.Box (VERTICAL, 0); dock_box.append (new TopMargin ()); diff --git a/src/WorkspaceSystem/DynamicWorkspaceItem.vala b/src/WorkspaceSystem/DynamicWorkspaceItem.vala index 9cac739f..57c037e6 100644 --- a/src/WorkspaceSystem/DynamicWorkspaceItem.vala +++ b/src/WorkspaceSystem/DynamicWorkspaceItem.vala @@ -27,6 +27,8 @@ public class Dock.DynamicWorkspaceIcon : ContainerItem, WorkspaceItem { _("New Workspace") ); + button.update_property (Gtk.AccessibleProperty.LABEL, _("New Workspace")); + WorkspaceSystem.get_default ().workspaces.items_changed.connect (update_active_state); WindowSystem.get_default ().notify["active-workspace"].connect (update_active_state); @@ -43,8 +45,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..2326fbb7 100644 --- a/src/WorkspaceSystem/WorkspaceIconGroup.vala +++ b/src/WorkspaceSystem/WorkspaceIconGroup.vala @@ -38,8 +38,11 @@ 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); + + update_accessible_label (); + workspace.notify["index"].connect (update_accessible_label); } public void window_entered (Window window) { @@ -55,4 +58,8 @@ public class Dock.WorkspaceIconGroup : BaseIconGroup, WorkspaceItem { additional_icons.remove_all (); unset_state_flags (DROP_ACTIVE); } + + private void update_accessible_label () { + button.update_property (Gtk.AccessibleProperty.LABEL, _("Workspace %d").printf (workspace_index + 1)); + } } 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',