diff --git a/data/Makefile.am b/data/Makefile.am index 7adb5beb9..f681ab424 100644 --- a/data/Makefile.am +++ b/data/Makefile.am @@ -7,6 +7,9 @@ desktop_DATA = org.gnome.Shell.desktop gnome-shell-extension-prefs.desktop if HAVE_NETWORKMANAGER desktop_DATA += org.gnome.Shell.PortalHelper.desktop +portaldir = $(datadir)/xdg-desktop-portal/portals +portal_DATA = gnome-shell.portal + servicedir = $(datadir)/dbus-1/services service_DATA = org.gnome.Shell.PortalHelper.service diff --git a/data/gnome-shell.portal b/data/gnome-shell.portal new file mode 100644 index 000000000..b70463777 --- /dev/null +++ b/data/gnome-shell.portal @@ -0,0 +1,4 @@ +[portal] +DBusName=org.freedesktop.impl.portal.desktop.gnome +Interfaces=org.freedesktop.impl.portal.Access +UseIn=gnome diff --git a/data/theme/gnome-shell-high-contrast.css b/data/theme/gnome-shell-high-contrast.css index 190aef523..9f7d65bcd 100644 --- a/data/theme/gnome-shell-high-contrast.css +++ b/data/theme/gnome-shell-high-contrast.css @@ -427,6 +427,29 @@ StScrollBar { .audio-selection-device-icon { icon-size: 64px; } +/* Access Dialog */ +.access-dialog { + spacing: 30px; } + +.access-dialog-main-layout { + padding: 12px 20px 0; + spacing: 12px; } + +.access-dialog-content { + max-width: 28em; + spacing: 20px; } + +.access-dialog-icon { + min-width: 48px; + icon-size: 48px; } + +.access-dialog-title { + font-weight: bold; } + +.access-dialog-subtitle { + color: #999999; + font-weight: bold; } + /* Geolocation Dialog */ .geolocation-dialog { spacing: 30px; } diff --git a/data/theme/gnome-shell-sass b/data/theme/gnome-shell-sass index d184e5c44..7ab278946 160000 --- a/data/theme/gnome-shell-sass +++ b/data/theme/gnome-shell-sass @@ -1 +1 @@ -Subproject commit d184e5c44ed824ab62fc35a4a8d75cda1f387206 +Subproject commit 7ab2789464b2e917b56aec9df6b78975db1a8eb6 diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css index 5b6135940..740ee9c38 100644 --- a/data/theme/gnome-shell.css +++ b/data/theme/gnome-shell.css @@ -427,6 +427,29 @@ StScrollBar { .audio-selection-device-icon { icon-size: 64px; } +/* Access Dialog */ +.access-dialog { + spacing: 30px; } + +.access-dialog-main-layout { + padding: 12px 20px 0; + spacing: 12px; } + +.access-dialog-content { + max-width: 28em; + spacing: 20px; } + +.access-dialog-icon { + min-width: 48px; + icon-size: 48px; } + +.access-dialog-title { + font-weight: bold; } + +.access-dialog-subtitle { + color: #8e8e80; + font-weight: bold; } + /* Geolocation Dialog */ .geolocation-dialog { spacing: 30px; } diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml index 0746d8b91..54f702b4a 100644 --- a/js/js-resources.gresource.xml +++ b/js/js-resources.gresource.xml @@ -31,6 +31,7 @@ portalHelper/main.js + ui/accessDialog.js ui/altTab.js ui/animation.js ui/appDisplay.js diff --git a/js/ui/accessDialog.js b/js/ui/accessDialog.js new file mode 100644 index 000000000..13b448c6b --- /dev/null +++ b/js/ui/accessDialog.js @@ -0,0 +1,202 @@ +const Clutter = imports.gi.Clutter; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const Lang = imports.lang; +const Pango = imports.gi.Pango; +const Shell = imports.gi.Shell; +const St = imports.gi.St; + +const CheckBox = imports.ui.checkBox; +const ModalDialog = imports.ui.modalDialog; + +const RequestIface = ' \ + \ + \ + \ +'; + +const AccessIface = ' \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ +'; + +const DialogResponse = { + OK: 0, + CANCEL: 1, + CLOSED: 2 +}; + +const AccessDialog = new Lang.Class({ + Name: 'AccessDialog', + Extends: ModalDialog.ModalDialog, + + _init: function(invocation, handle, title, subtitle, body, options) { + this.parent({ styleClass: 'access-dialog' }); + + this._invocation = invocation; + this._handle = handle; + + this._requestExported = false; + this._request = Gio.DBusExportedObject.wrapJSObject(RequestIface, this); + + for (let option in options) + options[option] = options[option].deep_unpack(); + + this._buildLayout(title, subtitle, body, options); + }, + + _buildLayout: function(title, subtitle, body, options) { + // No support for non-modal system dialogs, so ignore the option + //let modal = options['modal'] || true; + let denyLabel = options['deny_label'] || _("Deny Access"); + let grantLabel = options['grant_label'] || _("Grant Access"); + let iconName = options['icon'] || null; + let choices = options['choices'] || []; + + let mainContentBox = new St.BoxLayout(); + mainContentBox.style_class = 'access-dialog-main-layout'; + this.contentLayout.add_actor(mainContentBox); + + let icon = new St.Icon({ style_class: 'access-dialog-icon', + icon_name: iconName, + y_align: Clutter.ActorAlign.START }); + mainContentBox.add_actor(icon); + + let messageBox = new St.BoxLayout({ vertical: true }); + messageBox.style_class = 'access-dialog-content', + mainContentBox.add_actor(messageBox); + + let label; + label = new St.Label({ style_class: 'access-dialog-title headline', + text: title }); + messageBox.add_actor(label); + + label = new St.Label({ style_class: 'access-dialog-subtitle', + text: subtitle }); + label.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; + label.clutter_text.line_wrap = true; + messageBox.add_actor(label); + + this._choices = new Map(); + + for (let i = 0; i < choices.length; i++) { + let [id, name, opts, selected] = choices[i]; + if (opts.length > 0) + continue; // radio buttons, not implemented + + let check = new CheckBox.CheckBox(); + check.getLabelActor().text = name; + check.actor.checked = selected == "true"; + messageBox.add_actor(check.actor); + + this._choices.set(id, check); + } + + label = new St.Label({ text: body }); + label.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; + label.clutter_text.line_wrap = true; + messageBox.add_actor(label); + + this.addButton({ label: denyLabel, + action: () => { + this._sendResponse(DialogResponse.CANCEL); + }, + key: Clutter.KEY_Escape }); + this.addButton({ label: grantLabel, + action: () => { + this._sendResponse(DialogResponse.OK); + }}); + }, + + open: function() { + this.parent(); + + let connection = this._invocation.get_connection(); + this._requestExported = this._request.export(connection, this._handle); + }, + + CloseAsync: function(invocation, params) { + if (this._invocation.get_sender() != invocation.get_sender()) { + invocation.return_error_literal(Gio.DBusError, + Gio.DBusError.ACCESS_DENIED, + ''); + return; + } + + this._sendResponse(DialogResponse.CLOSED); + }, + + _sendResponse: function(response) { + if (this._requestExported) + this._request.unexport(); + this._requestExported = false; + + let results = {}; + if (response == DialogResponse.OK) { + for (let [id, check] of this._choices) { + let checked = check.actor.checked ? 'true' : 'false'; + results[id] = new GLib.Variant('s', checked); + } + } + + // Delay actual response until the end of the close animation (if any) + this.connect('closed', () => { + this._invocation.return_value(new GLib.Variant('(ua{sv})', + [response, results])); + }); + this.close(); + } +}); + +const AccessDialogDBus = new Lang.Class({ + Name: 'AccessDialogDBus', + + _init: function() { + this._accessDialog = null; + + this._windowTracker = Shell.WindowTracker.get_default(); + + this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(AccessIface, this); + this._dbusImpl.export(Gio.DBus.session, '/org/freedesktop/portal/desktop'); + + Gio.DBus.session.own_name('org.freedesktop.impl.portal.desktop.gnome', Gio.BusNameOwnerFlags.REPLACE, null, null); + }, + + AccessDialogAsync: function(params, invocation) { + if (this._accessDialog) { + invocation.return_error_literal(Gio.DBusError, + Gio.DBusError.LIMITS_EXCEEDED, + 'Already showing a system access dialog'); + return; + } + + let [handle, appId, parentWindow, title, subtitle, body, options] = params; + // We probably want to use parentWindow and global.display.focus_window + // for this check in the future + if (appId && appId + '.desktop' != this._windowTracker.focus_app.id) { + invocation.return_error_literal(Gio.DBusError, + Gio.DBusError.ACCESS_DENIED, + 'Only the focused app is allowed to show a system access dialog'); + return; + } + + let dialog = new AccessDialog(invocation, handle, title, + subtitle, body, options); + dialog.open(); + + dialog.connect('closed', () => { this._accessDialog = null; }); + + this._accessDialog = dialog; + } +}); diff --git a/js/ui/main.js b/js/ui/main.js index 789dd690c..bd42960f3 100644 --- a/js/ui/main.js +++ b/js/ui/main.js @@ -11,6 +11,7 @@ const Meta = imports.gi.Meta; const Shell = imports.gi.Shell; const St = imports.gi.St; +const AccessDialog = imports.ui.accessDialog; const AudioDeviceSelection = imports.ui.audioDeviceSelection; const Components = imports.ui.components; const CtrlAltTab = imports.ui.ctrlAltTab; @@ -63,6 +64,7 @@ let ctrlAltTabManager = null; let osdWindowManager = null; let osdMonitorLabeler = null; let sessionMode = null; +let shellAccessDialogDBusService = null; let shellAudioSelectionDBusService = null; let shellDBusService = null; let shellMountOpDBusService = null; @@ -122,6 +124,7 @@ function start() { _loadDefaultStylesheet); _initializeUI(); + shellAccessDialogDBusService = new AccessDialog.AccessDialogDBus(); shellAudioSelectionDBusService = new AudioDeviceSelection.AudioDeviceSelectionDBus(); shellDBusService = new ShellDBus.GnomeShell(); shellMountOpDBusService = new ShellMountOperation.GnomeShellMountOpHandler();