[gnome-shell/T27795: 36/138] trayArea: Implement a tray area and add it to the right side of the panel



commit 09066f6d6fb92e224c2c4c620825e8ba6e928975
Author: Mario Sanchez Prada <mario endlessm com>
Date:   Mon Feb 12 12:04:24 2018 +0000

    trayArea: Implement a tray area and add it to the right side of the panel
    
    GNOME Shell 3.26 dropped the small sliding area on the left where the
    icons for some applications still relying on a system's tray area would
    live (e.g. Steam, Dropbox, Skype...), so we're taking the code of the
    TopIcons Plus extension (now also unmaintained) and simplifying it to
    bring this functionality back.
    
    Additionally, as this might be a feature that will not please everyone
    due to its tendency to clutter the bottom panel, include a new GSetting
    key `enable-tray-area` (default to `true`) to allow disabling it.

 data/org.gnome.shell.gschema.xml.in       |  11 ++
 data/theme/gnome-shell-sass/_endless.scss |  10 ++
 js/js-resources.gresource.xml             |   1 +
 js/ui/appIconBar.js                       |  93 +++++++++---
 js/ui/components/trayArea.js              | 231 ++++++++++++++++++++++++++++++
 js/ui/main.js                             |   1 +
 js/ui/sessionMode.js                      |   6 +-
 7 files changed, 333 insertions(+), 20 deletions(-)
---
diff --git a/data/org.gnome.shell.gschema.xml.in b/data/org.gnome.shell.gschema.xml.in
index cb53eed4e1..79c0c2c784 100644
--- a/data/org.gnome.shell.gschema.xml.in
+++ b/data/org.gnome.shell.gschema.xml.in
@@ -112,6 +112,17 @@
 
     <!-- Endless-specific keys beyond this point -->
 
+    <key name="enable-tray-area" type="b">
+      <default>true</default>
+      <summary>
+        Whether the Tray Area will be enabled
+      </summary>
+      <description>
+        This enables an area on the right side of the bottom panel where the
+        icons for those applications still relying on the tray area will display
+        their status icons (e.g. Steam, Dropbox, Skype...).
+      </description>
+    </key>
     <key name="hot-corner-enabled" type="b">
       <default>false</default>
       <summary>
diff --git a/data/theme/gnome-shell-sass/_endless.scss b/data/theme/gnome-shell-sass/_endless.scss
index 591cc2169f..64a7ff1571 100644
--- a/data/theme/gnome-shell-sass/_endless.scss
+++ b/data/theme/gnome-shell-sass/_endless.scss
@@ -268,3 +268,13 @@
     &:bl { background-image: url("corner-ripple-bl.png"); }
     &:br { background-image: url("corner-ripple-br.png"); }
 }
+
+// Tray Area
+
+#panel {
+    .tray-area {
+        margin_top: 2px;
+        margin_bottom: 2px;
+        spacing: 10px;
+    }
+}
diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml
index d526d58433..b59be3806b 100644
--- a/js/js-resources.gresource.xml
+++ b/js/js-resources.gresource.xml
@@ -142,6 +142,7 @@
 
     <file>ui/appActivation.js</file>
     <file>ui/appIconBar.js</file>
+    <file>ui/components/trayArea.js</file>
     <file>ui/endlessButton.js</file>
     <file>ui/forceAppExitDialog.js</file>
     <file>ui/hotCorner.js</file>
diff --git a/js/ui/appIconBar.js b/js/ui/appIconBar.js
index 911eeba1d6..38c97e61fd 100644
--- a/js/ui/appIconBar.js
+++ b/js/ui/appIconBar.js
@@ -618,7 +618,9 @@ const ScrolledIconList = GObject.registerClass({
                              app: app });
         }
 
-        let favorites = AppFavorites.getAppFavorites().getFavorites();
+        let appFavorites = AppFavorites.getAppFavorites();
+        appFavorites.connect('changed', this._onAppFavoritesChanged.bind(this));
+        let favorites = appFavorites.getFavorites();
         for (let i = 0; i < favorites.length; i++) {
             this._addButtonAnimated(favorites[i]);
         }
