[gnome-shell] Add animated display of startup notification
- From: Maxim Ermilov <mermilov src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-shell] Add animated display of startup notification
- Date: Thu, 17 Jun 2010 19:20:12 +0000 (UTC)
commit 7f8f0f2358c29e3002654feadf17055a88c32a7e
Author: Maxim Ermilov <zaspire rambler ru>
Date: Thu Jun 10 16:07:33 2010 +0400
Add animated display of startup notification
The shell design says that upon launching an application,
no X window should have focus, and we should display an
animated launching indicator.
Implement this by in panel.js, keep track of the last started
application. If there isn't currently an X focus, show an animation
for the last starting application.
https://bugzilla.gnome.org/show_bug.cgi?id=598349
data/Makefile.am | 1 +
data/theme/gnome-shell.css | 7 ++
data/theme/process-working.png | Bin 0 -> 4097 bytes
js/ui/panel.js | 189 +++++++++++++++++++++++++++++++++++++---
4 files changed, 184 insertions(+), 13 deletions(-)
---
diff --git a/data/Makefile.am b/data/Makefile.am
index f0cc6d4..2dc4199 100644
--- a/data/Makefile.am
+++ b/data/Makefile.am
@@ -30,6 +30,7 @@ dist_theme_DATA = \
theme/mosaic-view-active.svg \
theme/mosaic-view.svg \
theme/move-window-on-new.svg \
+ theme/process-working.png \
theme/remove-workspace.svg \
theme/scroll-button-down-hover.png \
theme/scroll-button-down.png \
diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css
index 67e653e..b3abbe0 100644
--- a/data/theme/gnome-shell.css
+++ b/data/theme/gnome-shell.css
@@ -30,6 +30,13 @@
color: rgba(0,0,0,0.5);
}
+.label-real-shadow {
+ background-gradient-direction: horizontal;
+ background-gradient-start: rgba(0, 0, 0, 0);
+ background-gradient-end: rgba(0, 0, 0, 255);
+ width: 10px;
+}
+
StScrollBar
{
padding: 0px;
diff --git a/data/theme/process-working.png b/data/theme/process-working.png
new file mode 100644
index 0000000..402615b
Binary files /dev/null and b/data/theme/process-working.png differ
diff --git a/js/ui/panel.js b/js/ui/panel.js
index 25b0153..40d607e 100644
--- a/js/ui/panel.js
+++ b/js/ui/panel.js
@@ -28,6 +28,10 @@ const PANEL_ICON_SIZE = 24;
const HOT_CORNER_ACTIVATION_TIMEOUT = 0.5;
+const ANIMATED_ICON_UPDATE_TIMEOUT = 100;
+const SPINNER_UPDATE_TIMEOUT = 130;
+const SPINNER_SPEED = 0.02;
+
const STANDARD_TRAY_ICON_ORDER = ['keyboard', 'volume', 'bluetooth', 'network', 'battery'];
const STANDARD_TRAY_ICON_IMPLEMENTATIONS = {
'bluetooth-applet': 'bluetooth',
@@ -41,6 +45,49 @@ const CLOCK_CUSTOM_FORMAT_KEY = 'clock/custom_format';
const CLOCK_SHOW_DATE_KEY = 'clock/show_date';
const CLOCK_SHOW_SECONDS_KEY = 'clock/show_seconds';
+function AnimatedIcon(name, size) {
+ this._init(name, size);
+}
+
+AnimatedIcon.prototype = {
+ _init: function(name, size) {
+ this.actor = new St.Bin({ visible: false });
+ this.actor.connect('destroy', Lang.bind(this, this._onDestroy));
+ this.actor.connect('notify::visible', Lang.bind(this, function() {
+ if (this.actor.visible) {
+ this._timeoutId = Mainloop.timeout_add(ANIMATED_ICON_UPDATE_TIMEOUT, Lang.bind(this, this._update));
+ } else {
+ if (this._timeoutId)
+ Mainloop.source_remove(this._timeoutId);
+ this._timeoutId = 0;
+ }
+ }));
+
+ this._timeoutId = 0;
+ this._i = 0;
+ this._animations = St.TextureCache.get_default().load_sliced_image (global.datadir + '/theme/' + name, size, size);
+ this.actor.set_child(this._animations);
+ },
+
+ _update: function() {
+ this._animations.hide_all();
+ this._animations.show();
+ if (this._i && this._i < this._animations.get_n_children())
+ this._animations.get_nth_child(this._i++).show();
+ else {
+ this._i = 1;
+ if (this._animations.get_n_children())
+ this._animations.get_nth_child(0).show();
+ }
+ return true;
+ },
+
+ _onDestroy: function() {
+ if (this._timeoutId)
+ Mainloop.source_remove(this._timeoutId);
+ }
+};
+
function TextShadower() {
this._init();
}
@@ -207,13 +254,22 @@ AppMenuButton.prototype = {
this.hide();
}));
+ this._updateId = 0;
+ this._animationStep = 0;
+ this._clipWidth = AppDisplay.APPICON_SIZE / 2;
+ this._direction = SPINNER_SPEED;
+
+ this._spinner = new AnimatedIcon('process-working.png', 24);
+ this._container.add_actor(this._spinner.actor);
+ this._spinner.actor.lower_bottom();
+
+ this._shadow = new St.Bin({ style_class: 'label-real-shadow' });
+ this._shadow.hide();
+ this._container.add_actor(this._shadow);
+
let tracker = Shell.WindowTracker.get_default();
tracker.connect('notify::focus-app', Lang.bind(this, this._sync));
- // For now just resync on all running state changes; this is mainly to handle
- // cases where the focused window's application changes without the focus
- // changing. An example case is how we map Firefox based on the window
- // title which is a dynamic property.
- tracker.connect('app-state-changed', Lang.bind(this, this._sync));
+ tracker.connect('app-state-changed', Lang.bind(this, this._onAppStateChanged));
this._sync();
},
@@ -248,6 +304,66 @@ AppMenuButton.prototype = {
onCompleteScope: this });
},
+ _stopAnimation: function(animate) {
+ this._label.actor.remove_clip();
+ if (this._updateId) {
+ this._shadow.hide();
+ if (animate) {
+ Tweener.addTween(this._spinner.actor,
+ { opacity: 0,
+ time: 0.2,
+ transition: "easeOutQuad",
+ onCompleteScope: this,
+ onComplete: function() {
+ this._spinner.actor.opacity = 255;
+ this._spinner.actor.hide();
+ }
+ });
+ }
+ Mainloop.source_remove(this._updateId);
+ this._updateId = 0;
+ }
+ if (!animate)
+ this._spinner.actor.hide();
+ },
+
+ stopAnimation: function() {
+ this._direction = SPINNER_SPEED * 3;
+ this._stop = true;
+ },
+
+ _update: function() {
+ this._animationStep += this._direction;
+ if (this._animationStep > 1 && this._stop) {
+ this._animationStep = 1;
+ this._stopAnimation(true);
+ return false;
+ }
+ if (this._animationStep < 0 || this._animationStep > 1) {
+ this._direction = -this._direction;
+ this._animationStep += 2 * this._direction;
+ }
+ this._clipWidth = this._label.actor.width - (this._label.actor.width - AppDisplay.APPICON_SIZE / 2) * (1 - this._animationStep);
+ if (this.actor.get_direction() == St.TextDirection.LTR) {
+ this._label.actor.set_clip(0, 0, this._clipWidth + this._shadow.width, this.actor.height);
+ } else {
+ this._label.actor.set_clip(this._label.actor.width - this._clipWidth, 0, this._clipWidth, this.actor.height);
+ }
+ this._container.queue_relayout();
+ return true;
+ },
+
+ startAnimation: function() {
+ this._direction = SPINNER_SPEED;
+ this._stopAnimation(false);
+ this._animationStep = 0;
+ this._update();
+ this._stop = false;
+ this._updateId = Mainloop.timeout_add(SPINNER_UPDATE_TIMEOUT, Lang.bind(this, this._update));
+ this._spinner.actor.show();
+ this._shadow.show();
+ },
+
_getContentPreferredWidth: function(actor, forHeight, alloc) {
let [minSize, naturalSize] = this._iconBox.get_preferred_width(forHeight);
alloc.min_size = minSize;
@@ -305,6 +421,25 @@ AppMenuButton.prototype = {
childBox.x1 = Math.max(0, childBox.x2 - naturalWidth);
}
this._label.actor.allocate(childBox, flags);
+
+ if (direction == St.TextDirection.LTR) {
+ childBox.x1 = Math.floor(iconWidth / 2) + this._clipWidth + this._shadow.width;
+ childBox.x2 = childBox.x1 + this._spinner.actor.width;
+ childBox.y1 = box.y1;
+ childBox.y2 = box.y2 - 1;
+ this._spinner.actor.allocate(childBox, flags);
+ childBox.x1 = Math.floor(iconWidth / 2) + this._clipWidth + 2;
+ childBox.x2 = childBox.x1 + this._shadow.width;
+ childBox.y1 = box.y1;
+ childBox.y2 = box.y2 - 1;
+ this._shadow.allocate(childBox, flags);
+ } else {
+ childBox.x1 = this._label.actor.width - this._clipWidth - this._spinner.actor.width;
+ childBox.x2 = childBox.x1 + this._spinner.actor.width;
+ childBox.y1 = box.y1;
+ childBox.y2 = box.y2 - 1;
+ this._spinner.actor.allocate(childBox, flags);
+ }
},
_onQuit: function() {
@@ -313,27 +448,55 @@ AppMenuButton.prototype = {
this._focusedApp.request_quit();
},
+ _onAppStateChanged: function(tracker, app) {
+ let state = app.state;
+ if (app == this._lastStartedApp
+ && state != Shell.AppState.STARTING) {
+ this._lastStartedApp = null;
+ } else if (state == Shell.AppState.STARTING) {
+ this._lastStartedApp = app;
+ }
+ // For now just resync on all running state changes; this is mainly to handle
+ // cases where the focused window's application changes without the focus
+ // changing. An example case is how we map OpenOffice.org based on the window
+ // title which is a dynamic property.
+ this._sync();
+ },
+
_sync: function() {
let tracker = Shell.WindowTracker.get_default();
let focusedApp = tracker.focus_app;
- if (focusedApp == this._focusedApp)
- return;
+ if (focusedApp == this._focusedApp) {
+ if (focusedApp && focusedApp.get_state() != Shell.AppState.STARTING)
+ this.stopAnimation();
+ return;
+ } else {
+ this._stopAnimation();
+ }
if (this._iconBox.child != null)
this._iconBox.child.destroy();
this._iconBox.hide();
this._label.setText('');
+ this.actor.reactive = false;
this._focusedApp = focusedApp;
- if (this._focusedApp != null) {
- let icon = this._focusedApp.get_faded_icon(AppDisplay.APPICON_SIZE);
- let appName = this._focusedApp.get_name();
- this._label.setText(appName);
- this._quitMenu.label.set_text(_("Quit %s").format(appName));
+ let targetApp = this._focusedApp != null ? this._focusedApp : this._lastStartedApp;
+ if (targetApp != null) {
+ let icon = targetApp.get_faded_icon(AppDisplay.APPICON_SIZE);
+
+ this._label.setText(targetApp.get_name());
+ // TODO - _quit() doesn't really work on apps in state STARTING yet
+ this._quitMenu.label.set_text(_('Quit %s').format(targetApp.get_name()));
+
+ this.actor.reactive = true;
this._iconBox.set_child(icon);
this._iconBox.show();
+
+ if (targetApp.get_state() == Shell.AppState.STARTING)
+ this.startAnimation();
}
this.emit('changed');
@@ -597,7 +760,7 @@ Panel.prototype = {
Lang.bind(this, this._onHotCornerClicked));
// In addition to being triggered by the mouse enter event, the hot corner
- // can be triggered by clicking on it. This is useful if the user wants to
+ // can be triggered by clicking on it. This is useful if the user wants to
// undo the effect of triggering the hot corner once in the hot corner.
this._hotCorner.connect('enter-event',
Lang.bind(this, this._onHotCornerEntered));
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]