From a9a18eccb5c6c3f635cbccff3e3679bf31937f2c Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 9 Dec 2023 12:04:24 +0000 Subject: [PATCH 01/13] Initial work on screenshot portal --- data/pantheon.portal | 2 +- po/POTFILES | 1 + src/Screenshot/Dialog.vala | 145 ++++++++++++++++++++++++++++++ src/Screenshot/Portal.vala | 136 ++++++++++++++++++++++++++++ src/XdgDesktopPortalPantheon.vala | 3 + src/meson.build | 2 + 6 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 src/Screenshot/Dialog.vala create mode 100644 src/Screenshot/Portal.vala diff --git a/data/pantheon.portal b/data/pantheon.portal index 64dffdf3..813acc9d 100644 --- a/data/pantheon.portal +++ b/data/pantheon.portal @@ -1,4 +1,4 @@ [portal] DBusName=org.freedesktop.impl.portal.desktop.pantheon -Interfaces=org.freedesktop.impl.portal.Access;org.freedesktop.impl.portal.AppChooser;org.freedesktop.impl.portal.Background; +Interfaces=org.freedesktop.impl.portal.Access;org.freedesktop.impl.portal.AppChooser;org.freedesktop.impl.portal.Background;org.freedesktop.impl.portal.Screenshot; UseIn=pantheon diff --git a/po/POTFILES b/po/POTFILES index bc051e4e..40327e58 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -6,3 +6,4 @@ src/AppChooser/Portal.vala src/AppChooser/Dialog.vala src/AppChooser/AppButton.vala src/Background/NotificationRequest.vala +src/Screenshot/Dialog.vala \ No newline at end of file diff --git a/src/Screenshot/Dialog.vala b/src/Screenshot/Dialog.vala new file mode 100644 index 00000000..b84dda66 --- /dev/null +++ b/src/Screenshot/Dialog.vala @@ -0,0 +1,145 @@ +/* + * SPDX-FileCopyrightText: 2023 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +public class Screenshot.Dialog : Gtk.Window { + public string parent_window { get; construct; } + public bool permission_store_checked { get; construct; } + + public Dialog (string parent_window, bool modal, bool permission_store_checked) { + Object ( + resizable: false, + parent_window: parent_window, + modal: modal, + permission_store_checked: permission_store_checked + ); + } + + private Gtk.Image all_image; + + construct { + all_image = new Gtk.Image.from_icon_name ("grab-screen-symbolic"); + + var all = new Gtk.CheckButton () { + active = true, + tooltip_text = _("Grab the whole screen") + }; + all.add_css_class ("image-button"); + all_image.set_parent (all); + + var curr_image = new Gtk.Image.from_icon_name ("grab-window-symbolic"); + + var curr_window = new Gtk.CheckButton () { + group = all, + tooltip_text = _("Grab the current window") + }; + curr_window.add_css_class ("image-button"); + curr_image.set_parent (curr_window); + + var selection_image = new Gtk.Image.from_icon_name ("grab-area-symbolic"); + + var selection = new Gtk.CheckButton () { + group = all, + tooltip_text = _("Select area to grab") + }; + selection.add_css_class ("image-button"); + selection_image.set_parent (selection); + + var pointer_label = new Gtk.Label (_("Grab pointer:")) { + halign = END + }; + + var pointer_switch = new Gtk.Switch () { + halign = START + }; + + var close_label = new Gtk.Label (_("Close after saving:")) { + halign = END + }; + + var close_switch = new Gtk.Switch () { + halign = START + }; + + var redact_label = new Gtk.Label (_("Conceal text:")) { + halign = END + }; + + var redact_switch = new Gtk.Switch () { + halign = START + }; + + var delay_label = new Gtk.Label (_("Delay in seconds:")); + delay_label.halign = Gtk.Align.END; + + var delay_spin = new Gtk.SpinButton.with_range (0, 15, 1); + + var take_btn = new Gtk.Button.with_label (_("Take Screenshot")) { + receives_default = true + }; + take_btn.add_css_class (Granite.STYLE_CLASS_SUGGESTED_ACTION); + + var close_btn = new Gtk.Button.with_label (_("Close")); + + var radio_box = new Gtk.Box (HORIZONTAL, 18) { + halign = CENTER + }; + radio_box.append (all); + radio_box.append (curr_window); + radio_box.append (selection); + + var option_grid = new Gtk.Grid () { + column_spacing = 12, + row_spacing = 6 + }; + option_grid.attach (pointer_label, 0, 0); + option_grid.attach (pointer_switch, 1, 0); + option_grid.attach (close_label, 0, 1); + option_grid.attach (close_switch, 1, 1); + + option_grid.attach (redact_label, 0, 2); + option_grid.attach (redact_switch, 1, 2); + + option_grid.attach (delay_label, 0, 3); + option_grid.attach (delay_spin, 1, 3); + + var actions = new Gtk.Box (HORIZONTAL, 6) { + halign = END, + homogeneous = true + }; + actions.append (close_btn); + actions.append (take_btn); + + var box = new Gtk.Box (VERTICAL, 24) { + margin_top = 24, + margin_end = 12, + margin_bottom = 12, + margin_start = 12 + }; + box.append (radio_box); + box.append (option_grid); + box.append (actions); + + child = box; + + close_btn.clicked.connect (() => { + destroy (); + }); + + var gtk_settings = Gtk.Settings.get_default (); + gtk_settings.notify["gtk-application-prefer-dark-theme"].connect (() => { + update_icons (gtk_settings.gtk_application_prefer_dark_theme); + }); + + update_icons (gtk_settings.gtk_application_prefer_dark_theme); + } + + private void update_icons (bool prefers_dark) { + if (prefers_dark) { + all_image.icon_name = "grab-screen-symbolic-dark"; + } else { + all_image.icon_name = "grab-screen-symbolic"; + } + } +} \ No newline at end of file diff --git a/src/Screenshot/Portal.vala b/src/Screenshot/Portal.vala new file mode 100644 index 00000000..5e99257d --- /dev/null +++ b/src/Screenshot/Portal.vala @@ -0,0 +1,136 @@ +/* + * SPDX-FileCopyrightText: 2023 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +[DBus (name = "org.gnome.Shell.Screenshot")] +public interface Gala.ScreenshotProxy : Object { + public const string NAME = "org.gnome.Shell.Screenshot"; + public const string PATH = "/org/gnome/Shell/Screenshot"; + + public abstract async void conceal_text () throws GLib.Error; + public abstract async void screenshot (bool include_cursor, bool flash, string filename, out bool success, out string filename_used) throws GLib.Error; + public abstract async void screenshot_window (bool include_frame, bool include_cursor, bool flash, string filename, out bool success, out string filename_used) throws GLib.Error; + public abstract async void screenshot_area (int x, int y, int width, int height, bool flash, string filename, out bool success, out string filename_used) throws GLib.Error; + public abstract async void screenshot_area_with_cursor (int x, int y, int width, int height, bool include_cursor, bool flash, string filename, out bool success, out string filename_used) throws GLib.Error; + public abstract async void select_area (out int x, out int y, out int width, out int height) throws GLib.Error; + public abstract async void pick_color (out HashTable result) throws GLib.Error; +} + +[DBus (name = "org.freedesktop.impl.portal.Screenshot")] +public class Screenshot.Portal : Object { + private Gala.ScreenshotProxy screenshot_proxy; + private DBusConnection connection; + + public uint32 version { get; default = 2; } + + public Portal (DBusConnection connection) { + this.connection = connection; + + connection.get_proxy.begin ( + Gala.ScreenshotProxy.NAME, + Gala.ScreenshotProxy.PATH, + NONE, null, (obj, res) => { + try { + screenshot_proxy = connection.get_proxy.end (res); + } catch (GLib.Error e) { + warning ("Failed to get screenshot proxy, portal working with reduced functionality: %s", e.message); + } + } + ); + } + + public async void screenshot ( + ObjectPath handle, + string app_id, + string parent_window, + HashTable options, + out uint response, + out HashTable results + ) throws DBusError, IOError { + var modal = true; + var interactive = false; + var permission_store_checked = false; + + if (options["modal"] != null && options["modal"].get_type_string () == "b") { + modal = options["modal"].get_boolean (); + } + + if (options["interactive"] != null && options["interactive"].get_type_string () == "b") { + interactive = options["interactive"].get_boolean (); + } + + if (options["permission_store_checked"] != null && options["permission_store_checked"].get_type_string () == "b") { + permission_store_checked = options["permission_store_checked"].get_boolean (); + } + + debug ("screenshot: modal=%b, interactive=%b, permission_store_checked=%b", modal, interactive, permission_store_checked); + + if (!interactive && permission_store_checked) { + var success = false; + var filename_used = ""; + + try { + yield screenshot_proxy.screenshot (false, true, "", out success, out filename_used); + } catch (Error e) { + warning ("Couldn't call screenshot: %s\n", e.message); + response = 1; + results = new HashTable (str_hash, str_equal); + return; + } + + if (success) { + response = 0; + results = new HashTable (str_hash, str_equal); + results["filename"] = new Variant ("s", filename_used); + return; + } else { + response = 1; + results = new HashTable (str_hash, str_equal); + return; + } + } + + if (interactive) { + var dialog = new Dialog (parent_window, modal, permission_store_checked); + + dialog.show (); + } + + warning ("Unimplemented screenshot path, this should not be reached"); + response = 1; + results = new HashTable (str_hash, str_equal); + } + + public async void pick_color ( + ObjectPath handle, + string app_id, + string parent_window, + HashTable options, + out uint response, + out HashTable results + ) throws DBusError, IOError { + var _result = new HashTable (str_hash, str_equal); + + try { + yield screenshot_proxy.pick_color (out _result); + } catch (Error e) { + warning ("Couldn't call pick_color: %s\n", e.message); + response = 1; + results = new HashTable (str_hash, str_equal); + results["color"] = new Variant.array (new GLib.VariantType ("d"), { 0.0, 0.0, 0.0 }); + return; + } + + var color = _result["color"]; + if (color == null || color.get_type_string () != "(ddd)") { + response = 2; + results = new HashTable (str_hash, str_equal); + results["color"] = new Variant.array (new GLib.VariantType ("d"), { 0.0, 0.0, 0.0 }); + return; + } + + response = 0; + results = _result; + } +} \ No newline at end of file diff --git a/src/XdgDesktopPortalPantheon.vala b/src/XdgDesktopPortalPantheon.vala index 56faed74..ba4e3e49 100644 --- a/src/XdgDesktopPortalPantheon.vala +++ b/src/XdgDesktopPortalPantheon.vala @@ -40,6 +40,9 @@ private void on_bus_acquired (DBusConnection connection, string name) { connection.register_object ("/org/freedesktop/portal/desktop", new Background.Portal (connection)); debug ("Background Portal registered!"); + + connection.register_object ("/org/freedesktop/portal/desktop", new Screenshot.Portal (connection)); + debug ("Screenshot Portal registered!"); } catch (Error e) { critical ("Unable to register the object: %s", e.message); } diff --git a/src/meson.build b/src/meson.build index 4b88f74f..f004fc74 100644 --- a/src/meson.build +++ b/src/meson.build @@ -8,6 +8,8 @@ executable( 'AppChooser/Portal.vala', 'Background/NotificationRequest.vala', 'Background/Portal.vala', + 'Screenshot/Dialog.vala', + 'Screenshot/Portal.vala', configure_file(input: 'Config.vala.in', output: '@BASENAME@', configuration: conf_data), 'ExternalWindow.vala', 'XdgDesktopPortalPantheon.vala', From b06af27d63b22c009cc6a47ec4f1e8baac43e81d Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 11 Dec 2023 11:10:33 +0000 Subject: [PATCH 02/13] Add icon resources and fix property name in Screenshot.Portal class --- data/gresource.xml | 12 +++ data/icons/grab-area-symbolic.svg | 64 ++++++++++++++++ data/icons/grab-screen-symbolic-dark.svg | 93 ++++++++++++++++++++++++ data/icons/grab-screen-symbolic.svg | 74 +++++++++++++++++++ data/icons/grab-window-symbolic.svg | 71 ++++++++++++++++++ data/meson.build | 5 ++ meson.build | 1 + src/Screenshot/Portal.vala | 12 +-- src/XdgDesktopPortalPantheon.vala | 3 + src/meson.build | 1 + 10 files changed, 331 insertions(+), 5 deletions(-) create mode 100644 data/gresource.xml create mode 100644 data/icons/grab-area-symbolic.svg create mode 100644 data/icons/grab-screen-symbolic-dark.svg create mode 100644 data/icons/grab-screen-symbolic.svg create mode 100644 data/icons/grab-window-symbolic.svg diff --git a/data/gresource.xml b/data/gresource.xml new file mode 100644 index 00000000..a0172123 --- /dev/null +++ b/data/gresource.xml @@ -0,0 +1,12 @@ + + + + icons/grab-area-symbolic.svg + icons/grab-area-symbolic.svg + icons/grab-screen-symbolic.svg + icons/grab-screen-symbolic.svg + icons/grab-screen-symbolic-dark.svg + icons/grab-screen-symbolic-dark.svg + icons/grab-window-symbolic.svg + + \ No newline at end of file diff --git a/data/icons/grab-area-symbolic.svg b/data/icons/grab-area-symbolic.svg new file mode 100644 index 00000000..34ebd926 --- /dev/null +++ b/data/icons/grab-area-symbolic.svg @@ -0,0 +1,64 @@ + + + + + + + + + + image/svg+xml + + + + + + + diff --git a/data/icons/grab-screen-symbolic-dark.svg b/data/icons/grab-screen-symbolic-dark.svg new file mode 100644 index 00000000..7bc80c11 --- /dev/null +++ b/data/icons/grab-screen-symbolic-dark.svg @@ -0,0 +1,93 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/data/icons/grab-screen-symbolic.svg b/data/icons/grab-screen-symbolic.svg new file mode 100644 index 00000000..0b24c471 --- /dev/null +++ b/data/icons/grab-screen-symbolic.svg @@ -0,0 +1,74 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/data/icons/grab-window-symbolic.svg b/data/icons/grab-window-symbolic.svg new file mode 100644 index 00000000..ca1f6eac --- /dev/null +++ b/data/icons/grab-window-symbolic.svg @@ -0,0 +1,71 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/data/meson.build b/data/meson.build index 85490b18..e596191d 100644 --- a/data/meson.build +++ b/data/meson.build @@ -32,3 +32,8 @@ i18n.merge_file( install: true, install_dir: datadir / 'metainfo' ) + +icon_res = gnome.compile_resources( + 'screenshot-icon-resources', + 'gresource.xml' +) diff --git a/meson.build b/meson.build index 2d09a4f4..65dc7a87 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,6 @@ project('xdg-desktop-portal-pantheon', 'c', 'vala', version: '7.1.1', meson_version: '>=0.58') +gnome = import('gnome') i18n = import('i18n') prefix = get_option('prefix') diff --git a/src/Screenshot/Portal.vala b/src/Screenshot/Portal.vala index 5e99257d..3c26e366 100644 --- a/src/Screenshot/Portal.vala +++ b/src/Screenshot/Portal.vala @@ -22,6 +22,8 @@ public class Screenshot.Portal : Object { private Gala.ScreenshotProxy screenshot_proxy; private DBusConnection connection; + // Force the property name to be "version" instead of "Version" + [DBus (name = "version")] public uint32 version { get; default = 2; } public Portal (DBusConnection connection) { @@ -52,15 +54,15 @@ public class Screenshot.Portal : Object { var interactive = false; var permission_store_checked = false; - if (options["modal"] != null && options["modal"].get_type_string () == "b") { + if ("modal" in options && options["modal"].get_type_string () == "b") { modal = options["modal"].get_boolean (); } - if (options["interactive"] != null && options["interactive"].get_type_string () == "b") { + if ("interactive" in options && options["interactive"].get_type_string () == "b") { interactive = options["interactive"].get_boolean (); } - if (options["permission_store_checked"] != null && options["permission_store_checked"].get_type_string () == "b") { + if ("permission_store_checked" in options && options["permission_store_checked"].get_type_string () == "b") { permission_store_checked = options["permission_store_checked"].get_boolean (); } @@ -97,8 +99,8 @@ public class Screenshot.Portal : Object { dialog.show (); } - warning ("Unimplemented screenshot path, this should not be reached"); - response = 1; + warning ("Unimplemented screenshot code path, this should not be reached"); + response = 2; results = new HashTable (str_hash, str_equal); } diff --git a/src/XdgDesktopPortalPantheon.vala b/src/XdgDesktopPortalPantheon.vala index ba4e3e49..82c44ebd 100644 --- a/src/XdgDesktopPortalPantheon.vala +++ b/src/XdgDesktopPortalPantheon.vala @@ -83,6 +83,9 @@ int main (string[] args) { Gtk.init (); + weak Gtk.IconTheme default_theme = Gtk.IconTheme.get_for_display (Gdk.Display.get_default ()); + default_theme.add_resource_path ("/io/elementary/xdg-desktop-portal-pantheon"); + try { var opt_context = new OptionContext ("- portal backends"); opt_context.set_summary ("A backend implementation for xdg-desktop-portal."); diff --git a/src/meson.build b/src/meson.build index f004fc74..f66bf02e 100644 --- a/src/meson.build +++ b/src/meson.build @@ -13,6 +13,7 @@ executable( configure_file(input: 'Config.vala.in', output: '@BASENAME@', configuration: conf_data), 'ExternalWindow.vala', 'XdgDesktopPortalPantheon.vala', + icon_res, dependencies: [ glib_dep, gobject_dep, From 0d79bb22e18ef8494aa209b1f812b5a933965390 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 11 Dec 2023 11:18:55 +0000 Subject: [PATCH 03/13] Fix parent window association in Screenshot Dialog --- src/Screenshot/Dialog.vala | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/Screenshot/Dialog.vala b/src/Screenshot/Dialog.vala index b84dda66..80c08fda 100644 --- a/src/Screenshot/Dialog.vala +++ b/src/Screenshot/Dialog.vala @@ -6,7 +6,7 @@ public class Screenshot.Dialog : Gtk.Window { public string parent_window { get; construct; } public bool permission_store_checked { get; construct; } - + public Dialog (string parent_window, bool modal, bool permission_store_checked) { Object ( resizable: false, @@ -19,6 +19,16 @@ public class Screenshot.Dialog : Gtk.Window { private Gtk.Image all_image; construct { + if (parent_window != "") { + ((Gtk.Widget) this).realize.connect (() => { + try { + ExternalWindow.from_handle (parent_window).set_parent_of (get_surface ()); + } catch (Error e) { + warning ("Failed to associate portal window with parent %s: %s", parent_window, e.message); + } + }); + } + all_image = new Gtk.Image.from_icon_name ("grab-screen-symbolic"); var all = new Gtk.CheckButton () { @@ -121,7 +131,19 @@ public class Screenshot.Dialog : Gtk.Window { box.append (option_grid); box.append (actions); - child = box; + var window_handle = new Gtk.WindowHandle () { + child = box + }; + + child = window_handle; + + // We need to hide the title area + titlebar = new Gtk.Grid () { + visible = false + }; + + add_css_class ("dialog"); + add_css_class (Granite.STYLE_CLASS_MESSAGE_DIALOG); close_btn.clicked.connect (() => { destroy (); From 1655353477c53afdba0e04458cc04a32b14cf939 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 11 Dec 2023 13:12:23 +0000 Subject: [PATCH 04/13] Hook up a bunch of the functionality --- src/Screenshot/Dialog.vala | 65 ++++++++++++++++----- src/Screenshot/Portal.vala | 112 ++++++++++++++++++++++++++++++++----- 2 files changed, 150 insertions(+), 27 deletions(-) diff --git a/src/Screenshot/Dialog.vala b/src/Screenshot/Dialog.vala index 80c08fda..f0670a69 100644 --- a/src/Screenshot/Dialog.vala +++ b/src/Screenshot/Dialog.vala @@ -4,9 +4,22 @@ */ public class Screenshot.Dialog : Gtk.Window { + public enum ScreenshotType { + ALL, + WINDOW, + AREA + } + + public signal void response (Gtk.ResponseType response_type); + public string parent_window { get; construct; } public bool permission_store_checked { get; construct; } + public ScreenshotType screenshot_type { get; private set; default = ScreenshotType.ALL; } + public bool grab_pointer { get; private set; default = false; } + public bool redact_text { get; private set; default = false; } + public int delay { get; private set; default = 0; } + public Dialog (string parent_window, bool modal, bool permission_store_checked) { Object ( resizable: false, @@ -38,6 +51,12 @@ public class Screenshot.Dialog : Gtk.Window { all.add_css_class ("image-button"); all_image.set_parent (all); + all.toggled.connect (() => { + if (all.active) { + screenshot_type = ScreenshotType.ALL; + } + }); + var curr_image = new Gtk.Image.from_icon_name ("grab-window-symbolic"); var curr_window = new Gtk.CheckButton () { @@ -47,6 +66,12 @@ public class Screenshot.Dialog : Gtk.Window { curr_window.add_css_class ("image-button"); curr_image.set_parent (curr_window); + curr_window.toggled.connect (() => { + if (curr_window.active) { + screenshot_type = ScreenshotType.WINDOW; + } + }); + var selection_image = new Gtk.Image.from_icon_name ("grab-area-symbolic"); var selection = new Gtk.CheckButton () { @@ -56,6 +81,12 @@ public class Screenshot.Dialog : Gtk.Window { selection.add_css_class ("image-button"); selection_image.set_parent (selection); + selection.toggled.connect (() => { + if (selection.active) { + screenshot_type = ScreenshotType.AREA; + } + }); + var pointer_label = new Gtk.Label (_("Grab pointer:")) { halign = END }; @@ -64,13 +95,9 @@ public class Screenshot.Dialog : Gtk.Window { halign = START }; - var close_label = new Gtk.Label (_("Close after saving:")) { - halign = END - }; - - var close_switch = new Gtk.Switch () { - halign = START - }; + pointer_switch.activate.connect (() => { + grab_pointer = pointer_switch.active; + }); var redact_label = new Gtk.Label (_("Conceal text:")) { halign = END @@ -80,16 +107,28 @@ public class Screenshot.Dialog : Gtk.Window { halign = START }; + redact_switch.activate.connect (() => { + redact_text = redact_switch.active; + }); + var delay_label = new Gtk.Label (_("Delay in seconds:")); delay_label.halign = Gtk.Align.END; var delay_spin = new Gtk.SpinButton.with_range (0, 15, 1); + delay_spin.value_changed.connect (() => { + delay = (int) delay_spin.value; + }); + var take_btn = new Gtk.Button.with_label (_("Take Screenshot")) { receives_default = true }; take_btn.add_css_class (Granite.STYLE_CLASS_SUGGESTED_ACTION); + take_btn.clicked.connect (() => { + response (Gtk.ResponseType.OK); + }); + var close_btn = new Gtk.Button.with_label (_("Close")); var radio_box = new Gtk.Box (HORIZONTAL, 18) { @@ -105,14 +144,12 @@ public class Screenshot.Dialog : Gtk.Window { }; option_grid.attach (pointer_label, 0, 0); option_grid.attach (pointer_switch, 1, 0); - option_grid.attach (close_label, 0, 1); - option_grid.attach (close_switch, 1, 1); - option_grid.attach (redact_label, 0, 2); - option_grid.attach (redact_switch, 1, 2); + option_grid.attach (redact_label, 0, 1); + option_grid.attach (redact_switch, 1, 1); - option_grid.attach (delay_label, 0, 3); - option_grid.attach (delay_spin, 1, 3); + option_grid.attach (delay_label, 0, 2); + option_grid.attach (delay_spin, 1, 2); var actions = new Gtk.Box (HORIZONTAL, 6) { halign = END, @@ -146,7 +183,7 @@ public class Screenshot.Dialog : Gtk.Window { add_css_class (Granite.STYLE_CLASS_MESSAGE_DIALOG); close_btn.clicked.connect (() => { - destroy (); + response (Gtk.ResponseType.CLOSE); }); var gtk_settings = Gtk.Settings.get_default (); diff --git a/src/Screenshot/Portal.vala b/src/Screenshot/Portal.vala index 3c26e366..1c90aa09 100644 --- a/src/Screenshot/Portal.vala +++ b/src/Screenshot/Portal.vala @@ -42,6 +42,66 @@ public class Screenshot.Portal : Object { ); } + private async void do_delay (int seconds) { + if (seconds > 0) { + GLib.Timeout.add_seconds (seconds, () => { + do_delay.callback (); + return false; + }); + + yield; + } + } + + private async string do_screenshot ( + Dialog.ScreenshotType screenshot_type, + bool grab_pointer, + bool redact, + int delay + ) throws GLib.Error { + string filename_used = ""; + switch (screenshot_type) { + case Dialog.ScreenshotType.ALL: + var success = false; + + yield do_delay (delay); + yield screenshot_proxy.screenshot (grab_pointer, true, "/tmp/portal_screenshot.png", out success, out filename_used); + + if (!success) { + throw new GLib.IOError.FAILED ("Failed to take screenshot"); + } + + break; + case Dialog.ScreenshotType.WINDOW: + var success = false; + + yield do_delay (delay); + yield screenshot_proxy.screenshot_window (false, grab_pointer, true, "/tmp/portal_screenshot.png", out success, out filename_used); + + if (!success) { + throw new GLib.IOError.FAILED ("Failed to take screenshot"); + } + + break; + case Dialog.ScreenshotType.AREA: + var success = false; + + int x, y, width, height; + yield screenshot_proxy.select_area (out x, out y, out width, out height); + + yield do_delay (delay); + yield screenshot_proxy.screenshot_area (x, y, width, height, true, "/tmp/portal_screenshot.png", out success, out filename_used); + + if (!success) { + throw new GLib.IOError.FAILED ("Failed to take screenshot"); + } + + break; + } + + return GLib.Filename.to_uri (filename_used, null); + } + public async void screenshot ( ObjectPath handle, string app_id, @@ -69,11 +129,10 @@ public class Screenshot.Portal : Object { debug ("screenshot: modal=%b, interactive=%b, permission_store_checked=%b", modal, interactive, permission_store_checked); if (!interactive && permission_store_checked) { - var success = false; - var filename_used = ""; + var uri = ""; try { - yield screenshot_proxy.screenshot (false, true, "", out success, out filename_used); + uri = yield do_screenshot (Dialog.ScreenshotType.ALL, false, false, 0); } catch (Error e) { warning ("Couldn't call screenshot: %s\n", e.message); response = 1; @@ -81,22 +140,49 @@ public class Screenshot.Portal : Object { return; } - if (success) { - response = 0; - results = new HashTable (str_hash, str_equal); - results["filename"] = new Variant ("s", filename_used); - return; - } else { - response = 1; - results = new HashTable (str_hash, str_equal); - return; - } + response = 0; + results = new HashTable (str_hash, str_equal); + results["uri"] = uri; + return; } if (interactive) { var dialog = new Dialog (parent_window, modal, permission_store_checked); + bool cancelled = true; + dialog.response.connect ((response_id) => { + if (response_id == Gtk.ResponseType.OK) { + cancelled = false; + } + + screenshot.callback (); + }); + dialog.show (); + yield; + + dialog.destroy (); + + if (cancelled) { + response = 1; + results = new HashTable (str_hash, str_equal); + return; + } + + var uri = ""; + try { + uri = yield do_screenshot (dialog.screenshot_type, dialog.grab_pointer, dialog.redact_text, dialog.delay); + } catch (Error e) { + warning ("Couldn't call screenshot: %s\n", e.message); + response = 2; + results = new HashTable (str_hash, str_equal); + return; + } + + response = 0; + results = new HashTable (str_hash, str_equal); + results["uri"] = uri; + return; } warning ("Unimplemented screenshot code path, this should not be reached"); From 7861d2e30d92babf031f10e59429c6234da17fe1 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 11 Dec 2023 13:41:44 +0000 Subject: [PATCH 05/13] Add approval flow --- po/POTFILES | 3 +- src/Screenshot/ApprovalDialog.vala | 78 +++++++++++++++ src/Screenshot/Portal.vala | 94 +++++++++++++++++-- .../{Dialog.vala => SetupDialog.vala} | 8 +- src/meson.build | 3 +- 5 files changed, 169 insertions(+), 17 deletions(-) create mode 100644 src/Screenshot/ApprovalDialog.vala rename src/Screenshot/{Dialog.vala => SetupDialog.vala} (95%) diff --git a/po/POTFILES b/po/POTFILES index 40327e58..9d7aedce 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -6,4 +6,5 @@ src/AppChooser/Portal.vala src/AppChooser/Dialog.vala src/AppChooser/AppButton.vala src/Background/NotificationRequest.vala -src/Screenshot/Dialog.vala \ No newline at end of file +src/Screenshot/ApprovalDialog.vala +src/Screenshot/SetupDialog.vala \ No newline at end of file diff --git a/src/Screenshot/ApprovalDialog.vala b/src/Screenshot/ApprovalDialog.vala new file mode 100644 index 00000000..5e5ac372 --- /dev/null +++ b/src/Screenshot/ApprovalDialog.vala @@ -0,0 +1,78 @@ +/* + * SPDX-FileCopyrightText: 2023 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + + public class Screenshot.ApprovalDialog : Gtk.Window { + public signal void response (Gtk.ResponseType response_type); + + public string parent_window { get; construct; } + public string screenshot_uri { get; construct; } + + public ApprovalDialog (string parent_window, bool modal, string screenshot_uri) { + Object ( + resizable: false, + parent_window: parent_window, + modal: modal, + screenshot_uri: screenshot_uri + ); + } + + construct { + if (parent_window != "") { + ((Gtk.Widget) this).realize.connect (() => { + try { + ExternalWindow.from_handle (parent_window).set_parent_of (get_surface ()); + } catch (Error e) { + warning ("Failed to associate portal window with parent %s: %s", parent_window, e.message); + } + }); + } + + // TODO: Add a screenshot preview and wording + + var allow_button = new Gtk.Button.with_label (_("Share Screenshot")) { + receives_default = true + }; + allow_button.add_css_class (Granite.STYLE_CLASS_SUGGESTED_ACTION); + + allow_button.clicked.connect (() => { + response (Gtk.ResponseType.OK); + }); + + var close_btn = new Gtk.Button.with_label (_("Close")); + + var actions = new Gtk.Box (HORIZONTAL, 6) { + halign = END, + homogeneous = true + }; + actions.append (close_btn); + actions.append (allow_button); + + var box = new Gtk.Box (VERTICAL, 24) { + margin_top = 24, + margin_end = 12, + margin_bottom = 12, + margin_start = 12 + }; + box.append (actions); + + var window_handle = new Gtk.WindowHandle () { + child = box + }; + + child = window_handle; + + // We need to hide the title area + titlebar = new Gtk.Grid () { + visible = false + }; + + add_css_class ("dialog"); + add_css_class (Granite.STYLE_CLASS_MESSAGE_DIALOG); + + close_btn.clicked.connect (() => { + response (Gtk.ResponseType.CLOSE); + }); + } +} \ No newline at end of file diff --git a/src/Screenshot/Portal.vala b/src/Screenshot/Portal.vala index 1c90aa09..75b97397 100644 --- a/src/Screenshot/Portal.vala +++ b/src/Screenshot/Portal.vala @@ -54,14 +54,14 @@ public class Screenshot.Portal : Object { } private async string do_screenshot ( - Dialog.ScreenshotType screenshot_type, + SetupDialog.ScreenshotType screenshot_type, bool grab_pointer, bool redact, int delay ) throws GLib.Error { string filename_used = ""; switch (screenshot_type) { - case Dialog.ScreenshotType.ALL: + case SetupDialog.ScreenshotType.ALL: var success = false; yield do_delay (delay); @@ -72,7 +72,7 @@ public class Screenshot.Portal : Object { } break; - case Dialog.ScreenshotType.WINDOW: + case SetupDialog.ScreenshotType.WINDOW: var success = false; yield do_delay (delay); @@ -83,7 +83,7 @@ public class Screenshot.Portal : Object { } break; - case Dialog.ScreenshotType.AREA: + case SetupDialog.ScreenshotType.AREA: var success = false; int x, y, width, height; @@ -128,11 +128,12 @@ public class Screenshot.Portal : Object { debug ("screenshot: modal=%b, interactive=%b, permission_store_checked=%b", modal, interactive, permission_store_checked); + // Non-interactive screenshots for a pre-approved app, just take a fullscreen screenshot and send it if (!interactive && permission_store_checked) { var uri = ""; try { - uri = yield do_screenshot (Dialog.ScreenshotType.ALL, false, false, 0); + uri = yield do_screenshot (SetupDialog.ScreenshotType.ALL, false, false, 0); } catch (Error e) { warning ("Couldn't call screenshot: %s\n", e.message); response = 1; @@ -146,8 +147,50 @@ public class Screenshot.Portal : Object { return; } + if (!interactive && !permission_store_checked) { + var uri = ""; + + try { + uri = yield do_screenshot (SetupDialog.ScreenshotType.ALL, false, false, 0); + } catch (Error e) { + warning ("Couldn't call screenshot: %s\n", e.message); + response = 1; + results = new HashTable (str_hash, str_equal); + return; + } + + // This app has not been pre-approved to take screenshots, so we prompt the user + var dialog = new ApprovalDialog (parent_window, modal, uri); + + bool cancelled = true; + dialog.response.connect ((response_id) => { + if (response_id == Gtk.ResponseType.OK) { + cancelled = false; + } + + screenshot.callback (); + }); + + dialog.show (); + yield; + + + dialog.destroy (); + + if (cancelled) { + response = 1; + results = new HashTable (str_hash, str_equal); + return; + } else { + response = 0; + results = new HashTable (str_hash, str_equal); + results["uri"] = uri; + return; + } + } + if (interactive) { - var dialog = new Dialog (parent_window, modal, permission_store_checked); + var dialog = new SetupDialog (parent_window, modal); bool cancelled = true; dialog.response.connect ((response_id) => { @@ -179,10 +222,41 @@ public class Screenshot.Portal : Object { return; } - response = 0; - results = new HashTable (str_hash, str_equal); - results["uri"] = uri; - return; + // The user has already approved this app to take screenshots, so we send the screenshot without prompting + if (permission_store_checked) { + response = 0; + results = new HashTable (str_hash, str_equal); + results["uri"] = uri; + return; + } else { + // This app has not been pre-approved to take screenshots, so we prompt the user + var approval_dialog = new ApprovalDialog (parent_window, modal, uri); + + bool approval_cancelled = true; + approval_dialog.response.connect ((response_id) => { + if (response_id == Gtk.ResponseType.OK) { + approval_cancelled = false; + } + + screenshot.callback (); + }); + + approval_dialog.show (); + yield; + + approval_dialog.destroy (); + + if (approval_cancelled) { + response = 1; + results = new HashTable (str_hash, str_equal); + return; + } else { + response = 0; + results = new HashTable (str_hash, str_equal); + results["uri"] = uri; + return; + } + } } warning ("Unimplemented screenshot code path, this should not be reached"); diff --git a/src/Screenshot/Dialog.vala b/src/Screenshot/SetupDialog.vala similarity index 95% rename from src/Screenshot/Dialog.vala rename to src/Screenshot/SetupDialog.vala index f0670a69..c7939499 100644 --- a/src/Screenshot/Dialog.vala +++ b/src/Screenshot/SetupDialog.vala @@ -3,7 +3,7 @@ * SPDX-License-Identifier: LGPL-2.1-or-later */ -public class Screenshot.Dialog : Gtk.Window { +public class Screenshot.SetupDialog : Gtk.Window { public enum ScreenshotType { ALL, WINDOW, @@ -13,19 +13,17 @@ public class Screenshot.Dialog : Gtk.Window { public signal void response (Gtk.ResponseType response_type); public string parent_window { get; construct; } - public bool permission_store_checked { get; construct; } public ScreenshotType screenshot_type { get; private set; default = ScreenshotType.ALL; } public bool grab_pointer { get; private set; default = false; } public bool redact_text { get; private set; default = false; } public int delay { get; private set; default = 0; } - public Dialog (string parent_window, bool modal, bool permission_store_checked) { + public SetupDialog (string parent_window, bool modal) { Object ( resizable: false, parent_window: parent_window, - modal: modal, - permission_store_checked: permission_store_checked + modal: modal ); } diff --git a/src/meson.build b/src/meson.build index f66bf02e..e9b6771b 100644 --- a/src/meson.build +++ b/src/meson.build @@ -8,8 +8,9 @@ executable( 'AppChooser/Portal.vala', 'Background/NotificationRequest.vala', 'Background/Portal.vala', - 'Screenshot/Dialog.vala', + 'Screenshot/ApprovalDialog.vala', 'Screenshot/Portal.vala', + 'Screenshot/SetupDialog.vala', configure_file(input: 'Config.vala.in', output: '@BASENAME@', configuration: conf_data), 'ExternalWindow.vala', 'XdgDesktopPortalPantheon.vala', From 8c9c4b73f832f0b500b668c4e6f18e9f8cde988e Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 11 Dec 2023 14:21:09 +0000 Subject: [PATCH 06/13] Add a preview to the approval dialog --- src/Screenshot/ApprovalDialog.vala | 35 ++++++++++++++++++++++++++++-- src/Screenshot/Portal.vala | 4 ++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/Screenshot/ApprovalDialog.vala b/src/Screenshot/ApprovalDialog.vala index 5e5ac372..6de848d5 100644 --- a/src/Screenshot/ApprovalDialog.vala +++ b/src/Screenshot/ApprovalDialog.vala @@ -7,13 +7,15 @@ public signal void response (Gtk.ResponseType response_type); public string parent_window { get; construct; } + public string app_id { get; construct; } public string screenshot_uri { get; construct; } - public ApprovalDialog (string parent_window, bool modal, string screenshot_uri) { + public ApprovalDialog (string parent_window, bool modal, string app_id, string screenshot_uri) { Object ( resizable: false, parent_window: parent_window, modal: modal, + app_id: app_id, screenshot_uri: screenshot_uri ); } @@ -29,7 +31,32 @@ }); } - // TODO: Add a screenshot preview and wording + var title = new Gtk.Label (_("Share Screenshot")) { + max_width_chars = 0, // Wrap, but secondary label sets the width + selectable = true, + wrap = true, + xalign = 0 + }; + title.add_css_class (Granite.STYLE_CLASS_TITLE_LABEL); + + var subtitle = new Gtk.Label (_("Share this screenshot with the requesting app?")) { + max_width_chars = 50, + width_chars = 50, // Prevents a bug where extra height is preserved + selectable = true, + wrap = true, + xalign = 0 + }; + + if (app_id != null) { + var app_info = new GLib.DesktopAppInfo (app_id + ".desktop"); + if (app_info != null) { + subtitle.label = _("Share this screenshot with %s?").printf (app_info.get_display_name ()); + } + } + + var screenshot_filename = GLib.Filename.from_uri (screenshot_uri); + var screenshot_image = new Gtk.Picture.for_pixbuf (new Gdk.Pixbuf.from_file_at_scale (screenshot_filename, 384, 384, true)) { + }; var allow_button = new Gtk.Button.with_label (_("Share Screenshot")) { receives_default = true @@ -55,6 +82,10 @@ margin_bottom = 12, margin_start = 12 }; + box.add_css_class ("dialog-vbox"); + box.append (title); + box.append (subtitle); + box.append (screenshot_image); box.append (actions); var window_handle = new Gtk.WindowHandle () { diff --git a/src/Screenshot/Portal.vala b/src/Screenshot/Portal.vala index 75b97397..ccda9e5f 100644 --- a/src/Screenshot/Portal.vala +++ b/src/Screenshot/Portal.vala @@ -160,7 +160,7 @@ public class Screenshot.Portal : Object { } // This app has not been pre-approved to take screenshots, so we prompt the user - var dialog = new ApprovalDialog (parent_window, modal, uri); + var dialog = new ApprovalDialog (parent_window, modal, app_id, uri); bool cancelled = true; dialog.response.connect ((response_id) => { @@ -230,7 +230,7 @@ public class Screenshot.Portal : Object { return; } else { // This app has not been pre-approved to take screenshots, so we prompt the user - var approval_dialog = new ApprovalDialog (parent_window, modal, uri); + var approval_dialog = new ApprovalDialog (parent_window, modal, app_id, uri); bool approval_cancelled = true; approval_dialog.response.connect ((response_id) => { From 6458e0469af1ec59a017e9118e51b00c1d522dd3 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 11 Dec 2023 14:35:25 +0000 Subject: [PATCH 07/13] Fix newline at end of file in ApprovalDialog, Portal, and SetupDialog --- src/Screenshot/ApprovalDialog.vala | 2 +- src/Screenshot/Portal.vala | 2 +- src/Screenshot/SetupDialog.vala | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Screenshot/ApprovalDialog.vala b/src/Screenshot/ApprovalDialog.vala index 6de848d5..354f238f 100644 --- a/src/Screenshot/ApprovalDialog.vala +++ b/src/Screenshot/ApprovalDialog.vala @@ -106,4 +106,4 @@ response (Gtk.ResponseType.CLOSE); }); } -} \ No newline at end of file +} diff --git a/src/Screenshot/Portal.vala b/src/Screenshot/Portal.vala index ccda9e5f..b6ea85ca 100644 --- a/src/Screenshot/Portal.vala +++ b/src/Screenshot/Portal.vala @@ -295,4 +295,4 @@ public class Screenshot.Portal : Object { response = 0; results = _result; } -} \ No newline at end of file +} diff --git a/src/Screenshot/SetupDialog.vala b/src/Screenshot/SetupDialog.vala index c7939499..b16d551b 100644 --- a/src/Screenshot/SetupDialog.vala +++ b/src/Screenshot/SetupDialog.vala @@ -199,4 +199,4 @@ public class Screenshot.SetupDialog : Gtk.Window { all_image.icon_name = "grab-screen-symbolic"; } } -} \ No newline at end of file +} From 326d5bf90610cf8c84877581da3432f08351d577 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 11 Dec 2023 14:48:45 +0000 Subject: [PATCH 08/13] Update screenshot method to include cursor --- src/Screenshot/Portal.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Screenshot/Portal.vala b/src/Screenshot/Portal.vala index b6ea85ca..9a66076c 100644 --- a/src/Screenshot/Portal.vala +++ b/src/Screenshot/Portal.vala @@ -90,7 +90,7 @@ public class Screenshot.Portal : Object { yield screenshot_proxy.select_area (out x, out y, out width, out height); yield do_delay (delay); - yield screenshot_proxy.screenshot_area (x, y, width, height, true, "/tmp/portal_screenshot.png", out success, out filename_used); + yield screenshot_proxy.screenshot_area_with_cursor (x, y, width, height, grab_pointer, true, "/tmp/portal_screenshot.png", out success, out filename_used); if (!success) { throw new GLib.IOError.FAILED ("Failed to take screenshot"); From 9e5ab70e3104bd3a422629b3fe3880b13909b1a4 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 11 Dec 2023 14:56:25 +0000 Subject: [PATCH 09/13] Add redaction feature to screenshot functionality --- src/Screenshot/Portal.vala | 12 ++++++++++++ src/Screenshot/SetupDialog.vala | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Screenshot/Portal.vala b/src/Screenshot/Portal.vala index 9a66076c..56a2fb6a 100644 --- a/src/Screenshot/Portal.vala +++ b/src/Screenshot/Portal.vala @@ -65,6 +65,10 @@ public class Screenshot.Portal : Object { var success = false; yield do_delay (delay); + if (redact) { + yield screenshot_proxy.conceal_text (); + yield do_delay (1); + } yield screenshot_proxy.screenshot (grab_pointer, true, "/tmp/portal_screenshot.png", out success, out filename_used); if (!success) { @@ -76,6 +80,10 @@ public class Screenshot.Portal : Object { var success = false; yield do_delay (delay); + if (redact) { + yield screenshot_proxy.conceal_text (); + yield do_delay (1); + } yield screenshot_proxy.screenshot_window (false, grab_pointer, true, "/tmp/portal_screenshot.png", out success, out filename_used); if (!success) { @@ -90,6 +98,10 @@ public class Screenshot.Portal : Object { yield screenshot_proxy.select_area (out x, out y, out width, out height); yield do_delay (delay); + if (redact) { + yield screenshot_proxy.conceal_text (); + yield do_delay (1); + } yield screenshot_proxy.screenshot_area_with_cursor (x, y, width, height, grab_pointer, true, "/tmp/portal_screenshot.png", out success, out filename_used); if (!success) { diff --git a/src/Screenshot/SetupDialog.vala b/src/Screenshot/SetupDialog.vala index b16d551b..ff0d432f 100644 --- a/src/Screenshot/SetupDialog.vala +++ b/src/Screenshot/SetupDialog.vala @@ -93,7 +93,7 @@ public class Screenshot.SetupDialog : Gtk.Window { halign = START }; - pointer_switch.activate.connect (() => { + pointer_switch.state_set.connect (() => { grab_pointer = pointer_switch.active; }); @@ -105,7 +105,7 @@ public class Screenshot.SetupDialog : Gtk.Window { halign = START }; - redact_switch.activate.connect (() => { + redact_switch.state_set.connect (() => { redact_text = redact_switch.active; }); From 451a65d618a4c80e163815f688a9f2698ec4fae5 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 11 Dec 2023 16:26:51 +0000 Subject: [PATCH 10/13] Refactor Screenshot.Portal class: Initialize results HashTable earlier --- src/Screenshot/Portal.vala | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/Screenshot/Portal.vala b/src/Screenshot/Portal.vala index 56a2fb6a..1ef0f1c2 100644 --- a/src/Screenshot/Portal.vala +++ b/src/Screenshot/Portal.vala @@ -126,6 +126,8 @@ public class Screenshot.Portal : Object { var interactive = false; var permission_store_checked = false; + results = new HashTable (str_hash, str_equal); + if ("modal" in options && options["modal"].get_type_string () == "b") { modal = options["modal"].get_boolean (); } @@ -149,12 +151,10 @@ public class Screenshot.Portal : Object { } catch (Error e) { warning ("Couldn't call screenshot: %s\n", e.message); response = 1; - results = new HashTable (str_hash, str_equal); return; } response = 0; - results = new HashTable (str_hash, str_equal); results["uri"] = uri; return; } @@ -167,7 +167,6 @@ public class Screenshot.Portal : Object { } catch (Error e) { warning ("Couldn't call screenshot: %s\n", e.message); response = 1; - results = new HashTable (str_hash, str_equal); return; } @@ -191,11 +190,9 @@ public class Screenshot.Portal : Object { if (cancelled) { response = 1; - results = new HashTable (str_hash, str_equal); return; } else { response = 0; - results = new HashTable (str_hash, str_equal); results["uri"] = uri; return; } @@ -220,7 +217,6 @@ public class Screenshot.Portal : Object { if (cancelled) { response = 1; - results = new HashTable (str_hash, str_equal); return; } @@ -230,14 +226,12 @@ public class Screenshot.Portal : Object { } catch (Error e) { warning ("Couldn't call screenshot: %s\n", e.message); response = 2; - results = new HashTable (str_hash, str_equal); return; } // The user has already approved this app to take screenshots, so we send the screenshot without prompting if (permission_store_checked) { response = 0; - results = new HashTable (str_hash, str_equal); results["uri"] = uri; return; } else { @@ -260,11 +254,9 @@ public class Screenshot.Portal : Object { if (approval_cancelled) { response = 1; - results = new HashTable (str_hash, str_equal); return; } else { response = 0; - results = new HashTable (str_hash, str_equal); results["uri"] = uri; return; } From dad11d2d3bc0f4b90b1a2f36b1f195a69b58fbb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Thu, 14 Dec 2023 09:25:33 -0800 Subject: [PATCH 11/13] Approval dialog design, setup dialog fixes (#95) --- src/Screenshot/ApprovalDialog.vala | 97 +++++++----------------------- src/Screenshot/SetupDialog.vala | 32 ++++++---- 2 files changed, 43 insertions(+), 86 deletions(-) diff --git a/src/Screenshot/ApprovalDialog.vala b/src/Screenshot/ApprovalDialog.vala index 354f238f..796ade75 100644 --- a/src/Screenshot/ApprovalDialog.vala +++ b/src/Screenshot/ApprovalDialog.vala @@ -3,21 +3,26 @@ * SPDX-License-Identifier: LGPL-2.1-or-later */ - public class Screenshot.ApprovalDialog : Gtk.Window { - public signal void response (Gtk.ResponseType response_type); - + public class Screenshot.ApprovalDialog : Granite.MessageDialog { public string parent_window { get; construct; } - public string app_id { get; construct; } public string screenshot_uri { get; construct; } public ApprovalDialog (string parent_window, bool modal, string app_id, string screenshot_uri) { Object ( - resizable: false, parent_window: parent_window, modal: modal, - app_id: app_id, - screenshot_uri: screenshot_uri + screenshot_uri: screenshot_uri, + primary_text: _("Share this screenshot with the requesting app?"), + secondary_text: _("Only the app which requested this screenshot will be able to see it. This screenshot will not be saved elsewhere."), + buttons: Gtk.ButtonsType.CANCEL ); + + if (app_id != null) { + var app_info = new GLib.DesktopAppInfo (app_id + ".desktop"); + if (app_info != null) { + primary_text = _("Share this screenshot with ā€œ%sā€?").printf (app_info.get_display_name ()); + } + } } construct { @@ -31,79 +36,21 @@ }); } - var title = new Gtk.Label (_("Share Screenshot")) { - max_width_chars = 0, // Wrap, but secondary label sets the width - selectable = true, - wrap = true, - xalign = 0 - }; - title.add_css_class (Granite.STYLE_CLASS_TITLE_LABEL); - - var subtitle = new Gtk.Label (_("Share this screenshot with the requesting app?")) { - max_width_chars = 50, - width_chars = 50, // Prevents a bug where extra height is preserved - selectable = true, - wrap = true, - xalign = 0 - }; - - if (app_id != null) { - var app_info = new GLib.DesktopAppInfo (app_id + ".desktop"); - if (app_info != null) { - subtitle.label = _("Share this screenshot with %s?").printf (app_info.get_display_name ()); - } - } + image_icon = new ThemedIcon ("io.elementary.screenshot"); var screenshot_filename = GLib.Filename.from_uri (screenshot_uri); - var screenshot_image = new Gtk.Picture.for_pixbuf (new Gdk.Pixbuf.from_file_at_scale (screenshot_filename, 384, 384, true)) { + var screenshot_image = new Gtk.Picture.for_pixbuf (new Gdk.Pixbuf.from_file_at_scale (screenshot_filename, 400, 400, true)) { + overflow = HIDDEN, + width_request = 400 }; + screenshot_image.add_css_class (Granite.STYLE_CLASS_CARD); + screenshot_image.add_css_class (Granite.STYLE_CLASS_ROUNDED); + screenshot_image.add_css_class (Granite.STYLE_CLASS_CHECKERBOARD); - var allow_button = new Gtk.Button.with_label (_("Share Screenshot")) { - receives_default = true - }; + var allow_button = add_button (_("Share Screenshot"), Gtk.ResponseType.OK); + allow_button.receives_default = true; allow_button.add_css_class (Granite.STYLE_CLASS_SUGGESTED_ACTION); - allow_button.clicked.connect (() => { - response (Gtk.ResponseType.OK); - }); - - var close_btn = new Gtk.Button.with_label (_("Close")); - - var actions = new Gtk.Box (HORIZONTAL, 6) { - halign = END, - homogeneous = true - }; - actions.append (close_btn); - actions.append (allow_button); - - var box = new Gtk.Box (VERTICAL, 24) { - margin_top = 24, - margin_end = 12, - margin_bottom = 12, - margin_start = 12 - }; - box.add_css_class ("dialog-vbox"); - box.append (title); - box.append (subtitle); - box.append (screenshot_image); - box.append (actions); - - var window_handle = new Gtk.WindowHandle () { - child = box - }; - - child = window_handle; - - // We need to hide the title area - titlebar = new Gtk.Grid () { - visible = false - }; - - add_css_class ("dialog"); - add_css_class (Granite.STYLE_CLASS_MESSAGE_DIALOG); - - close_btn.clicked.connect (() => { - response (Gtk.ResponseType.CLOSE); - }); + custom_bin.append (screenshot_image); } } diff --git a/src/Screenshot/SetupDialog.vala b/src/Screenshot/SetupDialog.vala index ff0d432f..50ba76cd 100644 --- a/src/Screenshot/SetupDialog.vala +++ b/src/Screenshot/SetupDialog.vala @@ -40,7 +40,9 @@ public class Screenshot.SetupDialog : Gtk.Window { }); } - all_image = new Gtk.Image.from_icon_name ("grab-screen-symbolic"); + all_image = new Gtk.Image.from_icon_name ("grab-screen-symbolic") { + icon_size = LARGE + }; var all = new Gtk.CheckButton () { active = true, @@ -55,7 +57,9 @@ public class Screenshot.SetupDialog : Gtk.Window { } }); - var curr_image = new Gtk.Image.from_icon_name ("grab-window-symbolic"); + var curr_image = new Gtk.Image.from_icon_name ("grab-window-symbolic") { + icon_size = LARGE + }; var curr_window = new Gtk.CheckButton () { group = all, @@ -70,7 +74,9 @@ public class Screenshot.SetupDialog : Gtk.Window { } }); - var selection_image = new Gtk.Image.from_icon_name ("grab-area-symbolic"); + var selection_image = new Gtk.Image.from_icon_name ("grab-area-symbolic") { + icon_size = LARGE + }; var selection = new Gtk.CheckButton () { group = all, @@ -85,10 +91,6 @@ public class Screenshot.SetupDialog : Gtk.Window { } }); - var pointer_label = new Gtk.Label (_("Grab pointer:")) { - halign = END - }; - var pointer_switch = new Gtk.Switch () { halign = START }; @@ -97,8 +99,9 @@ public class Screenshot.SetupDialog : Gtk.Window { grab_pointer = pointer_switch.active; }); - var redact_label = new Gtk.Label (_("Conceal text:")) { - halign = END + var pointer_label = new Gtk.Label (_("Grab pointer:")) { + halign = END, + mnemonic_widget = pointer_switch }; var redact_switch = new Gtk.Switch () { @@ -109,8 +112,10 @@ public class Screenshot.SetupDialog : Gtk.Window { redact_text = redact_switch.active; }); - var delay_label = new Gtk.Label (_("Delay in seconds:")); - delay_label.halign = Gtk.Align.END; + var redact_label = new Gtk.Label (_("Conceal text:")) { + halign = END, + mnemonic_widget = redact_switch + }; var delay_spin = new Gtk.SpinButton.with_range (0, 15, 1); @@ -118,6 +123,11 @@ public class Screenshot.SetupDialog : Gtk.Window { delay = (int) delay_spin.value; }); + var delay_label = new Gtk.Label (_("Delay in seconds:")) { + halign = END, + mnemonic_widget = delay_spin + }; + var take_btn = new Gtk.Button.with_label (_("Take Screenshot")) { receives_default = true }; From a1271bff383aea58b1be296355fad7f38a731afb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Thu, 28 Mar 2024 15:55:41 -0700 Subject: [PATCH 12/13] remove approval dialog --- po/POTFILES | 3 +- src/Screenshot/ApprovalDialog.vala | 56 ------------------------- src/Screenshot/Portal.vala | 65 ------------------------------ src/meson.build | 1 - 4 files changed, 1 insertion(+), 124 deletions(-) delete mode 100644 src/Screenshot/ApprovalDialog.vala diff --git a/po/POTFILES b/po/POTFILES index 9d7aedce..434edde7 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -6,5 +6,4 @@ src/AppChooser/Portal.vala src/AppChooser/Dialog.vala src/AppChooser/AppButton.vala src/Background/NotificationRequest.vala -src/Screenshot/ApprovalDialog.vala -src/Screenshot/SetupDialog.vala \ No newline at end of file +src/Screenshot/SetupDialog.vala diff --git a/src/Screenshot/ApprovalDialog.vala b/src/Screenshot/ApprovalDialog.vala deleted file mode 100644 index 796ade75..00000000 --- a/src/Screenshot/ApprovalDialog.vala +++ /dev/null @@ -1,56 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 elementary, Inc. (https://elementary.io) - * SPDX-License-Identifier: LGPL-2.1-or-later - */ - - public class Screenshot.ApprovalDialog : Granite.MessageDialog { - public string parent_window { get; construct; } - public string screenshot_uri { get; construct; } - - public ApprovalDialog (string parent_window, bool modal, string app_id, string screenshot_uri) { - Object ( - parent_window: parent_window, - modal: modal, - screenshot_uri: screenshot_uri, - primary_text: _("Share this screenshot with the requesting app?"), - secondary_text: _("Only the app which requested this screenshot will be able to see it. This screenshot will not be saved elsewhere."), - buttons: Gtk.ButtonsType.CANCEL - ); - - if (app_id != null) { - var app_info = new GLib.DesktopAppInfo (app_id + ".desktop"); - if (app_info != null) { - primary_text = _("Share this screenshot with ā€œ%sā€?").printf (app_info.get_display_name ()); - } - } - } - - construct { - if (parent_window != "") { - ((Gtk.Widget) this).realize.connect (() => { - try { - ExternalWindow.from_handle (parent_window).set_parent_of (get_surface ()); - } catch (Error e) { - warning ("Failed to associate portal window with parent %s: %s", parent_window, e.message); - } - }); - } - - image_icon = new ThemedIcon ("io.elementary.screenshot"); - - var screenshot_filename = GLib.Filename.from_uri (screenshot_uri); - var screenshot_image = new Gtk.Picture.for_pixbuf (new Gdk.Pixbuf.from_file_at_scale (screenshot_filename, 400, 400, true)) { - overflow = HIDDEN, - width_request = 400 - }; - screenshot_image.add_css_class (Granite.STYLE_CLASS_CARD); - screenshot_image.add_css_class (Granite.STYLE_CLASS_ROUNDED); - screenshot_image.add_css_class (Granite.STYLE_CLASS_CHECKERBOARD); - - var allow_button = add_button (_("Share Screenshot"), Gtk.ResponseType.OK); - allow_button.receives_default = true; - allow_button.add_css_class (Granite.STYLE_CLASS_SUGGESTED_ACTION); - - custom_bin.append (screenshot_image); - } -} diff --git a/src/Screenshot/Portal.vala b/src/Screenshot/Portal.vala index 1ef0f1c2..d2ee2cd5 100644 --- a/src/Screenshot/Portal.vala +++ b/src/Screenshot/Portal.vala @@ -159,45 +159,6 @@ public class Screenshot.Portal : Object { return; } - if (!interactive && !permission_store_checked) { - var uri = ""; - - try { - uri = yield do_screenshot (SetupDialog.ScreenshotType.ALL, false, false, 0); - } catch (Error e) { - warning ("Couldn't call screenshot: %s\n", e.message); - response = 1; - return; - } - - // This app has not been pre-approved to take screenshots, so we prompt the user - var dialog = new ApprovalDialog (parent_window, modal, app_id, uri); - - bool cancelled = true; - dialog.response.connect ((response_id) => { - if (response_id == Gtk.ResponseType.OK) { - cancelled = false; - } - - screenshot.callback (); - }); - - dialog.show (); - yield; - - - dialog.destroy (); - - if (cancelled) { - response = 1; - return; - } else { - response = 0; - results["uri"] = uri; - return; - } - } - if (interactive) { var dialog = new SetupDialog (parent_window, modal); @@ -234,32 +195,6 @@ public class Screenshot.Portal : Object { response = 0; results["uri"] = uri; return; - } else { - // This app has not been pre-approved to take screenshots, so we prompt the user - var approval_dialog = new ApprovalDialog (parent_window, modal, app_id, uri); - - bool approval_cancelled = true; - approval_dialog.response.connect ((response_id) => { - if (response_id == Gtk.ResponseType.OK) { - approval_cancelled = false; - } - - screenshot.callback (); - }); - - approval_dialog.show (); - yield; - - approval_dialog.destroy (); - - if (approval_cancelled) { - response = 1; - return; - } else { - response = 0; - results["uri"] = uri; - return; - } } } diff --git a/src/meson.build b/src/meson.build index 10164203..90ffcddc 100644 --- a/src/meson.build +++ b/src/meson.build @@ -8,7 +8,6 @@ executable( 'AppChooser/Portal.vala', 'Background/NotificationRequest.vala', 'Background/Portal.vala', - 'Screenshot/ApprovalDialog.vala', 'Screenshot/Portal.vala', 'Screenshot/SetupDialog.vala', 'Wallpaper/Portal.vala', From 397b79bf24c2789f68a47070065fb55de71af163 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Sun, 7 Apr 2024 00:58:59 +0000 Subject: [PATCH 13/13] Use user cache dir for temporary filename (#101) --- src/Screenshot/Portal.vala | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Screenshot/Portal.vala b/src/Screenshot/Portal.vala index d2ee2cd5..b1cd9416 100644 --- a/src/Screenshot/Portal.vala +++ b/src/Screenshot/Portal.vala @@ -60,6 +60,8 @@ public class Screenshot.Portal : Object { int delay ) throws GLib.Error { string filename_used = ""; + var tmp_filename = get_tmp_filename (); + switch (screenshot_type) { case SetupDialog.ScreenshotType.ALL: var success = false; @@ -69,7 +71,7 @@ public class Screenshot.Portal : Object { yield screenshot_proxy.conceal_text (); yield do_delay (1); } - yield screenshot_proxy.screenshot (grab_pointer, true, "/tmp/portal_screenshot.png", out success, out filename_used); + yield screenshot_proxy.screenshot (grab_pointer, true, tmp_filename, out success, out filename_used); if (!success) { throw new GLib.IOError.FAILED ("Failed to take screenshot"); @@ -84,7 +86,7 @@ public class Screenshot.Portal : Object { yield screenshot_proxy.conceal_text (); yield do_delay (1); } - yield screenshot_proxy.screenshot_window (false, grab_pointer, true, "/tmp/portal_screenshot.png", out success, out filename_used); + yield screenshot_proxy.screenshot_window (false, grab_pointer, true, tmp_filename, out success, out filename_used); if (!success) { throw new GLib.IOError.FAILED ("Failed to take screenshot"); @@ -102,7 +104,7 @@ public class Screenshot.Portal : Object { yield screenshot_proxy.conceal_text (); yield do_delay (1); } - yield screenshot_proxy.screenshot_area_with_cursor (x, y, width, height, grab_pointer, true, "/tmp/portal_screenshot.png", out success, out filename_used); + yield screenshot_proxy.screenshot_area_with_cursor (x, y, width, height, grab_pointer, true, tmp_filename, out success, out filename_used); if (!success) { throw new GLib.IOError.FAILED ("Failed to take screenshot"); @@ -234,4 +236,10 @@ public class Screenshot.Portal : Object { response = 0; results = _result; } + + private string get_tmp_filename () { + var dir = Environment.get_user_cache_dir (); + var name = "io.elementary.portals.screenshot-%lu.png".printf (Random.next_int ()); + return Path.build_filename (dir, name); + } }