[MessageTray] reimplement the state machine
Previously, every time _updateState was called, it would make some change, and so it was necessary to very carefully set up all the calls to it, to ensure it was always called at exactly the right time. Now, instead, we keep a bunch of state variables like "_notificationState" and "_pointerInSummary", and potentially multiple timeouts, and _updateState looks at all of them and figure out what, if anything, needs to be changed. By making the rules about what causes changes more explicit, it will be easier to change those rules in the future as we add new functionality. Also, update the rules a bit, so that notifications can appear while the summary is visible, and the summary only shows after a notification if the summary has changed. https://bugzilla.gnome.org/show_bug.cgi?id=609765
This commit is contained in:
parent
02f67af464
commit
e86c821878
@ -19,11 +19,11 @@ const MESSAGE_TRAY_TIMEOUT = 0.2;
|
|||||||
|
|
||||||
const ICON_SIZE = 24;
|
const ICON_SIZE = 24;
|
||||||
|
|
||||||
const MessageTrayState = {
|
const State = {
|
||||||
HIDDEN: 0, // entire message tray is hidden
|
HIDDEN: 0,
|
||||||
NOTIFICATION: 1, // notifications are visible
|
SHOWING: 1,
|
||||||
SUMMARY: 2, // summary is visible
|
SHOWN: 2,
|
||||||
TRAY_ONLY: 3 // neither notifiations nor summary are visible, only tray
|
HIDING: 3
|
||||||
};
|
};
|
||||||
|
|
||||||
function _cleanMarkup(text) {
|
function _cleanMarkup(text) {
|
||||||
@ -360,20 +360,24 @@ MessageTray.prototype = {
|
|||||||
this._summaryBin.add(this._summary, { x_align: St.Align.END,
|
this._summaryBin.add(this._summary, { x_align: St.Align.END,
|
||||||
x_fill: false,
|
x_fill: false,
|
||||||
expand: true });
|
expand: true });
|
||||||
|
|
||||||
this._summary.connect('enter-event',
|
this._summary.connect('enter-event',
|
||||||
Lang.bind(this, this._showMessageTray));
|
Lang.bind(this, this._onSummaryEntered));
|
||||||
|
this._summary.connect('leave-event',
|
||||||
|
Lang.bind(this, this._onSummaryLeft));
|
||||||
this._summaryBin.opacity = 0;
|
this._summaryBin.opacity = 0;
|
||||||
|
|
||||||
this.actor.connect('enter-event',
|
this.actor.connect('enter-event', Lang.bind(this, this._onTrayEntered));
|
||||||
Lang.bind(this, function() {
|
this.actor.connect('leave-event', Lang.bind(this, this._onTrayLeft));
|
||||||
if (this._state == MessageTrayState.NOTIFICATION || this._state == MessageTrayState.SUMMARY)
|
|
||||||
this._showMessageTray();
|
this._trayState = State.HIDDEN;
|
||||||
}));
|
this._trayLeftTimeoutId = 0;
|
||||||
|
this._pointerInTray = false;
|
||||||
|
this._summaryState = State.HIDDEN;
|
||||||
|
this._summaryTimeoutId = 0;
|
||||||
|
this._pointerInSummary = false;
|
||||||
|
this._notificationState = State.HIDDEN;
|
||||||
|
this._notificationTimeoutId = 0;
|
||||||
|
|
||||||
this.actor.connect('leave-event',
|
|
||||||
Lang.bind(this, this._hideMessageTray));
|
|
||||||
this._state = MessageTrayState.HIDDEN;
|
|
||||||
this.actor.show();
|
this.actor.show();
|
||||||
Main.chrome.addActor(this.actor, { affectsStruts: false });
|
Main.chrome.addActor(this.actor, { affectsStruts: false });
|
||||||
Main.chrome.trackActor(this._notificationBin, { affectsStruts: false });
|
Main.chrome.trackActor(this._notificationBin, { affectsStruts: false });
|
||||||
@ -409,6 +413,7 @@ MessageTray.prototype = {
|
|||||||
let iconBox = new St.Bin({ reactive: true });
|
let iconBox = new St.Bin({ reactive: true });
|
||||||
iconBox.child = source.createIcon(ICON_SIZE);
|
iconBox.child = source.createIcon(ICON_SIZE);
|
||||||
this._summary.insert_actor(iconBox, 0);
|
this._summary.insert_actor(iconBox, 0);
|
||||||
|
this._summaryNeedsToBeShown = true;
|
||||||
this._icons[source.id] = iconBox;
|
this._icons[source.id] = iconBox;
|
||||||
this._sources[source.id] = source;
|
this._sources[source.id] = source;
|
||||||
|
|
||||||
@ -437,14 +442,18 @@ MessageTray.prototype = {
|
|||||||
}
|
}
|
||||||
this._notificationQueue = newNotificationQueue;
|
this._notificationQueue = newNotificationQueue;
|
||||||
|
|
||||||
// Update state if we are showing a notification from the removed source
|
|
||||||
if (this._state == MessageTrayState.NOTIFICATION &&
|
|
||||||
this._notification.source == source)
|
|
||||||
this._updateState();
|
|
||||||
|
|
||||||
this._summary.remove_actor(this._icons[source.id]);
|
this._summary.remove_actor(this._icons[source.id]);
|
||||||
|
this._summaryNeedsToBeShown = true;
|
||||||
delete this._icons[source.id];
|
delete this._icons[source.id];
|
||||||
delete this._sources[source.id];
|
delete this._sources[source.id];
|
||||||
|
|
||||||
|
if (this._notification && this._notification.source == source) {
|
||||||
|
if (this._notificationTimeoutId) {
|
||||||
|
Mainloop.source_remove(this._notificationTimeoutId);
|
||||||
|
this._notificationTimeoutId = 0;
|
||||||
|
}
|
||||||
|
this._updateState();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getSource: function(id) {
|
getSource: function(id) {
|
||||||
@ -453,115 +462,121 @@ MessageTray.prototype = {
|
|||||||
|
|
||||||
_onNotify: function(source, notification) {
|
_onNotify: function(source, notification) {
|
||||||
this._notificationQueue.push(notification);
|
this._notificationQueue.push(notification);
|
||||||
|
|
||||||
if (this._state == MessageTrayState.HIDDEN)
|
|
||||||
this._updateState();
|
this._updateState();
|
||||||
},
|
},
|
||||||
|
|
||||||
_showMessageTray: function() {
|
_onSummaryEntered: function() {
|
||||||
// Don't hide the message tray after a timeout if the user has moved
|
this._pointerInSummary = true;
|
||||||
// the mouse over it.
|
|
||||||
// We might have a timeout in place if the user moved the mouse away
|
|
||||||
// from the message tray for a very short period of time or if we are
|
|
||||||
// showing a notification.
|
|
||||||
if (this._updateTimeoutId > 0)
|
|
||||||
Mainloop.source_remove(this._updateTimeoutId);
|
|
||||||
|
|
||||||
if (this._state == MessageTrayState.HIDDEN)
|
|
||||||
this._updateState();
|
this._updateState();
|
||||||
else if (this._state == MessageTrayState.NOTIFICATION) {
|
|
||||||
if (this._notification.popOut()) {
|
|
||||||
Tweener.addTween(this._notificationBin,
|
|
||||||
{ y: this.actor.height - this._notificationBin.height,
|
|
||||||
time: ANIMATION_TIME,
|
|
||||||
transition: "easeOutQuad"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_hideMessageTray: function() {
|
_onSummaryLeft: function() {
|
||||||
if (this._state == MessageTrayState.HIDDEN)
|
this._pointerInSummary = false;
|
||||||
|
this._updateState();
|
||||||
|
},
|
||||||
|
|
||||||
|
_onTrayEntered: function() {
|
||||||
|
if (this._trayLeftTimeoutId) {
|
||||||
|
Mainloop.source_remove(this._trayLeftTimeoutId);
|
||||||
|
this._trayLeftTimeoutId = 0;
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// We wait just a little before hiding the message tray in case the
|
this._pointerInTray = true;
|
||||||
// user will quickly move the mouse back over it.
|
this._updateState();
|
||||||
let timeout = MESSAGE_TRAY_TIMEOUT * 1000;
|
|
||||||
this._updateTimeoutId = Mainloop.timeout_add(timeout, Lang.bind(this, this._updateState));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// As tray, notification box and summary view are all animated separately,
|
_onTrayLeft: function() {
|
||||||
// but dependant on each other's states, it appears less confusing
|
// We wait just a little before hiding the message tray in case the
|
||||||
// handling all transitions in a state machine rather than spread out
|
// user quickly moves the mouse back into it.
|
||||||
// over different event handlers.
|
let timeout = MESSAGE_TRAY_TIMEOUT * 1000;
|
||||||
//
|
this._trayLeftTimeoutId = Mainloop.timeout_add(timeout, Lang.bind(this, this._onTrayLeftTimeout));
|
||||||
// State changes are triggered when
|
},
|
||||||
// - a notification arrives (see _onNotify())
|
|
||||||
// - the mouse enters the tray (see _showMessageTray())
|
_onTrayLeftTimeout: function() {
|
||||||
// - the mouse leaves the tray (see _hideMessageTray())
|
this._trayLeftTimeoutId = 0;
|
||||||
// - a timeout expires (usually set up in a previous invocation of this function)
|
this._pointerInTray = false;
|
||||||
|
this._pointerInSummary = false;
|
||||||
|
this._updateState();
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
// All of the logic for what happens when occurs here; the various
|
||||||
|
// event handlers merely update variables such as
|
||||||
|
// "this._pointerInTray", "this._summaryState", etc, and
|
||||||
|
// _updateState() figures out what (if anything) needs to be done
|
||||||
|
// at the present time.
|
||||||
_updateState: function() {
|
_updateState: function() {
|
||||||
if (this._updateTimeoutId > 0)
|
// Notifications
|
||||||
Mainloop.source_remove(this._updateTimeoutId);
|
let notificationsPending = this._notificationQueue.length > 0;
|
||||||
|
let notificationPinned = this._pointerInTray && !this._pointerInSummary;
|
||||||
|
let notificationExpanded = this._notificationBin.y < 0;
|
||||||
|
let notificationExpired = this._notificationTimeoutId == 0 && !this._pointerInTray;
|
||||||
|
|
||||||
this._updateTimeoutId = 0;
|
if (this._notificationState == State.HIDDEN) {
|
||||||
let timeout = -1;
|
if (notificationsPending)
|
||||||
|
this._showNotification();
|
||||||
|
} else if (this._notificationState == State.SHOWN) {
|
||||||
|
if (notificationExpired)
|
||||||
|
this._hideNotification();
|
||||||
|
else if (notificationPinned && !notificationExpanded)
|
||||||
|
this._expandNotification();
|
||||||
|
}
|
||||||
|
|
||||||
switch (this._state) {
|
// Summary
|
||||||
case MessageTrayState.HIDDEN:
|
let summarySummoned = this._pointerInSummary;
|
||||||
if (this._notificationQueue.length > 0) {
|
let summaryPinned = this._summaryTimeoutId != 0 || this._pointerInTray || summarySummoned;
|
||||||
this._showNotification();
|
let notificationsVisible = (this._notificationState == State.SHOWING ||
|
||||||
this._showTray();
|
this._notificationState == State.SHOWN);
|
||||||
this._state = MessageTrayState.NOTIFICATION;
|
let notificationsDone = !notificationsVisible && !notificationsPending;
|
||||||
// Because we set up the timeout before we do the animation,
|
|
||||||
// we add ANIMATION_TIME to NOTIFICATION_TIMEOUT, so that
|
if (this._summaryState == State.HIDDEN) {
|
||||||
// NOTIFICATION_TIMEOUT represents the time the notifiation
|
if (notificationsDone && this._summaryNeedsToBeShown)
|
||||||
// is fully shown.
|
this._showSummary(true);
|
||||||
timeout = (ANIMATION_TIME + NOTIFICATION_TIMEOUT) * 1000;
|
else if (!notificationsVisible && summarySummoned)
|
||||||
} else {
|
this._showSummary(false);
|
||||||
this._showSummary();
|
} else if (this._summaryState == State.SHOWN) {
|
||||||
this._showTray();
|
if (!summaryPinned)
|
||||||
this._state = MessageTrayState.SUMMARY;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case MessageTrayState.NOTIFICATION:
|
|
||||||
if (this._notificationQueue.length > 0) {
|
|
||||||
this._hideNotification();
|
|
||||||
this._state = MessageTrayState.TRAY_ONLY;
|
|
||||||
timeout = ANIMATION_TIME * 1000;
|
|
||||||
} else {
|
|
||||||
this._hideNotification();
|
|
||||||
this._showSummary();
|
|
||||||
this._state = MessageTrayState.SUMMARY;
|
|
||||||
timeout = (ANIMATION_TIME + SUMMARY_TIMEOUT) * 1000;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case MessageTrayState.SUMMARY:
|
|
||||||
if (this._notificationQueue.length > 0) {
|
|
||||||
this._hideSummary();
|
|
||||||
this._showNotification();
|
|
||||||
this._state = MessageTrayState.NOTIFICATION;
|
|
||||||
timeout = (ANIMATION_TIME + NOTIFICATION_TIMEOUT) * 1000;
|
|
||||||
} else {
|
|
||||||
this._hideSummary();
|
this._hideSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tray itself
|
||||||
|
let trayIsVisible = (this._trayState == State.SHOWING ||
|
||||||
|
this._trayState == State.SHOWN);
|
||||||
|
let trayShouldBeVisible = (!notificationsDone ||
|
||||||
|
this._summaryState == State.SHOWING ||
|
||||||
|
this._summaryState == State.SHOWN);
|
||||||
|
if (!trayIsVisible && trayShouldBeVisible)
|
||||||
|
this._showTray();
|
||||||
|
else if (trayIsVisible && !trayShouldBeVisible)
|
||||||
this._hideTray();
|
this._hideTray();
|
||||||
this._state = MessageTrayState.HIDDEN;
|
},
|
||||||
}
|
|
||||||
break;
|
|
||||||
case MessageTrayState.TRAY_ONLY:
|
|
||||||
this._showNotification();
|
|
||||||
this._state = MessageTrayState.NOTIFICATION;
|
|
||||||
timeout = (ANIMATION_TIME + NOTIFICATION_TIMEOUT) * 1000;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timeout > -1)
|
_tween: function(actor, statevar, value, params) {
|
||||||
this._updateTimeoutId = Mainloop.timeout_add(timeout, Lang.bind(this, this._updateState));
|
let onComplete = params.onComplete;
|
||||||
|
let onCompleteScope = params.onCompleteScope;
|
||||||
|
let onCompleteParams = params.onCompleteParams;
|
||||||
|
|
||||||
|
params.onComplete = this._tweenComplete;
|
||||||
|
params.onCompleteScope = this;
|
||||||
|
params.onCompleteParams = [statevar, value, onComplete, onCompleteScope, onCompleteParams];
|
||||||
|
|
||||||
|
Tweener.addTween(actor, params);
|
||||||
|
|
||||||
|
let valuing = (value == State.SHOWN) ? State.SHOWING : State.HIDING;
|
||||||
|
this[statevar] = valuing;
|
||||||
|
},
|
||||||
|
|
||||||
|
_tweenComplete: function(statevar, value, onComplete, onCompleteScope, onCompleteParams) {
|
||||||
|
this[statevar] = value;
|
||||||
|
if (onComplete)
|
||||||
|
onComplete.apply(onCompleteScope, onCompleteParams);
|
||||||
|
this._updateState();
|
||||||
},
|
},
|
||||||
|
|
||||||
_showTray: function() {
|
_showTray: function() {
|
||||||
let primary = global.get_primary_monitor();
|
let primary = global.get_primary_monitor();
|
||||||
Tweener.addTween(this.actor,
|
this._tween(this.actor, "_trayState", State.SHOWN,
|
||||||
{ y: primary.y + primary.height - this.actor.height,
|
{ y: primary.y + primary.height - this.actor.height,
|
||||||
time: ANIMATION_TIME,
|
time: ANIMATION_TIME,
|
||||||
transition: "easeOutQuad"
|
transition: "easeOutQuad"
|
||||||
@ -570,13 +585,11 @@ MessageTray.prototype = {
|
|||||||
|
|
||||||
_hideTray: function() {
|
_hideTray: function() {
|
||||||
let primary = global.get_primary_monitor();
|
let primary = global.get_primary_monitor();
|
||||||
|
this._tween(this.actor, "_trayState", State.HIDDEN,
|
||||||
Tweener.addTween(this.actor,
|
|
||||||
{ y: primary.y + primary.height - 1,
|
{ y: primary.y + primary.height - 1,
|
||||||
time: ANIMATION_TIME,
|
time: ANIMATION_TIME,
|
||||||
transition: "easeOutQuad"
|
transition: "easeOutQuad"
|
||||||
});
|
});
|
||||||
return false;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_showNotification: function() {
|
_showNotification: function() {
|
||||||
@ -587,44 +600,94 @@ MessageTray.prototype = {
|
|||||||
this._notificationBin.y = this.actor.height;
|
this._notificationBin.y = this.actor.height;
|
||||||
this._notificationBin.show();
|
this._notificationBin.show();
|
||||||
|
|
||||||
Tweener.addTween(this._notificationBin,
|
this._tween(this._notificationBin, "_notificationState", State.SHOWN,
|
||||||
{ y: 0,
|
{ y: 0,
|
||||||
opacity: 255,
|
opacity: 255,
|
||||||
time: ANIMATION_TIME,
|
time: ANIMATION_TIME,
|
||||||
transition: "easeOutQuad" });
|
transition: "easeOutQuad",
|
||||||
|
onComplete: this._showNotificationCompleted,
|
||||||
|
onCompleteScope: this
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_showNotificationCompleted: function() {
|
||||||
|
this._notificationTimeoutId =
|
||||||
|
Mainloop.timeout_add(NOTIFICATION_TIMEOUT * 1000,
|
||||||
|
Lang.bind(this, this._notificationTimeout));
|
||||||
|
},
|
||||||
|
|
||||||
|
_notificationTimeout: function() {
|
||||||
|
this._notificationTimeoutId = 0;
|
||||||
|
this._updateState();
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
_hideNotification: function() {
|
_hideNotification: function() {
|
||||||
this._notification.popIn();
|
this._notification.popIn();
|
||||||
|
|
||||||
Tweener.addTween(this._notificationBin,
|
this._tween(this._notificationBin, "_notificationState", State.HIDDEN,
|
||||||
{ y: this.actor.height,
|
{ y: this.actor.height,
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
time: ANIMATION_TIME,
|
time: ANIMATION_TIME,
|
||||||
transition: "easeOutQuad",
|
transition: "easeOutQuad",
|
||||||
onComplete: Lang.bind(this, function() {
|
onComplete: this._hideNotificationCompleted,
|
||||||
|
onCompleteScope: this
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_hideNotificationCompleted: function() {
|
||||||
this._notificationBin.hide();
|
this._notificationBin.hide();
|
||||||
this._notificationBin.child = null;
|
this._notificationBin.child = null;
|
||||||
this._notification = null;
|
this._notification = null;
|
||||||
})});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_showSummary: function() {
|
_expandNotification: function() {
|
||||||
let primary = global.get_primary_monitor();
|
if (this._notification && this._notification.popOut()) {
|
||||||
this._summaryBin.opacity = 0;
|
this._tween(this._notificationBin, "_notificationState", State.SHOWN,
|
||||||
this._summaryBin.y = this.actor.height;
|
{ y: this.actor.height - this._notificationBin.height,
|
||||||
Tweener.addTween(this._summaryBin,
|
|
||||||
{ y: 0,
|
|
||||||
opacity: 255,
|
|
||||||
time: ANIMATION_TIME,
|
|
||||||
transition: "easeOutQuad" });
|
|
||||||
},
|
|
||||||
|
|
||||||
_hideSummary: function() {
|
|
||||||
Tweener.addTween(this._summaryBin,
|
|
||||||
{ opacity: 0,
|
|
||||||
time: ANIMATION_TIME,
|
time: ANIMATION_TIME,
|
||||||
transition: "easeOutQuad"
|
transition: "easeOutQuad"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_showSummary: function(withTimeout) {
|
||||||
|
let primary = global.get_primary_monitor();
|
||||||
|
this._summaryBin.opacity = 0;
|
||||||
|
this._summaryBin.y = this.actor.height;
|
||||||
|
this._tween(this._summaryBin, "_summaryState", State.SHOWN,
|
||||||
|
{ y: 0,
|
||||||
|
opacity: 255,
|
||||||
|
time: ANIMATION_TIME,
|
||||||
|
transition: "easeOutQuad",
|
||||||
|
onComplete: this._showSummaryCompleted,
|
||||||
|
onCompleteScope: this,
|
||||||
|
onCompleteParams: [withTimeout]
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_showSummaryCompleted: function(withTimeout) {
|
||||||
|
this._summaryNeedsToBeShown = false;
|
||||||
|
|
||||||
|
if (withTimeout) {
|
||||||
|
this._summaryTimeoutId =
|
||||||
|
Mainloop.timeout_add(SUMMARY_TIMEOUT * 1000,
|
||||||
|
Lang.bind(this, this._summaryTimeout));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_summaryTimeout: function() {
|
||||||
|
this._summaryTimeoutId = 0;
|
||||||
|
this._updateState();
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
_hideSummary: function() {
|
||||||
|
this._tween(this._summaryBin, "_summaryState", State.HIDDEN,
|
||||||
|
{ opacity: 0,
|
||||||
|
time: ANIMATION_TIME,
|
||||||
|
transition: "easeOutQuad"
|
||||||
|
});
|
||||||
|
this._summaryNeedsToBeShown = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user