mirror of
https://github.com/roytam1/palemoon27.git
synced 2026-05-29 18:18:27 +00:00
import changes from `dev' branch of rmottola/Arctic-Fox:
- Bug 1163719 - Store startup cache entries in loading order; r=nfroyd (410ef75ff) - Bug 1072313: P1. Make MacIOSurface refcount thread-safe. r=mattwoodrow (3be93ce91) - Bug 1072313: P2 Prevent instanciating MacIOSurfaceLib directly. r=mattwoodrow (8d5d7e2e5) - Bug 1178513 - Export libxul symbols needed by ACL. r=mattwoodrow (b01035079) - Bug 1143575. Remove unused CompositableClient::OnTransaction. r=nical (8901f1fe8) - Bug 1143575. Convert SetCurrentImage(nullptr) callers to call ClearAllImages instead. r=nical (1a89f04f0) - Bug 1143575. Fix indent. r=cpearce (6b8f4e725) - Bug 1179111 part 1 - Implement CSSAnimation.animationName; r=smaug, r=jwatt (0d27b5d2e) - Bug 1179111 part 2 - Implement CSSTransition.transitionProperty; r=smaug, r=jwatt (f395dfc55) - Bug 1144615 - 2 - Adds a playbackRate selector widget to the animation panel; r=vporof (d538eb734) - Bug 1144615 - 1 - Minor css fixes in animation panel; r=bgrins (661a6feab) - Bug 1144615 - 3 - Tests for the playbackRate selection UI in the animation panel; r=vporof (82a4b4f91) - Bug 1120833 - 3 - Refresh the list of animation widgets when new animations are added; r=miker (f833abeb2) - Bug 1153903 - Get rid of logspam during devtools talos test runs;r=pbrosset (77f1968d2) - Bug 1155653 - Preview animation target nodes in animationinspector panel; r=bgrins (61f197d75) - Bug 1151018 - Refresh the list of Animation widgets when the animation panel loads; r=bgrins (9351a0b64) - Bug 1155663 - Show animations as synchronized time blocks in animation inspector; r=bgrins (66f544aea) - Bug 1149999 - 2 - Send animation removed events to the animation-panel for re-starting transitions; r=past (2913b4d4b) - Bug 1179111 part 3 - Make DevTools read the appropriate name property; r=pbrosset (6dfecd643) - Bug 1179111 part 4 - Remove KeyframeEffectReadOnly.name; r=smaug (85894f42f) - Bug 1004383 follow-up: Mark ElementPropertyTransition::AsTransition() as override (f37e36e8a) - Bug 1179111 part 5 - Remove Name() methods; r=jwatt (8909c4781) - Bug 1143575. Remove Theora-only duplicate frame optimization. r=cpearce (fe53385ec) - Bug 1172825 - MDSM playback should depend on IsPlaying() instead of |mPlayState|. r=cpearce. (9a9cf656d) - Bug 1163223 - Switch test_buffered to use timeupdate rather than loadedmetadata. r=cpearce (b91463faf) - Bug 1163223 - Move bailout case in GetBuffered into the readers. r=jww (c875c1d71) - Bug 1161901 - Dispatch MediaDecoderReader::SetIdle directly. r=jww (a9ad2582d) - Bug 1161901 - Use ProxyMediaCall instead of MDSM::ShutdownReader. r=jww (c866b524c) - Bug 1161901 - Hoist MDSM shutdown logic into MDSM::Shutdown and remove brittle requirement that RunStateMachine only happen once in SHUTDOWN state. r=jww (0d81368d6) - Bug 1153149 - Remove IsWaitingMediaResources() from PlatformDecoderModule. r=jya (70bd67ee3) - Bug 1161984 - Get rid of IsWaitingMediaResources() in MP4Reader. r=jya (94dd1f58c) - Bug 1163223 - Introduce StartTimeRendezvous and route samples through it. r=jww (b2a80c47a) - Bug 1163223 - Use AwaitStartTime to handle metadata end time. r=jww (8e22fc3fd) - Bug 1163223 - Use AwaitStartTime to invoke MediaDecoderReader::SetStartTime. r=jww (6141f5303) - Bug 1172387 - Clean up code of MediaDecoderStateMachine::StopAudioThread. r=kinetik. (717640128) - Bug 1163223 - Adjust incoming samples for start time. r=jww (c4029f934)
This commit is contained in:
@@ -55,7 +55,7 @@ let shutdown = Task.async(function*() {
|
||||
yield AnimationsController.destroy();
|
||||
// Don't assume that AnimationsPanel is defined here, it's in another file.
|
||||
if (typeof AnimationsPanel !== "undefined") {
|
||||
yield AnimationsPanel.destroy()
|
||||
yield AnimationsPanel.destroy();
|
||||
}
|
||||
gToolbox = gInspector = null;
|
||||
});
|
||||
@@ -97,8 +97,11 @@ let AnimationsController = {
|
||||
}
|
||||
this.initialized = promise.defer();
|
||||
|
||||
this.onPanelVisibilityChange = this.onPanelVisibilityChange.bind(this);
|
||||
this.onNewNodeFront = this.onNewNodeFront.bind(this);
|
||||
this.onAnimationMutations = this.onAnimationMutations.bind(this);
|
||||
|
||||
let target = gToolbox.target;
|
||||
this.animationsFront = new AnimationsFront(target.client, target.form);
|
||||
|
||||
// Expose actor capabilities.
|
||||
this.hasToggleAll = yield target.actorHasMethod("animations", "toggleAll");
|
||||
@@ -106,12 +109,19 @@ let AnimationsController = {
|
||||
"setCurrentTime");
|
||||
this.hasMutationEvents = yield target.actorHasMethod("animations",
|
||||
"stopAnimationPlayerUpdates");
|
||||
this.hasSetPlaybackRate = yield target.actorHasMethod("animationplayer",
|
||||
"setPlaybackRate");
|
||||
this.hasTargetNode = yield target.actorHasMethod("domwalker",
|
||||
"getNodeFromActor");
|
||||
this.isNewUI = Services.prefs.getBoolPref("devtools.inspector.animationInspectorV3");
|
||||
|
||||
this.onPanelVisibilityChange = this.onPanelVisibilityChange.bind(this);
|
||||
this.onNewNodeFront = this.onNewNodeFront.bind(this);
|
||||
if (this.destroyed) {
|
||||
console.warn("Could not fully initialize the AnimationsController");
|
||||
return;
|
||||
}
|
||||
|
||||
this.animationsFront = new AnimationsFront(target.client, target.form);
|
||||
this.startListeners();
|
||||
|
||||
yield this.onNewNodeFront();
|
||||
|
||||
this.initialized.resolve();
|
||||
@@ -151,6 +161,9 @@ let AnimationsController = {
|
||||
gInspector.selection.off("new-node-front", this.onNewNodeFront);
|
||||
gInspector.sidebar.off("select", this.onPanelVisibilityChange);
|
||||
gToolbox.off("select", this.onPanelVisibilityChange);
|
||||
if (this.isListeningToMutations) {
|
||||
this.animationsFront.off("mutations", this.onAnimationMutations);
|
||||
}
|
||||
},
|
||||
|
||||
isPanelVisible: function() {
|
||||
@@ -213,15 +226,55 @@ let AnimationsController = {
|
||||
|
||||
this.animationPlayers = yield this.animationsFront.getAnimationPlayersForNode(nodeFront);
|
||||
this.startAllAutoRefresh();
|
||||
|
||||
// Start listening for animation mutations only after the first method call
|
||||
// otherwise events won't be sent.
|
||||
if (!this.isListeningToMutations && this.hasMutationEvents) {
|
||||
this.animationsFront.on("mutations", this.onAnimationMutations);
|
||||
this.isListeningToMutations = true;
|
||||
}
|
||||
}),
|
||||
|
||||
onAnimationMutations: Task.async(function*(changes) {
|
||||
// Insert new players into this.animationPlayers when new animations are
|
||||
// added.
|
||||
for (let {type, player} of changes) {
|
||||
if (type === "added") {
|
||||
this.animationPlayers.push(player);
|
||||
if (!this.isNewUI) {
|
||||
player.startAutoRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
if (type === "removed") {
|
||||
if (!this.isNewUI) {
|
||||
player.stopAutoRefresh();
|
||||
}
|
||||
yield player.release();
|
||||
let index = this.animationPlayers.indexOf(player);
|
||||
this.animationPlayers.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Let the UI know the list has been updated.
|
||||
this.emit(this.PLAYERS_UPDATED_EVENT, this.animationPlayers);
|
||||
}),
|
||||
|
||||
startAllAutoRefresh: function() {
|
||||
if (this.isNewUI) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let front of this.animationPlayers) {
|
||||
front.startAutoRefresh();
|
||||
}
|
||||
},
|
||||
|
||||
stopAllAutoRefresh: function() {
|
||||
if (this.isNewUI) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let front of this.animationPlayers) {
|
||||
front.stopAutoRefresh();
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<span class="label">&allAnimations;</span>
|
||||
<button id="toggle-all" standalone="true" class="devtools-button"></button>
|
||||
</div>
|
||||
<div id="players" class="theme-toolbar"></div>
|
||||
<div id="players"></div>
|
||||
<div id="error-message">
|
||||
<p>&invalidElement;</p>
|
||||
<p>&selectElement;</p>
|
||||
|
||||
@@ -3,9 +3,19 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
/* globals AnimationsController, document, performance, promise,
|
||||
gToolbox, gInspector, requestAnimationFrame, cancelAnimationFrame, L10N */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {createNode} = require("devtools/animationinspector/utils");
|
||||
const {
|
||||
PlayerMetaDataHeader,
|
||||
PlaybackRateSelector,
|
||||
AnimationTargetNode,
|
||||
AnimationsTimeline
|
||||
} = require("devtools/animationinspector/components");
|
||||
|
||||
/**
|
||||
* The main animations panel UI.
|
||||
*/
|
||||
@@ -14,6 +24,11 @@ let AnimationsPanel = {
|
||||
PANEL_INITIALIZED: "panel-initialized",
|
||||
|
||||
initialize: Task.async(function*() {
|
||||
if (AnimationsController.destroyed) {
|
||||
console.warn("Could not initialize the animation-panel, controller " +
|
||||
"was destroyed");
|
||||
return;
|
||||
}
|
||||
if (this.initialized) {
|
||||
return this.initialized.promise;
|
||||
}
|
||||
@@ -34,12 +49,19 @@ let AnimationsPanel = {
|
||||
this.togglePicker = hUtils.togglePicker.bind(hUtils);
|
||||
this.onPickerStarted = this.onPickerStarted.bind(this);
|
||||
this.onPickerStopped = this.onPickerStopped.bind(this);
|
||||
this.createPlayerWidgets = this.createPlayerWidgets.bind(this);
|
||||
this.refreshAnimations = this.refreshAnimations.bind(this);
|
||||
this.toggleAll = this.toggleAll.bind(this);
|
||||
this.onTabNavigated = this.onTabNavigated.bind(this);
|
||||
|
||||
this.startListeners();
|
||||
|
||||
if (AnimationsController.isNewUI) {
|
||||
this.animationsTimelineComponent = new AnimationsTimeline(gInspector);
|
||||
this.animationsTimelineComponent.init(this.playersEl);
|
||||
}
|
||||
|
||||
yield this.refreshAnimations();
|
||||
|
||||
this.initialized.resolve();
|
||||
|
||||
this.emit(this.PANEL_INITIALIZED);
|
||||
@@ -56,6 +78,11 @@ let AnimationsPanel = {
|
||||
this.destroyed = promise.defer();
|
||||
|
||||
this.stopListeners();
|
||||
|
||||
if (this.animationsTimelineComponent) {
|
||||
this.animationsTimelineComponent.destroy();
|
||||
this.animationsTimelineComponent = null;
|
||||
}
|
||||
yield this.destroyPlayerWidgets();
|
||||
|
||||
this.playersEl = this.errorMessageEl = null;
|
||||
@@ -66,7 +93,7 @@ let AnimationsPanel = {
|
||||
|
||||
startListeners: function() {
|
||||
AnimationsController.on(AnimationsController.PLAYERS_UPDATED_EVENT,
|
||||
this.createPlayerWidgets);
|
||||
this.refreshAnimations);
|
||||
|
||||
this.pickerButtonEl.addEventListener("click", this.togglePicker, false);
|
||||
gToolbox.on("picker-started", this.onPickerStarted);
|
||||
@@ -78,7 +105,7 @@ let AnimationsPanel = {
|
||||
|
||||
stopListeners: function() {
|
||||
AnimationsController.off(AnimationsController.PLAYERS_UPDATED_EVENT,
|
||||
this.createPlayerWidgets);
|
||||
this.refreshAnimations);
|
||||
|
||||
this.pickerButtonEl.removeEventListener("click", this.togglePicker, false);
|
||||
gToolbox.off("picker-started", this.onPickerStarted);
|
||||
@@ -109,16 +136,18 @@ let AnimationsPanel = {
|
||||
toggleAll: Task.async(function*() {
|
||||
let btnClass = this.toggleAllButtonEl.classList;
|
||||
|
||||
// Toggling all animations is async and it may be some time before each of
|
||||
// the current players get their states updated, so toggle locally too, to
|
||||
// avoid the timelines from jumping back and forth.
|
||||
if (this.playerWidgets) {
|
||||
let currentWidgetStateChange = [];
|
||||
for (let widget of this.playerWidgets) {
|
||||
currentWidgetStateChange.push(btnClass.contains("paused")
|
||||
? widget.play() : widget.pause());
|
||||
if (!AnimationsController.isNewUI) {
|
||||
// Toggling all animations is async and it may be some time before each of
|
||||
// the current players get their states updated, so toggle locally too, to
|
||||
// avoid the timelines from jumping back and forth.
|
||||
if (this.playerWidgets) {
|
||||
let currentWidgetStateChange = [];
|
||||
for (let widget of this.playerWidgets) {
|
||||
currentWidgetStateChange.push(btnClass.contains("paused")
|
||||
? widget.play() : widget.pause());
|
||||
}
|
||||
yield promise.all(currentWidgetStateChange).catch(Cu.reportError);
|
||||
}
|
||||
yield promise.all(currentWidgetStateChange).catch(Cu.reportError);
|
||||
}
|
||||
|
||||
btnClass.toggle("paused");
|
||||
@@ -129,14 +158,21 @@ let AnimationsPanel = {
|
||||
this.toggleAllButtonEl.classList.remove("paused");
|
||||
},
|
||||
|
||||
createPlayerWidgets: Task.async(function*() {
|
||||
refreshAnimations: Task.async(function*() {
|
||||
let done = gInspector.updating("animationspanel");
|
||||
|
||||
// Empty the whole panel first.
|
||||
this.hideErrorMessage();
|
||||
yield this.destroyPlayerWidgets();
|
||||
|
||||
// If there are no players to show, show the error message instead and return.
|
||||
// Re-render the timeline component.
|
||||
if (this.animationsTimelineComponent) {
|
||||
this.animationsTimelineComponent.render(
|
||||
AnimationsController.animationPlayers);
|
||||
}
|
||||
|
||||
// If there are no players to show, show the error message instead and
|
||||
// return.
|
||||
if (!AnimationsController.animationPlayers.length) {
|
||||
this.displayErrorMessage();
|
||||
this.emit(this.UI_UPDATED_EVENT);
|
||||
@@ -144,17 +180,21 @@ let AnimationsPanel = {
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, create player widgets.
|
||||
this.playerWidgets = [];
|
||||
let initPromises = [];
|
||||
// Otherwise, create player widgets (only when isNewUI is false, the
|
||||
// timeline has already been re-rendered).
|
||||
if (!AnimationsController.isNewUI) {
|
||||
this.playerWidgets = [];
|
||||
let initPromises = [];
|
||||
|
||||
for (let player of AnimationsController.animationPlayers) {
|
||||
let widget = new PlayerWidget(player, this.playersEl);
|
||||
initPromises.push(widget.initialize());
|
||||
this.playerWidgets.push(widget);
|
||||
for (let player of AnimationsController.animationPlayers) {
|
||||
let widget = new PlayerWidget(player, this.playersEl);
|
||||
initPromises.push(widget.initialize());
|
||||
this.playerWidgets.push(widget);
|
||||
}
|
||||
|
||||
yield initPromises;
|
||||
}
|
||||
|
||||
yield initPromises;
|
||||
this.emit(this.UI_UPDATED_EVENT);
|
||||
done();
|
||||
}),
|
||||
@@ -187,8 +227,15 @@ function PlayerWidget(player, containerEl) {
|
||||
this.onRewindBtnClick = this.onRewindBtnClick.bind(this);
|
||||
this.onFastForwardBtnClick = this.onFastForwardBtnClick.bind(this);
|
||||
this.onCurrentTimeChanged = this.onCurrentTimeChanged.bind(this);
|
||||
this.onPlaybackRateChanged = this.onPlaybackRateChanged.bind(this);
|
||||
|
||||
this.metaDataComponent = new PlayerMetaDataHeader();
|
||||
if (AnimationsController.hasSetPlaybackRate) {
|
||||
this.rateComponent = new PlaybackRateSelector();
|
||||
}
|
||||
if (AnimationsController.hasTargetNode) {
|
||||
this.targetNodeComponent = new AnimationTargetNode(gInspector);
|
||||
}
|
||||
}
|
||||
|
||||
PlayerWidget.prototype = {
|
||||
@@ -211,6 +258,12 @@ PlayerWidget.prototype = {
|
||||
this.stopTimelineAnimation();
|
||||
this.stopListeners();
|
||||
this.metaDataComponent.destroy();
|
||||
if (this.rateComponent) {
|
||||
this.rateComponent.destroy();
|
||||
}
|
||||
if (this.targetNodeComponent) {
|
||||
this.targetNodeComponent.destroy();
|
||||
}
|
||||
|
||||
this.el.remove();
|
||||
this.playPauseBtnEl = this.rewindBtnEl = this.fastForwardBtnEl = null;
|
||||
@@ -226,6 +279,9 @@ PlayerWidget.prototype = {
|
||||
this.fastForwardBtnEl.addEventListener("click", this.onFastForwardBtnClick);
|
||||
this.currentTimeEl.addEventListener("input", this.onCurrentTimeChanged);
|
||||
}
|
||||
if (this.rateComponent) {
|
||||
this.rateComponent.on("rate-changed", this.onPlaybackRateChanged);
|
||||
}
|
||||
},
|
||||
|
||||
stopListeners: function() {
|
||||
@@ -236,18 +292,27 @@ PlayerWidget.prototype = {
|
||||
this.fastForwardBtnEl.removeEventListener("click", this.onFastForwardBtnClick);
|
||||
this.currentTimeEl.removeEventListener("input", this.onCurrentTimeChanged);
|
||||
}
|
||||
if (this.rateComponent) {
|
||||
this.rateComponent.off("rate-changed", this.onPlaybackRateChanged);
|
||||
}
|
||||
},
|
||||
|
||||
createMarkup: function() {
|
||||
let state = this.player.state;
|
||||
|
||||
this.el = createNode({
|
||||
parent: this.containerEl,
|
||||
attributes: {
|
||||
"class": "player-widget " + state.playState
|
||||
}
|
||||
});
|
||||
|
||||
this.metaDataComponent.createMarkup(this.el);
|
||||
if (this.targetNodeComponent) {
|
||||
this.targetNodeComponent.init(this.el);
|
||||
this.targetNodeComponent.render(this.player);
|
||||
}
|
||||
|
||||
this.metaDataComponent.init(this.el);
|
||||
this.metaDataComponent.render(state);
|
||||
|
||||
// Timeline widget.
|
||||
@@ -293,6 +358,11 @@ PlayerWidget.prototype = {
|
||||
});
|
||||
}
|
||||
|
||||
if (this.rateComponent) {
|
||||
this.rateComponent.init(playbackControlsEl);
|
||||
this.rateComponent.render(state);
|
||||
}
|
||||
|
||||
// Sliders container.
|
||||
let slidersContainerEl = createNode({
|
||||
parent: timelineEl,
|
||||
@@ -335,8 +405,6 @@ PlayerWidget.prototype = {
|
||||
}
|
||||
});
|
||||
|
||||
this.containerEl.appendChild(this.el);
|
||||
|
||||
// Show the initial time.
|
||||
this.displayTime(state.currentTime);
|
||||
},
|
||||
@@ -351,9 +419,8 @@ PlayerWidget.prototype = {
|
||||
onPlayPauseBtnClick: function() {
|
||||
if (this.player.state.playState === "running") {
|
||||
return this.pause();
|
||||
} else {
|
||||
return this.play();
|
||||
}
|
||||
return this.play();
|
||||
},
|
||||
|
||||
onRewindBtnClick: function() {
|
||||
@@ -365,7 +432,7 @@ PlayerWidget.prototype = {
|
||||
|
||||
let time = state.duration;
|
||||
if (state.iterationCount) {
|
||||
time = state.iterationCount * state.duration;
|
||||
time = state.iterationCount * state.duration;
|
||||
}
|
||||
this.setCurrentTime(time, true);
|
||||
},
|
||||
@@ -378,19 +445,30 @@ PlayerWidget.prototype = {
|
||||
this.setCurrentTime(parseFloat(time), true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Executed when the playback rate dropdown value changes in the playbackrate
|
||||
* component.
|
||||
*/
|
||||
onPlaybackRateChanged: function(e, rate) {
|
||||
this.setPlaybackRate(rate);
|
||||
},
|
||||
|
||||
/**
|
||||
* Whenever a player state update is received.
|
||||
*/
|
||||
onStateChanged: function() {
|
||||
let state = this.player.state;
|
||||
|
||||
this.updateWidgetState(state);
|
||||
this.metaDataComponent.render(state);
|
||||
if (this.rateComponent) {
|
||||
this.rateComponent.render(state);
|
||||
}
|
||||
|
||||
switch (state.playState) {
|
||||
case "finished":
|
||||
this.stopTimelineAnimation();
|
||||
this.displayTime(this.player.state.duration);
|
||||
this.stopListeners();
|
||||
this.displayTime(this.player.state.currentTime);
|
||||
break;
|
||||
case "running":
|
||||
this.startTimelineAnimation();
|
||||
@@ -399,6 +477,10 @@ PlayerWidget.prototype = {
|
||||
this.stopTimelineAnimation();
|
||||
this.displayTime(this.player.state.currentTime);
|
||||
break;
|
||||
case "idle":
|
||||
this.stopTimelineAnimation();
|
||||
this.displayTime(0);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -410,7 +492,8 @@ PlayerWidget.prototype = {
|
||||
*/
|
||||
setCurrentTime: Task.async(function*(time, shouldPause) {
|
||||
if (!AnimationsController.hasSetCurrentTime) {
|
||||
throw new Error("This server version doesn't support setting animations' currentTime");
|
||||
throw new Error("This server version doesn't support setting " +
|
||||
"animations' currentTime");
|
||||
}
|
||||
|
||||
if (shouldPause) {
|
||||
@@ -429,16 +512,26 @@ PlayerWidget.prototype = {
|
||||
yield this.player.setCurrentTime(time);
|
||||
}),
|
||||
|
||||
/**
|
||||
* Set the playback rate of the animation.
|
||||
* @param {Number} rate.
|
||||
* @return {Promise} Resolves when the rate has been set.
|
||||
*/
|
||||
setPlaybackRate: function(rate) {
|
||||
if (!AnimationsController.hasSetPlaybackRate) {
|
||||
throw new Error("This server version doesn't support setting " +
|
||||
"animations' playbackRate");
|
||||
}
|
||||
|
||||
return this.player.setPlaybackRate(rate);
|
||||
},
|
||||
|
||||
/**
|
||||
* Pause the animation player via this widget.
|
||||
* @return {Promise} Resolves when the player is paused, the button is
|
||||
* switched to the right state, and the timeline animation is stopped.
|
||||
*/
|
||||
pause: function() {
|
||||
if (this.player.state.playState === "finished") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Switch to the right className on the element right away to avoid waiting
|
||||
// for the next state update to change the playPause icon.
|
||||
this.updateWidgetState({playState: "paused"});
|
||||
@@ -452,10 +545,6 @@ PlayerWidget.prototype = {
|
||||
* switched to the right state, and the timeline animation is started.
|
||||
*/
|
||||
play: function() {
|
||||
if (this.player.state.playState === "finished") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Switch to the right className on the element right away to avoid waiting
|
||||
// for the next state update to change the playPause icon.
|
||||
this.updateWidgetState({playState: "running"});
|
||||
@@ -479,7 +568,8 @@ PlayerWidget.prototype = {
|
||||
let start = performance.now();
|
||||
let loop = () => {
|
||||
this.rafID = requestAnimationFrame(loop);
|
||||
let now = state.currentTime + performance.now() - start;
|
||||
let delta = (performance.now() - start) * state.playbackRate;
|
||||
let now = state.currentTime + delta;
|
||||
this.displayTime(now);
|
||||
};
|
||||
|
||||
@@ -526,180 +616,3 @@ PlayerWidget.prototype = {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* UI component responsible for displaying and updating the player meta-data:
|
||||
* name, duration, iterations, delay.
|
||||
* The parent UI component for this should drive its updates by calling
|
||||
* render(state) whenever it wants the component to update.
|
||||
*/
|
||||
function PlayerMetaDataHeader() {
|
||||
// Store the various state pieces we need to only refresh the UI when things
|
||||
// change.
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
PlayerMetaDataHeader.prototype = {
|
||||
createMarkup: function(containerEl) {
|
||||
// The main title element.
|
||||
this.el = createNode({
|
||||
parent: containerEl,
|
||||
attributes: {
|
||||
"class": "animation-title"
|
||||
}
|
||||
});
|
||||
|
||||
// Animation name.
|
||||
this.nameLabel = createNode({
|
||||
parent: this.el,
|
||||
nodeType: "span"
|
||||
});
|
||||
|
||||
this.nameValue = createNode({
|
||||
parent: this.el,
|
||||
nodeType: "strong",
|
||||
attributes: {
|
||||
"style": "display:none;"
|
||||
}
|
||||
});
|
||||
|
||||
// Animation duration, delay and iteration container.
|
||||
let metaData = createNode({
|
||||
parent: this.el,
|
||||
nodeType: "span",
|
||||
attributes: {
|
||||
"class": "meta-data"
|
||||
}
|
||||
});
|
||||
|
||||
// Animation duration.
|
||||
this.durationLabel = createNode({
|
||||
parent: metaData,
|
||||
nodeType: "span"
|
||||
});
|
||||
this.durationLabel.textContent = L10N.getStr("player.animationDurationLabel");
|
||||
|
||||
this.durationValue = createNode({
|
||||
parent: metaData,
|
||||
nodeType: "strong"
|
||||
});
|
||||
|
||||
// Animation delay (hidden by default since there may not be a delay).
|
||||
this.delayLabel = createNode({
|
||||
parent: metaData,
|
||||
nodeType: "span",
|
||||
attributes: {
|
||||
"style": "display:none;"
|
||||
}
|
||||
});
|
||||
this.delayLabel.textContent = L10N.getStr("player.animationDelayLabel");
|
||||
|
||||
this.delayValue = createNode({
|
||||
parent: metaData,
|
||||
nodeType: "strong"
|
||||
});
|
||||
|
||||
// Animation iteration count (also hidden by default since we don't display
|
||||
// single iterations).
|
||||
this.iterationLabel = createNode({
|
||||
parent: metaData,
|
||||
nodeType: "span",
|
||||
attributes: {
|
||||
"style": "display:none;"
|
||||
}
|
||||
});
|
||||
this.iterationLabel.textContent = L10N.getStr("player.animationIterationCountLabel");
|
||||
|
||||
this.iterationValue = createNode({
|
||||
parent: metaData,
|
||||
nodeType: "strong",
|
||||
attributes: {
|
||||
"style": "display:none;"
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
this.state = null;
|
||||
this.el.remove();
|
||||
this.el = null;
|
||||
this.nameLabel = this.nameValue = null;
|
||||
this.durationLabel = this.durationValue = null;
|
||||
this.delayLabel = this.delayValue = null;
|
||||
this.iterationLabel = this.iterationValue = null;
|
||||
},
|
||||
|
||||
render: function(state) {
|
||||
// Update the name if needed.
|
||||
if (state.name !== this.state.name) {
|
||||
if (state.name) {
|
||||
// Animations (and transitions since bug 1122414) have names.
|
||||
this.nameLabel.textContent = L10N.getStr("player.animationNameLabel");
|
||||
this.nameValue.style.display = "inline";
|
||||
this.nameValue.textContent = state.name;
|
||||
} else {
|
||||
// With older actors, Css transitions don't have names.
|
||||
this.nameLabel.textContent = L10N.getStr("player.transitionNameLabel");
|
||||
this.nameValue.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
// update the duration value if needed.
|
||||
if (state.duration !== this.state.duration) {
|
||||
this.durationValue.textContent = L10N.getFormatStr("player.timeLabel",
|
||||
L10N.numberWithDecimals(state.duration / 1000, 2));
|
||||
}
|
||||
|
||||
// Update the delay if needed.
|
||||
if (state.delay !== this.state.delay) {
|
||||
if (state.delay) {
|
||||
this.delayLabel.style.display = "inline";
|
||||
this.delayValue.style.display = "inline";
|
||||
this.delayValue.textContent = L10N.getFormatStr("player.timeLabel",
|
||||
L10N.numberWithDecimals(state.delay / 1000, 2));
|
||||
} else {
|
||||
// Hide the delay elements if there is no delay defined.
|
||||
this.delayLabel.style.display = "none";
|
||||
this.delayValue.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
// Update the iterationCount if needed.
|
||||
if (state.iterationCount !== this.state.iterationCount) {
|
||||
if (state.iterationCount !== 1) {
|
||||
this.iterationLabel.style.display = "inline";
|
||||
this.iterationValue.style.display = "inline";
|
||||
let count = state.iterationCount ||
|
||||
L10N.getStr("player.infiniteIterationCount");
|
||||
this.iterationValue.innerHTML = count;
|
||||
} else {
|
||||
// Hide the iteration elements if iteration is 1.
|
||||
this.iterationLabel.style.display = "none";
|
||||
this.iterationValue.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
this.state = state;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* DOM node creation helper function.
|
||||
* @param {Object} Options to customize the node to be created.
|
||||
* @return {DOMNode} The newly created node.
|
||||
*/
|
||||
function createNode(options) {
|
||||
let type = options.nodeType || "div";
|
||||
let node = document.createElement(type);
|
||||
|
||||
for (let name in options.attributes || {}) {
|
||||
let value = options.attributes[name];
|
||||
node.setAttribute(name, value);
|
||||
}
|
||||
|
||||
if (options.parent) {
|
||||
options.parent.appendChild(node);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,826 @@
|
||||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
/* globals ViewHelpers */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Set of reusable UI components for the animation-inspector UI.
|
||||
// All components in this module share a common API:
|
||||
// 1. construct the component:
|
||||
// let c = new ComponentName();
|
||||
// 2. initialize the markup of the component in a given parent node:
|
||||
// c.init(containerElement);
|
||||
// 3. render the component, passing in some sort of state:
|
||||
// This may be called over and over again when the state changes, to update
|
||||
// the component output.
|
||||
// c.render(state);
|
||||
// 4. destroy the component:
|
||||
// c.destroy();
|
||||
|
||||
const {Cu} = require("chrome");
|
||||
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
|
||||
const {
|
||||
createNode,
|
||||
drawGraphElementBackground,
|
||||
findOptimalTimeInterval
|
||||
} = require("devtools/animationinspector/utils");
|
||||
|
||||
const STRINGS_URI = "chrome://browser/locale/devtools/animationinspector.properties";
|
||||
const L10N = new ViewHelpers.L10N(STRINGS_URI);
|
||||
const MILLIS_TIME_FORMAT_MAX_DURATION = 4000;
|
||||
// The minimum spacing between 2 time graduation headers in the timeline (ms).
|
||||
const TIME_GRADUATION_MIN_SPACING = 40;
|
||||
|
||||
/**
|
||||
* UI component responsible for displaying and updating the player meta-data:
|
||||
* name, duration, iterations, delay.
|
||||
* The parent UI component for this should drive its updates by calling
|
||||
* render(state) whenever it wants the component to update.
|
||||
*/
|
||||
function PlayerMetaDataHeader() {
|
||||
// Store the various state pieces we need to only refresh the UI when things
|
||||
// change.
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
exports.PlayerMetaDataHeader = PlayerMetaDataHeader;
|
||||
|
||||
PlayerMetaDataHeader.prototype = {
|
||||
init: function(containerEl) {
|
||||
// The main title element.
|
||||
this.el = createNode({
|
||||
parent: containerEl,
|
||||
attributes: {
|
||||
"class": "animation-title"
|
||||
}
|
||||
});
|
||||
|
||||
// Animation name.
|
||||
this.nameLabel = createNode({
|
||||
parent: this.el,
|
||||
nodeType: "span"
|
||||
});
|
||||
|
||||
this.nameValue = createNode({
|
||||
parent: this.el,
|
||||
nodeType: "strong",
|
||||
attributes: {
|
||||
"style": "display:none;"
|
||||
}
|
||||
});
|
||||
|
||||
// Animation duration, delay and iteration container.
|
||||
let metaData = createNode({
|
||||
parent: this.el,
|
||||
nodeType: "span",
|
||||
attributes: {
|
||||
"class": "meta-data"
|
||||
}
|
||||
});
|
||||
|
||||
// Animation duration.
|
||||
this.durationLabel = createNode({
|
||||
parent: metaData,
|
||||
nodeType: "span",
|
||||
textContent: L10N.getStr("player.animationDurationLabel")
|
||||
});
|
||||
|
||||
this.durationValue = createNode({
|
||||
parent: metaData,
|
||||
nodeType: "strong"
|
||||
});
|
||||
|
||||
// Animation delay (hidden by default since there may not be a delay).
|
||||
this.delayLabel = createNode({
|
||||
parent: metaData,
|
||||
nodeType: "span",
|
||||
attributes: {
|
||||
"style": "display:none;"
|
||||
},
|
||||
textContent: L10N.getStr("player.animationDelayLabel")
|
||||
});
|
||||
|
||||
this.delayValue = createNode({
|
||||
parent: metaData,
|
||||
nodeType: "strong"
|
||||
});
|
||||
|
||||
// Animation iteration count (also hidden by default since we don't display
|
||||
// single iterations).
|
||||
this.iterationLabel = createNode({
|
||||
parent: metaData,
|
||||
nodeType: "span",
|
||||
attributes: {
|
||||
"style": "display:none;"
|
||||
},
|
||||
textContent: L10N.getStr("player.animationIterationCountLabel")
|
||||
});
|
||||
|
||||
this.iterationValue = createNode({
|
||||
parent: metaData,
|
||||
nodeType: "strong",
|
||||
attributes: {
|
||||
"style": "display:none;"
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
this.state = null;
|
||||
this.el.remove();
|
||||
this.el = null;
|
||||
this.nameLabel = this.nameValue = null;
|
||||
this.durationLabel = this.durationValue = null;
|
||||
this.delayLabel = this.delayValue = null;
|
||||
this.iterationLabel = this.iterationValue = null;
|
||||
},
|
||||
|
||||
render: function(state) {
|
||||
// Update the name if needed.
|
||||
if (state.name !== this.state.name) {
|
||||
if (state.name) {
|
||||
// Animations (and transitions since bug 1122414) have names.
|
||||
this.nameLabel.textContent = L10N.getStr("player.animationNameLabel");
|
||||
this.nameValue.style.display = "inline";
|
||||
this.nameValue.textContent = state.name;
|
||||
} else {
|
||||
// With older actors, Css transitions don't have names.
|
||||
this.nameLabel.textContent = L10N.getStr("player.transitionNameLabel");
|
||||
this.nameValue.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
// update the duration value if needed.
|
||||
if (state.duration !== this.state.duration) {
|
||||
this.durationValue.textContent = L10N.getFormatStr("player.timeLabel",
|
||||
L10N.numberWithDecimals(state.duration / 1000, 2));
|
||||
}
|
||||
|
||||
// Update the delay if needed.
|
||||
if (state.delay !== this.state.delay) {
|
||||
if (state.delay) {
|
||||
this.delayLabel.style.display = "inline";
|
||||
this.delayValue.style.display = "inline";
|
||||
this.delayValue.textContent = L10N.getFormatStr("player.timeLabel",
|
||||
L10N.numberWithDecimals(state.delay / 1000, 2));
|
||||
} else {
|
||||
// Hide the delay elements if there is no delay defined.
|
||||
this.delayLabel.style.display = "none";
|
||||
this.delayValue.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
// Update the iterationCount if needed.
|
||||
if (state.iterationCount !== this.state.iterationCount) {
|
||||
if (state.iterationCount !== 1) {
|
||||
this.iterationLabel.style.display = "inline";
|
||||
this.iterationValue.style.display = "inline";
|
||||
let count = state.iterationCount ||
|
||||
L10N.getStr("player.infiniteIterationCount");
|
||||
this.iterationValue.innerHTML = count;
|
||||
} else {
|
||||
// Hide the iteration elements if iteration is 1.
|
||||
this.iterationLabel.style.display = "none";
|
||||
this.iterationValue.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
this.state = state;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* UI component responsible for displaying the playback rate drop-down in each
|
||||
* player widget, updating it when the state changes, and emitting events when
|
||||
* the user selects a new value.
|
||||
* The parent UI component for this should drive its updates by calling
|
||||
* render(state) whenever it wants the component to update.
|
||||
*/
|
||||
function PlaybackRateSelector() {
|
||||
this.currentRate = null;
|
||||
this.onSelectionChanged = this.onSelectionChanged.bind(this);
|
||||
EventEmitter.decorate(this);
|
||||
}
|
||||
|
||||
exports.PlaybackRateSelector = PlaybackRateSelector;
|
||||
|
||||
PlaybackRateSelector.prototype = {
|
||||
PRESETS: [.1, .5, 1, 2, 5, 10],
|
||||
|
||||
init: function(containerEl) {
|
||||
// This component is simple enough that we can re-create the markup every
|
||||
// time it's rendered. So here we only store the parentEl.
|
||||
this.parentEl = containerEl;
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
this.removeSelect();
|
||||
this.parentEl = this.el = null;
|
||||
},
|
||||
|
||||
removeSelect: function() {
|
||||
if (this.el) {
|
||||
this.el.removeEventListener("change", this.onSelectionChanged);
|
||||
this.el.remove();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the ordered list of presets, including the current playbackRate if
|
||||
* different from the existing presets.
|
||||
*/
|
||||
getCurrentPresets: function({playbackRate}) {
|
||||
return [...new Set([...this.PRESETS, playbackRate])].sort((a, b) => a > b);
|
||||
},
|
||||
|
||||
render: function(state) {
|
||||
if (state.playbackRate === this.currentRate) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.removeSelect();
|
||||
|
||||
this.el = createNode({
|
||||
parent: this.parentEl,
|
||||
nodeType: "select",
|
||||
attributes: {
|
||||
"class": "rate devtools-button"
|
||||
}
|
||||
});
|
||||
|
||||
for (let preset of this.getCurrentPresets(state)) {
|
||||
let option = createNode({
|
||||
parent: this.el,
|
||||
nodeType: "option",
|
||||
attributes: {
|
||||
value: preset,
|
||||
},
|
||||
textContent: L10N.getFormatStr("player.playbackRateLabel", preset)
|
||||
});
|
||||
if (preset === state.playbackRate) {
|
||||
option.setAttribute("selected", "");
|
||||
}
|
||||
}
|
||||
|
||||
this.el.addEventListener("change", this.onSelectionChanged);
|
||||
|
||||
this.currentRate = state.playbackRate;
|
||||
},
|
||||
|
||||
onSelectionChanged: function() {
|
||||
this.emit("rate-changed", parseFloat(this.el.value));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* UI component responsible for displaying a preview of the target dom node of
|
||||
* a given animation.
|
||||
* @param {InspectorPanel} inspector Requires a reference to the inspector-panel
|
||||
* to highlight and select the node, as well as refresh it when there are
|
||||
* mutations.
|
||||
* @param {Object} options Supported properties are:
|
||||
* - compact {Boolean} Defaults to false. If true, nodes will be previewed like
|
||||
* tag#id.class instead of <tag id="id" class="class">
|
||||
*/
|
||||
function AnimationTargetNode(inspector, options={}) {
|
||||
this.inspector = inspector;
|
||||
this.options = options;
|
||||
|
||||
this.onPreviewMouseOver = this.onPreviewMouseOver.bind(this);
|
||||
this.onPreviewMouseOut = this.onPreviewMouseOut.bind(this);
|
||||
this.onSelectNodeClick = this.onSelectNodeClick.bind(this);
|
||||
this.onMarkupMutations = this.onMarkupMutations.bind(this);
|
||||
|
||||
EventEmitter.decorate(this);
|
||||
}
|
||||
|
||||
exports.AnimationTargetNode = AnimationTargetNode;
|
||||
|
||||
AnimationTargetNode.prototype = {
|
||||
init: function(containerEl) {
|
||||
let document = containerEl.ownerDocument;
|
||||
|
||||
// Init the markup for displaying the target node.
|
||||
this.el = createNode({
|
||||
parent: containerEl,
|
||||
attributes: {
|
||||
"class": "animation-target"
|
||||
}
|
||||
});
|
||||
|
||||
// Icon to select the node in the inspector.
|
||||
this.selectNodeEl = createNode({
|
||||
parent: this.el,
|
||||
nodeType: "span",
|
||||
attributes: {
|
||||
"class": "node-selector"
|
||||
}
|
||||
});
|
||||
|
||||
// Wrapper used for mouseover/out event handling.
|
||||
this.previewEl = createNode({
|
||||
parent: this.el,
|
||||
nodeType: "span"
|
||||
});
|
||||
|
||||
if (!this.options.compact) {
|
||||
this.previewEl.appendChild(document.createTextNode("<"));
|
||||
}
|
||||
|
||||
// Tag name.
|
||||
this.tagNameEl = createNode({
|
||||
parent: this.previewEl,
|
||||
nodeType: "span",
|
||||
attributes: {
|
||||
"class": "tag-name theme-fg-color3"
|
||||
}
|
||||
});
|
||||
|
||||
// Id attribute container.
|
||||
this.idEl = createNode({
|
||||
parent: this.previewEl,
|
||||
nodeType: "span"
|
||||
});
|
||||
|
||||
if (!this.options.compact) {
|
||||
createNode({
|
||||
parent: this.idEl,
|
||||
nodeType: "span",
|
||||
attributes: {
|
||||
"class": "attribute-name theme-fg-color2"
|
||||
},
|
||||
textContent: "id"
|
||||
});
|
||||
this.idEl.appendChild(document.createTextNode("=\""));
|
||||
} else {
|
||||
createNode({
|
||||
parent: this.idEl,
|
||||
nodeType: "span",
|
||||
attributes: {
|
||||
"class": "theme-fg-color2"
|
||||
},
|
||||
textContent: "#"
|
||||
});
|
||||
}
|
||||
|
||||
createNode({
|
||||
parent: this.idEl,
|
||||
nodeType: "span",
|
||||
attributes: {
|
||||
"class": "attribute-value theme-fg-color6"
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.options.compact) {
|
||||
this.idEl.appendChild(document.createTextNode("\""));
|
||||
}
|
||||
|
||||
// Class attribute container.
|
||||
this.classEl = createNode({
|
||||
parent: this.previewEl,
|
||||
nodeType: "span"
|
||||
});
|
||||
|
||||
if (!this.options.compact) {
|
||||
createNode({
|
||||
parent: this.classEl,
|
||||
nodeType: "span",
|
||||
attributes: {
|
||||
"class": "attribute-name theme-fg-color2"
|
||||
},
|
||||
textContent: "class"
|
||||
});
|
||||
this.classEl.appendChild(document.createTextNode("=\""));
|
||||
} else {
|
||||
createNode({
|
||||
parent: this.classEl,
|
||||
nodeType: "span",
|
||||
attributes: {
|
||||
"class": "theme-fg-color6"
|
||||
},
|
||||
textContent: "."
|
||||
});
|
||||
}
|
||||
|
||||
createNode({
|
||||
parent: this.classEl,
|
||||
nodeType: "span",
|
||||
attributes: {
|
||||
"class": "attribute-value theme-fg-color6"
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.options.compact) {
|
||||
this.classEl.appendChild(document.createTextNode("\""));
|
||||
this.previewEl.appendChild(document.createTextNode(">"));
|
||||
}
|
||||
|
||||
// Init events for highlighting and selecting the node.
|
||||
this.previewEl.addEventListener("mouseover", this.onPreviewMouseOver);
|
||||
this.previewEl.addEventListener("mouseout", this.onPreviewMouseOut);
|
||||
this.selectNodeEl.addEventListener("click", this.onSelectNodeClick);
|
||||
|
||||
// Start to listen for markupmutation events.
|
||||
this.inspector.on("markupmutation", this.onMarkupMutations);
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
this.inspector.off("markupmutation", this.onMarkupMutations);
|
||||
this.previewEl.removeEventListener("mouseover", this.onPreviewMouseOver);
|
||||
this.previewEl.removeEventListener("mouseout", this.onPreviewMouseOut);
|
||||
this.selectNodeEl.removeEventListener("click", this.onSelectNodeClick);
|
||||
this.el.remove();
|
||||
this.el = this.tagNameEl = this.idEl = this.classEl = null;
|
||||
this.selectNodeEl = this.previewEl = null;
|
||||
this.nodeFront = this.inspector = this.playerFront = null;
|
||||
},
|
||||
|
||||
onPreviewMouseOver: function() {
|
||||
if (!this.nodeFront) {
|
||||
return;
|
||||
}
|
||||
this.inspector.toolbox.highlighterUtils.highlightNodeFront(this.nodeFront);
|
||||
},
|
||||
|
||||
onPreviewMouseOut: function() {
|
||||
this.inspector.toolbox.highlighterUtils.unhighlight();
|
||||
},
|
||||
|
||||
onSelectNodeClick: function() {
|
||||
if (!this.nodeFront) {
|
||||
return;
|
||||
}
|
||||
this.inspector.selection.setNodeFront(this.nodeFront, "animationinspector");
|
||||
},
|
||||
|
||||
onMarkupMutations: function(e, mutations) {
|
||||
if (!this.nodeFront || !this.playerFront) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let {target} of mutations) {
|
||||
if (target === this.nodeFront) {
|
||||
// Re-render with the same nodeFront to update the output.
|
||||
this.render(this.playerFront);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
render: Task.async(function*(playerFront) {
|
||||
this.playerFront = playerFront;
|
||||
this.nodeFront = undefined;
|
||||
|
||||
try {
|
||||
this.nodeFront = yield this.inspector.walker.getNodeFromActor(
|
||||
playerFront.actorID, ["node"]);
|
||||
} catch (e) {
|
||||
// We might have been destroyed in the meantime, or the node might not be
|
||||
// found.
|
||||
if (!this.el) {
|
||||
console.warn("Cound't retrieve the animation target node, widget " +
|
||||
"destroyed");
|
||||
}
|
||||
console.error(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.nodeFront || !this.el) {
|
||||
return;
|
||||
}
|
||||
|
||||
let {tagName, attributes} = this.nodeFront;
|
||||
|
||||
this.tagNameEl.textContent = tagName.toLowerCase();
|
||||
|
||||
let idIndex = attributes.findIndex(({name}) => name === "id");
|
||||
if (idIndex > -1 && attributes[idIndex].value) {
|
||||
this.idEl.querySelector(".attribute-value").textContent =
|
||||
attributes[idIndex].value;
|
||||
this.idEl.style.display = "inline";
|
||||
} else {
|
||||
this.idEl.style.display = "none";
|
||||
}
|
||||
|
||||
let classIndex = attributes.findIndex(({name}) => name === "class");
|
||||
if (classIndex > -1 && attributes[classIndex].value) {
|
||||
let value = attributes[classIndex].value;
|
||||
if (this.options.compact) {
|
||||
value = value.split(" ").join(".");
|
||||
}
|
||||
|
||||
this.classEl.querySelector(".attribute-value").textContent = value;
|
||||
this.classEl.style.display = "inline";
|
||||
} else {
|
||||
this.classEl.style.display = "none";
|
||||
}
|
||||
|
||||
this.emit("target-retrieved");
|
||||
})
|
||||
};
|
||||
|
||||
/**
|
||||
* The TimeScale helper object is used to know which size should something be
|
||||
* displayed with in the animation panel, depending on the animations that are
|
||||
* currently displayed.
|
||||
* If there are 5 animations displayed, and the first one starts at 10000ms and
|
||||
* the last one ends at 20000ms, then this helper can be used to convert any
|
||||
* time in this range to a distance in pixels.
|
||||
*
|
||||
* For the helper to know how to convert, it needs to know all the animations.
|
||||
* Whenever a new animation is added to the panel, addAnimation(state) should be
|
||||
* called. reset() can be called to start over.
|
||||
*/
|
||||
let TimeScale = {
|
||||
minStartTime: Infinity,
|
||||
maxEndTime: 0,
|
||||
|
||||
/**
|
||||
* Add a new animation to time scale.
|
||||
* @param {Object} state A PlayerFront.state object.
|
||||
*/
|
||||
addAnimation: function({startTime, delay, duration, iterationCount}) {
|
||||
this.minStartTime = Math.min(this.minStartTime, startTime);
|
||||
let length = delay + (duration * (!iterationCount ? 1 : iterationCount));
|
||||
this.maxEndTime = Math.max(this.maxEndTime, startTime + length);
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset the current time scale.
|
||||
*/
|
||||
reset: function() {
|
||||
this.minStartTime = Infinity;
|
||||
this.maxEndTime = 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert a startTime to a distance in pixels, in the current time scale.
|
||||
* @param {Number} time
|
||||
* @param {Number} containerWidth The width of the container element.
|
||||
* @return {Number}
|
||||
*/
|
||||
startTimeToDistance: function(time, containerWidth) {
|
||||
time -= this.minStartTime;
|
||||
return this.durationToDistance(time, containerWidth);
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert a duration to a distance in pixels, in the current time scale.
|
||||
* @param {Number} time
|
||||
* @param {Number} containerWidth The width of the container element.
|
||||
* @return {Number}
|
||||
*/
|
||||
durationToDistance: function(duration, containerWidth) {
|
||||
return containerWidth * duration / (this.maxEndTime - this.minStartTime);
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert a distance in pixels to a time, in the current time scale.
|
||||
* @param {Number} distance
|
||||
* @param {Number} containerWidth The width of the container element.
|
||||
* @return {Number}
|
||||
*/
|
||||
distanceToTime: function(distance, containerWidth) {
|
||||
return this.minStartTime +
|
||||
((this.maxEndTime - this.minStartTime) * distance / containerWidth);
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert a distance in pixels to a time, in the current time scale.
|
||||
* The time will be relative to the current minimum start time.
|
||||
* @param {Number} distance
|
||||
* @param {Number} containerWidth The width of the container element.
|
||||
* @return {Number}
|
||||
*/
|
||||
distanceToRelativeTime: function(distance, containerWidth) {
|
||||
let time = this.distanceToTime(distance, containerWidth);
|
||||
return time - this.minStartTime;
|
||||
},
|
||||
|
||||
/**
|
||||
* Depending on the time scale, format the given time as milliseconds or
|
||||
* seconds.
|
||||
* @param {Number} time
|
||||
* @return {String} The formatted time string.
|
||||
*/
|
||||
formatTime: function(time) {
|
||||
let duration = this.maxEndTime - this.minStartTime;
|
||||
|
||||
// Format in milliseconds if the total duration is short enough.
|
||||
if (duration <= MILLIS_TIME_FORMAT_MAX_DURATION) {
|
||||
return L10N.getFormatStr("timeline.timeGraduationLabel", time.toFixed(0));
|
||||
}
|
||||
|
||||
// Otherwise format in seconds.
|
||||
return L10N.getFormatStr("player.timeLabel", (time / 1000).toFixed(1));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* UI component responsible for displaying a timeline for animations.
|
||||
* The timeline is essentially a graph with time along the x axis and animations
|
||||
* along the y axis.
|
||||
* The time is represented with a graduation header at the top and a current
|
||||
* time play head.
|
||||
* Animations are organized by lines, with a left margin containing the preview
|
||||
* of the target DOM element the animation applies to.
|
||||
*/
|
||||
function AnimationsTimeline(inspector) {
|
||||
this.animations = [];
|
||||
this.targetNodes = [];
|
||||
this.inspector = inspector;
|
||||
|
||||
this.onAnimationStateChanged = this.onAnimationStateChanged.bind(this);
|
||||
}
|
||||
|
||||
exports.AnimationsTimeline = AnimationsTimeline;
|
||||
|
||||
AnimationsTimeline.prototype = {
|
||||
init: function(containerEl) {
|
||||
this.win = containerEl.ownerDocument.defaultView;
|
||||
|
||||
this.rootWrapperEl = createNode({
|
||||
parent: containerEl,
|
||||
attributes: {
|
||||
"class": "animation-timeline"
|
||||
}
|
||||
});
|
||||
|
||||
this.timeHeaderEl = createNode({
|
||||
parent: this.rootWrapperEl,
|
||||
attributes: {
|
||||
"class": "time-header"
|
||||
}
|
||||
});
|
||||
|
||||
this.animationsEl = createNode({
|
||||
parent: this.rootWrapperEl,
|
||||
nodeType: "ul",
|
||||
attributes: {
|
||||
"class": "animations"
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
this.unrender();
|
||||
|
||||
this.rootWrapperEl.remove();
|
||||
this.animations = [];
|
||||
|
||||
this.rootWrapperEl = null;
|
||||
this.timeHeaderEl = null;
|
||||
this.animationsEl = null;
|
||||
this.win = null;
|
||||
this.inspector = null;
|
||||
},
|
||||
|
||||
destroyTargetNodes: function() {
|
||||
for (let targetNode of this.targetNodes) {
|
||||
targetNode.destroy();
|
||||
}
|
||||
this.targetNodes = [];
|
||||
},
|
||||
|
||||
unrender: function() {
|
||||
for (let animation of this.animations) {
|
||||
animation.off("changed", this.onAnimationStateChanged);
|
||||
}
|
||||
|
||||
TimeScale.reset();
|
||||
this.destroyTargetNodes();
|
||||
this.animationsEl.innerHTML = "";
|
||||
},
|
||||
|
||||
render: function(animations) {
|
||||
this.unrender();
|
||||
|
||||
this.animations = animations;
|
||||
if (!this.animations.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Loop first to set the time scale for all current animations.
|
||||
for (let {state} of animations) {
|
||||
TimeScale.addAnimation(state);
|
||||
}
|
||||
|
||||
this.drawHeaderAndBackground();
|
||||
|
||||
for (let animation of this.animations) {
|
||||
animation.on("changed", this.onAnimationStateChanged);
|
||||
|
||||
// Each line contains the target animated node and the animation time
|
||||
// block.
|
||||
let animationEl = createNode({
|
||||
parent: this.animationsEl,
|
||||
nodeType: "li",
|
||||
attributes: {
|
||||
"class": "animation"
|
||||
}
|
||||
});
|
||||
|
||||
// Left sidebar for the animated node.
|
||||
let animatedNodeEl = createNode({
|
||||
parent: animationEl,
|
||||
attributes: {
|
||||
"class": "target"
|
||||
}
|
||||
});
|
||||
|
||||
let timeBlockEl = createNode({
|
||||
parent: animationEl,
|
||||
attributes: {
|
||||
"class": "time-block"
|
||||
}
|
||||
});
|
||||
|
||||
this.drawTimeBlock(animation, timeBlockEl);
|
||||
|
||||
// Draw the animated node target.
|
||||
let targetNode = new AnimationTargetNode(this.inspector, {compact: true});
|
||||
targetNode.init(animatedNodeEl);
|
||||
targetNode.render(animation);
|
||||
|
||||
// Save the targetNode so it can be destroyed later.
|
||||
this.targetNodes.push(targetNode);
|
||||
}
|
||||
},
|
||||
|
||||
onAnimationStateChanged: function() {
|
||||
// For now, simply re-render the component. The animation front's state has
|
||||
// already been updated.
|
||||
this.render(this.animations);
|
||||
},
|
||||
|
||||
drawHeaderAndBackground: function() {
|
||||
let width = this.timeHeaderEl.offsetWidth;
|
||||
let scale = width / (TimeScale.maxEndTime - TimeScale.minStartTime);
|
||||
drawGraphElementBackground(this.win.document, "time-graduations", width, scale);
|
||||
|
||||
// And the time graduation header.
|
||||
this.timeHeaderEl.innerHTML = "";
|
||||
let interval = findOptimalTimeInterval(scale, TIME_GRADUATION_MIN_SPACING);
|
||||
for (let i = 0; i < width; i += interval) {
|
||||
createNode({
|
||||
parent: this.timeHeaderEl,
|
||||
nodeType: "span",
|
||||
attributes: {
|
||||
"class": "time-tick",
|
||||
"style": `left:${i}px`
|
||||
},
|
||||
textContent: TimeScale.formatTime(
|
||||
TimeScale.distanceToRelativeTime(i, width))
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
drawTimeBlock: function({state}, el) {
|
||||
let width = el.offsetWidth;
|
||||
|
||||
// Container for all iterations and delay. Positioned at the right start
|
||||
// time.
|
||||
let x = TimeScale.startTimeToDistance(state.startTime + (state.delay || 0),
|
||||
width);
|
||||
// With the right width (duration*duration).
|
||||
let count = state.iterationCount || 1;
|
||||
let w = TimeScale.durationToDistance(state.duration, width);
|
||||
|
||||
let iterations = createNode({
|
||||
parent: el,
|
||||
attributes: {
|
||||
"class": "iterations" + (state.iterationCount ? "" : " infinite"),
|
||||
// Individual iterations are represented by setting the size of the
|
||||
// repeating linear-gradient.
|
||||
"style": `left:${x}px;
|
||||
width:${w * count}px;
|
||||
background-size:${Math.max(w, 2)}px 100%;`
|
||||
}
|
||||
});
|
||||
|
||||
// The animation name is displayed over the iterations.
|
||||
createNode({
|
||||
parent: iterations,
|
||||
attributes: {
|
||||
"class": "name"
|
||||
},
|
||||
textContent: state.name
|
||||
});
|
||||
|
||||
// Delay.
|
||||
if (state.delay) {
|
||||
let delay = TimeScale.durationToDistance(state.delay, width);
|
||||
createNode({
|
||||
parent: iterations,
|
||||
attributes: {
|
||||
"class": "delay",
|
||||
"style": `left:-${delay}px;
|
||||
width:${delay}px;`
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -5,3 +5,8 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
|
||||
|
||||
EXTRA_JS_MODULES.devtools.animationinspector += [
|
||||
'components.js',
|
||||
'utils.js',
|
||||
]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[DEFAULT]
|
||||
subsuite = devtools
|
||||
support-files =
|
||||
doc_body_animation.html
|
||||
doc_frame_script.js
|
||||
doc_simple_animation.html
|
||||
head.js
|
||||
@@ -11,16 +12,21 @@ support-files =
|
||||
[browser_animation_participate_in_inspector_update.js]
|
||||
[browser_animation_play_pause_button.js]
|
||||
[browser_animation_playerFronts_are_refreshed.js]
|
||||
[browser_animation_playerWidgets_appear_on_panel_init.js]
|
||||
[browser_animation_playerWidgets_destroy.js]
|
||||
[browser_animation_playerWidgets_disables_on_finished.js]
|
||||
[browser_animation_playerWidgets_dont_show_time_after_duration.js]
|
||||
[browser_animation_playerWidgets_have_control_buttons.js]
|
||||
[browser_animation_playerWidgets_meta_data.js]
|
||||
[browser_animation_playerWidgets_state_after_pause.js]
|
||||
[browser_animation_playerWidgets_target_nodes.js]
|
||||
[browser_animation_rate_select_shows_presets.js]
|
||||
[browser_animation_refresh_when_active.js]
|
||||
[browser_animation_same_nb_of_playerWidgets_and_playerFronts.js]
|
||||
[browser_animation_setting_currentTime_works_and_pauses.js]
|
||||
[browser_animation_setting_playbackRate_works.js]
|
||||
[browser_animation_shows_player_on_valid_node.js]
|
||||
[browser_animation_target_highlight_select.js]
|
||||
[browser_animation_timeline_animates.js]
|
||||
[browser_animation_timeline_is_enabled.js]
|
||||
[browser_animation_timeline_waits_for_delay.js]
|
||||
@@ -30,4 +36,5 @@ support-files =
|
||||
[browser_animation_toolbar_exists.js]
|
||||
[browser_animation_ui_updates_when_animation_changes.js]
|
||||
[browser_animation_ui_updates_when_animation_data_changes.js]
|
||||
[browser_animation_ui_updates_when_animation_rate_changes.js]
|
||||
[browser_animation_ui_updates_when_animation_time_changes.js]
|
||||
|
||||
+33
-6
@@ -8,17 +8,44 @@
|
||||
|
||||
add_task(function*() {
|
||||
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
|
||||
let {inspector, panel} = yield openAnimationInspector();
|
||||
|
||||
let {inspector, panel} = yield openAnimationInspector();
|
||||
yield testEmptyPanel(inspector, panel);
|
||||
|
||||
({inspector, panel}) = yield closeAnimationInspectorAndRestartWithNewUI();
|
||||
yield testEmptyPanel(inspector, panel, true);
|
||||
});
|
||||
|
||||
function* testEmptyPanel(inspector, panel, isNewUI=false) {
|
||||
info("Select node .still and check that the panel is empty");
|
||||
let stillNode = yield getNodeFront(".still", inspector);
|
||||
let onUpdated = panel.once(panel.UI_UPDATED_EVENT);
|
||||
yield selectNode(stillNode, inspector);
|
||||
ok(!panel.playerWidgets || !panel.playerWidgets.length,
|
||||
"No player widgets displayed for a still node");
|
||||
yield onUpdated;
|
||||
|
||||
if (isNewUI) {
|
||||
is(panel.animationsTimelineComponent.animations.length, 0,
|
||||
"No animation players stored in the timeline component for a still node");
|
||||
is(panel.animationsTimelineComponent.animationsEl.childNodes.length, 0,
|
||||
"No animation displayed in the timeline component for a still node");
|
||||
} else {
|
||||
ok(!panel.playerWidgets || !panel.playerWidgets.length,
|
||||
"No player widgets displayed for a still node");
|
||||
}
|
||||
|
||||
info("Select the comment text node and check that the panel is empty");
|
||||
let commentNode = yield inspector.walker.previousSibling(stillNode);
|
||||
onUpdated = panel.once(panel.UI_UPDATED_EVENT);
|
||||
yield selectNode(commentNode, inspector);
|
||||
ok(!panel.playerWidgets || !panel.playerWidgets.length,
|
||||
"No player widgets displayed for a text node");
|
||||
});
|
||||
yield onUpdated;
|
||||
|
||||
if (isNewUI) {
|
||||
is(panel.animationsTimelineComponent.animations.length, 0,
|
||||
"No animation players stored in the timeline component for a text node");
|
||||
is(panel.animationsTimelineComponent.animationsEl.childNodes.length, 0,
|
||||
"No animation displayed in the timeline component for a text node");
|
||||
} else {
|
||||
ok(!panel.playerWidgets || !panel.playerWidgets.length,
|
||||
"No player widgets displayed for a text node");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,4 +15,13 @@ add_task(function*() {
|
||||
|
||||
ok(panel, "The animation panel exists");
|
||||
ok(panel.playersEl, "The animation panel has been initialized");
|
||||
|
||||
({panel, controller}) = yield closeAnimationInspectorAndRestartWithNewUI();
|
||||
|
||||
ok(controller, "The animation controller exists");
|
||||
ok(controller.animationsFront, "The animation controller has been initialized");
|
||||
|
||||
ok(panel, "The animation panel exists");
|
||||
ok(panel.playersEl, "The animation panel has been initialized");
|
||||
ok(panel.animationsTimelineComponent, "The animation panel has been initialized");
|
||||
});
|
||||
|
||||
+10
-3
@@ -10,8 +10,15 @@
|
||||
|
||||
add_task(function*() {
|
||||
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
|
||||
let {inspector, panel, controller} = yield openAnimationInspector();
|
||||
|
||||
let ui = yield openAnimationInspector();
|
||||
yield testEventsOrder(ui);
|
||||
|
||||
ui = yield closeAnimationInspectorAndRestartWithNewUI();
|
||||
yield testEventsOrder(ui);
|
||||
});
|
||||
|
||||
function* testEventsOrder({inspector, panel, controller}) {
|
||||
info("Listen for the players-updated, ui-updated and inspector-updated events");
|
||||
let receivedEvents = [];
|
||||
controller.once(controller.PLAYERS_UPDATED_EVENT, () => {
|
||||
@@ -19,7 +26,7 @@ add_task(function*() {
|
||||
});
|
||||
panel.once(panel.UI_UPDATED_EVENT, () => {
|
||||
receivedEvents.push(panel.UI_UPDATED_EVENT);
|
||||
})
|
||||
});
|
||||
inspector.once("inspector-updated", () => {
|
||||
receivedEvents.push("inspector-updated");
|
||||
});
|
||||
@@ -36,4 +43,4 @@ add_task(function*() {
|
||||
"The second event received was the ui-updated event");
|
||||
is(receivedEvents[2], "inspector-updated",
|
||||
"The third event received was the inspector-updated event");
|
||||
});
|
||||
}
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that player widgets are displayed right when the animation panel is
|
||||
// initialized, if the selected node (<body> by default) is animated.
|
||||
|
||||
add_task(function*() {
|
||||
yield addTab(TEST_URL_ROOT + "doc_body_animation.html");
|
||||
|
||||
let {panel} = yield openAnimationInspector();
|
||||
is(panel.playerWidgets.length, 1,
|
||||
"One animation player is displayed after init");
|
||||
|
||||
({panel}) = yield closeAnimationInspectorAndRestartWithNewUI();
|
||||
is(panel.animationsTimelineComponent.animations.length, 1,
|
||||
"One animation is handled by the timeline after init");
|
||||
is(panel.animationsTimelineComponent.animationsEl.childNodes.length, 1,
|
||||
"One animation is displayed after init");
|
||||
});
|
||||
+21
@@ -25,6 +25,8 @@ add_task(function*() {
|
||||
"The second button is the rewind button");
|
||||
ok(container.children[2].classList.contains("ff"),
|
||||
"The third button is the fast-forward button");
|
||||
ok(container.querySelector("select"),
|
||||
"The container contains the playback rate select");
|
||||
|
||||
info("Faking an older server version by setting " +
|
||||
"AnimationsController.hasSetCurrentTime to false");
|
||||
@@ -46,4 +48,23 @@ add_task(function*() {
|
||||
|
||||
yield selectNode("body", inspector);
|
||||
controller.hasSetCurrentTime = true;
|
||||
|
||||
info("Faking an older server version by setting " +
|
||||
"AnimationsController.hasSetPlaybackRate to false");
|
||||
|
||||
yield selectNode("body", inspector);
|
||||
controller.hasSetPlaybackRate = false;
|
||||
|
||||
info("Selecting the animated node again");
|
||||
yield selectNode(".animated", inspector);
|
||||
|
||||
widget = panel.playerWidgets[0];
|
||||
container = widget.el.querySelector(".playback-controls");
|
||||
|
||||
ok(container, "The control buttons container still exists");
|
||||
ok(!container.querySelector("select"),
|
||||
"The playback rate select does not exist");
|
||||
|
||||
yield selectNode("body", inspector);
|
||||
controller.hasSetPlaybackRate = true;
|
||||
});
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that player widgets display information about target nodes
|
||||
|
||||
add_task(function*() {
|
||||
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
|
||||
let {inspector, panel} = yield openAnimationInspector();
|
||||
|
||||
info("Select the simple animated node");
|
||||
yield selectNode(".animated", inspector);
|
||||
|
||||
let widget = panel.playerWidgets[0];
|
||||
|
||||
// Make sure to wait for the target-retrieved event if the nodeFront hasn't
|
||||
// yet been retrieved by the TargetNodeComponent.
|
||||
if (!widget.targetNodeComponent.nodeFront) {
|
||||
yield widget.targetNodeComponent.once("target-retrieved");
|
||||
}
|
||||
|
||||
let targetEl = widget.el.querySelector(".animation-target");
|
||||
ok(targetEl, "The player widget has a target element");
|
||||
is(targetEl.textContent, "<divid=\"\"class=\"ball animated\">",
|
||||
"The target element's content is correct");
|
||||
|
||||
let selectorEl = targetEl.querySelector(".node-selector");
|
||||
ok(selectorEl,
|
||||
"The icon to select the target element in the inspector exists");
|
||||
|
||||
info("Test again with the new timeline UI");
|
||||
({inspector, panel}) = yield closeAnimationInspectorAndRestartWithNewUI();
|
||||
|
||||
info("Select the simple animated node");
|
||||
yield selectNode(".animated", inspector);
|
||||
|
||||
let targetNodeComponent = panel.animationsTimelineComponent.targetNodes[0];
|
||||
// Make sure to wait for the target-retrieved event if the nodeFront hasn't
|
||||
// yet been retrieved by the TargetNodeComponent.
|
||||
if (!targetNodeComponent.nodeFront) {
|
||||
yield targetNodeComponent.once("target-retrieved");
|
||||
}
|
||||
|
||||
is(targetNodeComponent.el.textContent, "div#.ball.animated",
|
||||
"The target element's content is correct");
|
||||
|
||||
selectorEl = targetNodeComponent.el.querySelector(".node-selector");
|
||||
ok(selectorEl,
|
||||
"The icon to select the target element in the inspector exists");
|
||||
});
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that the playbackRate select element contains a list of presets and
|
||||
// and that if the animation has a current rate that is not part of this list,
|
||||
// it is added.
|
||||
|
||||
add_task(function*() {
|
||||
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
|
||||
let {inspector, panel} = yield openAnimationInspector();
|
||||
|
||||
info("Selecting the test node");
|
||||
yield selectNode(".animated", inspector);
|
||||
|
||||
info("Get the playback rate UI component");
|
||||
let widget = panel.playerWidgets[0];
|
||||
let rateComponent = widget.rateComponent;
|
||||
|
||||
let options = rateComponent.el.querySelectorAll("option");
|
||||
is(options.length, rateComponent.PRESETS.length,
|
||||
"The playback rate select contains the right number of options");
|
||||
|
||||
for (let i = 0; i < rateComponent.PRESETS.length; i ++) {
|
||||
is(options[i].value, rateComponent.PRESETS[i] + "",
|
||||
"The playback rate option " + i + " has the right preset value " +
|
||||
rateComponent.PRESETS[i]);
|
||||
}
|
||||
|
||||
info("Set a custom rate (not part of the presets) via the DOM");
|
||||
let onRateChanged = waitForStateCondition(widget.player, state => {
|
||||
return state.playbackRate === 3.6
|
||||
});
|
||||
yield executeInContent("Test:SetAnimationPlayerPlaybackRate", {
|
||||
selector: ".animated",
|
||||
animationIndex: 0,
|
||||
playbackRate: 3.6
|
||||
});
|
||||
yield onRateChanged;
|
||||
|
||||
options = rateComponent.el.querySelectorAll("option");
|
||||
is(options.length, rateComponent.PRESETS.length + 1,
|
||||
"The playback rate select contains the right number of options (presets + 1)");
|
||||
|
||||
ok([...options].find(option => option.value === "3.6"),
|
||||
"The custom rate is part of the select");
|
||||
});
|
||||
@@ -8,8 +8,15 @@
|
||||
|
||||
add_task(function*() {
|
||||
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
|
||||
let {toolbox, inspector, panel} = yield openAnimationInspector();
|
||||
|
||||
let {inspector, panel} = yield openAnimationInspector();
|
||||
yield testRefresh(inspector, panel);
|
||||
|
||||
({inspector, panel}) = yield closeAnimationInspectorAndRestartWithNewUI();
|
||||
yield testRefresh(inspector, panel);
|
||||
});
|
||||
|
||||
function* testRefresh(inspector, panel) {
|
||||
info("Select a non animated node");
|
||||
yield selectNode(".still", inspector);
|
||||
|
||||
@@ -19,14 +26,14 @@ add_task(function*() {
|
||||
info("Select the animated node now");
|
||||
yield selectNode(".animated", inspector);
|
||||
|
||||
ok(!panel.playerWidgets || !panel.playerWidgets.length,
|
||||
assertAnimationsDisplayed(panel, 0,
|
||||
"The panel doesn't show the animation data while inactive");
|
||||
|
||||
info("Switch to the animation panel");
|
||||
inspector.sidebar.select("animationinspector");
|
||||
yield panel.once(panel.UI_UPDATED_EVENT);
|
||||
|
||||
is(panel.playerWidgets.length, 1,
|
||||
assertAnimationsDisplayed(panel, 1,
|
||||
"The panel shows the animation data after selecting it");
|
||||
|
||||
info("Switch again to the rule-view");
|
||||
@@ -35,13 +42,13 @@ add_task(function*() {
|
||||
info("Select the non animated node again");
|
||||
yield selectNode(".still", inspector);
|
||||
|
||||
is(panel.playerWidgets.length, 1,
|
||||
assertAnimationsDisplayed(panel, 1,
|
||||
"The panel still shows the previous animation data since it is inactive");
|
||||
|
||||
info("Switch to the animation panel again");
|
||||
inspector.sidebar.select("animationinspector");
|
||||
yield panel.once(panel.UI_UPDATED_EVENT);
|
||||
|
||||
ok(!panel.playerWidgets || !panel.playerWidgets.length,
|
||||
assertAnimationsDisplayed(panel, 0,
|
||||
"The panel is now empty after refreshing");
|
||||
});
|
||||
}
|
||||
|
||||
+12
@@ -22,4 +22,16 @@ add_task(function*() {
|
||||
is(widget.el.parentNode, panel.playersEl,
|
||||
"The player widget has been appended to the panel");
|
||||
}
|
||||
|
||||
info("Test again with the new UI, making sure the same number of " +
|
||||
"animation timelines is created");
|
||||
({inspector, panel, controller}) = yield closeAnimationInspectorAndRestartWithNewUI();
|
||||
let timeline = panel.animationsTimelineComponent;
|
||||
|
||||
info("Selecting the test animated node again");
|
||||
yield selectNode(".multi", inspector);
|
||||
|
||||
is(controller.animationPlayers.length,
|
||||
timeline.animationsEl.querySelectorAll(".animation").length,
|
||||
"As many timeline elements were created as there are playerFronts");
|
||||
});
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that setting an animation's playback rate by selecting a rate in the
|
||||
// presets drop-down sets the rate accordingly.
|
||||
|
||||
add_task(function*() {
|
||||
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
|
||||
let {toolbox, inspector, panel} = yield openAnimationInspector();
|
||||
|
||||
info("Select an animated node");
|
||||
yield selectNode(".animated", inspector);
|
||||
|
||||
info("Get the player widget for this node");
|
||||
let widget = panel.playerWidgets[0];
|
||||
let select = widget.rateComponent.el;
|
||||
let win = select.ownerDocument.defaultView;
|
||||
|
||||
info("Click on the rate drop-down");
|
||||
EventUtils.synthesizeMouseAtCenter(select, {type: "mousedown"}, win);
|
||||
|
||||
info("Click on a rate option");
|
||||
let option = select.options[select.options.length - 1];
|
||||
EventUtils.synthesizeMouseAtCenter(option, {type: "mouseup"}, win);
|
||||
let selectedRate = parseFloat(option.value);
|
||||
|
||||
info("Check that the rate was changed on the player at the next update");
|
||||
yield waitForStateCondition(widget.player, ({playbackRate}) => playbackRate === selectedRate);
|
||||
is(widget.player.state.playbackRate, selectedRate,
|
||||
"The rate was changed successfully");
|
||||
});
|
||||
+10
-4
@@ -9,12 +9,18 @@
|
||||
|
||||
add_task(function*() {
|
||||
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
|
||||
let {inspector, panel} = yield openAnimationInspector();
|
||||
|
||||
let {inspector, panel} = yield openAnimationInspector();
|
||||
yield testShowsAnimations(inspector, panel);
|
||||
|
||||
({inspector, panel}) = yield closeAnimationInspectorAndRestartWithNewUI();
|
||||
yield testShowsAnimations(inspector, panel);
|
||||
});
|
||||
|
||||
function* testShowsAnimations(inspector, panel) {
|
||||
info("Select node .animated and check that the panel is not empty");
|
||||
let node = yield getNodeFront(".animated", inspector);
|
||||
yield selectNode(node, inspector);
|
||||
|
||||
is(panel.playerWidgets.length, 1,
|
||||
"Exactly 1 player widget is shown for animated node");
|
||||
});
|
||||
assertAnimationsDisplayed(panel, 1);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that the DOM element targets displayed in animation player widgets can
|
||||
// be used to highlight elements in the DOM and select them in the inspector.
|
||||
|
||||
add_task(function*() {
|
||||
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
|
||||
|
||||
let ui = yield openAnimationInspector();
|
||||
yield testTargetNode(ui);
|
||||
|
||||
ui = yield closeAnimationInspectorAndRestartWithNewUI();
|
||||
yield testTargetNode(ui, true);
|
||||
});
|
||||
|
||||
function* testTargetNode({toolbox, inspector, panel}, isNewUI) {
|
||||
info("Select the simple animated node");
|
||||
yield selectNode(".animated", inspector);
|
||||
|
||||
// Make sure to wait for the target-retrieved event if the nodeFront hasn't
|
||||
// yet been retrieved by the TargetNodeComponent.
|
||||
let targetNodeComponent;
|
||||
if (isNewUI) {
|
||||
targetNodeComponent = panel.animationsTimelineComponent.targetNodes[0];
|
||||
} else {
|
||||
targetNodeComponent = panel.playerWidgets[0].targetNodeComponent;
|
||||
}
|
||||
if (!targetNodeComponent.nodeFront) {
|
||||
yield targetNodeComponent.once("target-retrieved");
|
||||
}
|
||||
|
||||
info("Retrieve the part of the widget that highlights the node on hover");
|
||||
let highlightingEl = targetNodeComponent.previewEl;
|
||||
|
||||
info("Listen to node-highlight event and mouse over the widget");
|
||||
let onHighlight = toolbox.once("node-highlight");
|
||||
EventUtils.synthesizeMouse(highlightingEl, 10, 5, {type: "mouseover"},
|
||||
highlightingEl.ownerDocument.defaultView);
|
||||
let nodeFront = yield onHighlight;
|
||||
|
||||
ok(true, "The node-highlight event was fired");
|
||||
is(targetNodeComponent.nodeFront, nodeFront,
|
||||
"The highlighted node is the one stored on the animation widget");
|
||||
is(nodeFront.tagName, "DIV",
|
||||
"The highlighted node has the correct tagName");
|
||||
is(nodeFront.attributes[0].name, "class",
|
||||
"The highlighted node has the correct attributes");
|
||||
is(nodeFront.attributes[0].value, "ball animated",
|
||||
"The highlighted node has the correct class");
|
||||
|
||||
info("Select the body node in order to have the list of all animations");
|
||||
yield selectNode("body", inspector);
|
||||
|
||||
// Make sure to wait for the target-retrieved event if the nodeFront hasn't
|
||||
// yet been retrieved by the TargetNodeComponent.
|
||||
if (isNewUI) {
|
||||
targetNodeComponent = panel.animationsTimelineComponent.targetNodes[0];
|
||||
} else {
|
||||
targetNodeComponent = panel.playerWidgets[0].targetNodeComponent;
|
||||
}
|
||||
if (!targetNodeComponent.nodeFront) {
|
||||
yield targetNodeComponent.once("target-retrieved");
|
||||
}
|
||||
|
||||
info("Click on the first animation widget's selector icon and wait for the " +
|
||||
"selection to change");
|
||||
let onSelection = inspector.selection.once("new-node-front");
|
||||
let onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
|
||||
let selectIconEl = targetNodeComponent.selectNodeEl;
|
||||
EventUtils.sendMouseEvent({type: "click"}, selectIconEl,
|
||||
selectIconEl.ownerDocument.defaultView);
|
||||
yield onSelection;
|
||||
|
||||
is(inspector.selection.nodeFront, targetNodeComponent.nodeFront,
|
||||
"The selected node is the one stored on the animation widget");
|
||||
|
||||
yield onPanelUpdated;
|
||||
}
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
|
||||
add_task(function*() {
|
||||
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
|
||||
let {inspector, panel} = yield openAnimationInspector();
|
||||
let {panel} = yield openAnimationInspector();
|
||||
|
||||
info("Click the toggle button");
|
||||
yield panel.toggleAll();
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
add_task(function*() {
|
||||
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
|
||||
let {inspector, panel, window} = yield openAnimationInspector();
|
||||
let {inspector, window} = yield openAnimationInspector();
|
||||
let doc = window.document;
|
||||
|
||||
let toolbar = doc.querySelector("#toolbar");
|
||||
|
||||
+44
-16
@@ -9,36 +9,64 @@
|
||||
|
||||
add_task(function*() {
|
||||
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
|
||||
let {panel, inspector} = yield openAnimationInspector();
|
||||
|
||||
let ui = yield openAnimationInspector();
|
||||
yield testDataUpdates(ui);
|
||||
|
||||
info("Close the toolbox, reload the tab, and try again with the new UI");
|
||||
ui = yield closeAnimationInspectorAndRestartWithNewUI(true);
|
||||
yield testDataUpdates(ui, true);
|
||||
});
|
||||
|
||||
function* testDataUpdates({panel, controller, inspector}, isNewUI=false) {
|
||||
info("Select the test node");
|
||||
yield selectNode(".animated", inspector);
|
||||
|
||||
info("Get the player widget");
|
||||
let widget = panel.playerWidgets[0];
|
||||
let animation = controller.animationPlayers[0];
|
||||
yield setStyle(animation, "animationDuration", "5.5s", isNewUI);
|
||||
yield setStyle(animation, "animationIterationCount", "300", isNewUI);
|
||||
yield setStyle(animation, "animationDelay", "45s", isNewUI);
|
||||
|
||||
yield setStyle(widget, "animationDuration", "5.5s");
|
||||
is(widget.metaDataComponent.durationValue.textContent, "5.50s",
|
||||
"The widget shows the new duration");
|
||||
if (isNewUI) {
|
||||
let animationsEl = panel.animationsTimelineComponent.animationsEl;
|
||||
let timeBlockEl = animationsEl.querySelector(".time-block");
|
||||
|
||||
yield setStyle(widget, "animationIterationCount", "300");
|
||||
is(widget.metaDataComponent.iterationValue.textContent, "300",
|
||||
"The widget shows the new iteration count");
|
||||
// 45s delay + (300 * 5.5)s duration
|
||||
let expectedTotalDuration = 1695 * 1000;
|
||||
let timeRatio = expectedTotalDuration / timeBlockEl.offsetWidth;
|
||||
|
||||
yield setStyle(widget, "animationDelay", "45s");
|
||||
is(widget.metaDataComponent.delayValue.textContent, "45s",
|
||||
"The widget shows the new delay");
|
||||
});
|
||||
// XXX: the nb and size of each iteration cannot be tested easily (displayed
|
||||
// using a linear-gradient background and capped at 2px wide). They should
|
||||
// be tested in bug 1173761.
|
||||
let delayWidth = parseFloat(timeBlockEl.querySelector(".delay").style.width);
|
||||
is(Math.round(delayWidth * timeRatio), 45 * 1000,
|
||||
"The timeline has the right delay");
|
||||
} else {
|
||||
let widget = panel.playerWidgets[0];
|
||||
is(widget.metaDataComponent.durationValue.textContent, "5.50s",
|
||||
"The widget shows the new duration");
|
||||
is(widget.metaDataComponent.iterationValue.textContent, "300",
|
||||
"The widget shows the new iteration count");
|
||||
is(widget.metaDataComponent.delayValue.textContent, "45s",
|
||||
"The widget shows the new delay");
|
||||
}
|
||||
}
|
||||
|
||||
function* setStyle(widget, name, value) {
|
||||
function* setStyle(animation, name, value, isNewUI=false) {
|
||||
info("Change the animation style via the content DOM. Setting " +
|
||||
name + " to " + value);
|
||||
|
||||
let onAnimationChanged = once(animation, "changed");
|
||||
yield executeInContent("devtools:test:setStyle", {
|
||||
selector: ".animated",
|
||||
propertyName: name,
|
||||
propertyValue: value
|
||||
});
|
||||
yield onAnimationChanged;
|
||||
|
||||
info("Wait for the next state update");
|
||||
yield onceNextPlayerRefresh(widget.player);
|
||||
// If this is the playerWidget-based UI, wait for the auto-refresh event too
|
||||
// to make sure the UI has updated.
|
||||
if (!isNewUI) {
|
||||
yield once(animation, animation.AUTO_REFRESH_EVENT);
|
||||
}
|
||||
}
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that setting an animation's playbackRate via the WebAnimations API (from
|
||||
// content), actually changes the rate in the corresponding widget too.
|
||||
|
||||
add_task(function*() {
|
||||
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
|
||||
let {inspector, panel} = yield openAnimationInspector();
|
||||
|
||||
info("Selecting the test node");
|
||||
yield selectNode(".animated", inspector);
|
||||
|
||||
info("Get the player widget");
|
||||
let widget = panel.playerWidgets[0];
|
||||
|
||||
info("Change the animation's playbackRate via the content DOM");
|
||||
let onRateChanged = waitForStateCondition(widget.player, state => {
|
||||
return state.playbackRate === 2;
|
||||
}, "playbackRate === 2");
|
||||
yield executeInContent("Test:SetAnimationPlayerPlaybackRate", {
|
||||
selector: ".animated",
|
||||
animationIndex: 0,
|
||||
playbackRate: 2
|
||||
});
|
||||
yield onRateChanged;
|
||||
|
||||
is(widget.rateComponent.el.value, "2",
|
||||
"The playbackRate select value was changed");
|
||||
|
||||
info("Change the animation's playbackRate again via the content DOM");
|
||||
onRateChanged = waitForStateCondition(widget.player, state => {
|
||||
return state.playbackRate === 0.3;
|
||||
}, "playbackRate === 0.3");
|
||||
yield executeInContent("Test:SetAnimationPlayerPlaybackRate", {
|
||||
selector: ".animated",
|
||||
animationIndex: 0,
|
||||
playbackRate: 0.3
|
||||
});
|
||||
yield onRateChanged;
|
||||
|
||||
is(widget.rateComponent.el.value, "0.3",
|
||||
"The playbackRate select value was changed again");
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body {
|
||||
background-color: white;
|
||||
color: black;
|
||||
animation: change-background-color 3s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes change-background-color {
|
||||
to {
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Animated body element</h1>
|
||||
</body>
|
||||
</html>
|
||||
@@ -52,6 +52,27 @@ addMessageListener("Test:SetAnimationPlayerCurrentTime", function(msg) {
|
||||
sendAsyncMessage("Test:SetAnimationPlayerCurrentTime");
|
||||
});
|
||||
|
||||
/**
|
||||
* Change the playbackRate of one of the animation players of a given node.
|
||||
* @param {Object} data
|
||||
* - {String} selector The CSS selector to get the node (can be a "super"
|
||||
* selector).
|
||||
* - {Number} animationIndex The index of the node's animationPlayers to change.
|
||||
* - {Number} playbackRate The rate to set.
|
||||
*/
|
||||
addMessageListener("Test:SetAnimationPlayerPlaybackRate", function(msg) {
|
||||
let {selector, animationIndex, playbackRate} = msg.data;
|
||||
let node = superQuerySelector(selector);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
let player = node.getAnimations()[animationIndex];
|
||||
player.playbackRate = playbackRate;
|
||||
|
||||
sendAsyncMessage("Test:SetAnimationPlayerPlaybackRate");
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the current playState of an animation player on a given node.
|
||||
* @param {Object} data
|
||||
|
||||
@@ -9,7 +9,7 @@ const {gDevTools} = Cu.import("resource://gre/modules/devtools/gDevTools.jsm", {
|
||||
const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
|
||||
const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
|
||||
const TargetFactory = devtools.TargetFactory;
|
||||
const {console} = Components.utils.import("resource://gre/modules/devtools/Console.jsm", {});
|
||||
const {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
|
||||
const {ViewHelpers} = Cu.import("resource://gre/modules/devtools/ViewHelpers.jsm", {});
|
||||
|
||||
// All tests are asynchronous
|
||||
@@ -19,17 +19,20 @@ const TEST_URL_ROOT = "http://example.com/browser/browser/devtools/animationinsp
|
||||
const ROOT_TEST_DIR = getRootDirectory(gTestPath);
|
||||
const FRAME_SCRIPT_URL = ROOT_TEST_DIR + "doc_frame_script.js";
|
||||
const COMMON_FRAME_SCRIPT_URL = "chrome://global/content/devtools/frame-script-utils.js";
|
||||
const NEW_UI_PREF = "devtools.inspector.animationInspectorV3";
|
||||
|
||||
// Auto clean-up when a test ends
|
||||
registerCleanupFunction(function*() {
|
||||
let target = TargetFactory.forTab(gBrowser.selectedTab);
|
||||
yield gDevTools.closeToolbox(target);
|
||||
yield closeAnimationInspector();
|
||||
|
||||
while (gBrowser.tabs.length > 1) {
|
||||
gBrowser.removeCurrentTab();
|
||||
}
|
||||
});
|
||||
|
||||
// Make sure the new UI is off by default.
|
||||
Services.prefs.setBoolPref(NEW_UI_PREF, false);
|
||||
|
||||
// Uncomment this pref to dump all devtools emitted events to the console.
|
||||
// Services.prefs.setBoolPref("devtools.dump.emit", true);
|
||||
|
||||
@@ -45,6 +48,7 @@ registerCleanupFunction(() => gDevTools.testing = false);
|
||||
registerCleanupFunction(() => {
|
||||
Services.prefs.clearUserPref("devtools.dump.emit");
|
||||
Services.prefs.clearUserPref("devtools.debugger.log");
|
||||
Services.prefs.clearUserPref(NEW_UI_PREF);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -77,6 +81,13 @@ function addTab(url) {
|
||||
return def.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch ON the new UI pref.
|
||||
*/
|
||||
function enableNewUI() {
|
||||
Services.prefs.setBoolPref(NEW_UI_PREF, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload the current tab location.
|
||||
*/
|
||||
@@ -119,6 +130,25 @@ let selectNode = Task.async(function*(data, inspector, reason="test") {
|
||||
yield updated;
|
||||
});
|
||||
|
||||
/**
|
||||
* Check if there are the expected number of animations being displayed in the
|
||||
* panel right now.
|
||||
* @param {AnimationsPanel} panel
|
||||
* @param {Number} nbAnimations The expected number of animations.
|
||||
* @param {String} msg An optional string to be used as the assertion message.
|
||||
*/
|
||||
function assertAnimationsDisplayed(panel, nbAnimations, msg="") {
|
||||
let isNewUI = Services.prefs.getBoolPref(NEW_UI_PREF);
|
||||
msg = msg || `There are ${nbAnimations} animations in the panel`;
|
||||
if (isNewUI) {
|
||||
is(panel.animationsTimelineComponent.animationsEl.childNodes.length,
|
||||
nbAnimations, msg);
|
||||
} else {
|
||||
is(panel.playersEl.querySelectorAll(".player-widget").length,
|
||||
nbAnimations, msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an Inspector panel that was just created, and waits
|
||||
* for a "inspector-updated" event as well as the animation inspector
|
||||
@@ -131,10 +161,9 @@ let waitForAnimationInspectorReady = Task.async(function*(inspector) {
|
||||
let win = inspector.sidebar.getWindowForTab("animationinspector");
|
||||
let updated = inspector.once("inspector-updated");
|
||||
|
||||
// In e10s, if we wait for underlying toolbox actors to
|
||||
// load (by setting gDevTools.testing to true), we miss the "animationinspector-ready"
|
||||
// event on the sidebar, so check to see if the iframe
|
||||
// is already loaded.
|
||||
// In e10s, if we wait for underlying toolbox actors to load (by setting
|
||||
// gDevTools.testing to true), we miss the "animationinspector-ready" event on
|
||||
// the sidebar, so check to see if the iframe is already loaded.
|
||||
let tabReady = win.document.readyState === "complete" ?
|
||||
promise.resolve() :
|
||||
inspector.sidebar.once("animationinspector-ready");
|
||||
@@ -145,7 +174,7 @@ let waitForAnimationInspectorReady = Task.async(function*(inspector) {
|
||||
/**
|
||||
* Open the toolbox, with the inspector tool visible and the animationinspector
|
||||
* sidebar selected.
|
||||
* @return a promise that resolves when the inspector is ready
|
||||
* @return a promise that resolves when the inspector is ready.
|
||||
*/
|
||||
let openAnimationInspector = Task.async(function*() {
|
||||
let target = TargetFactory.forTab(gBrowser.selectedTab);
|
||||
@@ -185,6 +214,35 @@ let openAnimationInspector = Task.async(function*() {
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Close the toolbox.
|
||||
* @return a promise that resolves when the toolbox has closed.
|
||||
*/
|
||||
let closeAnimationInspector = Task.async(function*() {
|
||||
let target = TargetFactory.forTab(gBrowser.selectedTab);
|
||||
yield gDevTools.closeToolbox(target);
|
||||
});
|
||||
|
||||
/**
|
||||
* During the time period we migrate from the playerWidgets-based UI to the new
|
||||
* AnimationTimeline UI, we'll want to run certain tests against both UI.
|
||||
* This closes the toolbox, switch the new UI pref ON, and opens the toolbox
|
||||
* again, with the animation inspector panel selected.
|
||||
* @param {Boolean} reload Optionally reload the page after the toolbox was
|
||||
* closed and before it is opened again.
|
||||
* @return a promise that resolves when the animation inspector is ready.
|
||||
*/
|
||||
let closeAnimationInspectorAndRestartWithNewUI = Task.async(function*(reload) {
|
||||
info("Close the toolbox and test again with the new UI");
|
||||
yield closeAnimationInspector();
|
||||
if (reload) {
|
||||
yield reloadTab();
|
||||
}
|
||||
enableNewUI();
|
||||
return yield openAnimationInspector();
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Wait for the toolbox frame to receive focus after it loads
|
||||
* @param {Toolbox} toolbox
|
||||
@@ -214,7 +272,7 @@ function hasSideBarTab(inspector, id) {
|
||||
* @param {Object} target An observable object that either supports on/off or
|
||||
* addEventListener/removeEventListener
|
||||
* @param {String} eventName
|
||||
* @param {Boolean} useCapture Optional, for addEventListener/removeEventListener
|
||||
* @param {Boolean} useCapture Optional, for add/removeEventListener
|
||||
* @return A promise that resolves when the event has been handled
|
||||
*/
|
||||
function once(target, eventName, useCapture=false) {
|
||||
@@ -278,9 +336,9 @@ function executeInContent(name, data={}, objects={}, expectResponse=true) {
|
||||
mm.sendAsyncMessage(name, data, objects);
|
||||
if (expectResponse) {
|
||||
return waitForContentMessage(name);
|
||||
} else {
|
||||
return promise.resolve();
|
||||
}
|
||||
|
||||
return promise.resolve();
|
||||
}
|
||||
|
||||
function onceNextPlayerRefresh(player) {
|
||||
@@ -293,7 +351,9 @@ function onceNextPlayerRefresh(player) {
|
||||
* Simulate a click on the playPause button of a playerWidget.
|
||||
*/
|
||||
let togglePlayPauseButton = Task.async(function*(widget) {
|
||||
let nextState = widget.player.state.playState === "running" ? "paused" : "running";
|
||||
let nextState = widget.player.state.playState === "running"
|
||||
? "paused"
|
||||
: "running";
|
||||
|
||||
// Note that instead of simulating a real event here, the callback is just
|
||||
// called. This is better because the callback returns a promise, so we know
|
||||
@@ -344,7 +404,8 @@ let waitForStateCondition = Task.async(function*(player, conditionCheck, desc=""
|
||||
* provided string.
|
||||
* @param {AnimationPlayerFront} player
|
||||
* @param {String} playState The playState to expect.
|
||||
* @return {Promise} Resolves when the playState has changed to the expected value.
|
||||
* @return {Promise} Resolves when the playState has changed to the expected
|
||||
* value.
|
||||
*/
|
||||
function waitForPlayState(player, playState) {
|
||||
return waitForStateCondition(player, state => {
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
// How many times, maximum, can we loop before we find the optimal time
|
||||
// interval in the timeline graph.
|
||||
const OPTIMAL_TIME_INTERVAL_MAX_ITERS = 100;
|
||||
// Background time graduations should be multiple of this number of millis.
|
||||
const TIME_INTERVAL_MULTIPLE = 10;
|
||||
const TIME_INTERVAL_SCALES = 3;
|
||||
// The default minimum spacing between time graduations in px.
|
||||
const TIME_GRADUATION_MIN_SPACING = 10;
|
||||
// RGB color for the time interval background.
|
||||
const TIME_INTERVAL_COLOR = [128, 136, 144];
|
||||
const TIME_INTERVAL_OPACITY_MIN = 32; // byte
|
||||
const TIME_INTERVAL_OPACITY_ADD = 32; // byte
|
||||
|
||||
/**
|
||||
* DOM node creation helper function.
|
||||
* @param {Object} Options to customize the node to be created.
|
||||
* - nodeType {String} Optional, defaults to "div",
|
||||
* - attributes {Object} Optional attributes object like
|
||||
* {attrName1:value1, attrName2: value2, ...}
|
||||
* - parent {DOMNode} Mandatory node to append the newly created node to.
|
||||
* - textContent {String} Optional text for the node.
|
||||
* @return {DOMNode} The newly created node.
|
||||
*/
|
||||
function createNode(options) {
|
||||
if (!options.parent) {
|
||||
throw new Error("Missing parent DOMNode to create new node");
|
||||
}
|
||||
|
||||
let type = options.nodeType || "div";
|
||||
let node = options.parent.ownerDocument.createElement(type);
|
||||
|
||||
for (let name in options.attributes || {}) {
|
||||
let value = options.attributes[name];
|
||||
node.setAttribute(name, value);
|
||||
}
|
||||
|
||||
if (options.textContent) {
|
||||
node.textContent = options.textContent;
|
||||
}
|
||||
|
||||
options.parent.appendChild(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
exports.createNode = createNode;
|
||||
|
||||
/**
|
||||
* Given a data-scale, draw the background for a graph (vertical lines) into a
|
||||
* canvas and set that canvas as an image-element with an ID that can be used
|
||||
* from CSS.
|
||||
* @param {Document} document The document where the image-element should be set.
|
||||
* @param {String} id The ID for the image-element.
|
||||
* @param {Number} graphWidth The width of the graph.
|
||||
* @param {Number} timeScale How many px is 1ms in the graph.
|
||||
*/
|
||||
function drawGraphElementBackground(document, id, graphWidth, timeScale) {
|
||||
let canvas = document.createElement("canvas");
|
||||
let ctx = canvas.getContext("2d");
|
||||
|
||||
// Set the canvas width (as requested) and height (1px, repeated along the Y
|
||||
// axis).
|
||||
canvas.width = graphWidth;
|
||||
canvas.height = 1;
|
||||
|
||||
// Create the image data array which will receive the pixels.
|
||||
let imageData = ctx.createImageData(canvas.width, canvas.height);
|
||||
let pixelArray = imageData.data;
|
||||
|
||||
let buf = new ArrayBuffer(pixelArray.length);
|
||||
let view8bit = new Uint8ClampedArray(buf);
|
||||
let view32bit = new Uint32Array(buf);
|
||||
|
||||
// Build new millisecond tick lines...
|
||||
let [r, g, b] = TIME_INTERVAL_COLOR;
|
||||
let alphaComponent = TIME_INTERVAL_OPACITY_MIN;
|
||||
let interval = findOptimalTimeInterval(timeScale);
|
||||
|
||||
// Insert one pixel for each division on each scale.
|
||||
for (let i = 1; i <= TIME_INTERVAL_SCALES; i++) {
|
||||
let increment = interval * Math.pow(2, i);
|
||||
for (let x = 0; x < canvas.width; x += increment) {
|
||||
let position = x | 0;
|
||||
view32bit[position] = (alphaComponent << 24) | (b << 16) | (g << 8) | r;
|
||||
}
|
||||
alphaComponent += TIME_INTERVAL_OPACITY_ADD;
|
||||
}
|
||||
|
||||
// Flush the image data and cache the waterfall background.
|
||||
pixelArray.set(view8bit);
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
document.mozSetImageElement(id, canvas);
|
||||
}
|
||||
|
||||
exports.drawGraphElementBackground = drawGraphElementBackground;
|
||||
|
||||
/**
|
||||
* Find the optimal interval between time graduations in the animation timeline
|
||||
* graph based on a time scale and a minimum spacing.
|
||||
* @param {Number} timeScale How many px is 1ms in the graph.
|
||||
* @param {Number} minSpacing The minimum spacing between 2 graduations,
|
||||
* defaults to TIME_GRADUATION_MIN_SPACING.
|
||||
* @return {Number} The optional interval, in pixels.
|
||||
*/
|
||||
function findOptimalTimeInterval(timeScale,
|
||||
minSpacing=TIME_GRADUATION_MIN_SPACING) {
|
||||
let timingStep = TIME_INTERVAL_MULTIPLE;
|
||||
let maxIters = OPTIMAL_TIME_INTERVAL_MAX_ITERS;
|
||||
let numIters = 0;
|
||||
|
||||
if (timeScale > minSpacing) {
|
||||
return timeScale;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
let scaledStep = timeScale * timingStep;
|
||||
if (++numIters > maxIters) {
|
||||
return scaledStep;
|
||||
}
|
||||
if (scaledStep < minSpacing) {
|
||||
timingStep *= 2;
|
||||
continue;
|
||||
}
|
||||
return scaledStep;
|
||||
}
|
||||
}
|
||||
|
||||
exports.findOptimalTimeInterval = findOptimalTimeInterval;
|
||||
@@ -385,6 +385,10 @@ InspectorPanel.prototype = {
|
||||
* reload
|
||||
*/
|
||||
set selectionCssSelector(cssSelector = null) {
|
||||
if (this._panelDestroyer) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._selectionCssSelector = {
|
||||
selector: cssSelector,
|
||||
url: this._target.url
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Set of actors that expose the Web Animations API to devtools protocol clients.
|
||||
* Set of actors that expose the Web Animations API to devtools protocol
|
||||
* clients.
|
||||
*
|
||||
* The |Animations| actor is the main entry point. It is used to discover
|
||||
* animation players on given nodes.
|
||||
@@ -29,11 +30,15 @@ const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
|
||||
const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
|
||||
const {setInterval, clearInterval} = require("sdk/timers");
|
||||
const protocol = require("devtools/server/protocol");
|
||||
const {ActorClass, Actor, FrontClass, Front, Arg, method, RetVal, types} = protocol;
|
||||
const {ActorClass, Actor, FrontClass, Front,
|
||||
Arg, method, RetVal, types} = protocol;
|
||||
// Make sure the nodeActor type is know here.
|
||||
const {NodeActor} = require("devtools/server/actors/inspector");
|
||||
const events = require("sdk/event/core");
|
||||
|
||||
const PLAYER_DEFAULT_AUTO_REFRESH_TIMEOUT = 500; // ms
|
||||
// How long (in ms) should we wait before polling again the state of an
|
||||
// animationPlayer.
|
||||
const PLAYER_DEFAULT_AUTO_REFRESH_TIMEOUT = 500;
|
||||
|
||||
/**
|
||||
* The AnimationPlayerActor provides information about a given animation: its
|
||||
@@ -47,6 +52,13 @@ const PLAYER_DEFAULT_AUTO_REFRESH_TIMEOUT = 500; // ms
|
||||
let AnimationPlayerActor = ActorClass({
|
||||
typeName: "animationplayer",
|
||||
|
||||
events: {
|
||||
"changed": {
|
||||
type: "changed",
|
||||
state: Arg(0, "json")
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {AnimationsActor} The main AnimationsActor instance
|
||||
* @param {AnimationPlayer} The player object returned by getAnimationPlayers
|
||||
@@ -58,14 +70,29 @@ let AnimationPlayerActor = ActorClass({
|
||||
initialize: function(animationsActor, player, playerIndex) {
|
||||
Actor.prototype.initialize.call(this, animationsActor.conn);
|
||||
|
||||
this.onAnimationMutation = this.onAnimationMutation.bind(this);
|
||||
|
||||
this.tabActor = animationsActor.tabActor;
|
||||
this.player = player;
|
||||
this.node = player.effect.target;
|
||||
this.playerIndex = playerIndex;
|
||||
this.styles = this.node.ownerDocument.defaultView.getComputedStyle(this.node);
|
||||
|
||||
let win = this.node.ownerDocument.defaultView;
|
||||
this.styles = win.getComputedStyle(this.node);
|
||||
|
||||
// Listen to animation mutations on the node to alert the front when the
|
||||
// current animation changes.
|
||||
this.observer = new win.MutationObserver(this.onAnimationMutation);
|
||||
this.observer.observe(this.node, {animations: true});
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
this.player = this.node = this.styles = null;
|
||||
// Only try to disconnect the observer if it's not already dead (i.e. if the
|
||||
// container view hasn't navigated since).
|
||||
if (this.observer && !Cu.isDeadWrapper(this.observer)) {
|
||||
this.observer.disconnect();
|
||||
}
|
||||
this.tabActor = this.player = this.node = this.styles = this.observer = null;
|
||||
Actor.prototype.destroy.call(this);
|
||||
},
|
||||
|
||||
@@ -86,6 +113,14 @@ let AnimationPlayerActor = ActorClass({
|
||||
return data;
|
||||
},
|
||||
|
||||
isAnimation: function(player=this.player) {
|
||||
return player instanceof this.tabActor.window.CSSAnimation;
|
||||
},
|
||||
|
||||
isTransition: function(player=this.player) {
|
||||
return player instanceof this.tabActor.window.CSSTransition;
|
||||
},
|
||||
|
||||
/**
|
||||
* Some of the player's properties are retrieved from the node's
|
||||
* computed-styles because the Web Animations API does not provide them yet.
|
||||
@@ -95,14 +130,14 @@ let AnimationPlayerActor = ActorClass({
|
||||
*/
|
||||
getPlayerIndex: function() {
|
||||
let names = this.styles.animationName;
|
||||
if (names === "none") {
|
||||
names = this.styles.transitionProperty;
|
||||
}
|
||||
|
||||
// If no names are found, then it's probably a transition, in which case we
|
||||
// can't find the actual index, so just trust the playerIndex passed by
|
||||
// the AnimationsActor at initialization time.
|
||||
// Note that this may be incorrect if by the time the AnimationPlayerActor
|
||||
// is initialized, one of the transitions has ended, but it's the best we
|
||||
// can do for now.
|
||||
if (!names) {
|
||||
// If we still don't have a name, let's fall back to the provided index
|
||||
// which may, by now, be wrong, but it's the best we can do until the waapi
|
||||
// gives us a way to get duration, delay, ... directly.
|
||||
if (!names || names === "none") {
|
||||
return this.playerIndex;
|
||||
}
|
||||
|
||||
@@ -113,14 +148,31 @@ let AnimationPlayerActor = ActorClass({
|
||||
|
||||
// If there are several names, retrieve the index of the animation name in
|
||||
// the list.
|
||||
let playerName = this.getName();
|
||||
names = names.split(",").map(n => n.trim());
|
||||
for (let i = 0; i < names.length; i ++) {
|
||||
if (names[i] === this.player.effect.name) {
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
if (names[i] === playerName) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the name associated with the player. This is used to match
|
||||
* up the player with values in the computed animation-name or
|
||||
* transition-property property.
|
||||
* @return {String}
|
||||
*/
|
||||
getName: function() {
|
||||
if (this.isAnimation()) {
|
||||
return this.player.animationName;
|
||||
} else if (this.isTransition()) {
|
||||
return this.player.transitionProperty;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the animation duration from this player, in milliseconds.
|
||||
* Note that the Web Animations API doesn't yet offer a way to retrieve this
|
||||
@@ -207,7 +259,7 @@ let AnimationPlayerActor = ActorClass({
|
||||
currentTime: this.player.currentTime,
|
||||
playState: this.player.playState,
|
||||
playbackRate: this.player.playbackRate,
|
||||
name: this.player.effect.name,
|
||||
name: this.getName(),
|
||||
duration: this.getDuration(),
|
||||
delay: this.getDelay(),
|
||||
iterationCount: this.getIterationCount(),
|
||||
@@ -244,6 +296,27 @@ let AnimationPlayerActor = ActorClass({
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Executed when the current animation changes, used to emit the new state
|
||||
* the the front.
|
||||
*/
|
||||
onAnimationMutation: function(mutations) {
|
||||
let hasChanged = false;
|
||||
for (let {changedAnimations} of mutations) {
|
||||
if (!changedAnimations.length) {
|
||||
return;
|
||||
}
|
||||
if (changedAnimations.some(animation => animation === this.player)) {
|
||||
hasChanged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChanged) {
|
||||
events.emit(this, "changed", this.getCurrentState());
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Pause the player.
|
||||
*/
|
||||
@@ -348,9 +421,18 @@ let AnimationPlayerFront = FrontClass(AnimationPlayerActor, {
|
||||
delay: this._form.delay,
|
||||
iterationCount: this._form.iterationCount,
|
||||
isRunningOnCompositor: this._form.isRunningOnCompositor
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Executed when the AnimationPlayerActor emits a "changed" event. Used to
|
||||
* update the local knowledge of the state.
|
||||
*/
|
||||
onChanged: protocol.preEvent("changed", function(partialState) {
|
||||
let {state} = this.reconstructState(partialState);
|
||||
this.state = state;
|
||||
}),
|
||||
|
||||
// About auto-refresh:
|
||||
//
|
||||
// The AnimationPlayerFront is capable of automatically refreshing its state
|
||||
@@ -404,11 +486,6 @@ let AnimationPlayerFront = FrontClass(AnimationPlayerActor, {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the animationplayer is now finished, stop auto-refreshing.
|
||||
if (data.playState === "finished") {
|
||||
this.stopAutoRefresh();
|
||||
}
|
||||
|
||||
if (this.currentStateHasChanged) {
|
||||
this.state = data;
|
||||
events.emit(this, this.AUTO_REFRESH_EVENT, this.state);
|
||||
@@ -421,19 +498,28 @@ let AnimationPlayerFront = FrontClass(AnimationPlayerActor, {
|
||||
*/
|
||||
getCurrentState: protocol.custom(function() {
|
||||
this.currentStateHasChanged = false;
|
||||
return this._getCurrentState().then(data => {
|
||||
for (let key in this.state) {
|
||||
if (typeof data[key] === "undefined") {
|
||||
data[key] = this.state[key];
|
||||
} else if (data[key] !== this.state[key]) {
|
||||
this.currentStateHasChanged = true;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
return this._getCurrentState().then(partialData => {
|
||||
let {state, hasChanged} = this.reconstructState(partialData);
|
||||
this.currentStateHasChanged = hasChanged;
|
||||
return state;
|
||||
});
|
||||
}, {
|
||||
impl: "_getCurrentState"
|
||||
}),
|
||||
|
||||
reconstructState: function(data) {
|
||||
let hasChanged = false;
|
||||
|
||||
for (let key in this.state) {
|
||||
if (typeof data[key] === "undefined") {
|
||||
data[key] = this.state[key];
|
||||
} else if (data[key] !== this.state[key]) {
|
||||
hasChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
return {state: data, hasChanged};
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -454,7 +540,7 @@ let AnimationsActor = exports.AnimationsActor = ActorClass({
|
||||
typeName: "animations",
|
||||
|
||||
events: {
|
||||
"mutations" : {
|
||||
"mutations": {
|
||||
type: "mutations",
|
||||
changes: Arg(0, "array:animationMutationChange")
|
||||
}
|
||||
@@ -502,7 +588,7 @@ let AnimationsActor = exports.AnimationsActor = ActorClass({
|
||||
// No care is taken here to destroy the previously stored actors because it
|
||||
// is assumed that the client is responsible for lifetimes of actors.
|
||||
this.actors = [];
|
||||
for (let i = 0; i < animations.length; i ++) {
|
||||
for (let i = 0; i < animations.length; i++) {
|
||||
// XXX: for now the index is passed along as the AnimationPlayerActor uses
|
||||
// it to retrieve animation information from CSS.
|
||||
let actor = AnimationPlayerActor(this, animations[i], i);
|
||||
@@ -531,7 +617,7 @@ let AnimationsActor = exports.AnimationsActor = ActorClass({
|
||||
onAnimationMutation: function(mutations) {
|
||||
let eventData = [];
|
||||
|
||||
for (let {addedAnimations, changedAnimations, removedAnimations} of mutations) {
|
||||
for (let {addedAnimations, removedAnimations} of mutations) {
|
||||
for (let player of removedAnimations) {
|
||||
// Note that animations are reported as removed either when they are
|
||||
// actually removed from the node (e.g. css class removed) or when they
|
||||
@@ -556,6 +642,24 @@ let AnimationsActor = exports.AnimationsActor = ActorClass({
|
||||
if (this.actors.find(a => a.player === player)) {
|
||||
continue;
|
||||
}
|
||||
// If the added player has the same name and target node as a player we
|
||||
// already have, it means it's a transition that's re-starting. So send
|
||||
// a "removed" event for the one we already have.
|
||||
let index = this.actors.findIndex(a => {
|
||||
return a.player.constructor === player.constructor &&
|
||||
((a.isAnimation() &&
|
||||
a.player.animationName === player.animationName) ||
|
||||
(a.isTransition() &&
|
||||
a.player.transitionProperty === player.transitionProperty));
|
||||
});
|
||||
if (index !== -1) {
|
||||
eventData.push({
|
||||
type: "removed",
|
||||
player: this.actors[index]
|
||||
});
|
||||
this.actors.splice(index, 1);
|
||||
}
|
||||
|
||||
let actor = AnimationPlayerActor(
|
||||
this, player, player.effect.target.getAnimations().indexOf(player));
|
||||
this.actors.push(actor);
|
||||
@@ -572,9 +676,9 @@ let AnimationsActor = exports.AnimationsActor = ActorClass({
|
||||
},
|
||||
|
||||
/**
|
||||
* After the client has called getAnimationPlayersForNode for a given DOM node,
|
||||
* the actor starts sending animation mutations for this node. If the client
|
||||
* doesn't want this to happen anymore, it should call this method.
|
||||
* After the client has called getAnimationPlayersForNode for a given DOM
|
||||
* node, the actor starts sending animation mutations for this node. If the
|
||||
* client doesn't want this to happen anymore, it should call this method.
|
||||
*/
|
||||
stopAnimationPlayerUpdates: method(function() {
|
||||
if (this.observer && !Cu.isDeadWrapper(this.observer)) {
|
||||
@@ -639,7 +743,7 @@ let AnimationsActor = exports.AnimationsActor = ActorClass({
|
||||
|
||||
/**
|
||||
* Play all animations in the current tabActor's frames.
|
||||
* This method only returns when the animations have left their pending states.
|
||||
* This method only returns when animations have left their pending states.
|
||||
*/
|
||||
playAll: method(function() {
|
||||
let readyPromises = [];
|
||||
@@ -657,9 +761,8 @@ let AnimationsActor = exports.AnimationsActor = ActorClass({
|
||||
toggleAll: method(function() {
|
||||
if (this.allAnimationsPaused) {
|
||||
return this.playAll();
|
||||
} else {
|
||||
return this.pauseAll();
|
||||
}
|
||||
return this.pauseAll();
|
||||
}, {
|
||||
request: {},
|
||||
response: {}
|
||||
|
||||
@@ -2754,19 +2754,13 @@ var WalkerActor = protocol.ActorClass({
|
||||
}),
|
||||
|
||||
/**
|
||||
* Given an StyleSheetActor (identified by its ID), commonly used in the
|
||||
* Given a StyleSheetActor (identified by its ID), commonly used in the
|
||||
* style-editor, get its ownerNode and return the corresponding walker's
|
||||
* NodeActor
|
||||
* NodeActor.
|
||||
* Note that getNodeFromActor was added later and can now be used instead.
|
||||
*/
|
||||
getStyleSheetOwnerNode: method(function(styleSheetActorID) {
|
||||
let styleSheetActor = this.conn.getActor(styleSheetActorID);
|
||||
let ownerNode = styleSheetActor.ownerNode;
|
||||
|
||||
if (!styleSheetActor || !ownerNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.attachElement(ownerNode);
|
||||
return this.getNodeFromActor(styleSheetActorID, ["ownerNode"]);
|
||||
}, {
|
||||
request: {
|
||||
styleSheetActorID: Arg(0, "string")
|
||||
@@ -2775,6 +2769,60 @@ var WalkerActor = protocol.ActorClass({
|
||||
ownerNode: RetVal("nullable:disconnectedNode")
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* This method can be used to retrieve NodeActor for DOM nodes from other
|
||||
* actors in a way that they can later be highlighted in the page, or
|
||||
* selected in the inspector.
|
||||
* If an actor has a reference to a DOM node, and the UI needs to know about
|
||||
* this DOM node (and possibly select it in the inspector), the UI should
|
||||
* first retrieve a reference to the walkerFront:
|
||||
*
|
||||
* // Make sure the inspector/walker have been initialized first.
|
||||
* toolbox.initInspector().then(() => {
|
||||
* // Retrieve the walker.
|
||||
* let walker = toolbox.walker;
|
||||
* });
|
||||
*
|
||||
* And then call this method:
|
||||
*
|
||||
* // Get the nodeFront from my actor, passing the ID and properties path.
|
||||
* walker.getNodeFromActor(myActorID, ["element"]).then(nodeFront => {
|
||||
* // Use the nodeFront, e.g. select the node in the inspector.
|
||||
* toolbox.getPanel("inspector").selection.setNodeFront(nodeFront);
|
||||
* });
|
||||
*
|
||||
* @param {String} actorID The ID for the actor that has a reference to the
|
||||
* DOM node.
|
||||
* @param {Array} path Where, on the actor, is the DOM node stored. If in the
|
||||
* scope of the actor, the node is available as `this.data.node`, then this
|
||||
* should be ["data", "node"].
|
||||
* @return {NodeActor} The attached NodeActor, or null if it couldn't be found.
|
||||
*/
|
||||
getNodeFromActor: method(function(actorID, path) {
|
||||
let actor = this.conn.getActor(actorID);
|
||||
if (!actor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let obj = actor;
|
||||
for (let name of path) {
|
||||
if (!(name in obj)) {
|
||||
return null;
|
||||
}
|
||||
obj = obj[name];
|
||||
}
|
||||
|
||||
return this.attachElement(obj);
|
||||
}, {
|
||||
request: {
|
||||
actorID: Arg(0, "string"),
|
||||
path: Arg(1, "array:string")
|
||||
},
|
||||
response: {
|
||||
node: RetVal("nullable:disconnectedNode")
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -2932,6 +2980,14 @@ var WalkerFront = exports.WalkerFront = protocol.FrontClass(WalkerActor, {
|
||||
impl: "_getStyleSheetOwnerNode"
|
||||
}),
|
||||
|
||||
getNodeFromActor: protocol.custom(function(actorID, path) {
|
||||
return this._getNodeFromActor(actorID, path).then(response => {
|
||||
return response ? response.node : null;
|
||||
});
|
||||
}, {
|
||||
impl: "_getNodeFromActor"
|
||||
}),
|
||||
|
||||
_releaseFront: function(node, force) {
|
||||
if (node.retained && !force) {
|
||||
node.reparent(null);
|
||||
@@ -3080,7 +3136,7 @@ var WalkerFront = exports.WalkerFront = protocol.FrontClass(WalkerActor, {
|
||||
*/
|
||||
onMutations: protocol.preEvent("new-mutations", function() {
|
||||
// Fetch and process the mutations.
|
||||
this.getMutations({cleanup: this.autoCleanup}).then(null, console.error);
|
||||
this.getMutations({cleanup: this.autoCleanup}).catch(() => {});
|
||||
}),
|
||||
|
||||
isLocal: function() {
|
||||
|
||||
@@ -103,6 +103,20 @@
|
||||
animation: move .5s, glow 1s 2s infinite, grow 3s 1s 100;
|
||||
}
|
||||
|
||||
.all-transitions {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: blue;
|
||||
transition: all .2s;
|
||||
}
|
||||
.all-transitions.expand {
|
||||
width: 200px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
@keyframes move {
|
||||
100% {
|
||||
transform: translateY(100px);
|
||||
@@ -130,6 +144,7 @@
|
||||
<div class="delayed-transition"></div>
|
||||
<div class="delayed-multiple-animations"></div>
|
||||
<div class="multiple-animations-2"></div>
|
||||
<div class="all-transitions"></div>
|
||||
<script type="text/javascript">
|
||||
// Get the transitions started when the page loads
|
||||
var players;
|
||||
|
||||
@@ -29,6 +29,7 @@ support-files =
|
||||
[browser_animation_actors_12.js]
|
||||
[browser_animation_actors_13.js]
|
||||
[browser_animation_actors_14.js]
|
||||
[browser_animation_actors_15.js]
|
||||
[browser_canvasframe_helper_01.js]
|
||||
[browser_canvasframe_helper_02.js]
|
||||
[browser_canvasframe_helper_03.js]
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// When a transition finishes, no "removed" event is sent because it may still
|
||||
// be used, but when it restarts again (transitions back), then a new
|
||||
// AnimationPlayerFront should be sent, and the old one should be removed.
|
||||
|
||||
const {AnimationsFront} = require("devtools/server/actors/animation");
|
||||
const {InspectorFront} = require("devtools/server/actors/inspector");
|
||||
|
||||
add_task(function*() {
|
||||
let doc = yield addTab(MAIN_DOMAIN + "animation.html");
|
||||
|
||||
initDebuggerServer();
|
||||
let client = new DebuggerClient(DebuggerServer.connectPipe());
|
||||
let form = yield connectDebuggerClient(client);
|
||||
let inspector = InspectorFront(client, form);
|
||||
let walker = yield inspector.getWalker();
|
||||
let animations = AnimationsFront(client, form);
|
||||
|
||||
info("Retrieve the test node");
|
||||
let node = yield walker.querySelector(walker.rootNode, ".all-transitions");
|
||||
|
||||
info("Retrieve the animation players for the node");
|
||||
let players = yield animations.getAnimationPlayersForNode(node);
|
||||
is(players.length, 0, "The node has no animation players yet");
|
||||
|
||||
info("Listen for new animations");
|
||||
let reportedMutations = [];
|
||||
function onMutations(mutations) {
|
||||
reportedMutations = [...reportedMutations, ...mutations];
|
||||
}
|
||||
animations.on("mutations", onMutations);
|
||||
|
||||
info("Transition the node by adding the expand class");
|
||||
let cpow = content.document.querySelector(".all-transitions");
|
||||
cpow.classList.add("expand");
|
||||
info("Wait for longer than the transition");
|
||||
yield wait(500);
|
||||
|
||||
is(reportedMutations.length, 2, "2 mutation events were received");
|
||||
is(reportedMutations[0].type, "added", "The first event was 'added'");
|
||||
is(reportedMutations[1].type, "added", "The second event was 'added'");
|
||||
|
||||
reportedMutations = [];
|
||||
|
||||
info("Transition back by removing the expand class");
|
||||
cpow.classList.remove("expand");
|
||||
info("Wait for longer than the transition");
|
||||
yield wait(500);
|
||||
|
||||
is(reportedMutations.length, 4, "4 new mutation events were received");
|
||||
is(reportedMutations.filter(m => m.type === "removed").length, 2,
|
||||
"2 'removed' events were sent (for the old transitions)");
|
||||
is(reportedMutations.filter(m => m.type === "added").length, 2,
|
||||
"2 'added' events were sent (for the new transitions)");
|
||||
|
||||
animations.off("mutations", onMutations);
|
||||
|
||||
yield closeDebuggerClient(client);
|
||||
gBrowser.removeCurrentTab();
|
||||
});
|
||||
|
||||
function wait(ms) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
@@ -52,6 +52,7 @@ skip-if = buildapp == 'mulet'
|
||||
[test_inspector-dead-nodes.html]
|
||||
[test_inspector_getImageData.html]
|
||||
skip-if = buildapp == 'mulet'
|
||||
[test_inspector_getNodeFromActor.html]
|
||||
[test_inspector-hide.html]
|
||||
[test_inspector-insert.html]
|
||||
[test_inspector-mutations-attr.html]
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
https://bugzilla.mozilla.org/show_bug.cgi?id=1155653
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test for Bug 1155653</title>
|
||||
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
<script type="application/javascript;version=1.8" src="inspector-helpers.js"></script>
|
||||
<script type="application/javascript;version=1.8">
|
||||
Components.utils.import("resource://gre/modules/devtools/Loader.jsm");
|
||||
const inspector = devtools.require("devtools/server/actors/inspector");
|
||||
|
||||
window.onload = function() {
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
runNextTest();
|
||||
}
|
||||
|
||||
var gWalker;
|
||||
|
||||
addTest(function() {
|
||||
let url = document.getElementById("inspectorContent").href;
|
||||
attachURL(url, function(err, client, tab, doc) {
|
||||
let {InspectorFront} = devtools.require("devtools/server/actors/inspector");
|
||||
let inspector = InspectorFront(client, tab);
|
||||
|
||||
promiseDone(inspector.getWalker().then(walker => {
|
||||
gWalker = walker;
|
||||
}).then(runNextTest));
|
||||
});
|
||||
});
|
||||
|
||||
addTest(function() {
|
||||
info("Try to get a NodeFront from an invalid actorID");
|
||||
gWalker.getNodeFromActor("invalid", ["node"]).then(node => {
|
||||
ok(!node, "The node returned is null");
|
||||
runNextTest();
|
||||
});
|
||||
});
|
||||
|
||||
addTest(function() {
|
||||
info("Try to get a NodeFront from a valid actorID but invalid path");
|
||||
gWalker.getNodeFromActor(gWalker.actorID, ["invalid", "path"]).then(node => {
|
||||
ok(!node, "The node returned is null");
|
||||
runNextTest();
|
||||
});
|
||||
});
|
||||
|
||||
addTest(function() {
|
||||
info("Try to get a NodeFront from a valid actorID and valid path");
|
||||
gWalker.getNodeFromActor(gWalker.actorID, ["rootDoc"]).then(rootDocNode => {
|
||||
ok(rootDocNode, "A node was returned");
|
||||
is(rootDocNode, gWalker.rootNode, "The right node was returned");
|
||||
runNextTest();
|
||||
});
|
||||
});
|
||||
|
||||
addTest(function() {
|
||||
info("Try to get a NodeFront from a valid actorID and valid complex path");
|
||||
gWalker.getNodeFromActor(gWalker.actorID,
|
||||
["tabActor", "window", "document", "body"]).then(bodyNode => {
|
||||
ok(bodyNode, "A node was returned");
|
||||
gWalker.querySelector(gWalker.rootNode, "body").then(node => {
|
||||
is(bodyNode, node, "The body node was returned");
|
||||
runNextTest();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
addTest(function() {
|
||||
gWalker = null;
|
||||
runNextTest();
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1155653">Mozilla Bug 1155653</a>
|
||||
<a id="inspectorContent" target="_blank" href="inspector_getImageData.html">Test Document</a>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none">
|
||||
|
||||
</div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user