Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions lib/Widget/FocusController.vala
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);
Copy link
Member

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

(stage.key_focus as Widget)?.focus_changed ();
}
}
83 changes: 83 additions & 0 deletions lib/Widget/FocusUtils.vala
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;
});
}
}
106 changes: 106 additions & 0 deletions lib/Widget/Widget.vala
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;
}
}
5 changes: 4 additions & 1 deletion lib/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ gala_lib_sources = files(
'Gestures/SwipeTrigger.vala',
'Gestures/ToucheggBackend.vala',
'Gestures/TouchpadBackend.vala',
'Gestures/WorkspaceHideTracker.vala'
'Gestures/WorkspaceHideTracker.vala',
'Widget/FocusController.vala',
'Widget/FocusUtils.vala',
'Widget/Widget.vala',
) + gala_common_enums

gala_resources = gnome.compile_resources(
Expand Down
2 changes: 1 addition & 1 deletion src/Widgets/MultitaskingView/MonitorClone.vala
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* as the WindowGroup is hidden while the view is active. Only used when
* workspaces-only-on-primary is set to true.
*/
public class Gala.MonitorClone : ActorTarget {
public class Gala.MonitorClone : Widget {
public signal void window_selected (Meta.Window window);

public WindowManager wm { get; construct; }
Expand Down
43 changes: 13 additions & 30 deletions src/Widgets/MultitaskingView/MultitaskingView.vala
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
* preparing the wm, opening the components and holds containers for
* the icon groups, the WorkspaceClones and the MonitorClones.
*/
public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableComponent {
public class Gala.MultitaskingView : Widget, RootTarget, ActivatableComponent {
public const int ANIMATION_DURATION = 250;

private GestureController workspaces_gesture_controller;
Expand All @@ -35,7 +35,7 @@ public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableCompone

private List<MonitorClone> window_containers_monitors;

private ActorTarget workspaces;
private Widget workspaces;
private Clutter.Actor primary_monitor_container;
private Clutter.BrightnessContrastEffect brightness_effect;
private BackgroundManager? blurred_bg = null;
Expand All @@ -59,6 +59,8 @@ public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableCompone
opened = false;
display = wm.get_display ();

add_action (new FocusController (wm.stage));

multitasking_gesture_controller = new GestureController (MULTITASKING_VIEW);
multitasking_gesture_controller.add_trigger (new GlobalTrigger (MULTITASKING_VIEW, wm));
add_gesture_controller (multitasking_gesture_controller);
Expand All @@ -82,7 +84,7 @@ public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableCompone
// Create a child container that will be sized to fit the primary monitor, to contain the "main"
// multitasking view UI. The Clutter.Actor of this class has to be allowed to grow to the size of the
// stage as it contains MonitorClones for each monitor.
primary_monitor_container = new ActorTarget ();
primary_monitor_container = new Widget ();
primary_monitor_container.add_child (workspaces);
add_child (primary_monitor_container);

Expand Down Expand Up @@ -249,7 +251,6 @@ public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableCompone
wm.window_group.hide ();
wm.top_window_group.hide ();
show ();
grab_key_focus ();

modal_proxy = wm.push_modal (get_stage (), false);
modal_proxy.allow_actions (MULTITASKING_VIEW | SWITCH_WORKSPACE | ZOOM | LOCATE_POINTER | MEDIA_KEYS | SCREENSHOT | SCREENSHOT_AREA);
Expand Down Expand Up @@ -370,34 +371,16 @@ public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableCompone
}
}

/**
* Collect key events, mainly for redirecting them to the WindowCloneContainers to
* select the active window.
*/
public override bool key_press_event (Clutter.Event event) {
if (!opened) {
return Clutter.EVENT_PROPAGATE;
}

return get_active_window_clone_container ().key_press_event (event);
}

/**
* Finds the active WorkspaceClone
*
* @return The active WorkspaceClone
*/
private WindowCloneContainer get_active_window_clone_container () {
unowned var manager = display.get_workspace_manager ();
unowned var active_workspace = manager.get_active_workspace ();
foreach (unowned var child in workspaces.get_children ()) {
unowned var workspace_clone = (WorkspaceClone) child;
if (workspace_clone.workspace == active_workspace) {
return workspace_clone.window_container;
}
switch (event.get_key_symbol ()) {
case Clutter.Key.Escape:
case Clutter.Key.Return:
case Clutter.Key.KP_Enter:
close ();
return Clutter.EVENT_STOP;
default:
return Clutter.EVENT_PROPAGATE;
}

assert_not_reached ();
}

private void window_selected (Meta.Window window) {
Expand Down
2 changes: 1 addition & 1 deletion src/Widgets/MultitaskingView/StaticWindowClone.vala
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* windows (e.g. on all workspaces or moving) and fades out while the multitasking view
* is being opened.
*/
public class Gala.StaticWindowClone : ActorTarget {
public class Gala.StaticWindowClone : Widget {
public Meta.Window window { get; construct; }

public StaticWindowClone (Meta.Window window) {
Expand Down
2 changes: 1 addition & 1 deletion src/Widgets/MultitaskingView/StaticWindowContainer.vala
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* The window container use this to know whether a window became static (they shouldn't show it anymore)
* or isn't static anymore (they have to show it now).
*/
public class Gala.StaticWindowContainer : ActorTarget {
public class Gala.StaticWindowContainer : Widget {
private static GLib.Once<StaticWindowContainer> instance;
public static StaticWindowContainer get_instance (Meta.Display display) {
return instance.once (() => new StaticWindowContainer (display));
Expand Down
Loading