[gnome-shell] Provide org.freedesktop.impl.portal.access implementation



commit 4fc0c513af941cefefcbe8a5d4e82c9cb4a72797
Author: Florian Müllner <fmuellner gnome org>
Date:   Tue Jul 12 22:47:32 2016 +0200

    Provide org.freedesktop.impl.portal.access implementation
    
    If a sandboxed app requests access to some system resource (camera,
    microphone, location), the portal frontend needs to ask the user
    for permission. In GNOME, we want this to be a system modal dialog,
    so provide an org.freedesktop.impl.portal.access implementation
    that exposes a generic system modal permission dialog on the bus.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=768669

 data/Makefile.am                         |    3 +
 data/gnome-shell.portal                  |    4 +
 data/theme/gnome-shell-high-contrast.css |   23 ++++
 data/theme/gnome-shell-sass              |    2 +-
 data/theme/gnome-shell.css               |   23 ++++
 js/js-resources.gresource.xml            |    1 +
 js/ui/accessDialog.js                    |  202 ++++++++++++++++++++++++++++++
 js/ui/main.js                            |    3 +
 8 files changed, 260 insertions(+), 1 deletions(-)
---
diff --git a/data/Makefile.am b/data/Makefile.am
index 7adb5be..f681ab4 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 0000000..b704637
--- /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 190aef5..9f7d65b 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 d184e5c..7ab2789 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 5b61359..740ee9c 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 0746d8b..54f702b 100644
--- a/js/js-resources.gresource.xml
+++ b/js/js-resources.gresource.xml
@@ -31,6 +31,7 @@
 
     <file>portalHelper/main.js</file>
 
+    <file>ui/accessDialog.js</file>
     <file>ui/altTab.js</file>
     <file>ui/animation.js</file>
     <file>ui/appDisplay.js</file>
diff --git a/js/ui/accessDialog.js b/js/ui/accessDialog.js
new file mode 100644
index 0000000..13b448c
--- /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 = '<node> \
+<interface name="org.freedesktop.impl.portal.Request"> \
+<method name="Close"/> \
+</interface> \
+</node>';
+
+const AccessIface = '<node> \
+<interface name="org.freedesktop.impl.portal.Access"> \
+<method name="AccessDialog"> \
+  <arg type="o" name="handle" direction="in"/> \
+  <arg type="s" name="app_id" direction="in"/> \
+  <arg type="s" name="parent_window" direction="in"/> \
+  <arg type="s" name="title" direction="in"/> \
+  <arg type="s" name="subtitle" direction="in"/> \
+  <arg type="s" name="body" direction="in"/> \
+  <arg type="a{sv}" name="options" direction="in"/> \
+  <arg type="u" name="response" direction="out"/> \
+  <arg type="a{sv}" name="results" direction="out"/> \
+</method> \
+</interface> \
+</node>';
+
+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 789dd69..bd42960 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();


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]