Skip to content

Commit 31b7534

Browse files
authored
Merge branch 'main' into lenemter/move-focus
2 parents 658783a + bdeace6 commit 31b7534

9 files changed

Lines changed: 335 additions & 42 deletions

File tree

compositor/NotificationStack.vala

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
/*
2+
* Copyright 2020-2025 elementary, Inc (https://elementary.io)
3+
* 2014 Tom Beckmann
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
public class GreeterCompositor.NotificationStack : Object {
20+
private const string TRANSITION_ENTRY_NAME = "entry";
21+
private const int CLOSE_ANIMATION_DURATION = 195;
22+
23+
// we need to keep a small offset to the top, because we clip the container to
24+
// its allocations and the close button would be off for the first notification
25+
private const int TOP_OFFSET = 2;
26+
private const int ADDITIONAL_MARGIN = 12;
27+
private const int MARGIN = 12;
28+
29+
private const int WIDTH = 300;
30+
31+
private int stack_y;
32+
private int stack_width;
33+
34+
public Meta.Display display { get; construct; }
35+
36+
private Gee.ArrayList<unowned Meta.WindowActor> notifications;
37+
38+
public NotificationStack (Meta.Display display) {
39+
Object (display: display);
40+
}
41+
42+
construct {
43+
notifications = new Gee.ArrayList<unowned Meta.WindowActor> ();
44+
45+
unowned var monitor_manager = display.get_context ().get_backend ().get_monitor_manager ();
46+
monitor_manager.monitors_changed_internal.connect (update_stack_allocation);
47+
display.workareas_changed.connect (update_stack_allocation);
48+
update_stack_allocation ();
49+
}
50+
51+
public void show_notification (Meta.WindowActor notification)
52+
requires (notification != null && !notification.is_destroyed () && !notifications.contains (notification)) {
53+
54+
notification.set_pivot_point (0.5f, 0.5f);
55+
56+
unowned var window = notification.get_meta_window ();
57+
if (window == null) {
58+
warning ("NotificationStack: Unable to show notification, window is null");
59+
return;
60+
}
61+
62+
var window_rect = window.get_frame_rect ();
63+
window.stick ();
64+
65+
if (Meta.Prefs.get_gnome_animations ()) {
66+
// Don't flicker at the beginning of the animation
67+
notification.opacity = 0;
68+
notification.rotation_angle_x = 90;
69+
70+
var opacity_transition = new Clutter.PropertyTransition ("opacity");
71+
opacity_transition.set_from_value (0);
72+
opacity_transition.set_to_value (255);
73+
74+
var flip_transition = new Clutter.KeyframeTransition ("rotation-angle-x");
75+
flip_transition.set_from_value (90.0);
76+
flip_transition.set_to_value (0.0);
77+
flip_transition.set_key_frames ({ 0.6 });
78+
flip_transition.set_values ({ -10.0 });
79+
80+
var entry = new Clutter.TransitionGroup () {
81+
duration = 400
82+
};
83+
entry.add_transition (opacity_transition);
84+
entry.add_transition (flip_transition);
85+
86+
notification.transitions_completed.connect (() => notification.remove_all_transitions ());
87+
notification.add_transition (TRANSITION_ENTRY_NAME, entry);
88+
}
89+
90+
/**
91+
* We will make space for the incoming notification
92+
* by shifting all current notifications by height
93+
* and then add it to the notifications list.
94+
*/
95+
update_positions (window_rect.height);
96+
97+
var primary = display.get_primary_monitor ();
98+
var area = display.get_workspace_manager ().get_active_workspace ().get_work_area_for_monitor (primary);
99+
var scale = display.get_monitor_scale (primary);
100+
101+
int notification_x_pos = area.x + area.width - window_rect.width;
102+
if (Clutter.get_default_text_direction () == Clutter.TextDirection.RTL) {
103+
notification_x_pos = 0;
104+
}
105+
106+
move_window (notification, notification_x_pos, stack_y + TOP_OFFSET + Utils.scale_to_int (ADDITIONAL_MARGIN, scale));
107+
notifications.insert (0, notification);
108+
}
109+
110+
private void update_stack_allocation () {
111+
var primary = display.get_primary_monitor ();
112+
var area = display.get_workspace_manager ().get_active_workspace ().get_work_area_for_monitor (primary);
113+
114+
var scale = display.get_monitor_scale (primary);
115+
stack_width = Utils.scale_to_int (WIDTH + MARGIN, scale);
116+
117+
stack_y = area.y;
118+
119+
update_positions ();
120+
}
121+
122+
private void update_positions (float add_y = 0.0f) {
123+
var scale = display.get_monitor_scale (display.get_primary_monitor ());
124+
125+
var y = stack_y + TOP_OFFSET + add_y + Utils.scale_to_int (ADDITIONAL_MARGIN, scale);
126+
var i = notifications.size;
127+
var delay_step = i > 0 ? 150 / i : 0;
128+
var iterator = 0;
129+
// Need to iterate like this since we might be removing entries
130+
while (notifications.size > iterator) {
131+
unowned var actor = notifications.get (iterator);
132+
iterator++;
133+
if (actor == null || actor.is_destroyed ()) {
134+
warning ("NotificationStack: Notification actor was null or destroyed");
135+
continue;
136+
}
137+
138+
if (Meta.Prefs.get_gnome_animations ()) {
139+
actor.save_easing_state ();
140+
actor.set_easing_mode (Clutter.AnimationMode.EASE_OUT_BACK);
141+
actor.set_easing_duration (200);
142+
actor.set_easing_delay ((i--) * delay_step);
143+
}
144+
145+
move_window (actor, -1, (int)y);
146+
147+
if (Meta.Prefs.get_gnome_animations ()) {
148+
actor.restore_easing_state ();
149+
}
150+
151+
unowned var window = actor.get_meta_window ();
152+
if (window == null) {
153+
// Mutter doesn't let us know when a window is closed if a workspace
154+
// transition is in progress. I'm not really sure why, but what this
155+
// means is that we have to remove the notification from the stack
156+
// manually.
157+
// See https://github.com/GNOME/mutter/blob/3.36.9/src/compositor/meta-window-actor.c#L882
158+
notifications.remove (actor);
159+
warning ("NotificationStack: Notification window was null (probably removed during workspace transition?)");
160+
continue;
161+
}
162+
163+
y += window.get_frame_rect ().height;
164+
}
165+
}
166+
167+
public void destroy_notification (Meta.WindowActor notification) {
168+
notification.save_easing_state ();
169+
notification.set_easing_duration (Utils.get_animation_duration (CLOSE_ANIMATION_DURATION));
170+
notification.set_easing_mode (Clutter.AnimationMode.EASE_IN_QUAD);
171+
notification.opacity = 0;
172+
173+
notification.x += stack_width;
174+
notification.restore_easing_state ();
175+
176+
notifications.remove (notification);
177+
update_positions ();
178+
}
179+
180+
/**
181+
* This function takes care of properly updating both the actor
182+
* position and the actual window position.
183+
*
184+
* To enable animations for a window we first need to move it's frame
185+
* in the compositor and then calculate & apply the coordinates for the window
186+
* actor.
187+
*/
188+
private static void move_window (Meta.WindowActor actor, int x, int y) requires (actor != null && !actor.is_destroyed ()) {
189+
unowned var window = actor.get_meta_window ();
190+
if (window == null) {
191+
warning ("NotificationStack: Unable to move the window, window is null");
192+
return;
193+
}
194+
195+
var rect = window.get_frame_rect ();
196+
197+
window.move_frame (false, x != -1 ? x : rect.x, y != -1 ? y : rect.y);
198+
199+
/**
200+
* move_frame does not guarantee that the frame rectangle
201+
* will be updated instantly, get the buffer rectangle.
202+
*/
203+
rect = window.get_buffer_rect ();
204+
actor.set_position (rect.x - ((actor.width - rect.width) / 2), rect.y - ((actor.height - rect.height) / 2));
205+
}
206+
207+
public static bool is_notification (Meta.Window window) {
208+
return window.window_type == NOTIFICATION || window.get_data (NOTIFICATION_DATA_KEY);
209+
}
210+
}

