diff --git a/data/theme/gnome-shell-high-contrast.css b/data/theme/gnome-shell-high-contrast.css
index b9698f47f..730d72576 100644
--- a/data/theme/gnome-shell-high-contrast.css
+++ b/data/theme/gnome-shell-high-contrast.css
@@ -1327,6 +1327,27 @@ StScrollBar {
color: pink; }
/* Eeeky things */
+.legacy-tray {
+ background-color: #000;
+ border: 1px solid black;
+ border-bottom-width: 0; }
+ .legacy-tray:ltr {
+ border-radius: 0 6px 0 0;
+ border-left-width: 0; }
+ .legacy-tray:rtl {
+ border-radius: 6px 0 0 0;
+ border-right-width: 0; }
+
+.legacy-tray-handle StIcon {
+ icon-size: 24px; }
+
+.legacy-tray-icon-box {
+ padding: 6px;
+ spacing: 12px; }
+ .legacy-tray-icon-box StButton {
+ width: 24px;
+ height: 24px; }
+
.magnifier-zoom-region {
border: 2px solid #215d9c; }
.magnifier-zoom-region.full-screen {
diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css
index 032c36293..117504f85 100644
--- a/data/theme/gnome-shell.css
+++ b/data/theme/gnome-shell.css
@@ -1327,6 +1327,27 @@ StScrollBar {
color: pink; }
/* Eeeky things */
+.legacy-tray {
+ background-color: #393f3f;
+ border: 1px solid #1c1f1f;
+ border-bottom-width: 0; }
+ .legacy-tray:ltr {
+ border-radius: 0 6px 0 0;
+ border-left-width: 0; }
+ .legacy-tray:rtl {
+ border-radius: 6px 0 0 0;
+ border-right-width: 0; }
+
+.legacy-tray-handle StIcon {
+ icon-size: 24px; }
+
+.legacy-tray-icon-box {
+ padding: 6px;
+ spacing: 12px; }
+ .legacy-tray-icon-box StButton {
+ width: 24px;
+ height: 24px; }
+
.magnifier-zoom-region {
border: 2px solid #215d9c; }
.magnifier-zoom-region.full-screen {
diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml
index 439c0aad9..bd8f30708 100644
--- a/js/js-resources.gresource.xml
+++ b/js/js-resources.gresource.xml
@@ -57,6 +57,7 @@
ui/layout.js
ui/lightbox.js
ui/lookingGlass.js
+ ui/legacyTray.js
ui/magnifier.js
ui/magnifierDBus.js
ui/main.js
diff --git a/js/ui/legacyTray.js b/js/ui/legacyTray.js
new file mode 100644
index 000000000..a167ea234
--- /dev/null
+++ b/js/ui/legacyTray.js
@@ -0,0 +1,178 @@
+const Clutter = imports.gi.Clutter;
+const GObject = imports.gi.GObject;
+const Shell = imports.gi.Shell;
+const St = imports.gi.St;
+
+const Lang = imports.lang;
+const Layout = imports.ui.layout;
+const Main = imports.ui.main;
+const Overview = imports.ui.overview;
+const OverviewControls = imports.ui.overviewControls;
+const Tweener = imports.ui.tweener;
+
+const STANDARD_TRAY_ICON_IMPLEMENTATIONS = {
+ 'bluetooth-applet': 'bluetooth',
+ 'gnome-volume-control-applet': 'volume', // renamed to gnome-sound-applet
+ // when moved to control center
+ 'gnome-sound-applet': 'volume',
+ 'nm-applet': 'network',
+ 'gnome-power-manager': 'battery',
+ 'keyboard': 'keyboard',
+ 'a11y-keyboard': 'a11y',
+ 'kbd-scrolllock': 'keyboard',
+ 'kbd-numlock': 'keyboard',
+ 'kbd-capslock': 'keyboard',
+ 'ibus-ui-gtk': 'keyboard'
+};
+
+// Offset of the original position from the bottom-right corner
+const CONCEALED_VISIBLE_FRACTION = 0.2;
+const REVEAL_ANIMATION_TIME = 0.2;
+
+const LegacyTray = new Lang.Class({
+ Name: 'LegacyTray',
+
+ _init: function() {
+ this.actor = new St.Widget({ clip_to_allocation: true,
+ layout_manager: new Clutter.BinLayout() });
+ let constraint = new Layout.MonitorConstraint({ primary: true,
+ work_area: true });
+ this.actor.add_constraint(constraint);
+
+ this._slideLayout = new OverviewControls.SlideLayout();
+ this._slideLayout.translationX = 0;
+ this._slideLayout.slideDirection = OverviewControls.SlideDirection.LEFT;
+
+ this._slider = new St.Widget({ style_class: 'legacy-tray',
+ x_expand: true, y_expand: true,
+ x_align: Clutter.ActorAlign.START,
+ y_align: Clutter.ActorAlign.END,
+ layout_manager: this._slideLayout });
+ this.actor.add_actor(this._slider);
+
+ this._box = new St.BoxLayout();
+ this._slider.add_actor(this._box);
+
+ this._concealHandle = new St.Button({ style_class: 'legacy-tray-handle' });
+ this._concealHandle.child = new St.Icon({ icon_name: 'go-previous-symbolic' });
+ this._box.add_child(this._concealHandle);
+
+ this._iconBox = new St.BoxLayout({ style_class: 'legacy-tray-icon-box' });
+ this._box.add_actor(this._iconBox);
+
+ this._revealHandle = new St.Button({ style_class: 'legacy-tray-handle' });
+ this._revealHandle.child = new St.Icon({ icon_name: 'go-next-symbolic' });
+ this._box.add_child(this._revealHandle);
+
+ this._revealHandle.bind_property('visible',
+ this._concealHandle, 'visible',
+ GObject.BindingFlags.BIDIRECTIONAL |
+ GObject.BindingFlags.INVERT_BOOLEAN);
+ this._revealHandle.connect('notify::visible',
+ Lang.bind(this, this._sync));
+ this._revealHandle.connect('notify::hover',
+ Lang.bind(this ,this._sync));
+ this._revealHandle.connect('clicked', Lang.bind(this,
+ function() {
+ this._concealHandle.show();
+ }));
+ this._concealHandle.connect('clicked', Lang.bind(this,
+ function() {
+ this._revealHandle.show();
+ }));
+
+ Main.layoutManager.addChrome(this.actor, { affectsInputRegion: false });
+ Main.layoutManager.trackChrome(this._slider, { affectsInputRegion: true });
+
+ this._trayManager = new Shell.TrayManager();
+ this._trayManager.connect('tray-icon-added', Lang.bind(this, this._onTrayIconAdded));
+ this._trayManager.connect('tray-icon-removed', Lang.bind(this, this._onTrayIconRemoved));
+ this._trayManager.manage_screen(global.screen, this.actor);
+
+ Main.overview.connect('showing', Lang.bind(this,
+ function() {
+ Tweener.removeTweens(this._slider);
+ Tweener.addTween(this._slider, { opacity: 0,
+ time: Overview.ANIMATION_TIME,
+ transition: 'easeOutQuad' });
+ }));
+ Main.overview.connect('shown', Lang.bind(this, this._sync));
+ Main.overview.connect('hiding', Lang.bind(this,
+ function() {
+ this._sync();
+ Tweener.removeTweens(this._slider);
+ Tweener.addTween(this._slider, { opacity: 255,
+ time: Overview.ANIMATION_TIME,
+ transition: 'easeOutQuad' });
+ }));
+
+ Main.layoutManager.connect('monitors-changed',
+ Lang.bind(this, this._sync));
+ global.screen.connect('in-fullscreen-changed',
+ Lang.bind(this, this._sync));
+ Main.sessionMode.connect('updated', Lang.bind(this, this._sync));
+
+ this._sync();
+ },
+
+ _onTrayIconAdded: function(tm, icon) {
+ let wmClass = icon.wm_class ? icon.wm_class.toLowerCase() : '';
+ if (STANDARD_TRAY_ICON_IMPLEMENTATIONS[wmClass] !== undefined)
+ return;
+
+ let button = new St.Button({ child: icon,
+ button_mask: St.ButtonMask.ONE |
+ St.ButtonMask.TWO |
+ St.ButtonMask.THREE,
+ x_fill: true, y_fill: true });
+ button.connect('clicked',
+ function() {
+ icon.click(Clutter.get_current_event());
+ });
+
+ this._iconBox.add_actor(button);
+ this._sync();
+ },
+
+ _onTrayIconRemoved: function(tm, icon) {
+ if (!this.actor.contains(icon))
+ return;
+
+ icon.get_parent().destroy();
+ this._sync();
+ },
+
+ _sync: function() {
+ // FIXME: we no longer treat tray icons as notifications
+ let allowed = Main.sessionMode.hasNotifications;
+ let hasIcons = this._iconBox.get_n_children() > 0;
+ let inOverview = Main.overview.visible && !Main.overview.animationInProgress;
+ let inFullscreen = Main.layoutManager.primaryMonitor.inFullscreen;
+ this.actor.visible = allowed && hasIcons && !inOverview && !inFullscreen;
+
+ if (!hasIcons)
+ this._concealHandle.hide();
+
+ let targetSlide;
+ if (this._concealHandle.visible) {
+ targetSlide = 1.0;
+ } else if (!hasIcons) {
+ targetSlide = 0.0;
+ } else {
+ let [, boxWidth] = this._box.get_preferred_width(-1);
+ let [, handleWidth] = this._revealHandle.get_preferred_width(-1);
+
+ targetSlide = handleWidth / boxWidth;
+ if (!this._revealHandle.hover)
+ targetSlide *= CONCEALED_VISIBLE_FRACTION;
+ }
+
+ if (this.actor.visible)
+ Tweener.addTween(this._slideLayout,
+ { slideX: targetSlide,
+ time: REVEAL_ANIMATION_TIME,
+ transition: 'easeOutQuad' });
+ else
+ this._slideLayout.slideX = targetSlide;
+ }
+});
diff --git a/js/ui/main.js b/js/ui/main.js
index eb6545864..2a80a9056 100644
--- a/js/ui/main.js
+++ b/js/ui/main.js
@@ -18,6 +18,7 @@ const Environment = imports.ui.environment;
const ExtensionSystem = imports.ui.extensionSystem;
const ExtensionDownloader = imports.ui.extensionDownloader;
const Keyboard = imports.ui.keyboard;
+const LegacyTray = imports.ui.legacyTray;
const MessageTray = imports.ui.messageTray;
const ModalDialog = imports.ui.modalDialog;
const OsdWindow = imports.ui.osdWindow;
@@ -52,6 +53,7 @@ let overview = null;
let runDialog = null;
let lookingGlass = null;
let wm = null;
+let legacyTray = null;
let messageTray = null;
let screenShield = null;
let notificationDaemon = null;
@@ -159,6 +161,7 @@ function _initializeUI() {
if (LoginManager.canLock())
screenShield = new ScreenShield.ScreenShield();
+ legacyTray = new LegacyTray.LegacyTray();
messageTray = new MessageTray.MessageTray();
panel = new Panel.Panel();
keyboard = new Keyboard.Keyboard();