diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css index 494c03311..bee168738 100644 --- a/data/theme/gnome-shell.css +++ b/data/theme/gnome-shell.css @@ -1726,6 +1726,28 @@ StScrollBar StButton#vhandle:active { -shell-counter-overlap-y: 13px; } +.message-tray-indicator { + spacing: 4px; +} + +.message-tray-indicator-count { + font-size: 1.2em; + font-weight: bold; + + color: black; + background-color: rgba(255, 255, 255, 0.7); + border-radius: 1em; + padding: 1em; + text-align: center; +} + +.message-tray-indicator-glow { + height: 4px; + background-gradient-start: rgba(255, 255, 255, 0); + background-gradient-end: rgba(255, 255, 255, 1); + background-gradient-direction: vertical; +} + /* OSD */ .osd-window { text-align: center; diff --git a/js/ui/layout.js b/js/ui/layout.js index 67c0611e5..9e5a4a1b6 100644 --- a/js/ui/layout.js +++ b/js/ui/layout.js @@ -22,11 +22,6 @@ const KEYBOARD_ANIMATION_TIME = 0.15; const BACKGROUND_FADE_ANIMATION_TIME = 1.0; const DEFAULT_BACKGROUND_COLOR = Clutter.Color.from_pixel(0x2e3436ff); -// The message tray takes this much pressure -// in the pressure barrier at once to release it. -const MESSAGE_TRAY_PRESSURE_THRESHOLD = 250; // pixels -const MESSAGE_TRAY_PRESSURE_TIMEOUT = 1000; // ms - const HOT_CORNER_PRESSURE_THRESHOLD = 100; // pixels const HOT_CORNER_PRESSURE_TIMEOUT = 1000; // ms @@ -150,7 +145,6 @@ const LayoutManager = new Lang.Class({ this._keyboardIndex = -1; this._rightPanelBarrier = null; - this._trayBarrier = null; this._inOverview = false; this._updateRegionIdle = 0; @@ -210,7 +204,6 @@ const LayoutManager = new Lang.Class({ this.trayBox = new St.Widget({ name: 'trayBox', layout_manager: new Clutter.BinLayout() }); this.addChrome(this.trayBox); - this._setupTrayPressure(); this.modalDialogGroup = new St.Widget({ name: 'modalDialogGroup', layout_manager: new Clutter.BinLayout() }); @@ -449,50 +442,9 @@ const LayoutManager = new Lang.Class({ } }, - _setupTrayPressure: function() { - this._trayPressure = new PressureBarrier(MESSAGE_TRAY_PRESSURE_THRESHOLD, - MESSAGE_TRAY_PRESSURE_TIMEOUT, - Shell.KeyBindingMode.NORMAL | - Shell.KeyBindingMode.OVERVIEW); - this._trayPressure.setEventFilter(this._trayBarrierEventFilter); - this._trayPressure.connect('trigger', function(barrier) { - if (Main.layoutManager.bottomMonitor.inFullscreen) - return; - - Main.messageTray.openTray(); - }); - }, - - _updateTrayBarrier: function() { - let monitor = this.bottomMonitor; - - if (this._trayBarrier) { - this._trayPressure.removeBarrier(this._trayBarrier); - this._trayBarrier.destroy(); - this._trayBarrier = null; - } - - this._trayBarrier = new Meta.Barrier({ display: global.display, - x1: monitor.x, x2: monitor.x + monitor.width, - y1: monitor.y + monitor.height, y2: monitor.y + monitor.height, - directions: Meta.BarrierDirection.NEGATIVE_Y }); - this._trayPressure.addBarrier(this._trayBarrier); - }, - - _trayBarrierEventFilter: function(event) { - // Throw out all events where the pointer was grabbed by another - // client, as the client that grabbed the pointer expects to have - // complete control over it - if (event.grabbed && Main.modalCount == 0) - return true; - - return false; - }, - _monitorsChanged: function() { this._updateMonitors(); this._updateBoxes(); - this._updateTrayBarrier(); this._updateHotCorners(); this._updateBackgrounds(); this._updateFullscreen(); @@ -1096,10 +1048,10 @@ const HotCorner = new Lang.Class({ this._setupFallbackCornerIfNeeded(layoutManager); - this._pressureBarrier = new PressureBarrier(HOT_CORNER_PRESSURE_THRESHOLD, - HOT_CORNER_PRESSURE_TIMEOUT, - Shell.KeyBindingMode.NORMAL | - Shell.KeyBindingMode.OVERVIEW); + this._pressureBarrier = new TriggerablePressureBarrier(HOT_CORNER_PRESSURE_THRESHOLD, + HOT_CORNER_PRESSURE_TIMEOUT, + Shell.KeyBindingMode.NORMAL | + Shell.KeyBindingMode.OVERVIEW); this._pressureBarrier.connect('trigger', Lang.bind(this, this._toggleOverview)); // Cache the three ripples instead of dynamically creating and destroying them. @@ -1277,14 +1229,12 @@ const PressureBarrier = new Lang.Class({ Name: 'PressureBarrier', _init: function(threshold, timeout, keybindingMode) { - this._threshold = threshold; - this._timeout = timeout; + this.threshold = threshold; + this.timeout = timeout; this._keybindingMode = keybindingMode; this._barriers = []; this._eventFilter = null; - - this._isTriggered = false; - this._reset(); + this.reset(); }, addBarrier: function(barrier) { @@ -1313,10 +1263,10 @@ const PressureBarrier = new Lang.Class({ this._eventFilter = filter; }, - _reset: function() { + reset: function() { this._barrierEvents = []; - this._currentPressure = 0; this._lastTime = 0; + this.currentPressure = 0; }, _isHorizontal: function(barrier) { @@ -1337,12 +1287,21 @@ const PressureBarrier = new Lang.Class({ return Math.abs(event.dy); }, + get currentPressure() { + return this._currentPressure; + }, + + set currentPressure(value) { + this._currentPressure = value; + this.emit('pressure-changed'); + }, + _trimBarrierEvents: function() { // Events are guaranteed to be sorted in time order from // oldest to newest, so just look for the first old event, // and then chop events after that off. let i = 0; - let threshold = this._lastTime - this._timeout; + let threshold = this._lastTime - this.timeout; while (i < this._barrierEvents.length) { let [time, distance] = this._barrierEvents[i]; @@ -1355,21 +1314,75 @@ const PressureBarrier = new Lang.Class({ for (i = 0; i < firstNewEvent; i++) { let [time, distance] = this._barrierEvents[i]; - this._currentPressure -= distance; + this.currentPressure = distance; } this._barrierEvents = this._barrierEvents.slice(firstNewEvent); }, _onBarrierLeft: function(barrier, event) { - this._reset(); + this.reset(); + }, + + _shouldUseEvent: function(barrier, event) { + if (this._eventFilter && this._eventFilter(event)) + return false; + + // Throw out all events not in the proper keybinding mode + if (!(this._keybindingMode & Main.keybindingMode)) + return false; + + let slide = this._getDistanceAlongBarrier(barrier, event); + let distance = this._getDistanceAcrossBarrier(barrier, event); + + // Throw out events where the cursor is move more + // along the axis of the barrier than moving with + // the barrier. + if (slide > distance) + return false; + + return true; + }, + + _appendEvent: function(barrier, event) { + let distance = this._getDistanceAcrossBarrier(barrier, event); + + this._lastTime = event.time; + + this._trimBarrierEvents(); + distance = Math.min(15, distance); + + this._barrierEvents.push([event.time, distance]); + this.currentPressure += distance; + }, + + _onBarrierHit: function(barrier, event) { + if (!this._shouldUseEvent(barrier, event)) + return; + + this._appendEvent(barrier, event); + } +}); +Signals.addSignalMethods(PressureBarrier.prototype); + +const TriggerablePressureBarrier = new Lang.Class({ + Name: 'TriggerablePressureBarrier', + Extends: PressureBarrier, + + _init: function(threshold, timeout, keybindingMode) { + this.parent(threshold, timeout, keybindingMode); this._isTriggered = false; }, _trigger: function() { this._isTriggered = true; this.emit('trigger'); - this._reset(); + this.reset(); + }, + + _onBarrierLeft: function() { + this.parent(); + this._isTriggered = false; }, _onBarrierHit: function(barrier, event) { @@ -1378,37 +1391,17 @@ const PressureBarrier = new Lang.Class({ if (this._isTriggered) return; - if (this._eventFilter && this._eventFilter(event)) + if (!this._shouldUseEvent(barrier, event)) return; - // Throw out all events not in the proper keybinding mode - if (!(this._keybindingMode & Main.keybindingMode)) - return; - - let slide = this._getDistanceAlongBarrier(barrier, event); - let distance = this._getDistanceAcrossBarrier(barrier, event); - - if (distance >= this._threshold) { + if (distance >= this.threshold) { this._trigger(); return; } - // Throw out events where the cursor is move more - // along the axis of the barrier than moving with - // the barrier. - if (slide > distance) - return; + this._appendEvent(barrier, event); - this._lastTime = event.time; - - this._trimBarrierEvents(); - distance = Math.min(15, distance); - - this._barrierEvents.push([event.time, distance]); - this._currentPressure += distance; - - if (this._currentPressure >= this._threshold) + if (this.currentPressure >= this.threshold) this._trigger(); - } + }, }); -Signals.addSignalMethods(PressureBarrier.prototype); diff --git a/js/ui/messageTray.js b/js/ui/messageTray.js index e01b376f3..3bd0b99d9 100644 --- a/js/ui/messageTray.js +++ b/js/ui/messageTray.js @@ -19,6 +19,7 @@ const BoxPointer = imports.ui.boxpointer; const CtrlAltTab = imports.ui.ctrlAltTab; const GnomeSession = imports.misc.gnomeSession; const GrabHelper = imports.ui.grabHelper; +const Layout = imports.ui.layout; const Lightbox = imports.ui.lightbox; const Main = imports.ui.main; const PointerWatcher = imports.ui.pointerWatcher; @@ -48,6 +49,9 @@ const TRAY_DWELL_CHECK_INTERVAL = 100; // ms const IDLE_TIME = 1000; +const MESSAGE_TRAY_PRESSURE_THRESHOLD = 250; // pixels +const MESSAGE_TRAY_PRESSURE_TIMEOUT = 1000; // ms + const State = { HIDDEN: 0, SHOWING: 1, @@ -1500,6 +1504,118 @@ const MessageTrayMenuButton = new Lang.Class({ }, }); +const MessageTrayIndicator = new Lang.Class({ + Name: 'MessageTrayIndicator', + + _init: function(tray) { + this._tray = tray; + + this.actor = new St.BoxLayout({ style_class: 'message-tray-indicator', + reactive: true, + track_hover: true, + vertical: true, + x_expand: true, + y_expand: true, + y_align: Clutter.ActorAlign.START }); + this.actor.connect('notify::height', Lang.bind(this, function() { + this.actor.translation_y = -this.actor.height; + })); + this.actor.connect('button-press-event', Lang.bind(this, function() { + this._tray.openTray(); + this._pressureBarrier.reset(); + })); + + this._count = new St.Label({ style_class: 'message-tray-indicator-count', + x_expand: true, + x_align: Clutter.ActorAlign.CENTER }); + this.actor.add_child(this._count); + + this._tray.connect('indicator-count-updated', Lang.bind(this, this._syncCount)); + this._syncCount(); + + this._glow = new St.Widget({ style_class: 'message-tray-indicator-glow', + x_expand: true }); + this.actor.add_child(this._glow); + + this._pressureBarrier = new Layout.PressureBarrier(MESSAGE_TRAY_PRESSURE_THRESHOLD, + MESSAGE_TRAY_PRESSURE_TIMEOUT, + Shell.KeyBindingMode.NORMAL | + Shell.KeyBindingMode.OVERVIEW); + this._pressureBarrier.setEventFilter(this._barrierEventFilter); + Main.layoutManager.connect('monitors-changed', Lang.bind(this, this._updateBarrier)); + this._updateBarrier(); + + this._pressureBarrier.connect('pressure-changed', Lang.bind(this, this._updatePressure)); + this._pressureValue = 0; + this._syncGlow(); + }, + + _updateBarrier: function() { + let monitor = Main.layoutManager.bottomMonitor; + + if (this._barrier) { + this._pressureBarrier.removeBarrier(this._trayBarrier); + this._barrier.destroy(); + this._barrier = null; + } + + this._barrier = new Meta.Barrier({ display: global.display, + x1: monitor.x, x2: monitor.x + monitor.width, + y1: monitor.y + monitor.height, y2: monitor.y + monitor.height, + directions: Meta.BarrierDirection.NEGATIVE_Y }); + this._pressureBarrier.addBarrier(this._barrier); + }, + + _trayBarrierEventFilter: function(event) { + // Throw out all events where the pointer was grabbed by another + // client, as the client that grabbed the pointer expects to have + // complete control over it + if (event.grabbed && Main.modalCount == 0) + return true; + + if (this._tray.hasVisibleNotification()) + return true; + + return false; + }, + + _syncCount: function() { + let count = this._tray.indicatorCount; + this._count.visible = (count > 0); + this._count.text = '' + count; + }, + + _syncGlow: function() { + let value = this._pressureValue; + let percent = value / this._pressureBarrier.threshold; + this.actor.opacity = Math.min(percent * 255, 255); + this.actor.visible = (value > 0); + }, + + get pressureValue() { + return this._pressureValue; + }, + + set pressureValue(value) { + this._pressureValue = value; + this._syncGlow(); + }, + + _updatePressure: function() { + let value = this._pressureBarrier.currentPressure; + this.pressureValue = value; + if (value > 0) { + Tweener.removeTweens(this); + Tweener.addTween(this, { time: this._pressureBarrier.timeout / 1000, + pressureValue: 0 }); + } + }, + + destroy: function() { + this.actor.destroy(); + }, +}); + const MessageTray = new Lang.Class({ Name: 'MessageTray', @@ -1683,6 +1799,11 @@ const MessageTray = new Lang.Class({ this._messageTrayMenuButton = new MessageTrayMenuButton(this); this.actor.add_actor(this._messageTrayMenuButton.actor); + + this._indicator = new MessageTrayIndicator(this); + Main.layoutManager.trayBox.add_child(this._indicator.actor); + Main.layoutManager.trackChrome(this._indicator.actor); + this._grabHelper.addActor(this._indicator.actor); }, close: function() { @@ -2149,6 +2270,10 @@ const MessageTray = new Lang.Class({ this._updateState(); }, + hasVisibleNotification: function() { + return this._notificationState != State.HIDDEN; + }, + // All of the logic for what happens when occurs here; the various // event handlers merely update variables such as // 'this._pointerInNotification', 'this._traySummoned', etc, and diff --git a/js/ui/overview.js b/js/ui/overview.js index 5e000cd90..1754b6134 100644 --- a/js/ui/overview.js +++ b/js/ui/overview.js @@ -263,8 +263,6 @@ const Overview = new Lang.Class({ this._overview.add(this._controls.actor, { y_fill: true, expand: true }); this._controls.actor.connect('scroll-event', Lang.bind(this, this._onScrollEvent)); - this._stack.add_actor(this._controls.indicatorActor); - // TODO - recalculate everything when desktop size changes this.dashIconSize = this._dash.iconSize; this._dash.connect('icon-size-changed', diff --git a/js/ui/overviewControls.js b/js/ui/overviewControls.js index 823ae90c6..09e2073e6 100644 --- a/js/ui/overviewControls.js +++ b/js/ui/overviewControls.js @@ -396,87 +396,6 @@ const DashSpacer = new Lang.Class({ } }); -const MessagesIndicator = new Lang.Class({ - Name: 'MessagesIndicator', - - _init: function(viewSelector) { - this._count = 0; - this._sources = []; - this._viewSelector = viewSelector; - - this._container = new St.BoxLayout({ style_class: 'messages-indicator-contents', - reactive: true, - track_hover: true, - x_expand: true, - y_expand: true, - x_align: Clutter.ActorAlign.CENTER }); - - this._icon = new St.Icon({ icon_name: 'user-idle-symbolic', - icon_size: 16 }); - this._container.add_actor(this._icon); - - this._label = new St.Label(); - this._container.add_actor(this._label); - - this._highlight = new St.Widget({ style_class: 'messages-indicator-highlight', - x_expand: true, - y_expand: true, - y_align: Clutter.ActorAlign.END, - visible: false }); - - this._container.connect('notify::hover', Lang.bind(this, - function() { - this._highlight.visible = this._container.hover; - })); - - let clickAction = new Clutter.ClickAction(); - this._container.add_action(clickAction); - clickAction.connect('clicked', Lang.bind(this, - function() { - Main.messageTray.openTray(); - })); - - Main.messageTray.connect('showing', Lang.bind(this, - function() { - this._highlight.visible = false; - this._container.hover = false; - })); - - let layout = new Clutter.BinLayout(); - this.actor = new St.Widget({ layout_manager: layout, - style_class: 'messages-indicator', - y_expand: true, - y_align: Clutter.ActorAlign.END, - visible: false }); - this.actor.add_actor(this._container); - this.actor.add_actor(this._highlight); - - Main.messageTray.connect('indicator-count-updated', Lang.bind(this, this._sync)); - this._sync(); - - this._viewSelector.connect('page-changed', Lang.bind(this, this._updateVisibility)); - Main.overview.connect('showing', Lang.bind(this, this._updateVisibility)); - }, - - _sync: function() { - let count = Main.messageTray.indicatorCount; - this._count = count; - this._label.text = ngettext("%d new message", - "%d new messages", - count).format(count); - - this._icon.visible = Main.messageTray.hasChatSources; - this._updateVisibility(); - }, - - _updateVisibility: function() { - let activePage = this._viewSelector.getActivePage(); - let visible = ((this._count > 0) && (activePage == ViewSelector.ViewPage.WINDOWS)); - - this.actor.visible = visible; - } -}); - const ControlsLayout = new Lang.Class({ Name: 'ControlsLayout', Extends: Clutter.BinLayout, @@ -505,9 +424,6 @@ const ControlsManager = new Lang.Class({ this.viewSelector.connect('page-changed', Lang.bind(this, this._setVisibility)); this.viewSelector.connect('page-empty', Lang.bind(this, this._onPageEmpty)); - this._indicator = new MessagesIndicator(this.viewSelector); - this.indicatorActor = this._indicator.actor; - let layout = new ControlsLayout(); this.actor = new St.Widget({ layout_manager: layout, reactive: true,