diff --git a/js/ui/messageTray.js b/js/ui/messageTray.js index 554163815..502b9f30a 100644 --- a/js/ui/messageTray.js +++ b/js/ui/messageTray.js @@ -1344,9 +1344,7 @@ const MessageTray = new Lang.Class({ this._presence = new GnomeSession.Presence(Lang.bind(this, function(proxy, error) { this._onStatusChanged(proxy.status); })); - this._userStatus = GnomeSession.PresenceStatus.AVAILABLE; this._busy = false; - this._backFromAway = false; this._presence.connectSignal('StatusChanged', Lang.bind(this, function(proxy, senderName, [status]) { this._onStatusChanged(status); })); @@ -1392,6 +1390,12 @@ const MessageTray = new Lang.Class({ this._summaryItemTitleWidth = 0; this._pointerBarrier = 0; + this._unseenNotifications = []; + this._idleMonitorWatchId = 0; + this._backFromAway = false; + + this.idleMonitor = new Shell.IdleMonitor(); + // To simplify the summary item animation code, we pretend // that there's an invisible SummaryItem to the left of the // leftmost real summary item, and that it's expanded when all @@ -1635,6 +1639,10 @@ const MessageTray = new Lang.Class({ }, _onNotificationDestroy: function(notification) { + let unseenNotificationsIndex = this._unseenNotifications.indexOf(notification); + if (unseenNotificationsIndex != -1) + this._unseenNotifications.splice(unseenNotificationsIndex, 1); + if (this._notification == notification && (this._notificationState == State.SHOWN || this._notificationState == State.SHOWING)) { this._updateNotificationTimeout(0); this._notificationRemoved = true; @@ -1918,16 +1926,10 @@ const MessageTray = new Lang.Class({ }, _onStatusChanged: function(status) { - this._backFromAway = (this._userStatus == GnomeSession.PresenceStatus.IDLE && this._userStatus != status); - this._userStatus = status; - if (status == GnomeSession.PresenceStatus.BUSY) { // remove notification and allow the summary to be closed now this._updateNotificationTimeout(0); - if (this._summaryTimeoutId) { - Mainloop.source_remove(this._summaryTimeoutId); - this._summaryTimeoutId = 0; - } + this._unsetSummaryTimeout(); this._busy = true; } else if (status != GnomeSession.PresenceStatus.IDLE) { // We preserve the previous value of this._busy if the status turns to IDLE @@ -1957,6 +1959,7 @@ const MessageTray = new Lang.Class({ this._pointerInTray = false; this._pointerInSummary = false; this._updateNotificationTimeout(0); + this._unsetSummaryTimeout(); this._updateState(); } return false; @@ -1967,6 +1970,7 @@ const MessageTray = new Lang.Class({ this._pointerInTray = false; this._pointerInSummary = false; this._updateNotificationTimeout(0); + this._unsetSummaryTimeout(); this._updateState(); }, @@ -2012,15 +2016,13 @@ const MessageTray = new Lang.Class({ || notificationsVisible; if (this._summaryState == State.HIDDEN && !mustHideSummary) { - if (this._backFromAway) { - // Immediately set this to false, so that we don't schedule a timeout later - this._backFromAway = false; - if (!this._busy) - this._showSummary(LONGER_SUMMARY_TIMEOUT); - } else if (notificationsDone && this._newSummaryItems.length > 0 && !this._busy) { - this._showSummary(SUMMARY_TIMEOUT); - } else if (summarySummoned) { + if (summarySummoned) { this._showSummary(0); + } else if (notificationsDone && !this._busy) { + if (this._backFromAway && this._unseenNotifications.length > 0) + this._showSummary(LONGER_SUMMARY_TIMEOUT); + else if (this._newSummaryItems.length > 0) + this._showSummary(SUMMARY_TIMEOUT); } } else if (this._summaryState == State.SHOWN) { if (!summaryPinned || mustHideSummary) @@ -2109,8 +2111,32 @@ const MessageTray = new Lang.Class({ }); }, + _onIdleMonitorWatch: function(monitor, id, userBecameIdle) { + this.idleMonitor.remove_watch(this._idleMonitorWatchId); + this._idleMonitorWatchId = 0; + + if (userBecameIdle) { + // The user became idle, which means the user was active while the notifications were + // shown and we can unset this._unseenNotifications . + this._unseenNotiications = []; + } else if (this._unseenNotifications.length == 1 && this._unseenNotifications[0] == this._notification) { + // The user became active while the only notification in this._unseenNotifications is being shown + // as this._notification , so we can unset this._unseenNotifications . + this._unseenNotifications = []; + } else { + // The user became active and we have one or more unseen notifications. We should show + // the message tray to the user to inform the user about the missed notifications. + this._backFromAway = true; + this._updateState(); + } + }, + _showNotification: function() { this._notification = this._notificationQueue.shift(); + this._unseenNotifications.push(this._notification); + if (this._idleMonitorWatchId == 0) + this._idleMonitorWatchId = this.idleMonitor.add_watch(1000, + Lang.bind(this, this._onIdleMonitorWatch)); this._notificationClickedId = this._notification.connect('done-displaying', Lang.bind(this, this._escapeTray)); this._notificationBin.child = this._notification.actor; @@ -2189,6 +2215,13 @@ const MessageTray = new Lang.Class({ Lang.bind(this, this._notificationTimeout)); }, + _unsetSummaryTimeout: function(timeout) { + if (this._summaryTimeoutId) { + Mainloop.source_remove(this._summaryTimeoutId); + this._summaryTimeoutId = 0; + } + }, + _notificationTimeout: function() { let [x, y, mods] = global.get_pointer(); if (y > this._lastSeenMouseY + 10 && !this.actor.hover) { @@ -2272,6 +2305,7 @@ const MessageTray = new Lang.Class({ }, _showSummary: function(timeout) { + this._updateSeenSummaryItems(); this._summaryBin.opacity = 0; this._summaryBin.y = this.actor.height; this._tween(this._summaryBin, '_summaryState', State.SHOWN, @@ -2286,8 +2320,6 @@ const MessageTray = new Lang.Class({ }, _showSummaryCompleted: function(timeout) { - this._newSummaryItems = []; - if (timeout != 0) { this._summaryTimeoutId = Mainloop.timeout_add(timeout * 1000, @@ -2302,6 +2334,7 @@ const MessageTray = new Lang.Class({ }, _hideSummary: function() { + this._updateSeenSummaryItems(); this._tween(this._summaryBin, '_summaryState', State.HIDDEN, { opacity: 0, time: ANIMATION_TIME, @@ -2309,13 +2342,20 @@ const MessageTray = new Lang.Class({ onComplete: this._hideSummaryCompleted, onCompleteScope: this, }); - this._newSummaryItems = []; }, _hideSummaryCompleted: function() { this._setExpandedSummaryItem(null); }, + _updateSeenSummaryItems: function() { + if (this._backFromAway) { + this._backFromAway = false; + this._unseenNotifications = []; + } + this._newSummaryItems = []; + }, + _showSummaryBoxPointer: function() { this._summaryBoxPointerItem = this._clickedSummaryItem; this._summaryBoxPointerContentUpdatedId = this._summaryBoxPointerItem.connect('content-updated', diff --git a/src/Makefile.am b/src/Makefile.am index b1f74a0f6..95f1a41cb 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -109,6 +109,7 @@ shell_public_headers_h = \ shell-generic-container.h \ shell-gtk-embed.h \ shell-global.h \ + shell-idle-monitor.h \ shell-mobile-providers.h \ shell-mount-operation.h \ shell-network-agent.h \ @@ -155,6 +156,7 @@ libgnome_shell_la_SOURCES = \ shell-generic-container.c \ shell-gtk-embed.c \ shell-global.c \ + shell-idle-monitor.c \ shell-keyring-prompt.h \ shell-keyring-prompt.c \ shell-mobile-providers.c \ diff --git a/src/shell-idle-monitor.c b/src/shell-idle-monitor.c new file mode 100644 index 000000000..22581d100 --- /dev/null +++ b/src/shell-idle-monitor.c @@ -0,0 +1,412 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Adapted from gnome-session/gnome-session/gs-idle-monitor.c + * + * Copyright (C) 2012 Red Hat, Inc. + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + * Authors: William Jon McCann + * + */ + +#include "config.h" + +#include +#include + +#include +#include + +#include +#include +#include + +#include "shell-idle-monitor.h" + +#define SHELL_IDLE_MONITOR_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), SHELL_TYPE_IDLE_MONITOR, ShellIdleMonitorPrivate)) + +struct ShellIdleMonitorPrivate +{ + Display *display; + + GHashTable *watches; + int sync_event_base; + XSyncCounter counter; +}; + +typedef struct +{ + Display *display; + guint id; + XSyncValue interval; + ShellIdleMonitorWatchFunc callback; + gpointer user_data; + GDestroyNotify notify; + XSyncAlarm xalarm_positive; + XSyncAlarm xalarm_negative; +} ShellIdleMonitorWatch; + +static guint32 watch_serial = 1; + +G_DEFINE_TYPE (ShellIdleMonitor, shell_idle_monitor, G_TYPE_OBJECT) + +static gint64 +_xsyncvalue_to_int64 (XSyncValue value) +{ + return ((guint64) XSyncValueHigh32 (value)) << 32 + | (guint64) XSyncValueLow32 (value); +} + +static XSyncValue +_int64_to_xsyncvalue (gint64 value) +{ + XSyncValue ret; + + XSyncIntsToValue (&ret, value, ((guint64)value) >> 32); + + return ret; +} + +static void +shell_idle_monitor_dispose (GObject *object) +{ + ShellIdleMonitor *monitor; + + monitor = SHELL_IDLE_MONITOR (object); + + if (monitor->priv->watches != NULL) { + g_hash_table_destroy (monitor->priv->watches); + monitor->priv->watches = NULL; + } + + G_OBJECT_CLASS (shell_idle_monitor_parent_class)->dispose (object); +} + +static gboolean +_find_alarm (gpointer key, + ShellIdleMonitorWatch *watch, + XSyncAlarm *alarm) +{ + /* g_debug ("Searching for %d in %d,%d", (int)*alarm, (int)watch->xalarm_positive, (int)watch->xalarm_negative); */ + if (watch->xalarm_positive == *alarm + || watch->xalarm_negative == *alarm) { + return TRUE; + } + return FALSE; +} + +static ShellIdleMonitorWatch * +find_watch_for_alarm (ShellIdleMonitor *monitor, + XSyncAlarm alarm) +{ + ShellIdleMonitorWatch *watch; + + watch = g_hash_table_find (monitor->priv->watches, + (GHRFunc)_find_alarm, + &alarm); + return watch; +} + +static void +handle_alarm_notify_event (ShellIdleMonitor *monitor, + XSyncAlarmNotifyEvent *alarm_event) +{ + ShellIdleMonitorWatch *watch; + gboolean condition; + + if (alarm_event->state == XSyncAlarmDestroyed) { + return; + } + + watch = find_watch_for_alarm (monitor, alarm_event->alarm); + + if (watch == NULL) { + /* g_debug ("Unable to find watch for alarm %d", (int)alarm_event->alarm); */ + return; + } + + /* g_debug ("Watch %d fired, idle time = %" G_GINT64_FORMAT, + watch->id, + _xsyncvalue_to_int64 (alarm_event->counter_value)); */ + + if (alarm_event->alarm == watch->xalarm_positive) { + condition = TRUE; + } else { + condition = FALSE; + } + + if (watch->callback != NULL) { + watch->callback (monitor, + watch->id, + condition, + watch->user_data); + } +} + +static GdkFilterReturn +xevent_filter (GdkXEvent *xevent, + GdkEvent *event, + ShellIdleMonitor *monitor) +{ + XEvent *ev; + XSyncAlarmNotifyEvent *alarm_event; + + ev = xevent; + if (ev->xany.type != monitor->priv->sync_event_base + XSyncAlarmNotify) { + return GDK_FILTER_CONTINUE; + } + + alarm_event = xevent; + + handle_alarm_notify_event (monitor, alarm_event); + + return GDK_FILTER_CONTINUE; +} + +static gboolean +init_xsync (ShellIdleMonitor *monitor) +{ + int sync_error_base; + int res; + int major; + int minor; + int i; + int ncounters; + XSyncSystemCounter *counters; + + res = XSyncQueryExtension (monitor->priv->display, + &monitor->priv->sync_event_base, + &sync_error_base); + if (! res) { + g_warning ("ShellIdleMonitor: Sync extension not present"); + return FALSE; + } + + res = XSyncInitialize (monitor->priv->display, &major, &minor); + if (! res) { + g_warning ("ShellIdleMonitor: Unable to initialize Sync extension"); + return FALSE; + } + + counters = XSyncListSystemCounters (monitor->priv->display, &ncounters); + for (i = 0; i < ncounters; i++) { + if (counters[i].name != NULL + && strcmp (counters[i].name, "IDLETIME") == 0) { + monitor->priv->counter = counters[i].counter; + break; + } + } + XSyncFreeSystemCounterList (counters); + + if (monitor->priv->counter == None) { + g_warning ("ShellIdleMonitor: IDLETIME counter not found"); + return FALSE; + } + + gdk_window_add_filter (NULL, (GdkFilterFunc)xevent_filter, monitor); + + return TRUE; +} + +static GObject * +shell_idle_monitor_constructor (GType type, + guint n_construct_properties, + GObjectConstructParam *construct_properties) +{ + ShellIdleMonitor *monitor; + + monitor = SHELL_IDLE_MONITOR (G_OBJECT_CLASS (shell_idle_monitor_parent_class)->constructor (type, + n_construct_properties, + construct_properties)); + + monitor->priv->display = GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()); + + if (! init_xsync (monitor)) { + g_object_unref (monitor); + return NULL; + } + + return G_OBJECT (monitor); +} + +static void +shell_idle_monitor_class_init (ShellIdleMonitorClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = shell_idle_monitor_dispose; + object_class->constructor = shell_idle_monitor_constructor; + + g_type_class_add_private (klass, sizeof (ShellIdleMonitorPrivate)); +} + +static guint32 +get_next_watch_serial (void) +{ + guint32 serial; + + serial = watch_serial++; + + if ((gint32)watch_serial < 0) { + watch_serial = 1; + } + + /* FIXME: make sure it isn't in the hash */ + + return serial; +} + +static ShellIdleMonitorWatch * +idle_monitor_watch_new (guint interval) +{ + ShellIdleMonitorWatch *watch; + + watch = g_slice_new0 (ShellIdleMonitorWatch); + watch->interval = _int64_to_xsyncvalue ((gint64)interval); + watch->id = get_next_watch_serial (); + watch->xalarm_positive = None; + watch->xalarm_negative = None; + + return watch; +} + +static void +idle_monitor_watch_free (ShellIdleMonitorWatch *watch) +{ + if (watch == NULL) { + return; + } + + if (watch->notify != NULL) { + watch->notify (watch->user_data); + } + + if (watch->xalarm_positive != None) { + XSyncDestroyAlarm (watch->display, watch->xalarm_positive); + } + if (watch->xalarm_negative != None) { + XSyncDestroyAlarm (watch->display, watch->xalarm_negative); + } + g_slice_free (ShellIdleMonitorWatch, watch); +} + +static void +shell_idle_monitor_init (ShellIdleMonitor *monitor) +{ + monitor->priv = SHELL_IDLE_MONITOR_GET_PRIVATE (monitor); + + monitor->priv->watches = g_hash_table_new_full (NULL, + NULL, + NULL, + (GDestroyNotify)idle_monitor_watch_free); + + monitor->priv->counter = None; +} + +ShellIdleMonitor * +shell_idle_monitor_new (void) +{ + GObject *idle_monitor; + + idle_monitor = g_object_new (SHELL_TYPE_IDLE_MONITOR, + NULL); + + return SHELL_IDLE_MONITOR (idle_monitor); +} + +static gboolean +_xsync_alarm_set (ShellIdleMonitor *monitor, + ShellIdleMonitorWatch *watch) +{ + XSyncAlarmAttributes attr; + XSyncValue delta; + guint flags; + + flags = XSyncCACounter + | XSyncCAValueType + | XSyncCATestType + | XSyncCAValue + | XSyncCADelta + | XSyncCAEvents; + + XSyncIntToValue (&delta, 0); + attr.trigger.counter = monitor->priv->counter; + attr.trigger.value_type = XSyncAbsolute; + attr.trigger.wait_value = watch->interval; + attr.delta = delta; + attr.events = TRUE; + + attr.trigger.test_type = XSyncPositiveTransition; + if (watch->xalarm_positive != None) { + /* g_debug ("ShellIdleMonitor: updating alarm for positive transition wait=%" G_GINT64_FORMAT, + _xsyncvalue_to_int64 (attr.trigger.wait_value)); */ + XSyncChangeAlarm (monitor->priv->display, watch->xalarm_positive, flags, &attr); + } else { + /* g_debug ("ShellIdleMonitor: creating new alarm for positive transition wait=%" G_GINT64_FORMAT, + _xsyncvalue_to_int64 (attr.trigger.wait_value)); */ + watch->xalarm_positive = XSyncCreateAlarm (monitor->priv->display, flags, &attr); + } + + attr.trigger.wait_value = _int64_to_xsyncvalue (_xsyncvalue_to_int64 (watch->interval) - 1); + attr.trigger.test_type = XSyncNegativeTransition; + if (watch->xalarm_negative != None) { + /* g_debug ("ShellIdleMonitor: updating alarm for negative transition wait=%" G_GINT64_FORMAT, + _xsyncvalue_to_int64 (attr.trigger.wait_value)); */ + XSyncChangeAlarm (monitor->priv->display, watch->xalarm_negative, flags, &attr); + } else { + /* g_debug ("ShellIdleMonitor: creating new alarm for negative transition wait=%" G_GINT64_FORMAT, + _xsyncvalue_to_int64 (attr.trigger.wait_value)); */ + watch->xalarm_negative = XSyncCreateAlarm (monitor->priv->display, flags, &attr); + } + + return TRUE; +} + +guint +shell_idle_monitor_add_watch (ShellIdleMonitor *monitor, + guint interval, + ShellIdleMonitorWatchFunc callback, + gpointer user_data, + GDestroyNotify notify) +{ + ShellIdleMonitorWatch *watch; + + g_return_val_if_fail (SHELL_IS_IDLE_MONITOR (monitor), 0); + g_return_val_if_fail (callback != NULL, 0); + + watch = idle_monitor_watch_new (interval); + watch->display = monitor->priv->display; + watch->callback = callback; + watch->user_data = user_data; + watch->notify = notify; + + _xsync_alarm_set (monitor, watch); + + g_hash_table_insert (monitor->priv->watches, + GUINT_TO_POINTER (watch->id), + watch); + return watch->id; +} + +void +shell_idle_monitor_remove_watch (ShellIdleMonitor *monitor, + guint id) +{ + g_return_if_fail (SHELL_IS_IDLE_MONITOR (monitor)); + + g_hash_table_remove (monitor->priv->watches, + GUINT_TO_POINTER (id)); +} diff --git a/src/shell-idle-monitor.h b/src/shell-idle-monitor.h new file mode 100644 index 000000000..1086d9216 --- /dev/null +++ b/src/shell-idle-monitor.h @@ -0,0 +1,72 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Adapted from gnome-session/gnome-session/gs-idle-monitor.h + * + * Copyright (C) 2012 Red Hat, Inc. + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + * Authors: William Jon McCann + * + */ + +#ifndef __SHELL_IDLE_MONITOR_H +#define __SHELL_IDLE_MONITOR_H + +#include + +G_BEGIN_DECLS + +#define SHELL_TYPE_IDLE_MONITOR (shell_idle_monitor_get_type ()) +#define SHELL_IDLE_MONITOR(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SHELL_TYPE_IDLE_MONITOR, ShellIdleMonitor)) +#define SHELL_IDLE_MONITOR_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), SHELL_TYPE_IDLE_MONITOR, ShellIdleMonitorClass)) +#define SHELL_IS_IDLE_MONITOR(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SHELL_TYPE_IDLE_MONITOR)) +#define SHELL_IS_IDLE_MONITOR_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), SHELL_TYPE_IDLE_MONITOR)) +#define SHELL_IDLE_MONITOR_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), SHELL_TYPE_IDLE_MONITOR, ShellIdleMonitorClass)) + +typedef struct ShellIdleMonitorPrivate ShellIdleMonitorPrivate; + +typedef struct +{ + GObject parent; + ShellIdleMonitorPrivate *priv; +} ShellIdleMonitor; + +typedef struct +{ + GObjectClass parent_class; +} ShellIdleMonitorClass; + +typedef void (*ShellIdleMonitorWatchFunc) (ShellIdleMonitor *monitor, + guint id, + gboolean condition, + gpointer user_data); + +GType shell_idle_monitor_get_type (void); + +ShellIdleMonitor * shell_idle_monitor_new (void); + +guint shell_idle_monitor_add_watch (ShellIdleMonitor *monitor, + guint interval, + ShellIdleMonitorWatchFunc callback, + gpointer user_data, + GDestroyNotify notify); + +void shell_idle_monitor_remove_watch (ShellIdleMonitor *monitor, + guint id); + +G_END_DECLS + +#endif /* __SHELL_IDLE_MONITOR_H */