Skip to content
16 changes: 16 additions & 0 deletions data/Application.css
Original file line number Diff line number Diff line change
@@ -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;
}
3 changes: 3 additions & 0 deletions data/gresource.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/io/elementary/xdg-desktop-portal-pantheon">
<file compressed="true">Application.css</file>
</gresource>
<gresource prefix="/io/elementary/xdg-desktop-portal-pantheon/icons">
<file alias="48x48/apps/accessories-screencast-tool.svg" compressed="true" preprocess="xml-stripblanks">icons/screencast.svg</file>
<file alias="48x48@2/apps/accessories-screencast-tool.svg" compressed="true" preprocess="xml-stripblanks">icons/screencast.svg</file>

Expand Down
117 changes: 15 additions & 102 deletions src/AppChooser/Dialog.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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; }

Expand All @@ -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
Expand All @@ -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")) {
Expand All @@ -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);
Expand All @@ -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);
});
Expand All @@ -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) {
Expand Down
5 changes: 3 additions & 2 deletions src/AppChooser/Portal.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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 ();
Expand Down
117 changes: 117 additions & 0 deletions src/PortalDialog.vala
Original file line number Diff line number Diff line change
@@ -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 ();
});
}
}
24 changes: 15 additions & 9 deletions src/ScreenCast/Dialog.vala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
* Authored by: Leonhard Kargl <leo.kargl@proton.me>
*/

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; }

Expand Down Expand Up @@ -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 () {
Expand Down
Loading