[gnome-shell/T27795: 41/138] userMenu: Implement the new user menu, based on our design specs



commit 79da0ab60319299e8123620a71a6792e61ccdcc7
Author: Mario Sanchez Prada <mario endlessm com>
Date:   Wed Feb 14 11:15:02 2018 +0000

    userMenu: Implement the new user menu, based on our design specs
    
    This is a fresh implementation from scratch based on the Endless
    design specs from T20327, T20485 and T20800, which includes all
    the additions incorporated since EOS 3.2 squashed together.
    
    https://phabricator.endlessm.com/T20327
    https://phabricator.endlessm.com/T20485
    https://phabricator.endlessm.com/T20800

 data/gnome-shell-theme.gresource.xml      |   2 +
 data/theme/endless-help-symbolic.svg      |   1 +
 data/theme/gnome-shell-sass/_endless.scss |  42 ++++++++
 data/theme/system-logout.png              | Bin 0 -> 18189 bytes
 js/js-resources.gresource.xml             |   1 +
 js/ui/panel.js                            |  47 ++++++---
 js/ui/popupMenu.js                        |   4 +-
 js/ui/sessionMode.js                      |   3 +-
 js/ui/status/system.js                    |  66 ++++++------
 js/ui/userMenu.js                         | 170 ++++++++++++++++++++++++++++++
 po/POTFILES.in                            |   1 +
 11 files changed, 286 insertions(+), 51 deletions(-)
---
diff --git a/data/gnome-shell-theme.gresource.xml b/data/gnome-shell-theme.gresource.xml
index c90a81a467..583e700788 100644
--- a/data/gnome-shell-theme.gresource.xml
+++ b/data/gnome-shell-theme.gresource.xml
@@ -39,8 +39,10 @@
     <file>corner-ripple-tl.png</file>
     <file>corner-ripple-tr.png</file>
     <file>endless-button-symbolic.svg</file>
+    <file>endless-help-symbolic.svg</file>
     <file>hot-corner-symbolic.svg</file>
     <file>hot-corner-rtl-symbolic.svg</file>
     <file>mini-icon-active-indicator.png</file>
+    <file>system-logout.png</file>
   </gresource>
 </gresources>
diff --git a/data/theme/endless-help-symbolic.svg b/data/theme/endless-help-symbolic.svg
new file mode 100755
index 0000000000..cf88058f39
--- /dev/null
+++ b/data/theme/endless-help-symbolic.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg"; viewBox="0 0 16 16"><title>Asset 4</title><rect width="16" 
height="16" style="opacity:0.020000020042061806;isolation:isolate"/><path 
d="M12,1.529H4a2.889,2.889,0,0,0-3,2.88v5.24a2.889,2.889,0,0,0,3,2.88H5.475l2.463,2.463,2.587-2.463H12a2.889,2.889,0,0,0,3-2.88V4.409A2.889,2.889,0,0,0,12,1.529ZM7.949,3.417c1.619,0,2.429.97,2.429,1.969a2.138,2.138,0,0,1-.97,1.8l-.38.279a1.154,1.154,0,0,0-.589.97H7.249A2.069,2.069,0,0,1,7.209,8a1.721,1.721,0,0,1,.76-1.409l.51-.38a.82.82,0,0,0,.359-.7.792.792,0,0,0-.889-.77.914.914,0,0,0-.919.939c0,.069,0,.126,0,.173L5.629,5.782c-.006-.071-.009-.141-.009-.206A2.159,2.159,0,0,1,7.949,3.417Zm.8,6.477a.865.865,0,1,1-.87-.869A.868.868,0,0,1,8.748,9.894Z"
 style="fill:#bebebe"/></svg>
\ No newline at end of file
diff --git a/data/theme/gnome-shell-sass/_endless.scss b/data/theme/gnome-shell-sass/_endless.scss
index 64a7ff1571..38f5b969ef 100644
--- a/data/theme/gnome-shell-sass/_endless.scss
+++ b/data/theme/gnome-shell-sass/_endless.scss
@@ -92,6 +92,11 @@
     -arrow-background-color: #000000;
 }
 
+popup-separator-menu-item {
+  margin: 12px 0;
+  padding: 0;
+}
+
 // Apps Icon Bar
 
 #appIconBar {
