diff --git a/protocol/pantheon-desktop-shell-v1.xml b/protocol/pantheon-desktop-shell-v1.xml
index fcd0f1758..f9d5af0cd 100644
--- a/protocol/pantheon-desktop-shell-v1.xml
+++ b/protocol/pantheon-desktop-shell-v1.xml
@@ -151,5 +151,13 @@
by the compositor.
+
+
+
+ This will block all user input outside the surface and most system shortcuts.
+
+
+
+
diff --git a/protocol/pantheon-desktop-shell.vapi b/protocol/pantheon-desktop-shell.vapi
index 778d1ca42..7b27752c6 100644
--- a/protocol/pantheon-desktop-shell.vapi
+++ b/protocol/pantheon-desktop-shell.vapi
@@ -61,6 +61,7 @@ namespace Pantheon.Desktop {
public SetKeepAbove set_keep_above;
public MakeCentered make_centered;
public Focus focus;
+ public MakeModal make_modal;
}
[CCode (has_target = false, has_typedef = false)]
@@ -88,5 +89,7 @@ namespace Pantheon.Desktop {
[CCode (has_target = false, has_typedef = false)]
public delegate void MakeCentered (Wl.Client client, Wl.Resource resource);
[CCode (has_target = false, has_typedef = false)]
+ public delegate void MakeModal (Wl.Client client, Wl.Resource resource, uint dim);
+ [CCode (has_target = false, has_typedef = false)]
public delegate void Destroy (Wl.Client client, Wl.Resource resource);
}
diff --git a/src/PantheonShell.vala b/src/PantheonShell.vala
index 936f6eb57..97202cfcb 100644
--- a/src/PantheonShell.vala
+++ b/src/PantheonShell.vala
@@ -53,6 +53,7 @@ namespace Gala {
set_keep_above,
make_centered,
focus_extended_behavior,
+ make_modal,
};
PanelSurface.quark = GLib.Quark.from_string ("-gala-wayland-panel-surface-data");
@@ -376,6 +377,21 @@ namespace Gala {
ShellClientsManager.get_instance ().make_centered (window);
}
+ internal static void make_modal (Wl.Client client, Wl.Resource resource, uint dim) {
+ unowned ExtendedBehaviorSurface? eb_surface = resource.get_user_data ();
+ if (eb_surface.wayland_surface == null) {
+ return;
+ }
+
+ Meta.Window? window;
+ eb_surface.wayland_surface.get ("window", out window, null);
+ if (window == null) {
+ return;
+ }
+
+ ShellClientsManager.get_instance ().make_modal (window, dim == 1);
+ }
+
internal static void destroy_panel_surface (Wl.Client client, Wl.Resource resource) {
resource.destroy ();
}
diff --git a/src/ShellClients/ExtendedBehaviorWindow.vala b/src/ShellClients/ExtendedBehaviorWindow.vala
index 9a28da2f2..0e83b7fac 100644
--- a/src/ShellClients/ExtendedBehaviorWindow.vala
+++ b/src/ShellClients/ExtendedBehaviorWindow.vala
@@ -6,11 +6,19 @@
*/
public class Gala.ExtendedBehaviorWindow : ShellWindow {
+ public bool modal { get; private set; default = false; }
+ public bool dim { get; private set; default = false; }
+
public ExtendedBehaviorWindow (Meta.Window window) {
var target = new PropertyTarget (CUSTOM, window.get_compositor_private (), "opacity", typeof (uint), 255u, 0u);
Object (window: window, hide_target: target);
}
+ public void make_modal (bool dim) {
+ modal = true;
+ this.dim = dim;
+ }
+
protected override void get_window_position (Mtk.Rectangle window_rect, out int x, out int y) {
var monitor_rect = window.display.get_monitor_geometry (window.get_monitor ());
diff --git a/src/ShellClients/ShellClientsManager.vala b/src/ShellClients/ShellClientsManager.vala
index 988ba69dd..e8a9537df 100644
--- a/src/ShellClients/ShellClientsManager.vala
+++ b/src/ShellClients/ShellClientsManager.vala
@@ -28,7 +28,7 @@ public class Gala.ShellClientsManager : Object, GestureTarget {
private int starting_panels = 0;
private GLib.HashTable panel_windows = new GLib.HashTable (null, null);
- private GLib.HashTable positioned_windows = new GLib.HashTable (null, null);
+ private GLib.HashTable positioned_windows = new GLib.HashTable (null, null);
private ShellClientsManager (WindowManager wm) {
Object (wm: wm);
@@ -240,6 +240,10 @@ public class Gala.ShellClientsManager : Object, GestureTarget {
window.unmanaging.connect_after ((_window) => positioned_windows.remove (_window));
}
+ public void make_modal (Meta.Window window, bool dim) requires (window in positioned_windows) {
+ positioned_windows[window].make_modal (dim);
+ }
+
public void propagate (UpdateType update_type, GestureAction action, double progress) {
foreach (var window in positioned_windows.get_values ()) {
window.propagate (update_type, action, progress);
@@ -267,6 +271,27 @@ public class Gala.ShellClientsManager : Object, GestureTarget {
return positioned;
}
+ private bool is_itself_system_modal (Meta.Window window) {
+ return (window in positioned_windows) && positioned_windows[window].modal;
+ }
+
+ public bool is_system_modal_window (Meta.Window window) {
+ var modal = is_itself_system_modal (window);
+ window.foreach_ancestor ((ancestor) => {
+ if (is_itself_system_modal (ancestor)) {
+ modal = true;
+ }
+
+ return !modal;
+ });
+
+ return modal;
+ }
+
+ public bool is_system_modal_dimmed (Meta.Window window) {
+ return is_itself_system_modal (window) && positioned_windows[window].dim;
+ }
+
//X11 only
private void parse_mutter_hints (Meta.Window window) requires (!Meta.Util.is_wayland_compositor ()) {
if (window.mutter_hints == null) {
diff --git a/src/Widgets/ModalGroup.vala b/src/Widgets/ModalGroup.vala
new file mode 100644
index 000000000..cabf80cd7
--- /dev/null
+++ b/src/Widgets/ModalGroup.vala
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2025 elementary, Inc. (https://elementary.io)
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * Authored by: Leonhard Kargl
+ */
+
+/**
+ * This class allows to make windows system modal i.e. dim
+ * the desktop behind them and only allow interaction with them.
+ * Not to be confused with WindowManager.push_modal which only
+ * works for our own Clutter.Actors.
+ */
+public class Gala.ModalGroup : Clutter.Actor {
+ public WindowManager wm { private get; construct; }
+ public ShellClientsManager shell_clients { private get; construct; }
+
+ private Gee.Set dimmed;
+ private ModalProxy? modal_proxy = null;
+
+ public ModalGroup (WindowManager wm, ShellClientsManager shell_clients) {
+ Object (wm: wm, shell_clients: shell_clients);
+ }
+
+ construct {
+ dimmed = new Gee.HashSet ();
+
+ visible = false;
+ reactive = true;
+#if HAS_MUTTER46
+ child_added.connect (on_child_added);
+ child_removed.connect (on_child_removed);
+#else
+ actor_added.connect (on_child_added);
+ actor_removed.connect (on_child_removed);
+#endif
+ }
+
+ private void on_child_added (Clutter.Actor child) {
+ if (child is Meta.WindowActor && shell_clients.is_system_modal_dimmed (child.meta_window)) {
+ dimmed.add (child);
+ }
+
+ if (get_n_children () == 1) {
+ assert (modal_proxy == null);
+
+ visible = true;
+ modal_proxy = wm.push_modal (this, false);
+ }
+
+ if (dimmed.size == 1) {
+ save_easing_state ();
+ set_easing_duration (Utils.get_animation_duration (AnimationDuration.OPEN));
+ background_color = { 0, 0, 0, 200 };
+ restore_easing_state ();
+ }
+ }
+
+ private void on_child_removed (Clutter.Actor child) {
+ dimmed.remove (child);
+
+ if (dimmed.size == 0) {
+ save_easing_state ();
+ set_easing_duration (Utils.get_animation_duration (AnimationDuration.CLOSE));
+ background_color = { 0, 0, 0, 0 };
+ restore_easing_state ();
+ }
+
+ if (get_n_children () == 0) {
+ wm.pop_modal (modal_proxy);
+ modal_proxy = null;
+
+ var transition = get_transition ("background-color");
+ if (transition != null) {
+ transition.completed.connect (() => visible = false);
+ } else {
+ visible = false;
+ }
+ }
+ }
+}
diff --git a/src/WindowManager.vala b/src/WindowManager.vala
index f8b2ec257..4d0cc33d2 100644
--- a/src/WindowManager.vala
+++ b/src/WindowManager.vala
@@ -51,6 +51,12 @@ namespace Gala {
private Clutter.Actor menu_group { get; set; }
+ /**
+ * The group that contains all WindowActors that are system modal.
+ * See {@link ShellClientsManager.is_system_modal_window}.
+ */
+ public ModalGroup modal_group { get; private set; }
+
/**
* {@inheritDoc}
*/
@@ -224,6 +230,7 @@ namespace Gala {
* +-- window overview
* +-- shell group
* +-- menu group
+ * +-- modal group
* +-- feedback group (e.g. DND icons)
* +-- pointer locator
* +-- dwell click timer
@@ -295,6 +302,10 @@ namespace Gala {
menu_group = new Clutter.Actor ();
ui_group.add_child (menu_group);
+ modal_group = new ModalGroup (this, ShellClientsManager.get_instance ());
+ modal_group.add_constraint (new Clutter.BindConstraint (stage, ALL, 0));
+ ui_group.add_child (modal_group);
+
var feedback_group = display.get_compositor ().get_feedback_group ();
stage.remove_child (feedback_group);
ui_group.add_child (feedback_group);
@@ -1006,6 +1017,12 @@ namespace Gala {
private void check_shell_window (Meta.WindowActor actor) {
unowned var window = actor.get_meta_window ();
+
+ if (ShellClientsManager.get_instance ().is_system_modal_window (window)) {
+ InternalUtils.clutter_actor_reparent (actor, modal_group);
+ return;
+ }
+
if (ShellClientsManager.get_instance ().is_positioned_window (window)) {
InternalUtils.clutter_actor_reparent (actor, shell_group);
}
diff --git a/src/meson.build b/src/meson.build
index d0503f355..cce4aaec7 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -60,6 +60,7 @@ gala_bin_sources = files(
'Widgets/MultitaskingView/WindowCloneContainer.vala',
'Widgets/MultitaskingView/WorkspaceClone.vala',
'Widgets/MultitaskingView/WorkspaceRow.vala',
+ 'Widgets/ModalGroup.vala',
'Widgets/PixelPicker.vala',
'Widgets/PointerLocator.vala',
'Widgets/SessionLocker.vala',