compositor/ShellClients/NotificationsClient.vala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2024 elementary, Inc. (https://elementary.io)
2+
* Copyright 2024-2025 elementary, Inc. (https://elementary.io)
33
* SPDX-License-Identifier: GPL-3.0-or-later
44
*
55
* Authored by: Leonhard Kargl <leo.kargl@proton.me>
@@ -26,6 +26,7 @@ public class GreeterCompositor.NotificationsClient : Object {
2626
client.window_created.connect ((window) => {
2727
window.set_data (NOTIFICATION_DATA_KEY, true);
2828
window.make_above ();
29+
window.stick ();
2930
#if HAS_MUTTER46
3031
client.wayland_client.make_dock (window);
3132
#endif

compositor/ShellClients/ShellClientsManager.vala

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2024 elementary, Inc. (https://elementary.io)
2+
* Copyright 2024-2025 elementary, Inc. (https://elementary.io)
33
* SPDX-License-Identifier: GPL-3.0-or-later
44
*
55
* Authored by: Leonhard Kargl <leo.kargl@proton.me>
@@ -178,7 +178,20 @@ public class GreeterCompositor.ShellClientsManager : Object {
178178
}
179179

180180
public bool is_itself_positioned (Meta.Window window) {
181-
return (window in positioned_windows) || (window in panel_windows) || window.get_data (NOTIFICATION_DATA_KEY);
181+
return (window in positioned_windows) || (window in panel_windows) || NotificationStack.is_notification (window);
182+
}
183+
184+
public bool is_positioned_window (Meta.Window window) {
185+
bool positioned = is_itself_positioned (window);
186+
window.foreach_ancestor ((ancestor) => {
187+
if (is_itself_positioned (ancestor)) {
188+
positioned = true;
189+
}
190+
191+
return !positioned;
192+
});
193+
194+
return positioned;
182195
}
183196

184197
//X11 only

compositor/Utils.vala

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
* Copyright 2012 Tom Beckmann, Rico Tzschichholz
3-
* Copyright 2018 elementary LLC. (https://elementary.io)
3+
* Copyright 2018-2025 elementary, Inc. (https://elementary.io)
44
*
55
* This program is free software: you can redistribute it and/or modify
66
* it under the terms of the GNU General Public License as published by
@@ -53,6 +53,58 @@ namespace GreeterCompositor {
5353
return (int) (Math.round ((float)value * scale_factor));
5454
}
5555

56+
/**
57+
* Utility that returns the given duration or 0 if animations are disabled.
58+
*/
59+
public static uint get_animation_duration (uint duration) {
60+
return Meta.Prefs.get_gnome_animations () ? duration : 0;
61+
}
62+
63+
public static void clutter_actor_reparent (Clutter.Actor actor, Clutter.Actor new_parent) {
64+
if (actor == new_parent) {
65+
return;
66+
}
67+
68+
actor.ref ();
69+
actor.get_parent ().remove_child (actor);
70+
new_parent.add_child (actor);
71+
actor.unref ();
72+
}
73+
74+
public delegate void WindowActorReadyCallback (Meta.WindowActor window_actor);
75+
76+
public static void wait_for_window_actor (Meta.Window window, owned WindowActorReadyCallback callback) {
77+
unowned var window_actor = (Meta.WindowActor) window.get_compositor_private ();
78+
if (window_actor != null) {
79+
callback (window_actor);
80+
return;
81+
}
82+
83+
Idle.add (() => {
84+
window_actor = (Meta.WindowActor) window.get_compositor_private ();
85+
86+
if (window_actor != null) {
87+
callback (window_actor);
88+
}
89+
90+
return Source.REMOVE;
91+
});
92+
}
93+
94+
public static void wait_for_window_actor_visible (Meta.Window window, owned WindowActorReadyCallback callback) {
95+
wait_for_window_actor (window, (window_actor) => {
96+
if (window_actor.visible) {
97+
callback (window_actor);
98+
} else {
99+
ulong show_handler = 0;
100+
show_handler = window_actor.show.connect (() => {
101+
window_actor.disconnect (show_handler);
102+
callback (window_actor);
103+
});
104+
}
105+
});
106+
}
107+
56108
private static Gtk.StyleContext selection_style_context = null;
57109
public static Gdk.RGBA get_theme_accent_color () {
58110
if (selection_style_context == null) {

compositor/WindowManager.vala

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,18 @@ namespace GreeterCompositor {
2525
public Clutter.Stage stage { get; protected set; }
2626
public Clutter.Actor window_group { get; protected set; }
2727
public Clutter.Actor top_window_group { get; protected set; }
28-
29-
/**
30-
* The background group is a container for the background actors forming the wallpaper
31-
*/
3228
public Meta.BackgroundGroup background_group { get; protected set; }
33-
3429
public PointerLocator pointer_locator { get; private set; }
35-
3630
public GreeterCompositor.SystemBackground system_background { get; private set; }
3731

32+
/**
33+
* The group that contains all WindowActors that make shell elements, that is all windows reported as
34+
* ShellClientsManager.is_positioned_window.
35+
* It will (eventually) never be hidden by other components and is always on top of everything. Therefore elements are
36+
* responsible themselves for hiding depending on the state we are currently in (e.g. normal desktop, open multitasking view, fullscreen, etc.).
37+
*/
38+
private Clutter.Actor shell_group;
39+
private NotificationStack notification_stack;
3840
private Clutter.Actor fade_in_screen;
3941

4042
#if !HAS_MUTTER48
@@ -89,6 +91,8 @@ namespace GreeterCompositor {
8991
DBusWingpanelManager.init (this);
9092
KeyboardManager.init (display);
9193

94+
notification_stack = new NotificationStack (display);
95+
9296
#if HAS_MUTTER48
9397
stage = display.get_compositor ().get_stage () as Clutter.Stage;
9498
#else
@@ -143,6 +147,10 @@ namespace GreeterCompositor {
143147
window_group.add_child (background_group);
144148
window_group.set_child_below_sibling (background_group, null);
145149

150+
// Add the remaining components that should be on top
151+
shell_group = new Clutter.Actor ();
152+
ui_group.add_child (shell_group);
153+
146154
pointer_locator = new PointerLocator (this);
147155
ui_group.add_child (pointer_locator);
148156

@@ -192,6 +200,10 @@ namespace GreeterCompositor {
192200
toggle_screen_reader (); // sync screen reader with gsettings key
193201
application_settings.changed["screen-reader-enabled"].connect (toggle_screen_reader);
194202

203+
display.window_created.connect ((window) =>
204+
Utils.wait_for_window_actor_visible (window, check_shell_window)
205+
);
206+
195207
stage.show ();
196208

197209
Idle.add (() => {
@@ -288,6 +300,17 @@ namespace GreeterCompositor {
288300
}
289301
}
290302

303+
private void check_shell_window (Meta.WindowActor actor) {
304+
unowned var window = actor.get_meta_window ();
305+
if (ShellClientsManager.get_instance ().is_positioned_window (window)) {
306+
Utils.clutter_actor_reparent (actor, shell_group);
307+
}
308+
309+
if (NotificationStack.is_notification (window)) {
310+
notification_stack.show_notification (actor);
311+
}
312+
}
313+
291314
public override void show_window_menu_for_rect (Meta.Window window, Meta.WindowMenuType menu, Mtk.Rectangle rect) {
292315
show_window_menu (window, menu, rect.x, rect.y);
293316
}

0 commit comments

Comments
 (0)