Compare commits

..

12 Commits

Author SHA1 Message Date
Jasper St. Pierre
672a4a56bd messageTray: Remove an unused variable 2014-06-04 12:58:34 -04:00
Jasper St. Pierre
50421ede37 messageTray: Add notification close button inside notification
And remove the old close button outside it.
2014-06-04 12:39:34 -04:00
Jasper St. Pierre
826a7fa02a messageTray: Implement new notification designs
Rather than use an StTable, a custom ShellGenericContainer, and plenty
of hacky style classes, replace them all with standard BoxLayouts and
Bins.

Remove the customContent parameter in favor of subclasses setting the
child of this._bodyBin instead.

With this comes a whole new notification implementation to implement
the new notification designs.

We lose a few of the fancy features like showing the first part of
the body, ellipsized, next the banner when it will fit, and some other
layout logic. But since the design of notifications is changing
substantially anyways, I don't feel too bad...
2014-06-04 12:39:23 -04:00
Jasper St. Pierre
d7844bc7f4 messageTray: Glue the notification to the bottom of the screen, always
We'll animate notifications popping up with another system soon enough,
instead. The idea here is that instead of carefully animating the Y
position of the notificationWidget when a notification updates, we
simply animate the height of the new actor inside the notification.
This will fix some of the awkward updates where instead of the
notification content expanding, we see the buttons or action area
pushed off the edge of the screen...

Animations that happen as a result of adding something new to the
notification or expanding it should be done by tweeing the new actors
in inside the notification.
2014-06-03 14:48:48 -04:00
Jasper St. Pierre
485444a451 messageTray: Remove addBody as a public API
As it's unused.
2014-06-03 14:48:48 -04:00
Jasper St. Pierre
e5318d5235 messageTray: Remove support for notifications with images
This sufficiently complicates the code, and won't fit in the
new design.
2014-06-03 14:48:48 -04:00
Jasper St. Pierre
e6e15489aa messageTray: Remove support for resident notifications
Now the only resident notification is a chat notification. The convenient
thing about this special-case behavior is that there's already special-case
code for it and the shell, and we always know that a chat notification will
always be 1:1 with its chat source.
2014-06-03 14:48:48 -04:00
Jasper St. Pierre
0951d36e8f notificationDaemon: Remove support for resident notifications
They're not really an API that has caught on, and not really one
we want to support, either.
2014-06-03 14:48:48 -04:00
Jasper St. Pierre
c38939902f notificationDaemon: Remove the special-case hack for system tray icons
Nothing in the system actually has a standard system tray icon anymore,
so this hack isn't necessary.
2014-06-03 14:48:47 -04:00
Jasper St. Pierre
97974a3f2a telepathyClient: Remove all the fancy features
This can be done with another app, like Empathy or Chat.
2014-06-03 14:48:47 -04:00
Jasper St. Pierre
8b6cab741d autorunManager: Remove the resident "Removable Devices" notification
Users aren't usually the best at obeying the rules, and systems can
deal with hotplug without ejecting first.

https://bugzilla.gnome.org/show_bug.cgi?id=719857
2014-06-03 14:48:47 -04:00
Jasper St. Pierre
2aad0eac31 messageTray: Don't use a member variable when we can use a local
Tiny cleanup.
2014-06-03 14:26:59 -04:00
7 changed files with 1273 additions and 618 deletions

View File

