// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- /* * Copyright 2011 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 AccountsService = imports.gi.AccountsService; const Clutter = imports.gi.Clutter; const CtrlAltTab = imports.ui.ctrlAltTab; const Gio = imports.gi.Gio; const GLib = imports.gi.GLib; const Gtk = imports.gi.Gtk; const Mainloop = imports.mainloop; const Lang = imports.lang; const Pango = imports.gi.Pango; const Signals = imports.signals; const Shell = imports.gi.Shell; const St = imports.gi.St; const GdmGreeter = imports.gi.GdmGreeter; const Batch = imports.gdm.batch; const Fprint = imports.gdm.fingerprint; const Lightbox = imports.ui.lightbox; const Main = imports.ui.main; const ModalDialog = imports.ui.modalDialog; const Tweener = imports.ui.tweener; const _PASSWORD_SERVICE_NAME = 'gdm-password'; const _FINGERPRINT_SERVICE_NAME = 'gdm-fingerprint'; const _FADE_ANIMATION_TIME = 0.16; const _RESIZE_ANIMATION_TIME = 0.25; const _SCROLL_ANIMATION_TIME = 2.0; const _TIMED_LOGIN_IDLE_THRESHOLD = 5.0; const _LOGO_ICON_NAME_SIZE = 48; const _LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen'; const _FINGERPRINT_AUTHENTICATION_KEY = 'enable-fingerprint-authentication'; const _LOGO_KEY = 'logo'; let _loginDialog = null; function _fadeInActor(actor) { let hold = new Batch.Hold(); if (actor.opacity == 255 && actor.visible) return null; actor.show(); let [minHeight, naturalHeight] = actor.get_preferred_height(-1); actor.opacity = 0; actor.set_height(0); Tweener.addTween(actor, { opacity: 255, height: naturalHeight, time: _FADE_ANIMATION_TIME, transition: 'easeOutQuad', onComplete: function() { actor.set_height(-1); hold.release(); }, onCompleteScope: this }); return hold; } function _fadeOutActor(actor) { let hold = new Batch.Hold(); if (!actor.visible) { actor.opacity = 0; return null; } if (actor.opacity == 0) { actor.hide(); return null; } Tweener.addTween(actor, { opacity: 0, height: 0, time: _FADE_ANIMATION_TIME, transition: 'easeOutQuad', onComplete: function() { actor.hide(); actor.set_height(-1); hold.release(); }, onCompleteScope: this }); return hold; } function _smoothlyResizeActor(actor, width, height) { let finalWidth; let finalHeight; if (width < 0) finalWidth = actor.width; else finalWidth = width; if (height < 0) finalHeight = actor.height; else finalHeight = height; actor.set_size(actor.width, actor.height); if (actor.width == finalWidth && actor.height == finalHeight) return null; let hold = new Batch.Hold(); Tweener.addTween(actor, { width: finalWidth, height: finalHeight, time: _RESIZE_ANIMATION_TIME, transition: 'easeOutQuad', onComplete: Lang.bind(this, function() { hold.release(); }) }); return hold; } const UserListItem = new Lang.Class({ Name: 'UserListItem', _init: function(user) { this.user = user; this._userChangedId = this.user.connect('changed', Lang.bind(this, this._onUserChanged)); this._verticalBox = new St.BoxLayout({ style_class: 'login-dialog-user-list-item-vertical-layout', vertical: true }); this.actor = new St.Button({ style_class: 'login-dialog-user-list-item', can_focus: true, child: this._verticalBox, reactive: true, x_align: St.Align.START, x_fill: true }); let layout = new St.BoxLayout({ vertical: false }); this._verticalBox.add(layout, { y_fill: true, x_fill: true, expand: true }); this._focusBin = new St.Bin({ style_class: 'login-dialog-user-list-item-focus-bin' }); this._verticalBox.add(this._focusBin, { x_fill: false, x_align: St.Align.MIDDLE, y_fill: false, expand: true }); this._iconBin = new St.Bin(); layout.add(this._iconBin); let textLayout = new St.BoxLayout({ style_class: 'login-dialog-user-list-item-text-box', vertical: true }); layout.add(textLayout, { y_fill: false, y_align: St.Align.MIDDLE, expand: true }); this._nameLabel = new St.Label({ text: this.user.get_real_name(), style_class: 'login-dialog-user-list-item-name' }); textLayout.add(this._nameLabel); this._updateIcon(); this.actor.connect('clicked', Lang.bind(this, this._onClicked)); }, _onUserChanged: function() { this._nameLabel.set_text(this.user.get_real_name()); this._updateIcon(); }, _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(); // We use background-image instead of, say, St.TextureCache // so the theme writers can add a rounded frame around the image // and so theme writers can pick the icon size. 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 icon = new St.Icon(); icon.set_icon_name(iconName) this._iconBin.child = icon; this._iconBin.show(); } else { this._iconBin.child = null; this._iconBin.hide(); } }, _updateIcon: function() { let iconFileName = this.user.get_icon_file(); let gicon = null; if (GLib.file_test(iconFileName, GLib.FileTest.EXISTS)) this._setIconFromFile(iconFileName, 'login-dialog-user-list-item-icon'); else this._setIconFromName('avatar-default', 'login-dialog-user-list-item-icon'); }, _onClicked: function() { this.emit('activate'); }, fadeOutName: function() { return _fadeOutActor(this._nameLabel); }, fadeInName: function() { return _fadeInActor(this._nameLabel); }, showFocusAnimation: function(time) { let hold = new Batch.Hold(); let node = this.actor.get_theme_node(); let padding = node.get_horizontal_padding(); let box = this._verticalBox.get_allocation_box(); Tweener.removeTweens(this._focusBin); this._focusBin.width = 0; Tweener.addTween(this._focusBin, { width: (box.x2 - box.x1 - padding), time: time, transition: 'linear', onComplete: function() { hold.release(); }, onCompleteScope: this }); return hold; } }); Signals.addSignalMethods(UserListItem.prototype); const UserList = new Lang.Class({ Name: 'UserList', _init: function() { this.actor = new St.ScrollView({ style_class: 'login-dialog-user-list-view'}); this.actor.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC); this._box = new St.BoxLayout({ vertical: true, style_class: 'login-dialog-user-list' }); this.actor.add_actor(this._box, { x_fill: true, y_fill: true, x_align: St.Align.START, y_align: St.Align.MIDDLE }); this._items = {}; this.actor.connect('key-focus-in', Lang.bind(this, this._moveFocusToItems)); }, _moveFocusToItems: function() { let hasItems = Object.keys(this._items).length > 0; if (!hasItems) return; if (global.stage.get_key_focus() != this.actor) return; this.actor.navigate_focus(null, Gtk.DirectionType.TAB_FORWARD, false); }, _showItem: function(item) { let tasks = [function() { return _fadeInActor(item.actor); }, function() { return item.fadeInName(); }]; let batch = new Batch.ConsecutiveBatch(this, tasks); return batch.run(); }, _onItemActivated: function(activatedItem) { this.emit('activate', activatedItem); }, giveUpWhitespace: function() { let container = this.actor.get_parent(); container.child_set(this.actor, { expand: false }); }, takeOverWhitespace: function() { let container = this.actor.get_parent(); container.child_set(this.actor, { expand: true }); }, pinInPlace: function() { this._box.set_size(this._box.width, this._box.height); }, shrinkToNaturalHeight: function() { let oldWidth = this._box.width; let oldHeight = this._box.height; this._box.set_size(-1, -1); let [minHeight, naturalHeight] = this._box.get_preferred_height(-1); this._box.set_size(oldWidth, oldHeight); let batch = new Batch.ConsecutiveBatch(this, [function() { return _smoothlyResizeActor(this._box, -1, naturalHeight); }, function() { this._box.set_size(-1, -1); } ]); return batch.run(); }, hideItemsExcept: function(exception) { let tasks = []; for (let userName in this._items) { let item = this._items[userName]; item.actor.can_focus = false; item._focusBin.width = 0; if (item != exception) tasks.push(function() { return _fadeOutActor(item.actor); }); } let batch = new Batch.ConsecutiveBatch(this, [function() { return _fadeOutActor(this.actor.vscroll); }, new Batch.ConcurrentBatch(this, tasks) ]); return batch.run(); }, hideItems: function() { return this.hideItemsExcept(null); }, _getExpandedHeight: function() { let hiddenActors = []; for (let userName in this._items) { let item = this._items[userName]; if (!item.actor.visible) { item.actor.show(); hiddenActors.push(item.actor); } } if (!this._box.visible) { this._box.show(); hiddenActors.push(this._box); } this._box.set_size(-1, -1); let [minHeight, naturalHeight] = this._box.get_preferred_height(-1); for (let i = 0; i < hiddenActors.length; i++) { let actor = hiddenActors[i]; actor.hide(); } return naturalHeight; }, showItems: function() { let tasks = []; for (let userName in this._items) { let item = this._items[userName]; item.actor.can_focus = true; tasks.push(function() { return this._showItem(item); }); } let batch = new Batch.ConsecutiveBatch(this, [function() { this.takeOverWhitespace(); }, function() { let fullHeight = this._getExpandedHeight(); return _smoothlyResizeActor(this._box, -1, fullHeight); }, new Batch.ConcurrentBatch(this, tasks), function() { this.actor.set_size(-1, -1); }, function() { return _fadeInActor(this.actor.vscroll); }]); return batch.run(); }, scrollToItem: function(item) { let box = item.actor.get_allocation_box(); let adjustment = this.actor.get_vscroll_bar().get_adjustment(); let value = (box.y1 + adjustment.step_increment / 2.0) - (adjustment.page_size / 2.0); Tweener.removeTweens(adjustment); Tweener.addTween (adjustment, { value: value, time: _SCROLL_ANIMATION_TIME, transition: 'linear' }); }, jumpToItem: function(item) { let box = item.actor.get_allocation_box(); let adjustment = this.actor.get_vscroll_bar().get_adjustment(); let value = (box.y1 + adjustment.step_increment / 2.0) - (adjustment.page_size / 2.0); adjustment.set_value(value); }, getItemFromUserName: function(userName) { let item = this._items[userName]; if (!item) return null; return item; }, addUser: function(user) { if (!user.is_loaded) return; if (user.is_system_account()) return; let userName = user.get_user_name(); if (!userName) return; this.removeUser(user); let item = new UserListItem(user); this._box.add(item.actor, { x_fill: true }); this._items[userName] = item; item.connect('activate', Lang.bind(this, this._onItemActivated)); // Try to keep the focused item front-and-center item.actor.connect('key-focus-in', Lang.bind(this, function() { this.scrollToItem(item); item.showFocusAnimation(0); })); this._moveFocusToItems(); this.emit('item-added', item); }, removeUser: function(user) { if (!user.is_loaded) return; let userName = user.get_user_name(); if (!userName) return; let item = this._items[userName]; if (!item) return; item.actor.destroy(); delete this._items[userName]; } }); Signals.addSignalMethods(UserList.prototype); const SessionListItem = new Lang.Class({ Name: 'SessionListItem', _init: function(id, name) { this.id = id; this.actor = new St.Button({ style_class: 'login-dialog-session-list-item', can_focus: true, reactive: true, x_fill: true, x_align: St.Align.START }); this._box = new St.BoxLayout({ style_class: 'login-dialog-session-list-item-box' }); this.actor.add_actor(this._box, { expand: true, x_fill: true, y_fill: true }); this.actor.connect('clicked', Lang.bind(this, this._onClicked)); this._dot = new St.DrawingArea({ style_class: 'login-dialog-session-list-item-dot' }); this._dot.connect('repaint', Lang.bind(this, this._onRepaintDot)); this._box.add_actor(this._dot); this.setShowDot(false); let label = new St.Label({ style_class: 'login-dialog-session-list-item-label', text: name }); this._box.add_actor(label, { expand: true, x_fill: true, y_fill: true }); }, setShowDot: function(show) { if (show) this._dot.opacity = 255; else this._dot.opacity = 0; }, _onRepaintDot: function(area) { let cr = area.get_context(); let [width, height] = area.get_surface_size(); let color = area.get_theme_node().get_foreground_color(); cr.setSourceRGBA (color.red / 255, color.green / 255, color.blue / 255, color.alpha / 255); cr.arc(width / 2, height / 2, width / 3, 0, 2 * Math.PI); cr.fill(); }, _onClicked: function() { this.emit('activate'); } }); Signals.addSignalMethods(SessionListItem.prototype); const SessionList = new Lang.Class({ Name: 'SessionList', _init: function() { this.actor = new St.Bin(); this._box = new St.BoxLayout({ style_class: 'login-dialog-session-list', vertical: true}); this.actor.child = this._box; this._button = new St.Button({ style_class: 'login-dialog-session-list-button', can_focus: true, x_fill: true, y_fill: true }); let box = new St.BoxLayout(); this._button.add_actor(box, { x_fill: true, y_fill: true, expand: true }); this._triangle = new St.Label({ style_class: 'login-dialog-session-list-triangle', text: '\u25B8' }); box.add_actor(this._triangle); let label = new St.Label({ style_class: 'login-dialog-session-list-label', text: _("Session...") }); box.add_actor(label, { x_fill: true, y_fill: true, expand: true }); this._button.connect('clicked', Lang.bind(this, this._onClicked)); this._box.add_actor(this._button, { x_fill: true, y_fill: true, expand: true }); this._scrollView = new St.ScrollView({ style_class: 'login-dialog-session-list-scroll-view'}); this._scrollView.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC); this._box.add_actor(this._scrollView, { x_fill: true, y_fill: true, expand: true }); this._itemList = new St.BoxLayout({ style_class: 'login-dialog-session-item-list', vertical: true }); this._scrollView.add_actor(this._itemList, { x_fill: true, y_fill: true, expand: true }); this._scrollView.hide(); this.isOpen = false; this._populate(); }, open: function() { if (this.isOpen) return; this._button.add_style_pseudo_class('open'); this._scrollView.show(); this._triangle.set_text('\u25BE'); this.isOpen = true; }, close: function() { if (!this.isOpen) return; this._button.remove_style_pseudo_class('open'); this._scrollView.hide(); this._triangle.set_text('\u25B8'); this.isOpen = false; }, _onClicked: function() { if (!this.isOpen) this.open(); else this.close(); }, setActiveSession: function(sessionId) { if (sessionId == this._activeSessionId) return; if (this._activeSessionId) this._items[this._activeSessionId].setShowDot(false); this._items[sessionId].setShowDot(true); this._activeSessionId = sessionId; this.emit('session-activated', this._activeSessionId); }, _populate: function() { this._itemList.destroy_children(); this._activeSessionId = null; this._items = {}; let ids = GdmGreeter.get_session_ids(); ids.sort(); if (ids.length <= 1) { this._box.hide(); this._button.hide(); } else { this._button.show(); this._box.show(); } for (let i = 0; i < ids.length; i++) { let [sessionName, sessionDescription] = GdmGreeter.get_session_name_and_description(ids[i]); let item = new SessionListItem(ids[i], sessionName); this._itemList.add_actor(item.actor, { x_align: St.Align.START, y_align: St.Align.START, x_fill: true, y_fill: true }); this._items[ids[i]] = item; if (!this._activeSessionId) this.setActiveSession(ids[i]); item.connect('activate', Lang.bind(this, function() { this.setActiveSession(item.id); })); } } }); Signals.addSignalMethods(SessionList.prototype); const LoginDialog = new Lang.Class({ Name: 'LoginDialog', Extends: ModalDialog.ModalDialog, _init: function() { this.parent({ shellReactive: true, styleClass: 'login-dialog' }); this.connect('destroy', Lang.bind(this, this._onDestroy)); this.connect('opened', Lang.bind(this, this._onOpened)); this._userManager = AccountsService.UserManager.get_default() this._greeterClient = new GdmGreeter.Client(); this._greeterClient.open_connection(); this._greeterClient.call_start_conversation(_PASSWORD_SERVICE_NAME); this._greeterClient.connect('reset', Lang.bind(this, this._onReset)); this._greeterClient.connect('default-session-changed', Lang.bind(this, this._onDefaultSessionChanged)); this._greeterClient.connect('info', Lang.bind(this, this._onInfo)); this._greeterClient.connect('problem', Lang.bind(this, this._onProblem)); this._greeterClient.connect('info-query', Lang.bind(this, this._onInfoQuery)); this._greeterClient.connect('secret-info-query', Lang.bind(this, this._onSecretInfoQuery)); this._greeterClient.connect('session-opened', Lang.bind(this, this._onSessionOpened)); this._greeterClient.connect('timed-login-requested', Lang.bind(this, this._onTimedLoginRequested)); this._greeterClient.connect('authentication-failed', Lang.bind(this, this._onAuthenticationFailed)); this._greeterClient.connect('conversation-stopped', Lang.bind(this, this._onConversationStopped)); this._settings = new Gio.Settings({ schema: _LOGIN_SCREEN_SCHEMA }); this._fprintManager = new Fprint.FprintManager(); this._startFingerprintConversationIfNeeded(); this._settings.connect('changed::' + _LOGO_KEY, Lang.bind(this, this._updateLogo)); this._logoBox = new St.Bin({ style_class: 'login-dialog-logo-box' }); this.contentLayout.add(this._logoBox); this._updateLogo(); this._titleLabel = new St.Label({ style_class: 'login-dialog-title', text: C_("title", "Sign In") }); this.contentLayout.add(this._titleLabel, { y_fill: false, y_align: St.Align.START }); let mainContentBox = new St.BoxLayout({ vertical: false }); this.contentLayout.add(mainContentBox, { expand: true, x_fill: true, y_fill: false }); this._userList = new UserList(); mainContentBox.add(this._userList.actor, { expand: true, x_fill: true, y_fill: true }); this.setInitialKeyFocus(this._userList.actor); this._promptBox = new St.BoxLayout({ style_class: 'login-dialog-prompt-layout', vertical: true }); mainContentBox.add(this._promptBox, { expand: true, x_fill: true, y_fill: true, x_align: St.Align.START }); this._promptLabel = new St.Label({ style_class: 'login-dialog-prompt-label' }); this._mainContentBox = mainContentBox; this._promptBox.add(this._promptLabel, { expand: true, x_fill: true, y_fill: true, x_align: St.Align.START }); this._promptEntry = new St.Entry({ style_class: 'login-dialog-prompt-entry', can_focus: true }); this._promptBox.add(this._promptEntry, { expand: true, x_fill: true, y_fill: false, x_align: St.Align.START }); // Translators: this message is shown below the password entry field // to indicate the user can swipe their finger instead this._promptFingerprintMessage = new St.Label({ text: _("(or swipe finger)"), style_class: 'login-dialog-prompt-fingerprint-message' }); this._promptFingerprintMessage.hide(); this._promptBox.add(this._promptFingerprintMessage); this._sessionList = new SessionList(); this._sessionList.connect('session-activated', Lang.bind(this, function(list, sessionId) { this._greeterClient.call_select_session (sessionId); })); this._promptBox.add(this._sessionList.actor, { expand: true, x_fill: false, y_fill: true, x_align: St.Align.START }); this._promptBox.hide(); // translators: this message is shown below the user list on the // login screen. It can be activated to reveal an entry for // manually entering the username. let notListedLabel = new St.Label({ text: _("Not listed?"), style_class: 'login-dialog-not-listed-label' }); this._notListedButton = new St.Button({ style_class: 'login-dialog-not-listed-button', can_focus: true, child: notListedLabel, reactive: true, x_align: St.Align.START, x_fill: true }); this._notListedButton.connect('clicked', Lang.bind(this, this._onNotListedClicked)); this.contentLayout.add(this._notListedButton, { expand: false, x_align: St.Align.START, x_fill: true }); if (!this._userManager.is_loaded) this._userManagerLoadedId = this._userManager.connect('notify::is-loaded', Lang.bind(this, function() { if (this._userManager.is_loaded) { this._loadUserList(); this._userManager.disconnect(this._userManagerLoadedId); this._userManagerLoadedId = 0; } })); else this._loadUserList(); this._userList.connect('activate', Lang.bind(this, function(userList, item) { this._onUserListActivated(item); })); }, _startFingerprintConversationIfNeeded: function() { this._haveFingerprintReader = false; if (!this._settings.get_boolean(_FINGERPRINT_AUTHENTICATION_KEY)) return; this._fprintManager.GetDefaultDeviceRemote(Gio.DBusCallFlags.NONE, Lang.bind(this, function(device, error) { if (!error && device) this._haveFingerprintReader = true; if (this._haveFingerprintReader) this._greeterClient.call_start_conversation(_FINGERPRINT_SERVICE_NAME); })); }, _updateLogo: function() { this._logoBox.child = null; let path = this._settings.get_string(_LOGO_KEY); if (path) { let file = Gio.file_new_for_path(path); let uri = file.get_uri(); let textureCache = St.TextureCache.get_default(); this._logoBox.child = textureCache.load_uri_async(uri, -1, _LOGO_ICON_NAME_SIZE); } }, _onReset: function(client, serviceName) { this._greeterClient.call_start_conversation(_PASSWORD_SERVICE_NAME); this._startFingerprintConversationIfNeeded(); let tasks = [this._hidePrompt, new Batch.ConcurrentBatch(this, [this._fadeInTitleLabel, this._fadeInNotListedButton, this._fadeInLogo]), function() { this._sessionList.close(); this._promptFingerprintMessage.hide(); this._userList.actor.show(); this._userList.actor.opacity = 255; return this._userList.showItems(); }, function() { this._userList.actor.reactive = true; this._userList.actor.grab_key_focus(); }]; this._user = null; let batch = new Batch.ConsecutiveBatch(this, tasks); batch.run(); }, _onDefaultSessionChanged: function(client, sessionId) { this._sessionList.setActiveSession(sessionId); }, _onInfo: function(client, serviceName, info) { // We don't display fingerprint messages, because they // have words like UPEK in them. Instead we use the messages // as a cue to display our own message. if (serviceName == _FINGERPRINT_SERVICE_NAME && this._haveFingerprintReader && (!this._promptFingerprintMessage.visible || this._promptFingerprintMessage.opacity != 255)) { _fadeInActor(this._promptFingerprintMessage); return; } if (serviceName != _PASSWORD_SERVICE_NAME) return; Main.notifyError(info); }, _onProblem: function(client, serviceName, problem) { // we don't want to show auth failed messages to // users who haven't enrolled their fingerprint. if (serviceName != _PASSWORD_SERVICE_NAME) return; Main.notifyError(problem); }, _onCancel: function(client) { this._greeterClient.call_cancel(); }, _fadeInPrompt: function() { let tasks = [function() { return _fadeInActor(this._promptLabel); }, function() { return _fadeInActor(this._promptEntry); }, function() { // Show it with 0 opacity so we preallocate space for it // in the event we need to fade in the message this._promptFingerprintMessage.opacity = 0; this._promptFingerprintMessage.show(); }, function() { return _fadeInActor(this._promptBox); }, function() { if (this._user && this._user.is_logged_in()) return null; return _fadeInActor(this._sessionList.actor); }, function() { this._promptEntry.grab_key_focus(); }]; this._sessionList.actor.hide(); let batch = new Batch.ConcurrentBatch(this, tasks); return batch.run(); }, _showPrompt: function() { let hold = new Batch.Hold(); let buttons = [{ action: Lang.bind(this, this._onCancel), label: _("Cancel"), key: Clutter.Escape }, { action: Lang.bind(this, function() { hold.release(); }), label: C_("button", "Sign In") }]; this._promptEntryActivateCallbackId = this._promptEntry.clutter_text.connect('activate', Lang.bind(this, function() { hold.release(); })); hold.connect('release', Lang.bind(this, function() { this._promptEntry.clutter_text.disconnect(this._promptEntryActivateCallbackId); this._promptEntryActivateCallbackId = null; })); let tasks = [function() { return this._fadeInPrompt(); }, function() { this.setButtons(buttons); }, hold]; let batch = new Batch.ConcurrentBatch(this, tasks); return batch.run(); }, _hidePrompt: function() { if (this._promptEntryActivateCallbackId) { this._promptEntry.clutter_text.disconnect(this._promptEntryActivateCallbackId); this._promptEntryActivateCallbackId = null; } this.setButtons([]); let tasks = [function() { return _fadeOutActor(this._promptBox); }, function() { this._promptFingerprintMessage.hide(); this._promptEntry.reactive = true; this._promptEntry.remove_style_pseudo_class('insensitive'); this._promptEntry.set_text(''); }]; let batch = new Batch.ConsecutiveBatch(this, tasks); return batch.run(); }, _askQuestion: function(serviceName, question) { this._promptLabel.set_text(question); let tasks = [this._showPrompt, function() { let _text = this._promptEntry.get_text(); this._promptEntry.reactive = false; this._promptEntry.add_style_pseudo_class('insensitive'); this._greeterClient.call_answer_query(serviceName, _text); }]; let batch = new Batch.ConsecutiveBatch(this, tasks); return batch.run(); }, _onInfoQuery: function(client, serviceName, question) { // We only expect questions to come from the main auth service if (serviceName != _PASSWORD_SERVICE_NAME) return; this._promptEntry.set_text(''); this._promptEntry.clutter_text.set_password_char(''); this._askQuestion(serviceName, question); }, _onSecretInfoQuery: function(client, serviceName, secretQuestion) { // We only expect secret requests to come from the main auth service if (serviceName != _PASSWORD_SERVICE_NAME) return; this._promptEntry.set_text(''); this._promptEntry.clutter_text.set_password_char('\u25cf'); this._askQuestion(serviceName, secretQuestion); }, _onSessionOpened: function(client, serviceName) { this._greeterClient.call_start_session_when_ready(serviceName, true); }, _waitForItemForUser: function(userName) { let item = this._userList.getItemFromUserName(userName); if (item) return null; let hold = new Batch.Hold(); let signalId = this._userList.connect('item-added', Lang.bind(this, function() { let item = this._userList.getItemFromUserName(userName); if (item) hold.release(); })); hold.connect('release', Lang.bind(this, function() { this._userList.disconnect(signalId); })); return hold; }, _showTimedLoginAnimation: function() { this._timedLoginItem.actor.grab_key_focus(); return this._timedLoginItem.showFocusAnimation(this._timedLoginAnimationTime); }, _blockTimedLoginUntilIdle: function() { // This blocks timed login from starting until a few // seconds after the user stops interacting with the // login screen. // // We skip this step if the timed login delay is very // short. if ((this._timedLoginDelay - _TIMED_LOGIN_IDLE_THRESHOLD) <= 0) return null; let hold = new Batch.Hold(); this._timedLoginIdleTimeOutId = Mainloop.timeout_add_seconds(_TIMED_LOGIN_IDLE_THRESHOLD, function() { this._timedLoginAnimationTime -= _TIMED_LOGIN_IDLE_THRESHOLD; hold.release(); }); return hold; }, _startTimedLogin: function(userName, delay) { this._timedLoginItem = null; this._timedLoginDelay = delay; this._timedLoginAnimationTime = delay; let tasks = [function() { return this._waitForItemForUser(userName); }, function() { this._timedLoginItem = this._userList.getItemFromUserName(userName); }, function() { // If we're just starting out, start on the right // item. if (!this.is_loaded) { this._userList.jumpToItem(this._timedLoginItem); this._timedLoginItem.showFocusAnimation(0); } }, this._blockTimedLoginUntilIdle, function() { this._userList.scrollToItem(this._timedLoginItem); }, this._showTimedLoginAnimation, function() { this._timedLoginBatch = null; this._greeterClient.call_begin_auto_login(userName); }]; this._timedLoginBatch = new Batch.ConsecutiveBatch(this, tasks); return this._timedLoginBatch.run(); }, _resetTimedLogin: function() { if (this._timedLoginBatch) { this._timedLoginBatch.cancel(); this._timedLoginBatch = null; } let userName = this._timedLoginItem.user.get_user_name(); if (userName) this._startTimedLogin(userName, this._timedLoginDelay); }, _onTimedLoginRequested: function(client, userName, seconds) { this._startTimedLogin(userName, seconds); global.stage.connect('captured-event', Lang.bind(this, function(actor, event) { if (this._timedLoginDelay == undefined) return false; if (event.type() == Clutter.EventType.KEY_PRESS || event.type() == Clutter.EventType.BUTTON_PRESS) { if (this._timedLoginBatch) { this._timedLoginBatch.cancel(); this._timedLoginBatch = null; } } else if (event.type() == Clutter.EventType.KEY_RELEASE || event.type() == Clutter.EventType.BUTTON_RELEASE) { this._resetTimedLogin(); } return false; })); }, _onAuthenticationFailed: function(client) { this._greeterClient.call_cancel(); }, _onConversationStopped: function(client, serviceName) { // if the password service fails, then cancel everything. // But if, e.g., fingerprint fails, still give // password authentication a chance to succeed if (serviceName == _PASSWORD_SERVICE_NAME) { this._greeterClient.call_cancel(); } else if (serviceName == _FINGERPRINT_SERVICE_NAME) { _fadeOutActor(this._promptFingerprintMessage); } }, _onNotListedClicked: function(user) { let tasks = [function() { return this._userList.hideItems(); }, function() { return this._userList.giveUpWhitespace(); }, function() { this._userList.actor.hide(); }, new Batch.ConcurrentBatch(this, [this._fadeOutTitleLabel, this._fadeOutNotListedButton, this._fadeOutLogo]), function() { this._greeterClient.call_begin_verification(_PASSWORD_SERVICE_NAME); }]; let batch = new Batch.ConsecutiveBatch(this, tasks); batch.run(); }, _fadeInLogo: function() { return _fadeInActor(this._logoBox); }, _fadeOutLogo: function() { return _fadeOutActor(this._logoBox); }, _fadeInTitleLabel: function() { return _fadeInActor(this._titleLabel); }, _fadeOutTitleLabel: function() { return _fadeOutActor(this._titleLabel); }, _fadeInNotListedButton: function() { return _fadeInActor(this._notListedButton); }, _fadeOutNotListedButton: function() { return _fadeOutActor(this._notListedButton); }, _onUserListActivated: function(activatedItem) { let tasks = [function() { this._userList.actor.reactive = false; return this._userList.pinInPlace(); }, function() { return this._userList.hideItemsExcept(activatedItem); }, function() { return this._userList.giveUpWhitespace(); }, function() { return activatedItem.fadeOutName(); }, new Batch.ConcurrentBatch(this, [this._fadeOutTitleLabel, this._fadeOutNotListedButton, this._fadeOutLogo]), function() { return this._userList.shrinkToNaturalHeight(); }, function() { let userName = activatedItem.user.get_user_name(); this._greeterClient.call_begin_verification_for_user(_PASSWORD_SERVICE_NAME, userName); if (this._haveFingerprintReader) this._greeterClient.call_begin_verification_for_user(_FINGERPRINT_SERVICE_NAME, userName); }]; this._user = activatedItem.user; let batch = new Batch.ConsecutiveBatch(this, tasks); batch.run(); }, _onDestroy: function() { if (this._userManagerLoadedId) { this._userManager.disconnect(this._userManagerLoadedId); this._userManagerLoadedId = 0; } }, _loadUserList: function() { let users = this._userManager.list_users(); for (let i = 0; i < users.length; i++) { this._userList.addUser(users[i]); } this._userManager.connect('user-added', Lang.bind(this, function(userManager, user) { this._userList.addUser(user); })); this._userManager.connect('user-removed', Lang.bind(this, function(userManager, user) { this._userList.removeUser(user); })); // emitted in idle so caller doesn't have to explicitly check if // it's loaded immediately after construction // (since there's no way the caller could be listening for // 'loaded' yet) Mainloop.idle_add(Lang.bind(this, function() { this.emit('loaded'); this.is_loaded = true; })); }, _onOpened: function() { Main.ctrlAltTabManager.addGroup(this._mainContentBox, _("Login Window"), 'dialog-password', { sortGroup: CtrlAltTab.SortGroup.MIDDLE }); }, close: function() { this.parent(); Main.ctrlAltTabManager.removeGroup(this._group); } });