@@ -212,6 +217,43 @@
     }
 }
 
+// User Menu
+
+.user-menu-button-icon {
+    border: 1px solid rgba(255, 255, 255, 0.2);
+    border-radius: 17px;
+    background-size: 34px;
+    padding: 0px;
+
+    &:hover {
+        border-radius: 18px;
+        background-size: 36px;
+    }
+}
+
+.user-menu-avatar {
+    width: 60px;
+    height: 60px;
+    background-size: contain;
+    background-color: rgba(50, 50, 50, 0.80);
+    border-radius: 30px;
+    border: 1px solid #000;
+
+    &:hover {
+        border: 1px solid #555;
+    }
+}
+
+.user-menu-name {
+    text-align: center;
+    font-weight: bold;
+    color: white;
+}
+
+.user-menu-items {
+    margin: 6px 0px;
+}
+
 // Endless Button
 
 %endless-button-hover {
diff --git a/data/theme/system-logout.png b/data/theme/system-logout.png
new file mode 100644
index 0000000000..e6de3e64ed
Binary files /dev/null and b/data/theme/system-logout.png differ
diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml
index 215de58dcc..48cb070d01 100644
--- a/js/js-resources.gresource.xml
+++ b/js/js-resources.gresource.xml
@@ -149,5 +149,6 @@
     <file>ui/monitor.js</file>
     <file>ui/sideComponent.js</file>
     <file>ui/status/orientation.js</file>
+    <file>ui/userMenu.js</file>
   </gresource>
 </gresources>
diff --git a/js/ui/panel.js b/js/ui/panel.js
index de65a134a9..aebb06056c 100644
--- a/js/ui/panel.js
+++ b/js/ui/panel.js
@@ -21,9 +21,6 @@ var APP_MENU_ICON_MARGIN = 0;
 
 var BUTTON_DND_ACTIVATION_TIMEOUT = 250;
 
-const SETTINGS_TEXT = _("All Settingsā€¦");
-const CONTROL_CENTER_LAUNCHER = "gnome-control-center.desktop";
-
 // To make sure the panel corners blend nicely with the panel,
 // we draw background and borders the same way, e.g. drawing
 // them as filled shapes from the outside inwards instead of
@@ -759,17 +756,6 @@ class AggregateMenu extends PanelMenu.Button {
         if (!userMode)
             this._indicators.add_child(PopupMenu.arrowIcon(St.Side.BOTTOM));
 
-        let gicon = new Gio.ThemedIcon({ name: 'applications-system-symbolic' });
-        this.menu.addAction(SETTINGS_TEXT, () => {
-            this.menu.close(BoxPointer.PopupAnimation.NONE);
-            Main.overview.hide();
-
-            let app = Shell.AppSystem.get_default().lookup_app(CONTROL_CENTER_LAUNCHER);
-            let context = new AppActivation.AppActivationContext(app);
-            context.activate();
-        }, gicon);
-
-        this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
         if (this._network) {
             this.menu.addMenuItem(this._network.menu);
         }
@@ -795,6 +781,38 @@ class AggregateMenu extends PanelMenu.Button {
     }
 });
 
