Skip to content

Commit bbbe951

Browse files
committed
Working version
1 parent 5929601 commit bbbe951

File tree

9 files changed

+232
-285
lines changed

9 files changed

+232
-285
lines changed

lib/FocusController.vala

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2025 elementary, Inc. (https://elementary.io)
3+
* SPDX-License-Identifier: GPL-3.0-or-later
4+
*
5+
* Authored by: Leonhard Kargl <[email protected]>
6+
*/
7+
8+
public class Gala.FocusController : Object {
9+
public static FocusController get_default (Clutter.Stage stage) {
10+
return instance.once (() => new FocusController (stage));
11+
}
12+
13+
public Clutter.Stage stage { get; construct; }
14+
15+
private bool _focus_visible = false;
16+
public bool focus_visible {
17+
get { return _focus_visible; }
18+
private set {
19+
_focus_visible = value;
20+
if (stage.get_key_focus () is Focusable) {
21+
((Focusable) stage.get_key_focus ()).notify_visible_focus_changed ();
22+
}
23+
}
24+
}
25+
26+
private static GLib.Once<FocusController> instance;
27+
28+
private Gee.List<unowned Focusable> root_focusables;
29+
30+
private uint timeout_id = 0;
31+
32+
public FocusController (Clutter.Stage stage) {
33+
Object (stage: stage);
34+
}
35+
36+
construct {
37+
root_focusables = new Gee.LinkedList<unowned Focusable> ();
38+
stage.key_press_event.connect (on_key_press_event);
39+
}
40+
41+
public void register_root (Focusable root) {
42+
if (root in root_focusables) {
43+
warning ("Trying to register root focusable multiple times.");
44+
return;
45+
}
46+
47+
root_focusables.add (root);
48+
root.weak_ref ((obj) => root_focusables.remove ((Focusable) obj));
49+
}
50+
51+
private bool on_key_press_event (Clutter.Event event) {
52+
if (handle_key_event (event) == Clutter.EVENT_STOP) {
53+
show_focus ();
54+
return Clutter.EVENT_STOP;
55+
}
56+
57+
return Clutter.EVENT_PROPAGATE;
58+
}
59+
60+
private bool handle_key_event (Clutter.Event event) {
61+
Focusable? mapped_root = null;
62+
foreach (var root_focusable in root_focusables) {
63+
if (root_focusable.mapped) {
64+
mapped_root = root_focusable;
65+
break;
66+
}
67+
}
68+
69+
var direction = Focusable.FocusDirection.get_for_event (event);
70+
71+
if (mapped_root == null || direction == null) {
72+
return Clutter.EVENT_PROPAGATE;
73+
}
74+
75+
if(!mapped_root.focus (direction)) {
76+
#if HAS_MUTTER47
77+
stage.context.get_backend ().get_default_seat ().bell_notify ();
78+
#else
79+
Clutter.get_default_backend ().get_default_seat ().bell_notify ();
80+
#endif
81+
}
82+
83+
return Clutter.EVENT_STOP;
84+
}
85+
86+
private void show_focus () {
87+
if (timeout_id != 0) {
88+
Source.remove (timeout_id);
89+
} else {
90+
focus_visible = true;
91+
}
92+
93+
timeout_id = Timeout.add_seconds (5, () => {
94+
focus_visible = false;
95+
timeout_id = 0;
96+
return Source.REMOVE;
97+
});
98+
}
99+
}

lib/Focusable.vala

Lines changed: 76 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* Authored by: Leonhard Kargl <[email protected]>
66
*/
77

8-
public interface Gala.Focusable : Clutter.Actor {
8+
public interface Gala.Focusable : Clutter.Actor{
99
public enum FocusDirection {
1010
UP,
1111
DOWN,
@@ -17,6 +17,23 @@ public interface Gala.Focusable : Clutter.Actor {
1717
public bool is_forward () {
1818
return this == DOWN || this == RIGHT || this == NEXT;
1919
}
20+
21+
public static FocusDirection? get_for_event (Clutter.Event event) {
22+
switch (event.get_key_symbol ()) {
23+
case Clutter.Key.Up: return UP;
24+
case Clutter.Key.Down: return DOWN;
25+
case Clutter.Key.Left: return LEFT;
26+
case Clutter.Key.Right: return RIGHT;
27+
case Clutter.Key.Tab:
28+
if (SHIFT_MASK in event.get_state ()) {
29+
return PREVIOUS;
30+
} else {
31+
return NEXT;
32+
}
33+
}
34+
35+
return null;
36+
}
2037
}
2138

2239
public bool focus (FocusDirection direction) {
@@ -62,6 +79,46 @@ public interface Gala.Focusable : Clutter.Actor {
6279
}
6380

6481
protected virtual bool move_focus (FocusDirection direction) {
82+
var children = get_focusable_children ();
83+
84+
filter_children_for_direction (children, direction);
85+
86+
switch (direction) {
87+
case NEXT:
88+
sort_children_for_direction (children, DOWN);
89+
sort_children_for_direction (children, RIGHT);
90+
break;
91+
92+
case PREVIOUS:
93+
sort_children_for_direction (children, UP);
94+
sort_children_for_direction (children, LEFT);
95+
break;
96+
97+
default:
98+
sort_children_for_direction (children, direction);
99+
break;
100+
}
101+
102+
foreach (var child in children) {
103+
if (child.focus (direction)) {
104+
return true;
105+
}
106+
}
107+
108+
return false;
109+
}
110+
111+
private Gee.List<Focusable> get_focusable_children () {
112+
var focusable_children = new Gee.ArrayList<Focusable> ();
113+
for (var child = get_first_child (); child != null; child = child.get_next_sibling ()) {
114+
if (child is Focusable) {
115+
focusable_children.add ((Focusable) child);
116+
}
117+
}
118+
return focusable_children;
119+
}
120+
121+
private void filter_children_for_direction (Gee.List<Focusable> children, FocusDirection direction) {
65122
var focus_actor = get_stage ().get_key_focus ();
66123

67124
Focusable? focus_child = null;
@@ -74,9 +131,9 @@ public interface Gala.Focusable : Clutter.Actor {
74131
}
75132
}
76133

77-
var possible_children = new Gee.ArrayList<Focusable> ();
78-
possible_children.add_all_iterator (get_focusable_children ().filter ((c) => {
79-
if (focus_child == null || c == focus_child) {
134+
var to_retain = new Gee.LinkedList<Focusable> ();
135+
to_retain.add_all_iterator (children.filter ((c) => {
136+
if (focus_child == null || c == focus_child || direction == NEXT || direction == PREVIOUS) {
80137
return true;
81138
}
82139

@@ -97,7 +154,15 @@ public interface Gala.Focusable : Clutter.Actor {
97154
);
98155
}));
99156

100-
possible_children.sort ((a, b) => {
157+
children.retain_all (to_retain);
158+
}
159+
160+
private inline Mtk.Rectangle get_allocation_rect (Clutter.Actor actor) {
161+
return {(int) actor.x, (int) actor.y, (int) actor.width, (int) actor.height};
162+
}
163+
164+
private void sort_children_for_direction (Gee.List<Focusable> children, FocusDirection direction) {
165+
children.sort ((a, b) => {
101166
if (direction == UP && a.y + a.height > b.y + b.height ||
102167
direction == DOWN && a.y < b.y ||
103168
direction == LEFT && a.x + a.width > b.x + b.width ||
@@ -108,28 +173,6 @@ public interface Gala.Focusable : Clutter.Actor {
108173

109174
return 1;
110175
});
111-
112-
foreach (var child in possible_children) {
113-
if (child.focus (direction)) {
114-
return true;
115-
}
116-
}
117-
118-
return false;
119-
}
120-
121-
private Mtk.Rectangle get_allocation_rect (Clutter.Actor actor) {
122-
return {(int) actor.x, (int) actor.y, (int) actor.width, (int) actor.height};
123-
}
124-
125-
private Gee.List<Focusable> get_focusable_children () {
126-
var focusable_children = new Gee.ArrayList<Focusable> ();
127-
for (var child = get_first_child (); child != null; child = child.get_next_sibling ()) {
128-
if (child is Focusable) {
129-
focusable_children.add ((Focusable) child);
130-
}
131-
}
132-
return focusable_children;
133176
}
134177

135178
private bool grab_focus () {
@@ -138,6 +181,8 @@ public interface Gala.Focusable : Clutter.Actor {
138181
}
139182

140183
get_stage ().set_key_focus (this);
184+
notify_visible_focus_changed ();
185+
key_focus_out.connect (notify_visible_focus_changed);
141186

142187
return true;
143188
}
@@ -146,37 +191,10 @@ public interface Gala.Focusable : Clutter.Actor {
146191
return false;
147192
}
148193

149-
public void mark_root (Clutter.Stage stage) {
150-
stage.key_press_event.connect (on_key_press_event);
194+
internal void notify_visible_focus_changed () {
195+
var stage = get_stage ();
196+
update_focus (stage?.get_key_focus () == this && FocusController.get_default (stage).focus_visible);
151197
}
152198

153-
private bool on_key_press_event (Clutter.Event event) {
154-
if (!mapped) {
155-
return Clutter.EVENT_PROPAGATE;
156-
}
157-
158-
switch (event.get_key_symbol ()) {
159-
case Clutter.Key.Tab:
160-
if (SHIFT_MASK in event.get_state ()) {
161-
focus (PREVIOUS);
162-
} else {
163-
focus (NEXT);
164-
}
165-
return Clutter.EVENT_STOP;
166-
case Clutter.Key.Up:
167-
focus (UP);
168-
return Clutter.EVENT_STOP;
169-
case Clutter.Key.Left:
170-
focus (LEFT);
171-
return Clutter.EVENT_STOP;
172-
case Clutter.Key.Down:
173-
focus (DOWN);
174-
return Clutter.EVENT_STOP;
175-
case Clutter.Key.Right:
176-
focus (RIGHT);
177-
return Clutter.EVENT_STOP;
178-
default:
179-
return Clutter.EVENT_PROPAGATE;
180-
}
181-
}
199+
protected virtual void update_focus (bool has_visible_focus) { }
182200
}

lib/meson.build

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ gala_lib_sources = files(
1313
'Drawing/Color.vala',
1414
'Drawing/StyleManager.vala',
1515
'Drawing/Utilities.vala',
16+
'Focusable.vala',
17+
'FocusController.vala',
1618
'Image.vala',
1719
'Plugin.vala',
1820
'RoundedCornersEffect.vala',

src/Widgets/MultitaskingView/MonitorClone.vala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
* as the WindowGroup is hidden while the view is active. Only used when
1212
* workspaces-only-on-primary is set to true.
1313
*/
14-
public class Gala.MonitorClone : ActorTarget {
14+
public class Gala.MonitorClone : ActorTarget, Focusable {
1515
public signal void window_selected (Meta.Window window);
1616

1717
public WindowManager wm { get; construct; }

src/Widgets/MultitaskingView/MultitaskingView.vala

Lines changed: 13 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@
1515
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1616
//
1717

18+
public class Gala.PrimaryMonitorContainer : ActorTarget, Focusable { }
19+
1820
/**
1921
* The central class for the MultitaskingView which takes care of
2022
* preparing the wm, opening the components and holds containers for
2123
* the icon groups, the WorkspaceClones and the MonitorClones.
2224
*/
23-
public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableComponent {
25+
public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableComponent, Focusable {
2426
public const int ANIMATION_DURATION = 250;
2527

2628
private GestureController workspaces_gesture_controller;
@@ -54,6 +56,8 @@ public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableCompone
5456
reactive = true;
5557
clip_to_allocation = true;
5658

59+
FocusController.get_default (wm.stage).register_root (this);
60+
5761
opened = false;
5862
display = wm.get_display ();
5963

@@ -78,7 +82,7 @@ public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableCompone
7882
// Create a child container that will be sized to fit the primary monitor, to contain the "main"
7983
// multitasking view UI. The Clutter.Actor of this class has to be allowed to grow to the size of the
8084
// stage as it contains MonitorClones for each monitor.
81-
primary_monitor_container = new ActorTarget ();
85+
primary_monitor_container = new PrimaryMonitorContainer ();
8286
primary_monitor_container.add_child (workspaces);
8387
add_child (primary_monitor_container);
8488

@@ -107,6 +111,8 @@ public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableCompone
107111
});
108112

109113
style_manager.notify["prefers-color-scheme"].connect (update_brightness_effect);
114+
115+
wm.stage.key_press_event.connect (on_stage_key_press_event);
110116
}
111117

112118
/**
@@ -245,7 +251,6 @@ public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableCompone
245251
wm.window_group.hide ();
246252
wm.top_window_group.hide ();
247253
show ();
248-
grab_key_focus ();
249254

250255
modal_proxy = wm.push_modal (get_stage (), false);
251256
modal_proxy.set_keybinding_filter (keybinding_filter);
@@ -369,34 +374,12 @@ public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableCompone
369374
}
370375
}
371376

372-
/**
373-
* Collect key events, mainly for redirecting them to the WindowCloneContainers to
374-
* select the active window.
375-
*/
376-
public override bool key_press_event (Clutter.Event event) {
377-
if (!opened) {
378-
return Clutter.EVENT_PROPAGATE;
379-
}
380-
381-
return get_active_window_clone_container ().key_press_event (event);
382-
}
383-
384-
/**
385-
* Finds the active WorkspaceClone
386-
*
387-
* @return The active WorkspaceClone
388-
*/
389-
private WindowCloneContainer get_active_window_clone_container () {
390-
unowned var manager = display.get_workspace_manager ();
391-
unowned var active_workspace = manager.get_active_workspace ();
392-
foreach (unowned var child in workspaces.get_children ()) {
393-
unowned var workspace_clone = (WorkspaceClone) child;
394-
if (workspace_clone.workspace == active_workspace) {
395-
return workspace_clone.window_container;
396-
}
377+
private bool on_stage_key_press_event (Clutter.Event event) {
378+
if (opened && event.get_key_symbol () == Clutter.Key.Escape) {
379+
close ();
380+
return Clutter.EVENT_STOP;
397381
}
398-
399-
assert_not_reached ();
382+
return Clutter.EVENT_PROPAGATE;
400383
}
401384

402385
private void window_selected (Meta.Window window) {

0 commit comments

Comments
 (0)