// -*- 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, see <http://www.gnu.org/licenses/>.
 */

const AccountsService = imports.gi.AccountsService;
const Atk = imports.gi.Atk;
const Clutter = imports.gi.Clutter;
const Gdm = imports.gi.Gdm;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const Gtk = imports.gi.Gtk;
const Lang = imports.lang;
const Mainloop = imports.mainloop;
const Meta = imports.gi.Meta;
const Pango = imports.gi.Pango;
const Shell = imports.gi.Shell;
const Signals = imports.signals;
const St = imports.gi.St;

const AuthPrompt = imports.gdm.authPrompt;
const Batch = imports.gdm.batch;
const BoxPointer = imports.ui.boxpointer;
const CtrlAltTab = imports.ui.ctrlAltTab;
const GdmUtil = imports.gdm.util;
const Layout = imports.ui.layout;
const LoginManager = imports.misc.loginManager;
const Main = imports.ui.main;
const PopupMenu = imports.ui.popupMenu;
const Realmd = imports.gdm.realmd;
const Tweener = imports.ui.tweener;
const UserWidget = imports.ui.userWidget;

const _FADE_ANIMATION_TIME = 0.25;
const _SCROLL_ANIMATION_TIME = 0.5;
const _TIMED_LOGIN_IDLE_THRESHOLD = 5.0;
const _LOGO_ICON_HEIGHT = 48;