+var UserMenu = GObject.registerClass(
+class UserMenu extends PanelMenu.Button {
+    _init() {
+        super._init(0.0, C_("User menu", "User Menu"), false);
+
+        this.accessible_role = Atk.Role.MENU;
+
+        let menuLayout = new AggregateLayout();
+        this.menu.box.set_layout_manager(menuLayout);
+        this.menu.actor.add_style_class_name('aggregate-menu');
+
+        this._userMenu = new imports.ui.userMenu.UserMenu();
+        this.add_child(this._userMenu.panelBox);
+        this.menu.addMenuItem(this._userMenu.menu);
+        this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
+
+        let systemIndicator = new imports.ui.status.system.Indicator();
+        this.menu.addMenuItem(systemIndicator.menu);
+
+        menuLayout.addSizeChild(systemIndicator.menu.actor);
+
+        // We need to monitor the session to know when to show the button
+        Main.sessionMode.connect('updated', this._sessionUpdated.bind(this));
+        this._sessionUpdated();
+    }
+
+    _sessionUpdated() {
+        this.visible = !Main.sessionMode.isGreeter;
+        this.setSensitive(!Main.sessionMode.isLocked);
+    }
+});
+
 var PowerMenu = GObject.registerClass(
 class PopoverMenu extends PanelMenu.SingleIconButton {
     _init() {
@@ -833,6 +851,7 @@ const PANEL_ITEM_IMPLEMENTATIONS = {
     'dwellClick': imports.ui.status.dwellClick.DwellClickIndicator,
     'hotCorner': imports.ui.hotCorner.HotCorner,
     'powerMenu': PowerMenu,
+    'userMenu': UserMenu,
 };
 
 var Panel = GObject.registerClass(
diff --git a/js/ui/popupMenu.js b/js/ui/popupMenu.js
index d5d5fdd265..052b879186 100644
--- a/js/ui/popupMenu.js
+++ b/js/ui/popupMenu.js
@@ -502,7 +502,7 @@ var PopupMenuBase = class {
         return menuItem;
     }
 
-    addSettingsAction(title, desktopFile) {
+    addSettingsAction(title, desktopFile, icon) {
         let menuItem = this.addAction(title, () => {
             let app = Shell.AppSystem.get_default().lookup_app(desktopFile);
 
@@ -513,7 +513,7 @@ var PopupMenuBase = class {
 
             Main.overview.hide();
             app.activate();
-        });
+        }, icon);
 
         menuItem.visible = Main.sessionMode.allowSettings;
         this._settingsActions[desktopFile] = menuItem;
diff --git a/js/ui/sessionMode.js b/js/ui/sessionMode.js
index 2212a16b4c..0352712649 100644
--- a/js/ui/sessionMode.js
+++ b/js/ui/sessionMode.js
@@ -103,7 +103,8 @@ const _modes = {
         panel: {
             left: ['endlessButton', 'appIcons'],
             center: [],
-            right: ['dwellClick', 'a11y', 'keyboard', 'aggregateMenu', 'dateMenu', 'hotCorner']
+            right: ['dwellClick', 'a11y', 'keyboard', 'aggregateMenu', 'dateMenu',
+                    'userMenu', 'hotCorner']
         }
     }
 };
diff --git a/js/ui/status/system.js b/js/ui/status/system.js
index 41ddac13b3..467f5278dc 100644
--- a/js/ui/status/system.js
+++ b/js/ui/status/system.js
@@ -1,7 +1,8 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported Indicator */
 
-const { AccountsService, Clutter, GLib, GObject, Shell, St } = imports.gi;
+const { AccountsService, Clutter, Gio,
+        GLib, GObject, Shell, St } = imports.gi;
 
 const BoxPointer = imports.ui.boxpointer;
 const SystemActions = imports.misc.systemActions;
@@ -132,9 +133,6 @@ var Indicator = class extends PanelMenu.SystemIndicator {
             this._systemActions.forceUpdate();
         });
         this._updateMultiUser();
-
-        Main.sessionMode.connect('updated', this._sessionUpdated.bind(this));
-        this._sessionUpdated();
     }
 
     _sessionUpdated() {
@@ -163,14 +161,30 @@ var Indicator = class extends PanelMenu.SystemIndicator {
             this._switchUserSubMenu.label.text = this._user.get_user_name();
     }
 
+    _createActionButtonBase(accessibleName) {
+        let button = new St.Button({
+            reactive: true,
+            can_focus: true,
+            track_hover: true,
+            accessible_name: accessibleName,
+            style_class: 'system-menu-action',
+        });
+        return button;
+    }
+
     _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;
+        let button = this._createActionButtonBase(accessibleName);
+        button.child = new St.Icon({ icon_name: iconName });
+        return button;
+    }
+
+    _createActionButtonForIconPath(iconPath, accessibleName) {
+        let iconFile = Gio.File.new_for_uri('resource:///org/gnome/shell' + iconPath);
+        let gicon = new Gio.FileIcon({ file: iconFile });
+
+        let button = this._createActionButtonBase(accessibleName);
+        button.child = new St.Icon({ gicon: gicon });
+        return button;
     }
 
     _createSubMenu() {
@@ -219,28 +233,18 @@ var Indicator = class extends PanelMenu.SystemIndicator {
         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;
 
-        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.add(this._settingsAction, { expand: true, x_fill: false });
+        this._logoutAction = this._createActionButtonForIconPath('/theme/system-logout.png', _("Log Out"));
+        this._logoutAction.connect('clicked',  () => {
+            this.menu.itemActivated(BoxPointer.PopupAnimation.NONE);
+            this._systemActions.activateLogout();
+        });
+        item.actor.add(this._logoutAction, { expand: true, x_fill: false });
 
         this._lockScreenAction = this._createActionButton('changes-prevent', _("Lock"));
         this._lockScreenAction.connect('clicked', () => {
@@ -279,7 +283,7 @@ var Indicator = class extends PanelMenu.SystemIndicator {
         this.menu.addMenuItem(item);
 
         let visibilityGroup = [
-            this._settingsAction,
+            this._logoutAction,
             this._lockScreenAction,
             this._altSwitcher.actor,
         ];
@@ -290,10 +294,4 @@ var Indicator = class extends PanelMenu.SystemIndicator {
             });
         }
     }
-
-    _onSettingsClicked() {
-        this.menu.itemActivated();
-        Main.overview.hide();
-        this._settingsApp.activate();
-    }
 };
diff --git a/js/ui/userMenu.js b/js/ui/userMenu.js
new file mode 100644
index 0000000000..056fa47384
--- /dev/null
+++ b/js/ui/userMenu.js
@@ -0,0 +1,170 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const { AccountsService, Clutter, GLib,
+        Gio, Pango, Shell, St } = imports.gi;
+
+const AppActivation = imports.ui.appActivation;
+const BoxPointer = imports.ui.boxpointer;
+const Main = imports.ui.main;
+const Panel = imports.ui.panel;
+const PanelMenu = imports.ui.panelMenu;
+const PopupMenu = imports.ui.popupMenu;
+const UserWidget = imports.ui.userWidget;
+
+const USER_ICON_SIZE = 34;
+
+const ONLINE_ACCOUNTS_TEXT = _("Social Accounts");
+const ONLINE_ACCOUNTS_PANEL_LAUNCHER = 'gnome-online-accounts-panel.desktop';
+
+const SETTINGS_TEXT = _("Settings");
+const SETTINGS_LAUNCHER = "gnome-control-center.desktop";
+
+const USER_ACCOUNTS_PANEL_LAUNCHER = 'gnome-user-accounts-panel.desktop';
+
+const HELP_CENTER_TEXT = _("Help");
+const HELP_CENTER_LAUNCHER = 'org.gnome.Yelp.desktop';
+
+const UserAccountSection = class extends PopupMenu.PopupMenuSection {
+    constructor(user) {
+        super();
+
+        // User account's icon
+        this.userIconItem = new PopupMenu.PopupBaseMenuItem({
+            reactive: false,
+            can_focus: false,
+        });
+        this._user = user;
+        this._avatar = new UserWidget.Avatar(this._user, {
+            reactive: true,
+            styleClass: 'user-menu-avatar',
+        });
+        let iconButton = new St.Button({ child: this._avatar.actor });
+        this.userIconItem.add(iconButton, { expand: true, span: -1 });
+
+        iconButton.connect('clicked', () => {
+            if (Main.sessionMode.allowSettings)
+                this.userIconItem.activate();
+        });
+
+        this.userIconItem.connect('notify::sensitive', () => {
+            this._avatar.setSensitive(this.userIconItem.getSensitive);
+        });
+        this.addMenuItem(this.userIconItem);
+
+        // User account's name
+        this.userLabelItem = new PopupMenu.PopupBaseMenuItem({
+            reactive: false,
+            can_focus: false,
+        });
+        this._label = new St.Label({ style_class: 'user-menu-name' });
+        this._label.clutter_text.set_line_wrap(true);
+        this._label.clutter_text.set_ellipsize(Pango.EllipsizeMode.NONE);
+        this.userLabelItem.add(this._label, { expand: true, span: -1 });
+        this.addMenuItem(this.userLabelItem);
+
+        // We need to monitor the session to know when to enable the user avatar
+        Main.sessionMode.connect('updated', this._sessionUpdated.bind(this));
+        this._sessionUpdated();
+    }
+
+    _sessionUpdated() {
+        this.userIconItem.setSensitive(Main.sessionMode.allowSettings);
+    }
+
+    update() {
+        this._avatar.update();
+
+        if (this._user.is_loaded)
+            this._label.set_text(this._user.get_real_name());
+        else
+            this._label.set_text('');
+    }
+};
+
+var UserMenu = class {
+    constructor() {
+        this._userManager = AccountsService.UserManager.get_default();
+        this._user = this._userManager.get_user(GLib.get_user_name());
+
+        this._user.connect('notify::is-loaded', this._updateUser.bind(this));
+        this._user.connect('changed', this._updateUser.bind(this));
+
+        this._createPanelIcon();
+        this._createPopupMenu();
+
+        this._updateUser();
+    }
+
+    _createPanelIcon() {
+        this.panelBox = new St.BoxLayout({
+            x_align: Clutter.ActorAlign.CENTER,
+            y_align: Clutter.ActorAlign.CENTER,
+        });
+        this._panelAvatar = new UserWidget.Avatar(this._user, {
+            iconSize: USER_ICON_SIZE,
+            styleClass: 'user-menu-button-icon',
+            reactive: true,
+        });
+        this.panelBox.add_actor(this._panelAvatar.actor);
+    }
+
+    _createPopupMenu() {
+        this.menu = new PopupMenu.PopupMenuSection();
+
+        this._accountSection = new UserAccountSection(this._user);
+        this._accountSection.userIconItem.connect('activate', () => {
+            this._launchApplication(USER_ACCOUNTS_PANEL_LAUNCHER);
+        });
+
+        this.menu.addMenuItem(this._accountSection);
+        this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
+
+        let menuItemsSection = new PopupMenu.PopupMenuSection();
+        menuItemsSection.box.style_class = 'user-menu-items';
+
+        // Control Center
+        let gicon = new Gio.ThemedIcon({ name: 'applications-system-symbolic' });
+        this._settingsItem = menuItemsSection.addAction(SETTINGS_TEXT, () => {
+            this.menu.close(BoxPointer.PopupAnimation.NONE);
+            Main.overview.hide();
+
+            let app = Shell.AppSystem.get_default().lookup_app(SETTINGS_LAUNCHER);
+            let context = new AppActivation.AppActivationContext(app);
+            context.activate();
+        }, gicon);
+
+        // Social
+        gicon = new Gio.ThemedIcon({ name: 'user-available-symbolic' });
+        menuItemsSection.addSettingsAction(ONLINE_ACCOUNTS_TEXT, ONLINE_ACCOUNTS_PANEL_LAUNCHER, gicon);
+
+        // Help center
+        let iconFile = Gio.File.new_for_uri('resource:///org/gnome/shell/theme/endless-help-symbolic.svg')
+        gicon = new Gio.FileIcon({ file: iconFile });
+        menuItemsSection.addAction(HELP_CENTER_TEXT, () => {
+            this._launchApplication(HELP_CENTER_LAUNCHER);
+        }, gicon);
+        this.menu.addMenuItem(menuItemsSection);
+
+        // We need to monitor the session to know when to show/hide the settings item
+        Main.sessionMode.connect('updated', this._sessionUpdated.bind(this));
+        this._sessionUpdated();
+    }
+
+    _launchApplication(launcherName) {
+        this.menu.close(BoxPointer.PopupAnimation.NONE);
+        Main.overview.hide();
+
+        let app = Shell.AppSystem.get_default().lookup_app(launcherName);
+        let context = new AppActivation.AppActivationContext(app);
+        context.activate();
+    }
+
+    _updateUser() {
+        this._panelAvatar.update();
+        this._accountSection.update();
+    }
+
+    _sessionUpdated() {
+        this._settingsItem.visible = Main.sessionMode.allowSettings;
+    }
+};
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 77aea9b8b2..955765760d 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -98,3 +98,4 @@ js/ui/endlessButton.js
 js/ui/forceAppExitDialog.js
 js/ui/hotCorner.js
 js/ui/status/orientation.js
+js/ui/userMenu.js


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