Blob Blame History Raw
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

const { AccountsService, Clutter, GLib, GObject, Shell, St } = imports.gi;

const BoxPointer = imports.ui.boxpointer;
const SystemActions = imports.misc.systemActions;
const Main = imports.ui.main;
const PanelMenu = imports.ui.panelMenu;
const PopupMenu = imports.ui.popupMenu;


var AltSwitcher = class {
    constructor(standard, alternate) {
        this._standard = standard;
        this._standard.connect('notify::visible', this._sync.bind(this));
        if (this._standard instanceof St.Button)
            this._standard.connect('clicked',
                                   () => { this._clickAction.release(); });

        this._alternate = alternate;
        this._alternate.connect('notify::visible', this._sync.bind(this));
        if (this._alternate instanceof St.Button)
            this._alternate.connect('clicked',
                                    () => { this._clickAction.release(); });

        this._capturedEventId = global.stage.connect('captured-event', this._onCapturedEvent.bind(this));

        this._flipped = false;

        this._clickAction = new Clutter.ClickAction();
        this._clickAction.connect('long-press', this._onLongPress.bind(this));

        this.actor = new St.Bin();
        this.actor.connect('destroy', this._onDestroy.bind(this));
        this.actor.connect('notify::mapped', () => { this._flipped = false; });
    }

    _sync() {
        let childToShow = null;

        if (this._standard.visible && this._alternate.visible) {
            let [x, y, mods] = global.get_pointer();
            let altPressed = (mods & Clutter.ModifierType.MOD1_MASK) != 0;
            if (this._flipped)
                childToShow = altPressed ? this._standard : this._alternate;
            else
                childToShow = altPressed ? this._alternate : this._standard;
        } else if (this._standard.visible) {
            childToShow = this._standard;
        } else if (this._alternate.visible) {
            childToShow = this._alternate;
        } else {
            this.actor.hide();
            return;
        }

        let childShown = this.actor.get_child();
        if (childShown != childToShow) {
            if (childShown) {
                if (childShown.fake_release)
                    childShown.fake_release();
                childShown.remove_action(this._clickAction);
            }
            childToShow.add_action(this._clickAction);

            let hasFocus = this.actor.contains(global.stage.get_key_focus());
            this.actor.set_child(childToShow);
            if (hasFocus)
                childToShow.grab_key_focus();

            // The actors might respond to hover, so
            // sync the pointer to make sure they update.
            global.sync_pointer();
        }

        this.actor.show();
    }

    _onDestroy() {
        if (this._capturedEventId > 0) {
            global.stage.disconnect(this._capturedEventId);
            this._capturedEventId = 0;
        }
    }

    _onCapturedEvent(actor, event) {
        let type = event.type();
        if (type == Clutter.EventType.KEY_PRESS || type == Clutter.EventType.KEY_RELEASE) {
            let key = event.get_key_symbol();
            if (key == Clutter.KEY_Alt_L || key == Clutter.KEY_Alt_R)
                this._sync();
        }

        return Clutter.EVENT_PROPAGATE;
    }

    _onLongPress(action, actor, state) {
        if (state == Clutter.LongPressState.QUERY ||
            state == Clutter.LongPressState.CANCEL)
            return true;

        this._flipped = !this._flipped;
        this._sync();
        return true;
    }
};

