-
-
Notifications
You must be signed in to change notification settings - Fork 83
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
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
20d7a64
Introduce Focusable
leolost2605 7182de9
ActorTarget: Inherit from Focusable
leolost2605 da9d0a5
WindowClone(Container): Rely on Focusable
leolost2605 a055353
Select WindowClone on enter
leolost2605 a685db8
MultitaskingView: Use FocusController
leolost2605 9c66fe7
WindowOverview: Use FocusController
leolost2605 d2eba69
WorkspaceRow: Allow focus only within currently active ws
leolost2605 6312516
WindowCloneContainer: Drop requested_close
leolost2605 d2ad271
Rename Focusable to Widget
leolost2605 c79cfc1
Root instead of focuscontroller
leolost2605 2c90b1e
Check for get root null
leolost2605 6b2d01b
FocusUtils: Use ActorBox instead of mtk rect
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,82 @@ | ||
| /* | ||
| * 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_alloc = focus_child.allocation; | ||
| var alloc = c.allocation; | ||
|
|
||
| var horiz_overlap = alloc.x1 < focus_alloc.x2 && alloc.x2 > focus_alloc.x1; | ||
| var vert_overlap = alloc.y1 < focus_alloc.y2 && alloc.y2 > focus_alloc.y1; | ||
|
|
||
| if ((direction == UP || direction == DOWN) && !horiz_overlap || | ||
| (direction == LEFT || direction == RIGHT) && !vert_overlap | ||
| ) { | ||
| return false; | ||
| } | ||
|
|
||
| return ( | ||
| direction == UP && alloc.y2 <= focus_alloc.y1 || | ||
| direction == DOWN && alloc.y1 >= focus_alloc.y2 || | ||
| direction == LEFT && alloc.x2 <= focus_alloc.x1 || | ||
| direction == RIGHT && alloc.x1 >= focus_alloc.x2 | ||
| ); | ||
| })); | ||
|
|
||
| children.retain_all (to_retain); | ||
| } | ||
|
|
||
| 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,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.Root : Widget { | ||
| private bool _focus_visible = false; | ||
| internal bool focus_visible { | ||
| get { return _focus_visible; } | ||
| set { | ||
| _focus_visible = value; | ||
| (get_stage ().key_focus as Widget)?.focus_changed (); | ||
| } | ||
| } | ||
|
|
||
| private uint timeout_id = 0; | ||
|
|
||
| public override void map () { | ||
| base.map (); | ||
| // In the case the key focus moves out of our widget tree by some other means | ||
| // make sure we can recapture it | ||
| get_stage ().key_press_event.connect (check_focus); | ||
| } | ||
|
|
||
| public override void unmap () { | ||
| base.unmap (); | ||
| get_stage ().key_press_event.disconnect (check_focus); | ||
| } | ||
|
|
||
| public override bool key_press_event (Clutter.Event event) { | ||
| return check_focus (event); | ||
| } | ||
|
|
||
| private bool check_focus (Clutter.Event event) { | ||
| var direction = FocusDirection.get_for_event (event); | ||
|
|
||
| if (direction == null) { | ||
| return Clutter.EVENT_PROPAGATE; | ||
| } | ||
|
|
||
| if (!focus (direction)) { | ||
| #if HAS_MUTTER47 | ||
| get_stage ().context.get_backend ().get_default_seat ().bell_notify (); | ||
| #else | ||
| Clutter.get_default_backend ().get_default_seat ().bell_notify (); | ||
| #endif | ||
|
|
||
| if (!(get_stage ().key_focus in this)) { | ||
| get_stage ().key_focus = this; | ||
| } | ||
| } | ||
|
|
||
| show_focus (); | ||
|
|
||
| return Clutter.EVENT_STOP; | ||
| } | ||
|
|
||
| private void show_focus () { | ||
| if (timeout_id != 0) { | ||
| Source.remove (timeout_id); | ||
| } else { | ||
| focus_visible = true; | ||
| } | ||
|
|
||
| timeout_id = Timeout.add_seconds (5, () => { | ||
| focus_visible = false; | ||
| timeout_id = 0; | ||
| return Source.REMOVE; | ||
| }); | ||
| } | ||
| } |
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,107 @@ | ||
| /* | ||
| * 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 ()?.focus_visible ?? false); | ||
| } | ||
|
|
||
| private Root? get_root () { | ||
| for (Clutter.Actor? actor = this; actor is Widget; actor = actor.get_parent ()) { | ||
| if (actor is Root) { | ||
| return (Root) actor; | ||
| } | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| public bool focus (FocusDirection direction) { | ||
| var focus_actor = get_stage ().get_key_focus (); | ||
|
lenemter marked this conversation as resolved.
|
||
|
|
||
| // 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 (); | ||
| } | ||
|
lenemter marked this conversation as resolved.
|
||
| } | ||
|
|
||
| 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.
Uh oh!
There was an error while loading. Please reload this page.