diff --git a/daemon/BackgroundMenu.vala b/daemon/BackgroundMenu.vala new file mode 100644 index 00000000..5923f330 --- /dev/null +++ b/daemon/BackgroundMenu.vala @@ -0,0 +1,57 @@ +/* + * Copyright 2024 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +public class Gala.Daemon.BackgroundMenu : Gtk.Menu { + public const string ACTION_GROUP_PREFIX = "background-menu"; + public const string ACTION_PREFIX = ACTION_GROUP_PREFIX + "."; + + construct { + var change_wallpaper = new Gtk.MenuItem.with_label (_("Change Wallpaper…")) { + action_name = ACTION_PREFIX + "launch-uri", + action_target = new Variant.string ("settings://desktop/appearance/wallpaper") + }; + + var display_settings = new Gtk.MenuItem.with_label (_("Display Settings…")) { + action_name = ACTION_PREFIX + "launch-uri", + action_target = new Variant.string ("settings://display") + }; + + + var system_settings = new Gtk.MenuItem.with_label (_("System Settings…")) { + action_name = ACTION_PREFIX + "launch-uri", + action_target = new Variant.string ("settings://") + }; + + append (change_wallpaper); + append (display_settings); + append (new Gtk.SeparatorMenuItem ()); + append (system_settings); + show_all (); + + var launch_action = new SimpleAction ("launch-uri", VariantType.STRING); + launch_action.activate.connect (action_launch); + + var action_group = new SimpleActionGroup (); + action_group.add_action (launch_action); + + insert_action_group (ACTION_GROUP_PREFIX, action_group); + } + + private void action_launch (SimpleAction action, Variant? variant) { + try { + AppInfo.launch_default_for_uri (variant.get_string (), null); + } catch (Error e) { + var message_dialog = new Granite.MessageDialog.with_image_from_icon_name ( + _("Failed to open System Settings"), + _("A handler for the “settings://” URI scheme must be installed."), + "dialog-error", + Gtk.ButtonsType.CLOSE + ); + message_dialog.show_error_details (e.message); + message_dialog.present (); + message_dialog.response.connect (message_dialog.destroy); + } + } +} diff --git a/daemon/Main.vala b/daemon/Main.vala index a8ff12e6..4c4ff6e6 100644 --- a/daemon/Main.vala +++ b/daemon/Main.vala @@ -1,160 +1,45 @@ -// -// Copyright (c) 2018 elementary LLC. (https://elementary.io) -// -// 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 . -// +/* + * Copyright 2024 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + */ -namespace Gala { - [DBus (name = "org.gnome.SessionManager")] - public interface SessionManager : Object { - public abstract async ObjectPath register_client ( - string app_id, - string client_start_id - ) throws DBusError, IOError; +public class Gala.Daemon.Application : Gtk.Application { + public Application () { + Object (application_id: "org.pantheon.gala.daemon"); } - [DBus (name = "org.gnome.SessionManager.ClientPrivate")] - public interface SessionClient : Object { - public abstract void end_session_response (bool is_ok, string reason) throws DBusError, IOError; + public override void startup () { + base.startup (); - public signal void stop () ; - public signal void query_end_session (uint flags); - public signal void end_session (uint flags); - public signal void cancel_end_session (); - } + var granite_settings = Granite.Settings.get_default (); + var gtk_settings = Gtk.Settings.get_default (); - public class Daemon { - private SessionClient? sclient = null; - - public Daemon () { - register.begin ((o, res)=> { - bool success = register.end (res); - if (!success) { - message ("Failed to register with Session manager"); - } - }); - - var granite_settings = Granite.Settings.get_default (); - var gtk_settings = Gtk.Settings.get_default (); + gtk_settings.gtk_application_prefer_dark_theme = granite_settings.prefers_color_scheme == Granite.Settings.ColorScheme.DARK; + granite_settings.notify["prefers-color-scheme"].connect (() => { gtk_settings.gtk_application_prefer_dark_theme = granite_settings.prefers_color_scheme == Granite.Settings.ColorScheme.DARK; - - granite_settings.notify["prefers-color-scheme"].connect (() => { - gtk_settings.gtk_application_prefer_dark_theme = granite_settings.prefers_color_scheme == Granite.Settings.ColorScheme.DARK; - }); - - var menu_daemon = new MenuDaemon (); - menu_daemon.setup_dbus (); - } - - public void run () { - Gtk.main (); - } - - public static async SessionClient? register_with_session (string app_id) { - ObjectPath? path = null; - string? msg = null; - string? start_id = null; - - SessionManager? session = null; - SessionClient? session_client = null; - - start_id = Environment.get_variable ("DESKTOP_AUTOSTART_ID"); - if (start_id != null) { - Environment.unset_variable ("DESKTOP_AUTOSTART_ID"); - } else { - start_id = ""; - warning ( - "DESKTOP_AUTOSTART_ID not set, session registration may be broken (not running via session?)" - ); - } - - try { - session = yield Bus.get_proxy ( - BusType.SESSION, - "org.gnome.SessionManager", - "/org/gnome/SessionManager" - ); - } catch (Error e) { - warning ("Unable to connect to session manager: %s", e.message); - return null; - } - - try { - path = yield session.register_client (app_id, start_id); - } catch (Error e) { - msg = e.message; - warning ("Error registering with session manager: %s", e.message); - return null; - } - - try { - session_client = yield Bus.get_proxy (BusType.SESSION, "org.gnome.SessionManager", path); - } catch (Error e) { - warning ("Unable to get private sessions client proxy: %s", e.message); - return null; - } - - return session_client; - } - - private async bool register () { - sclient = yield register_with_session ("org.pantheon.gala.daemon"); - - sclient.query_end_session.connect (() => end_session (false)); - sclient.end_session.connect (() => end_session (false)); - sclient.stop.connect (() => end_session (true)); - - return true; - } - - private void end_session (bool quit) { - if (quit) { - Gtk.main_quit (); - return; - } - - try { - sclient.end_session_response (true, ""); - } catch (Error e) { - warning ("Unable to respond to session manager: %s", e.message); - } - } + }); } - public static int main (string[] args) { - GLib.Intl.setlocale (LocaleCategory.ALL, ""); - GLib.Intl.bindtextdomain (Config.GETTEXT_PACKAGE, Config.LOCALEDIR); - GLib.Intl.bind_textdomain_codeset (Config.GETTEXT_PACKAGE, "UTF-8"); - GLib.Intl.textdomain (Config.GETTEXT_PACKAGE); + public override void activate () { + hold (); + } - Gtk.init (ref args); + public override bool dbus_register (DBusConnection connection, string object_path) throws Error { + base.dbus_register (connection, object_path); - var ctx = new OptionContext ("Gala Daemon"); - ctx.set_help_enabled (true); - ctx.add_group (Gtk.get_option_group (false)); + connection.register_object (object_path, new MenuDaemon ()); - try { - ctx.parse (ref args); - } catch (Error e) { - stderr.printf ("Error: %s\n", e.message); - return 0; - } - - var daemon = new Daemon (); - daemon.run (); - - return 0; + return true; } } + +public static int main (string[] args) { + GLib.Intl.setlocale (LocaleCategory.ALL, ""); + GLib.Intl.bindtextdomain (Config.GETTEXT_PACKAGE, Config.LOCALEDIR); + GLib.Intl.bind_textdomain_codeset (Config.GETTEXT_PACKAGE, "UTF-8"); + GLib.Intl.textdomain (Config.GETTEXT_PACKAGE); + + var app = new Gala.Daemon.Application (); + return app.run (); +} diff --git a/daemon/MenuDaemon.vala b/daemon/MenuDaemon.vala index 5029aab4..298807b3 100644 --- a/daemon/MenuDaemon.vala +++ b/daemon/MenuDaemon.vala @@ -1,367 +1,105 @@ -// -// Copyright 2018-2020 elementary, Inc. (https://elementary.io) -// -// 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 . -// +/* + * Copyright 2024 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + */ -namespace Gala { +[DBus (name = "org.pantheon.gala")] +public interface Gala.WMDBus : GLib.Object { + public abstract void perform_action (Gala.ActionType type) throws DBusError, IOError; +} + +[DBus (name = "org.pantheon.gala.daemon")] +public class Gala.Daemon.MenuDaemon : GLib.Object { private const string DBUS_NAME = "org.pantheon.gala"; private const string DBUS_OBJECT_PATH = "/org/pantheon/gala"; private const string DAEMON_DBUS_NAME = "org.pantheon.gala.daemon"; private const string DAEMON_DBUS_OBJECT_PATH = "/org/pantheon/gala/daemon"; - [DBus (name = "org.pantheon.gala")] - public interface WMDBus : GLib.Object { - public abstract void perform_action (Gala.ActionType type) throws DBusError, IOError; + private WMDBus? wm_proxy = null; + + private WindowMenu? window_menu; + private BackgroundMenu? background_menu; + + construct { + Bus.watch_name (BusType.SESSION, DBUS_NAME, BusNameWatcherFlags.NONE, gala_appeared, lost_gala); } - [DBus (name = "org.pantheon.gala.daemon")] - public class MenuDaemon : Object { - // Window Menu - private Granite.AccelLabel always_on_top_accellabel; - private Granite.AccelLabel close_accellabel; - private Granite.AccelLabel hide_accellabel; - private Granite.AccelLabel move_accellabel; - private Granite.AccelLabel move_left_accellabel; - private Granite.AccelLabel move_right_accellabel; - private Granite.AccelLabel on_visible_workspace_accellabel; - private Granite.AccelLabel resize_accellabel; - private Granite.AccelLabel screenshot_accellabel; - private Gtk.Menu? window_menu = null; - private Gtk.MenuItem hide; - private Gtk.MenuItem maximize; - private Gtk.MenuItem move; - private Gtk.MenuItem resize; - private Gtk.CheckMenuItem always_on_top; - private Gtk.CheckMenuItem on_visible_workspace; - private Gtk.MenuItem move_left; - private Gtk.MenuItem move_right; - private Gtk.MenuItem close; - private Gtk.MenuItem screenshot; - - // Desktop Menu - private Gtk.Menu? desktop_menu = null; - - private WMDBus? wm_proxy = null; - - private ulong always_on_top_sid = 0U; - private ulong on_visible_workspace_sid = 0U; - - private static GLib.Settings keybind_settings; - private static GLib.Settings gala_keybind_settings; - - static construct { - keybind_settings = new GLib.Settings ("org.gnome.desktop.wm.keybindings"); - gala_keybind_settings = new GLib.Settings ("org.pantheon.desktop.gala.keybindings"); + private void on_gala_get (GLib.Object? obj, GLib.AsyncResult? res) { + try { + wm_proxy = Bus.get_proxy.end (res); + } catch (Error e) { + warning ("Failed to get Gala proxy: %s", e.message); } + } - [DBus (visible = false)] - public void setup_dbus () { - var flags = BusNameOwnerFlags.ALLOW_REPLACEMENT | BusNameOwnerFlags.REPLACE; - Bus.own_name (BusType.SESSION, DAEMON_DBUS_NAME, flags, on_bus_acquired, () => {}, null); + private void lost_gala () { + wm_proxy = null; + } - Bus.watch_name (BusType.SESSION, DBUS_NAME, BusNameWatcherFlags.NONE, gala_appeared, lost_gala); + private void gala_appeared () { + if (wm_proxy == null) { + Bus.get_proxy.begin (BusType.SESSION, DBUS_NAME, DBUS_OBJECT_PATH, 0, null, on_gala_get); } + } - private void on_gala_get (GLib.Object? o, GLib.AsyncResult? res) { + private void perform_action (Gala.ActionType type) { + if (wm_proxy != null) { try { - wm_proxy = Bus.get_proxy.end (res); + wm_proxy.perform_action (type); } catch (Error e) { - warning ("Failed to get Gala proxy: %s", e.message); + warning ("Failed to perform Gala action over DBus: %s", e.message); } } + } - private void lost_gala () { - wm_proxy = null; + public void show_window_menu (Gala.WindowFlags flags, int display_width, int display_height, int x, int y) throws DBusError, IOError { + if (window_menu == null) { + window_menu = new WindowMenu (); + window_menu.perform_action.connect (perform_action); } - private void gala_appeared () { - if (wm_proxy == null) { - Bus.get_proxy.begin (BusType.SESSION, DBUS_NAME, DBUS_OBJECT_PATH, 0, null, on_gala_get); - } + window_menu.update (flags); + + show_menu (window_menu, display_width, display_height, x, y, true); + } + + public void show_desktop_menu (int display_width, int display_height, int x, int y) throws DBusError, IOError { + if (background_menu == null) { + background_menu = new BackgroundMenu (); } - private void on_bus_acquired (DBusConnection conn) { - try { - conn.register_object (DAEMON_DBUS_OBJECT_PATH, this); - } catch (Error e) { - stderr.printf ("Error registering MenuDaemon: %s\n", e.message); - } - } + show_menu (background_menu, display_width, display_height, x, y, false); + } - private void perform_action (Gala.ActionType type) { - if (wm_proxy != null) { - try { - wm_proxy.perform_action (type); - } catch (Error e) { - warning ("Failed to perform Gala action over DBus: %s", e.message); - } - } - } + private void show_menu (Gtk.Menu menu, int display_width, int display_height, int x, int y, bool ignore_first_release) { + var window = new Window (display_width, display_height); + window.present (); - private void init_window_menu () { - hide_accellabel = new Granite.AccelLabel (_("Hide")); + menu.attach_to_widget (window.content, null); - hide = new Gtk.MenuItem (); - hide.add (hide_accellabel); - hide.activate.connect (() => { - perform_action (Gala.ActionType.HIDE_CURRENT); - }); + Gdk.Rectangle rect = { + x, + y, + 0, + 0 + }; - maximize = new Gtk.MenuItem (); - maximize.activate.connect (() => { - perform_action (Gala.ActionType.MAXIMIZE_CURRENT); - }); + menu.show_all (); + menu.popup_at_rect (window.get_window (), rect, NORTH, NORTH_WEST); - move_accellabel = new Granite.AccelLabel (_("Move")); + menu.deactivate.connect (window.close); - move = new Gtk.MenuItem (); - move.add (move_accellabel); - move.activate.connect (() => { - perform_action (Gala.ActionType.START_MOVE_CURRENT); - }); - - resize_accellabel = new Granite.AccelLabel (_("Resize")); - - resize = new Gtk.MenuItem (); - resize.add (resize_accellabel); - resize.activate.connect (() => { - perform_action (Gala.ActionType.START_RESIZE_CURRENT); - }); - - always_on_top_accellabel = new Granite.AccelLabel (_("Always on Top")); - - always_on_top = new Gtk.CheckMenuItem (); - always_on_top.add (always_on_top_accellabel); - always_on_top_sid = always_on_top.activate.connect (() => { - perform_action (Gala.ActionType.TOGGLE_ALWAYS_ON_TOP_CURRENT); - }); - - on_visible_workspace_accellabel = new Granite.AccelLabel (_("Always on Visible Workspace")); - - on_visible_workspace = new Gtk.CheckMenuItem (); - on_visible_workspace.add (on_visible_workspace_accellabel); - on_visible_workspace_sid = on_visible_workspace.activate.connect (() => { - perform_action (Gala.ActionType.TOGGLE_ALWAYS_ON_VISIBLE_WORKSPACE_CURRENT); - }); - - move_left_accellabel = new Granite.AccelLabel (_("Move to Workspace Left")); - - move_left = new Gtk.MenuItem (); - move_left.add (move_left_accellabel); - move_left.activate.connect (() => { - perform_action (Gala.ActionType.MOVE_CURRENT_WORKSPACE_LEFT); - }); - - move_right_accellabel = new Granite.AccelLabel (_("Move to Workspace Right")); - - move_right = new Gtk.MenuItem (); - move_right.add (move_right_accellabel); - move_right.activate.connect (() => { - perform_action (Gala.ActionType.MOVE_CURRENT_WORKSPACE_RIGHT); - }); - - screenshot_accellabel = new Granite.AccelLabel (_("Take Screenshot")); - - screenshot = new Gtk.MenuItem (); - screenshot.add (screenshot_accellabel); - screenshot.activate.connect (() => { - perform_action (Gala.ActionType.SCREENSHOT_CURRENT); - }); - - close_accellabel = new Granite.AccelLabel (_("Close")); - - close = new Gtk.MenuItem (); - close.add (close_accellabel); - close.activate.connect (() => { - perform_action (Gala.ActionType.CLOSE_CURRENT); - }); - - window_menu = new Gtk.Menu (); - window_menu.append (screenshot); - window_menu.append (new Gtk.SeparatorMenuItem ()); - window_menu.append (always_on_top); - window_menu.append (on_visible_workspace); - window_menu.append (move_left); - window_menu.append (move_right); - window_menu.append (new Gtk.SeparatorMenuItem ()); - window_menu.append (move); - window_menu.append (resize); - window_menu.append (maximize); - window_menu.append (new Gtk.SeparatorMenuItem ()); - window_menu.append (hide); - window_menu.append (close); - window_menu.show_all (); - } - - public void show_window_menu (Gala.WindowFlags flags, int x, int y) throws DBusError, IOError { - if (window_menu == null) { - init_window_menu (); - } - - hide.visible = Gala.WindowFlags.CAN_HIDE in flags; - if (hide.visible) { - hide_accellabel.accel_string = keybind_settings.get_strv ("minimize")[0]; - } - - maximize.visible = Gala.WindowFlags.CAN_MAXIMIZE in flags; - if (maximize.visible) { - unowned string maximize_label; - if (Gala.WindowFlags.IS_MAXIMIZED in flags) { - maximize_label = (Gala.WindowFlags.IS_TILED in flags) ? _("Untile") : _("Unmaximize"); - } else { - maximize_label = _("Maximize"); + if (ignore_first_release) { + bool first = true; + menu.button_release_event.connect (() => { + if (first) { + first = false; + return Gdk.EVENT_STOP; } - maximize.get_child ().destroy (); - maximize.add ( - new Granite.AccelLabel ( - maximize_label, - keybind_settings.get_strv ("toggle-maximized")[0] - ) - ); - } - - - move.visible = Gala.WindowFlags.ALLOWS_MOVE in flags; - if (move.visible) { - move_accellabel.accel_string = keybind_settings.get_strv ("begin-move")[0]; - } - - resize.visible = Gala.WindowFlags.ALLOWS_RESIZE in flags; - if (resize.visible) { - resize_accellabel.accel_string = keybind_settings.get_strv ("begin-resize")[0]; - } - - // Setting active causes signal fires on activate so - // we temporarily block those signals from emissions - SignalHandler.block (always_on_top, always_on_top_sid); - SignalHandler.block (on_visible_workspace, on_visible_workspace_sid); - - always_on_top.active = Gala.WindowFlags.ALWAYS_ON_TOP in flags; - always_on_top_accellabel.accel_string = keybind_settings.get_strv ("always-on-top")[0]; - - on_visible_workspace.active = Gala.WindowFlags.ON_ALL_WORKSPACES in flags; - on_visible_workspace_accellabel.accel_string = keybind_settings.get_strv ("toggle-on-all-workspaces")[0]; - - SignalHandler.unblock (always_on_top, always_on_top_sid); - SignalHandler.unblock (on_visible_workspace, on_visible_workspace_sid); - - move_right.sensitive = !on_visible_workspace.active; - if (move_right.sensitive) { - move_right_accellabel.accel_string = keybind_settings.get_strv ("move-to-workspace-right")[0]; - } - - move_left.sensitive = !on_visible_workspace.active; - if (move_left.sensitive) { - move_left_accellabel.accel_string = keybind_settings.get_strv ("move-to-workspace-left")[0]; - } - - screenshot_accellabel.accel_string = gala_keybind_settings.get_strv ("window-screenshot")[0]; - - close.visible = Gala.WindowFlags.CAN_CLOSE in flags; - if (close.visible) { - close_accellabel.accel_string = keybind_settings.get_strv ("close")[0]; - } - - // `opened` is used as workaround for https://github.com/elementary/gala/issues/1387 - var opened = false; - Idle.add (() => { - window_menu.popup (null, null, (m, ref px, ref py, out push_in) => { - var scale = m.scale_factor; - px = x / scale; - // Move the menu 1 pixel outside of the pointer or else it closes instantly - // on the mouse up event - py = (y / scale) + 1; - push_in = true; - opened = true; - }, Gdk.BUTTON_SECONDARY, Gdk.CURRENT_TIME); - - return opened ? Source.REMOVE : Source.CONTINUE; + return Gdk.EVENT_PROPAGATE; }); } - - public void show_desktop_menu (int x, int y) throws DBusError, IOError { - if (desktop_menu == null) { - var change_wallpaper = new Gtk.MenuItem.with_label (_("Change Wallpaper…")); - change_wallpaper.activate.connect (() => { - try { - AppInfo.launch_default_for_uri ("settings://desktop/appearance/wallpaper", null); - } catch (Error e) { - var message_dialog = new Granite.MessageDialog.with_image_from_icon_name ( - "Failed to Open Wallpaper Settings", - "Unable to open System Settings. A handler for the `settings://` URI scheme must be installed.", - "dialog-error", - Gtk.ButtonsType.CLOSE - ); - message_dialog.show_error_details (e.message); - message_dialog.run (); - message_dialog.destroy (); - } - }); - - var display_settings = new Gtk.MenuItem.with_label (_("Display Settings…")); - display_settings.activate.connect (() => { - try { - AppInfo.launch_default_for_uri ("settings://display", null); - } catch (Error e) { - var message_dialog = new Granite.MessageDialog.with_image_from_icon_name ( - "Failed to Open Display Settings", - "Unable to open System Settings. A handler for the `settings://` URI scheme must be installed.", - "dialog-warning", - Gtk.ButtonsType.CLOSE - ); - message_dialog.show_error_details (e.message); - message_dialog.run (); - message_dialog.destroy (); - } - }); - - var system_settings = new Gtk.MenuItem.with_label (_("System Settings…")); - system_settings.activate.connect (() => { - try { - AppInfo.launch_default_for_uri ("settings://", null); - } catch (Error e) { - var message_dialog = new Granite.MessageDialog.with_image_from_icon_name ( - "Failed to Open System Settings", - "Unable to open System Settings. A handler for the `settings://` URI scheme must be installed.", - "dialog-warning", - Gtk.ButtonsType.CLOSE - ); - message_dialog.show_error_details (e.message); - message_dialog.run (); - message_dialog.destroy (); - } - }); - - desktop_menu = new Gtk.Menu (); - desktop_menu.append (change_wallpaper); - desktop_menu.append (display_settings); - desktop_menu.append (new Gtk.SeparatorMenuItem ()); - desktop_menu.append (system_settings); - desktop_menu.show_all (); - } - - desktop_menu.popup (null, null, (m, ref px, ref py, out push_in) => { - var scale = m.scale_factor; - px = x / scale; - // Move the menu 1 pixel outside of the pointer or else it closes instantly - // on the mouse up event - py = (y / scale) + 1; - push_in = false; - }, Gdk.BUTTON_SECONDARY, Gdk.CURRENT_TIME); - } } } diff --git a/daemon/Window.vala b/daemon/Window.vala new file mode 100644 index 00000000..2d2894ea --- /dev/null +++ b/daemon/Window.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 Gala.Daemon.Window : Gtk.Window { + static construct { + var app_provider = new Gtk.CssProvider (); + app_provider.load_from_resource ("io/elementary/desktop/gala-daemon/gala-daemon.css"); + Gtk.StyleContext.add_provider_for_screen (Gdk.Screen.get_default (), app_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); + } + + public Gtk.Box content { get; construct; } + + public Window (int width, int height) { + Object ( + default_width: width, + default_height: height + ); + } + + class construct { + set_css_name ("daemon-window"); + } + + construct { + decorated = false; + resizable = false; + deletable = false; + can_focus = false; + input_shape_combine_region (null); + accept_focus = false; + skip_taskbar_hint = true; + skip_pager_hint = true; + type_hint = Gdk.WindowTypeHint.DOCK; + set_keep_above (true); + + child = content = new Gtk.Box (HORIZONTAL, 0) { + hexpand = true, + vexpand = true + }; + + set_visual (get_screen ().get_rgba_visual ()); + + show_all (); + move (0, 0); + + button_press_event.connect (() => { + close (); + return Gdk.EVENT_STOP; + }); + } +} diff --git a/daemon/WindowMenu.vala b/daemon/WindowMenu.vala new file mode 100644 index 00000000..1aa6a700 --- /dev/null +++ b/daemon/WindowMenu.vala @@ -0,0 +1,194 @@ +/* + * Copyright 2024 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +public class Gala.Daemon.WindowMenu : Gtk.Menu { + private static GLib.Settings gala_keybind_settings = new GLib.Settings ("org.pantheon.desktop.gala.keybindings"); + private static GLib.Settings keybind_settings = new GLib.Settings ("org.gnome.desktop.wm.keybindings"); + + public signal void perform_action (Gala.ActionType type); + + private Granite.AccelLabel always_on_top_accellabel; + private Granite.AccelLabel close_accellabel; + private Granite.AccelLabel minimize_accellabel; + private Granite.AccelLabel move_accellabel; + private Granite.AccelLabel move_left_accellabel; + private Granite.AccelLabel move_right_accellabel; + private Granite.AccelLabel on_visible_workspace_accellabel; + private Granite.AccelLabel resize_accellabel; + private Granite.AccelLabel screenshot_accellabel; + private Gtk.MenuItem minimize; + private Gtk.MenuItem maximize; + private Gtk.MenuItem move; + private Gtk.MenuItem resize; + private Gtk.CheckMenuItem always_on_top; + private Gtk.CheckMenuItem on_visible_workspace; + private Gtk.MenuItem move_left; + private Gtk.MenuItem move_right; + private Gtk.MenuItem close; + private Gtk.MenuItem screenshot; + + private ulong always_on_top_sid = 0U; + private ulong on_visible_workspace_sid = 0U; + + construct { + minimize_accellabel = new Granite.AccelLabel (_("Hide")); + + minimize = new Gtk.MenuItem (); + minimize.add (minimize_accellabel); + minimize.activate.connect (() => { + perform_action (Gala.ActionType.HIDE_CURRENT); + }); + + maximize = new Gtk.MenuItem (); + maximize.activate.connect (() => { + perform_action (Gala.ActionType.MAXIMIZE_CURRENT); + }); + + move_accellabel = new Granite.AccelLabel (_("Move")); + + move = new Gtk.MenuItem (); + move.add (move_accellabel); + move.activate.connect (() => { + perform_action (Gala.ActionType.START_MOVE_CURRENT); + }); + + resize_accellabel = new Granite.AccelLabel (_("Resize")); + + resize = new Gtk.MenuItem (); + resize.add (resize_accellabel); + resize.activate.connect (() => { + perform_action (Gala.ActionType.START_RESIZE_CURRENT); + }); + + always_on_top_accellabel = new Granite.AccelLabel (_("Always on Top")); + + always_on_top = new Gtk.CheckMenuItem (); + always_on_top.add (always_on_top_accellabel); + always_on_top_sid = always_on_top.activate.connect (() => { + perform_action (Gala.ActionType.TOGGLE_ALWAYS_ON_TOP_CURRENT); + }); + + on_visible_workspace_accellabel = new Granite.AccelLabel (_("Always on Visible Workspace")); + + on_visible_workspace = new Gtk.CheckMenuItem (); + on_visible_workspace.add (on_visible_workspace_accellabel); + on_visible_workspace_sid = on_visible_workspace.activate.connect (() => { + perform_action (Gala.ActionType.TOGGLE_ALWAYS_ON_VISIBLE_WORKSPACE_CURRENT); + }); + + move_left_accellabel = new Granite.AccelLabel (_("Move to Workspace Left")); + + move_left = new Gtk.MenuItem (); + move_left.add (move_left_accellabel); + move_left.activate.connect (() => { + perform_action (Gala.ActionType.MOVE_CURRENT_WORKSPACE_LEFT); + }); + + move_right_accellabel = new Granite.AccelLabel (_("Move to Workspace Right")); + + move_right = new Gtk.MenuItem (); + move_right.add (move_right_accellabel); + move_right.activate.connect (() => { + perform_action (Gala.ActionType.MOVE_CURRENT_WORKSPACE_RIGHT); + }); + + screenshot_accellabel = new Granite.AccelLabel (_("Take Screenshot")); + + screenshot = new Gtk.MenuItem (); + screenshot.add (screenshot_accellabel); + screenshot.activate.connect (() => { + perform_action (Gala.ActionType.SCREENSHOT_CURRENT); + }); + + close_accellabel = new Granite.AccelLabel (_("Close")); + + close = new Gtk.MenuItem (); + close.add (close_accellabel); + close.activate.connect (() => { + perform_action (Gala.ActionType.CLOSE_CURRENT); + }); + + append (screenshot); + append (new Gtk.SeparatorMenuItem ()); + append (always_on_top); + append (on_visible_workspace); + append (move_left); + append (move_right); + append (new Gtk.SeparatorMenuItem ()); + append (move); + append (resize); + append (maximize); + append (new Gtk.SeparatorMenuItem ()); + append (minimize); + append (close); + } + + public void update (Gala.WindowFlags flags) { + minimize.visible = Gala.WindowFlags.CAN_HIDE in flags; + if (minimize.visible) { + minimize_accellabel.accel_string = keybind_settings.get_strv ("minimize")[0]; + } + + maximize.visible = Gala.WindowFlags.CAN_MAXIMIZE in flags; + if (maximize.visible) { + unowned string maximize_label; + if (Gala.WindowFlags.IS_MAXIMIZED in flags) { + maximize_label = (Gala.WindowFlags.IS_TILED in flags) ? _("Untile") : _("Unmaximize"); + } else { + maximize_label = _("Maximize"); + } + + maximize.get_child ().destroy (); + maximize.add ( + new Granite.AccelLabel ( + maximize_label, + keybind_settings.get_strv ("toggle-maximized")[0] + ) + ); + } + + + move.visible = Gala.WindowFlags.ALLOWS_MOVE in flags; + if (move.visible) { + move_accellabel.accel_string = keybind_settings.get_strv ("begin-move")[0]; + } + + resize.visible = Gala.WindowFlags.ALLOWS_RESIZE in flags; + if (resize.visible) { + resize_accellabel.accel_string = keybind_settings.get_strv ("begin-resize")[0]; + } + + // Setting active causes signal fires on activate so + // we temporarily block those signals from emissions + SignalHandler.block (always_on_top, always_on_top_sid); + SignalHandler.block (on_visible_workspace, on_visible_workspace_sid); + + always_on_top.active = Gala.WindowFlags.ALWAYS_ON_TOP in flags; + always_on_top_accellabel.accel_string = keybind_settings.get_strv ("always-on-top")[0]; + + on_visible_workspace.active = Gala.WindowFlags.ON_ALL_WORKSPACES in flags; + on_visible_workspace_accellabel.accel_string = keybind_settings.get_strv ("toggle-on-all-workspaces")[0]; + + SignalHandler.unblock (always_on_top, always_on_top_sid); + SignalHandler.unblock (on_visible_workspace, on_visible_workspace_sid); + + move_right.sensitive = !on_visible_workspace.active; + if (move_right.sensitive) { + move_right_accellabel.accel_string = keybind_settings.get_strv ("move-to-workspace-right")[0]; + } + + move_left.sensitive = !on_visible_workspace.active; + if (move_left.sensitive) { + move_left_accellabel.accel_string = keybind_settings.get_strv ("move-to-workspace-left")[0]; + } + + screenshot_accellabel.accel_string = gala_keybind_settings.get_strv ("window-screenshot")[0]; + + close.visible = Gala.WindowFlags.CAN_CLOSE in flags; + if (close.visible) { + close_accellabel.accel_string = keybind_settings.get_strv ("close")[0]; + } + } +} diff --git a/daemon/meson.build b/daemon/meson.build index 00e96d41..9f95b110 100644 --- a/daemon/meson.build +++ b/daemon/meson.build @@ -1,6 +1,9 @@ gala_daemon_sources = files( 'Main.vala', - 'MenuDaemon.vala' + 'MenuDaemon.vala', + 'Window.vala', + 'WindowMenu.vala', + 'BackgroundMenu.vala' ) gala_daemon_bin = executable( diff --git a/data/gala-daemon.css b/data/gala-daemon.css new file mode 100644 index 00000000..8332358b --- /dev/null +++ b/data/gala-daemon.css @@ -0,0 +1,8 @@ +/* + * Copyright 2024 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +daemon-window { + background-color: transparent; +} diff --git a/data/gala-daemon.desktop b/data/gala-daemon.desktop deleted file mode 100644 index 58df1496..00000000 --- a/data/gala-daemon.desktop +++ /dev/null @@ -1,14 +0,0 @@ -[Desktop Entry] -Type=Application -Name=Gala Background Services -Comment=Gala Background Services -Exec=gala-daemon -Terminal=false -Categories=GNOME;GTK;System;Core; -NoDisplay=true -StartupNotify=true -X-GNOME-AutoRestart=true -X-GNOME-Autostart-Notify=true -X-GNOME-Autostart-Phase=Desktop -X-GNOME-HiddenUnderSystemd=true -OnlyShowIn=Pantheon diff --git a/data/gala.gresource.xml b/data/gala.gresource.xml index 727942e9..2959b72a 100644 --- a/data/gala.gresource.xml +++ b/data/gala.gresource.xml @@ -25,4 +25,7 @@ shaders/colorblindness-correction.vert shaders/monochrome.vert + + gala-daemon.css + diff --git a/data/meson.build b/data/meson.build index 1efb96f3..d4ec7849 100644 --- a/data/meson.build +++ b/data/meson.build @@ -4,11 +4,6 @@ install_data( rename: 'org.pantheon.desktop.gala.gschema.xml' ) -install_data( - 'gala-daemon.desktop', - install_dir: join_paths(get_option('sysconfdir'), 'xdg', 'autostart') -) - i18n.merge_file( input: 'gala.metainfo.xml.in', output: meson.project_name() + '.metainfo.xml', diff --git a/po/POTFILES b/po/POTFILES index 5b356007..68b5e80f 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -1,7 +1,11 @@ src/Dialogs.vala src/ScreenshotManager.vala src/WindowManager.vala +daemon/BackgroundMenu.vala +daemon/Main.vala daemon/MenuDaemon.vala +daemon/Window.vala +daemon/WindowMenu.vala data/gala.metainfo.xml.in data/gala-multitaskingview.desktop.in data/gala-other.desktop.in diff --git a/src/DaemonManager.vala b/src/DaemonManager.vala new file mode 100644 index 00000000..6adc9738 --- /dev/null +++ b/src/DaemonManager.vala @@ -0,0 +1,133 @@ +/* + * Copyright 2024 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public class Gala.DaemonManager : GLib.Object { + private const string DAEMON_DBUS_NAME = "org.pantheon.gala.daemon"; + private const string DAEMON_DBUS_OBJECT_PATH = "/org/pantheon/gala/daemon"; + private const int SPACING = 12; + + [DBus (name = "org.pantheon.gala.daemon")] + public interface Daemon: GLib.Object { + public abstract async void show_window_menu (WindowFlags flags, int width, int height, int x, int y) throws Error; + public abstract async void show_desktop_menu (int display_width, int display_height, int x, int y) throws Error; + } + + public Meta.Display display { get; construct; } + + private Meta.WaylandClient daemon_client; + private Daemon? daemon_proxy = null; + + public DaemonManager (Meta.Display display) { + Object (display: display); + } + + construct { + Bus.watch_name (BusType.SESSION, DAEMON_DBUS_NAME, BusNameWatcherFlags.NONE, daemon_appeared, lost_daemon); + + if (Meta.Util.is_wayland_compositor ()) { + start_wayland.begin (); + + display.window_created.connect ((window) => { + if (daemon_client.owns_window (window)) { + window.shown.connect (handle_daemon_window); + } + }); + } else { + start_x.begin (); + } + } + + private async void start_wayland () { + var subprocess_launcher = new GLib.SubprocessLauncher (NONE); + try { +#if HAS_MUTTER44 + daemon_client = new Meta.WaylandClient (display.get_context (), subprocess_launcher); +#else + daemon_client = new Meta.WaylandClient (subprocess_launcher); +#endif + string[] args = {"gala-daemon"}; + var subprocess = daemon_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 { + var subprocess = new Subprocess (NONE, "gala-daemon"); + 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); + } + } + + private void handle_daemon_window (Meta.Window window) { + window.move_frame (false, 0, 0); + window.make_above (); + } + + private void lost_daemon () { + daemon_proxy = null; + } + + private void daemon_appeared () { + if (daemon_proxy == null) { + Bus.get_proxy.begin (BusType.SESSION, DAEMON_DBUS_NAME, DAEMON_DBUS_OBJECT_PATH, 0, null, (obj, res) => { + try { + daemon_proxy = Bus.get_proxy.end (res); + } catch (Error e) { + warning ("Failed to get Menu proxy: %s", e.message); + } + }); + } + } + + public async void show_background_menu (int x, int y) { + if (daemon_proxy == null) { + return; + } + + int width, height; + display.get_size (out width, out height); + + try { + yield daemon_proxy.show_desktop_menu (width, height, x, y); + } catch (Error e) { + warning ("Error invoking MenuManager: %s", e.message); + } + } + + public async void show_window_menu (WindowFlags flags, int x, int y) { + if (daemon_proxy == null) { + return; + } + + int width, height; + display.get_size (out width, out height); + + try { + yield daemon_proxy.show_window_menu (flags, width, height, x, y); + } catch (Error e) { + warning ("Error invoking MenuManager: %s", e.message); + } + } +} diff --git a/src/WindowManager.vala b/src/WindowManager.vala index a5683efe..49720167 100644 --- a/src/WindowManager.vala +++ b/src/WindowManager.vala @@ -16,15 +16,6 @@ // namespace Gala { - private const string DAEMON_DBUS_NAME = "org.pantheon.gala.daemon"; - private const string DAEMON_DBUS_OBJECT_PATH = "/org/pantheon/gala/daemon"; - - [DBus (name = "org.pantheon.gala.daemon")] - public interface Daemon: GLib.Object { - public abstract async void show_window_menu (WindowFlags flags, int x, int y) throws Error; - public abstract async void show_desktop_menu (int x, int y) throws Error; - } - public class WindowManagerGala : Meta.Plugin, WindowManager { /** * {@inheritDoc} @@ -90,7 +81,7 @@ namespace Gala { private Meta.Window? moving; //place for the window that is being moved over - private Daemon? daemon_proxy = null; + private DaemonManager daemon_manager; private NotificationStack notification_stack; @@ -147,9 +138,10 @@ namespace Gala { } public override void start () { + daemon_manager = new DaemonManager (get_display ()); + show_stage (); - Bus.watch_name (BusType.SESSION, DAEMON_DBUS_NAME, BusNameWatcherFlags.NONE, daemon_appeared, lost_daemon); AccessDialog.watch_portal (); unowned Meta.Display display = get_display (); @@ -158,22 +150,6 @@ namespace Gala { }); } - private void lost_daemon () { - daemon_proxy = null; - } - - private void daemon_appeared () { - if (daemon_proxy == null) { - Bus.get_proxy.begin (BusType.SESSION, DAEMON_DBUS_NAME, DAEMON_DBUS_OBJECT_PATH, 0, null, (obj, res) => { - try { - daemon_proxy = Bus.get_proxy.end (res); - } catch (Error e) { - warning ("Failed to get Menu proxy: %s", e.message); - } - }); - } - } - private bool show_stage () { unowned Meta.Display display = get_display (); @@ -240,7 +216,7 @@ namespace Gala { ui_group.add_child (window_group); background_group = new BackgroundContainer (this); - ((BackgroundContainer)background_group).show_background_menu.connect (on_show_background_menu); + ((BackgroundContainer)background_group).show_background_menu.connect (daemon_manager.show_background_menu); window_group.add_child (background_group); window_group.set_child_below_sibling (background_group, null); @@ -397,20 +373,6 @@ namespace Gala { } catch (Error e) { warning (e.message); } } - private void on_show_background_menu (int x, int y) { - if (daemon_proxy == null) { - return; - } - - daemon_proxy.show_desktop_menu.begin (x, y, (obj, res) => { - try { - ((Daemon) obj).show_desktop_menu.end (res); - } catch (Error e) { - message ("Error invoking MenuManager: %s", e.message); - } - }); - } - private void on_monitors_changed () { screen_shield.expand_to_screen_size (); } @@ -1048,7 +1010,7 @@ namespace Gala { public override void show_window_menu (Meta.Window window, Meta.WindowMenuType menu, int x, int y) { switch (menu) { case Meta.WindowMenuType.WM: - if (daemon_proxy == null || window.get_window_type () == Meta.WindowType.NOTIFICATION) { + if (window.get_window_type () == Meta.WindowType.NOTIFICATION) { return; } @@ -1083,13 +1045,7 @@ namespace Gala { if (window.can_close ()) flags |= WindowFlags.CAN_CLOSE; - daemon_proxy.show_window_menu.begin (flags, x, y, (obj, res) => { - try { - ((Daemon) obj).show_window_menu.end (res); - } catch (Error e) { - message ("Error invoking MenuManager: %s", e.message); - } - }); + daemon_manager.show_window_menu.begin (flags, x, y); break; case Meta.WindowMenuType.APP: // FIXME we don't have any sort of app menus diff --git a/src/meson.build b/src/meson.build index c1d56ef1..31f66de3 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,6 +1,7 @@ gala_bin_sources = files( 'DBus.vala', 'DBusAccelerator.vala', + 'DaemonManager.vala', 'DesktopIntegration.vala', 'Dialogs.vala', 'InternalUtils.vala',