From e73e4375b8c4f1f0982ca0b23c24706e81c25bad Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Thu, 6 Jan 2011 10:30:15 -0500 Subject: [PATCH] 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(+) create mode 100644 js/ui/endSessionDialog.js diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css index 2d83bc069..393cce95f 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 3dce4f292..1b02f4968 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 06f52a36b..ef4ef848d 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 000000000..62b69b12b --- /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 1d068ebe5..1d456af8d 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();