@@ -632,6 +634,7 @@ const ScrolledIconList = GObject.registerClass({
             this._addButtonAnimated(app);
         }
 
+        appSys.connect('installed-changed', this._onInstalledChanged.bind(this));
         appSys.connect('app-state-changed', this._onAppStateChanged.bind(this));
     }
 
@@ -791,7 +794,7 @@ const ScrolledIconList = GObject.registerClass({
         return -1;
     }
 
-    _addButtonAnimated(app) {
+    _addButtonAnimated(app, oldChild) {
         if (this._taskbarApps.has(app) || !this._isAppInteresting(app))
             return;
 
@@ -807,21 +810,65 @@ const ScrolledIconList = GObject.registerClass({
         });
         newChild.connect('app-icon-unpinned', () => {
             favorites.removeFavorite(app.get_id());
-            if (app.state == Shell.AppState.STOPPED) {
-                newActor.destroy();
-                this._taskbarApps.delete(app);
-                this._updatePage();
-            }
         });
         this._taskbarApps.set(app, newChild);
 
-        this._container.add_actor(newActor);
+        if (oldChild)
+            this._container.replace_child(oldChild, newChild);
+        else
+            this._container.add_actor(newChild);
     }
 
     _addButton(app) {
         this._addButtonAnimated(app);
     }
 
+    _onAppFavoritesChanged(appFavorites) {
+        let favoriteMap = appFavorites.getFavoriteMap();
+        var changed = false;
+
+        // Update existing favorites, and add new ones.
+        for (let id in favoriteMap) {
+            let app = favoriteMap[id];
+            if (!this._taskbarApps.has(app)) {
+                var childToReplace = null;
+
+                for (let [oldApp, oldChild] of this._taskbarApps) {
+                    if (oldApp.get_id() === id) {
+                        this._taskbarApps.delete(oldApp);
+                        childToReplace = oldChild;
+                        break;
+                    }
+                }
+
+                // TODO: in the case where no existing app matches, put the new
+                // app at the right position, not the end.
+                this._addButtonAnimated(app, childToReplace);
+                if (childToReplace)
+                    childToReplace.destroy();
+                changed = true;
+            }
+        }
+
+        // Get rid of any removed favorites
+        for (let [oldApp, oldChild] of this._taskbarApps) {
+            if (!this._isAppInteresting(oldApp)) {
+                oldChild.destroy();
+                this._taskbarApps.delete(oldApp);
+                changed = true;
+            }
+        }
+
+        if (changed)
+            this._updatePage();
+    }
+
+    _onInstalledChanged(appSys) {
+        let appFavorites = AppFavorites.getAppFavorites();
+        appFavorites.reload();
+        this._onAppFavoritesChanged(appFavorites);
+    }
+
     _onAppStateChanged(appSys, app) {
         let state = app.state;
         switch(state) {
@@ -837,8 +884,7 @@ const ScrolledIconList = GObject.registerClass({
 
             let oldChild = this._taskbarApps.get(app);
             if (oldChild) {
-                let oldButton = this._taskbarApps.get(app);
-                this._container.remove_actor(oldButton);
+                this._container.remove_actor(oldChild);
                 this._taskbarApps.delete(app);
             }
 
@@ -892,6 +938,9 @@ class AppIconBarContainer extends St.Widget {
     }
 
     vfunc_get_preferred_width(forHeight) {
+        let themeNode = this.get_theme_node();
+
+        forHeight = themeNode.adjust_for_height(forHeight);
         let [minBackWidth, natBackWidth] = this._backButton.get_preferred_width(forHeight);
         let [minForwardWidth, natForwardWidth] = this._forwardButton.get_preferred_width(forHeight);
 
@@ -902,13 +951,16 @@ class AppIconBarContainer extends St.Widget {
         let minContentWidth = this._scrolledIconList.getMinContentWidth(forHeight);
         let [, natContentWidth] = this._scrolledIconList.get_preferred_width(forHeight);
 
-        let minSize = minBackWidth + minForwardWidth + 2 * this._spacing + minContentWidth;
-        let naturalSize = natBackWidth + natForwardWidth + 2 * this._spacing + natContentWidth;
-        
-        return [minSize, naturalSize];
+        let minWidth = minBackWidth + minForwardWidth + 2 * this._spacing + minContentWidth;
+        let natWidth = natBackWidth + natForwardWidth + 2 * this._spacing + natContentWidth;
+
+        return themeNode.adjust_preferred_width(minWidth, natWidth);
     }
 
     vfunc_get_preferred_height(forWidth) {
+        let themeNode = this.get_theme_node();
+
+        forWidth = themeNode.adjust_for_width(forWidth);
         let [minListHeight, natListHeight] = this._scrolledIconList.get_preferred_height(forWidth);
         let [minBackHeight, natBackHeight] = this._backButton.get_preferred_height(forWidth);
         let [minForwardHeight, natForwardHeight] = this._forwardButton.get_preferred_height(forWidth);
@@ -916,17 +968,22 @@ class AppIconBarContainer extends St.Widget {
         let minButtonHeight = Math.max(minBackHeight, minForwardHeight);
         let natButtonHeight = Math.max(natBackHeight, natForwardHeight);
 
-        let minSize = Math.max(minButtonHeight, minListHeight);
-        let naturalSize = Math.max(natButtonHeight, natListHeight);
-        
-        return [minSize, naturalSize];
+        let minHeight = Math.max(minButtonHeight, minListHeight);
+        let natHeight = Math.max(natButtonHeight, natListHeight);
+
+        return themeNode.adjust_preferred_height(minHeight, natHeight);
     }
 
     vfunc_style_changed() {
+        super.vfunc_style_changed();
         this._spacing = this.get_theme_node().get_length('spacing');
     }
 
     vfunc_allocate(box, flags) {
+        this.set_allocation(box, flags);
+
+        box = this.get_theme_node().get_content_box(box);
+
         let allocWidth = box.x2 - box.x1;
         let allocHeight = box.y2 - box.y1;
 
diff --git a/js/ui/components/trayArea.js b/js/ui/components/trayArea.js
new file mode 100644
index 0000000000..309c7f30f9
--- /dev/null
+++ b/js/ui/components/trayArea.js
@@ -0,0 +1,231 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+//
+// Copyright (C) phocean <jc phocean net>
+// Copyright (C) 2017 Endless Mobile, Inc.
+//
+// This is a GNOME Shell component implemented to keep providing a way
+// for apps the still rely in the old-fashioned tray area present in
+// GNOME Shell < 3.26 to keep showing up somewhere in Endless OS.
+//
+// Since this is the same goal achieved by the -now unmaintained- extensions
+// TopIcons and TopIcons Plus, we are pretty much forking the code in the
+// latter here and integrating it as a component in our shell, so that our
+// users can keep enjoying the ability to actually close certain applications
+// that would keep running in the background otherwise (e.g. Steam, Dropbox..).
+//
+// The code from the original TopIcons Plus extension used as the source for
+// this component can be checked in https://github.com/phocean/TopIcons-plus.
+//
+// Licensed under the GNU General Public License Version 2
+//
+// 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
+// of the License, 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+const { Clutter, Gio, GLib, GObject, Shell, St } = imports.gi;
+
+const Main = imports.ui.main;
+const System = imports.system;
+const PanelMenu = imports.ui.panelMenu;
+const ExtensionUtils = imports.misc.extensionUtils;
+
+// The original TopIcons Plus extension allowed configuring these values via a
+// GSettings schema, but we don't need that for now, let's hardcode those values.
+const ICON_BRIGHTNESS = 0.0;   // range: [-1.0, 1.0]
+const ICON_CONTRAST = 0.0;     // range: [-1.0, 1.0]
+const ICON_OPACITY = 153;      // range: [0, 255]
+const ICON_DESATURATION = 1.0; // range: [0.0, 1.0]
+const ICON_SIZE = 16;
+
+// We don't want to show icons in the tray bar for apps that are already
+// handled by other extensions, such as in the case of Skype.
+const EXTENSIONS_BLACKLIST = [
+    ["skype","SkypeNotification chrisss404 gmail com"]
+];
+
+const SETTINGS_SCHEMA = 'org.gnome.shell';
+const SETTING_ENABLE_TRAY_AREA = 'enable-tray-area';
+
+var TrayArea = class TrayArea {
+    constructor() {
+        this._icons = [];
+        this._iconsBoxLayout = null;
+        this._iconsContainer = null;
+        this._trayManager = null;
+
+        this._settings = new Gio.Settings({ schema_id: SETTINGS_SCHEMA });
+
+        // The original extension allowed to configure the target side of
+        // the bottom panel but we will always be using the right one.
+        this._targetPanel = Main.panel._rightBox;
+
+        this._trayAreaEnabled = this._settings.get_boolean(SETTING_ENABLE_TRAY_AREA);
+
+        global.settings.connect('changed::' + SETTING_ENABLE_TRAY_AREA, () => {
+            this._trayAreaEnabled = this._settings.get_boolean(SETTING_ENABLE_TRAY_AREA);
+
+            if (this._trayManager)
+                this._destroyTray();
+
+            if (this._trayAreaEnabled)
+                this._createTray();
+        });
+    }
+
+    _updateIconStyle(icon) {
+        // Size
+        let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
+        if (arguments.length == 1) {
+            icon.get_parent().set_size(ICON_SIZE * scaleFactor, ICON_SIZE * scaleFactor);
+            icon.set_size(ICON_SIZE * scaleFactor, ICON_SIZE * scaleFactor);
+        } else {
+            for (let icon of this._icons) {
+                icon.get_parent().set_size(ICON_SIZE * scaleFactor, ICON_SIZE * scaleFactor);
+                icon.set_size(ICON_SIZE * scaleFactor, ICON_SIZE * scaleFactor);
+            }
+        }
+
+        // Opacity
+        if (arguments.length == 1) {
+            icon.get_parent().connect('enter-event', (actor, event) => icon.opacity = 255);
+            icon.get_parent().connect('leave-event', (actor, event) => icon.opacity = ICON_OPACITY);
+            icon.opacity = ICON_OPACITY;
+        } else {
+            for (let icon of this._icons) {
+                icon.get_parent().connect('enter-event', (actor, event) => icon.opacity = 255);
+                icon.get_parent().connect('leave-event', (actor, event) => icon.opacity = ICON_OPACITY);
+                icon.opacity = ICON_OPACITY;
+            }
+        }
+
+        // Saturation
+        if (arguments.length == 1) {
+            let desat_effect = new Clutter.DesaturateEffect({ factor : ICON_DESATURATION });
+            desat_effect.set_factor(ICON_DESATURATION);
+            icon.add_effect_with_name('desaturate', desat_effect);
+        } else {
+            for (let icon of this._icons) {
+                let effect = icon.get_effect('desaturate');
+                if (effect)
+                    effect.set_factor(ICON_DESATURATION);
+            }
+        }
+
+        // Brightness & Contrast
+        if (arguments.length == 1) {
+            let bright_effect = new Clutter.BrightnessContrastEffect({});
+            bright_effect.set_brightness(ICON_BRIGHTNESS);
+            bright_effect.set_contrast(ICON_CONTRAST);
+            icon.add_effect_with_name('brightness-contrast', bright_effect);
+        } else {
+            for (let icon of this._icons) {
+                let effect = icon.get_effect('brightness-contrast')
+                effect.set_brightness(ICON_BRIGHTNESS);
+                effect.set_contrast(ICON_CONTRAST);
+            }
+        }
+
+        icon.reactive = true;
+    }
+
+    _onTrayIconAdded(o, icon, role, delay=1000) {
+        // Loop through the array and hide the extension if extension X is
+        // enabled and corresponding application is running
+        let iconWmClass = icon.wm_class ? icon.wm_class.toLowerCase() : '';
+        for (let [wmClass, uuid] of EXTENSIONS_BLACKLIST) {
+            if (ExtensionUtils.extensions[uuid] !== undefined &&
+                ExtensionUtils.extensions[uuid].state === 1 &&
+                iconWmClass === wmClass)
+                return;
+        }
+
+        let iconContainer = new St.Button({
+            child: icon,
+            visible: false,
+        });
+
+        icon.connect("destroy", () => {
+            icon.clear_effects();
+            iconContainer.destroy();
+        });
+
+        iconContainer.connect('button-release-event', (actor, event) => {
+            icon.click(event);
+        });
+
+        GLib.timeout_add(GLib.PRIORITY_DEFAULT, delay, () => {
+            iconContainer.visible = true;
+            return GLib.SOURCE_REMOVE;
+        });
+
+        this._iconsBoxLayout.insert_child_at_index(iconContainer, 0);
+        this._updateIconStyle(icon);
+        this._icons.push(icon);
+    }
+
+    _onTrayIconRemoved(o, icon) {
+        if (this._icons.indexOf(icon) == -1)
+            return;
+
+        icon.get_parent().destroy();
+        this._icons.splice(this._icons.indexOf(icon), 1);
+    }
+
+    _createTray() {
+        this._iconsBoxLayout = new St.BoxLayout({ style_class: 'tray-area' });
+
+        // An empty ButtonBox will still display padding,therefore create it without visibility.
+        this._iconsContainer = new PanelMenu.ButtonBox({ visible: false });
+        this._iconsContainer.add_actor(this._iconsBoxLayout);
+        this._iconsContainer.show();
+
+        this._trayManager = new Shell.TrayManager();
+        this._trayManager.connect('tray-icon-added', this._onTrayIconAdded.bind(this));
+        this._trayManager.connect('tray-icon-removed', this._onTrayIconRemoved.bind(this));
+        this._trayManager.manage_screen(Main.panel);
+
+        // The actor for a PanelMenu.ButtonBox is already inside a StBin
+        // container, so remove that association before re-adding it below.
+        let parent = this._iconsContainer.get_parent();
+        if (parent)
+            parent.remove_actor(this._iconsContainer);
+
+        // Place our tray at the end of the panel (e.g. leftmost side for right panel).
+        this._targetPanel.insert_child_at_index(this._iconsContainer, 0);
+
+    }
+
+    _destroyTray() {
+        this._iconsContainer.destroy();
+
+        this._trayManager = null;
+        this._iconsContainer = null;
+        this._iconsBoxLayout = null;
+        this._icons = [];
+
+        // Force finalizing tray to unmanage screen
+        System.gc();
+    }
+
+    enable() {
+        Main.trayArea = this;
+        if (this._trayAreaEnabled)
+            this._createTray();
+    }
+
+    disable() {
+        this._destroyTray();
+        Main.trayArea = null;
+    }
+};
+var Component = TrayArea;
diff --git a/js/ui/main.js b/js/ui/main.js
index 1589fe2605..0ca07052fa 100644
--- a/js/ui/main.js
+++ b/js/ui/main.js
@@ -87,6 +87,7 @@ var kbdA11yDialog = null;
 var inputMethod = null;
 var introspectService = null;
 var locatePointer = null;
+var trayArea = null;
 let _startDate;
 let _defaultCssStylesheet = null;
 let _cssStylesheet = null;
diff --git a/js/ui/sessionMode.js b/js/ui/sessionMode.js
index c79ae7917e..2212a16b4c 100644
--- a/js/ui/sessionMode.js
+++ b/js/ui/sessionMode.js
@@ -94,9 +94,11 @@ const _modes = {
         unlockDialog: imports.ui.unlockDialog.UnlockDialog,
         components: Config.HAVE_NETWORKMANAGER
             ? ['networkAgent', 'polkitAgent', 'telepathyClient',
-               'keyring', 'autorunManager', 'automountManager']
+               'keyring', 'autorunManager', 'automountManager',
+               'trayArea']
             : ['polkitAgent', 'telepathyClient',
-               'keyring', 'autorunManager', 'automountManager'],
+               'keyring', 'autorunManager', 'automountManager',
+               'trayArea'],
 
         panel: {
             left: ['endlessButton', 'appIcons'],


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