From 8980c74572cbfe07ba9b2917c2fe4956bcafd2d4 Mon Sep 17 00:00:00 2001 From: David Mohammed Date: Sun, 26 Jan 2025 12:16:28 +0000 Subject: [PATCH] Straight conversion of screenshot code from mutter to wayland The budgie-wm elements have been moved to the power dialog - i.e. keeping the separate of daemon windows vs screencapture which cannot work in the same process. --- src/daemon/screenshot.vala | 89 +++++++--- src/daemon/screenshot_main.vala | 2 +- src/dialogs/power/application.vala | 5 + src/dialogs/power/meson.build | 1 + src/dialogs/power/osdkeys.vala | 254 +++++++++++++++++++++++++++++ src/dialogs/power/screenshot.vala | 121 ++++++++++++++ 6 files changed, 451 insertions(+), 21 deletions(-) create mode 100644 src/dialogs/power/osdkeys.vala create mode 100644 src/dialogs/power/screenshot.vala diff --git a/src/daemon/screenshot.vala b/src/daemon/screenshot.vala index c79eee521..7ace12226 100644 --- a/src/daemon/screenshot.vala +++ b/src/daemon/screenshot.vala @@ -152,6 +152,7 @@ namespace BudgieScr { public abstract async void Screenshot(bool include_cursor, bool flash, string filename, out bool success, out string filename_used) throws Error; public abstract async void ScreenshotWindow(bool include_frame, bool include_cursor, bool flash, string filename, out bool success, out string filename_used) throws Error; + public abstract bool SupportScreenshotWindow() throws Error; } class MakeScreenshot { @@ -166,7 +167,7 @@ namespace BudgieScr { CurrentState windowstate; static ScreenshotClient? client = null; - public MakeScreenshot(int[]? area) { + private static void MakeClientConnection() { if (client == null) { try { client = GLib.Bus.get_proxy_sync(BusType.SESSION, "org.buddiesofbudgie.BudgieScreenshot", "/org/buddiesofbudgie/Screenshot"); @@ -175,6 +176,22 @@ namespace BudgieScr { client = null; } } + } + + public static bool ScreenshotWindowSupport() { + MakeClientConnection(); + + try { + return client.SupportScreenshotWindow(); + } + catch { + warning("Unknown Screenshot Window support"); + return false; + } + } + + public MakeScreenshot(int[]? area) { + MakeClientConnection(); windowstate = new CurrentState(); this.area = area; @@ -327,6 +344,16 @@ namespace BudgieScr { private unowned Gtk.Switch? screenshotcapturesoundswitch; public ScreenshotHomeWindow() { + GtkLayerShell.init_for_window(this); + GtkLayerShell.set_layer(this, GtkLayerShell.Layer.TOP); + var primary_monitor = libxfce4windowing.Screen.get_default().get_primary_monitor(); + GtkLayerShell.set_monitor(this, primary_monitor.get_gdk_monitor()); + GtkLayerShell.set_anchor(this, GtkLayerShell.Edge.TOP, false); + GtkLayerShell.set_anchor(this, GtkLayerShell.Edge.LEFT, false); + GtkLayerShell.set_keyboard_mode(this, GtkLayerShell.KeyboardMode.ON_DEMAND); + + this.set_decorated(true); + windowstate = new CurrentState(); this.set_startup_id("org.buddiesofbudgie.BudgieScreenshot"); this.set_title(_("Budgie Screenshot")); @@ -357,7 +384,7 @@ namespace BudgieScr { topbar = new Gtk.HeaderBar(); topbar.show_close_button = true; this.set_titlebar(topbar); - this.set_keep_above(true); + //this.set_keep_above(true); /* * left or right windowbuttons, that's the question when @@ -401,6 +428,7 @@ namespace BudgieScr { return false; }); }); + this.show_all(); } @@ -432,12 +460,14 @@ namespace BudgieScr { Gtk.Popover newpopover = new Gtk.Popover(b); Grid popovergrid = new Gtk.Grid(); set_margins(popovergrid, 15, 15, 15, 15); - Label[] shortcutnames = { - // Translators: be as brief as possible; popovers ar cut off if broader than the window - new Label(_("Screenshot entire screen") + ":\t"), - new Label(_("Screenshot active window") + ":\t"), - new Label(_("Screenshot selected area") + ":\t"), - }; + bool window_support = MakeScreenshot.ScreenshotWindowSupport(); + Label[] shortcutnames = {}; + // Translators: be as brief as possible; popovers are cut off if broader than the window + shortcutnames += new Label(_("Screenshot entire screen") + ":\t"); + if (window_support) { + shortcutnames += new Label(_("Screenshot active window") + ":\t"); + } + shortcutnames += new Label(_("Screenshot selected area") + ":\t"); shortcutlabels = {}; // Shortcuts is about -keyboard- shortcuts @@ -542,16 +572,20 @@ namespace BudgieScr { string[] mode_options = {"Screen", "Window", "Selection"}; // don't translate, internal use int active = find_stringindex(mode, mode_options); + bool window_support = MakeScreenshot.ScreenshotWindowSupport(); // translate! These are the interface names - string[] areabuttons_labels = { - _("Screen"), _("Window"), _("Selection") - }; + string[] areabuttons_labels = {}; + string[] icon_names = {}; + areabuttons_labels += _("Screen"); + icon_names += "selectscreen-symbolic"; + + if (window_support) { + areabuttons_labels += _("Window"); + icon_names += "selectwindow-symbolic"; + } - string[] icon_names = { - "selectscreen-symbolic", - "selectwindow-symbolic", - "selectselection-symbolic" - }; + areabuttons_labels += _("Selection"); + icon_names += "selectselection-symbolic"; int i = 0; ToggleButton[] selectbuttons = {}; @@ -595,7 +629,13 @@ namespace BudgieScr { } private void select_action(ToggleButton b, ToggleButton[] btns) { - string[] selectmodes = {"Screen", "Window", "Selection"}; + string[] selectmodes = {}; + bool window_support = MakeScreenshot.ScreenshotWindowSupport(); + selectmodes += "Screen"; + if (window_support) { + selectmodes += "Window"; + } + selectmodes += "Selection"; int i = 0; foreach (ToggleButton bt in btns) { @@ -840,6 +880,16 @@ namespace BudgieScr { } public AfterShotWindow() { + GtkLayerShell.init_for_window(this); + GtkLayerShell.set_layer(this, GtkLayerShell.Layer.TOP); + var primary_monitor = libxfce4windowing.Screen.get_default().get_primary_monitor(); + GtkLayerShell.set_monitor(this, primary_monitor.get_gdk_monitor()); + GtkLayerShell.set_anchor(this, GtkLayerShell.Edge.TOP, false); + GtkLayerShell.set_anchor(this, GtkLayerShell.Edge.LEFT, false); + GtkLayerShell.set_keyboard_mode(this, GtkLayerShell.KeyboardMode.ON_DEMAND); + + this.set_decorated(true); + this.set_startup_id("org.buddiesofbudgie.BudgieScreenshot"); this.set_title(_("Budgie Screenshot")); this.set_wmclass("org.buddiesofbudgie.BudgieScreenshot", "org.buddiesofbudgie.BudgieScreenshot"); @@ -1291,9 +1341,8 @@ namespace BudgieScr { private int get_scaling() { // not very sophisticated, but for now, we'll assume one scale - Gdk.Monitor gdkmon = Gdk.Display.get_default().get_monitor(0); - int curr_scale = gdkmon.get_scale_factor(); - + var monitor = libxfce4windowing.Screen.get_default().get_primary_monitor(); + int curr_scale = (int)monitor.get_scale(); return curr_scale; } diff --git a/src/daemon/screenshot_main.vala b/src/daemon/screenshot_main.vala index 138d279a6..26ff5b0ac 100644 --- a/src/daemon/screenshot_main.vala +++ b/src/daemon/screenshot_main.vala @@ -27,7 +27,7 @@ namespace Budgie { const OptionEntry[] options = { { "interactive", 'i', 0, OptionArg.NONE, ref interactive, "Interactively set options" }, - { "window", 'w', 0, OptionArg.NONE, ref window, "Grab a window instead of the entire display" }, + { "window", 'w', 0, OptionArg.NONE, ref window, "Grab a window instead of the entire display. This capability only works for compatible window managers." }, { "area", 'a', 0, OptionArg.NONE, ref area, "Grab an area of the display instead of the entire display" }, { null } }; diff --git a/src/dialogs/power/application.vala b/src/dialogs/power/application.vala index c8e5dd97f..e2bd5cd94 100644 --- a/src/dialogs/power/application.vala +++ b/src/dialogs/power/application.vala @@ -22,6 +22,8 @@ namespace Budgie { private ShellShim? shim; + private ScreenshotManager? screenshot_manager; + public PowerApplication() { Object(application_id: "org.buddiesofbudgie.PowerDialog", flags: 0); @@ -32,6 +34,9 @@ namespace Budgie { */ shim = new ShellShim(); shim.serve(); + + screenshot_manager = new ScreenshotManager(); + screenshot_manager.serve(); } public override void activate() { diff --git a/src/dialogs/power/meson.build b/src/dialogs/power/meson.build index 7f0b8459a..164105236 100644 --- a/src/dialogs/power/meson.build +++ b/src/dialogs/power/meson.build @@ -4,6 +4,7 @@ powerdialog_sources = [ 'dialog_button.vala', 'main.vala', 'shim.vala', + 'screenshot.vala', 'window.vala' ] diff --git a/src/dialogs/power/osdkeys.vala b/src/dialogs/power/osdkeys.vala new file mode 100644 index 000000000..ff48210a0 --- /dev/null +++ b/src/dialogs/power/osdkeys.vala @@ -0,0 +1,254 @@ +/* + * This file is part of budgie-desktop + * + * Copyright Budgie Desktop Developers + * + * 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 2 of the License, or + * (at your option) any later version. + */ + + namespace Budgie { + + [DBus (name = "org.budgie_desktop.BudgieOSD")] + interface ShowOSD : GLib.Object { + public abstract async void ShowOSD(HashTable params) throws DBusError, IOError; + } + + /* + Controls the OSD for various controllable items + volume + brightness + caps-lock + num-lock + */ + class OSDKeys : Object { + + // vars for the brightness handling + GLib.DBusConnection conn = null; + uint signal_id; + ShowOSD osd = null; + double current_brightness_level = 0.0; + + // vars for the caps/numlock changes + unowned Gdk.Keymap? map; + bool capslock; + bool numlock; + bool firstrun = false; + + // vars for the volume changes + private Gvc.MixerControl? mixer; + private Gvc.MixerStream? stream; + private ulong notify_id; + + // need to handle initialisation of various methods so ensure the OSD is not accidently + // activated when budgie-daemon is started. + private bool initialising; + + public OSDKeys() { + initialising = true; + try { + osd = Bus.get_proxy_sync (BusType.SESSION, "org.budgie_desktop.BudgieOSD", + "/org/budgie_desktop/BudgieOSD"); + + } catch (Error e) { + stderr.printf ("%s\n", e.message); + } + + try { + conn = Bus.get_sync(GLib.BusType.SESSION, null); + } + catch(IOError e) { + print("%s", e.message); + } + + signal_id = conn.signal_subscribe("org.gnome.SettingsDaemon.Power", + "org.freedesktop.DBus.Properties", + "PropertiesChanged", null, null, + DBusSignalFlags.NONE, + signal_powerchanges); + + mixer = new Gvc.MixerControl("BD Volume Mixer"); + mixer.state_changed.connect(on_mixer_state_change); + mixer.default_sink_changed.connect(on_mixer_sink_changed); + mixer.open(); + + // wait a short while to allow the async mixer methods to complete otherwise + // we see the volume OSD on startup accidently when keymap changes are invoked + Timeout.add(200, () => { + initialising = false; + //map = Gdk.Keymap.get_for_display(Gdk.Display.get_default()); + //map.state_changed.connect(on_keymap_state_changed); + return false; + }); + } + + void on_mixer_sink_changed(uint id) { + set_default_mixer(); + } + + void set_default_mixer() { + if (stream != null) { + SignalHandler.disconnect(stream, notify_id); + } + + stream = mixer.get_default_sink(); + notify_id = stream.notify.connect(on_stream_notify); + update_volume(); + } + + void on_stream_notify(Object? o, ParamSpec? p) { + if (p.name == "volume" || p.name == "is-muted") { + update_volume(); + } + } + + /** + * Called when something changes on the mixer, i.e. we connected + * This is where we hook into the stream for changes + */ + void on_mixer_state_change(uint new_state) { + if (new_state == Gvc.MixerControlState.READY) { + set_default_mixer(); + } + } + + /** + * Update the OSD when something changes (volume/mute) + */ + void update_volume() { + if (initialising) return; + + var vol_norm = mixer.get_vol_max_norm(); + var vol = stream.get_volume(); + + /* Same maths as computed by volume.js in gnome-shell, carried over + * from C->Vala port of budgie-panel */ + int n = (int) Math.floor(3*vol/vol_norm)+1; + string image_name; + + // Work out an icon + if (stream.get_is_muted() || vol <= 0) { + image_name = "audio-volume-muted-symbolic"; + } else { + switch (n) { + case 1: + image_name = "audio-volume-low-symbolic"; + break; + case 2: + image_name = "audio-volume-medium-symbolic"; + break; + default: + image_name = "audio-volume-high-symbolic"; + break; + } + } + + var pct = (float)vol / (float)vol_norm; + if (pct > 1.0 && !stream.get_is_muted()) { + image_name = "audio-volume-overamplified-symbolic"; + } + + HashTable params; + params = new HashTable(null, null); + params.set("level", new Variant.double(pct)); + params.set("icon", new Variant.string(image_name)); + + osd.ShowOSD.begin(params); + } + + /* + we allow Gtk keymap to control when to display capslock/numlock state + It does appear though that there is a bug under wayland where the + state of the capslock/numlock is not set until another keymap is activated + This can be ctrl/shift as well as the caps/numlock. If the latter is activated + this can mean the OSD for keymap state is not activated on first use; only on + subsequent keypresses. + */ + private void on_keymap_state_changed() { + if (!firstrun) { + capslock = map.get_caps_lock_state(); + numlock = map.get_num_lock_state(); + firstrun = true; + } + + HashTable params = new HashTable(null, null); + string caption = ""; + bool skip = false; + if (map.get_caps_lock_state()) { + if (!capslock) { + params.set("icon", new Variant.string("caps-lock-symbolic")); + caption = _("Caps Lock is on"); + params.set("icon", new Variant.string("caps-lock-on-symbolic")); + } + capslock = true; + skip = true; + } else { + if (capslock) { + caption = _("Caps Lock is off"); + params.set("icon", new Variant.string("caps-lock-off-symbolic")); + } + capslock = false; + skip = true; + } + + if (!skip) { + if (map.get_num_lock_state()) { + if (!numlock) { + caption = _("Num Lock is on"); + params.set("icon", new Variant.string("num-lock-on-symbolic")); + } + numlock = true; + } else { + if (numlock) { + caption = _("Num Lock is off"); + params.set("icon", new Variant.string("num-lock-off-symbolic")); + } + numlock = false; + } + } + + if (caption != "") { + params.set("label", new Variant.string(caption)); + osd.ShowOSD.begin(params); + } + } + + private void signal_powerchanges(GLib.DBusConnection connection, + string? sender_name, + string object_path, + string interface_name, + string signal_name, + GLib.Variant parameters) { + if (initialising) return; + + GLib.VariantDict dict = new GLib.VariantDict(parameters.get_child_value(1)); + GLib.Variant? brightness = dict.lookup_value("Brightness", GLib.VariantType.INT32); + if (brightness == null) { + return; + } + + double level = (double) brightness.get_int32() / 100; + // nothing has changed therefore quit + // i.e. GSD power can and does signal more than just brightness changes + if (current_brightness_level == level) return; + + current_brightness_level = level; + + string icon = "display-brightness-symbolic"; + if (level == 1.0) { + icon = "display-brightness-high-symbolic"; + } else if (level == 0.0) { + icon = "display-brightness-low-symbolic"; + } + + HashTable params; + params = new HashTable(null, null); + params.set("level", new Variant.double(level)); + params.set("icon", new Variant.string(icon)); + + osd.ShowOSD.begin(params); + } + } + } diff --git a/src/dialogs/power/screenshot.vala b/src/dialogs/power/screenshot.vala new file mode 100644 index 000000000..30bbd1325 --- /dev/null +++ b/src/dialogs/power/screenshot.vala @@ -0,0 +1,121 @@ +/* + * This file is part of budgie-desktop + * + * Copyright Budgie Desktop Developers + * + * 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 2 of the License, or + * (at your option) any later version. + * + * Code has been inspired by the elementaryOS Gala ScreenshotManager.vala + * and the GNOME 42 shell-screenshot.c techniques. + */ + + +namespace Budgie { + const string EXTENSION = ".png"; + const string DBUS_SCREENSHOT = "org.buddiesofbudgie.BudgieScreenshot"; + const string DBUS_SCREENSHOT_PATH = "/org/buddiesofbudgie/Screenshot"; + + [DBus (name="org.buddiesofbudgie.BudgieScreenshot")] + public class ScreenshotManager : Object { + + [DBus (visible = false)] + public ScreenshotManager() { + } + + [DBus (visible = false)] + public void serve() { + /* Hook up screenshot dbus */ + Bus.own_name(BusType.SESSION, DBUS_SCREENSHOT, BusNameOwnerFlags.REPLACE, + on_bus_acquired, + () => {}, + () => warning("serve Could not acquire name\n") ); + } + + void on_bus_acquired(DBusConnection conn) { + try { + conn.register_object(DBUS_SCREENSHOT_PATH, this); + } catch (Error e) { + message("Unable to register Screenshot: %s", e.message); + } + } + + /* + stub function: we'll populate this for those window managers that support taking screenshots of the focussed window + */ + public bool SupportScreenshotWindow() throws DBusError, IOError { + return false; + } + + public async void screenshot(bool include_cursor, bool flash, string filename, out bool success, out string filename_used) throws DBusError, IOError { + success = false; + filename_used = ""; + + try { + filename_used = take_screenshot( filename, 0, 0, 0, 0, include_cursor); + + if (filename_used != "" ){ + success = true; + } + } + catch { + throw new DBusError.FAILED("Failed to take the screenshot"); + } + } + + /* + actually take the screenshot and if successful return the filename that was used_filename + */ + private string take_screenshot(string filename, int x, int y, int width, int height, bool include_cursor) throws Error { + string used_filename = filename; + try { + string cmd = "grim"; + if (include_cursor) { + cmd += " -c"; + } + + if (x != 0 || y != 0 || width != 0 || height != 0) { + cmd += " -g \"%d,%d %dx%d\"".printf(x, y, width, height); + } + + if (used_filename != "" && !Path.is_absolute(used_filename)) { + if (!used_filename.has_suffix(EXTENSION)) { + used_filename = used_filename.concat(EXTENSION); + } + var path = Environment.get_tmp_dir(); + used_filename = Path.build_filename(path, used_filename, null); + } + cmd += " " + used_filename; + Process.spawn_command_line_sync(cmd); + warning("command %s", cmd); + } catch (SpawnError e) { + warning("Error: %s\n", e.message); + used_filename = ""; + } + + return used_filename; + } + + public async void screenshot_area(int x, int y, int width, int height, bool include_cursor, bool flash, string filename, out bool success, out string filename_used) throws DBusError, IOError { + success = false; + filename_used = ""; + + try { + filename_used = take_screenshot( filename, x, y, width, height, include_cursor); + + if (filename_used != "" ){ + success = true; + } + } + catch { + throw new DBusError.FAILED("Failed to take the screenshot"); + } + } + + public async void screenshot_window(bool include_frame, bool include_cursor, bool flash, string filename, out bool success, out string filename_used) throws DBusError, IOError { + throw new DBusError.FAILED("Failed to save image"); + } + } +}