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
This commit is contained in:
Maxim Ermilov 2010-06-10 16:07:33 +04:00
parent cae61e62fd
commit 7f8f0f2358
4 changed files with 184 additions and 13 deletions

View File

@ -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 \

View File

@ -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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -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));