var Indicator = class extends PanelMenu.SystemIndicator {
    constructor() {
        super();

        let userManager = AccountsService.UserManager.get_default();
        this._user = userManager.get_user(GLib.get_user_name());

        this._systemActions = new SystemActions.getDefault();

        this._createSubMenu();

        this._loginScreenItem.actor.connect('notify::visible',
                                            () => { this._updateMultiUser(); });
        this._logoutItem.actor.connect('notify::visible',
                                       () => { this._updateMultiUser(); });
        // Whether shutdown is available or not depends on both lockdown
        // settings (disable-log-out) and Polkit policy - the latter doesn't
        // notify, so we update the menu item each time the menu opens or
        // the lockdown setting changes, which should be close enough.
        this.menu.connect('open-state-changed', (menu, open) => {
            if (!open)
                return;

            this._systemActions.forceUpdate();
        });
        this._updateMultiUser();

        Main.sessionMode.connect('updated', this._sessionUpdated.bind(this));
        this._sessionUpdated();
    }

    _updateActionsVisibility() {
        let visible = (this._settingsAction.visible ||
                       this._orientationLockAction.visible ||
                       this._lockScreenAction.visible ||
                       this._altSwitcher.actor.visible);

        this._actionsItem.actor.visible = visible;
    }

    _sessionUpdated() {
        this._settingsAction.visible = Main.sessionMode.allowSettings;
    }

    _updateMultiUser() {
        let hasSwitchUser = this._loginScreenItem.actor.visible;
        let hasLogout = this._logoutItem.actor.visible;

        this._switchUserSubMenu.actor.visible = hasSwitchUser || hasLogout;
    }

    _updateSwitchUserSubMenu() {
        this._switchUserSubMenu.label.text = this._user.get_real_name();
        let clutterText = this._switchUserSubMenu.label.clutter_text;

        // XXX -- for some reason, the ClutterText's width changes
        // rapidly unless we force a relayout of the actor. Probably
        // a size cache issue or something. Moving this to be a layout
        // manager would be a much better idea.
        clutterText.get_allocation_box();

        let layout = clutterText.get_layout();
        if (layout.is_ellipsized())
            this._switchUserSubMenu.label.text = this._user.get_user_name();
    }

    _createActionButton(iconName, accessibleName) {
        let icon = new St.Button({ reactive: true,
                                   can_focus: true,
                                   track_hover: true,
                                   accessible_name: accessibleName,
                                   style_class: 'system-menu-action' });
        icon.child = new St.Icon({ icon_name: iconName });
        return icon;
    }

    _createSubMenu() {
        let bindFlags = GObject.BindingFlags.DEFAULT | GObject.BindingFlags.SYNC_CREATE;
        let item;

        this._switchUserSubMenu = new PopupMenu.PopupSubMenuMenuItem('', true);
        this._switchUserSubMenu.icon.set({
            icon_name: 'avatar-default-symbolic',
            style_class: 'system-switch-user-submenu-icon'
        });

        // Since the label of the switch user submenu depends on the width of
        // the popup menu, and we can't easily connect on allocation-changed
        // or notify::width without creating layout cycles, simply update the
        // label whenever the menu is opened.
        this.menu.connect('open-state-changed', (menu, isOpen) => {
            if (isOpen)
                this._updateSwitchUserSubMenu();
        });

        item = new PopupMenu.PopupMenuItem(_("Switch User"));
        item.connect('activate', () => {
            this.menu.itemActivated(BoxPointer.PopupAnimation.NONE);
            this._systemActions.activateSwitchUser();
        });
        this._switchUserSubMenu.menu.addMenuItem(item);
        this._loginScreenItem = item;
        this._systemActions.bind_property('can-switch-user',
                                          this._loginScreenItem.actor,
                                          'visible',
                                          bindFlags);

        item = new PopupMenu.PopupMenuItem(_("Log Out"));
        item.connect('activate', () => {
            this.menu.itemActivated(BoxPointer.PopupAnimation.NONE);
            this._systemActions.activateLogout();
        });
        this._switchUserSubMenu.menu.addMenuItem(item);
        this._logoutItem = item;
        this._systemActions.bind_property('can-logout',
                                          this._logoutItem.actor,
                                          'visible',
                                          bindFlags);

        this._switchUserSubMenu.menu.addSettingsAction(_("Account Settings"),
                                                       'gnome-user-accounts-panel.desktop');

        this._user.connect('notify::is-loaded', this._updateSwitchUserSubMenu.bind(this));
        this._user.connect('changed', this._updateSwitchUserSubMenu.bind(this));

        this.menu.addMenuItem(this._switchUserSubMenu);

        this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());

        item = new PopupMenu.PopupBaseMenuItem({ reactive: false,
                                                 can_focus: false });
        this.buttonGroup = item.actor;

        let app = this._settingsApp = Shell.AppSystem.get_default().lookup_app(
            'gnome-control-center.desktop'
        );
        if (app) {
            let [icon, name] = [app.app_info.get_icon().names[0],
                                app.get_name()];
            this._settingsAction = this._createActionButton(icon, name);
            this._settingsAction.connect('clicked',
                                         this._onSettingsClicked.bind(this));
        } else {
            log('Missing required core component Settings, expect troubleā€¦');
            this._settingsAction = new St.Widget();
        }
        item.actor.add(this._settingsAction, { expand: true, x_fill: false });

        this._orientationLockAction = this._createActionButton('', _("Orientation Lock"));
        this._orientationLockAction.connect('clicked', () => {
            this.menu.itemActivated(BoxPointer.PopupAnimation.NONE),
            this._systemActions.activateLockOrientation();
        });
        item.actor.add(this._orientationLockAction, { expand: true, x_fill: false });
        this._systemActions.bind_property('can-lock-orientation',
                                          this._orientationLockAction,
                                          'visible',
                                          bindFlags);
        this._systemActions.bind_property('orientation-lock-icon',
                                          this._orientationLockAction.child,
                                          'icon-name',
                                          bindFlags);

        this._lockScreenAction = this._createActionButton('changes-prevent', _("Lock"));
        this._lockScreenAction.connect('clicked', () => {
            this.menu.itemActivated(BoxPointer.PopupAnimation.NONE);
            this._systemActions.activateLockScreen();
        });
        item.actor.add(this._lockScreenAction, { expand: true, x_fill: false });
        this._systemActions.bind_property('can-lock-screen',
                                          this._lockScreenAction,
                                          'visible',
                                          bindFlags);

        this._suspendAction = this._createActionButton('media-playback-pause', _("Suspend"));
        this._suspendAction.connect('clicked', () => {
            this.menu.itemActivated(BoxPointer.PopupAnimation.NONE);
            this._systemActions.activateSuspend();
        });
        this._systemActions.bind_property('can-suspend',
                                          this._suspendAction,
                                          'visible',
                                          bindFlags);

        this._powerOffAction = this._createActionButton('system-shutdown', _("Power Off"));
        this._powerOffAction.connect('clicked', () => {
            this.menu.itemActivated(BoxPointer.PopupAnimation.NONE);
            this._systemActions.activatePowerOff();
        });
        this._systemActions.bind_property('can-power-off',
                                          this._powerOffAction,
                                          'visible',
                                          bindFlags);

        this._altSwitcher = new AltSwitcher(this._powerOffAction, this._suspendAction);
        item.actor.add(this._altSwitcher.actor, { expand: true, x_fill: false });

        this._actionsItem = item;
        this.menu.addMenuItem(item);


        this._settingsAction.connect('notify::visible',
                                     () => { this._updateActionsVisibility(); });
        this._orientationLockAction.connect('notify::visible',
                                            () => { this._updateActionsVisibility(); });
        this._lockScreenAction.connect('notify::visible',
                                       () => { this._updateActionsVisibility(); });
        this._altSwitcher.actor.connect('notify::visible',
                                        () => { this._updateActionsVisibility(); });
    }

    _onSettingsClicked() {
        this.menu.itemActivated();
        Main.overview.hide();
        this._settingsApp.activate();
    }
};