diff --git a/data/Application.css b/data/Application.css new file mode 100644 index 00000000..0543b2c9 --- /dev/null +++ b/data/Application.css @@ -0,0 +1,16 @@ +.dialog toolbox box.top, +.dialog toolbox box.bottom { + padding: 1rem; +} + +.dialog toolbox .top overlay { + min-width: calc((48px * 2) - 1em); +} + +.dialog toolbox .top .large-icons { + -gtk-icon-size: 48px; +} + +.dialog toolbox .bottom button { + min-width: 5em; +} diff --git a/data/gresource.xml b/data/gresource.xml index 7e848a6f..38835c88 100644 --- a/data/gresource.xml +++ b/data/gresource.xml @@ -1,6 +1,9 @@ + Application.css + + icons/screencast.svg icons/screencast.svg diff --git a/src/AppChooser/Dialog.vala b/src/AppChooser/Dialog.vala index 065d9796..eed619d3 100644 --- a/src/AppChooser/Dialog.vala +++ b/src/AppChooser/Dialog.vala @@ -4,7 +4,7 @@ */ [DBus (name = "org.freedesktop.impl.portal.Request")] -public class AppChooser.Dialog : Gtk.Window { +public class AppChooser.Dialog : PortalDialog { public signal void choiced (string app_id); // The ID used to register this dialog on the DBusConnection @@ -13,8 +13,6 @@ public class AppChooser.Dialog : Gtk.Window { // The ID of the app sending the request public string app_id { get; construct; } - public string parent_window { get; construct; } - // The app id that was selected the last time public string last_choice { get; construct; } @@ -30,14 +28,12 @@ public class AppChooser.Dialog : Gtk.Window { public Dialog ( string app_id, - string parent_window, string last_choice, string content_type, string filename ) { Object ( app_id: app_id, - parent_window: parent_window, last_choice: last_choice, content_type: content_type, filename: filename @@ -49,7 +45,7 @@ public class AppChooser.Dialog : Gtk.Window { AppInfo? info = app_id == "" ? null : new DesktopAppInfo (app_id + ".desktop"); var content_description = ContentType.get_description ("text/plain"); - Icon secondary_icon = new ThemedIcon ("document-open"); + secondary_icon = new ThemedIcon ("document-open"); if (content_type != "") { content_description = ContentType.get_description (content_type); if (content_description.contains ("x-scheme-handler")) { @@ -63,59 +59,23 @@ public class AppChooser.Dialog : Gtk.Window { } } - var primary_text = _("An application wants to open a %s").printf (content_description); + title = _("An application wants to open a %s").printf (content_description); if (info != null) { - primary_text = _("“%s” wants to open a %s").printf (info.get_display_name (), content_description); + title = _("“%s” wants to open a %s").printf (info.get_display_name (), content_description); + primary_icon = info.get_icon (); } - var secondary_text = _("Open with…"); + secondary_text = _("Open with…"); if (filename != "") { secondary_text = _("Open “%s” with…").printf (filename); } - var primary_label = new Gtk.Label (primary_text) { - max_width_chars = 50, - selectable = false, - hexpand = true, - wrap = true, - xalign = 0 - }; - primary_label.add_css_class (Granite.STYLE_CLASS_TITLE_LABEL); - - var secondary_label = new Gtk.Label (secondary_text) { - max_width_chars = 50, - margin_bottom = 18, - use_markup = true, - wrap = true, - xalign = 0 - }; - - var mime_icon = new Gtk.Image.from_gicon (secondary_icon) { - pixel_size = 48 - }; - - var overlay = new Gtk.Overlay () { - child = mime_icon, - valign = Gtk.Align.START - }; - - if (info != null) { - var badge = new Gtk.Image.from_gicon (info.get_icon ()) { - halign = Gtk.Align.END, - valign = Gtk.Align.END, - pixel_size = 24 - }; - - overlay.add_overlay (badge); - } - var placeholder = new Granite.Placeholder (_("No installed apps can open %s").printf (content_description)) { description = _("New apps can be installed from AppCenter"), icon = new ThemedIcon ("application-default-icon") }; listbox = new Gtk.ListBox () { - hexpand = true, vexpand = true }; listbox.set_placeholder (placeholder); @@ -125,69 +85,23 @@ public class AppChooser.Dialog : Gtk.Window { }; var frame = new Gtk.Frame (null) { - child = scrolled_window + child = scrolled_window, + margin_top = 12, + margin_end = 12, + margin_bottom = 12, + margin_start = 12 }; - var cancel_button = new Gtk.Button.with_label (_("Cancel")); + var cancel_button = add_button (_("Cancel")); - open_button = new Gtk.Button.with_label (_("Open")) { - receives_default = true - }; + open_button = add_button (_("Open")); + open_button.receives_default = true; open_button.add_css_class (Granite.CssClass.SUGGESTED); - var button_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0) { - halign = Gtk.Align.END - }; - button_box.append (cancel_button); - button_box.append (open_button); - button_box.add_css_class ("dialog-action-area"); - - var grid = new Gtk.Grid () { - orientation = Gtk.Orientation.VERTICAL, - column_spacing = 12, - row_spacing = 6 - }; - - grid.attach (overlay, 0, 0, 1, 2); - grid.attach (primary_label, 1, 0); - grid.attach (secondary_label, 1, 1); - grid.attach (frame, 0, 3, 2); - grid.add_css_class (Granite.STYLE_CLASS_DIALOG_CONTENT_AREA); - - var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); - box.append (grid); - box.append (button_box); - box.add_css_class ("dialog-vbox"); - - var window_handle = new Gtk.WindowHandle () { - child = box - }; + content = frame; - child = window_handle; - - // We need to hide the title area - titlebar = new Gtk.Grid () { - visible = false - }; - - modal = true; - default_height = 400; - default_width = 350; default_widget = open_button; - add_css_class ("dialog"); - add_css_class (Granite.STYLE_CLASS_MESSAGE_DIALOG); - - 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); - } - }); - } - listbox.row_activated.connect ((row) => { choiced (((AppChooser.AppButton) row).app_id); }); @@ -201,7 +115,6 @@ public class AppChooser.Dialog : Gtk.Window { listbox.append (buttons[choice]); } - [DBus (visible = false)] public void update_choices (string[] choices) { foreach (var choice in choices) { diff --git a/src/AppChooser/Portal.vala b/src/AppChooser/Portal.vala index 6988201a..b2ca0b8c 100644 --- a/src/AppChooser/Portal.vala +++ b/src/AppChooser/Portal.vala @@ -40,11 +40,12 @@ public class AppChooser.Portal : Object { var dialog = new AppChooser.Dialog ( app_id, - parent_window, last_choice, content_type, filename - ); + ) { + parent_handle = parent_window + }; if ("modal" in options && options["modal"].is_of_type (VariantType.BOOLEAN)) { dialog.modal = options["modal"].get_boolean (); diff --git a/src/PortalDialog.vala b/src/PortalDialog.vala new file mode 100644 index 00000000..239c7cbb --- /dev/null +++ b/src/PortalDialog.vala @@ -0,0 +1,117 @@ +/* + * SPDX-FileCopyrightText: 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +public class PortalDialog : Gtk.Window, PantheonWayland.ExtendedBehavior { + /** + * The {@link GLib.Icon} that is used to display the primary_icon representing the app making the request + */ + public GLib.Icon primary_icon { get; set; } + + /** + * The {@link GLib.Icon} that is used to display a secondary_icon representing the action to be performed + */ + public GLib.Icon secondary_icon { get; set; } + + /** + * The secondary text, body of the dialog. + */ + public string secondary_text { get; set; } + + /** + * The child widget for the content area + */ + public Gtk.Widget content { get; set; } + + /** + * The parent window identifier as described by https://flatpak.github.io/xdg-desktop-portal/docs/window-identifiers.html + */ + public string parent_handle { get; set; } + + private Granite.Box button_box; + + construct { + var primary_icon = new Gtk.Image.from_icon_name ("application-default-icon") { + halign = START, + icon_size = LARGE + }; + + var secondary_icon = new Gtk.Image.from_icon_name ("preferences-system-privacy") { + halign = END, + icon_size = LARGE + }; + + var overlay = new Gtk.Overlay () { + child = secondary_icon, + halign = CENTER + }; + overlay.add_overlay (primary_icon); + + var header_label = new Granite.HeaderLabel ("") { + size = H3 + }; + + var header = new Granite.Box (VERTICAL); + header.append (overlay); + header.append (header_label); + + button_box = new Granite.Box (HORIZONTAL, HALF) { + homogeneous = true + }; + + var toolbarview = new Granite.ToolBox (); + toolbarview.add_bottom_bar (button_box); + toolbarview.add_top_bar (header); + + child = toolbarview; + + default_height = 425; + default_width = 325; + modal = true; + + // We need to hide the title area + titlebar = new Gtk.Grid () { + visible = false + }; + + add_css_class ("dialog"); + + bind_property ("primary-icon", primary_icon, "gicon"); + bind_property ("secondary-icon", secondary_icon, "gicon"); + bind_property ("content", toolbarview, "content"); + bind_property ("title", header_label, "label"); + bind_property ("secondary-text", header_label, "secondary-text"); + ((Gtk.Widget) this).realize.connect (on_realize); + } + + public Gtk.Button add_button (string button_text) { + var button = new Gtk.Button.with_label (button_text); + button_box.append (button); + + return button; + } + + private void on_realize () { + unowned var toplevel = (Gdk.Toplevel) get_surface (); + + if (parent_handle != "") { + try { + ExternalWindow.from_handle (parent_handle).set_parent_of (toplevel); + } catch (Error e) { + warning ("Failed to associate portal window with parent '%s': %s", parent_handle, e.message); + make_sticky (); + } + } else { + make_sticky (); + } + } + + private void make_sticky () { + child.realize.connect (() => { + connect_to_shell (); + make_centered (); + set_keep_above (); + }); + } +} diff --git a/src/ScreenCast/Dialog.vala b/src/ScreenCast/Dialog.vala index 7d230e53..33199a56 100644 --- a/src/ScreenCast/Dialog.vala +++ b/src/ScreenCast/Dialog.vala @@ -5,7 +5,9 @@ * Authored by: Leonhard Kargl */ -public class ScreenCast.Dialog : Granite.MessageDialog { +public class ScreenCast.Dialog : PortalDialog { + public signal void response (string response); + public SourceType source_types { get; construct; } public bool allow_multiple { get; construct; } @@ -61,23 +63,27 @@ public class ScreenCast.Dialog : Granite.MessageDialog { }; var frame = new Gtk.Frame (null) { - child = scrolled_window + child = scrolled_window, + margin_top = 12, + margin_end = 12, + margin_bottom = 12, + margin_start = 12 }; - custom_bin.append (frame); - - resizable = true; - default_height = 400; - default_width = 300; + content = frame; - add_button (_("Cancel"), Gtk.ResponseType.CANCEL); + var cancel_button = add_button (_("Cancel")); - var accept_button = add_button (_("Share"), Gtk.ResponseType.ACCEPT); + var accept_button = add_button (_("Share")); accept_button.add_css_class (Granite.CssClass.SUGGESTED); + bind_property ("n-selected", accept_button, "sensitive", SYNC_CREATE, (binding, from_val, ref to_val) => { to_val.set_boolean (n_selected > 0); return true; }); + + accept_button.clicked.connect (() => response ("accept")); + cancel_button.clicked.connect (() => response ("cancel")); } private async void populate_windows () { diff --git a/src/ScreenCast/Session.vala b/src/ScreenCast/Session.vala index fe5694b5..8f09bdc3 100644 --- a/src/ScreenCast/Session.vala +++ b/src/ScreenCast/Session.vala @@ -87,29 +87,24 @@ public class ScreenCast.Session : Object { internal void start (string app_id, string parent_window) { var dialog = new Dialog (source_types, allow_multiple) { - image_icon = new ThemedIcon ("accessories-screencast-tool"), - primary_text = _("An application wants to access the screen"), - secondary_text = _("Select which parts of the screen to share:") + title = _("An application wants to access the screen"), + secondary_icon = new ThemedIcon ("accessories-screencast-tool"), + secondary_text = _("Select which parts of the screen to share:"), + parent_handle = parent_window }; if (app_id != "") { var app_info = new DesktopAppInfo (app_id + ".desktop"); if (app_info != null) { - dialog.primary_text = _("“%s” wants to access the screen").printf (app_info.get_display_name ()); + dialog.primary_icon = app_info.get_icon (); + dialog.title = _("“%s” wants to access the screen").printf (app_info.get_display_name ()); } } - try { - var parent = ExternalWindow.from_handle (parent_window); - parent.set_parent_of (dialog.get_surface ()); - } catch (Error e) { - warning ("Failed to set parent: %s", e.message); - } - dialog.response.connect ((response) => { - dialog.destroy (); + dialog.close (); - if (response == Gtk.ResponseType.CANCEL) { + if (response == "cancel") { started (1, streams); } else { setup_recording.begin (dialog); diff --git a/src/XdgDesktopPortalPantheon.vala b/src/XdgDesktopPortalPantheon.vala index 69d8a778..1569817e 100644 --- a/src/XdgDesktopPortalPantheon.vala +++ b/src/XdgDesktopPortalPantheon.vala @@ -65,6 +65,10 @@ private void on_name_acquired () { watch_id = Bus.watch_name (BusType.SESSION, "org.freedesktop.portal.Desktop", BusNameWatcherFlags.NONE, () => { Granite.init (); + var css_provider = new Gtk.CssProvider (); + css_provider.load_from_resource ("/io/elementary/xdg-desktop-portal-pantheon/Application.css"); + Gtk.StyleContext.add_provider_for_display (Gdk.Display.get_default (), css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); + var granite_settings = Granite.Settings.get_default (); var gtk_settings = Gtk.Settings.get_default (); @@ -92,7 +96,7 @@ 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"); + default_theme.add_resource_path ("/io/elementary/xdg-desktop-portal-pantheon/icons"); try { var opt_context = new OptionContext ("- portal backends"); diff --git a/src/meson.build b/src/meson.build index 68f41c3d..843990a7 100644 --- a/src/meson.build +++ b/src/meson.build @@ -22,6 +22,7 @@ executable( configure_file(input: 'Config.vala.in', output: '@BASENAME@', configuration: conf_data), 'DesktopIntegration.vala', 'ExternalWindow.vala', + 'PortalDialog.vala', 'XdgDesktopPortalPantheon.vala', icon_res, dependencies: [