const UserListItem = new Lang.Class({
    Name: 'UserListItem',

    _init: function(user) {
        this.user = user;
        this._userChangedId = this.user.connect('changed',
                                                 Lang.bind(this, this._onUserChanged));

        let layout = new St.BoxLayout({ vertical: true });
        this.actor = new St.Button({ style_class: 'login-dialog-user-list-item',
                                     button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE,
                                     can_focus: true,
                                     child: layout,
                                     reactive: true,
                                     x_align: St.Align.START,
                                     x_fill: true });
        this.actor.connect('destroy',
                           Lang.bind(this, this._onDestroy));

        this._userWidget = new UserWidget.UserWidget(this.user);
        layout.add(this._userWidget.actor);

        this._timedLoginIndicator = new St.Bin({ style_class: 'login-dialog-timed-login-indicator',
                                                 scale_x: 0 });
        layout.add(this._timedLoginIndicator);

        this.actor.connect('clicked', Lang.bind(this, this._onClicked));
        this._onUserChanged();
    },

    _onUserChanged: function() {
        this._updateLoggedIn();
    },

    _updateLoggedIn: function() {
        if (this.user.is_logged_in())
            this.actor.add_style_pseudo_class('logged-in');
        else
            this.actor.remove_style_pseudo_class('logged-in');
    },

    _onDestroy: function() {
        this._user.disconnect(this._userChangedId);
    },

    _onClicked: function() {
        this.emit('activate');
    },

    showTimedLoginIndicator: function(time) {
        let hold = new Batch.Hold();

        this.hideTimedLoginIndicator();
        Tweener.addTween(this._timedLoginIndicator,
                         { scale_x: 1.,
                           time: time,
                           transition: 'linear',
                           onComplete: function() {
                               hold.release();
                           },
                           onCompleteScope: this
                         });
        return hold;
    },

    hideTimedLoginIndicator: function() {
        Tweener.removeTweens(this._timedLoginIndicator);
        this._timedLoginIndicator.scale_x = 0.;
    }
});
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',
                                       pseudo_class: 'expanded' });

        this.actor.add_actor(this._box);
        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;

        let focusSet = this.actor.navigate_focus(null, Gtk.DirectionType.TAB_FORWARD, false);
        if (!focusSet) {
            Meta.later_add(Meta.LaterType.BEFORE_REDRAW, Lang.bind(this, function() {
                this._moveFocusToItems();
                return false;
            }));
        }
    },

    _onItemActivated: function(activatedItem) {
        this.emit('activate', activatedItem);
    },

    updateStyle: function(isExpanded) {
        let tasks = [];

        if (isExpanded)
            this._box.add_style_pseudo_class('expanded');
        else
            this._box.remove_style_pseudo_class('expanded');

        for (let userName in this._items) {
            let item = this._items[userName];
            item.actor.sync_hover();
        }
    },

    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: 'easeOutQuad' });
    },

    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;

        if (user.locked)
           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);
                                     }));

        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 SessionMenuButton = new Lang.Class({
    Name: 'SessionMenuButton',

    _init: function() {
        let gearIcon = new St.Icon({ icon_name: 'emblem-system-symbolic' });
        this._button = new St.Button({ style_class: 'login-dialog-session-list-button',
                                       reactive: true,
                                       track_hover: true,
                                       can_focus: true,
                                       accessible_name: _("Choose Session"),
                                       accessible_role: Atk.Role.MENU,
                                       child: gearIcon });

        this.actor = new St.Bin({ child: this._button });

        this._menu = new PopupMenu.PopupMenu(this._button, 0, St.Side.TOP);
        Main.uiGroup.add_actor(this._menu.actor);
        this._menu.actor.hide();

        this._menu.connect('open-state-changed',
                           Lang.bind(this, function(menu, isOpen) {
                                if (isOpen)
                                    this._button.add_style_pseudo_class('active');
                                else
                                    this._button.remove_style_pseudo_class('active');
                           }));

        this._manager = new PopupMenu.PopupMenuManager({ actor: this._button });
        this._manager.addMenu(this._menu);

        this._button.connect('clicked', Lang.bind(this, function() {
            this._menu.toggle();
        }));

        this._items = {};
        this._activeSessionId = null;
        this._populate();
    },

    updateSensitivity: function(sensitive) {
        this._button.reactive = sensitive;
        this._button.can_focus = sensitive;
        this._menu.close(BoxPointer.PopupAnimation.NONE);
    },

    _updateOrnament: function() {
        let itemIds = Object.keys(this._items);
        for (let i = 0; i < itemIds.length; i++) {
            if (itemIds[i] == this._activeSessionId)
                this._items[itemIds[i]].setOrnament(PopupMenu.Ornament.DOT);
            else
                this._items[itemIds[i]].setOrnament(PopupMenu.Ornament.NONE);
        }
    },

    setActiveSession: function(sessionId) {
         if (sessionId == this._activeSessionId)
             return;

         this._activeSessionId = sessionId;
         this._updateOrnament();

         this.emit('session-activated', this._activeSessionId);
    },

    close: function() {
        this._menu.close();
    },

    _populate: function() {
        let ids = Gdm.get_session_ids();
        ids.sort();

        if (ids.length <= 1) {
            this._button.hide();
            return;
        }

        for (let i = 0; i < ids.length; i++) {
            let [sessionName, sessionDescription] = Gdm.get_session_name_and_description(ids[i]);

            let id = ids[i];
            let item = new PopupMenu.PopupMenuItem(sessionName);
            this._menu.addMenuItem(item);
            this._items[id] = item;

            if (!this._activeSessionId)
                this.setActiveSession(id);

            item.connect('activate', Lang.bind(this, function() {
                this.setActiveSession(id);
            }));
        }
    }
});
Signals.addSignalMethods(SessionMenuButton.prototype);