@@ -1491,54 +1491,58 @@ StScrollBar StButton#vhandle:active {
/* Message Tray */
.notification-drawer {
background: rgba(0,0,0,0.8);
padding: 1em;
border: 2px solid #4c4c4c;
border-bottom: 0px;
border-radius: 6px 6px 0 0;
#message-tray {
background: #2e3436 url(message-tray-background.png);
background-repeat: repeat;
height: 72px;
}
.system-tray-icons,
.notification-drawer-footer-actions {
spacing: 0.5em;
.message-tray-summary {
height: 72px;
}
.system-tray-icon-button,
.notification-drawer-button {
border-radius: 4px;
border: 1px solid #4c4c4c;
padding: 4px;
.message-tray-menu-button StIcon {
padding: 0 20px;
color: #aaaaaa;
icon-size: 24px;
}
.system-tray-icon-button {
width: 22px;
height: 22px;
}
.notification-drawer-button StIcon {
icon-size: 22px;
}
.system-tray-icon-button:hover,
.notification-drawer-button:hover {
background: 1px solid #4c4c4c;
.message-tray-menu-button:hover StIcon,
.message-tray-menu-button:active StIcon,
.message-tray-menu-button:focus StIcon {
color: #eeeeee;
}
.url-highlighter {
link-color: #ccccff;
}
.notification, #notification-container, .notification-drawer {
.no-messages-label {
color: #999999;
}
.notification, #notification-container {
font-size: 11pt;
width: 34em;
}
.notification-main-button,
.notification-button {
background: rgba(0,0,0,0.9);
}
.notification-main-button {
border-radius: 10px 10px 0px 0px;
}
.notification-main-content {
padding: 8px;
spacing: 8px;
border-radius: 10px 10px 0px 0px;
background: rgba(0,0,0,0.8);
}
.notification-close-button {
padding: 8px;
border-radius: 4px;
}
.notification-action-area {
@@ -1547,7 +1551,6 @@ StScrollBar StButton#vhandle:active {
.notification-action-area,
.notification-button {
background: rgba(0,0,0,0.8);
border-top: 1px solid #666;
}
@@ -1556,10 +1559,13 @@ StScrollBar StButton#vhandle:active {
border-right: 1px solid #666;
}
.notification-button:hover {
background: rgba(255,255,255,0.3);
.notification-main-button:hover,
.notification-button:hover,
.notification-close-button:hover {
background: rgba(100,100,100,0.9);
}
.notification-main-button:active,
.notification-button:active {
background: rgba(255,255,255,0.1);
}
@@ -1580,6 +1586,40 @@ StScrollBar StButton#vhandle:active {
padding-right: 6px;
}
.summary-boxpointer {
-arrow-border-radius: 15px;
-arrow-background-color: rgba(0,0,0,0.9);
-arrow-base: 36px;
-arrow-rise: 18px;
color: white;
-boxpointer-gap: 4px;
}
.summary-boxpointer .notification {
border-radius: 9px;
background: rgba(0,0,0,0) !important;
padding-bottom: 12px;
}
.summary-boxpointer #summary-right-click-menu {
padding-top: 12px;
padding-bottom: 12px;
}
.summary-notification-stack-scrollview {
max-height: 18em;
padding-top: 8px;
padding-bottom: 8px;
}
.summary-notification-stack-scrollview:ltr {
padding-right: 8px;
}
.summary-notification-stack-scrollview:rtl {
padding-left: 8px;
}
.notification-scrollview {
max-height: 10em;
-st-vfade-offset: 24px;
@@ -1700,28 +1740,6 @@ 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;

View File

@@ -735,11 +735,7 @@ const ChatNotification = new Lang.Class({
}
this._lastMessageBox = new St.BoxLayout({ vertical: false });
let revealer = new MessageTray.Revealer(body);
this._lastMessageBox.add(revealer, props.childProps);
revealer.show(true);
this._lastMessageBox.add(body, props.childProps);
this._bodyBox.add_child(this._lastMessageBox);
let timestamp = props.timestamp;

View File

@@ -22,6 +22,11 @@ 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
@@ -145,6 +150,7 @@ const LayoutManager = new Lang.Class({
this._keyboardIndex = -1;
this._rightPanelBarrier = null;
this._trayBarrier = null;
this._inOverview = false;
this._updateRegionIdle = 0;
@@ -204,6 +210,7 @@ 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() });
@@ -442,9 +449,50 @@ 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();
@@ -1048,10 +1096,10 @@ const HotCorner = new Lang.Class({
this._setupFallbackCornerIfNeeded(layoutManager);
this._pressureBarrier = new TriggerablePressureBarrier(HOT_CORNER_PRESSURE_THRESHOLD,
HOT_CORNER_PRESSURE_TIMEOUT,
Shell.KeyBindingMode.NORMAL |
Shell.KeyBindingMode.OVERVIEW);
this._pressureBarrier = new PressureBarrier(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.
@@ -1229,12 +1277,14 @@ 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.reset();
this._isTriggered = false;
this._reset();
},
addBarrier: function(barrier) {
@@ -1263,10 +1313,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) {
@@ -1287,21 +1337,12 @@ 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];
@@ -1314,75 +1355,21 @@ 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();
},
_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._reset();
this._isTriggered = false;
},
_trigger: function() {
this._isTriggered = true;
this.emit('trigger');
this.reset();
},
_onBarrierLeft: function() {
this.parent();
this._isTriggered = false;
this._reset();
},
_onBarrierHit: function(barrier, event) {
@@ -1391,17 +1378,37 @@ const TriggerablePressureBarrier = new Lang.Class({
if (this._isTriggered)
return;
if (!this._shouldUseEvent(barrier, event))
if (this._eventFilter && this._eventFilter(event))
return;
if (distance >= this.threshold) {
// 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) {
this._trigger();
return;
}
this._appendEvent(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;
if (this.currentPressure >= this.threshold)
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)
this._trigger();
},
}
});
Signals.addSignalMethods(PressureBarrier.prototype);

File diff suppressed because it is too large Load Diff

View File

@@ -105,10 +105,16 @@ const FdoNotificationDaemon = new Lang.Class({
this._nextNotificationId = 1;
this._trayManager = new Shell.TrayManager();
this._trayIconAddedId = this._trayManager.connect('tray-icon-added', Lang.bind(this, this._onTrayIconAdded));
this._trayIconRemovedId = this._trayManager.connect('tray-icon-removed', Lang.bind(this, this._onTrayIconRemoved));
Shell.WindowTracker.get_default().connect('notify::focus-app',
Lang.bind(this, this._onFocusAppChanged));
Main.overview.connect('hidden',
Lang.bind(this, this._onFocusAppChanged));
this._trayManager.manage_screen(global.screen, Main.messageTray.actor);
},
_imageForNotificationData: function(hints) {
@@ -149,23 +155,28 @@ const FdoNotificationDaemon = new Lang.Class({
return null;
},
_lookupSource: function(title, pid) {
_lookupSource: function(title, pid, trayIcon) {
for (let i = 0; i < this._sources.length; i++) {
let source = this._sources[i];
if (source.pid == pid && source.initialTitle == title)
if (source.pid == pid &&
(source.initialTitle == title || source.trayIcon || trayIcon))
return source;
}
return null;
},
// Returns the source associated with ndata.notification if it is set.
// If the existing or requested source is associated with a tray icon
// and passed in pid matches a pid of an existing source, the title
// match is ignored to enable representing a tray icon and notifications
// from the same application with a single source.
//
// If no existing source is found, a new source is created as long as
// pid is provided.
//
// Either a pid or ndata.notification is needed to retrieve or
// create a source.
_getSource: function(title, pid, ndata, sender) {
_getSource: function(title, pid, ndata, sender, trayIcon) {
if (!pid && !(ndata && ndata.notification))
return null;
@@ -176,13 +187,13 @@ const FdoNotificationDaemon = new Lang.Class({
if (ndata && ndata.notification)
return ndata.notification.source;
let source = this._lookupSource(title, pid);
let source = this._lookupSource(title, pid, trayIcon);
if (source) {
source.setTitle(title);
return source;
}
let source = new FdoNotificationDaemonSource(title, pid, sender, ndata ? ndata.hints['desktop-entry'] : null);
let source = new FdoNotificationDaemonSource(title, pid, sender, trayIcon, ndata ? ndata.hints['desktop-entry'] : null);
this._sources.push(source);
source.connect('destroy', Lang.bind(this, function() {
@@ -474,15 +485,26 @@ const FdoNotificationDaemon = new Lang.Class({
this._dbusImpl.emit_signal('ActionInvoked',
GLib.Variant.new('(us)', [id, action]));
},
_onTrayIconAdded: function(o, icon) {
let source = this._getSource(icon.title || icon.wm_class || C_("program", "Unknown"), icon.pid, null, null, icon);
},
_onTrayIconRemoved: function(o, icon) {
let source = this._lookupSource(null, icon.pid, true);
if (source)
source.destroy();
}
});
const FdoNotificationDaemonSource = new Lang.Class({
Name: 'FdoNotificationDaemonSource',
Extends: MessageTray.Source,
_init: function(title, pid, sender, appId) {
_init: function(title, pid, sender, trayIcon, appId) {
// Need to set the app before chaining up, so
// methods called from the parent constructor can find it
this.trayIcon = trayIcon;
this.pid = pid;
this.app = this._getApp(appId);
@@ -502,6 +524,12 @@ const FdoNotificationDaemonSource = new Lang.Class({
Lang.bind(this, this._onNameVanished));
else
this._nameWatcherId = 0;
if (this.trayIcon) {
// Try again finding the app, using the WM_CLASS from the tray icon
this._setSummaryIcon(this.trayIcon);
this.useNotificationIcon = false;
}
},
_createPolicy: function() {
@@ -517,15 +545,17 @@ const FdoNotificationDaemonSource = new Lang.Class({
// Destroy the notification source when its sender is removed from DBus.
// Only do so if this.app is set to avoid removing "notify-send" sources, senders
// of which аre removed from DBus immediately.
if (this.app)
// Sender being removed from DBus would normally result in a tray icon being removed,
// so allow the code path that handles the tray icon being removed to handle that case.
if (!this.trayIcon && this.app)
this.destroy();
},
processNotification: function(notification, gicon) {
if (gicon)
this._gicon = gicon;
this.iconUpdated();
if (!this.trayIcon)
this.iconUpdated();
let tracker = Shell.WindowTracker.get_default();
if (this.app && tracker.focus_app == this.app)
@@ -534,6 +564,29 @@ const FdoNotificationDaemonSource = new Lang.Class({
this.notify(notification);
},
handleSummaryClick: function(button) {
if (!this.trayIcon)
return false;
let event = Clutter.get_current_event();
// Left clicks are passed through only where there aren't unacknowledged
// notifications, so it possible to open them in summary mode; right
// clicks are always forwarded, as the right click menu is not useful for
// tray icons
if (button == 1 &&
this.notifications.length > 0)
return false;
let id = global.stage.connect('deactivate', Lang.bind(this, function () {
global.stage.disconnect(id);
this.trayIcon.click(event);
}));
Main.overview.hide();
return true;
},
_getApp: function(appId) {
let app;
@@ -541,6 +594,16 @@ const FdoNotificationDaemonSource = new Lang.Class({
if (app != null)
return app;
if (this.trayIcon) {
app = Shell.AppSystem.get_default().lookup_startup_wmclass(this.trayIcon.wm_class);
if (app != null)
return app;
app = Shell.AppSystem.get_default().lookup_desktop_wmclass(this.trayIcon.wm_class);
if (app != null)
return app;
}
if (appId) {
app = Shell.AppSystem.get_default().lookup_app(appId + '.desktop');
if (app != null)
@@ -566,7 +629,8 @@ const FdoNotificationDaemonSource = new Lang.Class({
},
_lastNotificationRemoved: function() {
this.destroy();
if (!this.trayIcon)
this.destroy();
},
openApp: function() {
@@ -587,7 +651,11 @@ const FdoNotificationDaemonSource = new Lang.Class({
},
createIcon: function(size) {
if (this.app) {
if (this.trayIcon) {
return new Clutter.Clone({ width: size,
height: size,
source: this.trayIcon });
} else if (this.app) {
return this.app.create_icon_texture(size);
} else if (this._gicon) {
return new St.Icon({ gicon: this._gicon,

View File

@@ -263,6 +263,8 @@ 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',

View File

@@ -396,6 +396,111 @@ 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('source-added', Lang.bind(this, this._onSourceAdded));
Main.messageTray.connect('source-removed', Lang.bind(this, this._onSourceRemoved));
let sources = Main.messageTray.getSources();
sources.forEach(Lang.bind(this, function(source) { this._onSourceAdded(null, source); }));
this._viewSelector.connect('page-changed', Lang.bind(this, this._updateVisibility));
Main.overview.connect('showing', Lang.bind(this, this._updateVisibility));
},
_onSourceAdded: function(tray, source) {
if (source.trayIcon)
return;
source.connect('count-updated', Lang.bind(this, this._updateCount));
this._sources.push(source);
this._updateCount();
},
_onSourceRemoved: function(tray, source) {
this._sources.splice(this._sources.indexOf(source), 1);
this._updateCount();
},
_updateCount: function() {
let count = 0;
let hasChats = false;
this._sources.forEach(Lang.bind(this,
function(source) {
count += source.indicatorCount;
hasChats |= source.isChat;
}));
this._count = count;
this._label.text = ngettext("%d new message",
"%d new messages",
count).format(count);
this._icon.visible = hasChats;
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,
@@ -424,6 +529,9 @@ 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,