Add a wrapper around tweener to do some extra integration

Automatically removes tweens on destroyed actors, and provides
additional "animation started/stopped" callbacks (eg, for tracking
whether or not to show window clone titles)
This commit is contained in:
Dan Winship 2009-02-10 11:12:58 -05:00
parent 7d474b2217
commit e79c776c2e
6 changed files with 305 additions and 146 deletions

View File

@ -1,8 +1,7 @@
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
const Clutter = imports.gi.Clutter;
const Big = imports.gi.Big;
const Tweener = imports.tweener.tweener;
const Clutter = imports.gi.Clutter;
const DEFAULT_BUTTON_COLOR = new Clutter.Color();
DEFAULT_BUTTON_COLOR.from_pixel(0xeeddcc66);

View File

@ -5,11 +5,11 @@ const Gio = imports.gi.Gio;
const Mainloop = imports.mainloop;
const Shell = imports.gi.Shell;
const Signals = imports.signals;
const Tweener = imports.tweener.tweener;
const Overlay = imports.ui.overlay;
const Panel = imports.ui.panel;
const RunDialog = imports.ui.runDialog;
const Tweener = imports.ui.tweener;
const WindowManager = imports.ui.windowManager;
const DEFAULT_BACKGROUND_COLOR = new Clutter.Color();
@ -21,78 +21,6 @@ let overlayActive = false;
let runDialog = null;
let wm = null;
// The "FrameTicker" object is an object used to feed new frames to Tweener
// so it can update values and redraw. The default frame ticker for
// Tweener just uses a simple timeout at a fixed frame rate and has no idea
// of "catching up" by dropping frames.
//
// We substitute it with custom frame ticker here that connects Tweener to
// a Clutter.TimeLine. Now, Clutter.Timeline itself isn't a whole lot more
// sophisticated than a simple timeout at a fixed frame rate, but at least
// it knows how to drop frames. (See HippoAnimationManager for a more
// sophisticated view of continous time updates; even better is to pay
// attention to the vertical vblank and sync to that when possible.)
//
function ClutterFrameTicker() {
this._init();
}
ClutterFrameTicker.prototype = {
FRAME_RATE : 60,
_init : function() {
// We don't have a finite duration; tweener will tell us to stop
// when we need to stop, so use 1000 seconds as "infinity"
this._timeline = new Clutter.Timeline({ fps: this.FRAME_RATE,
duration: 1000*1000 });
this._currentTime = 0;
this._frame = 0;
let me = this;
this._timeline.connect('new-frame',
function(timeline, frame) {
me._onNewFrame(frame);
});
},
_onNewFrame : function(frame) {
// If there is a lot of setup to start the animation, then
// first frame number we get from clutter might be a long ways
// into the animation (or the animation might even be done).
// That looks bad, so we always start one frame into the
// animation then only do frame dropping from there.
let delta;
if (this._frame == 0)
delta = 1;
else
delta = frame - this._frame;
if (delta == 0) // protect against divide-by-0 if we get a frame twice
delta = 1;
// currentTime is in milliseconds
this._currentTime += (1000 * delta) / this.FRAME_RATE;
this._frame = frame;
this.emit('prepare-frame');
},
getTime : function() {
return this._currentTime;
},
start : function() {
this._timeline.start();
},
stop : function() {
this._timeline.stop();
this._frame = 0;
this._currentTime = 0;
}
};
Signals.addSignalMethods(ClutterFrameTicker.prototype);
function start() {
let global = Shell.Global.get();
@ -101,7 +29,7 @@ function start() {
global.grab_dbus_service();
global.start_task_panel();
Tweener.setFrameTicker(new ClutterFrameTicker());
Tweener.init();
// The background color really only matters if there is no desktop
// window (say, nautilus) running. We set it mostly so things look good

View File

@ -7,13 +7,13 @@ const Gtk = imports.gi.Gtk;
const Mainloop = imports.mainloop;
const Shell = imports.gi.Shell;
const Signals = imports.signals;
const Tweener = imports.tweener.tweener;
const AppDisplay = imports.ui.appDisplay;
const DocDisplay = imports.ui.docDisplay;
const GenericDisplay = imports.ui.genericDisplay;
const Main = imports.ui.main;
const Panel = imports.ui.panel;
const Tweener = imports.ui.tweener;
const Workspaces = imports.ui.workspaces;
const OVERLAY_BACKGROUND_COLOR = new Clutter.Color();

255
js/ui/tweener.js Normal file
View File

@ -0,0 +1,255 @@
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
const Clutter = imports.gi.Clutter;
const Lang = imports.lang;
const Mainloop = imports.mainloop;
const Signals = imports.signals;
const Tweener = imports.tweener.tweener;
// This is a wrapper around imports.tweener.tweener that adds a bit of
// Clutter integration and some additional callbacks:
//
// 1. If the tweening target is a Clutter.Actor, then the tweenings
// will automatically be removed if the actor is destroyed
//
// 2. If target._delegate.onAnimationStart() exists, it will be
// called when the target starts being animated.
//
// 3. If target._delegate.onAnimationComplete() exists, it will be
// called once the target is no longer being animated.
//
// The onAnimationStart() and onAnimationComplete() callbacks differ
// from the tweener onStart and onComplete parameters, in that (1)
// they track whether or not the target has *any* tweens attached to
// it, as opposed to be called for *each* tween, and (2)
// onAnimationComplete() is always called when the object stops being
// animated, regardless of whether it stopped normally or abnormally.
//
// onAnimationComplete() is called at idle time, which means that if a
// tween completes and then another is added before returning to the
// main loop, the complete callback will not be called (until the new
// tween finishes).
// ActionScript Tweener methods that imports.tweener.tweener doesn't
// currently implement: getTweens, getVersion, registerTransition,
// setTimeScale, updateTime.
// imports.tweener.tweener methods that we don't re-export:
// pauseAllTweens, removeAllTweens, resumeAllTweens. (It would be hard
// to clean up properly after removeAllTweens, and also, any code that
// calls any of these is almost certainly wrong anyway, because they
// affect the entire application.)
// Called from Main.start
function init() {
Tweener.setFrameTicker(new ClutterFrameTicker());
}
function addCaller(target, tweeningParameters) {
_wrapTweening(target, tweeningParameters);
Tweener.addCaller(target, tweeningParameters);
}
function addTween(target, tweeningParameters) {
_wrapTweening(target, tweeningParameters);
Tweener.addTween(target, tweeningParameters);
}
function _wrapTweening(target, tweeningParameters) {
let state = _getTweenState(target);
if (target instanceof Clutter.Actor && !state.destroyedId)
state.destroyedId = target.connect('destroy', _actorDestroyed);
_addHandler(target, tweeningParameters, 'onStart', _tweenStarted);
_addHandler(target, tweeningParameters, 'onComplete', _tweenCompleted);
}
function _getTweenState(target) {
// If we were paranoid, we could keep a plist mapping targets to
// states... but we're not that paranoid.
if (!target.__ShellTweenerState)
_resetTweenState(target);
return target.__ShellTweenerState;
}
function _resetTweenState(target) {
let state = target.__ShellTweenerState;
if (state) {
if (state.destroyedId)
target.disconnect(state.destroyedId);
if (state.idleCompletedId)
Mainloop.source_remove(state.idleCompletedId);
}
target.__ShellTweenerState = {};
}
function _addHandler(target, params, name, handler) {
if (params[name]) {
let oldHandler = params[name];
let oldScope = params[name + 'Scope'];
let oldParams = params[name + 'Params'];
let eventScope = oldScope ? oldScope : target;
params[name] = function () {
oldHandler.apply(eventScope, oldParams);
handler(target);
};
} else
params[name] = function () { handler(target); };
}
function _actorDestroyed(target) {
_resetTweenState(target);
Tweener.removeTweens(target);
}
function _tweenStarted(target) {
let state = _getTweenState(target);
let delegate = target._delegate;
if (!state.running && delegate && delegate.onAnimationStart)
delegate.onAnimationStart();
state.running = true;
}
function _tweenCompleted(target) {
let state = _getTweenState(target);
if (!state.idleCompletedId)
state.idleCompletedId = Mainloop.idle_add(Lang.bind(null, _idleCompleted, target));
}
function _idleCompleted(target) {
let state = _getTweenState(target);
let delegate = target._delegate;
if (!isTweening(target)) {
_resetTweenState(target);
if (delegate && delegate.onAnimationComplete)
delegate.onAnimationComplete();
}
return false;
}
function getTweenCount(scope) {
return Tweener.getTweenCount(scope);
}
// imports.tweener.tweener doesn't provide this method (which exists
// in the ActionScript version) but it's easy to implement.
function isTweening(scope) {
return Tweener.getTweenCount(scope) != 0;
}
function removeTweens(scope) {
if (Tweener.removeTweens.apply(null, arguments)) {
// If we just removed the last active tween, clean up
if (Tweener.getTweenCount(scope) == 0)
_tweenCompleted(scope);
return true;
} else
return false;
}
function pauseTweens() {
return Tweener.pauseTweens.apply(null, arguments);
}
function resumeTweens() {
return Tweener.resumeTweens.apply(null, arguments);
}
function registerSpecialProperty(name, getFunction, setFunction,
parameters, preProcessFunction) {
Tweener.registerSpecialProperty(name, getFunction, setFunction,
parameters, preProcessFunction);
}
function registerSpecialPropertyModifier(name, modifyFunction, getFunction) {
Tweener.registerSpecialPropertyModifier(name, modifyFunction, getFunction);
}
function registerSpecialPropertySplitter(name, splitFunction, parameters) {
Tweener.registerSpecialPropertySplitter(name, splitFunction, parameters);
}
// The "FrameTicker" object is an object used to feed new frames to
// Tweener so it can update values and redraw. The default frame
// ticker for Tweener just uses a simple timeout at a fixed frame rate
// and has no idea of "catching up" by dropping frames.
//
// We substitute it with custom frame ticker here that connects
// Tweener to a Clutter.TimeLine. Now, Clutter.Timeline itself isn't a
// whole lot more sophisticated than a simple timeout at a fixed frame
// rate, but at least it knows how to drop frames. (See
// HippoAnimationManager for a more sophisticated view of continous
// time updates; even better is to pay attention to the vertical
// vblank and sync to that when possible.)
//
function ClutterFrameTicker() {
this._init();
}
ClutterFrameTicker.prototype = {
FRAME_RATE : 60,
_init : function() {
// We don't have a finite duration; tweener will tell us to stop
// when we need to stop, so use 1000 seconds as "infinity"
this._timeline = new Clutter.Timeline({ fps: this.FRAME_RATE,
duration: 1000*1000 });
this._currentTime = 0;
this._frame = 0;
let me = this;
this._timeline.connect('new-frame',
function(timeline, frame) {
me._onNewFrame(frame);
});
},
_onNewFrame : function(frame) {
// If there is a lot of setup to start the animation, then
// first frame number we get from clutter might be a long ways
// into the animation (or the animation might even be done).
// That looks bad, so we always start one frame into the
// animation then only do frame dropping from there.
let delta;
if (this._frame == 0)
delta = 1;
else
delta = frame - this._frame;
if (delta == 0) // protect against divide-by-0 if we get a frame twice
delta = 1;
// currentTime is in milliseconds
this._currentTime += (1000 * delta) / this.FRAME_RATE;
this._frame = frame;
this.emit('prepare-frame');
},
getTime : function() {
return this._currentTime;
},
start : function() {
this._timeline.start();
},
stop : function() {
this._timeline.stop();
this._frame = 0;
this._currentTime = 0;
}
};
Signals.addSignalMethods(ClutterFrameTicker.prototype);

View File

@ -4,9 +4,9 @@ const Clutter = imports.gi.Clutter;
const Mainloop = imports.mainloop;
const Meta = imports.gi.Meta;
const Shell = imports.gi.Shell;
const Tweener = imports.tweener.tweener;
const Main = imports.ui.main;
const Tweener = imports.ui.tweener;
const WINDOW_ANIMATION_TIME = 0.25;
const SWITCH_ANIMATION_TIME = 0.5;

View File

@ -10,11 +10,11 @@ const Meta = imports.gi.Meta;
const Pango = imports.gi.Pango;
const Shell = imports.gi.Shell;
const Signals = imports.signals;
const Tweener = imports.tweener.tweener;
const Main = imports.ui.main;
const Overlay = imports.ui.overlay;
const Panel = imports.ui.panel;
const Tweener = imports.ui.tweener;
// Windows are slightly translucent in the overlay mode
const WINDOW_OPACITY = 0.9 * 255;
@ -58,6 +58,7 @@ WindowClone.prototype = {
reactive: true,
x: realWindow.x,
y: realWindow.y });
this.actor._delegate = this;
this.realWindow = realWindow;
this.metaWindow = realWindow.meta_window;
this.origX = realWindow.x;
@ -77,12 +78,6 @@ WindowClone.prototype = {
this._havePointer = false;
this._inDrag = false;
this._buttonDown = false;
// We track the number of animations we are doing for the clone so we can
// hide the floating title while animating. It seems like it should be
// possible to use Tweener.getTweenCount(clone), but that annoyingly only
// updates after onComplete is called.
this._animationCount = 0;
},
destroy: function () {
@ -91,27 +86,6 @@ WindowClone.prototype = {
this._title.destroy();
},
addTween: function (params) {
this._animationCount++;
this._updateTitle();
if (params.onComplete) {
let oldOnComplete = params.onComplete;
let oldOnCompleteScope = params.onCompleteScope;
let oldOnCompleteParams = params.onCompleteParams;
let eventScope = oldOnCompleteScope ? oldOnCompleteScope : this.actor;
params.onComplete = function () {
oldOnComplete.apply(eventScope, oldOnCompleteParams);
this._onAnimationComplete();
};
} else
params.onComplete = this._onAnimationComplete;
params.onCompleteScope = this;
Tweener.addTween(this.actor, params);
},
_onEnter: function (actor, event) {
// If the user drags faster than we can follow, he'll end up
// leaving the window temporarily and then re-entering it
@ -132,7 +106,7 @@ WindowClone.prototype = {
this._havePointer = false;
if (this._animationCount)
if (Tweener.isTweening(this.actor))
return;
actor.raise(this.stackAbove);
@ -140,7 +114,7 @@ WindowClone.prototype = {
},
_onButtonPress : function (actor, event) {
if (this._animationCount)
if (Tweener.isTweening(this.actor))
return;
actor.raise_top();
@ -209,13 +183,14 @@ WindowClone.prototype = {
this.emit('dragged', stageX, stageY, event.get_time());
if (this.realWindow.get_workspace() == origWorkspace) {
// Didn't get moved elsewhere, restore position
this.addTween({ x: this._dragStartX + this._dragOffsetX,
y: this._dragStartY + this._dragOffsetY,
time: SNAP_BACK_ANIMATION_TIME,
transition: "easeOutQuad",
onComplete: this._onSnapBackComplete,
onCompleteScope: this
});
Tweener.addTween(this.actor,
{ x: this._dragStartX + this._dragOffsetX,
y: this._dragStartY + this._dragOffsetY,
time: SNAP_BACK_ANIMATION_TIME,
transition: "easeOutQuad",
onComplete: this._onSnapBackComplete,
onCompleteScope: this
});
// Most likely, the clone is going to move away from the
// pointer now. But that won't cause a leave-event, so
// do this by hand. Of course, if the window only snaps
@ -234,12 +209,15 @@ WindowClone.prototype = {
this.actor.set_position(this._dragOrigX, this._dragOrigY);
},
_onAnimationComplete : function () {
this._animationCount--;
if (this._animationCount == 0) {
this._updateTitle();
this.actor.raise(this.stackAbove);
}
// Called by Tweener
onAnimationStart : function () {
this._updateTitle();
},
// Called by Tweener
onAnimationComplete : function () {
this._updateTitle();
this.actor.raise(this.stackAbove);
},
_createTitle : function () {
@ -313,7 +291,7 @@ WindowClone.prototype = {
_updateTitle : function () {
let shouldShow = (this._havePointer &&
!this._buttonDown &&
this._animationCount == 0);
!Tweener.isTweening(this.actor));
if (shouldShow)
this._showTitle();
@ -528,21 +506,16 @@ Workspace.prototype = {
let desiredSize = global.screen_width * fraction;
let scale = Math.min(desiredSize / size, 1.0);
let tweenProperties = {
x: xCenter - 0.5 * scale * clone.actor.width,
y: yCenter - 0.5 * scale * clone.actor.height,
scale_x: scale,
scale_y: scale,
time: Overlay.ANIMATION_TIME,
opacity: WINDOW_OPACITY,
transition: "easeOutQuad"
};
// workspace_relative assumes that the workspace is zooming in or out
if (workspaceZooming)
tweenProperties['workspace_relative'] = this;
clone.addTween(tweenProperties);
Tweener.addTween(clone.actor,
{ x: xCenter - 0.5 * scale * clone.actor.width,
y: yCenter - 0.5 * scale * clone.actor.height,
scale_x: scale,
scale_y: scale,
workspace_relative: workspaceZooming ? this : null,
time: Overlay.ANIMATION_TIME,
opacity: WINDOW_OPACITY,
transition: "easeOutQuad"
});
}
},
@ -659,15 +632,16 @@ Workspace.prototype = {
for (let i = 1; i < this._windows.length; i++) {
let clone = this._windows[i];
clone.addTween({ x: clone.origX,
y: clone.origY,
scale_x: 1.0,
scale_y: 1.0,
workspace_relative: this,
time: Overlay.ANIMATION_TIME,
opacity: 255,
transition: "easeOutQuad"
});
Tweener.addTween(clone.actor,
{ x: clone.origX,
y: clone.origY,
scale_x: 1.0,
scale_y: 1.0,
workspace_relative: this,
time: Overlay.ANIMATION_TIME,
opacity: 255,
transition: "easeOutQuad"
});
}
this.leavingOverlay = false;
@ -1207,6 +1181,9 @@ Tweener.registerSpecialPropertyModifier("workspace_relative", _workspace_relativ
function _workspace_relative_modifier(workspace) {
let endX, endY;
if (!workspace)
return [];
if (workspace.leavingOverlay) {
endX = workspace.fullSizeX;
endY = workspace.fullSizeY;