-
-
Notifications
You must be signed in to change notification settings - Fork 78
Support keyboard navigation over multiple monitors in multitasking view #2536
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
leolost2605
wants to merge
9
commits into
main
Choose a base branch
from
leolost/focusable
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+323
−229
Open
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
1ce9a80
Introduce Focusable
leolost2605 e414215
ActorTarget: Inherit from Focusable
leolost2605 56cd4be
WindowClone(Container): Rely on Focusable
leolost2605 06a8e82
Select WindowClone on enter
leolost2605 d2f4da9
MultitaskingView: Use FocusController
leolost2605 9bb8108
WindowOverview: Use FocusController
leolost2605 0f0df94
WorkspaceRow: Allow focus only within currently active ws
leolost2605 9eea7a1
WindowCloneContainer: Drop requested_close
leolost2605 31a0d8f
Rename Focusable to Widget
leolost2605 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| /* | ||
| * Copyright 2025 elementary, Inc. (https://elementary.io) | ||
| * SPDX-License-Identifier: GPL-3.0-or-later | ||
| * | ||
| * Authored by: Leonhard Kargl <leo.kargl@proton.me> | ||
| */ | ||
|
|
||
| public class Gala.FocusController : Clutter.Action { | ||
| internal static Quark focus_visible_quark = Quark.from_string ("gala-focus-visible"); | ||
|
|
||
| private uint timeout_id = 0; | ||
|
|
||
| public Clutter.Stage stage { get; construct; } | ||
|
|
||
| public FocusController (Clutter.Stage stage) { | ||
| Object (stage: stage); | ||
| } | ||
|
|
||
| construct { | ||
| // In the case the key focus moves out of our widget tree by some other means | ||
| // make sure we can recapture it | ||
| stage.key_press_event.connect (check_focus); | ||
| } | ||
|
|
||
| public override bool handle_event (Clutter.Event event) { | ||
| return event.get_type () != KEY_PRESS ? Clutter.EVENT_PROPAGATE : check_focus (event); | ||
| } | ||
|
|
||
| private bool check_focus (Clutter.Event event) requires ( | ||
| actor is Widget && !(actor.get_parent () is Widget) // Make sure we are only attached to root widgets | ||
| ) { | ||
| var direction = FocusDirection.get_for_event (event); | ||
|
|
||
| if (direction == null) { | ||
| return Clutter.EVENT_PROPAGATE; | ||
| } | ||
|
|
||
| if (!((Widget) actor).focus (direction)) { | ||
| #if HAS_MUTTER47 | ||
| stage.context.get_backend ().get_default_seat ().bell_notify (); | ||
| #else | ||
| Clutter.get_default_backend ().get_default_seat ().bell_notify (); | ||
| #endif | ||
|
|
||
| if (!(stage.key_focus in actor)) { | ||
| stage.key_focus = actor; | ||
| } | ||
| } | ||
|
|
||
| show_focus (); | ||
|
|
||
| return Clutter.EVENT_STOP; | ||
| } | ||
|
|
||
| private void show_focus () { | ||
| if (timeout_id != 0) { | ||
| Source.remove (timeout_id); | ||
| } else { | ||
| set_focus_visible (true); | ||
| } | ||
|
|
||
| timeout_id = Timeout.add_seconds (5, () => { | ||
| set_focus_visible (false); | ||
| timeout_id = 0; | ||
| return Source.REMOVE; | ||
| }); | ||
| } | ||
|
|
||
| private void set_focus_visible (bool visible) { | ||
| actor.set_qdata (focus_visible_quark, visible); | ||
| (stage.key_focus as Widget)?.focus_changed (); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| /* | ||
| * Copyright 2025 elementary, Inc. (https://elementary.io) | ||
| * SPDX-License-Identifier: GPL-3.0-or-later | ||
| * | ||
| * Authored by: Leonhard Kargl <leo.kargl@proton.me> | ||
| */ | ||
|
|
||
| public enum Gala.FocusDirection { | ||
| UP, | ||
| DOWN, | ||
| LEFT, | ||
| RIGHT; | ||
|
|
||
| public bool is_forward () { | ||
| return this == DOWN || this == RIGHT; | ||
| } | ||
|
|
||
| public static FocusDirection? get_for_event (Clutter.Event event) { | ||
| switch (event.get_key_symbol ()) { | ||
| case Clutter.Key.Up: return UP; | ||
| case Clutter.Key.Down: return DOWN; | ||
| case Clutter.Key.Left: return LEFT; | ||
| case Clutter.Key.Right: return RIGHT; | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
| } | ||
|
|
||
| namespace Gala.FocusUtils { | ||
| public void filter_children_for_direction (Gee.List<Widget> children, Clutter.Actor focus_actor, FocusDirection direction) { | ||
| Widget? focus_child = null; | ||
| foreach (var child in children) { | ||
| if (focus_actor in child) { | ||
| focus_child = (Widget) child; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| var to_retain = new Gee.LinkedList<Widget> (); | ||
| to_retain.add_all_iterator (children.filter ((c) => { | ||
| if (focus_child == null || c == focus_child) { | ||
| return true; | ||
| } | ||
|
|
||
| var focus_rect = get_allocation_rect (focus_child); | ||
| var rect = get_allocation_rect (c); | ||
|
|
||
| if ((direction == UP || direction == DOWN) && !rect.horiz_overlap (focus_rect) || | ||
| (direction == LEFT || direction == RIGHT) && !rect.vert_overlap (focus_rect) | ||
| ) { | ||
| return false; | ||
| } | ||
|
|
||
| return ( | ||
| direction == UP && rect.y + rect.height <= focus_rect.y || | ||
| direction == DOWN && rect.y >= focus_rect.y + focus_rect.height || | ||
| direction == LEFT && rect.x + rect.width <= focus_rect.x || | ||
| direction == RIGHT && rect.x >= focus_rect.x + focus_rect.width | ||
| ); | ||
| })); | ||
|
|
||
| children.retain_all (to_retain); | ||
| } | ||
|
|
||
| private inline Mtk.Rectangle get_allocation_rect (Clutter.Actor actor) { | ||
| return {(int) actor.x, (int) actor.y, (int) actor.width, (int) actor.height}; | ||
| } | ||
|
|
||
| public void sort_children_for_direction (Gee.List<Widget> children, FocusDirection direction) { | ||
| children.sort ((a, b) => { | ||
| if (direction == UP && a.y + a.height > b.y + b.height || | ||
| direction == DOWN && a.y < b.y || | ||
| direction == LEFT && a.x + a.width > b.x + b.width || | ||
| direction == RIGHT && a.x < b.x | ||
| ) { | ||
| return -1; | ||
| } | ||
|
|
||
| return 1; | ||
| }); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| /* | ||
| * Copyright 2025 elementary, Inc. (https://elementary.io) | ||
| * SPDX-License-Identifier: GPL-3.0-or-later | ||
| * | ||
| * Authored by: Leonhard Kargl <leo.kargl@proton.me> | ||
| */ | ||
|
|
||
| public class Gala.Widget : ActorTarget { | ||
| public bool can_focus { get; set; default = false; } | ||
| public bool has_visible_focus { get; private set; default = false; } | ||
|
|
||
| construct { | ||
| key_focus_in.connect (focus_changed); | ||
| key_focus_out.connect (focus_changed); | ||
| } | ||
|
|
||
| internal void focus_changed () { | ||
| has_visible_focus = has_key_focus () && get_root ().get_qdata<bool> (FocusController.focus_visible_quark); | ||
| } | ||
|
|
||
| private Widget get_root () { | ||
| var parent = get_parent (); | ||
| if (parent is Widget) { | ||
| return parent.get_root (); | ||
| } | ||
|
|
||
| return this; | ||
| } | ||
|
|
||
| public bool focus (FocusDirection direction) { | ||
| var focus_actor = get_stage ().get_key_focus (); | ||
|
|
||
| // We have focus so try to move it to a child | ||
| if (focus_actor == this) { | ||
| if (direction.is_forward ()) { | ||
| return move_focus (direction); | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| // A child of us (or subchild) has focus, try to move it to the next one. | ||
| // If that doesn't work and we are moving backwards focus us | ||
| if (focus_actor != null && focus_actor is Widget && focus_actor in this) { | ||
| if (move_focus (direction)) { | ||
| return true; | ||
| } | ||
|
|
||
| if (direction.is_forward ()) { | ||
| return false; | ||
| } else { | ||
| return grab_focus (); | ||
| } | ||
| } | ||
|
|
||
| // Focus is outside of us, try to take it | ||
| if (direction.is_forward ()) { | ||
| if (grab_focus ()) { | ||
| return true; | ||
| } | ||
|
|
||
| return move_focus (direction); | ||
| } else { | ||
| if (move_focus (direction)) { | ||
| return true; | ||
| } | ||
|
|
||
| return grab_focus (); | ||
| } | ||
| } | ||
|
|
||
| private bool grab_focus () { | ||
| if (!can_focus) { | ||
| return false; | ||
| } | ||
|
|
||
| grab_key_focus (); | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| protected virtual bool move_focus (FocusDirection direction) { | ||
| var children = get_widget_children (); | ||
|
|
||
| FocusUtils.filter_children_for_direction (children, get_stage ().key_focus, direction); | ||
| FocusUtils.sort_children_for_direction (children, direction); | ||
|
|
||
| foreach (var child in children) { | ||
| if (child.focus (direction)) { | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| private Gee.List<Widget> get_widget_children () { | ||
| var widget_children = new Gee.ArrayList<Widget> (); | ||
| for (var child = get_first_child (); child != null; child = child.get_next_sibling ()) { | ||
| if (child is Widget && child.visible) { | ||
| widget_children.add ((Widget) child); | ||
| } | ||
| } | ||
| return widget_children; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should check if actor is Widget and use property to store the data, since we don't use qdata anywhere else