const LoginDialog = new Lang.Class({
    Name: 'LoginDialog',

    _init: function(parentActor) {
        this.actor = new Shell.GenericContainer({ style_class: 'login-dialog',
                                                  visible: false });
        this.actor.get_accessible().set_role(Atk.Role.WINDOW);

        this.actor.add_constraint(new Layout.MonitorConstraint({ primary: true }));
        this.actor.connect('allocate', Lang.bind(this, this._onAllocate));
        this.actor.connect('destroy', Lang.bind(this, this._onDestroy));
        parentActor.add_child(this.actor);

        this._userManager = AccountsService.UserManager.get_default()
        let gdmClient = new Gdm.Client();

        if (GLib.getenv('GDM_GREETER_TEST') != '1') {
            this._greeter = gdmClient.get_greeter_sync(null);

            this._defaultSessionChangedId = this._greeter.connect('default-session-name-changed',
                                                                  Lang.bind(this, this._onDefaultSessionChanged));
            this._sessionOpenedId = this._greeter.connect('session-opened',
                                                          Lang.bind(this, this._onSessionOpened));
            this._timedLoginRequestedId = this._greeter.connect('timed-login-requested',
                                                                Lang.bind(this, this._onTimedLoginRequested));
        }

        this._settings = new Gio.Settings({ schema_id: GdmUtil.LOGIN_SCREEN_SCHEMA });

        this._settings.connect('changed::' + GdmUtil.BANNER_MESSAGE_KEY,
                               Lang.bind(this, this._updateBanner));
        this._settings.connect('changed::' + GdmUtil.BANNER_MESSAGE_TEXT_KEY,
                               Lang.bind(this, this._updateBanner));
        this._settings.connect('changed::' + GdmUtil.DISABLE_USER_LIST_KEY,
                               Lang.bind(this, this._updateDisableUserList));
        this._settings.connect('changed::' + GdmUtil.LOGO_KEY,
                               Lang.bind(this, this._updateLogo));

        this._textureCache = St.TextureCache.get_default();
        this._updateLogoTextureId = this._textureCache.connect('texture-file-changed',
                                                               Lang.bind(this, this._updateLogoTexture));

        this._userSelectionBox = new St.BoxLayout({ style_class: 'login-dialog-user-selection-box',
                                                    x_align: Clutter.ActorAlign.CENTER,
                                                    y_align: Clutter.ActorAlign.CENTER,
                                                    vertical: true,
                                                    visible: false });
        this.actor.add_child(this._userSelectionBox);

        this._userList = new UserList();
        this._userSelectionBox.add(this._userList.actor,
                                   { expand: true,
                                     x_fill: true,
                                     y_fill: true });

        this._authPrompt = new AuthPrompt.AuthPrompt(gdmClient, AuthPrompt.AuthPromptMode.UNLOCK_OR_LOG_IN);
        this._authPrompt.connect('prompted', Lang.bind(this, this._onPrompted));
        this._authPrompt.connect('reset', Lang.bind(this, this._onReset));
        this._authPrompt.hide();
        this.actor.add_child(this._authPrompt.actor);

        // 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',
                                                button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE,
                                                can_focus: true,
                                                child: notListedLabel,
                                                reactive: true,
                                                x_align: St.Align.START,
                                                x_fill: true });

        this._notListedButton.connect('clicked', Lang.bind(this, this._hideUserListAskForUsernameAndBeginVerification));

        this._notListedButton.hide();

        this._userSelectionBox.add(this._notListedButton,
                                   { expand: false,
                                     x_align: St.Align.START,
                                     x_fill: true });

        this._bannerView = new St.ScrollView({ style_class: 'login-dialog-banner-view',
                                               opacity: 0,
                                               vscrollbar_policy: Gtk.PolicyType.AUTOMATIC,
                                               hscrollbar_policy: Gtk.PolicyType.NEVER });
        this.actor.add_child(this._bannerView);

        let bannerBox = new St.BoxLayout({ vertical: true });

        this._bannerView.add_actor(bannerBox);
        this._bannerLabel = new St.Label({ style_class: 'login-dialog-banner',
                                           text: '' });
        this._bannerLabel.clutter_text.line_wrap = true;
        this._bannerLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
        bannerBox.add_child(this._bannerLabel);
        this._updateBanner();

        this._logoBin = new St.Widget({ style_class: 'login-dialog-logo-bin',
                                        x_align: Clutter.ActorAlign.CENTER,
                                        y_align: Clutter.ActorAlign.END });
        this.actor.add_child(this._logoBin);
        this._updateLogo();

        this._userList.connect('activate',
                               Lang.bind(this, function(userList, item) {
                                   this._onUserListActivated(item);
                               }));


        this._sessionMenuButton = new SessionMenuButton();
        this._sessionMenuButton.connect('session-activated',
                                  Lang.bind(this, function(list, sessionId) {
                                                this._greeter.call_select_session_sync (sessionId, null);
                                            }));
        this._sessionMenuButton.actor.opacity = 0;
        this._sessionMenuButton.actor.show();
        this._authPrompt.addActorToDefaultButtonWell(this._sessionMenuButton.actor);

        this._disableUserList = undefined;
        this._userListLoaded = false;

        LoginManager.getLoginManager().getCurrentSessionProxy(Lang.bind(this, this._gotGreeterSessionProxy));

        // If the user list is enabled, it should take key focus; make sure the
        // screen shield is initialized first to prevent it from stealing the
        // focus later
        this._startupCompleteId = Main.layoutManager.connect('startup-complete',
                                                             Lang.bind(this, this._updateDisableUserList));
    },

    _getBannerAllocation: function (dialogBox) {
        let actorBox = new Clutter.ActorBox();

        let [minWidth, minHeight, natWidth, natHeight] = this._bannerView.get_preferred_size();
        let centerX = dialogBox.x1 + (dialogBox.x2 - dialogBox.x1) / 2;

        actorBox.x1 = centerX - natWidth / 2;
        actorBox.y1 = dialogBox.y1 + Main.layoutManager.panelBox.height;
        actorBox.x2 = actorBox.x1 + natWidth;
        actorBox.y2 = actorBox.y1 + natHeight;

        return actorBox;
    },

    _getLogoBinAllocation: function (dialogBox) {
        let actorBox = new Clutter.ActorBox();

        let [minWidth, minHeight, natWidth, natHeight] = this._logoBin.get_preferred_size();
        let centerX = dialogBox.x1 + (dialogBox.x2 - dialogBox.x1) / 2;

        actorBox.x1 = centerX - natWidth / 2;
        actorBox.y1 = dialogBox.y2 - natHeight;
        actorBox.x2 = actorBox.x1 + natWidth;
        actorBox.y2 = actorBox.y1 + natHeight;

        return actorBox;
    },

    _getCenterActorAllocation: function (dialogBox, actor) {
        let actorBox = new Clutter.ActorBox();

        let [minWidth, minHeight, natWidth, natHeight] = actor.get_preferred_size();
        let centerX = dialogBox.x1 + (dialogBox.x2 - dialogBox.x1) / 2;
        let centerY = dialogBox.y1 + (dialogBox.y2 - dialogBox.y1) / 2;

        actorBox.x1 = centerX - natWidth / 2;
        actorBox.y1 = centerY - natHeight / 2;
        actorBox.x2 = actorBox.x1 + natWidth;
        actorBox.y2 = actorBox.y1 + natHeight;

        return actorBox;
    },

    _onAllocate: function (actor, dialogBox, flags) {
        let dialogWidth = dialogBox.x2 - dialogBox.x1;
        let dialogHeight = dialogBox.y2 - dialogBox.y1;

        // First find out what space the children require
        let bannerAllocation = null;
        let bannerHeight = 0;
        let bannerWidth = 0;
        if (this._bannerView.visible) {
            bannerAllocation = this._getBannerAllocation(dialogBox, this._bannerView);
            bannerHeight = bannerAllocation.y2 - bannerAllocation.y1;
            bannerWidth = bannerAllocation.x2 - bannerAllocation.x1;
        }

        let authPromptAllocation = null;
        let authPromptHeight = 0;
        let authPromptWidth = 0;
        if (this._authPrompt.actor.visible) {
            authPromptAllocation = this._getCenterActorAllocation(dialogBox, this._authPrompt.actor);
            authPromptHeight = authPromptAllocation.y2 - authPromptAllocation.y1;
            authPromptWidth = authPromptAllocation.x2 - authPromptAllocation.x1;
        }

        let userSelectionAllocation = null;
        let userSelectionHeight = 0;
        if (this._userSelectionBox.visible) {
            userSelectionAllocation = this._getCenterActorAllocation(dialogBox, this._userSelectionBox);
            userSelectionHeight = userSelectionAllocation.y2 - userSelectionAllocation.y1;
        }

        let logoAllocation = null;
        let logoHeight = 0;
        if (this._logoBin.visible) {
            logoAllocation = this._getLogoBinAllocation(dialogBox);
            logoHeight = logoAllocation.y2 - logoAllocation.y1;
        }

        // Then figure out if we're overly constrained and need to
        // try a different layout, or if we have what extra space we
        // can hand out
        if (bannerAllocation) {
            let leftOverYSpace = dialogHeight - bannerHeight - authPromptHeight - logoHeight;

            if (leftOverYSpace > 0) {
                 // First figure out how much left over space is up top
                 let leftOverTopSpace = leftOverYSpace / 2;

                 // Then, shift the banner into the middle of that extra space
                 let yShift = leftOverTopSpace / 2;

                 bannerAllocation.y1 += yShift;
                 bannerAllocation.y2 += yShift;
            } else {
                 // Then figure out how much space there would be if we switched to a
                 // wide layout with banner on one side and authprompt on the other.
                 let leftOverXSpace = dialogWidth - authPromptWidth;

                 // In a wide view, half of the available space goes to the banner,
                 // and the other half goes to the margins.
                 let wideBannerWidth = leftOverXSpace / 2;
                 let wideSpacing  = leftOverXSpace - wideBannerWidth;

                 // If we do go with a wide layout, we need there to be at least enough
                 // space for the banner and the auth prompt to be the same width,
                 // so it doesn't look unbalanced.
                 if (authPromptWidth > 0 && wideBannerWidth > authPromptWidth) {
                     let centerX = dialogBox.x1 + dialogWidth / 2;
                     let centerY = dialogBox.y1 + dialogHeight / 2;

                     // A small portion of the spacing goes down the center of the
                     // screen to help delimit the two columns of the wide view
                     let centerGap = wideSpacing / 8;

                     // place the banner along the left edge of the center margin
                     bannerAllocation.x2 = centerX - centerGap / 2;
                     bannerAllocation.x1 = bannerAllocation.x2 - wideBannerWidth;

                     // figure out how tall it would like to be and try to accomodate
                     // but don't let it get too close to the logo
                     let [wideMinHeight, wideBannerHeight] = this._bannerView.get_preferred_height(wideBannerWidth);

                     let maxWideHeight = dialogHeight - 3 * logoHeight;
                     wideBannerHeight = Math.min(maxWideHeight, wideBannerHeight);
                     bannerAllocation.y1 = centerY - wideBannerHeight / 2;
                     bannerAllocation.y2 = bannerAllocation.y1 + wideBannerHeight;

                     // place the auth prompt along the right edge of the center margin
                     authPromptAllocation.x1 = centerX + centerGap / 2;
                     authPromptAllocation.x2 = authPromptAllocation.x1 + authPromptWidth;
                 } else {
                     // If we aren't going to do a wide view, then we need to limit
                     // the height of the banner so it will present scrollbars

                     // First figure out how much space there is without the banner
                     leftOverYSpace += bannerHeight;

                     // Then figure out how much of that space is up top
                     let availableTopSpace = leftOverYSpace / 2;

                     // Then give all of that space to the banner
                     bannerAllocation.y2 = bannerAllocation.y1 + availableTopSpace;
                 }
            }
        } else if (userSelectionAllocation) {
            // Grow the user list to fill the space
            let leftOverYSpace = dialogHeight - userSelectionHeight - logoHeight;

            if (leftOverYSpace > 0) {
                let topExpansion = leftOverYSpace / 2;
                let bottomExpansion = topExpansion;

                userSelectionAllocation.y1 -= topExpansion;
                userSelectionAllocation.y2 += bottomExpansion;
            }
        }

        // Finally hand out the allocations
        if (bannerAllocation) {
            this._bannerView.allocate(bannerAllocation, flags);
        }

        if (authPromptAllocation)
            this._authPrompt.actor.allocate(authPromptAllocation, flags);

        if (userSelectionAllocation)
            this._userSelectionBox.allocate(userSelectionAllocation, flags);

        if (logoAllocation)
            this._logoBin.allocate(logoAllocation, flags);
    },

    _ensureUserListLoaded: function() {
        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 {
            let id = GLib.idle_add(GLib.PRIORITY_DEFAULT, Lang.bind(this, this._loadUserList));
            GLib.Source.set_name_by_id(id, '[gnome-shell] _loadUserList');
        }
    },

    _updateDisableUserList: function() {
        let disableUserList = this._settings.get_boolean(GdmUtil.DISABLE_USER_LIST_KEY);

        if (disableUserList != this._disableUserList) {
            this._disableUserList = disableUserList;

            if (this._authPrompt.verificationStatus == AuthPrompt.AuthPromptStatus.NOT_VERIFYING)
                this._authPrompt.reset();
        }
    },

    _updateCancelButton: function() {
        let cancelVisible;

        // Hide the cancel button if the user list is disabled and we're asking for
        // a username
        if (this._authPrompt.verificationStatus == AuthPrompt.AuthPromptStatus.NOT_VERIFYING && this._disableUserList)
            cancelVisible = false;
        else
            cancelVisible = true;

        this._authPrompt.cancelButton.visible = cancelVisible;
    },

    _updateBanner: function() {
        let enabled = this._settings.get_boolean(GdmUtil.BANNER_MESSAGE_KEY);
        let text = this._settings.get_string(GdmUtil.BANNER_MESSAGE_TEXT_KEY);

        if (enabled && text) {
            this._bannerLabel.set_text(text);
            this._bannerLabel.show();
        } else {
            this._bannerLabel.hide();
        }
    },

    _fadeInBannerView: function() {
        Tweener.addTween(this._bannerView,
                         { opacity: 255,
                           time: _FADE_ANIMATION_TIME,
                           transition: 'easeOutQuad' });
    },

    _hideBannerView: function() {
        Tweener.removeTweens(this._bannerView);
        this._bannerView.opacity = 0;
    },

    _updateLogoTexture: function(cache, file) {
        if (this._logoFile && !this._logoFile.equal(file))
            return;

        this._logoBin.destroy_all_children();
        if (this._logoFile) {
            let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
            this._logoBin.add_child(this._textureCache.load_file_async(this._logoFile,
                                                                       -1, _LOGO_ICON_HEIGHT,
                                                                       scaleFactor));
        }
    },

    _updateLogo: function() {
        let path = this._settings.get_string(GdmUtil.LOGO_KEY);

        this._logoFile = path ? Gio.file_new_for_path(path) : null;
        this._updateLogoTexture(this._textureCache, this._logoFile);
    },

    _onPrompted: function() {
        this._sessionMenuButton.updateSensitivity(true);

        if (this._shouldShowSessionMenuButton())
            this._authPrompt.setActorInDefaultButtonWell(this._sessionMenuButton.actor);
        this._showPrompt();
    },

    _onReset: function(authPrompt, beginRequest) {
        this._sessionMenuButton.updateSensitivity(true);

        this._user = null;

        if (beginRequest == AuthPrompt.BeginRequestType.PROVIDE_USERNAME) {
            if (!this._disableUserList)
                this._showUserList();
            else
                this._hideUserListAskForUsernameAndBeginVerification();
        } else {
            this._hideUserListAndBeginVerification();
        }
    },

    _onDefaultSessionChanged: function(client, sessionId) {
        this._sessionMenuButton.setActiveSession(sessionId);
    },

    _shouldShowSessionMenuButton: function() {
        if (this._authPrompt.verificationStatus != AuthPrompt.AuthPromptStatus.VERIFYING &&
            this._authPrompt.verificationStatus != AuthPrompt.AuthPromptStatus.VERIFICATION_FAILED)
          return false;

        if (this._user && this._user.is_loaded && this._user.is_logged_in())
          return false;

        return true;
    },

    _showPrompt: function() {
        if (this._authPrompt.actor.visible)
            return;
        this._authPrompt.actor.opacity = 0;
        this._authPrompt.actor.show();
        Tweener.addTween(this._authPrompt.actor,
                         { opacity: 255,
                           time: _FADE_ANIMATION_TIME,
                           transition: 'easeOutQuad' });
        this._fadeInBannerView();
    },

    _showRealmLoginHint: function(realmManager, hint) {
        if (!hint)
            return;

        hint = hint.replace(/%U/g, 'user');
        hint = hint.replace(/%D/g, 'DOMAIN');
        hint = hint.replace(/%[^UD]/g, '');

        // Translators: this message is shown below the username entry field
        // to clue the user in on how to login to the local network realm
        this._authPrompt.setMessage(_("(e.g., user or %s)").format(hint), GdmUtil.MessageType.HINT);
    },

    _askForUsernameAndBeginVerification: function() {
        this._authPrompt.setPasswordChar('');
        this._authPrompt.setQuestion(_("Username: "));

        let realmManager = new Realmd.Manager();
        let realmSignalId = realmManager.connect('login-format-changed',
                                                 Lang.bind(this, this._showRealmLoginHint));
        this._showRealmLoginHint(realmManager.loginFormat);

        let nextSignalId = this._authPrompt.connect('next',
                                                    Lang.bind(this, function() {
                                                        this._authPrompt.disconnect(nextSignalId);
                                                        this._authPrompt.updateSensitivity(false);
                                                        let answer = this._authPrompt.getAnswer();
                                                        this._user = this._userManager.get_user(answer);
                                                        this._authPrompt.clear();
                                                        this._authPrompt.startSpinning();
                                                        this._authPrompt.begin({ userName: answer });
                                                        this._updateCancelButton();

                                                        realmManager.disconnect(realmSignalId)
                                                        realmManager.release();
                                                    }));
        this._updateCancelButton();

        this._authPrompt.updateSensitivity(true);
        this._showPrompt();
    },

    _loginScreenSessionActivated: function() {
        if (this._authPrompt.verificationStatus != AuthPrompt.AuthPromptStatus.VERIFICATION_SUCCEEDED)
            return;

        Tweener.addTween(this.actor,
                         { opacity: 255,
                           time: _FADE_ANIMATION_TIME,
                           transition: 'easeOutQuad',
                           onUpdate: function() {
                               let children = Main.layoutManager.uiGroup.get_children();

                               for (let i = 0; i < children.length; i++) {
                                   if (children[i] != Main.layoutManager.screenShieldGroup)
                                       children[i].opacity = this.actor.opacity;
                               }
                           },
                           onUpdateScope: this,
                           onComplete: function() {
                               this._authPrompt.reset();
                           },
                           onCompleteScope: this });
    },

    _gotGreeterSessionProxy: function(proxy) {
        this._greeterSessionProxy = proxy;
        this._greeterSessionProxyChangedId =
            proxy.connect('g-properties-changed', Lang.bind(this, function() {
                if (proxy.Active)
                    this._loginScreenSessionActivated();
            }));
    },

    _startSession: function(serviceName) {
        Tweener.addTween(this.actor,
                         { opacity: 0,
                           time: _FADE_ANIMATION_TIME,
                           transition: 'easeOutQuad',
                           onUpdate: function() {
                               let children = Main.layoutManager.uiGroup.get_children();

                               for (let i = 0; i < children.length; i++) {
                                   if (children[i] != Main.layoutManager.screenShieldGroup)
                                       children[i].opacity = this.actor.opacity;
                               }
                           },
                           onUpdateScope: this,
                           onComplete: function() {
                               let id = Mainloop.idle_add(Lang.bind(this, function() {
                                   this._greeter.call_start_session_when_ready_sync(serviceName, true, null);
                                   return GLib.SOURCE_REMOVE;
                               }));
                               GLib.Source.set_name_by_id(id, '[gnome-shell] this._greeter.call_start_session_when_ready_sync');
                           },
                           onCompleteScope: this });
    },

    _onSessionOpened: function(client, serviceName) {
        this._authPrompt.finish(Lang.bind(this, function() {
            this._startSession(serviceName);
        }));
    },

    _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.showTimedLoginIndicator(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 GLib.SOURCE_REMOVE;
                                                                     });
        GLib.Source.set_name_by_id(this._timedLoginIdleTimeOutId, '[gnome-shell] this._timedLoginAnimationTime');
        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._userManager.is_loaded) {
                             this._userList.jumpToItem(this._timedLoginItem);
                         }
                     },

                     this._blockTimedLoginUntilIdle,

                     function() {
                         this._userList.scrollToItem(this._timedLoginItem);
                     },

                     this._showTimedLoginAnimation,

                     function() {
                         this._timedLoginBatch = null;
                         this._greeter.call_begin_auto_login_sync(userName, null);
                     }];

        this._timedLoginBatch = new Batch.ConsecutiveBatch(this, tasks);

        return this._timedLoginBatch.run();
    },

    _resetTimedLogin: function() {
        if (this._timedLoginBatch) {
            this._timedLoginBatch.cancel();
            this._timedLoginBatch = null;
        }

        if (this._timedLoginItem)
            this._timedLoginItem.hideTimedLoginIndicator();

        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 Clutter.EVENT_PROPAGATE;

                                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 Clutter.EVENT_PROPAGATE;
                             }));
    },

    _setUserListExpanded: function(expanded) {
        this._userList.updateStyle(expanded);
        this._userSelectionBox.visible = expanded;
    },

    _hideUserList: function() {
        this._setUserListExpanded(false);
        if (this._userSelectionBox.visible)
            GdmUtil.cloneAndFadeOutActor(this._userSelectionBox);
    },

    _hideUserListAskForUsernameAndBeginVerification: function() {
        this._hideUserList();
        this._askForUsernameAndBeginVerification();
    },

    _hideUserListAndBeginVerification: function() {
        this._hideUserList();
        this._authPrompt.begin();
    },

    _showUserList: function() {
        this._ensureUserListLoaded();
        this._authPrompt.hide();
        this._hideBannerView();
        this._sessionMenuButton.close();
        this._setUserListExpanded(true);
        this._notListedButton.show();
        this._userList.actor.grab_key_focus();
    },

    _beginVerificationForItem: function(item) {
        this._authPrompt.setUser(item.user);

        let userName = item.user.get_user_name();
        let hold = new Batch.Hold();

        this._authPrompt.begin({ userName: userName,
                                 hold: hold });
        return hold;
    },

    _onUserListActivated: function(activatedItem) {
        let tasks = [function() {
                         return GdmUtil.cloneAndFadeOutActor(this._userSelectionBox);
                     },
                     function() {
                         this._setUserListExpanded(false);
                     }];

        this._user = activatedItem.user;

        this._updateCancelButton();

        let batch = new Batch.ConcurrentBatch(this, [new Batch.ConsecutiveBatch(this, tasks),
                                                     this._beginVerificationForItem(activatedItem)]);
        batch.run();
    },

    _onDestroy: function() {
        if (this._userManagerLoadedId) {
            this._userManager.disconnect(this._userManagerLoadedId);
            this._userManagerLoadedId = 0;
        }
        if (this._userAddedId) {
            this._userManager.disconnect(this._userAddedId);
            this._userAddedId = 0;
        }
        if (this._userRemovedId) {
            this._userManager.disconnect(this._userRemovedId);
            this._userRemovedId = 0;
        }
        this._textureCache.disconnect(this._updateLogoTextureId);
        Main.layoutManager.disconnect(this._startupCompleteId);
        if (this._settings) {
            this._settings.run_dispose();
            this._settings = null;
        }
        if (this._greeter) {
            this._greeter.disconnect(this._defaultSessionChangedId);
            this._greeter.disconnect(this._sessionOpenedId);
            this._greeter.disconnect(this._timedLoginRequestedId);
            this._greeter = null;
        }
        if (this._greeterSessionProxy) {
            this._greeterSessionProxy.disconnect(this._greeterSessionProxyChangedId);
            this._greeterSessionProxy = null;
        }
    },

    _loadUserList: function() {
        if (this._userListLoaded)
            return GLib.SOURCE_REMOVE;

        this._userListLoaded = true;

        let users = this._userManager.list_users();

        for (let i = 0; i < users.length; i++) {
            this._userList.addUser(users[i]);
        }

        this._userAddedId = this._userManager.connect('user-added',
                                                      Lang.bind(this, function(userManager, user) {
                                                          this._userList.addUser(user);
                                                      }));

        this._userRemovedId = this._userManager.connect('user-removed',
                                                        Lang.bind(this, function(userManager, user) {
                                                            this._userList.removeUser(user);
                                                        }));

        return GLib.SOURCE_REMOVE;
    },

    open: function() {
        Main.ctrlAltTabManager.addGroup(this.actor,
                                        _("Login Window"),
                                        'dialog-password-symbolic',
                                        { sortGroup: CtrlAltTab.SortGroup.MIDDLE });
        this._userList.actor.grab_key_focus();
        this.actor.show();
        this.actor.opacity = 0;

        Main.pushModal(this.actor, { actionMode: Shell.ActionMode.LOGIN_SCREEN });

        Tweener.addTween(this.actor,
                         { opacity: 255,
                           time: 1,
                           transition: 'easeInQuad' });

        return true;
    },

    close: function() {
        Main.popModal(this.actor);
        Main.ctrlAltTabManager.removeGroup(this.actor);
    },

    cancel: function() {
        this._authPrompt.cancel();
    },

    addCharacter: function(unichar) {
        this._authPrompt.addCharacter(unichar);
    },

    finish: function(onComplete) {
        this._authPrompt.finish(onComplete);
    },
});
Signals.addSignalMethods(LoginDialog.prototype);
