diff --git a/compositor/PantheonShell.vala b/compositor/PantheonShell.vala new file mode 100644 index 000000000..561cb08ba --- /dev/null +++ b/compositor/PantheonShell.vala @@ -0,0 +1,355 @@ +/* + * Copyright 2023 elementary, Inc. + * Copyright 2023 Corentin Noël + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +namespace GreeterCompositor { +#if !HAS_MUTTER45 + [Compact] + public class FakeMetaWaylandCompositor : GLib.Object { + // It is the third field and Vala adds a FakeMetaWaylandCompositorPrivate *priv + public Wl.Display wayland_display; + + [CCode (cname = "meta_context_get_wayland_compositor")] + public extern static unowned Gala.FakeMetaWaylandCompositor from_context (Meta.Context context); + } +#endif + public static inline unowned Wl.Display? get_display_from_context (Meta.Context context) { +#if HAS_MUTTER45 + unowned Meta.WaylandCompositor? compositor = context.get_wayland_compositor (); + if (compositor == null) { + return null; + } + + return (Wl.Display) compositor.get_wayland_display (); +#else + unowned FakeMetaWaylandCompositor compositor = Gala.FakeMetaWaylandCompositor.from_context (context); + if (compositor == null) { + return null; + } + + return compositor.wayland_display; +#endif + } + + private static Pantheon.Desktop.ShellInterface wayland_pantheon_shell_interface; + private static Pantheon.Desktop.PanelInterface wayland_pantheon_panel_interface; + private static Pantheon.Desktop.WidgetInterface wayland_pantheon_widget_interface; + private static Pantheon.Desktop.ExtendedBehaviorInterface wayland_pantheon_extended_behavior_interface; + private static Wl.Global shell_global; + + public void init_pantheon_shell (Meta.Context context) { + unowned Wl.Display? wl_disp = get_display_from_context (context); + if (wl_disp == null) { + debug ("Not running under Wayland, no Pantheon Shell protocol"); + return; + } + + wayland_pantheon_shell_interface = { + get_panel, + get_widget, + get_extended_behavior, + }; + + wayland_pantheon_panel_interface = { + destroy_panel_surface, + set_anchor, + focus_panel, + set_size, + }; + + wayland_pantheon_widget_interface = { + destroy_widget_surface, + }; + + wayland_pantheon_extended_behavior_interface = { + destroy_extended_behavior_surface, + set_keep_above, + make_centered, + focus_extended_behavior, + }; + + PanelSurface.quark = GLib.Quark.from_string ("-gala-wayland-panel-surface-data"); + WidgetSurface.quark = GLib.Quark.from_string ("-gala-wayland-widget-surface-data"); + ExtendedBehaviorSurface.quark = GLib.Quark.from_string ("-gala-wayland-extended-behavior-surface-data"); + + shell_global = Wl.Global.create (wl_disp, ref Pantheon.Desktop.ShellInterface.iface, 1, (client, version, id) => { + unowned var resource = client.create_resource (ref Pantheon.Desktop.ShellInterface.iface, (int) version, id); + resource.set_implementation (&wayland_pantheon_shell_interface, null, (res) => {}); + }); + } + + public class PanelSurface : GLib.Object { + public static GLib.Quark quark = 0; + public unowned GLib.Object? wayland_surface; + + public PanelSurface (GLib.Object wayland_surface) { + this.wayland_surface = wayland_surface; + } + + ~PanelSurface () { + if (wayland_surface != null) { + wayland_surface.steal_qdata (quark); + } + } + + public void on_wayland_surface_disposed () { + wayland_surface = null; + } + } + + public class WidgetSurface : GLib.Object { + public static GLib.Quark quark = 0; + public unowned GLib.Object? wayland_surface; + + public WidgetSurface (GLib.Object wayland_surface) { + this.wayland_surface = wayland_surface; + } + + ~WidgetSurface () { + if (wayland_surface != null) { + wayland_surface.steal_qdata (quark); + } + } + + public void on_wayland_surface_disposed () { + wayland_surface = null; + } + } + + public class ExtendedBehaviorSurface : GLib.Object { + public static GLib.Quark quark = 0; + public unowned GLib.Object? wayland_surface; + + public ExtendedBehaviorSurface (GLib.Object wayland_surface) { + this.wayland_surface = wayland_surface; + } + + ~ExtendedBehaviorSurface () { + if (wayland_surface != null) { + wayland_surface.steal_qdata (quark); + } + } + + public void on_wayland_surface_disposed () { + wayland_surface = null; + } + } + + static void unref_obj_on_destroy (Wl.Resource resource) { + resource.get_user_data ().unref (); + } + + internal static void get_panel (Wl.Client client, Wl.Resource resource, uint32 output, Wl.Resource surface_resource) { + unowned GLib.Object? wayland_surface = surface_resource.get_user_data (); + PanelSurface? panel_surface = wayland_surface.get_qdata (PanelSurface.quark); + if (panel_surface != null) { + surface_resource.post_error ( + Wl.DisplayError.INVALID_OBJECT, + "io_elementary_pantheon_shell_v1_interface::get_panel already requested" + ); + return; + } + + panel_surface = new PanelSurface (wayland_surface); + unowned var panel_resource = client.create_resource ( + ref Pantheon.Desktop.PanelInterface.iface, + resource.get_version (), + output + ); + panel_resource.set_implementation ( + &wayland_pantheon_panel_interface, + panel_surface.ref (), + unref_obj_on_destroy + ); + wayland_surface.set_qdata_full ( + PanelSurface.quark, + panel_surface, + (GLib.DestroyNotify) PanelSurface.on_wayland_surface_disposed + ); + } + + internal static void get_widget (Wl.Client client, Wl.Resource resource, uint32 output, Wl.Resource surface_resource) { + unowned GLib.Object? wayland_surface = surface_resource.get_user_data (); + WidgetSurface? widget_surface = wayland_surface.get_qdata (WidgetSurface.quark); + if (widget_surface != null) { + surface_resource.post_error ( + Wl.DisplayError.INVALID_OBJECT, + "io_elementary_pantheon_shell_v1_interface::get_widget already requested" + ); + return; + } + + widget_surface = new WidgetSurface (wayland_surface); + unowned var widget_resource = client.create_resource ( + ref Pantheon.Desktop.WidgetInterface.iface, + resource.get_version (), + output + ); + widget_resource.set_implementation ( + &wayland_pantheon_widget_interface, + widget_surface.ref (), + unref_obj_on_destroy + ); + wayland_surface.set_qdata_full ( + WidgetSurface.quark, + widget_surface, + (GLib.DestroyNotify) WidgetSurface.on_wayland_surface_disposed + ); + } + + internal static void get_extended_behavior (Wl.Client client, Wl.Resource resource, uint32 output, Wl.Resource surface_resource) { + unowned GLib.Object? wayland_surface = surface_resource.get_user_data (); + ExtendedBehaviorSurface? eb_surface = wayland_surface.get_qdata (ExtendedBehaviorSurface.quark); + if (eb_surface != null) { + surface_resource.post_error ( + Wl.DisplayError.INVALID_OBJECT, + "io_elementary_pantheon_shell_v1_interface::get_extended_behavior already requested" + ); + return; + } + + eb_surface = new ExtendedBehaviorSurface (wayland_surface); + unowned var eb_resource = client.create_resource ( + ref Pantheon.Desktop.ExtendedBehaviorInterface.iface, + resource.get_version (), + output + ); + eb_resource.set_implementation ( + &wayland_pantheon_extended_behavior_interface, + eb_surface.ref (), + unref_obj_on_destroy + ); + wayland_surface.set_qdata_full ( + ExtendedBehaviorSurface.quark, + eb_surface, + (GLib.DestroyNotify) ExtendedBehaviorSurface.on_wayland_surface_disposed + ); + } + + internal static void set_anchor (Wl.Client client, Wl.Resource resource, [CCode (type = "uint32_t")] Pantheon.Desktop.Anchor anchor) { + unowned PanelSurface? panel_surface = resource.get_user_data (); + if (panel_surface.wayland_surface == null) { + warning ("Window tried to set anchor but wayland surface is null."); + return; + } + + Meta.Window? window; + panel_surface.wayland_surface.get ("window", out window, null); + if (window == null) { + warning ("Window tried to set anchor but wayland surface had no associated window."); + return; + } + + Meta.Side side = TOP; + switch (anchor) { + case TOP: + break; + + case BOTTOM: + side = BOTTOM; + break; + + case LEFT: + side = LEFT; + break; + + case RIGHT: + side = RIGHT; + break; + } + + ShellClientsManager.get_instance ().set_anchor (window, side); + } + + internal static void focus_panel (Wl.Client client, Wl.Resource resource) { + unowned PanelSurface? panel_surface = resource.get_user_data (); + if (panel_surface.wayland_surface == null) { + warning ("Window tried to focus but wayland surface is null."); + return; + } + + focus (panel_surface.wayland_surface); + } + + internal static void focus_extended_behavior (Wl.Client client, Wl.Resource resource) { + unowned ExtendedBehaviorSurface? extended_behavior_surface = resource.get_user_data (); + if (extended_behavior_surface.wayland_surface == null) { + warning ("Window tried to focus but wayland surface is null."); + return; + } + + focus (extended_behavior_surface.wayland_surface); + } + + internal static void focus (Object wayland_surface) { + Meta.Window? window; + wayland_surface.get ("window", out window, null); + if (window == null) { + warning ("Window tried to focus but wayland surface had no associated window."); + return; + } + + window.focus (window.get_display ().get_current_time ()); + } + + internal static void set_size (Wl.Client client, Wl.Resource resource, int width, int height) { + unowned PanelSurface? panel_surface = resource.get_user_data (); + if (panel_surface.wayland_surface == null) { + warning ("Window tried to set size but wayland surface is null."); + return; + } + + Meta.Window? window; + panel_surface.wayland_surface.get ("window", out window, null); + if (window == null) { + warning ("Window tried to set size but wayland surface had no associated window."); + return; + } + + ShellClientsManager.get_instance ().set_size (window, width, height); + } + + internal static void set_keep_above (Wl.Client client, Wl.Resource resource) { + 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; + } + + window.make_above (); + } + + internal static void make_centered (Wl.Client client, Wl.Resource resource) { + 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_centered (window); + } + + internal static void destroy_panel_surface (Wl.Client client, Wl.Resource resource) { + resource.destroy (); + } + + internal static void destroy_widget_surface (Wl.Client client, Wl.Resource resource) { + resource.destroy (); + } + + internal static void destroy_extended_behavior_surface (Wl.Client client, Wl.Resource resource) { + resource.destroy (); + } +} diff --git a/compositor/ShellClients/CenteredWindow.vala b/compositor/ShellClients/CenteredWindow.vala new file mode 100644 index 000000000..7b75b27ab --- /dev/null +++ b/compositor/ShellClients/CenteredWindow.vala @@ -0,0 +1,55 @@ +/* + * Copyright 2024 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public class GreeterCompositor.CenteredWindow : Object { + public WindowManager wm { get; construct; } + public Meta.Window window { get; construct; } + + private uint idle_move_id = 0; + + public CenteredWindow (WindowManager wm, Meta.Window window) { + Object (wm: wm, window: window); + } + + construct { + window.size_changed.connect (position_window); + window.stick (); + + var monitor_manager = wm.get_display ().get_context ().get_backend ().get_monitor_manager (); + monitor_manager.monitors_changed.connect (() => position_window ()); + + position_window (); + + window.shown.connect (() => window.focus (wm.get_display ().get_current_time ())); + + window.unmanaging.connect (() => { + if (idle_move_id != 0) { + Source.remove (idle_move_id); + } + }); + } + + private void position_window () { + var display = wm.get_display (); + var monitor_geom = display.get_monitor_geometry (display.get_primary_monitor ()); + var window_rect = window.get_frame_rect (); + + var x = monitor_geom.x + (monitor_geom.width - window_rect.width) / 2; + var y = monitor_geom.y + (monitor_geom.height - window_rect.height) / 2; + + if (idle_move_id != 0) { + Source.remove (idle_move_id); + } + + idle_move_id = Idle.add (() => { + window.move_frame (false, x, y); + + idle_move_id = 0; + return Source.REMOVE; + }); + } +} diff --git a/compositor/ShellClients/ManagedClient.vala b/compositor/ShellClients/ManagedClient.vala new file mode 100644 index 000000000..51f579204 --- /dev/null +++ b/compositor/ShellClients/ManagedClient.vala @@ -0,0 +1,87 @@ +/* + * Copyright 2024 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +/** + * Utility class that takes care of launching and restarting a subprocess. + * On wayland this uses a WaylandClient and emits window_created if a window for the client was created. + * On X this just launches a normal subprocess and never emits window_created. + */ +public class GreeterCompositor.ManagedClient : Object { + public signal void window_created (Meta.Window window); + + public Meta.Display display { get; construct; } + public string[] args { get; construct; } + + public Meta.WaylandClient? wayland_client { get; private set; } + + private Subprocess? subprocess; + + public ManagedClient (Meta.Display display, string[] args) { + Object (display: display, args: args); + } + + construct { + if (Meta.Util.is_wayland_compositor ()) { + start_wayland.begin (); + + display.window_created.connect ((window) => { + if (wayland_client != null && wayland_client.owns_window (window)) { + window_created (window); + + // We have to manage is alive manually since windows created by WaylandClients have our pid + // and we don't want to end our own process + window.notify["is-alive"].connect (() => { + if (!window.is_alive && subprocess != null) { + subprocess.force_exit (); + warning ("WaylandClient window became unresponsive, killing the client."); + } + }); + } + }); + } else { + start_x.begin (); + } + } + + private async void start_wayland () { + var subprocess_launcher = new GLib.SubprocessLauncher (STDERR_PIPE | STDOUT_PIPE); + try { +#if HAS_MUTTER44 + wayland_client = new Meta.WaylandClient (display.get_context (), subprocess_launcher); +#else + wayland_client = new Meta.WaylandClient (subprocess_launcher); +#endif + subprocess = wayland_client.spawnv (display, args); + + yield subprocess.wait_async (); + + //Restart the daemon if it crashes + Timeout.add_seconds (1, () => { + start_wayland.begin (); + return Source.REMOVE; + }); + } catch (Error e) { + warning ("Failed to create dock client: %s", e.message); + return; + } + } + + private async void start_x () { + try { + subprocess = new Subprocess.newv (args, NONE); + yield subprocess.wait_async (); + + //Restart the daemon if it crashes + Timeout.add_seconds (1, () => { + start_x.begin (); + return Source.REMOVE; + }); + } catch (Error e) { + warning ("Failed to create daemon subprocess with x: %s", e.message); + } + } +} diff --git a/compositor/ShellClients/NotificationsClient.vala b/compositor/ShellClients/NotificationsClient.vala new file mode 100644 index 000000000..b0819cc4d --- /dev/null +++ b/compositor/ShellClients/NotificationsClient.vala @@ -0,0 +1,34 @@ +/* + * Copyright 2024 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +/** + * Used as a key for Object.set_data on Meta.Windows that should be + * treated as notifications. Has to be set before the window is mapped. + */ +public const string NOTIFICATION_DATA_KEY = "elementary-notification"; + +public class GreeterCompositor.NotificationsClient : Object { + public Meta.Display display { get; construct; } + + private ManagedClient client; + + public NotificationsClient (Meta.Display display) { + Object (display: display); + } + + construct { + client = new ManagedClient (display, { "io.elementary.notifications" }); + + client.window_created.connect ((window) => { + window.set_data (NOTIFICATION_DATA_KEY, true); + window.make_above (); +#if HAS_MUTTER46 + client.wayland_client.make_dock (window); +#endif + }); + } +} diff --git a/compositor/ShellClients/PanelClone.vala b/compositor/ShellClients/PanelClone.vala new file mode 100644 index 000000000..f639b4668 --- /dev/null +++ b/compositor/ShellClients/PanelClone.vala @@ -0,0 +1,111 @@ +/* + * Copyright 2024 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public class GreeterCompositor.PanelClone : Object { + private const int ANIMATION_DURATION = 250; + + public WindowManager wm { get; construct; } + public unowned PanelWindow panel { get; construct; } + + public Pantheon.Desktop.HideMode hide_mode { + get { + return NEVER; + } + set { + show (); + } + } + + public bool panel_hidden { get; private set; default = true; } + + private SafeWindowClone clone; + private Meta.WindowActor actor; + + public PanelClone (WindowManager wm, PanelWindow panel) { + Object (wm: wm, panel: panel); + } + + construct { + clone = new SafeWindowClone (panel.window, true); + wm.ui_group.add_child (clone); + + actor = (Meta.WindowActor) panel.window.get_compositor_private (); + // WindowActor position and Window position aren't necessarily the same. + // The clone needs the actor position + actor.notify["x"].connect (update_clone_position); + actor.notify["y"].connect (update_clone_position); + // Actor visibility might be changed by something else e.g. workspace switch + // but we want to keep it in sync with us + actor.notify["visible"].connect (update_visible); + + notify["panel-hidden"].connect (() => { + update_visible (); + }); + + update_visible (); + update_clone_position (); + + Idle.add_once (() => { + show (); + }); + } + + private void update_visible () { + actor.visible = !panel_hidden; + } + + private void update_clone_position () { + clone.set_position (calculate_clone_x (panel_hidden), calculate_clone_y (panel_hidden)); + } + + private float calculate_clone_x (bool hidden) { + switch (panel.anchor) { + case TOP: + case BOTTOM: + return actor.x; + default: + return 0; + } + } + + private float calculate_clone_y (bool hidden) { + switch (panel.anchor) { + case TOP: + return hidden ? actor.y - actor.height : actor.y; + case BOTTOM: + return hidden ? actor.y + actor.height : actor.y; + default: + return 0; + } + } + + private int get_animation_duration () { + var fullscreen = wm.get_display ().get_monitor_in_fullscreen (panel.window.get_monitor ()); + var should_animate = !fullscreen; + return should_animate ? ANIMATION_DURATION : 0; + } + + public void show () { + if (!panel_hidden) { + return; + } + + var animation_duration = get_animation_duration (); + + clone.save_easing_state (); + clone.set_easing_mode (Clutter.AnimationMode.EASE_OUT_QUAD); + clone.set_easing_duration (animation_duration); + clone.y = calculate_clone_y (false); + clone.restore_easing_state (); + + Timeout.add (animation_duration, () => { + clone.visible = false; + panel_hidden = false; + return Source.REMOVE; + }); + } +} diff --git a/compositor/ShellClients/PanelWindow.vala b/compositor/ShellClients/PanelWindow.vala new file mode 100644 index 000000000..422013b0d --- /dev/null +++ b/compositor/ShellClients/PanelWindow.vala @@ -0,0 +1,198 @@ +/* + * Copyright 2024 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public class GreeterCompositor.PanelWindow : Object { + private const int BARRIER_OFFSET = 50; // Allow hot corner trigger + + private static HashTable window_struts = new HashTable (null, null); + + public WindowManager wm { get; construct; } + public Meta.Window window { get; construct; } + + public bool hidden { get; private set; default = false; } + + public Meta.Side anchor; + + private PanelClone clone; + + private uint idle_move_id = 0; + + private int width = -1; + private int height = -1; + + public PanelWindow (WindowManager wm, Meta.Window window, Meta.Side anchor) { + Object (wm: wm, window: window); + + // Meta.Side seems to be currently not supported as GLib.Object property ...? + // At least it always crashed for me with some paramspec, g_type_fundamental backtrace + this.anchor = anchor; + } + + construct { + window.size_changed.connect (position_window); + + window.unmanaging.connect (() => { + if (idle_move_id != 0) { + Source.remove (idle_move_id); + } + + if (window_struts.remove (window)) { + update_struts (); + } + }); + + window.stick (); + + clone = new PanelClone (wm, this); + + var monitor_manager = wm.get_display ().get_context ().get_backend ().get_monitor_manager (); + monitor_manager.monitors_changed.connect (() => update_anchor (anchor)); + + var workspace_manager = wm.get_display ().get_workspace_manager (); + workspace_manager.workspace_added.connect (update_strut); + workspace_manager.workspace_removed.connect (update_strut); + } + +#if HAS_MUTTER46 + public Mtk.Rectangle get_custom_window_rect () { +#else + public Meta.Rectangle get_custom_window_rect () { +#endif + var window_rect = window.get_frame_rect (); + + if (width > 0) { + window_rect.width = width; + } + + if (height > 0) { + window_rect.height = height; + } + + return window_rect; + } + + public void set_size (int width, int height) { + this.width = width; + this.height = height; + + position_window (); + set_hide_mode (clone.hide_mode); // Resetup barriers etc. + } + + public void update_anchor (Meta.Side anchor) { + this.anchor = anchor; + + position_window (); + set_hide_mode (clone.hide_mode); // Resetup barriers etc. + } + + private void position_window () { + var display = wm.get_display (); + var monitor_geom = display.get_monitor_geometry (display.get_primary_monitor ()); + var window_rect = get_custom_window_rect (); + + switch (anchor) { + case TOP: + position_window_top (monitor_geom, window_rect); + break; + + case BOTTOM: + position_window_bottom (monitor_geom, window_rect); + break; + + default: + warning ("Side not supported yet"); + break; + } + + update_strut (); + } + +#if HAS_MUTTER45 + private void position_window_top (Mtk.Rectangle monitor_geom, Mtk.Rectangle window_rect) { +#else + private void position_window_top (Meta.Rectangle monitor_geom, Meta.Rectangle window_rect) { +#endif + var x = monitor_geom.x + (monitor_geom.width - window_rect.width) / 2; + + move_window_idle (x, monitor_geom.y); + } + +#if HAS_MUTTER45 + private void position_window_bottom (Mtk.Rectangle monitor_geom, Mtk.Rectangle window_rect) { +#else + private void position_window_bottom (Meta.Rectangle monitor_geom, Meta.Rectangle window_rect) { +#endif + var x = monitor_geom.x + (monitor_geom.width - window_rect.width) / 2; + var y = monitor_geom.y + monitor_geom.height - window_rect.height; + + move_window_idle (x, y); + } + + private void move_window_idle (int x, int y) { + if (idle_move_id != 0) { + Source.remove (idle_move_id); + } + + idle_move_id = Idle.add (() => { + window.move_frame (false, x, y); + + idle_move_id = 0; + return Source.REMOVE; + }); + } + + public void set_hide_mode (Pantheon.Desktop.HideMode hide_mode) { + clone.hide_mode = hide_mode; + + if (hide_mode == NEVER) { + make_exclusive (); + } else { + unmake_exclusive (); + } + } + + private void make_exclusive () { + update_strut (); + } + + private void update_strut () { + if (clone.hide_mode != NEVER) { + return; + } + + var rect = get_custom_window_rect (); + + Meta.Strut strut = { + rect, + anchor + }; + + window_struts[window] = strut; + + update_struts (); + } + + private void update_struts () { + var list = new SList (); + + foreach (var window_strut in window_struts.get_values ()) { + list.append (window_strut); + } + + foreach (var workspace in wm.get_display ().get_workspace_manager ().get_workspaces ()) { + workspace.set_builtin_struts (list); + } + } + + private void unmake_exclusive () { + if (window in window_struts) { + window_struts.remove (window); + update_struts (); + } + } +} diff --git a/compositor/ShellClients/ShellClientsManager.vala b/compositor/ShellClients/ShellClientsManager.vala new file mode 100644 index 000000000..4256c7b81 --- /dev/null +++ b/compositor/ShellClients/ShellClientsManager.vala @@ -0,0 +1,209 @@ +/* + * Copyright 2024 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public class GreeterCompositor.ShellClientsManager : Object { + private static ShellClientsManager instance; + + public static void init (WindowManager wm) { + if (instance != null) { + return; + } + + instance = new ShellClientsManager (wm); + } + + public static ShellClientsManager? get_instance () { + return instance; + } + + public WindowManager wm { get; construct; } + + private NotificationsClient notifications_client; + private ManagedClient[] protocol_clients = {}; + + private GLib.HashTable windows = new GLib.HashTable (null, null); + private GLib.HashTable centered_windows = new GLib.HashTable (null, null); + + private ShellClientsManager (WindowManager wm) { + Object (wm: wm); + } + + construct { + notifications_client = new NotificationsClient (wm.get_display ()); + + start_clients.begin (); + + if (!Meta.Util.is_wayland_compositor ()) { + wm.get_display ().window_created.connect ((window) => { + window.notify["mutter-hints"].connect ((obj, pspec) => parse_mutter_hints ((Meta.Window) obj)); + parse_mutter_hints (window); + }); + } + } + + private async void start_clients () { + protocol_clients += new ManagedClient (wm.get_display (), { "io.elementary.wingpanel", "-g" }); + } + + public void make_dock (Meta.Window window) { + if (Meta.Util.is_wayland_compositor ()) { + make_dock_wayland (window); + } else { + make_dock_x11 (window); + } + } + + private void make_dock_wayland (Meta.Window window) requires (Meta.Util.is_wayland_compositor ()) { + foreach (var client in protocol_clients) { + if (client.wayland_client.owns_window (window)) { + client.wayland_client.make_dock (window); + break; + } + } + } + + private void make_dock_x11 (Meta.Window window) requires (!Meta.Util.is_wayland_compositor ()) { + unowned var x11_display = wm.get_display ().get_x11_display (); + +#if HAS_MUTTER46 + var x_window = x11_display.lookup_xwindow (window); +#else + var x_window = window.get_xwindow (); +#endif + // gtk3's gdk_x11_window_set_type_hint() is used as a reference + unowned var xdisplay = x11_display.get_xdisplay (); + var atom = xdisplay.intern_atom ("_NET_WM_WINDOW_TYPE", false); + var dock_atom = xdisplay.intern_atom ("_NET_WM_WINDOW_TYPE_DOCK", false); + + // (X.Atom) 4 is XA_ATOM + // 32 is format + // 0 means replace + xdisplay.change_property (x_window, atom, (X.Atom) 4, 32, 0, (uchar[]) dock_atom, 1); + } + + public void set_anchor (Meta.Window window, Meta.Side side) { + if (window in windows) { + windows[window].update_anchor (side); + return; + } + + make_dock (window); + // TODO: Return if requested by window that's not a trusted client? + + windows[window] = new PanelWindow (wm, window, side); + + // connect_after so we make sure the PanelWindow can destroy its barriers and struts + window.unmanaging.connect_after (() => windows.remove (window)); + } + + /** + * The size given here is only used for the hide mode. I.e. struts + * and collision detection with other windows use this size. By default + * or if set to -1 the size of the window is used. + * + * TODO: Maybe use for strut only? + */ + public void set_size (Meta.Window window, int width, int height) { + if (!(window in windows)) { + warning ("Set anchor for window before size."); + return; + } + + windows[window].set_size (width, height); + } + + public void set_hide_mode (Meta.Window window, Pantheon.Desktop.HideMode hide_mode) { + if (!(window in windows)) { + warning ("Set anchor for window before hide mode."); + return; + } + + windows[window].set_hide_mode (hide_mode); + } + + public void make_centered (Meta.Window window) { + if (window in centered_windows) { + return; + } + + centered_windows[window] = new CenteredWindow (wm, window); + + window.unmanaging.connect_after (() => centered_windows.remove (window)); + } + + public bool is_positioned_window (Meta.Window window) { + bool positioned = (window in centered_windows) || (window in windows); + window.foreach_ancestor ((ancestor) => { + if (ancestor in centered_windows || ancestor in windows) { + positioned = true; + } + + return !positioned; + }); + + return positioned; + } + + //X11 only + private void parse_mutter_hints (Meta.Window window) requires (!Meta.Util.is_wayland_compositor ()) { + if (window.mutter_hints == null) { + return; + } + + var mutter_hints = window.mutter_hints.split (":"); + foreach (var mutter_hint in mutter_hints) { + var split = mutter_hint.split ("="); + + if (split.length != 2) { + continue; + } + + var key = split[0]; + var val = split[1]; + + switch (key) { + case "anchor": + int parsed; // Will be used as Meta.Side which is a 4 value bitfield so check bounds for that + if (int.try_parse (val, out parsed) && 0 <= parsed && parsed <= 15) { + set_anchor (window, parsed); + } else { + warning ("Failed to parse %s as anchor", val); + } + break; + + case "hide-mode": + int parsed; // Will be used as Pantheon.Desktop.HideMode which is a 5 value enum so check bounds for that + if (int.try_parse (val, out parsed) && 0 <= parsed && parsed <= 4) { + set_hide_mode (window, parsed); + } else { + warning ("Failed to parse %s as hide mode", val); + } + break; + + case "size": + var split_val = val.split (","); + if (split_val.length != 2) { + break; + } + int parsed_width, parsed_height = 0; //set to 0 because vala doesn't realize height will be set too + if (int.try_parse (split_val[0], out parsed_width) && int.try_parse (split_val[1], out parsed_height)) { + set_size (window, parsed_width, parsed_height); + } else { + warning ("Failed to parse %s as width and height", val); + } + break; + + case "centered": + make_centered (window); + break; + + default: + break; + } + } + } +} diff --git a/compositor/Widgets/SafeWindowClone.vala b/compositor/Widgets/SafeWindowClone.vala new file mode 100644 index 000000000..ff11abf72 --- /dev/null +++ b/compositor/Widgets/SafeWindowClone.vala @@ -0,0 +1,66 @@ +// +// Copyright (C) 2014 Tom Beckmann +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +namespace GreeterCompositor { + /** + * A clone for a MetaWindowActor that will guard against the + * meta_window_appears_focused crash by disabling painting the clone + * as soon as it gets unavailable. + */ + public class SafeWindowClone : Clutter.Clone { + public Meta.Window window { get; construct; } + + /** + * If set to true, the SafeWindowClone will destroy itself when the connected + * window is unmanaged + */ + public bool destroy_on_unmanaged { get; construct set; default = false; } + + /** + * Creates a new SafeWindowClone + * + * @param window The window to clone from + * @param destroy_on_unmanaged see destroy_on_unmanaged property + */ + public SafeWindowClone (Meta.Window window, bool destroy_on_unmanaged = false) { + var actor = (Meta.WindowActor) window.get_compositor_private (); + + Object (window: window, + source: actor, + destroy_on_unmanaged: destroy_on_unmanaged); + } + + construct { + if (source != null) + window.unmanaged.connect (reset_source); + } + + ~SafeWindowClone () { + window.unmanaged.disconnect (reset_source); + } + + private void reset_source () { + // actually destroying the clone will be handled somewhere else (unless we were + // requested to destroy it), we just need to make sure the clone doesn't attempt + // to draw a clone of a window that has been destroyed + source = null; + + if (destroy_on_unmanaged) + destroy (); + } + } +} diff --git a/compositor/WindowManager.vala b/compositor/WindowManager.vala index 629b0e081..91c22dce4 100644 --- a/compositor/WindowManager.vala +++ b/compositor/WindowManager.vala @@ -210,7 +210,7 @@ namespace GreeterCompositor { Idle.add (() => { // let the session manager move to the next phase display.get_context ().notify_ready (); - start_command.begin ({ "io.elementary.wingpanel", "-g" }); + ShellClientsManager.init (this); if (GLib.Environment.get_variable ("DESKTOP_SESSION") != "installer") { start_command.begin ({ "io.elementary.greeter" }); diff --git a/compositor/meson.build b/compositor/meson.build index c6fab913f..d5ef13baa 100644 --- a/compositor/meson.build +++ b/compositor/meson.build @@ -84,6 +84,14 @@ compositor_files = files( 'Background/BackgroundSource.vala', 'Background/BlurEffect.vala', 'Background/SystemBackground.vala', + 'PantheonShell.vala', + 'ShellClients/CenteredWindow.vala', + 'ShellClients/ManagedClient.vala', + 'ShellClients/NotificationsClient.vala', + 'ShellClients/PanelClone.vala', + 'ShellClients/PanelWindow.vala', + 'ShellClients/ShellClientsManager.vala', + 'Widgets/SafeWindowClone.vala', 'WingpanelManager/WingpanelManager.vala', 'WingpanelManager/DBusWingpanelManager.vala', 'WingpanelManager/FocusManager.vala', @@ -107,7 +115,7 @@ executable( compositor_files, compositor_resources, config_header, - dependencies: [glib_dep, gtk_dep, gee_dep, m_dep, posix_dep, mutter_dep, gnome_desktop_dep], + dependencies: [glib_dep, gtk_dep, gee_dep, m_dep, posix_dep, mutter_dep, gnome_desktop_dep, pantheon_desktop_shell_dep], vala_args: vala_flags, c_args: compositor_c_args, build_rpath: mutter_typelib_dir, diff --git a/meson.build b/meson.build index 647178008..4820b6fc0 100644 --- a/meson.build +++ b/meson.build @@ -7,6 +7,7 @@ project( gnome = import('gnome') i18n = import('i18n') +vala = meson.get_compiler('vala') conf_data = configuration_data() conf_data.set('CONF_DIR', join_paths(get_option('sysconfdir'), 'lightdm')) @@ -47,6 +48,7 @@ compositor_resources = gnome.compile_resources( source_dir: 'data' ) +subdir('protocol') subdir('src') subdir('compositor') subdir('data') diff --git a/protocol/meson.build b/protocol/meson.build new file mode 100644 index 000000000..771af1a0f --- /dev/null +++ b/protocol/meson.build @@ -0,0 +1,32 @@ +dep_scanner = dependency('wayland-scanner', native: true) +prog_scanner = find_program(dep_scanner.get_variable(pkgconfig: 'wayland_scanner')) + +protocol_file = files('pantheon-desktop-shell-v1.xml') + +pantheon_desktop_shell_sources = [] +pantheon_desktop_shell_sources += custom_target( + 'pantheon-desktop-shell-server-protocol.h', + command: [ prog_scanner, 'server-header', '@INPUT@', '@OUTPUT@' ], + input: protocol_file, + output: 'pantheon-desktop-shell-server-protocol.h', +) + +output_type = 'private-code' +if dep_scanner.version().version_compare('< 1.14.91') + output_type = 'code' +endif +pantheon_desktop_shell_sources += custom_target( + 'pantheon-desktop-shell-protocol.c', + command: [ prog_scanner, output_type, '@INPUT@', '@OUTPUT@' ], + input: protocol_file, + output: 'pantheon-desktop-shell-protocol.c', +) + +pantheon_desktop_shell_dep = declare_dependency( + dependencies: [ + vala.find_library('pantheon-desktop-shell', dirs: meson.current_source_dir()), + dependency('wayland-server'), + ], + include_directories: include_directories('.'), + sources: pantheon_desktop_shell_sources +) diff --git a/protocol/pantheon-desktop-shell-v1.xml b/protocol/pantheon-desktop-shell-v1.xml new file mode 100644 index 000000000..63d9d5982 --- /dev/null +++ b/protocol/pantheon-desktop-shell-v1.xml @@ -0,0 +1,131 @@ + + + + + SPDX-License-Identifier: LGPL-2.1-or-later + ]]> + + + + This interface is used by the Pantheon Wayland shell to communicate with + the compositor. + + + + + Create a panel surface from an existing surface. + + + + + + + + Create a desktop widget surface from an existing surface. + + + + + + + + Create a desktop-specific surface from an existing surface. + + + + + + + + + + + + The anchor is a placement hint to the compositor. + + + + + + + + + + How the shell should handle the window. + + + + + + + + + + + Tell the shell which side of the screen the panel is + located. This is so that new windows do not overlap the panel + and maximized windows maximize properly. + + + + + + + + Request keyboard focus, taking it away from any other window. + Keyboard focus must always be manually be requested and is + - in contrast to normal windows - never automatically granted + by the compositor. + + + + + + The given size is only used for exclusive zones and + collision tracking for auto hide. By default and if set + to -1 the size of the surface is used. + + + + + + + + + Tell the shell when to hide the panel. + + + + + + + + + + + + + + + Tell the shell to keep the surface above on all workspaces + + + + + + Request to keep the surface centered. This will cause keyboard focus + to not be granted automatically but having to be requested via focus. + + + + + + Request keyboard focus, taking it away from any other window. + Keyboard focus must always be manually be requested and is + - in contrast to normal windows - never automatically granted + by the compositor. + + + + diff --git a/protocol/pantheon-desktop-shell.deps b/protocol/pantheon-desktop-shell.deps new file mode 100644 index 000000000..62acb1e0a --- /dev/null +++ b/protocol/pantheon-desktop-shell.deps @@ -0,0 +1 @@ +wayland-server diff --git a/protocol/pantheon-desktop-shell.vapi b/protocol/pantheon-desktop-shell.vapi new file mode 100644 index 000000000..4c57de974 --- /dev/null +++ b/protocol/pantheon-desktop-shell.vapi @@ -0,0 +1,83 @@ +/* + * Copyright 2023 elementary, Inc. + * Copyright 2023 Corentin Noël + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +namespace Pantheon.Desktop { + [CCode (cheader_filename = "pantheon-desktop-shell-server-protocol.h", cname = "struct io_elementary_pantheon_shell_v1_interface")] + public struct ShellInterface { + [CCode (cheader_filename = "pantheon-desktop-shell-server-protocol.h", cname = "io_elementary_pantheon_shell_v1_interface")] + public static Wl.Interface iface; + public Pantheon.Desktop.GetPanel get_panel; + public Pantheon.Desktop.GetWidget get_widget; + public Pantheon.Desktop.GetExtendedBehavior get_extended_behavior; + + } + + [CCode (cheader_filename = "pantheon-desktop-shell-server-protocol.h", cname = "enum io_elementary_pantheon_panel_v1_anchor", cprefix="IO_ELEMENTARY_PANTHEON_PANEL_V1_ANCHOR_", has_type_id = false)] + public enum Anchor { + TOP, + BOTTOM, + LEFT, + RIGHT, + } + + [CCode (cheader_filename = "pantheon-desktop-shell-server-protocol.h", cname = "enum io_elementary_pantheon_panel_v1_hide_mode", cprefix="IO_ELEMENTARY_PANTHEON_PANEL_V1_HIDE_MODE_", has_type_id = false)] + public enum HideMode { + NEVER, + MAXIMIZED_FOCUS_WINDOW, + OVERLAPPING_FOCUS_WINDOW, + OVERLAPPING_WINDOW, + ALWAYS + } + + [CCode (cheader_filename = "pantheon-desktop-shell-server-protocol.h", cname = "struct io_elementary_pantheon_panel_v1_interface")] + public struct PanelInterface { + [CCode (cheader_filename = "pantheon-desktop-shell-server-protocol.h", cname = "io_elementary_pantheon_panel_v1_interface")] + public static Wl.Interface iface; + public Destroy destroy; + public SetAnchor set_anchor; + public Focus focus; + public SetSize set_size; + public SetHideMode set_hide_mode; + } + + [CCode (cheader_filename = "pantheon-desktop-shell-server-protocol.h", cname = "struct io_elementary_pantheon_widget_v1_interface")] + public struct WidgetInterface { + [CCode (cheader_filename = "pantheon-desktop-shell-server-protocol.h", cname = "io_elementary_pantheon_widget_v1_interface")] + public static Wl.Interface iface; + public Destroy destroy; + } + + [CCode (cheader_filename = "pantheon-desktop-shell-server-protocol.h", cname = "struct io_elementary_pantheon_extended_behavior_v1_interface")] + public struct ExtendedBehaviorInterface { + [CCode (cheader_filename = "pantheon-desktop-shell-server-protocol.h", cname = "io_elementary_pantheon_extended_behavior_v1_interface")] + public static Wl.Interface iface; + public Destroy destroy; + public SetKeepAbove set_keep_above; + public MakeCentered make_centered; + public Focus focus; + } + + [CCode (has_target = false, has_typedef = false)] + public delegate void GetPanel (Wl.Client client, Wl.Resource resource, uint32 output, Wl.Resource surface); + [CCode (has_target = false, has_typedef = false)] + public delegate void GetWidget (Wl.Client client, Wl.Resource resource, uint32 output, Wl.Resource surface); + [CCode (has_target = false, has_typedef = false)] + public delegate void GetExtendedBehavior (Wl.Client client, Wl.Resource resource, uint32 output, Wl.Resource surface); + [CCode (has_target = false, has_typedef = false)] + public delegate void SetAnchor (Wl.Client client, Wl.Resource resource, [CCode (type = "uint32_t")] Anchor anchor); + [CCode (has_target = false, has_typedef = false)] + public delegate void Focus (Wl.Client client, Wl.Resource resource); + [CCode (has_target = false, has_typedef = false)] + public delegate void SetSize (Wl.Client client, Wl.Resource resource, int width, int height); + [CCode (has_target = false, has_typedef = false)] + public delegate void SetHideMode (Wl.Client client, Wl.Resource resource, [CCode (type = "uint32_t")] HideMode hide_mode); + [CCode (has_target = false, has_typedef = false)] + public delegate void SetKeepAbove (Wl.Client client, Wl.Resource resource); + [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 Destroy (Wl.Client client, Wl.Resource resource); +} diff --git a/vapi/wayland-server.deps b/vapi/wayland-server.deps new file mode 100644 index 000000000..b3188f742 --- /dev/null +++ b/vapi/wayland-server.deps @@ -0,0 +1 @@ +posix diff --git a/vapi/wayland-server.vapi b/vapi/wayland-server.vapi new file mode 100644 index 000000000..6fa1885b3 --- /dev/null +++ b/vapi/wayland-server.vapi @@ -0,0 +1,125 @@ +/* wayland-server.vapi + * + * Copyright 2022 Corentin Noël + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice (including the + * next paragraph) shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * Authors: + * Corentin Noël + */ + +[CCode (cprefix = "wl_", lower_case_cprefix = "wl_", cheader_filename = "wayland-server.h")] +namespace Wl { + [Compact] + [CCode (cname = "struct wl_display", free_function = "wl_display_destroy")] + public class Display { + [CCode (cname = "wl_display_create")] + public Display (); + public int add_socket (string name); + public unowned string add_socket_auto (); + public int add_socket_fd (int sock_fd); + public void terminate (); + public void run (); + public void flush_clients (); + public void destroy_clients (); + public uint32 get_serial (); + public uint32 next_serial (); + } + + [Compact] + [CCode (cname = "struct wl_client", free_function = "wl_client_destroy")] + public class Client { + [CCode (cname = "wl_client_create")] + public Client (Wl.Display display, int fd); + public void flush (); + public void get_credentials (out Posix.pid_t pid, out Posix.uid_t uid, out Posix.gid_t gid); + public int get_fd (); + public unowned Wl.Display get_display (); + [CCode (cname = "wl_resource_create")] + public unowned Wl.Resource? create_resource (ref Wl.Interface interface, int version, uint32 id); + } + + [Compact] + [CCode (cname = "struct wl_resource", free_function = "wl_resource_destroy")] + public class Resource { + public uint32 get_id (); + public unowned Wl.Client get_client (); + [CCode (simple_generics = true)] + public void set_user_data (T? data); + [CCode (simple_generics = true)] + public unowned T? get_user_data (); + public int get_version (); + public unowned string get_class (); + public void destroy (); + public void set_implementation (void* implementation, void* data, [CCode (delegate_target = false)] ResourceDestroyFunc destroy); + [PrintfFormat] + public void post_error(uint32 code, string format, ...); + } + [Compact] + [CCode (cname = "struct wl_interface")] + public class Interface { + public string name; + public int version; + [CCode (array_length = "method_count")] + public Wl.Message[] methods; + [CCode (array_length = "event_count")] + public Wl.Message[] events; + } + + [Compact] + [CCode (cname = "struct wl_message")] + public class Message { + public string name; + public string signature; + [CCode (array_length = false)] + public Wl.Interface?[] types; + } + + [Compact] + [CCode (cname = "struct wl_global", free_function = "wl_global_destroy")] + public class Global { + [CCode (cname = "wl_global_create")] + public static Wl.Global? create (Wl.Display display, ref Wl.Interface interface, int version, [CCode (delegate_target_pos = 3.9) ] Wl.GlobalBindFunc bind); + } + + [CCode (cheader_filename = "wayland-server-protocol.h", cname = "enum wl_display_error", cprefix="WL_DISPLAY_ERROR_", has_type_id = false)] + public enum DisplayError { + INVALID_OBJECT, + INVALID_METHOD, + NO_MEMORY, + IMPLEMENTATION, + } + + [CCode (cname = "wl_global_bind_func_t", instance_pos = 1.9)] + public delegate void GlobalBindFunc (Wl.Client client, uint32 version, uint32 id); + [CCode (cname = "wl_resource_destroy_func_t", has_target = false)] + public delegate void ResourceDestroyFunc (Wl.Resource resource); + [CCode (cname = "WAYLAND_VERSION_MAJOR")] + public const int VERSION_MAJOR; + [CCode (cname = "WAYLAND_VERSION_MINOR")] + public const int VERSION_MINOR; + [CCode (cname = "WAYLAND_VERSION_MICRO")] + public const int VERSION_MICRO; + [CCode (cname = "WAYLAND_VERSION")] + public const string VERSION; +} +