[gnome-shell] endSessionDialog: Add logout/shutdown dialog



commit e73e4375b8c4f1f0982ca0b23c24706e81c25bad
Author: Ray Strode <rstrode redhat com>
Date:   Thu Jan 6 10:30:15 2011 -0500

    endSessionDialog: Add logout/shutdown dialog
    
    This commit adds a dialog for gnome-session to
    privately use when initiating log outs and shut
    downs.
    
    Coordination is done over the bus.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=637187

 data/theme/gnome-shell.css |   56 +++++
 js/Makefile.am             |    1 +
 js/misc/gnomeSession.js    |   58 +++++
 js/ui/endSessionDialog.js  |  504 ++++++++++++++++++++++++++++++++++++++++++++
 js/ui/main.js              |    5 +
 5 files changed, 624 insertions(+), 0 deletions(-)
---
diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css
index 2d83bc0..393cce9 100644
--- a/data/theme/gnome-shell.css
+++ b/data/theme/gnome-shell.css
@@ -1165,6 +1165,62 @@ StTooltip StLabel {
     background-color: rgba(0, 0, 0, 0.4);
 }
 
+/* End Session Dialog */
+.end-session-dialog-subject {
+    font: 12pt sans-serif;
+    font-weight: bold;
+    color: #666666;
+    padding-top: 10px;
+    padding-left: 17px;
+    padding-bottom: 30px;
+}
+
+.end-session-dialog-description {
+    font: 10pt sans-serif;
+    color: white;
+    padding-left: 17px;
+    padding-right: 40px;
+    width: 16em;
+}
+
+.end-session-dialog-logout-icon {
+    border: 2px solid #8b8b8b;
+    border-radius: 5px;
+    width: 32px;
+    height: 32px;
+}
+
+.end-session-dialog-shutdown-icon {
+    width: 32px;
+    height: 32px;
+}
+
+.end-session-dialog-app-list {
+    font: 10pt sans-serif;
+    max-height: 200px;
+    padding-top: 42px;
+    padding-bottom: 42px;
+    padding-left: 17px;
+    padding-right: 32px;
+}
+
+.end-session-dialog-app-list-item {
+    padding-right: 1em;
+}
+
+.end-session-dialog-app-list-item-icon {
+    padding-right: 17px;
+}
+
+.end-session-dialog-app-list-item-name {
+    font: 10pt sans-serif;
+}
+
+.end-session-dialog-app-list-item-description {
+    font: 8pt sans-serif;
+    color: #444444;
+}
+
 /* Magnifier */
 
 .magnifier-zoom-region {
diff --git a/js/Makefile.am b/js/Makefile.am
index 3dce4f2..1b02f49 100644
--- a/js/Makefile.am
+++ b/js/Makefile.am
@@ -21,6 +21,7 @@ nobase_dist_js_DATA = 	\
 	ui/dash.js		\
 	ui/dnd.js		\
 	ui/docDisplay.js	\
+	ui/endSessionDialog.js	\
 	ui/environment.js	\
 	ui/extensionSystem.js	\
 	ui/genericDisplay.js	\
diff --git a/js/misc/gnomeSession.js b/js/misc/gnomeSession.js
index 06f52a3..ef4ef84 100644
--- a/js/misc/gnomeSession.js
+++ b/js/misc/gnomeSession.js
@@ -2,6 +2,7 @@
 
 const DBus = imports.dbus;
 const Lang = imports.lang;
+const Signals = imports.signals;
 
 const PresenceIface = {
     name: 'org.gnome.SessionManager.Presence',
@@ -43,3 +44,60 @@ Presence.prototype = {
     }
 };
 DBus.proxifyPrototype(Presence.prototype, PresenceIface);
+
+// Note inhibitors are immutable objects, so they don't
+// change at runtime (changes always come in the form
+// of new inhibitors)
+const InhibitorIface = {
+    name: 'org.gnome.SessionManager.Inhibitor',
+    properties: [{ name: 'app_id',
+                   signature: 's',
+                   access: 'readonly' },
+                 { name: 'client_id',
+                   signature: 's',
+                   access: 'readonly' },
+                 { name: 'reason',
+                   signature: 's',
+                   access: 'readonly' },
+                 { name: 'flags',
+                   signature: 'u',
+                   access: 'readonly' },
+                 { name: 'toplevel_xid',
+                   signature: 'u',
+                   access: 'readonly' },
+                 { name: 'cookie',
+                   signature: 'u',
+                   access: 'readonly' }],
+};
+
+function Inhibitor(objectPath) {
+    this._init(objectPath);
+}
+
+Inhibitor.prototype = {
+    _init: function(objectPath) {
+        DBus.session.proxifyObject(this,
+                                   "org.gnome.SessionManager",
+                                   objectPath);
+        this.isLoaded = false;
+        this._loadingPropertiesCount = InhibitorIface.properties.length;
+        for (let i = 0; i < InhibitorIface.properties.length; i++) {
+            let propertyName = InhibitorIface.properties[i].name;
+            this.GetRemote(propertyName, Lang.bind(this,
+                function(value, exception) {
+                    if (exception)
+                        return;
+
+                    this[propertyName] = value;
+                    this._loadingPropertiesCount--;
+
+                    if (this._loadingPropertiesCount == 0) {
+                        this.isLoaded = true;
+                        this.emit("is-loaded");
+                    }
+                }));
+        }
+    },
+};
+DBus.proxifyPrototype(Inhibitor.prototype, InhibitorIface);
+Signals.addSignalMethods(Inhibitor.prototype);
diff --git a/js/ui/endSessionDialog.js b/js/ui/endSessionDialog.js
new file mode 100644
index 0000000..62b69b1
--- /dev/null
+++ b/js/ui/endSessionDialog.js
@@ -0,0 +1,504 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*-
+ *
+ * Copyright 2010 Red Hat, Inc
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
+ * 02111-1307, USA.
+ */
+
+const DBus = imports.dbus;
+const Lang = imports.lang;
+const Signals = imports.signals;
+
+const Gettext = imports.gettext.domain('gnome-shell');
+const _ = Gettext.gettext;
+
+const Clutter = imports.gi.Clutter;
+const Gdm = imports.gi.Gdm;
+const GLib = imports.gi.GLib;
+const Gtk = imports.gi.Gtk;
+const Pango = imports.gi.Pango;
+const St = imports.gi.St;
+const Shell = imports.gi.Shell;
+
+const GnomeSession = imports.misc.gnomeSession
+const Lightbox = imports.ui.lightbox;
+const Main = imports.ui.main;
+const ModalDialog = imports.ui.modalDialog;
+const Tweener = imports.ui.tweener;
+
+let _endSessionDialog = null;
+
+const _ITEM_ICON_SIZE = 48;
+const _DIALOG_ICON_SIZE = 32;
+
+const GSM_SESSION_MANAGER_LOGOUT_FORCE = 2;
+
+const EndSessionDialogIface = {
+    name: 'org.gnome.SessionManager.EndSessionDialog',
+    methods: [{ name: 'Open',
+                inSignature: 'uuuao',
+                outSignature: ''
+              }
+             ],
+    signals: [{ name: 'Canceled',
+                outSignature: '',
+              }],
+    properties: []
+};
+
+const logoutDialogContent = {
+    subjectWithUser: _("Log Out %s"),
+    subject: _("Log Out"),
+    inhibitedDescription: _("Click Log Out to quit these applications and log out of the system."),
+    uninhibitedDescriptionWithUser: _("%s will be logged out automatically in %d seconds."),
+    uninhibitedDescription: _("You will be logged out automatically in %d seconds."),
+    endDescription: _("Logging out of the system."),
+    confirmButtonText: _("Log Out"),
+    iconStyleClass: 'end-session-dialog-logout-icon'
+};
+
+const shutdownDialogContent = {
+    subject: _("Shut Down"),
+    inhibitedDescription: _("Click Shut Down to quit these applications and shut down the system."),
+    uninhibitedDescription: _("The system will shut down automatically in %d seconds."),
+    endDescription: _("Shutting down the system."),
+    confirmButtonText: _("Shut Down"),
+    iconName: 'system-shutdown',
+    iconStyleClass: 'end-session-dialog-shutdown-icon'
+};
+
+const restartDialogContent = {
+    subject: _("Restart"),
+    inhibitedDescription: _("Click Restart to quit these applications and restart the system."),
+    uninhibitedDescription: _("The system will restart automatically in %d seconds."),
+    endDescription: _("Restarting the system."),
+    confirmButtonText: _("Restart"),
+    iconName: 'system-shutdown',
+    iconStyleClass: 'end-session-dialog-shutdown-icon'
+};
+
+const DialogContent = {
+    0 /* GSM_SHELL_END_SESSION_DIALOG_TYPE_LOGOUT */: logoutDialogContent,
+    1 /* GSM_SHELL_END_SESSION_DIALOG_TYPE_SHUTDOWN */: shutdownDialogContent,
+    2 /* GSM_SHELL_END_SESSION_DIALOG_TYPE_RESTART */: restartDialogContent
+};
+
+function findAppFromInhibitor(inhibitor) {
+    let desktopFile = inhibitor.app_id;
+
+    if (!GLib.str_has_suffix(desktopFile, '.desktop'))
+      desktopFile += '.desktop';
+
+    let candidateDesktopFiles = [];
+
+    candidateDesktopFiles.push(desktopFile);
+    candidateDesktopFiles.push('gnome-' + desktopFile);
+
+    let appSystem = Shell.AppSystem.get_default();
+    let app = null;
+    for (let i = 0; i < candidateDesktopFiles.length; i++) {
+        try {
+            app = appSystem.get_app(candidateDesktopFiles[i]);
+
+            if (app)
+                break;
+        } catch(e) {
+            // ignore errors
+        }
+    }
+
+    return app;
+}
+
+function ListItem(app, reason) {
+    this._init(app, reason);
+}
+
+ListItem.prototype = {
+    _init: function(app, reason) {
+        this._app = app;
+        this._reason = reason;
+
+        if (this._reason == null)
+          this._reason = '';
+
+        let layout = new St.BoxLayout({ vertical: false});
+
+        this.actor = new St.Clickable({ style_class: 'end-session-dialog-app-list-item',
+                                        can_focus:   true,
+                                        child:       layout,
+                                        reactive:    true,
+                                        x_align:     St.Align.START,
+                                        x_fill:      true });
+
+        this._icon = this._app.create_icon_texture(_ITEM_ICON_SIZE);
+
+        let iconBin = new St.Bin({ style_class: 'end-session-dialog-app-list-item-icon',
+                                   child:       this._icon });
+        layout.add(iconBin);
+
+        let textLayout = new St.BoxLayout({ style_class: 'end-session-dialog-app-list-item-text-box',
+                                            vertical:    true });
+        layout.add(textLayout);
+
+        this._nameLabel = new St.Label({ text:        this._app.get_name(),
+                                         style_class: 'end-session-dialog-app-list-item-name' });
+        textLayout.add(this._nameLabel,
+                       { expand: false,
+                         x_fill: true });
+
+        this._descriptionLabel = new St.Label({ text:        this._reason,
+                                                style_class: 'end-session-dialog-app-list-item-description' });
+        textLayout.add(this._descriptionLabel,
+                       { expand: true,
+                         x_fill: true });
+
+        this.actor.connect('clicked', Lang.bind(this, this._onClicked));
+    },
+
+    _onClicked: function() {
+        this.emit('activate');
+        this._app.activate();
+    }
+};
+Signals.addSignalMethods(ListItem.prototype);
+
+// The logout timer only shows updates every 10 seconds
+// until the last 10 seconds, then it shows updates every
+// second.  This function takes a given time and returns
+// what we should show to the user for that time.
+function _roundSecondsToInterval(totalSeconds, secondsLeft, interval) {
+    let time;
+
+    time = Math.ceil(secondsLeft);
+
+    // Final count down is in decrements of 1
+    if (time <= interval)
+        return time;
+
+    // Round up higher than last displayable time interval
+    time += interval - 1;
+
+    // Then round down to that time interval
+    if (time > totalSeconds)
+        time = Math.ceil(totalSeconds);
+    else
+        time -= time % interval;
+
+    return time;
+}
+
+function _setLabelText(label, text) {
+    if (text) {
+        label.set_text(text);
+        label.show();
+    } else {
+        label.set_text('');
+        label.hide();
+    }
+}
+
+function EndSessionDialog() {
+    if (_endSessionDialog == null) {
+        this._init();
+        DBus.session.exportObject('/org/gnome/SessionManager/EndSessionDialog',
+                                  this);
+        _endSessionDialog = this;
+    }
+
+    return _endSessionDialog;
+}
+
+function init() {
+    // This always returns the same singleton object
+    // By instantiating it initially, we register the
+    // bus object, etc.
+    let dialog = new EndSessionDialog();
+}
+
+EndSessionDialog.prototype = {
+    __proto__: ModalDialog.ModalDialog.prototype,
+
+    _init: function() {
+        ModalDialog.ModalDialog.prototype._init.call(this);
+
+        this._user = Gdm.UserManager.ref_default().get_user(GLib.get_user_name());
+
+        this._secondsLeft = 0;
+        this._totalSecondsToStayOpen = 0;
+        this._inhibitors = [];
+
+        this.connect('destroy',
+                     Lang.bind(this, this._onDestroy));
+        this.connect('opened',
+                     Lang.bind(this, this._onOpened));
+
+        this._userLoadedId = this._user.connect('notify::is_loaded',
+                                                Lang.bind(this, this._updateContent));
+
+        this._userChangedId = this._user.connect('changed',
+                                                 Lang.bind(this, this._updateContent));
+
+        let mainContentLayout = new St.BoxLayout({ vertical: false });
+        this.contentLayout.add(mainContentLayout,
+                               { x_fill: true,
+                                 y_fill: false });
+
+        this._iconBin = new St.Bin();
+        mainContentLayout.add(this._iconBin,
+                              { x_fill:  true,
+                                y_fill:  false,
+                                x_align: St.Align.END,
+                                y_align: St.Align.START });
+
+        let messageLayout = new St.BoxLayout({ vertical: true });
+        mainContentLayout.add(messageLayout,
+                              { y_align: St.Align.START });
+
+        this._subjectLabel = new St.Label({ style_class: 'end-session-dialog-subject' });
+
+        messageLayout.add(this._subjectLabel,
+                          { y_fill:  false,
+                            y_align: St.Align.START });
+
+        this._descriptionLabel = new St.Label({ style_class: 'end-session-dialog-description' });
+        this._descriptionLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+        this._descriptionLabel.clutter_text.line_wrap = true;
+
+        messageLayout.add(this._descriptionLabel,
+                          { y_fill:  true,
+                            y_align: St.Align.START });
+
+        let scrollView = new St.ScrollView({ style_class: 'end-session-dialog-app-list'});
+        scrollView.set_policy(Gtk.PolicyType.NEVER,
+                              Gtk.PolicyType.AUTOMATIC);
+        this.contentLayout.add(scrollView,
+                               { x_fill: true,
+                                 y_fill: true });
+        this._applicationList = new St.BoxLayout({ vertical: true });
+        scrollView.add_actor(this._applicationList,
+                             { x_fill:  true,
+                               y_fill:  true,
+                               x_align: St.Align.START,
+                               y_align: St.Align.MIDDLE });
+    },
+
+    _onDestroy: function() {
+        this._user.disconnect(this._userLoadedId);
+        this._user.disconnect(this._userChangedId);
+    },
+
+    _setIconFromFile: function(iconFile, styleClass) {
+        if (styleClass)
+            this._iconBin.set_style_class_name(styleClass);
+        this._iconBin.set_style(null);
+
+        this._iconBin.child = null;
+        if (iconFile) {
+            this._iconBin.show();
+            this._iconBin.set_style('background-image: url("' + iconFile + '");');
+        } else {
+            this._iconBin.hide();
+        }
+    },
+
+    _setIconFromName: function(iconName, styleClass) {
+        if (styleClass)
+            this._iconBin.set_style_class_name(styleClass);
+        this._iconBin.set_style(null);
+
+        if (iconName != null) {
+            let textureCache = St.TextureCache.get_default();
+            let icon = textureCache.load_icon_name(this._iconBin.get_theme_node(),
+                                                   iconName,
+                                                   St.IconType.SYMBOLIC,
+                                                   _DIALOG_ICON_SIZE);
+
+            this._iconBin.child = icon;
+            this._iconBin.show();
+        } else {
+            this._iconBin.child = null;
+            this._iconBin.hide();
+        }
+    },
+
+    _updateContent: function() {
+        if (this.state != ModalDialog.State.OPENING &&
+            this.state != ModalDialog.State.OPENED)
+            return;
+
+        let dialogContent = DialogContent[this._type];
+
+        let subject = dialogContent.subject;
+        let description;
+
+        if (this._user.is_loaded && !dialogContent.iconName) {
+            let iconFile = this._user.get_icon_file();
+
+            this._setIconFromFile(iconFile, dialogContent.iconStyleClass);
+        } else if (dialogContent.iconName) {
+            this._setIconFromName(dialogContent.iconName,
+                                  dialogContent.iconStyleClass);
+        }
+
+        if (this._inhibitors.length > 0) {
+            this._stopTimer();
+            description = dialogContent.inhibitedDescription;
+        } else if (this._secondsLeft > 0 && this._inhibitors.length == 0) {
+            let displayTime = _roundSecondsToInterval(this._totalSecondsToStayOpen,
+                                                      this._secondsLeft,
+                                                      10);
+
+            if (this._user.is_loaded) {
+                let realName = this._user.get_real_name();
+
+                if (realName != null) {
+                    if (dialogContent.subjectWithUser)
+                        subject = dialogContent.subjectWithUser.format(realName);
+
+                    if (dialogContent.uninhibitedDescriptionWithUser)
+                        description = dialogContent.uninhibitedDescriptionWithUser.format(realName, displayTime);
+                    else
+                        description = dialogContent.uninhibitedDescription.format(displayTime);
+                }
+            }
+
+            if (!description)
+                description = dialogContent.uninhibitedDescription.format(displayTime);
+        } else {
+            description = dialogContent.endDescription;
+        }
+
+        _setLabelText(this._subjectLabel, subject);
+        _setLabelText(this._descriptionLabel, description);
+    },
+
+    _updateButtons: function() {
+        if (this.state != ModalDialog.State.OPENING &&
+            this.state != ModalDialog.State.OPENED)
+            return;
+
+        let dialogContent = DialogContent[this._type];
+        let confirmButtonText = _("Confirm");
+
+        if (dialogContent.confirmButtonText)
+            confirmButtonText = dialogContent.confirmButtonText;
+
+        this.setButtons([{ label: _("Cancel"),
+                           action: Lang.bind(this, this.cancel),
+                           key:    Clutter.Escape
+                         },
+                         { label:  confirmButtonText,
+                           action: Lang.bind(this, this._confirm)
+                         }]);
+    },
+
+    cancel: function() {
+        this._stopTimer();
+        DBus.session.emit_signal('/org/gnome/SessionManager/EndSessionDialog',
+                                 'org.gnome.SessionManager.EndSessionDialog',
+                                 'Canceled', '', []);
+        this.close(global.get_current_time());
+    },
+
+    _confirm: function() {
+        this._fadeOutDialog();
+        this._stopTimer();
+        DBus.session.emit_signal('/org/gnome/SessionManager/EndSessionDialog',
+                                 'org.gnome.SessionManager.EndSessionDialog',
+                                 'Confirmed', '', []);
+    },
+
+    _onOpened: function() {
+        if (this._inhibitors.length == 0)
+            this._startTimer();
+    },
+
+    _startTimer: function() {
+        this._secondsLeft = this._totalSecondsToStayOpen;
+        Tweener.addTween(this,
+                         { _secondsLeft: 0,
+                           time: this._secondsLeft,
+                           transition: 'linear',
+                           onUpdate: Lang.bind(this, this._updateContent),
+                           onComplete: Lang.bind(this, this._confirm),
+                         });
+    },
+
+    _stopTimer: function() {
+        Tweener.removeTweens(this);
+        this._secondsLeft = 0;
+    },
+
+    _onInhibitorLoaded: function(inhibitor) {
+        if (this._inhibitors.indexOf(inhibitor) < 0) {
+            // Stale inhibitor
+            return;
+        }
+
+        let app = findAppFromInhibitor(inhibitor);
+
+        if (app) {
+            let item = new ListItem(app, inhibitor.reason);
+            item.connect('activate',
+                         Lang.bind(this, function() {
+                             this.close(global.get_current_time());
+                         }));
+            this._applicationList.add(item.actor, { x_fill: true });
+            this._stopTimer();
+        } else {
+            // inhibiting app is a service, not an application
+            this._inhibitors.splice(this._inhibitors.indexOf(inhibitor), 1);
+        }
+
+        this._updateContent();
+    },
+
+    OpenAsync: function(type, timestamp, totalSecondsToStayOpen, inhibitorObjectPaths, callback) {
+        this._totalSecondsToStayOpen = totalSecondsToStayOpen;
+        this._inhibitors = [];
+        this._applicationList.remove_all();
+        this._type = type;
+
+        if (!(this._type in DialogContent))
+            throw new DBus.DBusError('org.gnome.Shell.ModalDialog.TypeError',
+                                     "Unknown dialog type requested");
+
+        for (let i = 0; i < inhibitorObjectPaths.length; i++) {
+            let inhibitor = new GnomeSession.Inhibitor(inhibitorObjectPaths[i]);
+
+            inhibitor.connect('is-loaded',
+                              Lang.bind(this, function() {
+                                  this._onInhibitorLoaded(inhibitor);
+                              }));
+            this._inhibitors.push(inhibitor);
+        }
+
+        if (!this.open(timestamp))
+            throw new DBus.DBusError('org.gnome.Shell.ModalDialog.GrabError',
+                                     "Cannot grab pointer and keyboard");
+
+        this._updateButtons();
+        this._updateContent();
+
+        let signalId = this.connect('opened',
+                                    Lang.bind(this, function() {
+                                        callback();
+                                        this.disconnect(signalId);
+                                    }));
+    }
+};
+DBus.conformExport(EndSessionDialog.prototype, EndSessionDialogIface);
diff --git a/js/ui/main.js b/js/ui/main.js
index 1d068eb..1d456af 100644
--- a/js/ui/main.js
+++ b/js/ui/main.js
@@ -22,6 +22,7 @@ const _ = Gettext.gettext;
 
 const Chrome = imports.ui.chrome;
 const CtrlAltTab = imports.ui.ctrlAltTab;
+const EndSessionDialog = imports.ui.endSessionDialog;
 const Environment = imports.ui.environment;
 const ExtensionSystem = imports.ui.extensionSystem;
 const MessageTray = imports.ui.messageTray;
@@ -171,6 +172,10 @@ function start() {
         }
     });
 
+    // Provide the bus object for gnome-session to
+    // initiate logouts.
+    EndSessionDialog.init();
+
     global.gdk_screen.connect('monitors-changed', _relayout);
 
     ExtensionSystem.init();



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