Web Inspector: add instrumentation for showing existing Web Animations
authordrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 29 Jan 2020 23:46:15 +0000 (23:46 +0000)
committerdrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 29 Jan 2020 23:46:15 +0000 (23:46 +0000)
https://bugs.webkit.org/show_bug.cgi?id=205434
<rdar://problem/28328087>

Reviewed by Brian Burg.

Source/JavaScriptCore:

* inspector/protocol/Animation.json:
Add types/commands/events for instrumenting the lifecycle of `Animation` objects, as well as
commands for getting the JavaScript wrapper object and the target DOM node.

Source/WebCore:

Add types/commands/events for instrumenting the lifecycle of `Animation` objects, as well as
commands for getting the JavaScript wrapper object and the target DOM node.

Tests: inspector/animation/effectChanged.html
       inspector/animation/lifecycle-css-animation.html
       inspector/animation/lifecycle-css-transition.html
       inspector/animation/lifecycle-web-animation.html
       inspector/animation/requestEffectTarget.html
       inspector/animation/resolveAnimation.html
       inspector/animation/targetChanged.html

* animation/WebAnimation.h:
* animation/WebAnimation.cpp:
(WebCore::WebAnimation::instances): Added.
(WebCore::WebAnimation::instancesMutex): Added.
(WebCore::WebAnimation::create):
(WebCore::WebAnimation::WebAnimation):
(WebCore::WebAnimation::~WebAnimation):
(WebCore::WebAnimation::effectTimingDidChange):
(WebCore::WebAnimation::setEffectInternal):
(WebCore::WebAnimation::effectTargetDidChange):
* animation/CSSAnimation.cpp:
(WebCore::CSSAnimation::create):
* animation/CSSTransition.cpp:
(WebCore::CSSTransition::create):

* animation/KeyframeEffect.h:
(WebCore::KeyframeEffect::parsedKeyframes const): Added.
(WebCore::KeyframeEffect::blendingKeyframes const): Added.
(WebCore::KeyframeEffect::hasBlendingKeyframes const): Deleted.
Provide a way to access the list of keyframes.

* inspector/InspectorInstrumentation.h:
(WebCore::InspectorInstrumentation::didSetWebAnimationEffect): Added.
(WebCore::InspectorInstrumentation::didChangeWebAnimationEffectTiming): Added.
(WebCore::InspectorInstrumentation::didChangeWebAnimationEffectTarget): Added.
(WebCore::InspectorInstrumentation::didCreateWebAnimation): Added.
(WebCore::InspectorInstrumentation::didChangeWebAnimationEffect): Deleted.
* inspector/InspectorInstrumentation.cpp:
(WebCore::InspectorInstrumentation::didCommitLoadImpl):
(WebCore::InspectorInstrumentation::didSetWebAnimationEffectImpl): Added.
(WebCore::InspectorInstrumentation::didChangeWebAnimationEffectTimingImpl): Added.
(WebCore::InspectorInstrumentation::didChangeWebAnimationEffectTargetImpl): Added.
(WebCore::InspectorInstrumentation::didCreateWebAnimationImpl): Added.
(WebCore::InspectorInstrumentation::willDestroyWebAnimationImpl):
(WebCore::InspectorInstrumentation::didChangeWebAnimationEffectImpl): Deleted.

* inspector/InstrumentingAgents.h:
(WebCore::InstrumentingAgents::enabledInspectorAnimationAgent const): Added.
(WebCore::InstrumentingAgents::setEnabledInspectorAnimationAgent): Added.
* inspector/InstrumentingAgents.cpp:
(WebCore::InstrumentingAgents::reset):

* inspector/agents/InspectorAnimationAgent.h:
* inspector/agents/InspectorAnimationAgent.cpp:
(WebCore::protocolValueForSeconds): Added.
(WebCore::protocolValueForPlaybackDirection): Added.
(WebCore::protocolValueForFillMode): Added.
(WebCore::buildObjectForKeyframes): Added.
(WebCore::buildObjectForEffect): Added.
(WebCore::InspectorAnimationAgent::InspectorAnimationAgent):
(WebCore::InspectorAnimationAgent::willDestroyFrontendAndBackend):
(WebCore::InspectorAnimationAgent::enable): Added.
(WebCore::InspectorAnimationAgent::disable): Added.
(WebCore::InspectorAnimationAgent::requestEffectTarget): Added.
(WebCore::InspectorAnimationAgent::resolveAnimation): Added.
(WebCore::InspectorAnimationAgent::didSetWebAnimationEffect): Added.
(WebCore::InspectorAnimationAgent::didChangeWebAnimationEffectTiming): Added.
(WebCore::InspectorAnimationAgent::didChangeWebAnimationEffectTarget): Added.
(WebCore::InspectorAnimationAgent::didCreateWebAnimation): Added.
(WebCore::InspectorAnimationAgent::willDestroyWebAnimation):
(WebCore::InspectorAnimationAgent::frameNavigated):
(WebCore::InspectorAnimationAgent::findAnimationId): Added.
(WebCore::InspectorAnimationAgent::assertAnimation): Added.
(WebCore::InspectorAnimationAgent::bindAnimation): Added.
(WebCore::InspectorAnimationAgent::unbindAnimation): Added.
(WebCore::InspectorAnimationAgent::animationDestroyedTimerFired): Added.
(WebCore::InspectorAnimationAgent::reset): Added.
(WebCore::InspectorAnimationAgent::didChangeWebAnimationEffect): Deleted.

* inspector/agents/InspectorDOMAgent.h:
* inspector/agents/InspectorDOMAgent.cpp:
(WebCore::InspectorDOMAgent::pushNodeToFrontend):
(WebCore::InspectorDOMAgent::querySelector):
(WebCore::InspectorDOMAgent::pushNodePathToFrontend):
(WebCore::InspectorDOMAgent::setNodeName):
(WebCore::InspectorDOMAgent::setOuterHTML):
(WebCore::InspectorDOMAgent::moveTo):
(WebCore::InspectorDOMAgent::requestNode):
(WebCore::InspectorDOMAgent::pushNodeByPathToFrontend):
Add an overload for `pushNodePathToFrontend` that exposes an `ErrorString`.

Source/WebInspectorUI:

* UserInterface/Controllers/AnimationManager.js: Added.
(WI.AnimationManager):
(WI.AnimationManager.prototype.get domains):
(WI.AnimationManager.prototype.activateExtraDomain):
(WI.AnimationManager.prototype.initializeTarget):
(WI.AnimationManager.prototype.get animationCollection):
(WI.AnimationManager.prototype.get supported):
(WI.AnimationManager.prototype.enable):
(WI.AnimationManager.prototype.disable):
(WI.AnimationManager.prototype.animationCreated):
(WI.AnimationManager.prototype.effectChanged):
(WI.AnimationManager.prototype.targetChanged):
(WI.AnimationManager.prototype.animationDestroyed):
(WI.AnimationManager.prototype._handleMainResourceDidChange):
* UserInterface/Protocol/AnimationObserver.js:
(WI.AnimationObserver.prototype.animationCreated): Added.
(WI.AnimationObserver.prototype.effectChanged): Added.
(WI.AnimationObserver.prototype.targetChanged): Added.
(WI.AnimationObserver.prototype.animationDestroyed): Added.

* UserInterface/Models/AnimationCollection.js: Added.
(WI.AnimationCollection):
(WI.AnimationCollection.prototype.get animationType):
(WI.AnimationCollection.prototype.get displayName):
(WI.AnimationCollection.prototype.objectIsRequiredType):
(WI.AnimationCollection.prototype.animationCollectionForType):
(WI.AnimationCollection.prototype.itemAdded):
(WI.AnimationCollection.prototype.itemRemoved):
(WI.AnimationCollection.prototype.itemsCleared):
Similar to `WI.ResourceCollection`, create a subclass of `WI.Collection` that maintains it's
own sub-`WI.AnimationCollection`s for each type of `WI.Animation.Type`.

* UserInterface/Models/Animation.js: Added.
(WI.Animation):
(WI.Animation.fromPayload):
(WI.Animation.displayNameForAnimationType):
(WI.Animation.displayNameForPlaybackDirection):
(WI.Animation.displayNameForFillMode):
(WI.Animation.resetUniqueDisplayNameNumbers):
(WI.Animation.prototype.get animationId):
(WI.Animation.prototype.get backtrace):
(WI.Animation.prototype.get animationType):
(WI.Animation.prototype.get startDelay):
(WI.Animation.prototype.get endDelay):
(WI.Animation.prototype.get iterationCount):
(WI.Animation.prototype.get iterationStart):
(WI.Animation.prototype.get iterationDuration):
(WI.Animation.prototype.get timingFunction):
(WI.Animation.prototype.get playbackDirection):
(WI.Animation.prototype.get fillMode):
(WI.Animation.prototype.get keyframes):
(WI.Animation.prototype.get displayName):
(WI.Animation.prototype.requestEffectTarget):
(WI.Animation.prototype.effectChanged):
(WI.Animation.prototype.targetChanged):
(WI.Animation.prototype._updateEffect):
* UserInterface/Protocol/RemoteObject.js:
(WI.RemoteObject.resolveAnimation): Added.

* UserInterface/Views/GraphicsTabContentView.js: Renamed from Source/WebInspectorUI/UserInterface/Views/CanvasTabContentView.js.
(WI.GraphicsTabContentView):
(WI.GraphicsTabContentView.tabInfo):
(WI.GraphicsTabContentView.isTabAllowed):
(WI.GraphicsTabContentView.prototype.get type):
(WI.GraphicsTabContentView.prototype.showRepresentedObject): Added.
(WI.GraphicsTabContentView.prototype.canShowRepresentedObject):
(WI.GraphicsTabContentView.prototype.closed):
(WI.GraphicsTabContentView.prototype.attached):
(WI.GraphicsTabContentView.prototype.detached):
(WI.GraphicsTabContentView.prototype.initialLayout): Added.
(WI.GraphicsTabContentView.prototype._handleOverviewTreeOutlineSelectionDidChange): Added.
* UserInterface/Views/GraphicsTabContentView.css: Renamed from Source/WebInspectorUI/UserInterface/Views/CanvasTabContentView.css.
Rename the Canvas Tab to Graphics Tab and display four sections:
 - Canvases
 - Web Animations
 - CSS Animations
 - CSS Transitions

* UserInterface/Views/CanvasSidebarPanel.js:
(WI.CanvasSidebarPanel.prototype.canShowRepresentedObject):
Only appear if a `WI.Canvas` or `WI.Recording` is selected.

* UserInterface/Views/GraphicsOverviewContentView.js: Added.
(WI.GraphicsOverviewContentView):
(WI.GraphicsOverviewContentView.prototype.get supplementalRepresentedObjects):
(WI.GraphicsOverviewContentView.prototype.get navigationItems):
(WI.GraphicsOverviewContentView.prototype.attached):
(WI.GraphicsOverviewContentView.prototype.detached):
(WI.GraphicsOverviewContentView.prototype.initialLayout):
(WI.GraphicsOverviewContentView.prototype.dropZoneShouldAppearForDragEvent):
(WI.GraphicsOverviewContentView.prototype.dropZoneHandleDrop):
(WI.GraphicsOverviewContentView.prototype._handleRefreshButtonClicked):
(WI.GraphicsOverviewContentView.prototype._handleShowGridButtonClicked):
(WI.GraphicsOverviewContentView.prototype._handleShowImageGridSettingChanged):
(WI.GraphicsOverviewContentView.prototype._handleImportButtonNavigationItemClicked):
(WI.GraphicsOverviewContentView.prototype._handleOverviewViewSelectedItemChanged):
(WI.GraphicsOverviewContentView.prototype._handleOverviewViewSupplementalRepresentedObjectsDidChange):
(WI.GraphicsOverviewContentView.prototype._handleClick):
* UserInterface/Views/GraphicsOverviewContentView.css: Added.
(.content-view.graphics-overview):
(.content-view.graphics-overview > section):
(.content-view.graphics-overview > section:not(:first-child)):
(.content-view.graphics-overview > section > .header):
(.content-view.graphics-overview > section:not(:first-of-type) > .header):
(.content-view.graphics-overview > section > .header > h1):
(.content-view.graphics-overview > section > .header > .navigation-bar):
(.content-view.graphics-overview > .content-view.canvas-overview):
(@media (prefers-color-scheme: light) .content-view.graphics-overview):
(@media (prefers-color-scheme: light) .content-view.graphics-overview > section > .header):
Add sticky headers for each of the sections described above.

* UserInterface/Views/AnimationCollectionContentView.js: Added.
(WI.AnimationCollectionContentView):
(WI.AnimationCollectionContentView.prototype.handleRefreshButtonClicked):
(WI.AnimationCollectionContentView.prototype.contentViewAdded):
(WI.AnimationCollectionContentView.prototype.contentViewRemoved):
(WI.AnimationCollectionContentView.prototype.detached):
(WI.AnimationCollectionContentView.prototype._handleContentViewMouseEnter):
(WI.AnimationCollectionContentView.prototype._handleContentViewMouseLeave):
* UserInterface/Views/AnimationCollectionContentView.css: Added.
(.content-view.animation-collection):

* UserInterface/Views/AnimationContentView.js: Added.
(WI.AnimationContentView):
(WI.AnimationContentView.get previewHeight):
(WI.AnimationContentView.prototype.handleRefreshButtonClicked):
(WI.AnimationContentView.prototype.initialLayout):
(WI.AnimationContentView.prototype.layout):
(WI.AnimationContentView.prototype.sizeDidChange):
(WI.AnimationContentView.prototype.attached):
(WI.AnimationContentView.prototype.detached):
(WI.AnimationContentView.prototype._refreshSubtitle):
(WI.AnimationContentView.prototype._refreshPreview.addTitle):
(WI.AnimationContentView.prototype._refreshPreview):
(WI.AnimationContentView.prototype._handleEffectChanged):
(WI.AnimationContentView.prototype._handleTargetChanged):
(WI.AnimationContentView.prototype._populateAnimationTargetButtonContextMenu):
* UserInterface/Views/AnimationContentView.css: Added.
(.content-view.animation):
(.content-view.animation.selected):
(.content-view.animation > header):
(.content-view.animation > header > .titles):
(.content-view.animation > header > .titles > .title):
(.content-view.animation > header > .titles > .subtitle):
(.content-view.animation > header > .titles > .subtitle:not(:empty)::before):
(.content-view.animation > header > .navigation-bar):
(.content-view.animation:hover > header > .navigation-bar):
(.content-view.animation > .preview):
(.content-view.animation > .preview > svg):
(body[dir=rtl] .content-view.animation > .preview > svg):
(.content-view.animation > .preview > svg rect):
(.content-view.animation > .preview > svg > .delay line):
(.content-view.animation > .preview > svg > .active path):
(.content-view.animation > .preview > svg > .active circle):
(.content-view.animation > .preview > svg > .active line):
(.content-view.animation > .preview > span):
(@media (prefers-color-scheme: dark) .content-view.animation > header > .titles > .title):
(@media (prefers-color-scheme: dark) .content-view.animation > header > .titles > .subtitle):
(@media (prefers-color-scheme: dark) .content-view.animation > .preview):
Visualize the start/end delay and keyframes of the given animation as a series of bezier
curves separated by markers.

* UserInterface/Views/AnimationDetailsSidebarPanel.js: Added.
(WI.AnimationDetailsSidebarPanel):
(WI.AnimationDetailsSidebarPanel.prototype.inspect):
(WI.AnimationDetailsSidebarPanel.prototype.get animation):
(WI.AnimationDetailsSidebarPanel.prototype.set animation):
(WI.AnimationDetailsSidebarPanel.prototype.initialLayout):
(WI.AnimationDetailsSidebarPanel.prototype.layout):
(WI.AnimationDetailsSidebarPanel.prototype._refreshIdentitySection):
(WI.AnimationDetailsSidebarPanel.prototype._refreshEffectSection):
(WI.AnimationDetailsSidebarPanel.prototype._refreshBacktraceSection):
(WI.AnimationDetailsSidebarPanel.prototype._handleAnimationEffectChanged):
(WI.AnimationDetailsSidebarPanel.prototype._handleAnimationTargetChanged):
(WI.AnimationDetailsSidebarPanel.prototype._handleDetailsSectionCollapsedStateChanged):
* UserInterface/Views/AnimationDetailsSidebarPanel.css: Added.
(.sidebar > .panel.details.animation > .content > .details-section.animation-keyframes .header > .subtitle):
(.sidebar > .panel.details.animation > .content > .details-section.animation-keyframes .details-section):
(.sidebar > .panel.details.animation > .content > .details-section.animation-keyframes .details-section .row.styles):
(.sidebar > .panel.details.animation > .content > .details-section.animation-keyframes .details-section .row.styles .CodeMirror):
(.sidebar > .panel.details.animation > .content > .details-section.animation-keyframes .details-section):
Show collected information about the selected animation, its effect, and its target.

* UserInterface/Controllers/CanvasManager.js:
(WI.CanvasManager):
(WI.CanvasManager.prototype.get canvasCollection): Added.
(WI.CanvasManager.prototype.disable):
(WI.CanvasManager.prototype.canvasAdded):
(WI.CanvasManager.prototype.canvasRemoved):
(WI.CanvasManager.prototype._saveRecordings): Added.
(WI.CanvasManager.prototype._mainResourceDidChange):
(WI.CanvasManager.prototype.get canvases): Deleted.
(WI.CanvasManager.prototype._removeCanvas): Deleted.
Rather than have the `WI.CanvasTabContentView` mainain the `WI.CanvasCollection` and have to
listen for events from the `WI.CanvasManager`, just have the `WI.CanvasManager` hold on to
it instead and provide a getter for it.

* UserInterface/Views/CanvasOverviewContentView.js:
(WI.CanvasOverviewContentView):
(WI.CanvasOverviewContentView.prototype.get navigationItems):
(WI.CanvasOverviewContentView.prototype.handleRefreshButtonClicked):
(WI.CanvasOverviewContentView.prototype.contentViewAdded):
(WI.CanvasOverviewContentView.prototype.contentViewRemoved):
(WI.CanvasOverviewContentView.prototype.attached):
(WI.CanvasOverviewContentView.prototype.detached):
(WI.CanvasOverviewContentView.prototype._addSavedRecording):
(WI.CanvasOverviewContentView.prototype.hidden): Deleted.
(WI.CanvasOverviewContentView.prototype.get _itemMargin): Deleted.
(WI.CanvasOverviewContentView.prototype._refreshPreviews): Deleted.
(WI.CanvasOverviewContentView.prototype._updateNavigationItems): Deleted.
(WI.CanvasOverviewContentView.prototype._showGridButtonClicked): Deleted.
(WI.CanvasOverviewContentView.prototype._updateShowImageGrid): Deleted.
* UserInterface/Views/CanvasOverviewContentView.css:
(.content-view.canvas-overview):
(.content-view.canvas-overview > .content-view.canvas):
(@media (prefers-color-scheme: dark) .content-view.canvas-overview): Deleted.

* UserInterface/Views/CanvasContentView.js:
(WI.CanvasContentView):
(WI.CanvasContentView.prototype.handleRefreshButtonClicked): Added.
(WI.CanvasContentView.prototype.dropZoneShouldAppearForDragEvent): Added.
(WI.CanvasContentView.prototype.dropZoneHandleDrop): Added.
(WI.CanvasContentView.prototype.initialLayout):
(WI.CanvasContentView.prototype.attached):
(WI.CanvasContentView.prototype._populateCanvasElementButtonContextMenu):
(WI.CanvasContentView.prototype.shown): Deleted.
Move the "Log Canvas Context" to be the first item in the canvas element button context menu.
Drive-by: add a `WI.DropZoneView` for when recording JSON files are dragged on top.
* UserInterface/Views/CanvasContentView.css:
Drive-by: drop `:not(.tab)` from all selectors since the Canvas Tab doesn't exist anymore.
* UserInterface/Views/CollectionContentView.js:
(WI.CollectionContentView):
(WI.CollectionContentView.prototype.get selectedItem): Added.
(WI.CollectionContentView.prototype.set selectedItem): Added.
(WI.CollectionContentView.prototype.addContentViewForItem):
(WI.CollectionContentView.prototype.removeContentViewForItem):
(WI.CollectionContentView.prototype.showContentPlaceholder):
(WI.CollectionContentView.prototype.initialLayout):
(WI.CollectionContentView.prototype._selectItem):
(WI.CollectionContentView.prototype._handleClick): Added.
(WI.CollectionContentView.prototype.setSelectedItem): Deleted.
* UserInterface/Views/CollectionContentView.css:
(.content-view.collection > .placeholder:not(.message-text-view)): Added.
(.content-view.collection .resource.image img): Deleted.
(.content-view.collection .resource.image img:hover): Deleted.
When selection is enabled, clicking outside of any of the content views should dismiss the
current selection. Clients should also be able to get the currently selected item.

* UserInterface/Views/DetailsSectionSimpleRow.js:
(WI.DetailsSectionSimpleRow.prototype.set value):
Ensure that `0` is considered as a valid value.

* UserInterface/Base/Main.js:
(WI.loaded):
(WI.contentLoaded):
(WI.tabContentViewClassForRepresentedObject):
* UserInterface/Views/ContentView.js:
(WI.ContentView.createFromRepresentedObject):
(WI.ContentView.isViewable):
Allow `WI.Animation` to be viewable.

* UserInterface/Views/Main.css:
(.navigation-item-help): Added.
(.navigation-item-help > .navigation-bar): Added.
(.navigation-item-help > .navigation-bar > .item): Added.
(.message-text-view .navigation-item-help): Deleted.
(.message-text-view .navigation-item-help .navigation-bar): Deleted.
(.message-text-view .navigation-item-help .navigation-bar > .item): Deleted.
Allow `WI.createNavigationItemHelp` to be used independently of `WI.createMessageTextView`.

* UserInterface/Controllers/DOMManager.js:
(WI.DOMManager.prototype.nodeForId):
* UserInterface/Controllers/TimelineManager.js:
(WI.TimelineManager.prototype.animationTrackingUpdated):
* UserInterface/Models/AuditTestCaseResult.js:
(WI.AuditTestCaseResult.async fromPayload):
Add a fallback so callers don't need to.

* UserInterface/Views/ResourceCollectionContentView.js:
(WI.ResourceCollectionContentView):
* UserInterface/Views/ResourceCollectionContentView.css:
(.content-view.resource-collection > .resource.image img): Added.
(.content-view.resource-collection > .resource.image img:hover): Added.
Drive-by: move these styles to the right file and make them more specific.
* UserInterface/Models/Canvas.js:
(WI.Canvas.displayNameForContextType):
* UserInterface/Models/Recording.js:
(WI.Recording.displayNameForRecordingType): Added.
Drive-by: fix localized strings.
* UserInterface/Views/RecordingContentView.css:
Drive-by: drop `:not(.tab)` from all selectors since the Recording Tab doesn't exist anymore.
* UserInterface/Main.html:
* UserInterface/Images/Graphics.svg: Renamed from Source/WebInspectorUI/UserInterface/Images/Canvas.svg.
* Localizations/en.lproj/localizedStrings.js:

* UserInterface/Test.html:
* UserInterface/Test/Test.js:
(WI.loaded):

* UserInterface/Test/TestHarness.js:
(TestHarness.prototype.expectEmpty): Added.
(TestHarness.prototype.expectNotEmpty): Added.
(TestHarness.prototype._expectationMessageFormat):
(TestHarness.prototype._expectedValueFormat):
Add utility function for checking whether the given value is empty:
 - Array `length === 0`
 - String `length === 0`
 - Set `size === 0`
 - Map `size === 0`
 - Object `isEmptyObject`
Any other type will automatically fail, as non-objects can't be "empty" (e.g. `42`).

LayoutTests:

* inspector/animation/effectChanged.html: Added.
* inspector/animation/effectChanged-expected.txt: Added.
* inspector/animation/lifecycle-css-animation.html: Added.
* inspector/animation/lifecycle-css-animation-expected.txt: Added.
* inspector/animation/lifecycle-css-transition.html: Added.
* inspector/animation/lifecycle-css-transition-expected.txt: Added.
* inspector/animation/lifecycle-web-animation.html: Added.
* inspector/animation/lifecycle-web-animation-expected.txt: Added.
* inspector/animation/requestEffectTarget.html: Added.
* inspector/animation/requestEffectTarget-expected.txt: Added.
* inspector/animation/resolveAnimation.html: Added.
* inspector/animation/resolveAnimation-expected.txt: Added.
* inspector/animation/targetChanged.html: Added.
* inspector/animation/targetChanged-expected.txt: Added.
* inspector/animation/resources/lifecycle-utilities.js: Added.
(createAnimation):
(destroyAnimations):
(InspectorTest.AnimationLifecycleUtilities.async awaitAnimationCreated):
(InspectorTest.AnimationLifecycleUtilities.async awaitAnimationDestroyed):
(InspectorTest.AnimationLifecycleUtilities.async createAnimation):
(InspectorTest.AnimationLifecycleUtilities.async destroyAnimations):

* inspector/canvas/create-context-webgpu.html:
* inspector/canvas/resources/create-context-utilities.js:
(destroyCanvases):
(awaitCanvasAdded):
(InspectorTest.CreateContextUtilities.initializeTestSuite):

* inspector/canvas/context-attributes.html:
* inspector/canvas/extensions.html:
* inspector/canvas/memory.html:
* inspector/canvas/requestClientNodes.html:
* inspector/canvas/requestContent-2d.html:
* inspector/canvas/requestContent-bitmaprenderer.html:
* inspector/canvas/requestContent-webgl.html:
* inspector/canvas/requestContent-webgl2.html:
* inspector/canvas/requestNode.html:
* inspector/canvas/resolveContext-2d.html:
* inspector/canvas/resolveContext-bitmaprenderer.html:
* inspector/canvas/resolveContext-webgl.html:
* inspector/canvas/resolveContext-webgl2.html:
* inspector/canvas/resolveContext-webgpu.html:

* inspector/canvas/recording.html:
* inspector/canvas/setRecordingAutoCaptureFrameCount.html:
* inspector/canvas/resources/recording-utilities.js:
(window.getCanvas):

* inspector/canvas/shaderProgram-add-remove-webgpu.html:
* inspector/canvas/updateShader-webgpu-sharedVertexFragment.html:
* inspector/canvas/resources/shaderProgram-utilities-webgpu.js:
* inspector/canvas/resources/shaderProgram-utilities-webgl.js:
(deleteContext):
(whenProgramAdded):
(window.initializeTestSuite):
(window.addParentCanvasRemovedTestCase):

* inspector/unit-tests/test-harness-expect-functions.html:
* inspector/unit-tests/test-harness-expect-functions-expected.txt:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@255396 268f45cc-cd09-0410-ab3c-d52691b4dbfc

97 files changed:
LayoutTests/ChangeLog
LayoutTests/inspector/animation/effectChanged-expected.txt [new file with mode: 0644]
LayoutTests/inspector/animation/effectChanged.html [new file with mode: 0644]
LayoutTests/inspector/animation/lifecycle-css-animation-expected.txt [new file with mode: 0644]
LayoutTests/inspector/animation/lifecycle-css-animation.html [new file with mode: 0644]
LayoutTests/inspector/animation/lifecycle-css-transition-expected.txt [new file with mode: 0644]
LayoutTests/inspector/animation/lifecycle-css-transition.html [new file with mode: 0644]
LayoutTests/inspector/animation/lifecycle-web-animation-expected.txt [new file with mode: 0644]
LayoutTests/inspector/animation/lifecycle-web-animation.html [new file with mode: 0644]
LayoutTests/inspector/animation/resolveAnimation-expected.txt [new file with mode: 0644]
LayoutTests/inspector/animation/resolveAnimation.html [new file with mode: 0644]
LayoutTests/inspector/animation/resources/lifecycle-utilities.js [new file with mode: 0644]
LayoutTests/inspector/animation/targetChanged-expected.txt [new file with mode: 0644]
LayoutTests/inspector/animation/targetChanged.html [new file with mode: 0644]
LayoutTests/inspector/canvas/context-attributes.html
LayoutTests/inspector/canvas/create-context-webgpu.html
LayoutTests/inspector/canvas/extensions.html
LayoutTests/inspector/canvas/memory.html
LayoutTests/inspector/canvas/recording.html
LayoutTests/inspector/canvas/requestClientNodes.html
LayoutTests/inspector/canvas/requestContent-2d.html
LayoutTests/inspector/canvas/requestContent-bitmaprenderer.html
LayoutTests/inspector/canvas/requestContent-webgl.html
LayoutTests/inspector/canvas/requestContent-webgl2.html
LayoutTests/inspector/canvas/requestNode.html
LayoutTests/inspector/canvas/resolveContext-2d.html
LayoutTests/inspector/canvas/resolveContext-bitmaprenderer.html
LayoutTests/inspector/canvas/resolveContext-webgl.html
LayoutTests/inspector/canvas/resolveContext-webgl2.html
LayoutTests/inspector/canvas/resolveContext-webgpu.html
LayoutTests/inspector/canvas/resources/create-context-utilities.js
LayoutTests/inspector/canvas/resources/recording-utilities.js
LayoutTests/inspector/canvas/resources/shaderProgram-utilities-webgl.js
LayoutTests/inspector/canvas/resources/shaderProgram-utilities-webgpu.js
LayoutTests/inspector/canvas/setRecordingAutoCaptureFrameCount.html
LayoutTests/inspector/canvas/shaderProgram-add-remove-webgpu.html
LayoutTests/inspector/canvas/updateShader-webgpu-sharedVertexFragment.html
LayoutTests/inspector/unit-tests/test-harness-expect-functions-expected.txt
LayoutTests/inspector/unit-tests/test-harness-expect-functions.html
Source/JavaScriptCore/ChangeLog
Source/JavaScriptCore/inspector/protocol/Animation.json
Source/WebCore/ChangeLog
Source/WebCore/animation/CSSAnimation.cpp
Source/WebCore/animation/CSSTransition.cpp
Source/WebCore/animation/KeyframeEffect.h
Source/WebCore/animation/WebAnimation.cpp
Source/WebCore/animation/WebAnimation.h
Source/WebCore/inspector/InspectorInstrumentation.cpp
Source/WebCore/inspector/InspectorInstrumentation.h
Source/WebCore/inspector/InstrumentingAgents.cpp
Source/WebCore/inspector/InstrumentingAgents.h
Source/WebCore/inspector/agents/InspectorAnimationAgent.cpp
Source/WebCore/inspector/agents/InspectorAnimationAgent.h
Source/WebCore/inspector/agents/InspectorDOMAgent.cpp
Source/WebCore/inspector/agents/InspectorDOMAgent.h
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
Source/WebInspectorUI/UserInterface/Base/Main.js
Source/WebInspectorUI/UserInterface/Controllers/AnimationManager.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Controllers/CanvasManager.js
Source/WebInspectorUI/UserInterface/Controllers/DOMManager.js
Source/WebInspectorUI/UserInterface/Controllers/TimelineManager.js
Source/WebInspectorUI/UserInterface/Images/Graphics.svg [moved from Source/WebInspectorUI/UserInterface/Images/Canvas.svg with 82% similarity]
Source/WebInspectorUI/UserInterface/Main.html
Source/WebInspectorUI/UserInterface/Models/Animation.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Models/AnimationCollection.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Models/AuditTestCaseResult.js
Source/WebInspectorUI/UserInterface/Models/Canvas.js
Source/WebInspectorUI/UserInterface/Models/Recording.js
Source/WebInspectorUI/UserInterface/Protocol/AnimationObserver.js
Source/WebInspectorUI/UserInterface/Protocol/RemoteObject.js
Source/WebInspectorUI/UserInterface/Test.html
Source/WebInspectorUI/UserInterface/Test/Test.js
Source/WebInspectorUI/UserInterface/Test/TestHarness.js
Source/WebInspectorUI/UserInterface/Views/AnimationCollectionContentView.css [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/AnimationCollectionContentView.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/AnimationContentView.css [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/AnimationContentView.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/AnimationDetailsSidebarPanel.css [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/AnimationDetailsSidebarPanel.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/CanvasContentView.css
Source/WebInspectorUI/UserInterface/Views/CanvasContentView.js
Source/WebInspectorUI/UserInterface/Views/CanvasOverviewContentView.css
Source/WebInspectorUI/UserInterface/Views/CanvasOverviewContentView.js
Source/WebInspectorUI/UserInterface/Views/CanvasSidebarPanel.js
Source/WebInspectorUI/UserInterface/Views/CollectionContentView.css
Source/WebInspectorUI/UserInterface/Views/CollectionContentView.js
Source/WebInspectorUI/UserInterface/Views/ContentView.js
Source/WebInspectorUI/UserInterface/Views/DetailsSectionSimpleRow.js
Source/WebInspectorUI/UserInterface/Views/GraphicsOverviewContentView.css [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/GraphicsOverviewContentView.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/GraphicsTabContentView.css [moved from Source/WebInspectorUI/UserInterface/Views/CanvasTabContentView.css with 70% similarity]
Source/WebInspectorUI/UserInterface/Views/GraphicsTabContentView.js [moved from Source/WebInspectorUI/UserInterface/Views/CanvasTabContentView.js with 57% similarity]
Source/WebInspectorUI/UserInterface/Views/Main.css
Source/WebInspectorUI/UserInterface/Views/RecordingContentView.css
Source/WebInspectorUI/UserInterface/Views/ResourceCollectionContentView.css
Source/WebInspectorUI/UserInterface/Views/ResourceCollectionContentView.js

index b74d638..2d41c89 100644 (file)
@@ -1,3 +1,71 @@
+2020-01-29  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: add instrumentation for showing existing Web Animations
+        https://bugs.webkit.org/show_bug.cgi?id=205434
+        <rdar://problem/28328087>
+
+        Reviewed by Brian Burg.
+
+        * inspector/animation/effectChanged.html: Added.
+        * inspector/animation/effectChanged-expected.txt: Added.
+        * inspector/animation/lifecycle-css-animation.html: Added.
+        * inspector/animation/lifecycle-css-animation-expected.txt: Added.
+        * inspector/animation/lifecycle-css-transition.html: Added.
+        * inspector/animation/lifecycle-css-transition-expected.txt: Added.
+        * inspector/animation/lifecycle-web-animation.html: Added.
+        * inspector/animation/lifecycle-web-animation-expected.txt: Added.
+        * inspector/animation/requestEffectTarget.html: Added.
+        * inspector/animation/requestEffectTarget-expected.txt: Added.
+        * inspector/animation/resolveAnimation.html: Added.
+        * inspector/animation/resolveAnimation-expected.txt: Added.
+        * inspector/animation/targetChanged.html: Added.
+        * inspector/animation/targetChanged-expected.txt: Added.
+        * inspector/animation/resources/lifecycle-utilities.js: Added.
+        (createAnimation):
+        (destroyAnimations):
+        (InspectorTest.AnimationLifecycleUtilities.async awaitAnimationCreated):
+        (InspectorTest.AnimationLifecycleUtilities.async awaitAnimationDestroyed):
+        (InspectorTest.AnimationLifecycleUtilities.async createAnimation):
+        (InspectorTest.AnimationLifecycleUtilities.async destroyAnimations):
+
+        * inspector/canvas/create-context-webgpu.html:
+        * inspector/canvas/resources/create-context-utilities.js:
+        (destroyCanvases):
+        (awaitCanvasAdded):
+        (InspectorTest.CreateContextUtilities.initializeTestSuite):
+
+        * inspector/canvas/context-attributes.html:
+        * inspector/canvas/extensions.html:
+        * inspector/canvas/memory.html:
+        * inspector/canvas/requestClientNodes.html:
+        * inspector/canvas/requestContent-2d.html:
+        * inspector/canvas/requestContent-bitmaprenderer.html:
+        * inspector/canvas/requestContent-webgl.html:
+        * inspector/canvas/requestContent-webgl2.html:
+        * inspector/canvas/requestNode.html:
+        * inspector/canvas/resolveContext-2d.html:
+        * inspector/canvas/resolveContext-bitmaprenderer.html:
+        * inspector/canvas/resolveContext-webgl.html:
+        * inspector/canvas/resolveContext-webgl2.html:
+        * inspector/canvas/resolveContext-webgpu.html:
+
+        * inspector/canvas/recording.html:
+        * inspector/canvas/setRecordingAutoCaptureFrameCount.html:
+        * inspector/canvas/resources/recording-utilities.js:
+        (window.getCanvas):
+
+        * inspector/canvas/shaderProgram-add-remove-webgpu.html:
+        * inspector/canvas/updateShader-webgpu-sharedVertexFragment.html:
+        * inspector/canvas/resources/shaderProgram-utilities-webgpu.js:
+        * inspector/canvas/resources/shaderProgram-utilities-webgl.js:
+        (deleteContext):
+        (whenProgramAdded):
+        (window.initializeTestSuite):
+        (window.addParentCanvasRemovedTestCase):
+
+        * inspector/unit-tests/test-harness-expect-functions.html:
+        * inspector/unit-tests/test-harness-expect-functions-expected.txt:
+
 2020-01-29  Jason Lawrence  <lawrence.j@apple.com>
 
         REGRESSION: [ iOS ] scrollingcoordinator/ios/scroll-position-after-reattach.html is a flaky failure
diff --git a/LayoutTests/inspector/animation/effectChanged-expected.txt b/LayoutTests/inspector/animation/effectChanged-expected.txt
new file mode 100644 (file)
index 0000000..b2f33cb
--- /dev/null
@@ -0,0 +1,18 @@
+Tests for the Animation.effectChanged event.
+
+
+== Running test suite: Animation.effectChanged
+-- Running test case: Animation.effectChanged.NewEffect
+PASS: Animation should have an effect.
+Changing effect...
+
+PASS: Animation should have an effect.
+PASS: Animation effect should have changed.
+
+-- Running test case: Animation.effectChanged.NullEffect
+PASS: Animation should have an effect.
+Changing effect...
+
+PASS: Animation should not have an effect.
+PASS: Animation effect should have changed.
+
diff --git a/LayoutTests/inspector/animation/effectChanged.html b/LayoutTests/inspector/animation/effectChanged.html
new file mode 100644 (file)
index 0000000..045d85b
--- /dev/null
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="../../http/tests/inspector/resources/inspector-test.js"></script>
+<script>
+function load() {
+    window.animation = document.body.animate([{opacity: 0}]);
+
+    runTest();
+}
+
+function test()
+{
+    let suite = InspectorTest.createAsyncSuite("Animation.effectChanged");
+
+    suite.addTestCase({
+        name: "Animation.effectChanged.NewEffect",
+        description: "Should return a valid object for the given animation identifier.",
+        async test() {
+            let animations = Array.from(WI.animationManager.animationCollection);
+            InspectorTest.assert(animations.length === 1, "There should only be one animation.");
+
+            let animation = animations[0];
+            if (!animation) {
+                throw `Missing animation.`;
+                return;
+            }
+
+            InspectorTest.assert(animation.animationType === WI.Animation.Type.WebAnimation, "Animation should be a web animation.");
+
+            let oldKeyframes = animation.keyframes;
+            InspectorTest.expectNotEmpty(oldKeyframes, "Animation should have an effect.");
+
+            InspectorTest.log("Changing effect...\n");
+            await Promise.all([
+                animation.awaitEvent(WI.Animation.Event.EffectChanged),
+                InspectorTest.evaluateInPage(`window.animation.effect = new KeyframeEffect(window.animation.effect.target, [{opacity: 1}], {})`),
+            ]);
+
+            let newKeyframes = animation.keyframes;
+            InspectorTest.expectNotEmpty(newKeyframes, "Animation should have an effect.");
+
+            InspectorTest.expectNotShallowEqual(newKeyframes, oldKeyframes, "Animation effect should have changed.");
+        },
+    });
+
+    suite.addTestCase({
+        name: "Animation.effectChanged.NullEffect",
+        description: "Should return a valid object for the given animation identifier.",
+        async test() {
+            let animations = Array.from(WI.animationManager.animationCollection);
+            InspectorTest.assert(animations.length === 1, "There should only be one animation.");
+
+            let animation = animations[0];
+            if (!animation) {
+                throw `Missing animation.`;
+                return;
+            }
+
+            InspectorTest.assert(animation.animationType === WI.Animation.Type.WebAnimation, "Animation should be a web animation.");
+
+            let oldKeyframes = animation.keyframes;
+            InspectorTest.expectNotEmpty(oldKeyframes, "Animation should have an effect.");
+
+            InspectorTest.log("Changing effect...\n");
+            await Promise.all([
+                animation.awaitEvent(WI.Animation.Event.EffectChanged),
+                InspectorTest.evaluateInPage(`window.animation.effect = null`),
+            ]);
+
+            let newKeyframes = animation.keyframes;
+            InspectorTest.expectEmpty(newKeyframes, "Animation should not have an effect.");
+
+            InspectorTest.expectNotShallowEqual(newKeyframes, oldKeyframes, "Animation effect should have changed.");
+        },
+    });
+
+    suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body onload="load()">
+    <p>Tests for the Animation.effectChanged event.</p>
+</body>
+</html>
diff --git a/LayoutTests/inspector/animation/lifecycle-css-animation-expected.txt b/LayoutTests/inspector/animation/lifecycle-css-animation-expected.txt
new file mode 100644 (file)
index 0000000..90dd2bf
--- /dev/null
@@ -0,0 +1,38 @@
+Tests for the Animation.animationCreated and Animation.animationDestroyed events.
+
+
+== Running test suite: Animation.Lifecycle
+-- Running test case: Animation.Lifecycle.CSSAnimation
+PASS: There should not be any animations.
+PASS: Animation created.
+PASS: Animation type should be CSS Animation.
+startDelay: 100
+iterationCount: 2
+iterationDuration: 400
+timingFunction: "linear"
+playbackDirection: "alternate"
+fillMode: "both"
+keyframes:
+[
+  {
+    "offset": 0,
+    "easing": "cubic-bezier(0.1, 0.2, 0.3, 0.4)",
+    "style": "color: rgb(255, 0, 0);\nopacity: 0;"
+  },
+  {
+    "offset": 0.5,
+    "easing": "cubic-bezier(0.1, 0.2, 0.3, 0.4)",
+    "style": "color: rgb(0, 128, 0);\nopacity: 0.5;"
+  },
+  {
+    "offset": 1,
+    "easing": "cubic-bezier(0.1, 0.2, 0.3, 0.4)",
+    "style": "color: rgb(0, 0, 255);\nopacity: 1;"
+  }
+]
+
+Destroying animations...
+
+PASS: Animation destroyed.
+PASS: Removed animation has expected ID.
+
diff --git a/LayoutTests/inspector/animation/lifecycle-css-animation.html b/LayoutTests/inspector/animation/lifecycle-css-animation.html
new file mode 100644 (file)
index 0000000..46da095
--- /dev/null
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="../../http/tests/inspector/resources/inspector-test.js"></script>
+<script src="resources/lifecycle-utilities.js"></script>
+<style>
+@keyframes fade-in {
+    from {
+        color: red;
+        opacity: 0;
+    }
+    50% {
+        color: green;
+        opacity: 0.5;
+    }
+    to {
+        color: blue;
+        opacity: 1;
+    }
+}
+div#target.active {
+    animation-name: fade-in;
+    animation-duration: 400ms;
+    animation-timing-function: cubic-bezier(0.1, 0.2, 0.3, 0.4);
+    animation-delay: 100ms;
+    animation-iteration-count: 2;
+    animation-direction: alternate;
+    animation-fill-mode: both;
+}
+</style>
+<script>
+function test()
+{
+    let suite = InspectorTest.createAsyncSuite("Animation.Lifecycle");
+
+    suite.addTestCase({
+        name: "Animation.Lifecycle.CSSAnimation",
+        description: "Check that Web Inspector is notified whenever CSS animations are created/destroyed.",
+        async test() {
+            InspectorTest.expectEqual(WI.animationManager.animationCollection.size, 0, "There should not be any animations.");
+
+            let [animation] = await Promise.all([
+                InspectorTest.AnimationLifecycleUtilities.awaitAnimationCreated(WI.Animation.Type.CSSAnimation),
+                InspectorTest.evaluateInPage(`document.getElementById("target").classList.add("active")`),
+            ]);
+
+            await Promise.all([
+                InspectorTest.AnimationLifecycleUtilities.awaitAnimationDestroyed(animation.animationId),
+                InspectorTest.AnimationLifecycleUtilities.destroyAnimations(),
+                InspectorTest.evaluateInPage(`document.getElementById("target").classList.remove("active")`),
+            ]);
+        },
+    });
+
+    suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body onload="runTest()">
+    <p>Tests for the Animation.animationCreated and Animation.animationDestroyed events.</p>
+    <div id="target"></div>
+</body>
+</html>
diff --git a/LayoutTests/inspector/animation/lifecycle-css-transition-expected.txt b/LayoutTests/inspector/animation/lifecycle-css-transition-expected.txt
new file mode 100644 (file)
index 0000000..fab2bb5
--- /dev/null
@@ -0,0 +1,33 @@
+Tests for the Animation.animationCreated and Animation.animationDestroyed events.
+
+
+== Running test suite: Animation.Lifecycle
+-- Running test case: Animation.Lifecycle.CSSTransition
+PASS: There should not be any animations.
+PASS: Animation created.
+PASS: Animation type should be CSS Transition.
+startDelay: 100
+iterationCount: 1
+iterationDuration: 400
+timingFunction: "cubic-bezier(0.1, 0.2, 0.3, 0.4)"
+playbackDirection: "normal"
+fillMode: "backwards"
+keyframes:
+[
+  {
+    "offset": 0,
+    "easing": "cubic-bezier(0.1, 0.2, 0.3, 0.4)",
+    "style": "opacity: 0;"
+  },
+  {
+    "offset": 1,
+    "easing": "cubic-bezier(0.1, 0.2, 0.3, 0.4)",
+    "style": "opacity: 1;"
+  }
+]
+
+Destroying animations...
+
+PASS: Animation destroyed.
+PASS: Removed animation has expected ID.
+
diff --git a/LayoutTests/inspector/animation/lifecycle-css-transition.html b/LayoutTests/inspector/animation/lifecycle-css-transition.html
new file mode 100644 (file)
index 0000000..cd45a38
--- /dev/null
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="../../http/tests/inspector/resources/inspector-test.js"></script>
+<script src="resources/lifecycle-utilities.js"></script>
+<style>
+div#target {
+    color: red;
+    opacity: 0;
+
+}
+div#target.active {
+    color: blue;
+    opacity: 1;
+
+    transition-property: opacity;
+    transition-duration: 400ms;
+    transition-timing-function: cubic-bezier(0.1, 0.2, 0.3, 0.4);
+    transition-delay: 100ms;
+}
+</style>
+<script>
+function test()
+{
+    let suite = InspectorTest.createAsyncSuite("Animation.Lifecycle");
+
+    suite.addTestCase({
+        name: "Animation.Lifecycle.CSSTransition",
+        description: "Check that Web Inspector is notified whenever CSS transitions are created/destroyed.",
+        async test() {
+            InspectorTest.expectEqual(WI.animationManager.animationCollection.size, 0, "There should not be any animations.");
+
+            let [animation] = await Promise.all([
+                InspectorTest.AnimationLifecycleUtilities.awaitAnimationCreated(WI.Animation.Type.CSSTransition),
+                InspectorTest.evaluateInPage(`document.getElementById("target").classList.add("active")`),
+            ]);
+
+            await Promise.all([
+                InspectorTest.AnimationLifecycleUtilities.awaitAnimationDestroyed(animation.animationId),
+                InspectorTest.AnimationLifecycleUtilities.destroyAnimations(),
+                InspectorTest.evaluateInPage(`document.getElementById("target").classList.remove("active")`),
+            ]);
+        },
+    });
+
+    suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body onload="runTest()">
+    <p>Tests for the Animation.animationCreated and Animation.animationDestroyed events.</p>
+    <div id="target"></div>
+</body>
+</html>
diff --git a/LayoutTests/inspector/animation/lifecycle-web-animation-expected.txt b/LayoutTests/inspector/animation/lifecycle-web-animation-expected.txt
new file mode 100644 (file)
index 0000000..9aa6b8d
--- /dev/null
@@ -0,0 +1,39 @@
+Tests for the Animation.animationCreated and Animation.animationDestroyed events.
+
+
+== Running test suite: Animation.Lifecycle
+-- Running test case: Animation.Lifecycle.WebAnimation
+PASS: There should not be any animations.
+Creating animation...
+
+PASS: Animation created.
+  0: animate - [native code]
+  1: createAnimation - inspector/animation/resources/lifecycle-utilities.js:7:35
+  2: Global Code - [program code]
+  3: evaluateWithScopeExtension - [native code]
+  4: (anonymous function) - [native code]
+  5: _wrapCall - [native code]
+PASS: Animation type should be Web Animation.
+startDelay: 100
+endDelay: 200
+iterationCount: 10
+iterationStart: 5
+iterationDuration: 300
+timingFunction: "ease-in-out"
+playbackDirection: "alternate"
+fillMode: "both"
+keyframes:
+[
+  {
+    "offset": 0.25,
+    "easing": "cubic-bezier(0.1, 0.2, 0.3, 0.4)",
+    "style": "color: red;\nopacity: 0;"
+  },
+  {
+    "offset": 0.75,
+    "easing": "cubic-bezier(0.6, 0.7, 0.8, 0.9)",
+    "style": "color: blue;\nopacity: 1;"
+  }
+]
+
+
diff --git a/LayoutTests/inspector/animation/lifecycle-web-animation.html b/LayoutTests/inspector/animation/lifecycle-web-animation.html
new file mode 100644 (file)
index 0000000..f0270a4
--- /dev/null
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="../../http/tests/inspector/resources/inspector-test.js"></script>
+<script src="resources/lifecycle-utilities.js"></script>
+<script>
+function test()
+{
+    let suite = InspectorTest.createAsyncSuite("Animation.Lifecycle");
+
+    suite.addTestCase({
+        name: "Animation.Lifecycle.WebAnimation",
+        description: "Check that Web Inspector is notified whenever web animations are created/destroyed.",
+        async test() {
+            InspectorTest.expectEqual(WI.animationManager.animationCollection.size, 0, "There should not be any animations.");
+
+            let animation = await InspectorTest.AnimationLifecycleUtilities.createAnimation(WI.Animation.Type.WebAnimation, {
+                selector: "body",
+                keyframes: [
+                    {
+                        offset: 0.25,
+                        easing: "cubic-bezier(0.1, 0.2, 0.3, 0.4)",
+                        color: "red",
+                        opacity: 0,
+                    },
+                    {
+                        offset: 0.75,
+                        easing: "cubic-bezier(0.6, 0.7, 0.8, 0.9)",
+                        color: "blue",
+                        opacity: 1,
+                    },
+                ],
+                options: {
+                    delay: 100,
+                    endDelay: 200,
+                    duration: 300,
+                    easing: "ease-in-out",
+                    direction: "alternate",
+                    fill: "both",
+                    iterations: 10,
+                    iterationStart: 5,
+                },
+            });
+        },
+    });
+
+    suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body onload="runTest()">
+    <p>Tests for the Animation.animationCreated and Animation.animationDestroyed events.</p>
+</body>
+</html>
diff --git a/LayoutTests/inspector/animation/resolveAnimation-expected.txt b/LayoutTests/inspector/animation/resolveAnimation-expected.txt
new file mode 100644 (file)
index 0000000..bbd48da
--- /dev/null
@@ -0,0 +1,12 @@
+Tests for the Animation.resolveAnimation command.
+
+
+== Running test suite: Animation.resolveAnimation
+-- Running test case: Animation.resolveAnimation.ValidIdentifier
+PASS: Payload should have type "object".
+PASS: Payload should have className "Animation".
+
+-- Running test case: Animation.resolveAnimation.InvalidIdentifier
+PASS: Should produce an exception.
+Error: Missing animation for given animationId
+
diff --git a/LayoutTests/inspector/animation/resolveAnimation.html b/LayoutTests/inspector/animation/resolveAnimation.html
new file mode 100644 (file)
index 0000000..fa23e39
--- /dev/null
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="../../http/tests/inspector/resources/inspector-test.js"></script>
+<script>
+function load() {
+    window.animation = document.body.animate([]);
+
+    runTest();
+}
+
+function test()
+{
+    let suite = InspectorTest.createAsyncSuite("Animation.resolveAnimation");
+
+    suite.addTestCase({
+        name: "Animation.resolveAnimation.ValidIdentifier",
+        description: "Should return a valid object for the given animation identifier.",
+        async test() {
+            let animations = Array.from(WI.animationManager.animationCollection);
+            InspectorTest.assert(animations.length === 1, "There should only be one animation.");
+
+            let animation = animations[0];
+            if (!animation) {
+                throw `Missing animation.`;
+                return;
+            }
+
+            InspectorTest.assert(animation.animationType === WI.Animation.Type.WebAnimation, "Animation should be a web animation.");
+
+            const objectGroup = "test";
+            let {object} = await AnimationAgent.resolveAnimation(animation.animationId, objectGroup);
+            InspectorTest.expectEqual(object.type, "object", `Payload should have type "object".`);
+            InspectorTest.expectEqual(object.className, "Animation", `Payload should have className "Animation".`);
+        },
+    });
+
+    // ------
+
+    suite.addTestCase({
+        name: "Animation.resolveAnimation.InvalidIdentifier",
+        description: "Invalid animation identifiers should cause an error.",
+        async test() {
+            const identifier = "DOES_NOT_EXIST";
+            const objectGroup = "test";
+
+            await InspectorTest.expectException(async () => {
+                await AnimationAgent.resolveAnimation(identifier, objectGroup);
+            });
+        },
+    });
+
+    suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body onload="load()">
+    <p>Tests for the Animation.resolveAnimation command.</p>
+</body>
+</html>
diff --git a/LayoutTests/inspector/animation/resources/lifecycle-utilities.js b/LayoutTests/inspector/animation/resources/lifecycle-utilities.js
new file mode 100644 (file)
index 0000000..d9c212f
--- /dev/null
@@ -0,0 +1,130 @@
+window.animations = [];
+
+function createAnimation(selector, keyframes = [], options = {}) {
+    let animation = null;
+
+    let target = document.querySelector(selector);
+    if (target)
+        animation = target.animate(keyframes, options);
+    else
+        animation = new Animation(new KeyframeEffect(null, keyframes, options));
+
+    window.animations.push(animation);
+}
+
+function destroyAnimations() {
+    for (let animation of window.animations) {
+        if (!animation)
+            return;
+
+        animation.cancel();
+
+        if (animation.effect) {
+            if (animation.effect.target) {
+                animation.effect.target.remove();
+                animation.effect.target = null;
+            }
+
+            animation.effect = null;
+        }
+    }
+
+    window.animations = [];
+
+    // Force GC to make sure the animation and it's target are both destroyed, as otherwise the
+    // frontend will not receive Animation.animationDestroyed events.
+    setTimeout(() => {
+        GCController.collect();
+    });
+}
+
+TestPage.registerInitializer(() => {
+    function jsonKeyframeFilter(key, value) {
+        if (key === "easing" && value instanceof WI.CubicBezier)
+            return value.toString();
+        return value;
+    }
+
+    InspectorTest.AnimationLifecycleUtilities = {};
+
+    InspectorTest.AnimationLifecycleUtilities.awaitAnimationCreated = async function(animationType) {
+        let animationCollectionItemAddedEvent = await WI.animationManager.animationCollection.awaitEvent(WI.Collection.Event.ItemAdded);
+
+        InspectorTest.pass("Animation created.");
+
+        let animation = animationCollectionItemAddedEvent.data.item;
+
+        for (let i = 0; i < animation.backtrace.length; ++i) {
+            let callFrame = animation.backtrace[i];
+            let traceText = `  ${i}: `;
+            traceText += callFrame.functionName || "(anonymous function)";
+
+            if (callFrame.nativeCode)
+                traceText += " - [native code]";
+            else if (callFrame.programCode)
+                traceText += " - [program code]";
+            else if (callFrame.sourceCodeLocation) {
+                let location = callFrame.sourceCodeLocation;
+                traceText += " - " + sanitizeURL(location.sourceCode.url) + `:${location.lineNumber}:${location.columnNumber}`;
+            }
+
+            InspectorTest.log(traceText);
+        }
+
+        InspectorTest.expectEqual(animation.animationType, animationType, `Animation type should be ${WI.Animation.displayNameForAnimationType(animationType)}.`);
+
+        if (animation.startDelay)
+            InspectorTest.log("startDelay: " + JSON.stringify(animation.startDelay));
+        if (animation.endDelay)
+            InspectorTest.log("endDelay: " + JSON.stringify(animation.endDelay));
+        if (animation.iterationCount)
+            InspectorTest.log("iterationCount: " + JSON.stringify(animation.iterationCount));
+        if (animation.iterationStart)
+            InspectorTest.log("iterationStart: " + JSON.stringify(animation.iterationStart));
+        if (animation.iterationDuration)
+            InspectorTest.log("iterationDuration: " + JSON.stringify(animation.iterationDuration));
+        if (animation.timingFunction)
+            InspectorTest.log("timingFunction: " + JSON.stringify(String(animation.timingFunction)));
+        if (animation.playbackDirection)
+            InspectorTest.log("playbackDirection: " + JSON.stringify(animation.playbackDirection));
+        if (animation.fillMode)
+            InspectorTest.log("fillMode: " + JSON.stringify(animation.fillMode));
+        if (animation.keyframes.length) {
+            InspectorTest.log("keyframes:");
+            InspectorTest.json(animation.keyframes, jsonKeyframeFilter);
+        }
+
+        InspectorTest.newline();
+
+        return animation
+    };
+
+    InspectorTest.AnimationLifecycleUtilities.awaitAnimationDestroyed = async function(animationIdentifier) {
+        let animationCollectionItemRemovedEvent = await WI.animationManager.animationCollection.awaitEvent(WI.Collection.Event.ItemRemoved)
+
+        InspectorTest.pass("Animation destroyed.");
+
+        let animation = animationCollectionItemRemovedEvent.data.item;
+        InspectorTest.expectEqual(animation.animationId, animationIdentifier, "Removed animation has expected ID.");
+    };
+
+    InspectorTest.AnimationLifecycleUtilities.createAnimation = async function(animationType, {selector, keyframes, options} = {}) {
+        InspectorTest.log("Creating animation...\n");
+
+        let stringifiedSelector = JSON.stringify(selector || null);
+        let stringifiedKeyframes = JSON.stringify(keyframes || []);
+        let stringifiedOptions = JSON.stringify(options || {});
+
+        let [animation] = await Promise.all([
+            InspectorTest.AnimationLifecycleUtilities.awaitAnimationCreated(animationType),
+            InspectorTest.evaluateInPage(`createAnimation(${stringifiedSelector}, ${stringifiedKeyframes}, ${stringifiedOptions})`)
+        ]);
+
+        return animation;
+    };
+
+    InspectorTest.AnimationLifecycleUtilities.destroyAnimations = async function() {
+        InspectorTest.log("Destroying animations...\n");
+        await InspectorTest.evaluateInPage(`destroyAnimations()`);
+    };
+});
diff --git a/LayoutTests/inspector/animation/targetChanged-expected.txt b/LayoutTests/inspector/animation/targetChanged-expected.txt
new file mode 100644 (file)
index 0000000..988de22
--- /dev/null
@@ -0,0 +1,18 @@
+Tests for the Animation.targetChanged event.
+
+
+== Running test suite: Animation.targetChanged
+-- Running test case: Animation.targetChanged.NewTarget
+PASS: Animation should have a target.
+Changing target...
+
+PASS: Animation should have a target.
+PASS: Animation effect should have changed.
+
+-- Running test case: Animation.targetChanged.NullTarget
+PASS: Animation should have a target.
+Changing target...
+
+PASS: Animation should not have a target.
+PASS: Animation effect should have changed.
+
diff --git a/LayoutTests/inspector/animation/targetChanged.html b/LayoutTests/inspector/animation/targetChanged.html
new file mode 100644 (file)
index 0000000..be705f6
--- /dev/null
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="../../http/tests/inspector/resources/inspector-test.js"></script>
+<script>
+function load() {
+    window.animation = document.body.animate([]);
+
+    runTest();
+}
+
+function test()
+{
+    let suite = InspectorTest.createAsyncSuite("Animation.targetChanged");
+
+    suite.addTestCase({
+        name: "Animation.targetChanged.NewTarget",
+        description: "Should return a valid object for the given animation identifier.",
+        async test() {
+            let animations = Array.from(WI.animationManager.animationCollection);
+            InspectorTest.assert(animations.length === 1, "There should only be one animation.");
+
+            let animation = animations[0];
+            if (!animation) {
+                throw `Missing animation.`;
+                return;
+            }
+
+            InspectorTest.assert(animation.animationType === WI.Animation.Type.WebAnimation, "Animation should be a web animation.");
+
+            let [oldTarget] = await promisify((callback) => { animation.requestEffectTarget(callback); });
+            InspectorTest.expectThat(oldTarget instanceof WI.DOMNode, "Animation should have a target.");
+
+            InspectorTest.log("Changing target...\n");
+            await Promise.all([
+                animation.awaitEvent(WI.Animation.Event.TargetChanged),
+                InspectorTest.evaluateInPage(`window.animation.effect.target = document.createElement("div")`),
+            ]);
+
+            let [newTarget] = await promisify((callback) => { animation.requestEffectTarget(callback); });
+            InspectorTest.expectThat(newTarget instanceof WI.DOMNode, "Animation should have a target.");
+
+            InspectorTest.expectNotEqual(newTarget, oldTarget, "Animation effect should have changed.");
+        },
+    });
+
+    suite.addTestCase({
+        name: "Animation.targetChanged.NullTarget",
+        description: "Should return a valid object for the given animation identifier.",
+        async test() {
+            let animations = Array.from(WI.animationManager.animationCollection);
+            InspectorTest.assert(animations.length === 1, "There should only be one animation.");
+
+            let animation = animations[0];
+            if (!animation) {
+                throw `Missing animation.`;
+                return;
+            }
+
+            InspectorTest.assert(animation.animationType === WI.Animation.Type.WebAnimation, "Animation should be a web animation.");
+
+            let [oldTarget] = await promisify((callback) => { animation.requestEffectTarget(callback); });
+            InspectorTest.expectThat(oldTarget instanceof WI.DOMNode, "Animation should have a target.");
+
+            InspectorTest.log("Changing target...\n");
+            await Promise.all([
+                animation.awaitEvent(WI.Animation.Event.TargetChanged),
+                InspectorTest.evaluateInPage(`window.animation.effect.target = null`),
+            ]);
+
+            let [newTarget] = await promisify((callback) => { animation.requestEffectTarget(callback); });
+            InspectorTest.expectNull(newTarget, "Animation should not have a target.");
+
+            InspectorTest.expectNotEqual(newTarget, oldTarget, "Animation effect should have changed.");
+        },
+    });
+
+    suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body onload="load()">
+    <p>Tests for the Animation.targetChanged event.</p>
+</body>
+</html>
index d4e111f..2d6274b 100644 (file)
@@ -20,9 +20,9 @@ function test() {
             name,
             description: "Check that created canvases have the correct type and attributes sent to the frontend.",
             test(resolve, reject) {
-                WI.canvasManager.awaitEvent(WI.CanvasManager.Event.CanvasAdded)
+                WI.canvasManager.canvasCollection.awaitEvent(WI.Collection.Event.ItemAdded)
                 .then((event) => {
-                    let canvas = event.data.canvas;
+                    let canvas = event.data.item;
                     InspectorTest.log("Added canvas.");
 
                     let contextDisplayName = WI.Canvas.displayNameForContextType(contextType);
index bc181f6..b140f11 100644 (file)
@@ -34,14 +34,14 @@ function test() {
         description: "Ensure that attached GPUCanvasContext aren't tracked as a canvas, instead of the WebGPUDevice.",
         async test() {
             let created = false;
-            let listener = WI.canvasManager.addEventListener(WI.CanvasManager.Event.CanvasAdded, (event) => {
-                InspectorTest.assert(event.target.contextType === WI.Canvas.ContextType.WebGPU);
+            let listener = WI.canvasManager.canvasCollection.addEventListener(WI.Collection.Event.ItemAdded, (event) => {
+                InspectorTest.assert(event.data.item.contextType === WI.Canvas.ContextType.WebGPU);
                 created = true;
             });
 
             await InspectorTest.evaluateInPage(`createAttachedCanvas("gpu")`)
 
-            WI.canvasManager.removeEventListener(WI.CanvasManager.Event.CanvasAdded, listener);
+            WI.canvasManager.canvasCollection.removeEventListener(WI.Collection.Event.ItemAdded, listener);
 
             InspectorTest.expectFalse(created, "Inspector canvas should not be created for attached GPUCanvasContext without connected WebGPUDevice.");
         },
@@ -52,14 +52,14 @@ function test() {
         description: "Ensure that detached GPUCanvasContext aren't tracked as a canvas, instead of the WebGPUDevice.",
         async test() {
             let created = false;
-            let listener = WI.canvasManager.addEventListener(WI.CanvasManager.Event.CanvasAdded, (event) => {
-                InspectorTest.assert(event.target.contextType === WI.Canvas.ContextType.WebGPU);
+            let listener = WI.canvasManager.canvasCollection.addEventListener(WI.Collection.Event.ItemAdded, (event) => {
+                InspectorTest.assert(event.data.item.contextType === WI.Canvas.ContextType.WebGPU);
                 created = true;
             });
 
             await InspectorTest.evaluateInPage(`createDetachedCanvas("gpu")`)
 
-            WI.canvasManager.removeEventListener(WI.CanvasManager.Event.CanvasAdded, listener);
+            WI.canvasManager.canvasCollection.removeEventListener(WI.Collection.Event.ItemAdded, listener);
 
             InspectorTest.expectFalse(created, "Inspector canvas should not be created for detached GPUCanvasContext without connected WebGPUDevice.");
         },
@@ -70,14 +70,14 @@ function test() {
         description: "Ensure that CSS GPUCanvasContext aren't tracked as a canvas, instead of the WebGPUDevice.",
         async test() {
             let created = false;
-            let listener = WI.canvasManager.addEventListener(WI.CanvasManager.Event.CanvasAdded, (event) => {
-                InspectorTest.assert(event.target.contextType === WI.Canvas.ContextType.WebGPU);
+            let listener = WI.canvasManager.canvasCollection.addEventListener(WI.Collection.Event.ItemAdded, (event) => {
+                InspectorTest.assert(event.data.item.contextType === WI.Canvas.ContextType.WebGPU);
                 created = true;
             });
 
             await InspectorTest.evaluateInPage(`createCSSCanvas("gpu", "css-canvas")`)
 
-            WI.canvasManager.removeEventListener(WI.CanvasManager.Event.CanvasAdded, listener);
+            WI.canvasManager.canvasCollection.removeEventListener(WI.Collection.Event.ItemAdded, listener);
 
             InspectorTest.expectFalse(created, "Inspector canvas should not be created for CSS GPUCanvasContext without connected WebGPUDevice.");
         },
index e8fcbfe..937c1f3 100644 (file)
@@ -20,7 +20,7 @@ function test() {
         name: "Canvas.extensions.enable",
         description: "Check that enabling an extension notifies the frontend.",
         test(resolve, reject) {
-            let canvases = WI.canvasManager.canvases.filter((canvas) => canvas.contextType === WI.Canvas.ContextType.WebGL);
+            let canvases = Array.from(WI.canvasManager.canvasCollection).filter((canvas) => canvas.contextType === WI.Canvas.ContextType.WebGL);
             if (!canvases.length) {
                 reject("Missing WebGL canvas.");
                 return;
index 2a49e03..4bfd7a6 100644 (file)
@@ -28,7 +28,7 @@ function test() {
             // performed on that canvas that requires the image buffer.  A blank canvas that was
             // just created will not have a buffer, so its memory cost will be 0/NaN.
 
-            let canvases = WI.canvasManager.canvases.filter((canvas) => canvas.contextType === WI.Canvas.ContextType.Canvas2D);
+            let canvases = Array.from(WI.canvasManager.canvasCollection).filter((canvas) => canvas.contextType === WI.Canvas.ContextType.Canvas2D);
             if (!canvases.length) {
                 reject("Missing 2D canvas.");
                 return;
@@ -45,7 +45,7 @@ function test() {
         name: "Canvas.memory.canvasMemoryChanged",
         description: "Check that memory cost is updated when the backend value changes.",
         test(resolve, reject) {
-            let canvases = WI.canvasManager.canvases.filter((canvas) => canvas.contextType === WI.Canvas.ContextType.Canvas2D);
+            let canvases = Array.from(WI.canvasManager.canvasCollection).filter((canvas) => canvas.contextType === WI.Canvas.ContextType.Canvas2D);
             if (!canvases.length) {
                 reject("Missing 2D canvas.");
                 return;
index 4d3cfe9..be08a20 100644 (file)
@@ -25,7 +25,7 @@ function test() {
         name: "Canvas.ActionParameterNaN",
         description: "Check that NaN is converted into the proper value for serialization.",
         test(resolve, reject) {
-            let canvas = WI.canvasManager.canvases[0];
+            let canvas = Array.from(WI.canvasManager.canvasCollection)[0];
             InspectorTest.assert(canvas, "There should be at least one canvas context.");
 
             canvas.awaitEvent(WI.Canvas.Event.RecordingStopped)
@@ -58,7 +58,7 @@ function test() {
         name: "Canvas.MultipleRecording",
         description: "Check that multiple recordings are able to be started/stopped at the same time.",
         test(resolve, reject) {
-            let canvases = WI.canvasManager.canvases;
+            let canvases = Array.from(WI.canvasManager.canvasCollection);
             InspectorTest.assert(canvases.length === 2, "There should be two canvas contexts.");
 
             canvases[1].awaitEvent(WI.Canvas.Event.RecordingStopped)
@@ -112,7 +112,7 @@ function test() {
         name: "Canvas.NoActions",
         description: "Check that a canvas is still able to be recorded after stopping a recording with no actions.",
         test(resolve, reject) {
-            let canvas = WI.canvasManager.canvases[0];
+            let canvas = Array.from(WI.canvasManager.canvasCollection)[0];
             InspectorTest.assert(canvas, "There should be at least one canvas context.");
 
             let eventCount = 0;
index 23cd048..f74d15e 100644 (file)
@@ -16,7 +16,7 @@ function test() {
         name: "Canvas.requestClientNodes.Initial",
         description: "Check that creating a CSS canvas client node is tracked correctly.",
         test(resolve, reject) {
-            let canvas = WI.canvasManager.canvases[0];
+            let canvas = Array.from(WI.canvasManager.canvasCollection)[0];
             InspectorTest.assert(canvas, "There should be at least one canvas.");
 
             canvas.requestClientNodes((clientNodes) => {
index 5350957..e9cba1d 100644 (file)
@@ -18,7 +18,7 @@ function test() {
         name: "Canvas.requestContent2D.validCanvasId",
         description: "Get the base64 encoded data for the canvas on the page with the given type.",
         test(resolve, reject) {
-            let canvas = WI.canvasManager.canvases.find((canvas) => canvas.contextType === WI.Canvas.ContextType.Canvas2D);
+            let canvas = Array.from(WI.canvasManager.canvasCollection).find((canvas) => canvas.contextType === WI.Canvas.ContextType.Canvas2D);
             if (!canvas) {
                 reject(`Missing Canvas.`);
                 return;
index cd5083c..f5266ef 100644 (file)
@@ -22,7 +22,7 @@ function test() {
         name: "Canvas.requestContentBitmapRenderer.validCanvasId",
         description: "Get the base64 encoded data for the BitmapRenderer canvas on the page.",
         test(resolve, reject) {
-            let canvas = WI.canvasManager.canvases.find((canvas) => canvas.contextType === WI.Canvas.ContextType.BitmapRenderer);
+            let canvas = Array.from(WI.canvasManager.canvasCollection).find((canvas) => canvas.contextType === WI.Canvas.ContextType.BitmapRenderer);
             if (!canvas) {
                 reject(`Missing Canvas.`);
                 return;
index aeb81ef..70c21fe 100644 (file)
@@ -18,7 +18,7 @@ function test() {
         name: "Canvas.requestContentWebGL.validCanvasId",
         description: "Get the base64 encoded data for the WebGL canvas on the page.",
         test(resolve, reject) {
-            let canvas = WI.canvasManager.canvases.find((canvas) => canvas.contextType === WI.Canvas.ContextType.WebGL);
+            let canvas = Array.from(WI.canvasManager.canvasCollection).find((canvas) => canvas.contextType === WI.Canvas.ContextType.WebGL);
             if (!canvas) {
                 reject(`Missing Canvas.`);
                 return;
index 6b03d5e..f7b55f4 100644 (file)
@@ -21,7 +21,7 @@ function test() {
         name: "Canvas.requestContentWebGL2.validCanvasId",
         description: "Get the base64 encoded data for the WebGL2 canvas on the page.",
         test(resolve, reject) {
-            let canvas = WI.canvasManager.canvases.find((canvas) => canvas.contextType === WI.Canvas.ContextType.WebGL2);
+            let canvas = Array.from(WI.canvasManager.canvasCollection).find((canvas) => canvas.contextType === WI.Canvas.ContextType.WebGL2);
             if (!canvas) {
                 reject(`Missing Canvas.`);
                 return;
index e5c6147..dcc4bcc 100644 (file)
@@ -18,7 +18,7 @@ function test() {
         name: "Canvas.requestNode.validCanvasId",
         description: "Get the node id for each canvas on the page.",
         test(resolve, reject) {
-            let canvases = WI.canvasManager.canvases;
+            let canvases = Array.from(WI.canvasManager.canvasCollection);
             let expectedLength = canvases.length;
             InspectorTest.assert(expectedLength === 3, "The page has 3 canvases.");
 
index c41cc6b..fe14117 100644 (file)
@@ -17,7 +17,7 @@ function test()
         name: `Canvas.resolveContext2D.validIdentifier`,
         description: "Should return a valid object for the given canvas identifier.",
         test(resolve, reject) {
-            let canvas = WI.canvasManager.canvases.find((canvas) => canvas.contextType === WI.Canvas.ContextType.Canvas2D);
+            let canvas = Array.from(WI.canvasManager.canvasCollection).find((canvas) => canvas.contextType === WI.Canvas.ContextType.Canvas2D);
             if (!canvas) {
                 reject(`Missing Canvas.`);
                 return;
index 8625292..297d441 100644 (file)
@@ -17,7 +17,7 @@ function test()
         name: `Canvas.resolveContextBitmapRenderer.validIdentifier`,
         description: "Should return a valid object for the given canvas identifier.",
         test(resolve, reject) {
-            let canvas = WI.canvasManager.canvases.find((canvas) => canvas.contextType === WI.Canvas.ContextType.BitmapRenderer);
+            let canvas = Array.from(WI.canvasManager.canvasCollection).find((canvas) => canvas.contextType === WI.Canvas.ContextType.BitmapRenderer);
             if (!canvas) {
                 reject(`Missing Canvas.`);
                 return;
index 95adf12..2a70fa4 100644 (file)
@@ -17,7 +17,7 @@ function test()
         name: `Canvas.resolveContextWebGL.validIdentifier`,
         description: "Should return a valid object for the given canvas identifier.",
         test(resolve, reject) {
-            let canvas = WI.canvasManager.canvases.find((canvas) => canvas.contextType === WI.Canvas.ContextType.WebGL);
+            let canvas = Array.from(WI.canvasManager.canvasCollection).find((canvas) => canvas.contextType === WI.Canvas.ContextType.WebGL);
             if (!canvas) {
                 reject(`Missing Canvas.`);
                 return;
index 1151f91..d8e0831 100644 (file)
@@ -20,7 +20,7 @@ function test()
         name: `Canvas.resolveContextWebGL2.validIdentifier`,
         description: "Should return a valid object for the given canvas identifier.",
         test(resolve, reject) {
-            let canvas = WI.canvasManager.canvases.find((canvas) => canvas.contextType === WI.Canvas.ContextType.WebGL2);
+            let canvas = Array.from(WI.canvasManager.canvasCollection).find((canvas) => canvas.contextType === WI.Canvas.ContextType.WebGL2);
             if (!canvas) {
                 reject(`Missing Canvas.`);
                 return;
index dc00ef1..e925b27 100644 (file)
@@ -22,14 +22,14 @@ function test()
         name: `Canvas.resolveContextWebGPU.validIdentifier`,
         description: "Should return a valid object for the given canvas identifier.",
         async test() {
-            InspectorTest.assert(!WI.canvasManager.canvases.length, "There should be no canvases.");
+            InspectorTest.assert(!WI.canvasManager.canvasCollection.size, "There should be no canvases.");
 
-            let [canvasAddedEvent] = await Promise.all([
-                WI.canvasManager.awaitEvent(WI.CanvasManager.Event.CanvasAdded),
+            let [canvasCollectionItemAddedEvent] = await Promise.all([
+                WI.canvasManager.canvasCollection.awaitEvent(WI.Collection.Event.ItemAdded),
                 InspectorTest.evaluateInPage(`createDevice()`),
             ]);
 
-            let {canvas} = canvasAddedEvent.data;
+            let canvas = canvasCollectionItemAddedEvent.data.item;
             InspectorTest.assert(canvas.contextType, WI.Canvas.ContextType.WebGPU, "Added canvas should be a WebGPU device.");
 
             const objectGroup = "test";
index 3a46045..1591580 100644 (file)
@@ -35,7 +35,7 @@ function destroyCanvases() {
     window.contexts = [];
 
     // Force GC to make sure the canvas element is destroyed, otherwise the frontend
-    // does not receive WI.CanvasManager.Event.CanvasRemoved events.
+    // does not receive Canvas.canvasRemoved events.
     setTimeout(() => { GCController.collect(); }, 0);
 }
 
@@ -43,9 +43,9 @@ TestPage.registerInitializer(() => {
     let suite = null;
 
     function awaitCanvasAdded(contextType) {
-        return WI.canvasManager.awaitEvent(WI.CanvasManager.Event.CanvasAdded)
+        return WI.canvasManager.canvasCollection.awaitEvent(WI.Collection.Event.ItemAdded)
         .then((event) => {
-            let canvas = event.data.canvas;
+            let canvas = event.data.item;
             let contextDisplayName = WI.Canvas.displayNameForContextType(contextType);
             InspectorTest.expectEqual(canvas.contextType, contextType, `Canvas context should be ${contextDisplayName}.`);
 
@@ -73,9 +73,9 @@ TestPage.registerInitializer(() => {
     }
 
     function awaitCanvasRemoved(canvasIdentifier) {
-        return WI.canvasManager.awaitEvent(WI.CanvasManager.Event.CanvasRemoved)
+        return WI.canvasManager.canvasCollection.awaitEvent(WI.Collection.Event.ItemRemoved)
         .then((event) => {
-            let canvas = event.data.canvas;
+            let canvas = event.data.item;
             InspectorTest.expectEqual(canvas.identifier, canvasIdentifier, "Removed canvas has expected ID.");
         });
     }
@@ -89,7 +89,7 @@ TestPage.registerInitializer(() => {
             name: `${suite.name}.NoCanvases`,
             description: "Check that the CanvasManager has no canvases initially.",
             test(resolve, reject) {
-                InspectorTest.expectEqual(WI.canvasManager.canvases.length, 0, "CanvasManager should have no canvases.");
+                InspectorTest.expectEqual(WI.canvasManager.canvasCollection.size, 0, "CanvasManager should have no canvases.");
                 resolve();
             }
         });
index 60146ff..77a0ea3 100644 (file)
@@ -86,7 +86,7 @@ TestPage.registerInitializer(() => {
     }
 
     window.getCanvas = function(type) {
-        let canvases = WI.canvasManager.canvases.filter((canvas) => canvas.contextType === type);
+        let canvases = Array.from(WI.canvasManager.canvasCollection).filter((canvas) => canvas.contextType === type);
         InspectorTest.assert(canvases.length === 1, `There should only be one canvas with type "${type}".`);
         if (canvases.length !== 1)
             return null;
index 5275c44..f43e6f2 100644 (file)
@@ -39,7 +39,7 @@ function deleteProgram() {
 function deleteContext() {
     context = null;
     // Force GC to make sure the canvas element is destroyed, otherwise the frontend
-    // does not receive WI.CanvasManager.Event.CanvasRemoved events.
+    // does not receive Canvas.canvasRemoved events.
     setTimeout(() => { GCController.collect(); }, 0);
 }
 
@@ -47,8 +47,9 @@ TestPage.registerInitializer(() => {
     let suite = null;
 
     function whenProgramAdded(callback) {
-        InspectorTest.assert(WI.canvasManager.canvases.length === 1, "There should only be one canvas.");
-        WI.canvasManager.canvases[0].shaderProgramCollection.singleFireEventListener(WI.Collection.Event.ItemAdded, (event) => {
+        let canvases = Array.from(WI.canvasManager.canvasCollection);
+        InspectorTest.assert(canvases.length === 1, "There should only be one canvas.");
+        canvases[0].shaderProgramCollection.singleFireEventListener(WI.Collection.Event.ItemAdded, (event) => {
             let program = event.data.item;
             InspectorTest.expectThat(program instanceof WI.ShaderProgram, "Added ShaderProgram.");
             InspectorTest.expectThat(program.canvas instanceof WI.Canvas, "ShaderProgram should have a parent Canvas.");
@@ -57,8 +58,9 @@ TestPage.registerInitializer(() => {
     }
 
     function whenProgramRemoved(callback) {
-        InspectorTest.assert(WI.canvasManager.canvases.length === 1, "There should only be one canvas.");
-        WI.canvasManager.canvases[0].shaderProgramCollection.singleFireEventListener(WI.Collection.Event.ItemRemoved, (event) => {
+        let canvases = Array.from(WI.canvasManager.canvasCollection);
+        InspectorTest.assert(canvases.length === 1, "There should only be one canvas.");
+        canvases[0].shaderProgramCollection.singleFireEventListener(WI.Collection.Event.ItemRemoved, (event) => {
             let program = event.data.item;
             InspectorTest.expectThat(program instanceof WI.ShaderProgram, "Removed ShaderProgram.");
             InspectorTest.expectThat(program.canvas instanceof WI.Canvas, "ShaderProgram should have a parent Canvas.");
@@ -74,7 +76,7 @@ TestPage.registerInitializer(() => {
             description: "Check that ShaderProgramAdded is sent for a program created before CanvasAgent is enabled.",
             test(resolve, reject) {
                 // This can't use `awaitEvent` since the promise resolution happens on the next tick.
-                WI.canvasManager.singleFireEventListener(WI.CanvasManager.Event.CanvasAdded, (event) => {
+                WI.canvasManager.canvasCollection.singleFireEventListener(WI.Collection.Event.ItemAdded, (event) => {
                     whenProgramAdded((program) => {
                         resolve();
                     });
@@ -113,7 +115,7 @@ TestPage.registerInitializer(() => {
             test(resolve, reject) {
                 let canvasRemoved = false;
 
-                WI.canvasManager.singleFireEventListener(WI.CanvasManager.Event.CanvasRemoved, (event) => {
+                WI.canvasManager.canvasCollection.singleFireEventListener(WI.Collection.Event.ItemRemoved, (event) => {
                     canvasRemoved = true;
                 });
 
index fe48d8f..b8de34c 100644 (file)
@@ -64,7 +64,7 @@ async function createRenderPipeline(code = renderPipelineSource) {
 function deleteDevice() {
     device = null;
     // Force GC to make sure the device is destroyed, otherwise the frontend
-    // does not receive WI.CanvasManager.Event.CanvasRemoved events.
+    // does not receive Canvas.canvasRemoved events.
     setTimeout(() => { GCController.collect(); }, 0);
 }
 
index d98f0fe..55e5524 100644 (file)
@@ -77,8 +77,8 @@ function test() {
                     handleRecordingStopped(canvas, event.data.recording);
                 }
 
-                WI.canvasManager.singleFireEventListener(WI.CanvasManager.Event.CanvasAdded, (event) => {
-                    canvas = event.data.canvas;
+                WI.canvasManager.canvasCollection.singleFireEventListener(WI.Collection.Event.ItemAdded, (event) => {
+                    canvas = event.data.item;
                     InspectorTest.assert(!canvas.recordingActive)
 
                     canvas.addEventListener(WI.Canvas.Event.RecordingStarted, handleRecordingStartedWrapper);
index d40fab7..d5fb877 100644 (file)
@@ -14,9 +14,11 @@ window.beforeTest = async function() {
 function test() {
     let suite = InspectorTest.createAsyncSuite(`Canvas.ShaderProgram.WebGPU`);
 
-    let canvas = WI.canvasManager.canvases[0];
+    let canvases = Array.from(WI.canvasManager.canvasCollection);
+    InspectorTest.assert(canvases.length === 1, "There should only be one canvas.");
+
+    let canvas = canvases[0];
     InspectorTest.assert(canvas, "There should be a canvas.");
-    InspectorTest.assert(WI.canvasManager.canvases.length === 1, "There should only be one canvas.");
     InspectorTest.assert(canvas.contextType === WI.Canvas.ContextType.WebGPU, "Canvas should be WebGPU.");
 
     async function awaitProgramAdded(programType) {
@@ -118,7 +120,7 @@ function test() {
         test(resolve, reject) {
             let canvasRemoved = false;
 
-            WI.canvasManager.singleFireEventListener(WI.CanvasManager.Event.CanvasRemoved, (event) => {
+            WI.canvasManager.canvasCollection.singleFireEventListener(WI.Collection.Event.ItemRemoved, (event) => {
                 canvasRemoved = true;
             });
 
index 34d02ab..8cf9a47 100644 (file)
@@ -57,10 +57,10 @@ function test() {
         InspectorTest.log("Creating render pipeline...");
         let evalutePromise = InspectorTest.evaluateInPage(`createRenderPipeline()`);
 
-        if (!WI.canvasManager.canvases.length)
-            await WI.canvasManager.awaitEvent(WI.CanvasManager.Event.CanvasAdded);
+        if (!WI.canvasManager.canvasCollection.size)
+            await WI.canvasManager.canvasCollection.awaitEvent(WI.Collection.Event.ItemAdded);
 
-        let canvas = WI.canvasManager.canvases[0];
+        let canvas = Array.from(WI.canvasManager.canvasCollection)[0];
         InspectorTest.assert(canvas.contextType === WI.Canvas.ContextType.WebGPU, "Canvas should be WebGPU.");
 
         let itemAddedEvent = await canvas.shaderProgramCollection.awaitEvent(WI.Collection.Event.ItemAdded);
index 66bdbab..dbf6351 100644 (file)
@@ -54,6 +54,74 @@ FAIL: expectFalse([])
     Expected: falsey
     Actual: []
 
+-- Running test case: InspectorTest.expectEmpty
+Expected to PASS
+PASS: expectEmpty(null)
+PASS: expectEmpty("")
+PASS: expectEmpty({})
+PASS: expectEmpty([])
+PASS: expectEmpty({})
+PASS: expectEmpty({})
+Expected to FAIL
+FAIL: expectEmpty("test")
+    Expected: empty
+    Actual: "test"
+FAIL: expectEmpty({"test":1})
+    Expected: empty
+    Actual: {"test":1}
+FAIL: expectEmpty("test")
+    Expected: empty
+    Actual: "test"
+FAIL: expectEmpty({})
+    Expected: empty
+    Actual: {}
+FAIL: expectEmpty({})
+    Expected: empty
+    Actual: {}
+FAIL: expectEmpty should not be called with a non-object:
+    Actual: true
+FAIL: expectEmpty should not be called with a non-object:
+    Actual: false
+FAIL: expectEmpty should not be called with a non-object:
+    Actual: 1
+FAIL: expectEmpty should not be called with a non-object:
+    Actual: undefined
+
+-- Running test case: InspectorTest.expectNotEmpty
+Expected to PASS
+PASS: expectNotEmpty("test")
+PASS: expectNotEmpty({"test":1})
+PASS: expectNotEmpty("test")
+PASS: expectNotEmpty({})
+PASS: expectNotEmpty({})
+Expected to FAIL
+FAIL: expectNotEmpty(null)
+    Expected: not empty
+    Actual: null
+FAIL: expectNotEmpty("")
+    Expected: not empty
+    Actual: ""
+FAIL: expectNotEmpty({})
+    Expected: not empty
+    Actual: {}
+FAIL: expectNotEmpty([])
+    Expected: not empty
+    Actual: []
+FAIL: expectNotEmpty({})
+    Expected: not empty
+    Actual: {}
+FAIL: expectNotEmpty({})
+    Expected: not empty
+    Actual: {}
+FAIL: expectNotEmpty should not be called with a non-object:
+    Actual: true
+FAIL: expectNotEmpty should not be called with a non-object:
+    Actual: false
+FAIL: expectNotEmpty should not be called with a non-object:
+    Actual: 1
+FAIL: expectNotEmpty should not be called with a non-object:
+    Actual: undefined
+
 -- Running test case: InspectorTest.expectNull
 Expected to PASS
 PASS: expectNull(null)
index 91e8580..e3864a0 100644 (file)
@@ -48,6 +48,20 @@ function test()
     addTestCase(expectThatTestCase);
     addInverseTestCase("expectFalse", expectThatTestCase);
 
+    let expectEmptyTestCase = {
+        functionName: "expectEmpty",
+        passingInputs: [null, "", {}, [], new Map, new Set],
+        failingInputs: ["test", {test: 1}, ["test"], new Map([["test", 1]]), new Set(["test"]), true, false, 1, undefined],
+    };
+    addTestCase(expectEmptyTestCase);
+
+    let expectNotEmptyTestCase = {
+        functionName: "expectNotEmpty",
+        passingInputs: ["test", {test: 1}, ["test"], new Map([["test", 1]]), new Set(["test"])],
+        failingInputs: [null, "", {}, [], new Map, new Set, true, false, 1, undefined],
+    };
+    addTestCase(expectNotEmptyTestCase);
+
     let expectNullTestCase = {
         functionName: "expectNull",
         passingInputs: [null],
index d37e0ab..0142ea9 100644 (file)
@@ -1,3 +1,15 @@
+2020-01-29  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: add instrumentation for showing existing Web Animations
+        https://bugs.webkit.org/show_bug.cgi?id=205434
+        <rdar://problem/28328087>
+
+        Reviewed by Brian Burg.
+
+        * inspector/protocol/Animation.json:
+        Add types/commands/events for instrumenting the lifecycle of `Animation` objects, as well as
+        commands for getting the JavaScript wrapper object and the target DOM node.
+
 2020-01-29  Robin Morisset  <rmorisset@apple.com>
 
         Don't include CCallHelpers.h in B3Procedure.h
index beaf8de..22ee882 100644 (file)
             "enum": ["ready", "delayed", "active", "canceled", "done"]
         },
         {
+            "id": "PlaybackDirection",
+            "type": "string",
+            "enum": ["normal", "reverse", "alternate", "alternate-reverse"]
+        },
+        {
+            "id": "FillMode",
+            "type": "string",
+            "enum": ["none", "forwards", "backwards", "both", "auto"]
+        },
+        {
+            "id": "Animation",
+            "type": "object",
+            "properties": [
+                { "name": "animationId", "$ref": "AnimationId" },
+                { "name": "cssAnimationName", "type": "string", "optional": true, "description": "Equal to the corresponding `animation-name` CSS property. Should not be provided if `transitionProperty` is also provided." },
+                { "name": "cssTransitionProperty", "type": "string", "optional": true, "description": "Equal to the corresponding `transition-property` CSS property. Should not be provided if `animationName` is also provided." },
+                { "name": "effect", "$ref": "Effect", "optional": true },
+                { "name": "backtrace", "type": "array", "items": { "$ref": "Console.CallFrame" }, "optional": true, "description": "Backtrace that was captured when this `WebAnimation` was created." }
+            ]
+        },
+        {
+            "id": "Effect",
+            "type": "object",
+            "properties": [
+                { "name": "startDelay", "type": "number", "optional": true },
+                { "name": "endDelay", "type": "number", "optional": true },
+                { "name": "iterationCount", "type": "number", "optional": true, "description": "Number of iterations in the animation." },
+                { "name": "iterationStart", "type": "number", "optional": true, "description": "Index of which iteration to start at." },
+                { "name": "iterationDuration", "type": "number", "optional": true, "description": "Total time of each iteration, measured in milliseconds." },
+                { "name": "timingFunction", "type": "string", "optional": true, "description": "CSS timing function of the overall animation." },
+                { "name": "playbackDirection", "$ref": "PlaybackDirection", "optional": true },
+                { "name": "fillMode", "$ref": "FillMode", "optional": true },
+                { "name": "keyframes", "type": "array", "items": { "$ref": "Keyframe" }, "optional": true }
+            ]
+        },
+        {
+            "id": "Keyframe",
+            "type": "object",
+            "properties": [
+                { "name": "offset", "type": "number", "description": "Decimal percentage [0,1] representing where this keyframe is in the entire duration of the animation." },
+                { "name": "easing", "type": "string", "optional": true, "description": "CSS timing function for how the `style` is applied." },
+                { "name": "style", "type": "string", "optional": true, "description": "CSS style declaration of the CSS properties that will be animated." }
+            ]
+        },
+        {
             "id": "TrackingUpdate",
             "type": "object",
             "properties": [
     ],
     "commands": [
         {
+            "name": "enable",
+            "description": "Enables Canvas domain events."
+        },
+        {
+            "name": "disable",
+            "description": "Disables Canvas domain events."
+        },
+        {
+            "name": "requestEffectTarget",
+            "description": "Gets the `DOM.NodeId` for the target of the effect of the animation with the given `AnimationId`.",
+            "parameters": [
+                { "name": "animationId", "$ref": "AnimationId" }
+            ],
+            "returns": [
+                { "name": "nodeId", "$ref": "DOM.NodeId" }
+            ]
+        },
+        {
+            "name": "resolveAnimation",
+            "description": "Resolves JavaScript `WebAnimation` object for given `AnimationId`.",
+            "parameters": [
+                { "name": "animationId", "$ref": "AnimationId" },
+                { "name": "objectGroup", "type": "string", "optional": true, "description": "Symbolic group name that can be used to release multiple objects." }
+            ],
+            "returns": [
+                { "name": "object", "$ref": "Runtime.RemoteObject" }
+            ]
+        },
+        {
             "name": "startTracking",
             "description": "Start tracking animations. This will produce a `trackingStart` event."
         },
     ],
     "events": [
         {
+            "name": "animationCreated",
+            "description": "Dispatched whenever a `WebAnimation` is created.",
+            "parameters": [
+                { "name": "animation", "$ref": "Animation" }
+            ]
+        },
+        {
+            "name": "effectChanged",
+            "description": "Dispatched whenever the effect of any animation is changed in any way.",
+            "parameters": [
+                { "name": "animationId", "$ref": "AnimationId" },
+                { "name": "effect", "$ref": "Effect", "optional": true, "description": "This is omitted when the effect is removed without a replacement." }
+            ]
+        },
+        {
+            "name": "targetChanged",
+            "description": "Dispatched whenever the target of any effect of any animation is changed in any way.",
+            "parameters": [
+                { "name": "animationId", "$ref": "AnimationId" }
+            ]
+        },
+        {
+            "name": "animationDestroyed",
+            "description": "Dispatched whenever a `WebAnimation` is destroyed.",
+            "parameters": [
+                { "name": "animationId", "$ref": "AnimationId" }
+            ]
+        },
+        {
             "name": "trackingStart",
             "description": "Dispatched after `startTracking` command.",
             "parameters": [
index 7ac4299..88b8633 100644 (file)
@@ -1,3 +1,103 @@
+2020-01-29  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: add instrumentation for showing existing Web Animations
+        https://bugs.webkit.org/show_bug.cgi?id=205434
+        <rdar://problem/28328087>
+
+        Reviewed by Brian Burg.
+
+        Add types/commands/events for instrumenting the lifecycle of `Animation` objects, as well as
+        commands for getting the JavaScript wrapper object and the target DOM node.
+
+        Tests: inspector/animation/effectChanged.html
+               inspector/animation/lifecycle-css-animation.html
+               inspector/animation/lifecycle-css-transition.html
+               inspector/animation/lifecycle-web-animation.html
+               inspector/animation/requestEffectTarget.html
+               inspector/animation/resolveAnimation.html
+               inspector/animation/targetChanged.html
+
+        * animation/WebAnimation.h:
+        * animation/WebAnimation.cpp:
+        (WebCore::WebAnimation::instances): Added.
+        (WebCore::WebAnimation::instancesMutex): Added.
+        (WebCore::WebAnimation::create):
+        (WebCore::WebAnimation::WebAnimation):
+        (WebCore::WebAnimation::~WebAnimation):
+        (WebCore::WebAnimation::effectTimingDidChange):
+        (WebCore::WebAnimation::setEffectInternal):
+        (WebCore::WebAnimation::effectTargetDidChange):
+        * animation/CSSAnimation.cpp:
+        (WebCore::CSSAnimation::create):
+        * animation/CSSTransition.cpp:
+        (WebCore::CSSTransition::create):
+
+        * animation/KeyframeEffect.h:
+        (WebCore::KeyframeEffect::parsedKeyframes const): Added.
+        (WebCore::KeyframeEffect::blendingKeyframes const): Added.
+        (WebCore::KeyframeEffect::hasBlendingKeyframes const): Deleted.
+        Provide a way to access the list of keyframes.
+
+        * inspector/InspectorInstrumentation.h:
+        (WebCore::InspectorInstrumentation::didSetWebAnimationEffect): Added.
+        (WebCore::InspectorInstrumentation::didChangeWebAnimationEffectTiming): Added.
+        (WebCore::InspectorInstrumentation::didChangeWebAnimationEffectTarget): Added.
+        (WebCore::InspectorInstrumentation::didCreateWebAnimation): Added.
+        (WebCore::InspectorInstrumentation::didChangeWebAnimationEffect): Deleted.
+        * inspector/InspectorInstrumentation.cpp:
+        (WebCore::InspectorInstrumentation::didCommitLoadImpl):
+        (WebCore::InspectorInstrumentation::didSetWebAnimationEffectImpl): Added.
+        (WebCore::InspectorInstrumentation::didChangeWebAnimationEffectTimingImpl): Added.
+        (WebCore::InspectorInstrumentation::didChangeWebAnimationEffectTargetImpl): Added.
+        (WebCore::InspectorInstrumentation::didCreateWebAnimationImpl): Added.
+        (WebCore::InspectorInstrumentation::willDestroyWebAnimationImpl):
+        (WebCore::InspectorInstrumentation::didChangeWebAnimationEffectImpl): Deleted.
+
+        * inspector/InstrumentingAgents.h:
+        (WebCore::InstrumentingAgents::enabledInspectorAnimationAgent const): Added.
+        (WebCore::InstrumentingAgents::setEnabledInspectorAnimationAgent): Added.
+        * inspector/InstrumentingAgents.cpp:
+        (WebCore::InstrumentingAgents::reset):
+
+        * inspector/agents/InspectorAnimationAgent.h:
+        * inspector/agents/InspectorAnimationAgent.cpp:
+        (WebCore::protocolValueForSeconds): Added.
+        (WebCore::protocolValueForPlaybackDirection): Added.
+        (WebCore::protocolValueForFillMode): Added.
+        (WebCore::buildObjectForKeyframes): Added.
+        (WebCore::buildObjectForEffect): Added.
+        (WebCore::InspectorAnimationAgent::InspectorAnimationAgent):
+        (WebCore::InspectorAnimationAgent::willDestroyFrontendAndBackend):
+        (WebCore::InspectorAnimationAgent::enable): Added.
+        (WebCore::InspectorAnimationAgent::disable): Added.
+        (WebCore::InspectorAnimationAgent::requestEffectTarget): Added.
+        (WebCore::InspectorAnimationAgent::resolveAnimation): Added.
+        (WebCore::InspectorAnimationAgent::didSetWebAnimationEffect): Added.
+        (WebCore::InspectorAnimationAgent::didChangeWebAnimationEffectTiming): Added.
+        (WebCore::InspectorAnimationAgent::didChangeWebAnimationEffectTarget): Added.
+        (WebCore::InspectorAnimationAgent::didCreateWebAnimation): Added.
+        (WebCore::InspectorAnimationAgent::willDestroyWebAnimation):
+        (WebCore::InspectorAnimationAgent::frameNavigated):
+        (WebCore::InspectorAnimationAgent::findAnimationId): Added.
+        (WebCore::InspectorAnimationAgent::assertAnimation): Added.
+        (WebCore::InspectorAnimationAgent::bindAnimation): Added.
+        (WebCore::InspectorAnimationAgent::unbindAnimation): Added.
+        (WebCore::InspectorAnimationAgent::animationDestroyedTimerFired): Added.
+        (WebCore::InspectorAnimationAgent::reset): Added.
+        (WebCore::InspectorAnimationAgent::didChangeWebAnimationEffect): Deleted.
+
+        * inspector/agents/InspectorDOMAgent.h:
+        * inspector/agents/InspectorDOMAgent.cpp:
+        (WebCore::InspectorDOMAgent::pushNodeToFrontend):
+        (WebCore::InspectorDOMAgent::querySelector):
+        (WebCore::InspectorDOMAgent::pushNodePathToFrontend):
+        (WebCore::InspectorDOMAgent::setNodeName):
+        (WebCore::InspectorDOMAgent::setOuterHTML):
+        (WebCore::InspectorDOMAgent::moveTo):
+        (WebCore::InspectorDOMAgent::requestNode):
+        (WebCore::InspectorDOMAgent::pushNodeByPathToFrontend):
+        Add an overload for `pushNodePathToFrontend` that exposes an `ErrorString`.
+
 2020-01-29  Ross Kirsling  <ross.kirsling@sony.com>
 
         [PlayStation] Fix MIMETypeRegistry
index 7c72f62..fd3eb9d 100644 (file)
@@ -28,6 +28,7 @@
 
 #include "Animation.h"
 #include "Element.h"
+#include "InspectorInstrumentation.h"
 #include "RenderStyle.h"
 #include <wtf/IsoMallocInlines.h>
 
@@ -39,6 +40,9 @@ Ref<CSSAnimation> CSSAnimation::create(Element& owningElement, const Animation&
 {
     auto result = adoptRef(*new CSSAnimation(owningElement, backingAnimation, newStyle));
     result->initialize(oldStyle, newStyle);
+
+    InspectorInstrumentation::didCreateWebAnimation(result.get());
+
     return result;
 }
 
index dff5fb1..aaae5e2 100644 (file)
@@ -28,6 +28,7 @@
 
 #include "Animation.h"
 #include "Element.h"
+#include "InspectorInstrumentation.h"
 #include "KeyframeEffect.h"
 #include <wtf/IsoMallocInlines.h>
 
@@ -40,6 +41,9 @@ Ref<CSSTransition> CSSTransition::create(Element& owningElement, CSSPropertyID p
     auto result = adoptRef(*new CSSTransition(owningElement, property, generationTime, backingAnimation, newStyle, reversingAdjustedStartStyle, reversingShorteningFactor));
     result->initialize(oldStyle, newStyle);
     result->setTimingProperties(delay, duration);
+
+    InspectorInstrumentation::didCreateWebAnimation(result.get());
+
     return result;
 }
 
index 72d9c61..65e40aa 100644 (file)
@@ -99,6 +99,8 @@ public:
         CompositeOperationOrAuto composite { CompositeOperationOrAuto::Auto };
     };
 
+    const Vector<ParsedKeyframe>& parsedKeyframes() const { return m_parsedKeyframes; }
+
     Element* target() const { return m_target.get(); }
     void setTarget(RefPtr<Element>&&);
 
@@ -136,7 +138,7 @@ public:
     bool colorFilterFunctionListsMatch() const override { return m_colorFilterFunctionListsMatch; }
 
     void computeDeclarativeAnimationBlendingKeyframes(const RenderStyle* oldStyle, const RenderStyle& newStyle);
-    bool hasBlendingKeyframes() const { return m_blendingKeyframes.size(); }
+    const KeyframeList& blendingKeyframes() const { return m_blendingKeyframes; }
     const HashSet<CSSPropertyID>& animatedProperties() const { return m_blendingKeyframes.properties(); }
 
     bool computeExtentOfTransformAnimation(LayoutRect&) const;
index 81b1cb5..a8b48c2 100644 (file)
@@ -45,6 +45,8 @@
 #include "StyledElement.h"
 #include "WebAnimationUtilities.h"
 #include <wtf/IsoMallocInlines.h>
+#include <wtf/Lock.h>
+#include <wtf/NeverDestroyed.h>
 #include <wtf/Optional.h>
 #include <wtf/text/TextStream.h>
 #include <wtf/text/WTFString.h>
@@ -53,11 +55,30 @@ namespace WebCore {
 
 WTF_MAKE_ISO_ALLOCATED_IMPL(WebAnimation);
 
+HashSet<WebAnimation*>& WebAnimation::instances(const LockHolder&)
+{
+    static NeverDestroyed<HashSet<WebAnimation*>> instances;
+    return instances;
+}
+
+Lock& WebAnimation::instancesMutex()
+{
+    static LazyNeverDestroyed<Lock> mutex;
+    static std::once_flag initializeMutex;
+    std::call_once(initializeMutex, [] {
+        mutex.construct();
+    });
+    return mutex.get();
+}
+
 Ref<WebAnimation> WebAnimation::create(Document& document, AnimationEffect* effect)
 {
     auto result = adoptRef(*new WebAnimation(document));
     result->setEffect(effect);
     result->setTimeline(&document.timeline());
+
+    InspectorInstrumentation::didCreateWebAnimation(result.get());
+
     return result;
 }
 
@@ -67,6 +88,9 @@ Ref<WebAnimation> WebAnimation::create(Document& document, AnimationEffect* effe
     result->setEffect(effect);
     if (timeline)
         result->setTimeline(timeline);
+
+    InspectorInstrumentation::didCreateWebAnimation(result.get());
+
     return result;
 }
 
@@ -77,6 +101,9 @@ WebAnimation::WebAnimation(Document& document)
 {
     m_readyPromise->resolve(*this);
     suspendIfNeeded();
+
+    LockHolder lock(instancesMutex());
+    instances(lock).add(this);
 }
 
 WebAnimation::~WebAnimation()
@@ -85,6 +112,10 @@ WebAnimation::~WebAnimation()
 
     if (m_timeline)
         m_timeline->forgetAnimation(this);
+
+    LockHolder lock(instancesMutex());
+    ASSERT(instances(lock).contains(this));
+    instances(lock).remove(this);
 }
 
 void WebAnimation::contextDestroyed()
@@ -116,6 +147,8 @@ void WebAnimation::unsuspendEffectInvalidation()
 void WebAnimation::effectTimingDidChange()
 {
     timingDidChange(DidSeek::No, SynchronouslyNotify::Yes);
+
+    InspectorInstrumentation::didChangeWebAnimationEffectTiming(*this);
 }
 
 void WebAnimation::setEffect(RefPtr<AnimationEffect>&& newEffect)
@@ -190,7 +223,7 @@ void WebAnimation::setEffectInternal(RefPtr<AnimationEffect>&& newEffect, bool d
             m_timeline->animationWasAddedToElement(*this, *newTarget);
     }
 
-    InspectorInstrumentation::didChangeWebAnimationEffect(*this);
+    InspectorInstrumentation::didSetWebAnimationEffect(*this);
 }
 
 void WebAnimation::setTimeline(RefPtr<AnimationTimeline>&& timeline)
@@ -249,17 +282,18 @@ void WebAnimation::setTimelineInternal(RefPtr<AnimationTimeline>&& timeline)
 
 void WebAnimation::effectTargetDidChange(Element* previousTarget, Element* newTarget)
 {
-    if (!m_timeline)
-        return;
+    if (m_timeline) {
+        if (previousTarget)
+            m_timeline->animationWasRemovedFromElement(*this, *previousTarget);
 
-    if (previousTarget)
-        m_timeline->animationWasRemovedFromElement(*this, *previousTarget);
+        if (newTarget)
+            m_timeline->animationWasAddedToElement(*this, *newTarget);
 
-    if (newTarget)
-        m_timeline->animationWasAddedToElement(*this, *newTarget);
+        // This could have changed whether we have replaced animations, so we may need to schedule an update.
+        m_timeline->animationTimingDidChange(*this);
+    }
 
-    // This could have changed whether we have replaced animations, so we may need to schedule an update.
-    m_timeline->animationTimingDidChange(*this);
+    InspectorInstrumentation::didChangeWebAnimationEffectTarget(*this);
 }
 
 Optional<double> WebAnimation::startTime() const
index 03e40ec..0e037fc 100644 (file)
@@ -30,6 +30,7 @@
 #include "ExceptionOr.h"
 #include "IDLTypes.h"
 #include "WebAnimationUtilities.h"
+#include <wtf/Forward.h>
 #include <wtf/Markable.h>
 #include <wtf/RefCounted.h>
 #include <wtf/Seconds.h>
@@ -54,6 +55,9 @@ public:
     static Ref<WebAnimation> create(Document&, AnimationEffect*, AnimationTimeline*);
     ~WebAnimation();
 
+    static HashSet<WebAnimation*>& instances(const LockHolder&);
+    static Lock& instancesMutex();
+
     virtual bool isDeclarativeAnimation() const { return false; }
     virtual bool isCSSAnimation() const { return false; }
     virtual bool isCSSTransition() const { return false; }
index d0c430e..3dc7b49 100644 (file)
@@ -720,6 +720,9 @@ void InspectorInstrumentation::didCommitLoadImpl(InstrumentingAgents& instrument
     if (InspectorCanvasAgent* canvasAgent = instrumentingAgents.inspectorCanvasAgent())
         canvasAgent->frameNavigated(frame);
 
+    if (auto* animationAgent = instrumentingAgents.enabledInspectorAnimationAgent())
+        animationAgent->frameNavigated(frame);
+
     if (InspectorDOMAgent* domAgent = instrumentingAgents.inspectorDOMAgent())
         domAgent->didCommitLoad(frame.document());
 
@@ -1130,15 +1133,37 @@ void InspectorInstrumentation::willApplyKeyframeEffectImpl(InstrumentingAgents&
         animationAgent->willApplyKeyframeEffect(target, effect, computedTiming);
 }
 
-void InspectorInstrumentation::didChangeWebAnimationEffectImpl(InstrumentingAgents& instrumentingAgents, WebAnimation& animation)
+void InspectorInstrumentation::didSetWebAnimationEffectImpl(InstrumentingAgents& instrumentingAgents, WebAnimation& animation)
 {
-    if (auto* animationAgent = instrumentingAgents.trackingInspectorAnimationAgent())
-        animationAgent->didChangeWebAnimationEffect(animation);
+    if (auto* animationAgent = instrumentingAgents.enabledInspectorAnimationAgent())
+        animationAgent->didSetWebAnimationEffect(animation);
+    else if (auto* animationAgent = instrumentingAgents.trackingInspectorAnimationAgent())
+        animationAgent->didSetWebAnimationEffect(animation);
+}
+
+void InspectorInstrumentation::didChangeWebAnimationEffectTimingImpl(InstrumentingAgents& instrumentingAgents, WebAnimation& animation)
+{
+    if (auto* animationAgent = instrumentingAgents.enabledInspectorAnimationAgent())
+        animationAgent->didChangeWebAnimationEffectTiming(animation);
+}
+
+void InspectorInstrumentation::didChangeWebAnimationEffectTargetImpl(InstrumentingAgents& instrumentingAgents, WebAnimation& animation)
+{
+    if (auto* animationAgent = instrumentingAgents.enabledInspectorAnimationAgent())
+        animationAgent->didChangeWebAnimationEffectTarget(animation);
+}
+
+void InspectorInstrumentation::didCreateWebAnimationImpl(InstrumentingAgents& instrumentingAgents, WebAnimation& animation)
+{
+    if (auto* animationAgent = instrumentingAgents.enabledInspectorAnimationAgent())
+        animationAgent->didCreateWebAnimation(animation);
 }
 
 void InspectorInstrumentation::willDestroyWebAnimationImpl(InstrumentingAgents& instrumentingAgents, WebAnimation& animation)
 {
-    if (auto* animationAgent = instrumentingAgents.trackingInspectorAnimationAgent())
+    if (auto* animationAgent = instrumentingAgents.enabledInspectorAnimationAgent())
+        animationAgent->willDestroyWebAnimation(animation);
+    else if (auto* animationAgent = instrumentingAgents.trackingInspectorAnimationAgent())
         animationAgent->willDestroyWebAnimation(animation);
 }
 
index e781836..b246eaa 100644 (file)
@@ -305,7 +305,10 @@ public:
 #endif
 
     static void willApplyKeyframeEffect(Element&, KeyframeEffect&, ComputedEffectTiming);
-    static void didChangeWebAnimationEffect(WebAnimation&);
+    static void didSetWebAnimationEffect(WebAnimation&);
+    static void didChangeWebAnimationEffectTiming(WebAnimation&);
+    static void didChangeWebAnimationEffectTarget(WebAnimation&);
+    static void didCreateWebAnimation(WebAnimation&);
     static void willDestroyWebAnimation(WebAnimation&);
 
     static void networkStateChanged(Page&);
@@ -505,7 +508,10 @@ private:
 #endif
 
     static void willApplyKeyframeEffectImpl(InstrumentingAgents&, Element&, KeyframeEffect&, ComputedEffectTiming);
-    static void didChangeWebAnimationEffectImpl(InstrumentingAgents&, WebAnimation&);
+    static void didSetWebAnimationEffectImpl(InstrumentingAgents&, WebAnimation&);
+    static void didChangeWebAnimationEffectTimingImpl(InstrumentingAgents&, WebAnimation&);
+    static void didChangeWebAnimationEffectTargetImpl(InstrumentingAgents&, WebAnimation&);
+    static void didCreateWebAnimationImpl(InstrumentingAgents&, WebAnimation&);
     static void willDestroyWebAnimationImpl(InstrumentingAgents&, WebAnimation&);
 
     static void layerTreeDidChangeImpl(InstrumentingAgents&);
@@ -1466,11 +1472,32 @@ inline void InspectorInstrumentation::willApplyKeyframeEffect(Element& target, K
         willApplyKeyframeEffectImpl(*instrumentingAgents, target, effect, computedTiming);
 }
 
-inline void InspectorInstrumentation::didChangeWebAnimationEffect(WebAnimation& animation)
+inline void InspectorInstrumentation::didSetWebAnimationEffect(WebAnimation& animation)
 {
     FAST_RETURN_IF_NO_FRONTENDS(void());
     if (auto* instrumentingAgents = instrumentingAgentsForContext(animation.scriptExecutionContext()))
-        didChangeWebAnimationEffectImpl(*instrumentingAgents, animation);
+        didSetWebAnimationEffectImpl(*instrumentingAgents, animation);
+}
+
+inline void InspectorInstrumentation::didChangeWebAnimationEffectTiming(WebAnimation& animation)
+{
+    FAST_RETURN_IF_NO_FRONTENDS(void());
+    if (auto* instrumentingAgents = instrumentingAgentsForContext(animation.scriptExecutionContext()))
+        didChangeWebAnimationEffectTimingImpl(*instrumentingAgents, animation);
+}
+
+inline void InspectorInstrumentation::didChangeWebAnimationEffectTarget(WebAnimation& animation)
+{
+    FAST_RETURN_IF_NO_FRONTENDS(void());
+    if (auto* instrumentingAgents = instrumentingAgentsForContext(animation.scriptExecutionContext()))
+        didChangeWebAnimationEffectTargetImpl(*instrumentingAgents, animation);
+}
+
+inline void InspectorInstrumentation::didCreateWebAnimation(WebAnimation& animation)
+{
+    FAST_RETURN_IF_NO_FRONTENDS(void());
+    if (auto* instrumentingAgents = instrumentingAgentsForContext(animation.scriptExecutionContext()))
+        didCreateWebAnimationImpl(*instrumentingAgents, animation);
 }
 
 inline void InspectorInstrumentation::willDestroyWebAnimation(WebAnimation& animation)
index 1f9a0d7..d7a2f71 100644 (file)
@@ -70,6 +70,7 @@ void InstrumentingAgents::reset()
     m_pageDOMDebuggerAgent = nullptr;
     m_inspectorCanvasAgent = nullptr;
     m_persistentInspectorAnimationAgent = nullptr;
+    m_enabledInspectorAnimationAgent = nullptr;
     m_trackingInspectorAnimationAgent = nullptr;
 }
 
index f6fea7d..f59c3db 100644 (file)
@@ -156,6 +156,9 @@ public:
     InspectorAnimationAgent* persistentInspectorAnimationAgent() const { return m_persistentInspectorAnimationAgent; }
     void setPersistentInspectorAnimationAgent(InspectorAnimationAgent* agent) { m_persistentInspectorAnimationAgent = agent; }
 
+    InspectorAnimationAgent* enabledInspectorAnimationAgent() const { return m_enabledInspectorAnimationAgent; }
+    void setEnabledInspectorAnimationAgent(InspectorAnimationAgent* agent) { m_enabledInspectorAnimationAgent = agent; }
+
     InspectorAnimationAgent* trackingInspectorAnimationAgent() const { return m_trackingInspectorAnimationAgent; }
     void setTrackingInspectorAnimationAgent(InspectorAnimationAgent* agent) { m_trackingInspectorAnimationAgent = agent; }
 
@@ -190,6 +193,7 @@ private:
     PageDOMDebuggerAgent* m_pageDOMDebuggerAgent { nullptr };
     InspectorCanvasAgent* m_inspectorCanvasAgent { nullptr };
     InspectorAnimationAgent* m_persistentInspectorAnimationAgent { nullptr };
+    InspectorAnimationAgent* m_enabledInspectorAnimationAgent { nullptr };
     InspectorAnimationAgent* m_trackingInspectorAnimationAgent { nullptr };
 };
 
index de9a2c5..2143b3d 100644 (file)
 #include "AnimationEffect.h"
 #include "AnimationEffectPhase.h"
 #include "CSSAnimation.h"
+#include "CSSComputedStyleDeclaration.h"
+#include "CSSPropertyNames.h"
 #include "CSSTransition.h"
+#include "CSSValue.h"
 #include "DeclarativeAnimation.h"
 #include "Element.h"
 #include "Event.h"
+#include "FillMode.h"
+#include "Frame.h"
 #include "InspectorDOMAgent.h"
 #include "InstrumentingAgents.h"
+#include "JSWebAnimation.h"
 #include "KeyframeEffect.h"
+#include "KeyframeList.h"
+#include "Page.h"
+#include "PlaybackDirection.h"
+#include "RenderElement.h"
+#include "TimingFunction.h"
 #include "WebAnimation.h"
 #include <JavaScriptCore/IdentifiersFactory.h>
 #include <JavaScriptCore/InspectorEnvironment.h>
+#include <JavaScriptCore/ScriptCallStackFactory.h>
 #include <wtf/HashMap.h>
+#include <wtf/Optional.h>
 #include <wtf/Seconds.h>
 #include <wtf/Stopwatch.h>
+#include <wtf/Vector.h>
+#include <wtf/text/StringBuilder.h>
 #include <wtf/text/WTFString.h>
 
 namespace WebCore {
 
 using namespace Inspector;
 
+static Optional<double> protocolValueForSeconds(const Seconds& seconds)
+{
+    if (seconds == Seconds::infinity() || seconds == Seconds::nan())
+        return WTF::nullopt;
+    return seconds.milliseconds();
+}
+
+static Optional<Inspector::Protocol::Animation::PlaybackDirection> protocolValueForPlaybackDirection(PlaybackDirection playbackDirection)
+{
+    switch (playbackDirection) {
+    case PlaybackDirection::Normal:
+        return Inspector::Protocol::Animation::PlaybackDirection::Normal;
+    case PlaybackDirection::Reverse:
+        return Inspector::Protocol::Animation::PlaybackDirection::Reverse;
+    case PlaybackDirection::Alternate:
+        return Inspector::Protocol::Animation::PlaybackDirection::Alternate;
+    case PlaybackDirection::AlternateReverse:
+        return Inspector::Protocol::Animation::PlaybackDirection::AlternateReverse;
+    }
+
+    ASSERT_NOT_REACHED();
+    return WTF::nullopt;
+}
+
+static Optional<Inspector::Protocol::Animation::FillMode> protocolValueForFillMode(FillMode fillMode)
+{
+    switch (fillMode) {
+    case FillMode::None:
+        return Inspector::Protocol::Animation::FillMode::None;
+    case FillMode::Forwards:
+        return Inspector::Protocol::Animation::FillMode::Forwards;
+    case FillMode::Backwards:
+        return Inspector::Protocol::Animation::FillMode::Backwards;
+    case FillMode::Both:
+        return Inspector::Protocol::Animation::FillMode::Both;
+    case FillMode::Auto:
+        return Inspector::Protocol::Animation::FillMode::Auto;
+    }
+
+    ASSERT_NOT_REACHED();
+    return WTF::nullopt;
+}
+
+static Ref<JSON::ArrayOf<Inspector::Protocol::Animation::Keyframe>> buildObjectForKeyframes(KeyframeEffect& keyframeEffect)
+{
+    auto keyframesPayload = JSON::ArrayOf<Inspector::Protocol::Animation::Keyframe>::create();
+
+    const auto& blendingKeyframes = keyframeEffect.blendingKeyframes();
+    const auto& parsedKeyframes = keyframeEffect.parsedKeyframes();
+
+    if (is<DeclarativeAnimation>(keyframeEffect.animation())) {
+        ASSERT(keyframeEffect.target());
+        auto* renderer = keyframeEffect.target()->renderer();
+
+        // Synthesize CSS style declarations for each keyframe so the frontend can display them.
+        ComputedStyleExtractor computedStyleExtractor(keyframeEffect.target());
+
+        for (size_t i = 0; i < blendingKeyframes.size(); ++i) {
+            auto& blendingKeyframe = blendingKeyframes[i];
+
+            ASSERT(blendingKeyframe.style());
+            auto& style = *blendingKeyframe.style();
+
+            auto keyframePayload = Inspector::Protocol::Animation::Keyframe::create()
+                .setOffset(blendingKeyframe.key())
+                .release();
+
+            RefPtr<TimingFunction> timingFunction;
+            if (!parsedKeyframes.isEmpty())
+                timingFunction = parsedKeyframes[i].timingFunction;
+            if (!timingFunction)
+                timingFunction = blendingKeyframe.timingFunction();
+            if (!timingFunction)
+                timingFunction = downcast<DeclarativeAnimation>(*keyframeEffect.animation()).backingAnimation().timingFunction();
+            if (timingFunction)
+                keyframePayload->setEasing(timingFunction->cssText());
+
+            StringBuilder stylePayloadBuilder;
+            auto& cssPropertyIds = blendingKeyframe.properties();
+            size_t count = cssPropertyIds.size();
+            for (auto cssPropertyId : cssPropertyIds) {
+                --count;
+                if (cssPropertyId == CSSPropertyCustom)
+                    continue;
+
+                stylePayloadBuilder.append(getPropertyNameString(cssPropertyId));
+                stylePayloadBuilder.append(": ");
+                if (auto value = computedStyleExtractor.valueForPropertyInStyle(style, cssPropertyId, renderer))
+                    stylePayloadBuilder.append(value->cssText());
+                stylePayloadBuilder.append(';');
+                if (count > 0)
+                    stylePayloadBuilder.append(' ');
+            }
+            if (!stylePayloadBuilder.isEmpty())
+                keyframePayload->setStyle(stylePayloadBuilder.toString());
+
+            keyframesPayload->addItem(WTFMove(keyframePayload));
+        }
+    } else {
+        for (const auto& parsedKeyframe : parsedKeyframes) {
+            auto keyframePayload = Inspector::Protocol::Animation::Keyframe::create()
+                .setOffset(parsedKeyframe.computedOffset)
+                .release();
+
+            if (!parsedKeyframe.easing.isEmpty())
+                keyframePayload->setEasing(parsedKeyframe.easing);
+            else if (const auto& timingFunction = parsedKeyframe.timingFunction)
+                keyframePayload->setEasing(timingFunction->cssText());
+
+            if (!parsedKeyframe.style->isEmpty())
+                keyframePayload->setStyle(parsedKeyframe.style->asText());
+
+            keyframesPayload->addItem(WTFMove(keyframePayload));
+        }
+    }
+
+    return keyframesPayload;
+}
+
+static Ref<Inspector::Protocol::Animation::Effect> buildObjectForEffect(AnimationEffect& effect)
+{
+    auto effectPayload = Inspector::Protocol::Animation::Effect::create()
+        .release();
+
+    if (auto startDelay = protocolValueForSeconds(effect.delay()))
+        effectPayload->setStartDelay(startDelay.value());
+
+    if (auto endDelay = protocolValueForSeconds(effect.endDelay()))
+        effectPayload->setEndDelay(endDelay.value());
+
+    effectPayload->setIterationCount(effect.iterations());
+    effectPayload->setIterationStart(effect.iterationStart());
+
+    if (auto iterationDuration = protocolValueForSeconds(effect.iterationDuration()))
+        effectPayload->setIterationDuration(iterationDuration.value());
+
+    if (auto* timingFunction = effect.timingFunction())
+        effectPayload->setTimingFunction(timingFunction->cssText());
+
+    if (auto playbackDirection = protocolValueForPlaybackDirection(effect.direction()))
+        effectPayload->setPlaybackDirection(playbackDirection.value());
+
+    if (auto fillMode = protocolValueForFillMode(effect.fill()))
+        effectPayload->setFillMode(fillMode.value());
+
+    if (is<KeyframeEffect>(effect))
+        effectPayload->setKeyframes(buildObjectForKeyframes(downcast<KeyframeEffect>(effect)));
+
+    return effectPayload;
+}
+
 InspectorAnimationAgent::InspectorAnimationAgent(PageAgentContext& context)
     : InspectorAgentBase("Animation"_s, context)
     , m_frontendDispatcher(makeUnique<Inspector::AnimationFrontendDispatcher>(context.frontendRouter))
     , m_backendDispatcher(Inspector::AnimationBackendDispatcher::create(context.backendDispatcher, this))
+    , m_injectedScriptManager(context.injectedScriptManager)
+    , m_inspectedPage(context.inspectedPage)
+    , m_animationDestroyedTimer(*this, &InspectorAnimationAgent::animationDestroyedTimerFired)
 {
 }
 
@@ -67,11 +236,103 @@ void InspectorAnimationAgent::willDestroyFrontendAndBackend(DisconnectReason)
 {
     ErrorString ignored;
     stopTracking(ignored);
+    disable(ignored);
 
     ASSERT(m_instrumentingAgents.persistentInspectorAnimationAgent() == this);
     m_instrumentingAgents.setPersistentInspectorAnimationAgent(nullptr);
 }
 
+void InspectorAnimationAgent::enable(ErrorString& errorString)
+{
+    if (m_instrumentingAgents.enabledInspectorAnimationAgent() == this) {
+        errorString = "Animation domain already enabled"_s;
+        return;
+    }
+
+    m_instrumentingAgents.setEnabledInspectorAnimationAgent(this);
+
+    const auto existsInCurrentPage = [&] (ScriptExecutionContext* scriptExecutionContext) {
+        if (!is<Document>(scriptExecutionContext))
+            return false;
+
+        // FIXME: <https://webkit.org/b/168475> Web Inspector: Correctly display iframe's WebSockets
+        auto* document = downcast<Document>(scriptExecutionContext);
+        return document->page() == &m_inspectedPage;
+    };
+
+    {
+        LockHolder lock(WebAnimation::instancesMutex());
+        for (auto* animation : WebAnimation::instances(lock)) {
+            if (existsInCurrentPage(animation->scriptExecutionContext()))
+                bindAnimation(*animation, false);
+        }
+    }
+}
+
+void InspectorAnimationAgent::disable(ErrorString&)
+{
+    m_instrumentingAgents.setEnabledInspectorAnimationAgent(nullptr);
+
+    reset();
+}
+
+void InspectorAnimationAgent::requestEffectTarget(ErrorString& errorString, const String& animationId, int* nodeId)
+{
+    auto* animation = assertAnimation(errorString, animationId);
+    if (!animation)
+        return;
+
+    auto* domAgent = m_instrumentingAgents.inspectorDOMAgent();
+    if (!domAgent) {
+        errorString = "DOM domain must be enabled"_s;
+        return;
+    }
+
+    auto* effect = animation->effect();
+    if (!is<KeyframeEffect>(effect)) {
+        errorString = "Animation for given animationId does not have an effect"_s;
+        return;
+    }
+
+    auto& keyframeEffect = downcast<KeyframeEffect>(*effect);
+
+    auto* target = keyframeEffect.target();
+    if (!target) {
+        errorString = "Animation for given animationId does not have a target"_s;
+        return;
+    }
+
+    *nodeId = domAgent->pushNodePathToFrontend(errorString, target);
+}
+
+void InspectorAnimationAgent::resolveAnimation(ErrorString& errorString, const String& animationId, const String* objectGroup, RefPtr<Inspector::Protocol::Runtime::RemoteObject>& result)
+{
+    auto* animation = assertAnimation(errorString, animationId);
+    if (!animation)
+        return;
+
+    auto* state = animation->scriptExecutionContext()->execState();
+    auto injectedScript = m_injectedScriptManager.injectedScriptFor(state);
+    ASSERT(!injectedScript.hasNoValue());
+
+    JSC::JSValue value;
+    {
+        JSC::JSLockHolder lock(state);
+
+        auto* globalObject = deprecatedGlobalObjectForPrototype(state);
+        value = toJS(state, globalObject, animation);
+    }
+
+    if (!value) {
+        ASSERT_NOT_REACHED();
+        errorString = "Internal error: unknown Animation for given animationId"_s;
+        return;
+    }
+
+    String objectGroupName = objectGroup ? *objectGroup : String();
+    result = injectedScript.wrapObject(value, objectGroupName);
+}
+
 void InspectorAnimationAgent::startTracking(ErrorString& errorString)
 {
     if (m_instrumentingAgents.trackingInspectorAnimationAgent() == this) {
@@ -170,16 +431,151 @@ void InspectorAnimationAgent::willApplyKeyframeEffect(Element& target, KeyframeE
     m_frontendDispatcher->trackingUpdate(m_environment.executionStopwatch()->elapsedTime().seconds(), WTFMove(event));
 }
 
-void InspectorAnimationAgent::didChangeWebAnimationEffect(WebAnimation& animation)
+void InspectorAnimationAgent::didSetWebAnimationEffect(WebAnimation& animation)
 {
     if (is<DeclarativeAnimation>(animation))
         stopTrackingDeclarativeAnimation(downcast<DeclarativeAnimation>(animation));
+
+    didChangeWebAnimationEffectTiming(animation);
+    didChangeWebAnimationEffectTarget(animation);
+}
+
+void InspectorAnimationAgent::didChangeWebAnimationEffectTiming(WebAnimation& animation)
+{
+    // The `animationId` may be empty if Animation is tracking but not enabled.
+    auto animationId = findAnimationId(animation);
+    if (animationId.isEmpty())
+        return;
+
+    if (auto* effect = animation.effect())
+        m_frontendDispatcher->effectChanged(animationId, buildObjectForEffect(*effect));
+    else
+        m_frontendDispatcher->effectChanged(animationId, nullptr);
+}
+
+void InspectorAnimationAgent::didChangeWebAnimationEffectTarget(WebAnimation& animation)
+{
+    // The `animationId` may be empty if Animation is tracking but not enabled.
+    auto animationId = findAnimationId(animation);
+    if (animationId.isEmpty())
+        return;
+
+    m_frontendDispatcher->targetChanged(animationId);
+}
+
+void InspectorAnimationAgent::didCreateWebAnimation(WebAnimation& animation)
+{
+    if (!findAnimationId(animation).isEmpty()) {
+        ASSERT_NOT_REACHED();
+        return;
+    }
+
+    bindAnimation(animation, true);
 }
 
 void InspectorAnimationAgent::willDestroyWebAnimation(WebAnimation& animation)
 {
     if (is<DeclarativeAnimation>(animation))
         stopTrackingDeclarativeAnimation(downcast<DeclarativeAnimation>(animation));
+
+    // The `animationId` may be empty if Animation is tracking but not enabled.
+    auto animationId = findAnimationId(animation);
+    if (!animationId.isEmpty())
+        unbindAnimation(animationId);
+}
+
+void InspectorAnimationAgent::frameNavigated(Frame& frame)
+{
+    if (frame.isMainFrame()) {
+        reset();
+        return;
+    }
+
+    Vector<String> animationIdsToRemove;
+    for (auto& [animationId, animation] : m_animationIdMap) {
+        if (auto* scriptExecutionContext = animation->scriptExecutionContext()) {
+            if (is<Document>(scriptExecutionContext) && downcast<Document>(*scriptExecutionContext).frame() == &frame)
+                animationIdsToRemove.append(animationId);
+        }
+    }
+    for (const auto& animationId : animationIdsToRemove)
+        unbindAnimation(animationId);
+}
+
+String InspectorAnimationAgent::findAnimationId(WebAnimation& animation)
+{
+    for (auto& [animationId, existingAnimation] : m_animationIdMap) {
+        if (existingAnimation == &animation)
+            return animationId;
+    }
+    return nullString();
+}
+
+WebAnimation* InspectorAnimationAgent::assertAnimation(ErrorString& errorString, const String& animationId)
+{
+    auto* animation = m_animationIdMap.get(animationId);
+    if (!animation)
+        errorString = "Missing animation for given animationId"_s;
+    return animation;
+}
+
+void InspectorAnimationAgent::bindAnimation(WebAnimation& animation, bool captureBacktrace)
+{
+    auto animationId = makeString("animation:" + IdentifiersFactory::createIdentifier());
+    m_animationIdMap.set(animationId, &animation);
+
+    auto animationPayload = Inspector::Protocol::Animation::Animation::create()
+        .setAnimationId(animationId)
+        .release();
+
+    if (is<CSSAnimation>(animation))
+        animationPayload->setCssAnimationName(downcast<CSSAnimation>(animation).animationName());
+    else if (is<CSSTransition>(animation))
+        animationPayload->setCssTransitionProperty(downcast<CSSTransition>(animation).transitionProperty());
+
+    if (auto* effect = animation.effect())
+        animationPayload->setEffect(buildObjectForEffect(*effect));
+
+    if (captureBacktrace) {
+        auto stackTrace = Inspector::createScriptCallStack(JSExecState::currentState(), Inspector::ScriptCallStack::maxCallStackSizeToCapture);
+        animationPayload->setBacktrace(stackTrace->buildInspectorArray());
+    }
+
+    m_frontendDispatcher->animationCreated(WTFMove(animationPayload));
+}
+
+void InspectorAnimationAgent::unbindAnimation(const String& animationId)
+{
+    m_animationIdMap.remove(animationId);
+
+    // This can be called in response to GC. Due to the single-process model used in WebKit1, the
+    // event must be dispatched from a timer to prevent the frontend from making JS allocations
+    // while the GC is still active.
+    m_removedAnimationIds.append(animationId);
+
+    if (!m_animationDestroyedTimer.isActive())
+        m_animationDestroyedTimer.startOneShot(0_s);
+}
+
+void InspectorAnimationAgent::animationDestroyedTimerFired()
+{
+    if (!m_removedAnimationIds.size())
+        return;
+
+    for (auto& identifier : m_removedAnimationIds)
+        m_frontendDispatcher->animationDestroyed(identifier);
+
+    m_removedAnimationIds.clear();
+}
+
+void InspectorAnimationAgent::reset()
+{
+    m_animationIdMap.clear();
+
+    m_removedAnimationIds.clear();
+
+    if (m_animationDestroyedTimer.isActive())
+        m_animationDestroyedTimer.stop();
 }
 
 void InspectorAnimationAgent::stopTrackingDeclarativeAnimation(DeclarativeAnimation& animation)
index 7a24bbc..d266d7e 100644 (file)
 
 #include "ComputedEffectTiming.h"
 #include "InspectorWebAgentBase.h"
+#include "Timer.h"
 #include <JavaScriptCore/InspectorBackendDispatchers.h>
 #include <JavaScriptCore/InspectorFrontendDispatchers.h>
+#include <JavaScriptCore/InspectorProtocolObjects.h>
 #include <wtf/Forward.h>
 
 namespace WebCore {
 
+class AnimationEffect;
 class DeclarativeAnimation;
 class Element;
 class Event;
+class Frame;
 class KeyframeEffect;
+class Page;
 class WebAnimation;
 
 typedef String ErrorString;
@@ -53,20 +58,42 @@ public:
     void willDestroyFrontendAndBackend(Inspector::DisconnectReason) override;
 
     // AnimationBackendDispatcherHandler
+    void enable(ErrorString&) override;
+    void disable(ErrorString&) override;
+    void requestEffectTarget(ErrorString&, const String& animationId, int* nodeId) override;
+    void resolveAnimation(ErrorString&, const String& animationId, const String* objectGroup, RefPtr<Inspector::Protocol::Runtime::RemoteObject>&) override;
     void startTracking(ErrorString&) override;
     void stopTracking(ErrorString&) override;
 
     // InspectorInstrumentation
     void willApplyKeyframeEffect(Element&, KeyframeEffect&, ComputedEffectTiming);
-    void didChangeWebAnimationEffect(WebAnimation&);
+    void didSetWebAnimationEffect(WebAnimation&);
+    void didChangeWebAnimationEffectTiming(WebAnimation&);
+    void didChangeWebAnimationEffectTarget(WebAnimation&);
+    void didCreateWebAnimation(WebAnimation&);
     void willDestroyWebAnimation(WebAnimation&);
+    void frameNavigated(Frame&);
 
 private:
+    String findAnimationId(WebAnimation&);
+    WebAnimation* assertAnimation(ErrorString&, const String& animationId);
+    void bindAnimation(WebAnimation&, bool captureBacktrace);
+    void unbindAnimation(const String& animationId);
+    void animationDestroyedTimerFired();
+    void reset();
+
     void stopTrackingDeclarativeAnimation(DeclarativeAnimation&);
 
     std::unique_ptr<Inspector::AnimationFrontendDispatcher> m_frontendDispatcher;
     RefPtr<Inspector::AnimationBackendDispatcher> m_backendDispatcher;
 
+    Inspector::InjectedScriptManager& m_injectedScriptManager;
+    Page& m_inspectedPage;
+
+    HashMap<String, WebAnimation*> m_animationIdMap;
+    Vector<String> m_removedAnimationIds;
+    Timer m_animationDestroyedTimer;
+
     struct TrackedDeclarativeAnimationData {
         String trackingAnimationId;
         ComputedEffectTiming lastComputedTiming;
index aecc79b..0f46a30 100644 (file)
@@ -566,7 +566,7 @@ int InspectorDOMAgent::pushNodeToFrontend(ErrorString& errorString, int document
         return 0;
     }
 
-    return pushNodePathToFrontend(nodeToPush);
+    return pushNodePathToFrontend(errorString, nodeToPush);
 }
 
 Node* InspectorDOMAgent::nodeForId(int id)
@@ -613,7 +613,7 @@ void InspectorDOMAgent::querySelector(ErrorString& errorString, int nodeId, cons
     }
 
     if (auto* element = queryResult.releaseReturnValue())
-        *elementId = pushNodePathToFrontend(element);
+        *elementId = pushNodePathToFrontend(errorString, element);
 }
 
 void InspectorDOMAgent::querySelectorAll(ErrorString& errorString, int nodeId, const String& selectors, RefPtr<JSON::ArrayOf<int>>& result)
@@ -640,12 +640,23 @@ void InspectorDOMAgent::querySelectorAll(ErrorString& errorString, int nodeId, c
 
 int InspectorDOMAgent::pushNodePathToFrontend(Node* nodeToPush)
 {
+    ErrorString ignored;
+    return pushNodePathToFrontend(ignored, nodeToPush);
+}
+
+int InspectorDOMAgent::pushNodePathToFrontend(ErrorString errorString, Node* nodeToPush)
+{
     ASSERT(nodeToPush);  // Invalid input
 
-    if (!m_document)
+    if (!m_document) {
+        errorString = "Missing document"_s;
         return 0;
-    if (!m_documentNodeToIdMap.contains(m_document))
+    }
+
+    if (!m_documentNodeToIdMap.contains(m_document)) {
+        errorString = "Document must have been requested"_s;
         return 0;
+    }
 
     // Return id in case the node is known.
     int result = m_documentNodeToIdMap.get(nodeToPush);
@@ -790,7 +801,7 @@ void InspectorDOMAgent::setNodeName(ErrorString& errorString, int nodeId, const
     if (!m_domEditor->removeChild(*parent, *oldNode, errorString))
         return;
 
-    *newId = pushNodePathToFrontend(newElement.ptr());
+    *newId = pushNodePathToFrontend(errorString, newElement.ptr());
     if (m_childrenRequested.contains(nodeId))
         pushChildNodesToFrontend(*newId);
 }
@@ -830,7 +841,7 @@ void InspectorDOMAgent::setOuterHTML(ErrorString& errorString, int nodeId, const
         return;
     }
 
-    int newId = pushNodePathToFrontend(newNode);
+    int newId = pushNodePathToFrontend(errorString, newNode);
 
     bool childrenRequested = m_childrenRequested.contains(nodeId);
     if (childrenRequested)
@@ -1422,7 +1433,7 @@ void InspectorDOMAgent::moveTo(ErrorString& errorString, int nodeId, int targetE
     if (!m_domEditor->insertBefore(*targetElement, *node, anchorNode, errorString))
         return;
 
-    *newNodeId = pushNodePathToFrontend(node);
+    *newNodeId = pushNodePathToFrontend(errorString, node);
 }
 
 void InspectorDOMAgent::undo(ErrorString& errorString)
@@ -1498,11 +1509,11 @@ void InspectorDOMAgent::getAttributes(ErrorString& errorString, int nodeId, RefP
     result = buildArrayForElementAttributes(element);
 }
 
-void InspectorDOMAgent::requestNode(ErrorString&, const String& objectId, int* nodeId)
+void InspectorDOMAgent::requestNode(ErrorString& errorString, const String& objectId, int* nodeId)
 {
     Node* node = nodeForObjectId(objectId);
     if (node)
-        *nodeId = pushNodePathToFrontend(node);
+        *nodeId = pushNodePathToFrontend(errorString, node);
     else
         *nodeId = 0;
 }
@@ -2646,7 +2657,7 @@ Node* InspectorDOMAgent::nodeForObjectId(const String& objectId)
 void InspectorDOMAgent::pushNodeByPathToFrontend(ErrorString& errorString, const String& path, int* nodeId)
 {
     if (Node* node = nodeForPath(path))
-        *nodeId = pushNodePathToFrontend(node);
+        *nodeId = pushNodePathToFrontend(errorString, node);
     else
         errorString = "Missing node for given path"_s;
 }
index 51639ab..7df0e83 100644 (file)
@@ -180,6 +180,8 @@ public:
 
     int pushNodeToFrontend(Node*);
     int pushNodeToFrontend(ErrorString&, int documentNodeId, Node*);
+    int pushNodePathToFrontend(Node*);
+    int pushNodePathToFrontend(ErrorString, Node*);
     Node* nodeForId(int nodeId);
     int boundNodeId(const Node*);
 
@@ -217,7 +219,6 @@ private:
     Node* assertEditableNode(ErrorString&, int nodeId);
     Element* assertEditableElement(ErrorString&, int nodeId);
 
-    int pushNodePathToFrontend(Node*);
     void pushChildNodesToFrontend(int nodeId, int depth = 1);
 
     Ref<Inspector::Protocol::DOM::Node> buildObjectForNode(Node*, int depth, NodeToIdMap*);
index 872a871..4a39106 100644 (file)
@@ -1,3 +1,328 @@
+2020-01-29  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: add instrumentation for showing existing Web Animations
+        https://bugs.webkit.org/show_bug.cgi?id=205434
+        <rdar://problem/28328087>
+
+        Reviewed by Brian Burg.
+
+        * UserInterface/Controllers/AnimationManager.js: Added.
+        (WI.AnimationManager):
+        (WI.AnimationManager.prototype.get domains):
+        (WI.AnimationManager.prototype.activateExtraDomain):
+        (WI.AnimationManager.prototype.initializeTarget):
+        (WI.AnimationManager.prototype.get animationCollection):
+        (WI.AnimationManager.prototype.get supported):
+        (WI.AnimationManager.prototype.enable):
+        (WI.AnimationManager.prototype.disable):
+        (WI.AnimationManager.prototype.animationCreated):
+        (WI.AnimationManager.prototype.effectChanged):
+        (WI.AnimationManager.prototype.targetChanged):
+        (WI.AnimationManager.prototype.animationDestroyed):
+        (WI.AnimationManager.prototype._handleMainResourceDidChange):
+        * UserInterface/Protocol/AnimationObserver.js:
+        (WI.AnimationObserver.prototype.animationCreated): Added.
+        (WI.AnimationObserver.prototype.effectChanged): Added.
+        (WI.AnimationObserver.prototype.targetChanged): Added.
+        (WI.AnimationObserver.prototype.animationDestroyed): Added.
+
+        * UserInterface/Models/AnimationCollection.js: Added.
+        (WI.AnimationCollection):
+        (WI.AnimationCollection.prototype.get animationType):
+        (WI.AnimationCollection.prototype.get displayName):
+        (WI.AnimationCollection.prototype.objectIsRequiredType):
+        (WI.AnimationCollection.prototype.animationCollectionForType):
+        (WI.AnimationCollection.prototype.itemAdded):
+        (WI.AnimationCollection.prototype.itemRemoved):
+        (WI.AnimationCollection.prototype.itemsCleared):
+        Similar to `WI.ResourceCollection`, create a subclass of `WI.Collection` that maintains it's
+        own sub-`WI.AnimationCollection`s for each type of `WI.Animation.Type`.
+
+        * UserInterface/Models/Animation.js: Added.
+        (WI.Animation):
+        (WI.Animation.fromPayload):
+        (WI.Animation.displayNameForAnimationType):
+        (WI.Animation.displayNameForPlaybackDirection):
+        (WI.Animation.displayNameForFillMode):
+        (WI.Animation.resetUniqueDisplayNameNumbers):
+        (WI.Animation.prototype.get animationId):
+        (WI.Animation.prototype.get backtrace):
+        (WI.Animation.prototype.get animationType):
+        (WI.Animation.prototype.get startDelay):
+        (WI.Animation.prototype.get endDelay):
+        (WI.Animation.prototype.get iterationCount):
+        (WI.Animation.prototype.get iterationStart):
+        (WI.Animation.prototype.get iterationDuration):
+        (WI.Animation.prototype.get timingFunction):
+        (WI.Animation.prototype.get playbackDirection):
+        (WI.Animation.prototype.get fillMode):
+        (WI.Animation.prototype.get keyframes):
+        (WI.Animation.prototype.get displayName):
+        (WI.Animation.prototype.requestEffectTarget):
+        (WI.Animation.prototype.effectChanged):
+        (WI.Animation.prototype.targetChanged):
+        (WI.Animation.prototype._updateEffect):
+        * UserInterface/Protocol/RemoteObject.js:
+        (WI.RemoteObject.resolveAnimation): Added.
+
+        * UserInterface/Views/GraphicsTabContentView.js: Renamed from Source/WebInspectorUI/UserInterface/Views/CanvasTabContentView.js.
+        (WI.GraphicsTabContentView):
+        (WI.GraphicsTabContentView.tabInfo):
+        (WI.GraphicsTabContentView.isTabAllowed):
+        (WI.GraphicsTabContentView.prototype.get type):
+        (WI.GraphicsTabContentView.prototype.showRepresentedObject): Added.
+        (WI.GraphicsTabContentView.prototype.canShowRepresentedObject):
+        (WI.GraphicsTabContentView.prototype.closed):
+        (WI.GraphicsTabContentView.prototype.attached):
+        (WI.GraphicsTabContentView.prototype.detached):
+        (WI.GraphicsTabContentView.prototype.initialLayout): Added.
+        (WI.GraphicsTabContentView.prototype._handleOverviewTreeOutlineSelectionDidChange): Added.
+        * UserInterface/Views/GraphicsTabContentView.css: Renamed from Source/WebInspectorUI/UserInterface/Views/CanvasTabContentView.css.
+        Rename the Canvas Tab to Graphics Tab and display four sections:
+         - Canvases
+         - Web Animations
+         - CSS Animations
+         - CSS Transitions
+
+        * UserInterface/Views/CanvasSidebarPanel.js:
+        (WI.CanvasSidebarPanel.prototype.canShowRepresentedObject):
+        Only appear if a `WI.Canvas` or `WI.Recording` is selected.
+
+        * UserInterface/Views/GraphicsOverviewContentView.js: Added.
+        (WI.GraphicsOverviewContentView):
+        (WI.GraphicsOverviewContentView.prototype.get supplementalRepresentedObjects):
+        (WI.GraphicsOverviewContentView.prototype.get navigationItems):
+        (WI.GraphicsOverviewContentView.prototype.attached):
+        (WI.GraphicsOverviewContentView.prototype.detached):
+        (WI.GraphicsOverviewContentView.prototype.initialLayout):
+        (WI.GraphicsOverviewContentView.prototype.dropZoneShouldAppearForDragEvent):
+        (WI.GraphicsOverviewContentView.prototype.dropZoneHandleDrop):
+        (WI.GraphicsOverviewContentView.prototype._handleRefreshButtonClicked):
+        (WI.GraphicsOverviewContentView.prototype._handleShowGridButtonClicked):
+        (WI.GraphicsOverviewContentView.prototype._handleShowImageGridSettingChanged):
+        (WI.GraphicsOverviewContentView.prototype._handleImportButtonNavigationItemClicked):
+        (WI.GraphicsOverviewContentView.prototype._handleOverviewViewSelectedItemChanged):
+        (WI.GraphicsOverviewContentView.prototype._handleOverviewViewSupplementalRepresentedObjectsDidChange):
+        (WI.GraphicsOverviewContentView.prototype._handleClick):
+        * UserInterface/Views/GraphicsOverviewContentView.css: Added.
+        (.content-view.graphics-overview):
+        (.content-view.graphics-overview > section):
+        (.content-view.graphics-overview > section:not(:first-child)):
+        (.content-view.graphics-overview > section > .header):
+        (.content-view.graphics-overview > section:not(:first-of-type) > .header):
+        (.content-view.graphics-overview > section > .header > h1):
+        (.content-view.graphics-overview > section > .header > .navigation-bar):
+        (.content-view.graphics-overview > .content-view.canvas-overview):
+        (@media (prefers-color-scheme: light) .content-view.graphics-overview):
+        (@media (prefers-color-scheme: light) .content-view.graphics-overview > section > .header):
+        Add sticky headers for each of the sections described above.
+
+        * UserInterface/Views/AnimationCollectionContentView.js: Added.
+        (WI.AnimationCollectionContentView):
+        (WI.AnimationCollectionContentView.prototype.handleRefreshButtonClicked):
+        (WI.AnimationCollectionContentView.prototype.contentViewAdded):
+        (WI.AnimationCollectionContentView.prototype.contentViewRemoved):
+        (WI.AnimationCollectionContentView.prototype.detached):
+        (WI.AnimationCollectionContentView.prototype._handleContentViewMouseEnter):
+        (WI.AnimationCollectionContentView.prototype._handleContentViewMouseLeave):
+        * UserInterface/Views/AnimationCollectionContentView.css: Added.
+        (.content-view.animation-collection):
+
+        * UserInterface/Views/AnimationContentView.js: Added.
+        (WI.AnimationContentView):
+        (WI.AnimationContentView.get previewHeight):
+        (WI.AnimationContentView.prototype.handleRefreshButtonClicked):
+        (WI.AnimationContentView.prototype.initialLayout):
+        (WI.AnimationContentView.prototype.layout):
+        (WI.AnimationContentView.prototype.sizeDidChange):
+        (WI.AnimationContentView.prototype.attached):
+        (WI.AnimationContentView.prototype.detached):
+        (WI.AnimationContentView.prototype._refreshSubtitle):
+        (WI.AnimationContentView.prototype._refreshPreview.addTitle):
+        (WI.AnimationContentView.prototype._refreshPreview):
+        (WI.AnimationContentView.prototype._handleEffectChanged):
+        (WI.AnimationContentView.prototype._handleTargetChanged):
+        (WI.AnimationContentView.prototype._populateAnimationTargetButtonContextMenu):
+        * UserInterface/Views/AnimationContentView.css: Added.
+        (.content-view.animation):
+        (.content-view.animation.selected):
+        (.content-view.animation > header):
+        (.content-view.animation > header > .titles):
+        (.content-view.animation > header > .titles > .title):
+        (.content-view.animation > header > .titles > .subtitle):
+        (.content-view.animation > header > .titles > .subtitle:not(:empty)::before):
+        (.content-view.animation > header > .navigation-bar):
+        (.content-view.animation:hover > header > .navigation-bar):
+        (.content-view.animation > .preview):
+        (.content-view.animation > .preview > svg):
+        (body[dir=rtl] .content-view.animation > .preview > svg):
+        (.content-view.animation > .preview > svg rect):
+        (.content-view.animation > .preview > svg > .delay line):
+        (.content-view.animation > .preview > svg > .active path):
+        (.content-view.animation > .preview > svg > .active circle):
+        (.content-view.animation > .preview > svg > .active line):
+        (.content-view.animation > .preview > span):
+        (@media (prefers-color-scheme: dark) .content-view.animation > header > .titles > .title):
+        (@media (prefers-color-scheme: dark) .content-view.animation > header > .titles > .subtitle):
+        (@media (prefers-color-scheme: dark) .content-view.animation > .preview):
+        Visualize the start/end delay and keyframes of the given animation as a series of bezier
+        curves separated by markers.
+
+        * UserInterface/Views/AnimationDetailsSidebarPanel.js: Added.
+        (WI.AnimationDetailsSidebarPanel):
+        (WI.AnimationDetailsSidebarPanel.prototype.inspect):
+        (WI.AnimationDetailsSidebarPanel.prototype.get animation):
+        (WI.AnimationDetailsSidebarPanel.prototype.set animation):
+        (WI.AnimationDetailsSidebarPanel.prototype.initialLayout):
+        (WI.AnimationDetailsSidebarPanel.prototype.layout):
+        (WI.AnimationDetailsSidebarPanel.prototype._refreshIdentitySection):
+        (WI.AnimationDetailsSidebarPanel.prototype._refreshEffectSection):
+        (WI.AnimationDetailsSidebarPanel.prototype._refreshBacktraceSection):
+        (WI.AnimationDetailsSidebarPanel.prototype._handleAnimationEffectChanged):
+        (WI.AnimationDetailsSidebarPanel.prototype._handleAnimationTargetChanged):
+        (WI.AnimationDetailsSidebarPanel.prototype._handleDetailsSectionCollapsedStateChanged):
+        * UserInterface/Views/AnimationDetailsSidebarPanel.css: Added.
+        (.sidebar > .panel.details.animation > .content > .details-section.animation-keyframes .header > .subtitle):
+        (.sidebar > .panel.details.animation > .content > .details-section.animation-keyframes .details-section):
+        (.sidebar > .panel.details.animation > .content > .details-section.animation-keyframes .details-section .row.styles):
+        (.sidebar > .panel.details.animation > .content > .details-section.animation-keyframes .details-section .row.styles .CodeMirror):
+        (.sidebar > .panel.details.animation > .content > .details-section.animation-keyframes .details-section):
+        Show collected information about the selected animation, its effect, and its target.
+
+        * UserInterface/Controllers/CanvasManager.js:
+        (WI.CanvasManager):
+        (WI.CanvasManager.prototype.get canvasCollection): Added.
+        (WI.CanvasManager.prototype.disable):
+        (WI.CanvasManager.prototype.canvasAdded):
+        (WI.CanvasManager.prototype.canvasRemoved):
+        (WI.CanvasManager.prototype._saveRecordings): Added.
+        (WI.CanvasManager.prototype._mainResourceDidChange):
+        (WI.CanvasManager.prototype.get canvases): Deleted.
+        (WI.CanvasManager.prototype._removeCanvas): Deleted.
+        Rather than have the `WI.CanvasTabContentView` mainain the `WI.CanvasCollection` and have to
+        listen for events from the `WI.CanvasManager`, just have the `WI.CanvasManager` hold on to
+        it instead and provide a getter for it.
+
+        * UserInterface/Views/CanvasOverviewContentView.js:
+        (WI.CanvasOverviewContentView):
+        (WI.CanvasOverviewContentView.prototype.get navigationItems):
+        (WI.CanvasOverviewContentView.prototype.handleRefreshButtonClicked):
+        (WI.CanvasOverviewContentView.prototype.contentViewAdded):
+        (WI.CanvasOverviewContentView.prototype.contentViewRemoved):
+        (WI.CanvasOverviewContentView.prototype.attached):
+        (WI.CanvasOverviewContentView.prototype.detached):
+        (WI.CanvasOverviewContentView.prototype._addSavedRecording):
+        (WI.CanvasOverviewContentView.prototype.hidden): Deleted.
+        (WI.CanvasOverviewContentView.prototype.get _itemMargin): Deleted.
+        (WI.CanvasOverviewContentView.prototype._refreshPreviews): Deleted.
+        (WI.CanvasOverviewContentView.prototype._updateNavigationItems): Deleted.
+        (WI.CanvasOverviewContentView.prototype._showGridButtonClicked): Deleted.
+        (WI.CanvasOverviewContentView.prototype._updateShowImageGrid): Deleted.
+        * UserInterface/Views/CanvasOverviewContentView.css:
+        (.content-view.canvas-overview):
+        (.content-view.canvas-overview > .content-view.canvas):
+        (@media (prefers-color-scheme: dark) .content-view.canvas-overview): Deleted.
+
+        * UserInterface/Views/CanvasContentView.js:
+        (WI.CanvasContentView):
+        (WI.CanvasContentView.prototype.handleRefreshButtonClicked): Added.
+        (WI.CanvasContentView.prototype.dropZoneShouldAppearForDragEvent): Added.
+        (WI.CanvasContentView.prototype.dropZoneHandleDrop): Added.
+        (WI.CanvasContentView.prototype.initialLayout):
+        (WI.CanvasContentView.prototype.attached):
+        (WI.CanvasContentView.prototype._populateCanvasElementButtonContextMenu):
+        (WI.CanvasContentView.prototype.shown): Deleted.
+        Move the "Log Canvas Context" to be the first item in the canvas element button context menu.
+        Drive-by: add a `WI.DropZoneView` for when recording JSON files are dragged on top.
+
+        * UserInterface/Views/CanvasContentView.css:
+        Drive-by: drop `:not(.tab)` from all selectors since the Canvas Tab doesn't exist anymore.
+
+        * UserInterface/Views/CollectionContentView.js:
+        (WI.CollectionContentView):
+        (WI.CollectionContentView.prototype.get selectedItem): Added.
+        (WI.CollectionContentView.prototype.set selectedItem): Added.
+        (WI.CollectionContentView.prototype.addContentViewForItem):
+        (WI.CollectionContentView.prototype.removeContentViewForItem):
+        (WI.CollectionContentView.prototype.showContentPlaceholder):
+        (WI.CollectionContentView.prototype.initialLayout):
+        (WI.CollectionContentView.prototype._selectItem):
+        (WI.CollectionContentView.prototype._handleClick): Added.
+        (WI.CollectionContentView.prototype.setSelectedItem): Deleted.
+        * UserInterface/Views/CollectionContentView.css:
+        (.content-view.collection > .placeholder:not(.message-text-view)): Added.
+        (.content-view.collection .resource.image img): Deleted.
+        (.content-view.collection .resource.image img:hover): Deleted.
+        When selection is enabled, clicking outside of any of the content views should dismiss the
+        current selection. Clients should also be able to get the currently selected item.
+
+        * UserInterface/Views/DetailsSectionSimpleRow.js:
+        (WI.DetailsSectionSimpleRow.prototype.set value):
+        Ensure that `0` is considered as a valid value.
+
+        * UserInterface/Base/Main.js:
+        (WI.loaded):
+        (WI.contentLoaded):
+        (WI.tabContentViewClassForRepresentedObject):
+        * UserInterface/Views/ContentView.js:
+        (WI.ContentView.createFromRepresentedObject):
+        (WI.ContentView.isViewable):
+        Allow `WI.Animation` to be viewable.
+
+        * UserInterface/Views/Main.css:
+        (.navigation-item-help): Added.
+        (.navigation-item-help > .navigation-bar): Added.
+        (.navigation-item-help > .navigation-bar > .item): Added.
+        (.message-text-view .navigation-item-help): Deleted.
+        (.message-text-view .navigation-item-help .navigation-bar): Deleted.
+        (.message-text-view .navigation-item-help .navigation-bar > .item): Deleted.
+        Allow `WI.createNavigationItemHelp` to be used independently of `WI.createMessageTextView`.
+
+        * UserInterface/Controllers/DOMManager.js:
+        (WI.DOMManager.prototype.nodeForId):
+        * UserInterface/Controllers/TimelineManager.js:
+        (WI.TimelineManager.prototype.animationTrackingUpdated):
+        * UserInterface/Models/AuditTestCaseResult.js:
+        (WI.AuditTestCaseResult.async fromPayload):
+        Add a fallback so callers don't need to.
+
+        * UserInterface/Views/ResourceCollectionContentView.js:
+        (WI.ResourceCollectionContentView):
+        * UserInterface/Views/ResourceCollectionContentView.css:
+        (.content-view.resource-collection > .resource.image img): Added.
+        (.content-view.resource-collection > .resource.image img:hover): Added.
+        Drive-by: move these styles to the right file and make them more specific.
+
+        * UserInterface/Models/Canvas.js:
+        (WI.Canvas.displayNameForContextType):
+        * UserInterface/Models/Recording.js:
+        (WI.Recording.displayNameForRecordingType): Added.
+        Drive-by: fix localized strings.
+
+        * UserInterface/Views/RecordingContentView.css:
+        Drive-by: drop `:not(.tab)` from all selectors since the Recording Tab doesn't exist anymore.
+
+        * UserInterface/Main.html:
+        * UserInterface/Images/Graphics.svg: Renamed from Source/WebInspectorUI/UserInterface/Images/Canvas.svg.
+        * Localizations/en.lproj/localizedStrings.js:
+
+        * UserInterface/Test.html:
+        * UserInterface/Test/Test.js:
+        (WI.loaded):
+
+        * UserInterface/Test/TestHarness.js:
+        (TestHarness.prototype.expectEmpty): Added.
+        (TestHarness.prototype.expectNotEmpty): Added.
+        (TestHarness.prototype._expectationMessageFormat):
+        (TestHarness.prototype._expectedValueFormat):
+        Add utility function for checking whether the given value is empty:
+         - Array `length === 0`
+         - String `length === 0`
+         - Set `size === 0`
+         - Map `size === 0`
+         - Object `isEmptyObject`
+        Any other type will automatically fail, as non-objects can't be "empty" (e.g. `42`).
+
 2020-01-27  Devin Rousso  <drousso@apple.com>
 
         Web Inspector: don't use `:matches(:before, :after)` after r255059
index 59f3fa3..8391bff 100644 (file)
@@ -125,12 +125,15 @@ localizedStrings["An error occurred trying to read the \u201C%s\u201D table."] =
 localizedStrings["An unexpected error %s occurred."] = "An unexpected error %s occurred.";
 localizedStrings["An unexpected error occurred."] = "An unexpected error occurred.";
 localizedStrings["Angle"] = "Angle";
+localizedStrings["Animation"] = "Animation";
+localizedStrings["Animation %d"] = "Animation %d";
 localizedStrings["Animation Frame %d Canceled"] = "Animation Frame %d Canceled";
 localizedStrings["Animation Frame %d Fired"] = "Animation Frame %d Fired";
 localizedStrings["Animation Frame %d Requested"] = "Animation Frame %d Requested";
 localizedStrings["Animation Frame Canceled"] = "Animation Frame Canceled";
 localizedStrings["Animation Frame Fired"] = "Animation Frame Fired";
 localizedStrings["Animation Frame Requested"] = "Animation Frame Requested";
+localizedStrings["Animation Target"] = "Animation Target";
 localizedStrings["Anonymous Script %d"] = "Anonymous Script %d";
 localizedStrings["Anonymous Scripts"] = "Anonymous Scripts";
 localizedStrings["Anonymous Style Sheet %d"] = "Anonymous Style Sheet %d";
@@ -201,9 +204,11 @@ localizedStrings["CPU"] = "CPU";
 localizedStrings["CPU Usage"] = "CPU Usage";
 localizedStrings["CSP Hash"] = "CSP Hash";
 localizedStrings["CSS Animation"] = "CSS Animation";
+localizedStrings["CSS Animations"] = "CSS Animations";
 localizedStrings["CSS Canvas"] = "CSS Canvas";
 localizedStrings["CSS Changes:"] = "CSS Changes:";
 localizedStrings["CSS Transition"] = "CSS Transition";
+localizedStrings["CSS Transitions"] = "CSS Transitions";
 localizedStrings["CSS canvas \u201C%s\u201D"] = "CSS canvas \u201C%s\u201D";
 localizedStrings["Cached"] = "Cached";
 localizedStrings["Call Frames Truncated"] = "Call Frames Truncated";
@@ -218,6 +223,8 @@ localizedStrings["Canceled"] = "Canceled";
 localizedStrings["Canvas"] = "Canvas";
 localizedStrings["Canvas %d"] = "Canvas %d";
 localizedStrings["Canvas %s"] = "Canvas %s";
+/* Bitmap Renderer is a type of rendering context associated with a <canvas> element */
+localizedStrings["Canvas Context Type Bitmap Renderer"] = "Bitmap Renderer";
 localizedStrings["Canvas Element"] = "Canvas Element";
 localizedStrings["Canvases"] = "Canvases";
 /* Capture screenshot of the selected DOM node */
@@ -587,6 +594,7 @@ localizedStrings["Global Code"] = "Global Code";
 localizedStrings["Global Lexical Environment"] = "Global Lexical Environment";
 localizedStrings["Global Variables"] = "Global Variables";
 localizedStrings["Grammar"] = "Grammar";
+localizedStrings["Graphics"] = "Graphics";
 localizedStrings["Group"] = "Group";
 localizedStrings["Group By Resource"] = "Group By Resource";
 localizedStrings["Group Media Requests"] = "Group Media Requests";
@@ -634,6 +642,7 @@ localizedStrings["Images"] = "Images";
 localizedStrings["Images:"] = "Images:";
 localizedStrings["Immediate Pause Requested"] = "Immediate Pause Requested";
 localizedStrings["Import"] = "Import";
+localizedStrings["Import Recording"] = "Import Recording";
 localizedStrings["Imported"] = "Imported";
 localizedStrings["Imported - %s"] = "Imported - %s";
 localizedStrings["Imported \u2014 %s"] = "Imported \u2014 %s";
@@ -698,6 +707,7 @@ localizedStrings["Local Storage"] = "Local Storage";
 localizedStrings["Local Variables"] = "Local Variables";
 localizedStrings["Located at %s"] = "Located at %s";
 localizedStrings["Location"] = "Location";
+localizedStrings["Log Animation"] = "Log Animation";
 localizedStrings["Log Canvas Context"] = "Log Canvas Context";
 /* Log (print) DOM element to Console */
 localizedStrings["Log Element"] = "Log Element";
@@ -771,6 +781,7 @@ localizedStrings["No Enabled Audits"] = "No Enabled Audits";
 localizedStrings["No Entries"] = "No Entries";
 localizedStrings["No Event Listeners"] = "No Event Listeners";
 localizedStrings["No Filter Results"] = "No Filter Results";
+localizedStrings["No Keyframes"] = "No Keyframes";
 localizedStrings["No Layer Available"] = "No Layer Available";
 localizedStrings["No Overrides"] = "No Overrides";
 localizedStrings["No Parameters"] = "No Parameters";
@@ -782,6 +793,7 @@ localizedStrings["No Response Headers"] = "No Response Headers";
 localizedStrings["No Result"] = "No Result";
 localizedStrings["No Results Found"] = "No Results Found";
 localizedStrings["No Search Results"] = "No Search Results";
+localizedStrings["No Styles"] = "No Styles";
 localizedStrings["No Watch Expressions"] = "No Watch Expressions";
 localizedStrings["No audit selected"] = "No audit selected";
 localizedStrings["No certificate security information."] = "No certificate security information.";
@@ -906,6 +918,8 @@ localizedStrings["Recording"] = "Recording";
 localizedStrings["Recording %d"] = "Recording %d";
 localizedStrings["Recording Error: %s"] = "Recording Error: %s";
 localizedStrings["Recording Timeline Data"] = "Recording Timeline Data";
+/* A type of canvas recording in the Graphics Tab */
+localizedStrings["Recording Type Canvas Bitmap Renderer"] = "Bitmap Renderer";
 localizedStrings["Recording Warning: %s"] = "Recording Warning: %s";
 localizedStrings["Recording stop requested \u2014 %s"] = "Recording stop requested \u2014 %s";
 localizedStrings["Recordings"] = "Recordings";
@@ -914,7 +928,6 @@ localizedStrings["Redirects"] = "Redirects";
 localizedStrings["Reference Issue"] = "Reference Issue";
 localizedStrings["Reflection"] = "Reflection";
 localizedStrings["Refresh"] = "Refresh";
-localizedStrings["Refresh all"] = "Refresh all";
 localizedStrings["Refresh watch expressions"] = "Refresh watch expressions";
 localizedStrings["Region announced in its entirety."] = "Region announced in its entirety.";
 localizedStrings["Regular Expression"] = "Regular Expression";
@@ -1024,6 +1037,8 @@ localizedStrings["Security Origin"] = "Security Origin";
 localizedStrings["Select baseline snapshot"] = "Select baseline snapshot";
 localizedStrings["Select comparison snapshot"] = "Select comparison snapshot";
 localizedStrings["Selected"] = "Selected";
+/* Appears as a label when a given web animation is logged to the Console */
+localizedStrings["Selected Animation"] = "Selected Animation";
 localizedStrings["Selected Canvas Context"] = "Selected Canvas Context";
 /* Selected DOM element */
 localizedStrings["Selected Element"] = "Selected Element";
@@ -1179,6 +1194,8 @@ localizedStrings["These tests serve as a demonstration of the functionality and
 localizedStrings["This Resource came from a Local Resource Override"] = "This Resource came from a Local Resource Override";
 localizedStrings["This action causes no visual change"] = "This action causes no visual change";
 localizedStrings["This action moves the path outside the visible area"] = "This action moves the path outside the visible area";
+localizedStrings["This animation has no duration."] = "This animation has no duration.";
+localizedStrings["This animation has no keyframes."] = "This animation has no keyframes.";
 localizedStrings["This audit is not supported"] = "This audit is not supported";
 localizedStrings["This is an example of how custom result data is shown."] = "This is an example of how custom result data is shown.";
 localizedStrings["This is an example of how errors are shown. The error was thrown manually, but execution errors will appear in the same way."] = "This is an example of how errors are shown. The error was thrown manually, but execution errors will appear in the same way.";
@@ -1291,12 +1308,65 @@ localizedStrings["View Shader"] = "View Shader";
 localizedStrings["Viewport"] = "Viewport";
 localizedStrings["Visible"] = "Visible";
 localizedStrings["Waiting"] = "Waiting";
+localizedStrings["Waiting for animations created by CSS."] = "Waiting for animations created by CSS.";
+localizedStrings["Waiting for animations created by JavaScript."] = "Waiting for animations created by JavaScript.";
 localizedStrings["Waiting for canvas contexts created by script or CSS."] = "Waiting for canvas contexts created by script or CSS.";
 localizedStrings["Waiting for frames\u2026"] = "Waiting for frames\u2026";
+localizedStrings["Waiting for transitions created by CSS."] = "Waiting for transitions created by CSS.";
 localizedStrings["Warning: "] = "Warning: ";
 localizedStrings["Warnings"] = "Warnings";
 localizedStrings["Watch Expressions"] = "Watch Expressions";
 localizedStrings["Waterfall"] = "Waterfall";
+localizedStrings["Web Animation"] = "Web Animation";
+/* Section title for the JavaScript backtrace of the creation of a web animation */
+localizedStrings["Web Animation Backtrace Title"] = "Backtrace";
+/* Label for the cubic-bezier timing function of a web animation */
+localizedStrings["Web Animation Easing Label"] = "Easing";
+/* Section title for information about the effect of a web animation */
+localizedStrings["Web Animation Effect Title"] = "Effect";
+/* Label for the end delay time of a web animation  */
+localizedStrings["Web Animation End Delay Label"] = "End Delay";
+/* Tooltip for section of graph representing delay after a web animation finishes applying styles */
+localizedStrings["Web Animation End Delay Tooltip"] = "End Delay %s";
+/* Indicates that this web animation either does not apply any styles before it begins and after it ends or that it applies to both, depending on it's configuration */
+localizedStrings["Web Animation Fill Mode Auto"] = "Auto";
+/* Indicates that this web animation also applies styles before it begins */
+localizedStrings["Web Animation Fill Mode Backwards"] = "Backwards";
+/* Indicates that this web animation also applies styles before it begins and after it ends */
+localizedStrings["Web Animation Fill Mode Both"] = "Both";
+/* Indicates that this web animation also applies styles after it ends */
+localizedStrings["Web Animation Fill Mode Forwards"] = "Forwards";
+/* Label for the fill mode of a web animation */
+localizedStrings["Web Animation Fill Mode Label"] = "Fill";
+/* Indicates that this web animation does not apply any styles before it begins and after it ends */
+localizedStrings["Web Animation Fill Mode None"] = "None";
+/* Section title for information about a web animation */
+localizedStrings["Web Animation Identity Title"] = "Identity";
+/* Label for the number of iterations of a web animation */
+localizedStrings["Web Animation Iteration Count Label"] = "Iterations";
+/* Label for the time duration of each iteration of a web animation */
+localizedStrings["Web Animation Iteration Duration Label"] = "Duration";
+/* Label for the number describing which iteration a web animation should start at */
+localizedStrings["Web Animation Iteration Start Label"] = "Start";
+/* Section title for information about the keyframes of a web animation */
+localizedStrings["Web Animation Keyframes Title"] = "Keyframes";
+/* Indicates that the playback direction of this web animation alternates between normal and reversed on each iteration */
+localizedStrings["Web Animation Playback Direction Alternate"] = "Alternate";
+/* Indicates that the playback direction of this web animation alternates between reversed and normal on each iteration */
+localizedStrings["Web Animation Playback Direction Alternate Reverse"] = "Alternate Reverse";
+/* Label for the playback direction of a web animation */
+localizedStrings["Web Animation Playback Direction Label"] = "Direction";
+/* Indicates that the playback direction of this web animation is normal (e.g. forwards) */
+localizedStrings["Web Animation Playback Direction Normal"] = "Normal";
+/* Indicates that the playback direction of this web animation is reversed (e.g. backwards) */
+localizedStrings["Web Animation Playback Direction Reverse"] = "Reverse";
+/* Label for the start delay time of a web animation  */
+localizedStrings["Web Animation Start Delay Label"] = "Start Delay";
+/* Tooltip for section of graph representing delay before a web animation begins applying styles */
+localizedStrings["Web Animation Start Delay Tooltip"] = "Start Delay %s";
+/* Label for the current DOM node target of a web animation */
+localizedStrings["Web Animation Target Label"] = "Target";
+localizedStrings["Web Animations"] = "Web Animations";
 localizedStrings["Web Inspector"] = "Web Inspector";
 localizedStrings["Web Inspector Reference"] = "Web Inspector Reference";
 localizedStrings["Web Page"] = "Web Page";
index 8271e13..eace126 100644 (file)
@@ -123,6 +123,7 @@ WI.loaded = function()
         WI.workerManager = new WI.WorkerManager,
         WI.domDebuggerManager = new WI.DOMDebuggerManager,
         WI.canvasManager = new WI.CanvasManager,
+        WI.animationManager = new WI.AnimationManager,
     ];
 
     // Register for events.
@@ -147,13 +148,13 @@ WI.loaded = function()
         WI.SourcesTabContentView.Type,
         WI.TimelineTabContentView.Type,
         WI.StorageTabContentView.Type,
-        WI.CanvasTabContentView.Type,
+        WI.GraphicsTabContentView.Type,
         WI.AuditTabContentView.Type,
         WI.ConsoleTabContentView.Type,
     ]);
     WI._selectedTabIndexSetting = new WI.Setting("selected-tab-index", 0);
 
-    // Replace the Debugger/Resources Tab with the Sources Tab.
+    // FIXME: <https://webkit.org/b/205826> Web Inspector: remove legacy code for replacing the Resources Tab and Debugger Tab with the Sources Tab
     let debuggerIndex = WI._openTabsSetting.value.indexOf("debugger");
     let resourcesIndex = WI._openTabsSetting.value.indexOf("resources");
     if (debuggerIndex >= 0 || resourcesIndex >= 0) {
@@ -173,6 +174,13 @@ WI.loaded = function()
             WI._selectedTabIndexSetting.value = sourcesIndex;
     }
 
+    // FIXME: <https://webkit.org/b/205827> Web Inspector: remove legacy code for replacing the Canvas Tab with the Graphics Tab
+    let canvasIndex = WI._openTabsSetting.value.indexOf("canvas");
+    if (canvasIndex >= 0) {
+        WI._openTabsSetting.value.splice(canvasIndex, 1, WI.GraphicsTabContentView.Type);
+        WI._openTabsSetting.save();
+    }
+
     // State.
     WI.printStylesEnabled = false;
     WI.setZoomFactor(WI.settings.zoomFactor.value);
@@ -465,7 +473,7 @@ WI.contentLoaded = function()
         WI.SourcesTabContentView,
         WI.TimelineTabContentView,
         WI.StorageTabContentView,
-        WI.CanvasTabContentView,
+        WI.GraphicsTabContentView,
         WI.LayersTabContentView,
         WI.AuditTabContentView,
         WI.ConsoleTabContentView,
@@ -1223,11 +1231,8 @@ WI.tabContentViewClassForRepresentedObject = function(representedObject)
         || representedObject instanceof WI.AuditTestCaseResult || representedObject instanceof WI.AuditTestGroupResult)
         return WI.AuditTabContentView;
 
-    if (representedObject instanceof WI.CanvasCollection)
-        return WI.CanvasTabContentView;
-
-    if (representedObject instanceof WI.Recording)
-        return WI.CanvasTabContentView;
+    if (representedObject instanceof WI.Canvas || representedObject instanceof WI.ShaderProgram || representedObject instanceof WI.Recording || representedObject instanceof WI.Animation)
+        return WI.GraphicsTabContentView;
 
     return null;
 };
diff --git a/Source/WebInspectorUI/UserInterface/Controllers/AnimationManager.js b/Source/WebInspectorUI/UserInterface/Controllers/AnimationManager.js
new file mode 100644 (file)
index 0000000..ffb412c
--- /dev/null
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2019 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// FIXME: AnimationManager lacks advanced multi-target support. (Animations per-target)
+
+WI.AnimationManager = class AnimationManager
+{
+    constructor()
+    {
+        this._enabled = false;
+        this._animationCollection = new WI.AnimationCollection;
+        this._animationIdMap = new Map;
+
+        WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._handleMainResourceDidChange, this);
+    }
+
+    // Agent
+
+    get domains() { return ["Animation"]; }
+
+    activateExtraDomain(domain)
+    {
+        console.assert(domain === "Animation");
+
+        for (let target of WI.targets)
+            this.initializeTarget(target);
+    }
+
+    // Target
+
+    initializeTarget(target)
+    {
+        if (!this._enabled)
+            return;
+
+        // COMPATIBILITY (iOS 13.1): Animation.enable did not exist yet.
+        if (target.hasCommand("Animation.enable"))
+            target.AnimationAgent.enable();
+    }
+
+    // Public
+
+    get animationCollection() { return this._animationCollection; }
+
+    get supported()
+    {
+        // COMPATIBILITY (iOS 13.1): Animation.enable did not exist yet.
+        return InspectorBackend.hasCommand("Animation.enable");
+    }
+
+    enable()
+    {
+        console.assert(!this._enabled);
+
+        this._enabled = true;
+
+        for (let target of WI.targets)
+            this.initializeTarget(target);
+    }
+
+    disable()
+    {
+        console.assert(this._enabled);
+
+        for (let target of WI.targets) {
+            // COMPATIBILITY (iOS 13.1): Animation.disable did not exist yet.
+            if (target.hasCommand("Animation.disable"))
+                target.AnimationAgent.disable();
+        }
+
+        this._animationCollection.clear();
+        this._animationIdMap.clear();
+
+        this._enabled = false;
+    }
+
+    // AnimationObserver
+
+    animationCreated(animationPayload)
+    {
+        console.assert(!this._animationIdMap.has(animationPayload.animationId), `Animation already exists with id ${animationPayload.animationId}.`);
+
+        let animation = WI.Animation.fromPayload(animationPayload);
+        this._animationCollection.add(animation);
+        this._animationIdMap.set(animation.animationId, animation);
+    }
+
+    effectChanged(animationId, effect)
+    {
+        let animation = this._animationIdMap.get(animationId);
+        console.assert(animation);
+        if (!animation)
+            return;
+
+        animation.effectChanged(effect);
+    }
+
+    targetChanged(animationId, effect)
+    {
+        let animation = this._animationIdMap.get(animationId);
+        console.assert(animation);
+        if (!animation)
+            return;
+
+        animation.targetChanged(effect);
+    }
+
+    animationDestroyed(animationId)
+    {
+        let animation = this._animationIdMap.take(animationId);
+        console.assert(animation);
+        if (!animation)
+            return;
+
+        this._animationCollection.remove(animation);
+    }
+
+    // Private
+
+    _handleMainResourceDidChange(event)
+    {
+        console.assert(event.target instanceof WI.Frame);
+        if (!event.target.isMainFrame())
+            return;
+
+        WI.Animation.resetUniqueDisplayNameNumbers();
+
+        this._animationCollection.clear();
+        this._animationIdMap.clear();
+    }
+};
index 135c166..dbb8e6f 100644 (file)
@@ -32,6 +32,7 @@ WI.CanvasManager = class CanvasManager extends WI.Object
         super();
 
         this._enabled = false;
+        this._canvasCollection = new WI.CanvasCollection;
         this._canvasIdentifierMap = new Map;
         this._shaderProgramIdentifierMap = new Map;
         this._savedRecordings = new Set;
@@ -76,13 +77,9 @@ WI.CanvasManager = class CanvasManager extends WI.Object
 
     // Public
 
+    get canvasCollection() { return this._canvasCollection; }
     get savedRecordings() { return this._savedRecordings; }
 
-    get canvases()
-    {
-        return Array.from(this._canvasIdentifierMap.values());
-    }
-
     get shaderPrograms()
     {
         return Array.from(this._shaderProgramIdentifierMap.values());
@@ -133,6 +130,7 @@ WI.CanvasManager = class CanvasManager extends WI.Object
                 target.CanvasAgent.disable();
         }
 
+        this._canvasCollection.clear();
         this._canvasIdentifierMap.clear();
         this._shaderProgramIdentifierMap.clear();
         this._savedRecordings.clear();
@@ -161,9 +159,8 @@ WI.CanvasManager = class CanvasManager extends WI.Object
         console.assert(!this._canvasIdentifierMap.has(canvasPayload.canvasId), `Canvas already exists with id ${canvasPayload.canvasId}.`);
 
         let canvas = WI.Canvas.fromPayload(canvasPayload);
+        this._canvasCollection.add(canvas);
         this._canvasIdentifierMap.set(canvas.identifier, canvas);
-
-        this.dispatchEventToListeners(WI.CanvasManager.Event.CanvasAdded, {canvas});
     }
 
     canvasRemoved(canvasIdentifier)
@@ -173,7 +170,14 @@ WI.CanvasManager = class CanvasManager extends WI.Object
         if (!canvas)
             return;
 
-        this._removeCanvas(canvas);
+        this._saveRecordings(canvas);
+
+        this._canvasCollection.remove(canvas);
+
+        for (let program of canvas.shaderProgramCollection)
+            this._shaderProgramIdentifierMap.delete(program.identifier);
+
+        canvas.shaderProgramCollection.clear();
     }
 
     canvasMemoryChanged(canvasIdentifier, memoryCost)
@@ -275,21 +279,14 @@ WI.CanvasManager = class CanvasManager extends WI.Object
 
     // Private
 
-    _removeCanvas(canvas)
+    _saveRecordings(canvas)
     {
-        for (let program of canvas.shaderProgramCollection)
-            this._shaderProgramIdentifierMap.delete(program.identifier);
-
-        canvas.shaderProgramCollection.clear();
-
         for (let recording of canvas.recordingCollection) {
             recording.source = null;
             recording.createDisplayName(recording.displayName);
             this._savedRecordings.add(recording);
             this.dispatchEventToListeners(WI.CanvasManager.Event.RecordingSaved, {recording});
         }
-
-        this.dispatchEventToListeners(WI.CanvasManager.Event.CanvasRemoved, {canvas});
     }
 
     _mainResourceDidChange(event)
@@ -301,15 +298,14 @@ WI.CanvasManager = class CanvasManager extends WI.Object
         WI.Canvas.resetUniqueDisplayNameNumbers();
 
         for (let canvas of this._canvasIdentifierMap.values())
-            this._removeCanvas(canvas);
+            this._saveRecordings(canvas);
 
-        this._shaderProgramIdentifierMap.clear();
+        this._canvasCollection.clear();
         this._canvasIdentifierMap.clear();
+        this._shaderProgramIdentifierMap.clear();
     }
 };
 
 WI.CanvasManager.Event = {
-    CanvasAdded: "canvas-manager-canvas-was-added",
-    CanvasRemoved: "canvas-manager-canvas-was-removed",
     RecordingSaved: "canvas-manager-recording-saved",
 };
index 1a8df68..7185558 100644 (file)
@@ -323,7 +323,7 @@ WI.DOMManager = class DOMManager extends WI.Object
 
     nodeForId(nodeId)
     {
-        return this._idToDOMNode[nodeId];
+        return this._idToDOMNode[nodeId] || null;
     }
 
     _documentUpdated()
index 7633de1..729696f 100644 (file)
@@ -781,7 +781,7 @@ WI.TimelineManager = class TimelineManager extends WI.Object
                 return;
             }
 
-            let domNode = WI.domManager.nodeForId(event.nodeId) || null;
+            let domNode = WI.domManager.nodeForId(event.nodeId);
             console.assert(domNode);
 
             record = new WI.MediaTimelineRecord(eventType, domNode, details);
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright © 2017 Apple Inc. All rights reserved. -->
+<!-- Copyright © 2019 Apple Inc. All rights reserved. -->
 <svg xmlns="http://www.w3.org/2000/svg" id="root" version="1.1" viewBox="0 0 16 16">
     <rect x="1.5" y="2.5" width="13" height="11" fill="none" stroke="currentColor"/>
     <polygon points="11 8 9 11 6 7 3 11 3 12 13 12 13 11 11 8"/>
index dd1d4c6..5b85652 100644 (file)
@@ -30,6 +30,9 @@
 
     <link rel="stylesheet" href="External/CodeMirror/codemirror.css">
 
+    <link rel="stylesheet" href="Views/AnimationCollectionContentView.css">
+    <link rel="stylesheet" href="Views/AnimationContentView.css">
+    <link rel="stylesheet" href="Views/AnimationDetailsSidebarPanel.css">
     <link rel="stylesheet" href="Views/ApplicationCacheFrameContentView.css">
     <link rel="stylesheet" href="Views/AuditNavigationSidebarPanel.css">
     <link rel="stylesheet" href="Views/AuditTestCaseContentView.css">
@@ -56,7 +59,6 @@
     <link rel="stylesheet" href="Views/CanvasDetailsSidebarPanel.css">
     <link rel="stylesheet" href="Views/CanvasOverviewContentView.css">
     <link rel="stylesheet" href="Views/CanvasSidebarPanel.css">
-    <link rel="stylesheet" href="Views/CanvasTabContentView.css">
     <link rel="stylesheet" href="Views/ChangesDetailsSidebarPanel.css">
     <link rel="stylesheet" href="Views/ChartDetailsSectionRow.css">
     <link rel="stylesheet" href="Views/CheckboxNavigationItem.css">
     <link rel="stylesheet" href="Views/GoToLineDialog.css">
     <link rel="stylesheet" href="Views/GradientEditor.css">
     <link rel="stylesheet" href="Views/GradientSlider.css">
+    <link rel="stylesheet" href="Views/GraphicsOverviewContentView.css">
+    <link rel="stylesheet" href="Views/GraphicsTabContentView.css">
     <link rel="stylesheet" href="Views/HeapAllocationsTimelineOverviewGraph.css">
     <link rel="stylesheet" href="Views/HeapAllocationsTimelineView.css">
     <link rel="stylesheet" href="Views/HeapSnapshotInstancesContentView.css">
     <script src="Protocol/WorkerObserver.js"></script>
 
     <script src="Models/BreakpointAction.js"></script>
+    <script src="Models/Collection.js"></script>
     <script src="Models/ConsoleMessage.js"></script>
     <script src="Models/Instrument.js"></script>
     <script src="Models/SourceCode.js"></script>
     <script src="Models/Script.js"></script>
     <script src="Models/LocalScript.js"></script>
 
+    <script src="Models/Animation.js"></script>
+    <script src="Models/AnimationCollection.js"></script>
     <script src="Models/ApplicationCacheFrame.js"></script>
     <script src="Models/ApplicationCacheManifest.js"></script>
     <script src="Models/BackForwardEntry.js"></script>
     <script src="Models/CallingContextTree.js"></script>
     <script src="Models/CallingContextTreeNode.js"></script>
     <script src="Models/Canvas.js"></script>
-    <script src="Models/Collection.js"></script>
     <script src="Models/CollectionEntry.js"></script>
     <script src="Models/CollectionEntryPreview.js"></script>
     <script src="Models/CollectionTypes.js"></script>
     <script src="Views/StyleDetailsPanel.js"></script>
 
     <script src="Views/AuditTabContentView.js"></script>
-    <script src="Views/CanvasTabContentView.js"></script>
     <script src="Views/ConsoleTabContentView.js"></script>
     <script src="Views/ElementsTabContentView.js"></script>
+    <script src="Views/GraphicsTabContentView.js"></script>
     <script src="Views/LayersTabContentView.js"></script>
     <script src="Views/ResourceTreeElement.js"></script>
     <script src="Views/ScriptTreeElement.js"></script>
 
     <script src="Views/ActivateButtonNavigationItem.js"></script>
     <script src="Views/ActivateButtonToolbarItem.js"></script>
+    <script src="Views/AnimationCollectionContentView.js"></script>
+    <script src="Views/AnimationContentView.js"></script>
+    <script src="Views/AnimationDetailsSidebarPanel.js"></script>
     <script src="Views/ApplicationCacheDetailsSidebarPanel.js"></script>
     <script src="Views/ApplicationCacheFrameContentView.js"></script>
     <script src="Views/ApplicationCacheFrameTreeElement.js"></script>
     <script src="Views/GoToLineDialog.js"></script>
     <script src="Views/GradientEditor.js"></script>
     <script src="Views/GradientSlider.js"></script>
+    <script src="Views/GraphicsOverviewContentView.js"></script>
     <script src="Views/HeapAllocationsTimelineDataGridNode.js"></script>
     <script src="Views/HeapAllocationsTimelineDataGridNodePathComponent.js"></script>
     <script src="Views/HeapAllocationsTimelineOverviewGraph.js"></script>
     <script src="Controllers/Annotator.js"></script>
     <script src="Controllers/CodeMirrorEditingController.js"></script>
 
+    <script src="Controllers/AnimationManager.js"></script>
     <script src="Controllers/AuditManager.js"></script>
     <script src="Controllers/ApplicationCacheManager.js"></script>
     <script src="Controllers/BasicBlockAnnotator.js"></script>
diff --git a/Source/WebInspectorUI/UserInterface/Models/Animation.js b/Source/WebInspectorUI/UserInterface/Models/Animation.js
new file mode 100644 (file)
index 0000000..2365de5
--- /dev/null
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2020 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+WI.Animation = class Animation extends WI.Object
+{
+    constructor(animationId, {cssAnimationName, cssTransitionProperty, effect, backtrace} = {})
+    {
+        super();
+
+        console.assert(animationId);
+        console.assert((!cssAnimationName && !cssTransitionProperty) || !!cssAnimationName !== !!cssTransitionProperty);
+
+        this._animationId = animationId;
+
+        this._cssAnimationName = cssAnimationName || null;
+        this._cssTransitionProperty = cssTransitionProperty || null;
+        this._updateEffect(effect);
+        this._backtrace = backtrace || [];
+
+        this._effectTarget = undefined;
+        this._requestEffectTargetCallbacks = null;
+    }
+
+    // Static
+
+    static fromPayload(payload)
+    {
+        return new WI.Animation(payload.animationId, {
+            cssAnimationName: payload.cssAnimationName,
+            cssTransitionProperty: payload.cssTransitionProperty,
+            effect: payload.effect,
+            backtrace: Array.isArray(payload.backtrace) ? payload.backtrace.map((item) => WI.CallFrame.fromPayload(WI.mainTarget, item)) : [],
+        });
+    }
+
+    static displayNameForAnimationType(animationType, plural)
+    {
+        switch (animationType) {
+        case WI.Animation.Type.WebAnimation:
+            return plural ? WI.UIString("Web Animations") : WI.UIString("Web Animation");
+        case WI.Animation.Type.CSSAnimation:
+            return plural ? WI.UIString("CSS Animations") : WI.UIString("CSS Animation");
+        case WI.Animation.Type.CSSTransition:
+            return plural ? WI.UIString("CSS Transitions") : WI.UIString("CSS Transition");
+        }
+
+        console.assert(false, "Unknown animation type", animationType);
+        return null;
+    }
+
+    static displayNameForPlaybackDirection(playbackDirection)
+    {
+        switch (playbackDirection) {
+        case WI.Animation.PlaybackDirection.Normal:
+            return WI.UIString("Normal", "Web Animation Playback Direction Normal", "Indicates that the playback direction of this web animation is normal (e.g. forwards)");
+        case WI.Animation.PlaybackDirection.Reverse:
+            return WI.UIString("Reverse", "Web Animation Playback Direction Reverse", "Indicates that the playback direction of this web animation is reversed (e.g. backwards)");
+        case WI.Animation.PlaybackDirection.Alternate:
+            return WI.UIString("Alternate", "Web Animation Playback Direction Alternate", "Indicates that the playback direction of this web animation alternates between normal and reversed on each iteration");
+        case WI.Animation.PlaybackDirection.AlternateReverse:
+            return WI.UIString("Alternate Reverse", "Web Animation Playback Direction Alternate Reverse", "Indicates that the playback direction of this web animation alternates between reversed and normal on each iteration");
+        }
+
+        console.assert(false, "Unknown playback direction", playbackDirection);
+        return null;
+    }
+
+    static displayNameForFillMode(fillMode)
+    {
+        switch (fillMode) {
+        case WI.Animation.FillMode.None:
+            return WI.UIString("None", "Web Animation Fill Mode None", "Indicates that this web animation does not apply any styles before it begins and after it ends");
+        case WI.Animation.FillMode.Forwards:
+            return WI.UIString("Forwards", "Web Animation Fill Mode Forwards", "Indicates that this web animation also applies styles after it ends");
+        case WI.Animation.FillMode.Backwards:
+            return WI.UIString("Backwards", "Web Animation Fill Mode Backwards", "Indicates that this web animation also applies styles before it begins");
+        case WI.Animation.FillMode.Both:
+            return WI.UIString("Both", "Web Animation Fill Mode Both", "Indicates that this web animation also applies styles before it begins and after it ends");
+        case WI.Animation.FillMode.Auto:
+            return WI.UIString("Auto", "Web Animation Fill Mode Auto", "Indicates that this web animation either does not apply any styles before it begins and after it ends or that it applies to both, depending on it's configuration");
+        }
+
+        console.assert(false, "Unknown fill mode", fillMode);
+        return null;
+    }
+
+    static resetUniqueDisplayNameNumbers()
+    {
+        WI.Animation._nextUniqueDisplayNameNumber = 1;
+    }
+
+    // Public
+
+    get animationId() { return this._animationId; }
+    get backtrace() { return this._backtrace; }
+
+    get animationType()
+    {
+        if (this._cssAnimationName)
+            return WI.Animation.Type.CSSAnimation;
+        if (this._cssTransitionProperty)
+            return WI.Animation.Type.CSSTransition;
+        return WI.Animation.Type.WebAnimation;
+    }
+
+    get startDelay()
+    {
+        return "startDelay" in this._effect ? this._effect.startDelay : NaN;
+    }
+
+    get endDelay()
+    {
+        return "endDelay" in this._effect ? this._effect.endDelay : NaN;
+    }
+
+    get iterationCount()
+    {
+        return "iterationCount" in this._effect ? this._effect.iterationCount : NaN;
+    }
+
+    get iterationStart()
+    {
+        return "iterationStart" in this._effect ? this._effect.iterationStart : NaN;
+    }
+
+    get iterationDuration()
+    {
+        return "iterationDuration" in this._effect ? this._effect.iterationDuration : NaN;
+    }
+
+    get timingFunction()
+    {
+        return "timingFunction" in this._effect ? this._effect.timingFunction : null;
+    }
+
+    get playbackDirection()
+    {
+        return "playbackDirection" in this._effect ? this._effect.playbackDirection : null;
+    }
+
+    get fillMode()
+    {
+        return "fillMode" in this._effect ? this._effect.fillMode : null;
+    }
+
+    get keyframes()
+    {
+        return "keyframes" in this._effect ? this._effect.keyframes : [];
+    }
+
+    get displayName()
+    {
+        if (this._cssAnimationName)
+            return this._cssAnimationName;
+
+        if (this._cssTransitionProperty)
+            return this._cssTransitionProperty;
+
+        if (!this._uniqueDisplayNameNumber)
+            this._uniqueDisplayNameNumber = WI.Animation._nextUniqueDisplayNameNumber++;
+        return WI.UIString("Animation %d").format(this._uniqueDisplayNameNumber);
+    }
+
+    requestEffectTarget(callback)
+    {
+        if (this._effectTarget !== undefined) {
+            callback(this._effectTarget);
+            return;
+        }
+
+        if (this._requestEffectTargetCallbacks) {
+            this._requestEffectTargetCallbacks.push(callback);
+            return;
+        }
+
+        this._requestEffectTargetCallbacks = [callback];
+
+        WI.domManager.ensureDocument();
+
+        let target = WI.assumingMainTarget();
+        target.AnimationAgent.requestEffectTarget(this._animationId, (error, nodeId) => {
+            this._effectTarget = !error ? WI.domManager.nodeForId(nodeId) : null;
+
+            for (let requestEffectTargetCallback of this._requestEffectTargetCallbacks)
+                requestEffectTargetCallback(this._effectTarget);
+
+            this._requestEffectTargetCallbacks = null;
+        });
+    }
+
+    // AnimationManager
+
+    effectChanged(effect)
+    {
+        this._updateEffect(effect);
+    }
+
+    targetChanged()
+    {
+        this._effectTarget = undefined;
+
+        this.dispatchEventToListeners(WI.Animation.Event.TargetChanged);
+    }
+
+    // Private
+
+    _updateEffect(effect)
+    {
+        this._effect = effect || {};
+
+        if (this._effect.timingFunction)
+            this._effect.timingFunction = WI.CubicBezier.fromString(this._effect.timingFunction);
+
+        if (this._effect.keyframes) {
+            for (let keyframe of this._effect.keyframes) {
+                if (keyframe.easing)
+                    keyframe.easing = WI.CubicBezier.fromString(keyframe.easing);
+
+                if (keyframe.style)
+                    keyframe.style = keyframe.style.replaceAll(/;\s+/g, ";\n");
+            }
+        }
+
+        this.dispatchEventToListeners(WI.Animation.Event.EffectChanged);
+    }
+};
+
+WI.Animation._nextUniqueDisplayNameNumber = 1;
+
+WI.Animation.Type = {
+    WebAnimation: "web-animation",
+    CSSAnimation: "css-animation",
+    CSSTransition: "css-transition",
+};
+
+WI.Animation.PlaybackDirection = {
+    Normal: "normal",
+    Reverse: "reverse",
+    Alternate: "alternate",
+    AlternateReverse: "alternate-reverse",
+};
+
+WI.Animation.FillMode = {
+    None: "none",
+    Forwards: "forwards",
+    Backwards: "backwards",
+    Both: "both",
+    Auto: "auto",
+};
+
+WI.Animation.Event = {
+    EffectChanged: "animation-effect-changed",
+    TargetChanged: "animation-target-changed",
+};
diff --git a/Source/WebInspectorUI/UserInterface/Models/AnimationCollection.js b/Source/WebInspectorUI/UserInterface/Models/AnimationCollection.js
new file mode 100644 (file)
index 0000000..9a28a7b
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2020 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+WI.AnimationCollection = class AnimationCollection extends WI.Collection
+{
+    constructor(animationType)
+    {
+        console.assert(!animationType || Object.values(WI.Animation.Type).includes(animationType));
+
+        super();
+
+        this._animationType = animationType || null;
+
+        if (!this._animationType)
+            this._animationCollectionForTypeMap = null;
+    }
+
+    // Public
+
+    get animationType() { return this._animationType; }
+
+    get displayName()
+    {
+        if (this._animationType) {
+            const plural = true;
+            return WI.Animation.displayNameForType(this._animationType, plural);
+        }
+
+        return WI.UIString("Web Animations");
+    }
+
+    objectIsRequiredType(object)
+    {
+        if (!(object instanceof WI.Animation))
+            return false;
+
+        return !this._animationType || object.animationType === this._animationType;
+    }
+
+    animationCollectionForType(animationType)
+    {
+        console.assert(Object.values(WI.Animation.Type).includes(animationType));
+
+        if (this._animationType) {
+            console.assert(animationType === this._animationType);
+            return this;
+        }
+
+        if (!this._animationCollectionForTypeMap)
+            this._animationCollectionForTypeMap = new Map;
+
+        let animationCollectionForType = this._animationCollectionForTypeMap.get(animationType);
+        if (!animationCollectionForType) {
+            animationCollectionForType = new WI.AnimationCollection(animationType);
+            this._animationCollectionForTypeMap.set(animationType, animationCollectionForType);
+        }
+        return animationCollectionForType;
+    }
+
+    // Protected
+
+    itemAdded(item)
+    {
+        super.itemAdded(item);
+
+        if (!this._animationType) {
+            let animationCollectionForType = this.animationCollectionForType(item.animationType);
+            animationCollectionForType.add(item);
+        }
+    }
+
+    itemRemoved(item)
+    {
+        if (!this._animationType) {
+            let animationCollectionForType = this.animationCollectionForType(item.animationType);
+            animationCollectionForType.remove(item);
+        }
+
+        super.itemRemoved(item);
+    }
+
+    itemsCleared(items)
+    {
+        if (this._animationCollectionForTypeMap) {
+            for (let animationCollectionForType of this._animationCollectionForTypeMap.values())
+                animationCollectionForType.clear();
+        }
+
+        super.itemsCleared(items);
+    }
+};
index c9cc396..31d627d 100644 (file)
@@ -146,7 +146,7 @@ WI.AuditTestCaseResult = class AuditTestCaseResult extends WI.AuditTestResultBas
                             try {
                                 nodeId = await documentNode.querySelector(domNodeString);
                             } catch { }
-                            return WI.domManager.nodeForId(nodeId) || null;
+                            return WI.domManager.nodeForId(nodeId);
                         }));
                     }
                 }
index 62ba264..ff71af6 100644 (file)
@@ -97,7 +97,7 @@ WI.Canvas = class Canvas extends WI.Object
         case WI.Canvas.ContextType.Canvas2D:
             return WI.UIString("2D");
         case WI.Canvas.ContextType.BitmapRenderer:
-            return WI.unlocalizedString("Bitmap Renderer");
+            return WI.UIString("Bitmap Renderer", "Canvas Context Type Bitmap Renderer", "Bitmap Renderer is a type of rendering context associated with a <canvas> element");
         case WI.Canvas.ContextType.WebGL:
             return WI.unlocalizedString("WebGL");
         case WI.Canvas.ContextType.WebGL2:
@@ -106,9 +106,10 @@ WI.Canvas = class Canvas extends WI.Object
             return WI.unlocalizedString("Web GPU");
         case WI.Canvas.ContextType.WebMetal:
             return WI.unlocalizedString("WebMetal");
-        default:
-            console.error("Invalid canvas context type", contextType);
         }
+
+        console.assert(false, "Unknown canvas context type", contextType);
+        return null;
     }
 
     static resetUniqueDisplayNameNumbers()
index 1f8dd7e..513ef76 100644 (file)
@@ -148,6 +148,23 @@ WI.Recording = class Recording extends WI.Object
         return new WI.Recording(payload.version, type, payload.initialState, frames, payload.data);
     }
 
+    static displayNameForRecordingType(recordingType)
+    {
+        switch (recordingType) {
+        case Recording.Type.Canvas2D:
+            return WI.UIString("2D");
+        case Recording.Type.CanvasBitmapRenderer:
+            return WI.UIString("Bitmap Renderer", "Recording Type Canvas Bitmap Renderer", "A type of canvas recording in the Graphics Tab");
+        case Recording.Type.CanvasWebGL:
+            return WI.unlocalizedString("WebGL");
+        case Recording.Type.CanvasWebGL2:
+            return WI.unlocalizedString("WebGL2");
+        }
+
+        console.assert(false, "Unknown recording type", recordingType);
+        return null;
+    }
+
     static displayNameForSwizzleType(swizzleType)
     {
         switch (swizzleType) {
index b1195ed..7e56e8b 100644 (file)
@@ -27,6 +27,26 @@ WI.AnimationObserver = class AnimationObserver extends InspectorBackend.Dispatch
 {
     // Events defined by the "Animation" domain.
 
+    animationCreated(animation)
+    {
+        WI.animationManager.animationCreated(animation);
+    }
+
+    effectChanged(animationId, effect)
+    {
+        WI.animationManager.effectChanged(animationId, effect);
+    }
+
+    targetChanged(animationId)
+    {
+        WI.animationManager.targetChanged(animationId);
+    }
+
+    animationDestroyed(animationId)
+    {
+        WI.animationManager.animationDestroyed(animationId);
+    }
+
     trackingStart(timestamp)
     {
         WI.timelineManager.animationTrackingStarted(timestamp);
index dd00af5..2cba564 100644 (file)
@@ -188,6 +188,25 @@ WI.RemoteObject = class RemoteObject
         target.CanvasAgent.resolveContext(canvas.identifier, objectGroup, wrapCallback);
     }
 
+    static resolveAnimation(animation, objectGroup, callback)
+    {
+        console.assert(typeof callback === "function");
+
+        function wrapCallback(error, object) {
+            if (error || !object)
+                callback(null);
+            else
+                callback(WI.RemoteObject.fromPayload(object, WI.mainTarget));
+        }
+
+        let target = WI.assumingMainTarget();
+
+        // COMPATIBILITY (iOS 13.1): Animation.resolveAnimation did not exist yet.
+        console.assert(target.hasCommand("Animation.resolveAnimation"));
+
+        target.AnimationAgent.resolveAnimation(animation.animationId, objectGroup, wrapCallback);
+    }
+
     // Public
 
     get target()
index 1d445a0..0ba560b 100644 (file)
     <script src="Protocol/WorkerObserver.js"></script>
 
     <script src="Models/BreakpointAction.js"></script>
+    <script src="Models/Collection.js"></script>
     <script src="Models/ConsoleMessage.js"></script>
     <script src="Models/Instrument.js"></script>
     <script src="Models/SourceCode.js"></script>
     <script src="Models/Script.js"></script>
     <script src="Models/LocalScript.js"></script>
 
+    <script src="Models/Animation.js"></script>
+    <script src="Models/AnimationCollection.js"></script>
     <script src="Models/Breakpoint.js"></script>
     <script src="Models/CPUInstrument.js"></script>
     <script src="Models/CPUTimeline.js"></script>
     <script src="Models/CallingContextTree.js"></script>
     <script src="Models/CallingContextTreeNode.js"></script>
     <script src="Models/Canvas.js"></script>
-    <script src="Models/Collection.js"></script>
     <script src="Models/CollectionEntry.js"></script>
     <script src="Models/CollectionEntryPreview.js"></script>
     <script src="Models/CollectionTypes.js"></script>
     <script src="Proxies/HeapSnapshotProxy.js"></script>
     <script src="Proxies/HeapSnapshotWorkerProxy.js"></script>
 
+    <script src="Controllers/AnimationManager.js"></script>
     <script src="Controllers/AuditManager.js"></script>
     <script src="Controllers/ApplicationCacheManager.js"></script>
     <script src="Controllers/BreakpointLogMessageLexer.js"></script>
index 3c43ae7..006447a 100644 (file)
@@ -70,6 +70,7 @@ WI.loaded = function()
         WI.workerManager = new WI.WorkerManager,
         WI.domDebuggerManager = new WI.DOMDebuggerManager,
         WI.canvasManager = new WI.CanvasManager,
+        WI.animationManager = new WI.AnimationManager,
     ];
 
     // Register for events.
@@ -91,6 +92,7 @@ WI.loaded = function()
 WI.contentLoaded = function()
 {
     // Things that would normally get called by the UI, that we still want to do in tests.
+    WI.animationManager.enable();
     WI.applicationCacheManager.enable();
     WI.canvasManager.enable();
     WI.databaseManager.enable();
@@ -181,6 +183,7 @@ WI.updateVisibilityState = () => {};
             get() { return WI.mainTarget._agents[domainName]; },
         });
     }
+    makeAgentGetter("Animation");
     makeAgentGetter("Audit");
     makeAgentGetter("ApplicationCache");
     makeAgentGetter("CPUProfiler");
index d447b89..d2e99af 100644 (file)
@@ -131,6 +131,46 @@ TestHarness = class TestHarness extends WI.Object
         this._expect(TestHarness.ExpectationType.False, !actual, message, actual);
     }
 
+    expectEmpty(actual, message)
+    {
+        if (Array.isArray(actual) || typeof actual === "string") {
+            this._expect(TestHarness.ExpectationType.Empty, !actual.length, message, actual);
+            return;
+        }
+
+        if (actual instanceof Set || actual instanceof Map) {
+            this._expect(TestHarness.ExpectationType.Empty, !actual.size, message, actual);
+            return;
+        }
+
+        if (typeof actual === "object") {
+            this._expect(TestHarness.ExpectationType.Empty, isEmptyObject(actual), message, actual);
+            return;
+        }
+
+        this.fail("expectEmpty should not be called with a non-object:\n    Actual: " + this._expectationValueAsString(actual));
+    }
+
+    expectNotEmpty(actual, message)
+    {
+        if (Array.isArray(actual) || typeof actual === "string") {
+            this._expect(TestHarness.ExpectationType.NotEmpty, !!actual.length, message, actual);
+            return;
+        }
+
+        if (actual instanceof Set || actual instanceof Map) {
+            this._expect(TestHarness.ExpectationType.NotEmpty, !!actual.size, message, actual);
+            return;
+        }
+
+        if (typeof actual === "object") {
+            this._expect(TestHarness.ExpectationType.NotEmpty, !isEmptyObject(actual), message, actual);
+            return;
+        }
+
+        this.fail("expectNotEmpty should not be called with a non-object:\n    Actual: " + this._expectationValueAsString(actual));
+    }
+
     expectNull(actual, message)
     {
         this._expect(TestHarness.ExpectationType.Null, actual === null, message, actual, null);
@@ -381,6 +421,10 @@ TestHarness = class TestHarness extends WI.Object
             return "expectThat(%s)";
         case TestHarness.ExpectationType.False:
             return "expectFalse(%s)";
+        case TestHarness.ExpectationType.Empty:
+            return "expectEmpty(%s)";
+        case TestHarness.ExpectationType.NotEmpty:
+            return "expectNotEmpty(%s)";
         case TestHarness.ExpectationType.Null:
             return "expectNull(%s)";
         case TestHarness.ExpectationType.NotNull:
@@ -416,6 +460,10 @@ TestHarness = class TestHarness extends WI.Object
             return "truthy";
         case TestHarness.ExpectationType.False:
             return "falsey";
+        case TestHarness.ExpectationType.Empty:
+            return "empty";
+        case TestHarness.ExpectationType.NotEmpty:
+            return "not empty";
         case TestHarness.ExpectationType.NotNull:
             return "not null";
         case TestHarness.ExpectationType.NotEqual:
@@ -440,6 +488,8 @@ TestHarness = class TestHarness extends WI.Object
 TestHarness.ExpectationType = {
     True: Symbol("expect-true"),
     False: Symbol("expect-false"),
+    Empty: Symbol("expect-empty"),
+    NotEmpty: Symbol("expect-not-empty"),
     Null: Symbol("expect-null"),
     NotNull: Symbol("expect-not-null"),
     Equal: Symbol("expect-equal"),
diff --git a/Source/WebInspectorUI/UserInterface/Views/AnimationCollectionContentView.css b/Source/WebInspectorUI/UserInterface/Views/AnimationCollectionContentView.css
new file mode 100644 (file)
index 0000000..94a38f1
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2020 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+.content-view.animation-collection {
+    position: relative;
+    padding: 4px;
+}
diff --git a/Source/WebInspectorUI/UserInterface/Views/AnimationCollectionContentView.js b/Source/WebInspectorUI/UserInterface/Views/AnimationCollectionContentView.js
new file mode 100644 (file)
index 0000000..9ec1d2c
--- /dev/null
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2020 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+WI.AnimationCollectionContentView = class AnimationCollectionContentView extends WI.CollectionContentView
+{
+    constructor(representedObject)
+    {
+        console.assert(representedObject instanceof WI.AnimationCollection);
+
+        let contentPlaceholder = document.createElement("div");
+
+        let descriptionElement = contentPlaceholder.appendChild(document.createElement("div"));
+        descriptionElement.className = "description";
+
+        switch (representedObject.animationType) {
+        case WI.Animation.Type.WebAnimation:
+            descriptionElement.textContent = WI.UIString("Waiting for animations created by JavaScript.");
+            break;
+
+        case WI.Animation.Type.CSSAnimation:
+            descriptionElement.textContent = WI.UIString("Waiting for animations created by CSS.");
+            break;
+
+        case WI.Animation.Type.CSSTransition:
+            descriptionElement.textContent = WI.UIString("Waiting for transitions created by CSS.");
+            break;
+        }
+        console.assert(descriptionElement.textContent);
+
+        super(representedObject, WI.AnimationContentView, contentPlaceholder);
+
+        this.selectionEnabled = true;
+
+        this.element.classList.add("animation-collection");
+    }
+
+    // Public
+
+    handleRefreshButtonClicked()
+    {
+        for (let subview of this.subviews) {
+            if (subview instanceof WI.AnimationContentView)
+                subview.handleRefreshButtonClicked();
+        }
+    }
+
+    // Protected
+
+    contentViewAdded(contentView)
+    {
+        contentView.element.addEventListener("mouseenter", this._handleContentViewMouseEnter);
+        contentView.element.addEventListener("mouseleave", this._handleContentViewMouseLeave);
+    }
+
+    contentViewRemoved(contentView)
+    {
+        contentView.element.removeEventListener("mouseenter", this._handleContentViewMouseEnter);
+        contentView.element.removeEventListener("mouseleave", this._handleContentViewMouseLeave);
+    }
+
+    detached()
+    {
+        WI.domManager.hideDOMNodeHighlight();
+
+        super.detached();
+    }
+
+    // Private
+
+    _handleContentViewMouseEnter(event)
+    {
+        let contentView = WI.View.fromElement(event.target);
+        if (!(contentView instanceof WI.AnimationContentView))
+            return;
+
+        let animation = contentView.representedObject;
+        animation.requestEffectTarget((node) => {
+            if (!node || !node.ownerDocument)
+                return;
+            node.highlight();
+        });
+    }
+
+    _handleContentViewMouseLeave(event)
+    {
+        WI.domManager.hideDOMNodeHighlight();
+    }
+};
diff --git a/Source/WebInspectorUI/UserInterface/Views/AnimationContentView.css b/Source/WebInspectorUI/UserInterface/Views/AnimationContentView.css
new file mode 100644 (file)
index 0000000..7c59904
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2020 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+.content-view.animation {
+    position: relative;
+    width: 100%;
+    margin: 4px;
+    background-color: var(--background-color-content);
+    border: 1px solid var(--border-color);
+}
+
+.content-view.animation.selected {
+    outline: auto -webkit-focus-ring-color;
+}
+
+.content-view.animation > header {
+    display: flex;
+    align-items: center;
+    height: var(--navigation-bar-height);
+    padding: 0 6px;
+    font-size: 13px;
+}
+
+.content-view.animation > header > .titles {
+    white-space: nowrap;
+}
+
+.content-view.animation > header > .titles > .title {
+    color: var(--text-color-gray-dark);
+}
+
+.content-view.animation > header > .titles > .subtitle {
+    color: var(--text-color-gray-medium);
+}
+
+.content-view.animation > header > .titles > .subtitle:not(:empty)::before {
+    content: "\00A0\2014\00A0"; /* &nbsp;&mdash;&nbsp; */;
+}
+
+.content-view.animation > header > .navigation-bar {
+    border: none;
+    opacity: 0;
+}
+
+.content-view.animation:hover > header > .navigation-bar {
+    opacity: 1;
+}
+
+.content-view.animation > .preview {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    background-color: hsl(0, 0%, 96%);
+}
+
+.content-view.animation > .preview > svg {
+    width: 100%;
+}
+
+body[dir=rtl] .content-view.animation > .preview > svg {
+    transform: scaleX(-1);
+}
+
+.content-view.animation > .preview > svg rect {
+    fill: transparent;
+}
+
+.content-view.animation > .preview > svg > .delay line {
+    stroke: var(--border-color);
+    stroke-width: 2;
+}
+
+.content-view.animation > .preview > svg > .active path {
+    fill: var(--glyph-color-active);
+    fill-opacity: 0.5;
+    stroke: var(--glyph-color-active);
+    stroke-width: 1;
+}
+
+.content-view.animation > .preview > svg > .active circle {
+    fill: var(--text-color);
+}
+
+.content-view.animation > .preview > svg > .active line {
+    stroke: var(--text-color);
+    stroke-width: 2;
+}
+
+.content-view.animation > .preview > span {
+    padding: 4px;
+    color: var(--text-color-secondary);
+}
+
+@media (prefers-color-scheme: dark) {
+    .content-view.animation > header > .titles > .title {
+        color: var(--text-color);
+    }
+
+    .content-view.animation > header > .titles > .subtitle {
+        color: var(--text-color-secondary);
+    }
+
+    .content-view.animation > .preview {
+        background-color: hsl(0, 0%, 20%);
+    }
+}
diff --git a/Source/WebInspectorUI/UserInterface/Views/AnimationContentView.js b/Source/WebInspectorUI/UserInterface/Views/AnimationContentView.js
new file mode 100644 (file)
index 0000000..49d6067
--- /dev/null
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2020 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+WI.AnimationContentView = class AnimationContentView extends WI.ContentView
+{
+    constructor(representedObject)
+    {
+        console.assert(representedObject instanceof WI.Animation);
+
+        super(representedObject);
+
+        this._animationTargetDOMNode = null;
+        this._cachedWidth = NaN;
+
+        this.element.classList.add("animation");
+    }
+
+    // Static
+
+    static get previewHeight()
+    {
+        return 40;
+    }
+
+    // Public
+
+    handleRefreshButtonClicked()
+    {
+        this._refreshSubtitle();
+    }
+
+    // Protected
+
+    initialLayout()
+    {
+        super.initialLayout();
+
+        let headerElement = this.element.appendChild(document.createElement("header"));
+
+        let titlesContainer = headerElement.appendChild(document.createElement("div"));
+        titlesContainer.className = "titles";
+
+        let titleElement = titlesContainer.appendChild(document.createElement("span"));
+        titleElement.className = "title";
+        titleElement.textContent = this.representedObject.displayName;
+
+        this._subtitleElement = titlesContainer.appendChild(document.createElement("span"));
+        this._subtitleElement.className = "subtitle";
+
+        let navigationBar = new WI.NavigationBar;
+
+        let animationTargetButtonNavigationItem = new WI.ButtonNavigationItem("animation-target", WI.UIString("Animation Target"), "Images/Markup.svg", 16, 16);
+        animationTargetButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
+        WI.addMouseDownContextMenuHandlers(animationTargetButtonNavigationItem.element, this._populateAnimationTargetButtonContextMenu.bind(this));
+        navigationBar.addNavigationItem(animationTargetButtonNavigationItem);
+
+        headerElement.append(navigationBar.element);
+        this.addSubview(navigationBar);
+
+        this._previewContainer = this.element.appendChild(document.createElement("div"));
+        this._previewContainer.className = "preview";
+    }
+
+    layout()
+    {
+        super.layout();
+
+        this._refreshSubtitle();
+        this._refreshPreview();
+    }
+
+    sizeDidChange()
+    {
+        super.sizeDidChange();
+
+        this._cachedWidth = this.element.realOffsetWidth;
+    }
+
+    attached()
+    {
+        super.attached();
+
+        this.representedObject.addEventListener(WI.Animation.Event.EffectChanged, this._handleEffectChanged, this);
+        this.representedObject.addEventListener(WI.Animation.Event.TargetChanged, this._handleTargetChanged, this);
+    }
+
+    detached()
+    {
+        this.representedObject.removeEventListener(WI.Animation.Event.TargetChanged, this._handleTargetChanged, this);
+        this.representedObject.removeEventListener(WI.Animation.Event.EffectChanged, this._handleEffectChanged, this);
+
+        super.detached();
+    }
+
+    // Private
+
+    _refreshSubtitle()
+    {
+        this.representedObject.requestEffectTarget((domNode) => {
+            this._animationTargetDOMNode = domNode;
+
+            this._subtitleElement.removeChildren();
+            if (domNode)
+                this._subtitleElement.appendChild(WI.linkifyNodeReference(domNode));
+        });
+    }
+
+    _refreshPreview()
+    {
+        this._previewContainer.removeChildren();
+
+        let keyframes = this.representedObject.keyframes;
+        if (!keyframes.length) {
+            let span = this._previewContainer.appendChild(document.createElement("span"));
+            span.textContent = WI.UIString("This animation has no keyframes.");
+            return;
+        }
+
+        let startDelay = this.representedObject.startDelay || 0;
+        let iterationDuration = this.representedObject.iterationDuration || 0;
+        let endDelay = this.representedObject.endDelay || 0;
+        let totalDuration = startDelay + iterationDuration + endDelay;
+        if (totalDuration === 0) {
+            let span = this._previewContainer.appendChild(document.createElement("span"));
+            span.textContent = WI.UIString("This animation has no duration.");
+            return;
+        }
+
+        const previewHeight = WI.AnimationContentView.previewHeight;
+
+        const markerHeadRadius = 4;
+        const markerHeadPadding = 2;
+
+        // Squeeze the entire preview so that markers aren't cut off.
+        const squeezeXStart = (iterationDuration && startDelay) ? 0 : markerHeadRadius + markerHeadPadding;
+        const squeezeXEnd = (iterationDuration && endDelay) ? 0 : markerHeadRadius + markerHeadPadding;
+        const squeezeYStart = markerHeadRadius + (markerHeadPadding * 2);
+
+        // Move the easing line down to cut off the bottom border.
+        const adjustEasingY = 0.5;
+
+        let secondsPerPixel = this._cachedWidth / totalDuration;
+
+        startDelay *= secondsPerPixel;
+        iterationDuration = (iterationDuration * secondsPerPixel) - squeezeXStart - squeezeXEnd;
+        endDelay *= secondsPerPixel;
+
+        let svg = this._previewContainer.appendChild(createSVGElement("svg"));
+        svg.setAttribute("viewBox", `0 0 ${this._cachedWidth} ${previewHeight}`);
+
+        function addTitle(parent, title) {
+            let titleElement = parent.appendChild(createSVGElement("title"));
+            titleElement.textContent = title;
+        }
+
+        if (startDelay) {
+            let startDelayContainer = svg.appendChild(createSVGElement("g"));
+            startDelayContainer.classList.add("delay", "start");
+
+            let startDelayLine = startDelayContainer.appendChild(createSVGElement("line"));
+            startDelayLine.setAttribute("y1", (previewHeight + squeezeYStart) / 2);
+            startDelayLine.setAttribute("x2", startDelay);
+            startDelayLine.setAttribute("y2", (previewHeight + squeezeYStart) / 2);
+
+            let startDelayElement = startDelayContainer.appendChild(createSVGElement("rect"));
+            startDelayElement.setAttribute("width", startDelay);
+            startDelayElement.setAttribute("height", previewHeight);
+
+            const startDelayTitleFormat = WI.UIString("Start Delay %s", "Web Animation Start Delay Tooltip", "Tooltip for section of graph representing delay before a web animation begins applying styles");
+            addTitle(startDelayElement, startDelayTitleFormat.format(Number.secondsToString(this.representedObject.startDelay / 1000)));
+        }
+
+        if (endDelay) {
+            let endDelayContainer = svg.appendChild(createSVGElement("g"));
+            endDelayContainer.setAttribute("transform", `translate(${startDelay + iterationDuration + squeezeXStart}, 0)`);
+            endDelayContainer.classList.add("delay", "end");
+
+            let endDelayLine = endDelayContainer.appendChild(createSVGElement("line"));
+            endDelayLine.setAttribute("y1", (previewHeight + squeezeYStart) / 2);
+            endDelayLine.setAttribute("x2", endDelay);
+            endDelayLine.setAttribute("y2", (previewHeight + squeezeYStart) / 2);
+
+            let endDelayElement = endDelayContainer.appendChild(createSVGElement("rect"));
+            endDelayElement.setAttribute("width", startDelay + iterationDuration + endDelay);
+            endDelayElement.setAttribute("height", previewHeight);
+
+            const endDelayTitleFormat = WI.UIString("End Delay %s", "Web Animation End Delay Tooltip", "Tooltip for section of graph representing delay after a web animation finishes applying styles");
+            addTitle(endDelayElement, endDelayTitleFormat.format(Number.secondsToString(this.representedObject.endDelay / 1000)));
+        }
+
+        if (iterationDuration) {
+            let timingFunction = this.representedObject.timingFunction;
+
+            let activeDurationContainer = svg.appendChild(createSVGElement("g"));
+            activeDurationContainer.classList.add("active");
+            activeDurationContainer.setAttribute("transform", `translate(${startDelay + squeezeXStart}, ${squeezeYStart})`);
+
+            const startY = 0;
+            const endY = previewHeight - squeezeYStart;
+            const height = endY - startY;
+
+            for (let [keyframeA, keyframeB] of keyframes.adjacencies()) {
+                let startX = iterationDuration * keyframeA.offset;
+                let endX = iterationDuration * keyframeB.offset;
+                let width = endX - startX;
+
+                let easing = keyframeA.easing || timingFunction;
+
+                let easingContainer = activeDurationContainer.appendChild(createSVGElement("g"));
+                easingContainer.classList.add("easing");
+                easingContainer.setAttribute("transform", `translate(${startX}, ${startY})`);
+
+                let x1 = easing.inPoint.x * width;
+                let y1 = ((1 - easing.inPoint.y) * height) + adjustEasingY;
+                let x2 = easing.outPoint.x * width;
+                let y2 = ((1 - easing.outPoint.y) * height) + adjustEasingY;
+
+                let easingPath = easingContainer.appendChild(createSVGElement("path"));
+                easingPath.setAttribute("d", `M 0 ${height + adjustEasingY} C ${x1} ${y1} ${x2} ${y2} ${width} ${adjustEasingY} V ${height + adjustEasingY} Z`);
+
+                let titleRect = easingContainer.appendChild(createSVGElement("rect"));
+                titleRect.setAttribute("width", width);
+                titleRect.setAttribute("height", height);
+
+                addTitle(titleRect, easing.toString());
+            }
+
+            for (let keyframe of keyframes) {
+                let x = iterationDuration * keyframe.offset;
+
+                let keyframeContainer = activeDurationContainer.appendChild(createSVGElement("g"));
+                keyframeContainer.classList.add("keyframe");
+                keyframeContainer.setAttribute("transform", `translate(${x}, ${startY})`);
+
+                let keyframeMarkerHead = keyframeContainer.appendChild(createSVGElement("circle"));
+                keyframeMarkerHead.setAttribute("r", markerHeadRadius);
+
+                let keyframeMarkerLine = keyframeContainer.appendChild(createSVGElement("line"));
+                keyframeMarkerLine.setAttribute("y1", height);
+
+                let titleRect = keyframeContainer.appendChild(createSVGElement("rect"));
+                titleRect.setAttribute("x", -1 * (markerHeadRadius + markerHeadPadding));
+                titleRect.setAttribute("y", -1 * squeezeYStart);
+                titleRect.setAttribute("width", (markerHeadRadius + markerHeadPadding) * 2);
+                titleRect.setAttribute("height", height + squeezeYStart);
+
+                addTitle(titleRect, keyframe.style);
+            }
+        }
+    }
+
+    _handleEffectChanged(event)
+    {
+        this._refreshPreview();
+    }
+
+    _handleTargetChanged(event)
+    {
+        this._refreshSubtitle();
+    }
+
+    _populateAnimationTargetButtonContextMenu(contextMenu)
+    {
+        contextMenu.appendItem(WI.UIString("Log Animation"), () => {
+            WI.RemoteObject.resolveAnimation(this.representedObject, WI.RuntimeManager.ConsoleObjectGroup, (remoteObject) => {
+                if (!remoteObject)
+                    return;
+
+                const text = WI.UIString("Selected Animation", "Appears as a label when a given web animation is logged to the Console");
+                const addSpecialUserLogClass = true;
+                WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, addSpecialUserLogClass);
+            });
+        });
+
+        contextMenu.appendSeparator();
+
+        if (this._animationTargetDOMNode)
+            WI.appendContextMenuItemsForDOMNode(contextMenu, this._animationTargetDOMNode);
+    }
+};
diff --git a/Source/WebInspectorUI/UserInterface/Views/AnimationDetailsSidebarPanel.css b/Source/WebInspectorUI/UserInterface/Views/AnimationDetailsSidebarPanel.css
new file mode 100644 (file)
index 0000000..05db46e
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2020 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+.sidebar > .panel.details.animation > .content > .details-section.animation-keyframes .header > .subtitle {
+    color: var(--text-color-gray-medium);
+}
+
+.sidebar > .panel.details.animation > .content > .details-section.animation-keyframes .details-section {
+    background-color: hsl(0, 0%, 97%);
+}
+
+.sidebar > .panel.details.animation > .content > .details-section.animation-keyframes .details-section .row.styles {
+    display: table-row;
+}
+
+.sidebar > .panel.details.animation > .content > .details-section.animation-keyframes .details-section .row.styles .CodeMirror {
+    height: auto;
+    padding: 4px 0;
+    background-color: transparent;
+}
+
+@media (prefers-color-scheme: dark) {
+    .sidebar > .panel.details.animation > .content > .details-section.animation-keyframes .details-section {
+        background-color: hsl(0, 0%, 18%);
+    }
+}
diff --git a/Source/WebInspectorUI/UserInterface/Views/AnimationDetailsSidebarPanel.js b/Source/WebInspectorUI/UserInterface/Views/AnimationDetailsSidebarPanel.js
new file mode 100644 (file)
index 0000000..0691a5c
--- /dev/null
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2020 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+WI.AnimationDetailsSidebarPanel = class AnimationDetailsSidebarPanel extends WI.DetailsSidebarPanel
+{
+    constructor()
+    {
+        super("animation", WI.UIString("Animation"));
+
+        this._animation = null;
+
+        this._codeMirrorSectionMap = new Map;
+    }
+
+    // Public
+
+    inspect(objects)
+    {
+        if (!(objects instanceof Array))
+            objects = [objects];
+
+        this.animation = objects.find((object) => object instanceof WI.Animation);
+
+        return !!this.animation;
+    }
+
+    get animation()
+    {
+        return this._animation;
+    }
+
+    set animation(animation)
+    {
+        if (animation === this._animation)
+            return;
+
+        if (this._animation) {
+            this._animation.removeEventListener(WI.Animation.Event.TargetChanged, this._handleAnimationTargetChanged, this);
+            this._animation.removeEventListener(WI.Animation.Event.EffectChanged, this._handleAnimationEffectChanged, this);
+        }
+
+        this._animation = animation || null;
+
+        if (this._animation) {
+            this._animation.addEventListener(WI.Animation.Event.EffectChanged, this._handleAnimationEffectChanged, this);
+            this._animation.addEventListener(WI.Animation.Event.TargetChanged, this._handleAnimationTargetChanged, this);
+        }
+
+        this.needsLayout();
+    }
+
+    // Protected
+
+    initialLayout()
+    {
+        super.initialLayout();
+
+        this._nameRow = new WI.DetailsSectionSimpleRow(WI.UIString("Name"));
+        this._typeRow = new WI.DetailsSectionSimpleRow(WI.UIString("Type"));
+        this._targetRow = new WI.DetailsSectionSimpleRow(WI.UIString("Target", "Web Animation Target Label", "Label for the current DOM node target of a web animation"));
+
+        const identitySectionTitle = WI.UIString("Identity", "Web Animation Identity Title", "Section title for information about a web animation");
+        let identitySection = new WI.DetailsSection("animation-identity", identitySectionTitle, [new WI.DetailsSectionGroup([this._nameRow, this._typeRow, this._targetRow])]);
+        this.contentView.element.appendChild(identitySection.element);
+
+        this._iterationCountRow = new WI.DetailsSectionSimpleRow(WI.UIString("Iterations", "Web Animation Iteration Count Label", "Label for the number of iterations of a web animation"));
+        this._iterationStartRow = new WI.DetailsSectionSimpleRow(WI.UIString("Start", "Web Animation Iteration Start Label", "Label for the number describing which iteration a web animation should start at"));
+        this._iterationDurationRow = new WI.DetailsSectionSimpleRow(WI.UIString("Duration", "Web Animation Iteration Duration Label", "Label for the time duration of each iteration of a web animation"));
+        let iterationsGroup = new WI.DetailsSectionGroup([this._iterationCountRow, this._iterationStartRow, this._iterationDurationRow]);
+
+        this._startDelayRow = new WI.DetailsSectionSimpleRow(WI.UIString("Start Delay", "Web Animation Start Delay Label", "Label for the start delay time of a web animation "));
+        this._endDelayRow = new WI.DetailsSectionSimpleRow(WI.UIString("End Delay", "Web Animation End Delay Label", "Label for the end delay time of a web animation "));
+        this._timingFunctionRow = new WI.DetailsSectionSimpleRow(WI.UIString("Easing", "Web Animation Easing Label", "Label for the cubic-bezier timing function of a web animation"));
+        let timingGroup = new WI.DetailsSectionGroup([this._startDelayRow, this._endDelayRow, this._timingFunctionRow]);
+
+        this._playbackDirectionRow = new WI.DetailsSectionSimpleRow(WI.UIString("Direction", "Web Animation Playback Direction Label", "Label for the playback direction of a web animation"));
+        this._fillModeRow = new WI.DetailsSectionSimpleRow(WI.UIString("Fill", "Web Animation Fill Mode Label", "Label for the fill mode of a web animation"));
+        let fillDirectionGroup = new WI.DetailsSectionGroup([this._playbackDirectionRow, this._fillModeRow]);
+
+        const effectSectionTitle = WI.UIString("Effect", "Web Animation Effect Title", "Section title for information about the effect of a web animation");
+        let effectSection = new WI.DetailsSection("animation-effect", effectSectionTitle, [iterationsGroup, timingGroup, fillDirectionGroup]);
+        this.contentView.element.appendChild(effectSection.element);
+
+        this._keyframesGroup = new WI.DetailsSectionGroup;
+
+        const keyframesSectionTitle = WI.UIString("Keyframes", "Web Animation Keyframes Title", "Section title for information about the keyframes of a web animation");
+        let keyframesSection = new WI.DetailsSection("animation-keyframes", keyframesSectionTitle, [this._keyframesGroup]);
+        this.contentView.element.appendChild(keyframesSection.element);
+
+        const selectable = false;
+        let backtraceTreeOutline = new WI.TreeOutline(selectable);
+        backtraceTreeOutline.disclosureButtons = false;
+        this._backtraceTreeController = new WI.CallFrameTreeController(backtraceTreeOutline);
+
+        let backtraceRow = new WI.DetailsSectionRow;
+        backtraceRow.element.appendChild(backtraceTreeOutline.element);
+
+        const backtraceSectionTitle = WI.UIString("Backtrace", "Web Animation Backtrace Title", "Section title for the JavaScript backtrace of the creation of a web animation");
+        this._backtraceSection = new WI.DetailsSection("animation-backtrace", backtraceSectionTitle, [new WI.DetailsSectionGroup([backtraceRow])]);
+        this._backtraceSection.element.hidden = true;
+        this.contentView.element.appendChild(this._backtraceSection.element);
+    }
+
+    layout()
+    {
+        super.layout();
+
+        if (!this._animation)
+            return;
+
+        this._refreshIdentitySection();
+        this._refreshEffectSection();
+        this._refreshBacktraceSection();
+
+        for (let codeMirror of this._codeMirrorSectionMap.values())
+            codeMirror.refresh();
+    }
+
+    // Private
+
+    _refreshIdentitySection()
+    {
+        this._nameRow.value = this._animation.displayName;
+        this._typeRow.value = WI.Animation.displayNameForAnimationType(this._animation.animationType);
+
+        this._targetRow.value = null;
+        this._animation.requestEffectTarget((domNode) => {
+            this._targetRow.value = domNode ? WI.linkifyNodeReference(domNode) : null;
+        });
+    }
+
+    _refreshEffectSection()
+    {
+        for (let section of this._codeMirrorSectionMap.keys())
+            section.removeEventListener(WI.DetailsSection.Event.CollapsedStateChanged, this._handleDetailsSectionCollapsedStateChanged, this);
+        this._codeMirrorSectionMap.clear();
+
+        const precision = 0;
+        const readOnly = true;
+
+        this._iterationCountRow.value = !isNaN(this._animation.iterationCount) ? this._animation.iterationCount.toLocaleString() : null;
+        this._iterationStartRow.value = !isNaN(this._animation.iterationStart) ? this._animation.iterationStart.toLocaleString() : null;
+        this._iterationDurationRow.value = !isNaN(this._animation.iterationDuration) ? Number.secondsToString(this._animation.iterationDuration / 1000) : null;
+
+        this._startDelayRow.value = this._animation.startDelay ? Number.secondsToString(this._animation.startDelay / 1000) : null;
+        this._endDelayRow.value = this._animation.endDelay ? Number.secondsToString(this._animation.endDelay / 1000) : null;
+        this._timingFunctionRow.value = this._animation.timingFunction ? this._animation.timingFunction.toString() : null;
+
+        this._playbackDirectionRow.value = this._animation.playbackDirection ? WI.Animation.displayNameForPlaybackDirection(this._animation.playbackDirection) : null;
+        this._fillModeRow.value = this._animation.fillMode ? WI.Animation.displayNameForFillMode(this._animation.fillMode) : null;
+
+        let keyframeSections = [];
+        for (let keyframe of this._animation.keyframes) {
+            let rows = [];
+
+            let keyframeSection = new WI.DetailsSection("animation-keyframe-offset-" + keyframe.offset, Number.percentageString(keyframe.offset, precision));
+            keyframeSection.addEventListener(WI.DetailsSection.Event.CollapsedStateChanged, this._handleDetailsSectionCollapsedStateChanged, this);
+            keyframeSections.push(keyframeSection);
+
+            if (keyframe.easing) {
+                let subtitle = keyframeSection.headerElement.appendChild(document.createElement("span"));
+                subtitle.className = "subtitle";
+                subtitle.textContent = ` ${emDash} ${keyframe.easing.toString()}`;
+            }
+
+            if (keyframe.style) {
+                let codeMirrorElement = document.createElement("div");
+                let codeMirror = WI.CodeMirrorEditor.create(codeMirrorElement, {
+                    mode: "css",
+                    readOnly: "nocursor",
+                    lineWrapping: true,
+                });
+                codeMirror.setValue(keyframe.style);
+
+                const range = null;
+                function optionsForType(type) {
+                    return {
+                        allowedTokens: /\btag\b/,
+                        callback(marker, valueObject, valueString) {
+                            let swatch = new WI.InlineSwatch(type, valueObject, readOnly);
+                            codeMirror.setUniqueBookmark(marker.range.startPosition().toCodeMirror(), swatch.element);
+                        }
+                    };
+                }
+                createCodeMirrorColorTextMarkers(codeMirror, range, optionsForType(WI.InlineSwatch.Type.Color));
+                createCodeMirrorGradientTextMarkers(codeMirror, range, optionsForType(WI.InlineSwatch.Type.Gradient));
+                createCodeMirrorCubicBezierTextMarkers(codeMirror, range, optionsForType(WI.InlineSwatch.Type.Bezier));
+                createCodeMirrorSpringTextMarkers(codeMirror, range, optionsForType(WI.InlineSwatch.Type.Spring));
+
+                let row = new WI.DetailsSectionRow;
+                row.element.classList.add("styles");
+                row.element.appendChild(codeMirrorElement);
+                rows.push(row);
+
+                this._codeMirrorSectionMap.set(keyframeSection, codeMirror);
+            }
+
+            if (!rows.length) {
+                let emptyRow = new WI.DetailsSectionRow(WI.UIString("No Styles"));
+                emptyRow.showEmptyMessage();
+                rows.push(emptyRow);
+            }
+
+            keyframeSection.groups = [new WI.DetailsSectionGroup(rows)];
+        }
+        if (!keyframeSections.length) {
+            let emptyRow = new WI.DetailsSectionRow(WI.UIString("No Keyframes"));
+            emptyRow.showEmptyMessage();
+            keyframeSections.push(emptyRow);
+        }
+        this._keyframesGroup.rows = keyframeSections;
+    }
+
+    _refreshBacktraceSection()
+    {
+        let callFrames = this._animation.backtrace;
+        this._backtraceTreeController.callFrames = callFrames;
+        this._backtraceSection.element.hidden = !callFrames.length;
+    }
+
+    _handleAnimationEffectChanged(event)
+    {
+        this._refreshEffectSection();
+    }
+
+    _handleAnimationTargetChanged(event)
+    {
+        this._refreshIdentitySection();
+    }
+
+    _handleDetailsSectionCollapsedStateChanged(event)
+    {
+        let codeMirror = this._codeMirrorSectionMap.get(event.target);
+        codeMirror.refresh();
+    }
+};
index 4e5a90e..cc166bd 100644 (file)
  * THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-.content-view.canvas:not(.tab) {
+.content-view.canvas {
     background-color: hsl(0, 0%, 90%);
 }
 
-.content-view.canvas:not(.tab) > .preview {
+.content-view.canvas > .preview {
     display: flex;
     justify-content: center;
     align-items: center;
     padding: 15px;
 }
 
-.content-view.canvas:not(.tab) {
+.content-view.canvas {
     display: flex;
     flex-direction: column;
 }
 
-.content-view.canvas:not(.tab) > .preview > img {
+.content-view.canvas > .preview > img {
     max-width: 100%;
     max-height: 100%;
 }
 
-.content-view.canvas:not(.tab) > :matches(header, footer) {
+.content-view.canvas > :matches(header, footer) {
     display: none;
 }
 
@@ -56,7 +56,7 @@
 }
 
 @media (prefers-color-scheme: dark) {
-    .content-view.canvas:not(.tab) {
+    .content-view.canvas {
         background-color: unset;
     }
 }
index 9b4fd6f..993da51 100644 (file)
@@ -45,7 +45,7 @@ WI.CanvasContentView = class CanvasContentView extends WI.ContentView
 
         this._refreshButtonNavigationItem = new WI.ButtonNavigationItem("refresh", WI.UIString("Refresh"), "Images/ReloadFull.svg", 13, 13);
         this._refreshButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
-        this._refreshButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this.refreshPreview, this);
+        this._refreshButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this.handleRefreshButtonClicked, this);
 
         this._showGridButtonNavigationItem = new WI.ActivateButtonNavigationItem("show-grid", WI.UIString("Show transparency grid"), WI.UIString("Hide transparency grid"), "Images/NavigationItemCheckers.svg", 13, 13);
         this._showGridButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._showGridButtonClicked, this);
@@ -77,6 +77,29 @@ WI.CanvasContentView = class CanvasContentView extends WI.ContentView
         });
     }
 
+    handleRefreshButtonClicked()
+    {
+        this.refreshPreview();
+    }
+
+    // DropZoneView delegate
+
+    dropZoneShouldAppearForDragEvent(dropZone, event)
+    {
+        return event.dataTransfer.types.includes("Files");
+    }
+
+    dropZoneHandleDrop(dropZone, event)
+    {
+        let files = event.dataTransfer.files;
+        if (files.length !== 1) {
+            InspectorFrontendHost.beep();
+            return;
+        }
+
+        WI.FileUtilities.readJSON(files, (result) => WI.canvasManager.processJSON(result));
+    }
+
     // Protected
 
     initialLayout()
@@ -160,6 +183,13 @@ WI.CanvasContentView = class CanvasContentView extends WI.ContentView
 
         if (isCard)
             this._refreshPixelSize();
+
+        if (!isCard) {
+            let dropZoneView = new WI.DropZoneView(this);
+            dropZoneView.text = WI.UIString("Import Recording");
+            dropZoneView.targetElement = this.element;
+            this.addSubview(dropZoneView);
+        }
     }
 
     layout()
@@ -191,13 +221,6 @@ WI.CanvasContentView = class CanvasContentView extends WI.ContentView
         this._updateImageGrid();
     }
 
-    shown()
-    {
-        super.shown();
-
-        this.refreshPreview();
-    }
-
     attached()
     {
         super.attached();
@@ -223,6 +246,8 @@ WI.CanvasContentView = class CanvasContentView extends WI.ContentView
         });
 
         WI.settings.showImageGrid.addEventListener(WI.Setting.Event.Changed, this._updateImageGrid, this);
+
+        this.refreshPreview();
     }
 
     detached()
@@ -291,11 +316,6 @@ WI.CanvasContentView = class CanvasContentView extends WI.ContentView
 
     _populateCanvasElementButtonContextMenu(contextMenu)
     {
-        if (this._canvasNode)
-            WI.appendContextMenuItemsForDOMNode(contextMenu, this._canvasNode);
-
-        contextMenu.appendSeparator();
-
         contextMenu.appendItem(WI.UIString("Log Canvas Context"), () => {
             WI.RemoteObject.resolveCanvasContext(this.representedObject, WI.RuntimeManager.ConsoleObjectGroup, (remoteObject) => {
                 if (!remoteObject)
@@ -306,6 +326,11 @@ WI.CanvasContentView = class CanvasContentView extends WI.ContentView
                 WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, addSpecialUserLogClass);
             });
         });
+
+        contextMenu.appendSeparator();
+
+        if (this._canvasNode)
+            WI.appendContextMenuItemsForDOMNode(contextMenu, this._canvasNode);
     }
 
     _showGridButtonClicked()
index 10f37c0..1c6d643 100644 (file)
 .content-view.canvas-overview {
     justify-content: center;
     align-items: flex-start;
-    background-color: hsl(0, 0%, 90%);
-
-    --item-margin: 10px;
+    position: relative;
+    padding: 4px 0;
 }
 
-.content-view.canvas-overview .content-view.canvas {
+.content-view.canvas-overview .content-view.canvas {
     flex-grow: 0;
-    margin: var(--item-margin);
+    flex-shrink: 0;
     width: 400px;
+    margin: 4px;
     border: 1px solid var(--border-color);
     cursor: pointer;
 }
 
-.content-view.canvas-overview .content-view.canvas,
-.content-view.canvas-overview .content-view.canvas > .preview > img {
+.content-view.canvas-overview .content-view.canvas,
+.content-view.canvas-overview .content-view.canvas > .preview > img {
     background-color: white;
 }
 
-.content-view.canvas-overview .content-view.canvas.recording-active {
+.content-view.canvas-overview .content-view.canvas.recording-active {
     border-color: red;
 }
 
-.content-view.canvas-overview .content-view.canvas > :matches(header, footer) {
+.content-view.canvas-overview .content-view.canvas > :matches(header, footer) {
     display: flex;
     flex-direction: row;
     flex-shrink: 0;
     cursor: default;
 }
 
-.content-view.canvas-overview .content-view.canvas > header {
+.content-view.canvas-overview .content-view.canvas > header {
     font-size: 13px;
 }
 
-.content-view.canvas-overview .content-view.canvas.recording-active > header {
+.content-view.canvas-overview .content-view.canvas.recording-active > header {
     background-color: red;
 }
 
-.content-view.canvas-overview .content-view.canvas > header > .titles,
-.content-view.canvas-overview .content-view.canvas > footer > .size {
+.content-view.canvas-overview .content-view.canvas > header > .titles,
+.content-view.canvas-overview .content-view.canvas > footer > .size {
     white-space: nowrap;
 }
 
-.content-view.canvas-overview .content-view.canvas > header > .titles > .title {
+.content-view.canvas-overview .content-view.canvas > header > .titles > .title {
     color: var(--text-color-gray-dark);
 }
 
-.content-view.canvas-overview .content-view.canvas > header > .titles > .subtitle,
-.content-view.canvas-overview .content-view.canvas > footer .memory-cost {
+.content-view.canvas-overview .content-view.canvas > header > .titles > .subtitle,
+.content-view.canvas-overview .content-view.canvas > footer .memory-cost {
     color: var(--text-color-gray-medium);
 }
 
-.content-view.canvas-overview .content-view.canvas > header .subtitle::before {
+.content-view.canvas-overview .content-view.canvas > header .subtitle::before {
     content: "\00A0\2014\00A0"; /* &nbsp;&mdash;&nbsp; */;
 }
 
-.content-view.canvas-overview .content-view.canvas.recording-active > header > .titles > .title {
+.content-view.canvas-overview .content-view.canvas.recording-active > header > .titles > .title {
     color: white;
 }
 
-.content-view.canvas-overview .content-view.canvas.recording-active > header > .titles > .subtitle {
+.content-view.canvas-overview .content-view.canvas.recording-active > header > .titles > .subtitle {
     color: var(--selected-secondary-text-color);
 }
 
-.content-view.canvas-overview .content-view.canvas.recording-active > header > .navigation-bar > .item {
+.content-view.canvas-overview .content-view.canvas.recording-active > header > .navigation-bar > .item {
     filter: brightness(0) invert();
 }
 
-.content-view.canvas-overview .content-view.canvas > header > .navigation-bar {
+.content-view.canvas-overview .content-view.canvas > header > .navigation-bar {
     align-items: initial;
     border: none;
     opacity: 0;
     transition: opacity 200ms ease-in-out;
 }
 
-.content-view.canvas-overview .content-view.canvas:matches(:hover, .recording-active) > header > .navigation-bar {
+.content-view.canvas-overview .content-view.canvas:matches(:hover, .recording-active) > header > .navigation-bar {
     opacity: 1;
     transition: opacity 200ms ease-in-out;
 }
 
-.content-view.canvas-overview .content-view.canvas:not(.recording-active) > header > .navigation-bar > .item.record-start-stop.disabled {
+.content-view.canvas-overview .content-view.canvas:not(.recording-active) > header > .navigation-bar > .item.record-start-stop.disabled {
     filter: grayscale();
     opacity: 0.5;
 }
 
-.content-view.canvas-overview .content-view.canvas:not(.recording-active) > header > .navigation-bar > .item.record-start-stop:not(.disabled):hover {
+.content-view.canvas-overview .content-view.canvas:not(.recording-active) > header > .navigation-bar > .item.record-start-stop:not(.disabled):hover {
     filter: brightness(95%);
 }
 
-.content-view.canvas-overview .content-view.canvas:not(.recording-active) > header > .navigation-bar > .item.record-start-stop:not(.disabled):active {
+.content-view.canvas-overview .content-view.canvas:not(.recording-active) > header > .navigation-bar > .item.record-start-stop:not(.disabled):active {
     filter: brightness(80%);
 }
 
-.content-view.canvas-overview .content-view.canvas.recording-active > .progress-view,
-.content-view.canvas-overview .content-view.canvas > .preview {
+.content-view.canvas-overview .content-view.canvas.recording-active > .progress-view,
+.content-view.canvas-overview .content-view.canvas > .preview {
     height: 280px;
     transition: background-color 200ms ease-in-out;
 }
 
-.content-view.canvas-overview .content-view.canvas.recording-active > .progress-view:hover,
-.content-view.canvas-overview .content-view.canvas > .preview:hover {
+.content-view.canvas-overview .content-view.canvas.recording-active > .progress-view:hover,
+.content-view.canvas-overview .content-view.canvas > .preview:hover {
     background-color: hsl(0, 0%, 96%);
 }
 
-.content-view.canvas-overview .content-view.canvas.recording-active > .preview {
+.content-view.canvas-overview .content-view.canvas.recording-active > .preview {
     display: none;
 }
 
-.content-view.canvas-overview .content-view.canvas > .preview > img {
+.content-view.canvas-overview .content-view.canvas > .preview > img {
     border-radius: 4px;
     box-shadow: 1px 2px 6px rgba(0, 0, 0, 0.58);
 }
 
-.content-view.canvas-overview .content-view.canvas > .preview > .message-text-view {
+.content-view.canvas-overview .content-view.canvas > .preview > .message-text-view {
     position: static;
 }
 
-.content-view.canvas-overview .content-view.canvas > footer {
+.content-view.canvas-overview .content-view.canvas > footer {
     border-top: none;
 }
 
-.content-view.canvas-overview .content-view.canvas > footer > .view-related-items {
+.content-view.canvas-overview .content-view.canvas > footer > .view-related-items {
     display: flex;
     align-items: center;
 }
 
-.content-view.canvas-overview .content-view.canvas > footer > .view-related-items > :matches(.view-shader, .view-recording) {
+.content-view.canvas-overview .content-view.canvas > footer > .view-related-items > :matches(.view-shader, .view-recording) {
     width: 16px;
     height: 16px;
 }
 
-.content-view.canvas-overview .content-view.canvas > footer > .view-related-items > img + img {
+.content-view.canvas-overview .content-view.canvas > footer > .view-related-items > img + img {
     -webkit-margin-start: 4px;
 }
 
-.content-view.canvas-overview .content-view.canvas > footer > .view-related-items > .view-shader {
+.content-view.canvas-overview .content-view.canvas > footer > .view-related-items > .view-shader {
     content: image-set(url(../Images/DocumentGL.png) 1x, url(../Images/DocumentGL@2x.png) 2x);
 }
 
-.content-view.canvas-overview .content-view.canvas > footer > .view-related-items > .view-recording {
+.content-view.canvas-overview .content-view.canvas > footer > .view-related-items > .view-recording {
     content: url(../Images/Recording.svg);
 }
 
-.content-view.canvas-overview .content-view.canvas > footer > .flexible-space {
+.content-view.canvas-overview .content-view.canvas > footer > .flexible-space {
     flex: 1;
 }
 
-.content-view.canvas-overview .content-view.canvas > footer .memory-cost {
+.content-view.canvas-overview .content-view.canvas > footer .memory-cost {
     -webkit-padding-start: 4px;
 }
 
-.content-view.canvas-overview .content-view.canvas.saved-recordings {
+.content-view.canvas-overview .content-view.canvas.saved-recordings {
     height: 340px;
 }
 
-.content-view.canvas-overview .content-view.canvas.saved-recordings .tree-outline {
+.content-view.canvas-overview .content-view.canvas.saved-recordings .tree-outline {
     overflow-y: scroll;
 }
 
-.content-view.canvas-overview .content-view.canvas.saved-recordings .tree-outline > .item.recording > .icon {
+.content-view.canvas-overview .content-view.canvas.saved-recordings .tree-outline > .item.recording > .icon {
     content: url(../Images/Recording.svg);
 }
 
 }
 
 @media (prefers-color-scheme: dark) {
-    .content-view.canvas-overview {
-        background-color: unset;
-    }
-
-    .content-view.canvas-overview .content-view.canvas,
-    .content-view.canvas-overview .content-view.canvas > .preview > img {
+    .content-view.canvas-overview > .content-view.canvas,
+    .content-view.canvas-overview > .content-view.canvas > .preview > img {
         background-color: var(--background-color-secondary);
     }
 
-    .content-view.canvas-overview .content-view.canvas.recording-active > .progress-view:hover,
-    .content-view.canvas-overview .content-view.canvas > .preview:hover {
+    .content-view.canvas-overview .content-view.canvas.recording-active > .progress-view:hover,
+    .content-view.canvas-overview .content-view.canvas > .preview:hover {
         background-color: var(--background-color-tertiary);
     }
 
-    .content-view.canvas-overview .content-view.canvas.recording-active {
+    .content-view.canvas-overview .content-view.canvas.recording-active {
         --recording-color: hsl(0, 100%, 39%);
         border-color: var(--recording-color);
     }
 
-    .content-view.canvas-overview .content-view.canvas.recording-active > header {
+    .content-view.canvas-overview .content-view.canvas.recording-active > header {
         background-color: var(--recording-color);
     }
 
-    .content-view.canvas-overview .content-view.canvas > header > .titles > .title {
+    .content-view.canvas-overview .content-view.canvas > header > .titles > .title {
         color: var(--text-color);
     }
 
-    .content-view.canvas-overview .content-view.canvas > header > .titles > .subtitle,
-    .content-view.canvas-overview .content-view.canvas > footer .memory-cost {
+    .content-view.canvas-overview .content-view.canvas > header > .titles > .subtitle,
+    .content-view.canvas-overview .content-view.canvas > footer .memory-cost {
         color: var(--text-color-secondary);
     }
 
-    .content-view.canvas-overview .content-view.canvas > footer .view-recording {
+    .content-view.canvas-overview .content-view.canvas > footer .view-recording {
         filter: invert();
     }
 
-    .content-view.canvas-overview .content-view.canvas.recording-active > header > .titles > .subtitle {
+    .content-view.canvas-overview .content-view.canvas.recording-active > header > .titles > .subtitle {
         color: unset;
         opacity: 0.5
     }
index 639b36d..0d165dd 100644 (file)
@@ -29,7 +29,8 @@ WI.CanvasOverviewContentView = class CanvasOverviewContentView extends WI.Collec
     {
         console.assert(representedObject instanceof WI.CanvasCollection);
 
-        let contentPlaceholder = WI.createMessageTextView(WI.UIString("No Canvas Contexts"));
+        let contentPlaceholder = WI.animationManager.supported ? document.createElement("div") : WI.createMessageTextView(WI.UIString("No Canvas Contexts"));
+
         let descriptionElement = contentPlaceholder.appendChild(document.createElement("div"));
         descriptionElement.className = "description";
         descriptionElement.textContent = WI.UIString("Waiting for canvas contexts created by script or CSS.");
@@ -62,21 +63,7 @@ WI.CanvasOverviewContentView = class CanvasOverviewContentView extends WI.Collec
             this._updateRecordingAutoCaptureCheckboxLabel(frameCount);
         }
 
-        this._importButtonNavigationItem = new WI.ButtonNavigationItem("import-recording", WI.UIString("Import"), "Images/Import.svg", 15, 15);
-        this._importButtonNavigationItem.tooltip = WI.UIString("Import");
-        this._importButtonNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText;
-
-        this._refreshButtonNavigationItem = new WI.ButtonNavigationItem("refresh-all", WI.UIString("Refresh all"), "Images/ReloadFull.svg", 13, 13);
-        this._refreshButtonNavigationItem.enabled = false;
-        this._refreshButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._refreshPreviews, this);
-
-        this._showGridButtonNavigationItem = new WI.ActivateButtonNavigationItem("show-grid", WI.UIString("Show transparency grid"), WI.UIString("Hide transparency grid"), "Images/NavigationItemCheckers.svg", 13, 13);
-        this._showGridButtonNavigationItem.activated = !!WI.settings.showImageGrid.value;
-        this._showGridButtonNavigationItem.enabled = false;
-        this._showGridButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._showGridButtonClicked, this);
-
         importNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleImportButtonNavigationItemClicked, this);
-        this._importButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleImportButtonNavigationItemClicked, this);
 
         this._savedRecordingsContentView = null;
         this._savedRecordingsTreeOutline = null;
@@ -90,17 +77,18 @@ WI.CanvasOverviewContentView = class CanvasOverviewContentView extends WI.Collec
 
     get navigationItems()
     {
-        let navigationItems = [this._importButtonNavigationItem, new WI.DividerNavigationItem, this._refreshButtonNavigationItem, this._showGridButtonNavigationItem];
-        if (WI.CanvasManager.supportsRecordingAutoCapture())
-            navigationItems.unshift(this._recordingAutoCaptureNavigationItem, new WI.DividerNavigationItem);
+        let navigationItems = [];
+        if (this._recordingAutoCaptureNavigationItem)
+            navigationItems.push(this._recordingAutoCaptureNavigationItem);
         return navigationItems;
     }
 
-    hidden()
+    handleRefreshButtonClicked()
     {
-        WI.domManager.hideDOMNodeHighlight();
-
-        super.hidden();
+        for (let subview of this.subviews) {
+            if (subview instanceof WI.CanvasContentView)
+                subview.handleRefreshButtonClicked();
+        }
     }
 
     // Protected
@@ -115,23 +103,18 @@ WI.CanvasOverviewContentView = class CanvasOverviewContentView extends WI.Collec
             this.removeSubview(this._savedRecordingsContentView);
             this.addSubview(this._savedRecordingsContentView);
         }
-
-        this._updateNavigationItems();
     }
 
     contentViewRemoved(contentView)
     {
         contentView.element.removeEventListener("mouseenter", this._contentViewMouseEnter);
         contentView.element.removeEventListener("mouseleave", this._contentViewMouseLeave);
-
-        this._updateNavigationItems();
     }
 
     attached()
     {
         super.attached();
 
-        WI.settings.showImageGrid.addEventListener(WI.Setting.Event.Changed, this._updateShowImageGrid, this);
         WI.settings.canvasRecordingAutoCaptureEnabled.addEventListener(WI.Setting.Event.Changed, this._handleCanvasRecordingAutoCaptureEnabledChanged, this);
         WI.settings.canvasRecordingAutoCaptureFrameCount.addEventListener(WI.Setting.Event.Changed, this._handleCanvasRecordingAutoCaptureFrameCountChanged, this);
 
@@ -141,49 +124,27 @@ WI.CanvasOverviewContentView = class CanvasOverviewContentView extends WI.Collec
             this._savedRecordingsTreeOutline.removeChildren();
         for (let recording of WI.canvasManager.savedRecordings)
             this._addSavedRecording(recording);
+
+        for (let subview of this.subviews) {
+            if (subview instanceof WI.CanvasContentView)
+                subview.refreshPreview();
+        }
     }
 
     detached()
     {
+        WI.domManager.hideDOMNodeHighlight();
+
         WI.canvasManager.removeEventListener(null, null, this);
 
         WI.settings.canvasRecordingAutoCaptureFrameCount.removeEventListener(null, null, this);
         WI.settings.canvasRecordingAutoCaptureEnabled.removeEventListener(null, null, this);
-        WI.settings.showImageGrid.removeEventListener(null, null, this);
 
         super.detached();
     }
 
     // Private
 
-    get _itemMargin()
-    {
-        return parseInt(window.getComputedStyle(this.element).getPropertyValue("--item-margin"));
-    }
-
-    _refreshPreviews()
-    {
-        for (let canvasContentView of this.subviews)
-            canvasContentView.refreshPreview();
-    }
-
-    _updateNavigationItems()
-    {
-        let hasItems = !!this.representedObject.size;
-        this._refreshButtonNavigationItem.enabled = hasItems;
-        this._showGridButtonNavigationItem.enabled = hasItems;
-    }
-
-    _showGridButtonClicked(event)
-    {
-        WI.settings.showImageGrid.value = !this._showGridButtonNavigationItem.activated;
-    }
-
-    _updateShowImageGrid()
-    {
-        this._showGridButtonNavigationItem.activated = !!WI.settings.showImageGrid.value;
-    }
-
     _contentViewMouseEnter(event)
     {
         let contentView = WI.View.fromElement(event.target);
@@ -301,8 +262,7 @@ WI.CanvasOverviewContentView = class CanvasOverviewContentView extends WI.Collec
             this._savedRecordingsContentView.element.appendChild(this._savedRecordingsTreeOutline.element);
         }
 
-        const subtitle = null;
-        let recordingTreeElement = new WI.GeneralTreeElement(["recording"], recording.displayName, subtitle, recording);
+        let recordingTreeElement = new WI.GeneralTreeElement(["recording"], recording.displayName, WI.Recording.displayNameForRecordingType(recording.type), recording);
         recordingTreeElement.selectable = false;
         this._savedRecordingsTreeOutline.appendChild(recordingTreeElement);
     }
index cb5b90d..603f5ae 100644 (file)
@@ -237,10 +237,8 @@ WI.CanvasSidebarPanel = class CanvasSidebarPanel extends WI.NavigationSidebarPan
 
     canShowRepresentedObject(representedObject)
     {
-        if (representedObject instanceof WI.CanvasCollection)
-            return false;
-
-        return super.canShowRepresentedObject(representedObject);
+        return representedObject instanceof WI.Canvas
+            || representedObject instanceof WI.Recording;
     }
 
     // Protected
index fe33e7a..5ce8db0 100644 (file)
     flex-grow: 1;
 }
 
-.content-view.collection .resource.image img {
-    outline: 0.5px solid var(--border-color);
-}
-
-.content-view.collection .resource.image img:hover {
-    outline-color: var(--glyph-color-active)
-}
-
 .content-view.collection > .content-view[hidden] {
     display: none;
 }
+
+.content-view.collection > .placeholder:not(.message-text-view) {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    width: 100%;
+    text-align: center;
+    font-size: 13px;
+    color: var(--text-color-gray-medium);
+}
index 0547416..505a4a7 100644 (file)
@@ -28,7 +28,7 @@ WI.CollectionContentView = class CollectionContentView extends WI.ContentView
     constructor(collection, contentViewConstructor, contentPlaceholder)
     {
         console.assert(collection instanceof WI.Collection);
-        console.assert(contentViewConstructor instanceof WI.ContentView);
+        console.assert(!contentViewConstructor || contentViewConstructor.prototype instanceof WI.ContentView);
 
         super(collection);
 
@@ -38,7 +38,6 @@ WI.CollectionContentView = class CollectionContentView extends WI.ContentView
         this._contentPlaceholderElement = null;
         this._contentViewConstructor = contentViewConstructor;
         this._contentViewMap = new Map;
-        this._handleClickMap = new WeakMap;
         this._selectedItem = null;
         this._selectionEnabled = false;
     }
@@ -83,19 +82,25 @@ WI.CollectionContentView = class CollectionContentView extends WI.ContentView
             this._selectItem(null);
     }
 
-    setSelectedItem(item)
+    get selectedItem()
+    {
+        return this._selectedItem;
+    }
+
+    set selectedItem(item)
     {
         console.assert(this._selectionEnabled, "Attempted to set selected item when selection is disabled.");
         if (!this._selectionEnabled)
             return;
 
-        let contentView = this._contentViewMap.get(item);
-        console.assert(contentView, "Missing contet view for item.", item);
-        if (!contentView)
-            return;
-
         this._selectItem(item);
-        contentView.element.scrollIntoViewIfNeeded();
+
+        if (item) {
+            let contentView = this._contentViewMap.get(item);
+            console.assert(contentView, "Missing content view for item.", item);
+            if (contentView)
+                contentView.element.scrollIntoViewIfNeeded();
+        }
     }
 
     // Protected
@@ -120,20 +125,9 @@ WI.CollectionContentView = class CollectionContentView extends WI.ContentView
 
         let contentView = new this._contentViewConstructor(item, this.contentViewConstructorOptions);
         console.assert(contentView instanceof WI.ContentView);
-
-        let handleClick = (event) => {
-            if (event.button !== 0 || event.ctrlKey)
-                return;
-
-            if (this._selectionEnabled)
-                this._selectItem(item);
-            else
-                WI.showRepresentedObject(item);
-        };
+        console.assert(contentView.representedObject === item);
 
         this._contentViewMap.set(item, contentView);
-        this._handleClickMap.set(item, handleClick);
-        contentView.element.addEventListener("click", handleClick);
 
         this.addSubview(contentView);
         this.contentViewAdded(contentView);
@@ -169,14 +163,6 @@ WI.CollectionContentView = class CollectionContentView extends WI.ContentView
 
         contentView.removeEventListener(null, null, this);
 
-        let handleClick = this._handleClickMap.get(item);
-        console.assert(handleClick);
-
-        if (handleClick) {
-            contentView.element.removeEventListener("click", handleClick);
-            this._handleClickMap.delete(item);
-        }
-
         if (!this.subviews.length)
             this.showContentPlaceholder();
     }
@@ -200,6 +186,8 @@ WI.CollectionContentView = class CollectionContentView extends WI.ContentView
                 this._contentPlaceholderElement = this._contentPlaceholder;
         }
 
+        this._contentPlaceholderElement.classList.add("placeholder");
+
         if (!this._contentPlaceholderElement.parentNode)
             this.element.appendChild(this._contentPlaceholderElement);
     }
@@ -212,10 +200,11 @@ WI.CollectionContentView = class CollectionContentView extends WI.ContentView
 
     initialLayout()
     {
-        if (!this.representedObject.size || !this._contentViewConstructor) {
+        if (this._contentViewConstructor)
+            this.element.addEventListener("click", this._handleClick.bind(this));
+
+        if (!this.representedObject.size || !this._contentViewConstructor)
             this.showContentPlaceholder();
-            return;
-        }
     }
 
     attached()
@@ -292,6 +281,30 @@ WI.CollectionContentView = class CollectionContentView extends WI.ContentView
             selectedContentView.element.classList.add("selected");
         }
 
+        this.dispatchEventToListeners(WI.CollectionContentView.Event.SelectedItemChanged);
         this.dispatchEventToListeners(WI.ContentView.Event.SupplementalRepresentedObjectsDidChange);
     }
+
+    _handleClick(event)
+    {
+        if (event.button !== 0 || event.ctrlKey)
+            return;
+
+        for (let [item, contentView] of this._contentViewMap) {
+            if (contentView.element.contains(event.target)) {
+                if (this._selectionEnabled)
+                    this._selectItem(item);
+                else
+                    WI.showRepresentedObject(item);
+                return;
+            }
+        }
+
+        if (this._selectionEnabled)
+            this._selectItem(null);
+    }
+};
+
+WI.CollectionContentView.Event = {
+    SelectedItemChanged: "collection-content-view-selected-item-changed",
 };
index df912dd..296c2a5 100644 (file)
@@ -60,9 +60,6 @@ WI.ContentView = class ContentView extends WI.View
         if (representedObject instanceof WI.Canvas)
             return new WI.CanvasContentView(representedObject, extraArguments);
 
-        if (representedObject instanceof WI.CanvasCollection)
-            return new WI.CanvasOverviewContentView(representedObject, extraArguments);
-
         if (representedObject instanceof WI.ShaderProgram)
             return new WI.ShaderProgramContentView(representedObject, extraArguments);
 
@@ -182,6 +179,9 @@ WI.ContentView = class ContentView extends WI.View
         if (representedObject instanceof WI.AuditTestGroup || representedObject instanceof WI.AuditTestGroupResult)
             return new WI.AuditTestGroupContentView(representedObject, extraArguments);
 
+        if (representedObject instanceof WI.Animation)
+            return new WI.AnimationContentView(representedObject, extraArguments);
+
         if (representedObject instanceof WI.Collection)
             return new WI.CollectionContentView(representedObject, extraArguments);
 
@@ -273,8 +273,6 @@ WI.ContentView = class ContentView extends WI.View
             return true;
         if (representedObject instanceof WI.Canvas)
             return true;
-        if (representedObject instanceof WI.CanvasCollection)
-            return true;
         if (representedObject instanceof WI.ShaderProgram)
             return true;
         if (representedObject instanceof WI.TimelineRecording)
@@ -318,10 +316,14 @@ WI.ContentView = class ContentView extends WI.View
         if (representedObject instanceof WI.AuditTestCase || representedObject instanceof WI.AuditTestGroup
             || representedObject instanceof WI.AuditTestCaseResult || representedObject instanceof WI.AuditTestGroupResult)
             return true;
+        if (representedObject instanceof WI.Animation)
+            return true;
         if (representedObject instanceof WI.Collection)
             return true;
         if (typeof representedObject === "string" || representedObject instanceof String)
             return true;
+        if (representedObject[WI.ContentView.isViewableSymbol])
+            return true;
         return false;
     }
 
@@ -512,4 +514,5 @@ WI.ContentView.Event = {
     NavigationItemsDidChange: "content-view-navigation-items-did-change"
 };
 
+WI.ContentView.isViewableSymbol = Symbol("is-viewable");
 WI.ContentView.ContentViewForRepresentedObjectSymbol = Symbol("content-view-for-represented-object");
index e5a5fec..7ed0c82 100644 (file)
@@ -96,7 +96,7 @@ WI.DetailsSectionSimpleRow = class DetailsSectionSimpleRow extends WI.DetailsSec
     {
         this._value = value || "";
 
-        if (this._value) {
+        if (this._value || this._value === 0) {
             this.element.classList.remove(WI.DetailsSectionSimpleRow.EmptyStyleClassName);
 
             // If the value has space characters that cause word wrapping then we don't need the data class.
diff --git a/Source/WebInspectorUI/UserInterface/Views/GraphicsOverviewContentView.css b/Source/WebInspectorUI/UserInterface/Views/GraphicsOverviewContentView.css
new file mode 100644 (file)
index 0000000..3d663e6
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2020 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+.content-view.graphics-overview {
+    background-color: var(--background-color-content);
+    overflow-y: scroll;
+    padding-bottom: var(--navigation-bar-height);
+    background-color: hsl(0, 0%, 90%);
+}
+
+.content-view.graphics-overview > section {
+    position: relative;
+    background-color: inherit;
+}
+
+.content-view.graphics-overview > section:not(:first-child) {
+    margin-top: 20px;
+}
+
+.content-view.graphics-overview > section > .header {
+    display: flex;
+    align-items: center;
+    position: sticky;
+    top: 0;
+    z-index: 1;
+    min-height: var(--navigation-bar-height);
+    -webkit-padding-start: 8px;
+    background-color: hsl(0, 0%, 97%);
+    border-bottom: 1px solid var(--border-color);
+}
+
+.content-view.graphics-overview > section:not(:first-of-type) > .header {
+    border-top: 1px solid var(--border-color);
+}
+
+.content-view.graphics-overview > section > .header > h1 {
+    margin: 0;
+}
+
+.content-view.graphics-overview > section > .header > .navigation-bar {
+    flex-grow: 1;
+    justify-content: flex-end;
+    border-bottom: none;
+}
+
+.content-view.graphics-overview > .content-view.canvas-overview {
+    position: absolute;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+}
+
+@media (prefers-color-scheme: dark) {
+    .content-view.graphics-overview {
+        background-color: hsl(0, 0%, 14%);
+    }
+
+    .content-view.graphics-overview > section > .header {
+        background-color: hsl(0, 0%, 21%);
+    }
+}
+
diff --git a/Source/WebInspectorUI/UserInterface/Views/GraphicsOverviewContentView.js b/Source/WebInspectorUI/UserInterface/Views/GraphicsOverviewContentView.js
new file mode 100644 (file)
index 0000000..e0c0ae0
--- /dev/null
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2020 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+WI.GraphicsOverviewContentView = class GraphicsOverviewContentView extends WI.ContentView
+{
+    constructor()
+    {
+        super();
+
+        this.element.classList.add("graphics-overview");
+
+        this._importButtonNavigationItem = new WI.ButtonNavigationItem("import-recording", WI.UIString("Import"), "Images/Import.svg", 15, 15);
+        this._importButtonNavigationItem.tooltip = WI.UIString("Import");
+        this._importButtonNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText;
+        this._importButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleImportButtonNavigationItemClicked, this);
+
+        this._refreshButtonNavigationItem = new WI.ButtonNavigationItem("refresh", WI.UIString("Refresh"), "Images/ReloadFull.svg", 13, 13);
+        this._refreshButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleRefreshButtonClicked, this);
+
+        this._showGridButtonNavigationItem = new WI.ActivateButtonNavigationItem("show-grid", WI.UIString("Show transparency grid"), WI.UIString("Hide transparency grid"), "Images/NavigationItemCheckers.svg", 13, 13);
+        this._showGridButtonNavigationItem.activated = !!WI.settings.showImageGrid.value;
+        this._showGridButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleShowGridButtonClicked, this);
+
+        this._canvasOverviewContentView = new WI.CanvasOverviewContentView(WI.canvasManager.canvasCollection);
+
+        this._overviewViews = [];
+    }
+
+    // Public
+
+    get supplementalRepresentedObjects()
+    {
+        return this._overviewViews.flatMap((subview) => subview.supplementalRepresentedObjects);
+    }
+
+    get navigationItems()
+    {
+        let navigationItems = [];
+        if (!WI.animationManager.supported) {
+            navigationItems.pushAll(this._canvasOverviewContentView.navigationItems);
+            navigationItems.push(new WI.DividerNavigationItem);
+        }
+        navigationItems.push(this._importButtonNavigationItem, new WI.DividerNavigationItem, this._refreshButtonNavigationItem, this._showGridButtonNavigationItem);
+        return navigationItems;
+    }
+
+    // Protected
+
+    attached()
+    {
+        super.attached();
+
+        WI.settings.showImageGrid.addEventListener(WI.Setting.Event.Changed, this._handleShowImageGridSettingChanged, this);
+        this._handleShowImageGridSettingChanged();
+    }
+
+    detached()
+    {
+        WI.domManager.hideDOMNodeHighlight();
+
+        WI.settings.showImageGrid.removeEventListener(WI.Setting.Event.Changed, this._handleShowImageGridSettingChanged, this);
+
+        super.detached();
+    }
+
+    initialLayout()
+    {
+        super.initialLayout();
+
+        if (WI.animationManager.supported) {
+            let createSection = (identifier, title, overviewView) => {
+                console.assert(overviewView instanceof WI.ContentView);
+
+                let sectionElement = this.element.appendChild(document.createElement("section"));
+                sectionElement.className = identifier;
+
+                let headerElement = sectionElement.appendChild(document.createElement("div"));
+                headerElement.className = "header";
+
+                let titleElement = headerElement.appendChild(document.createElement("h1"));
+                titleElement.textContent = title;
+
+                let navigationItems = overviewView.navigationItems;
+                if (navigationItems.length) {
+                    let navigationBar = new WI.NavigationBar(document.createElement("nav"), navigationItems);
+                    headerElement.appendChild(navigationBar.element);
+                    this.addSubview(navigationBar);
+                }
+
+                let contentElement = sectionElement.appendChild(document.createElement("div"));
+                contentElement.className = "content";
+
+                contentElement.appendChild(overviewView.element);
+                this.addSubview(overviewView);
+
+                overviewView.addEventListener(WI.CollectionContentView.Event.SelectedItemChanged, this._handleOverviewViewSelectedItemChanged, this);
+                overviewView.addEventListener(WI.ContentView.Event.SupplementalRepresentedObjectsDidChange, this._handleOverviewViewSupplementalRepresentedObjectsDidChange, this);
+                this._overviewViews.push(overviewView);
+            };
+
+            createSection("canvas", WI.UIString("Canvases"), this._canvasOverviewContentView);
+
+            let animationCollection = WI.animationManager.animationCollection;
+            createSection("web-animation", WI.UIString("Web Animations"), new WI.AnimationCollectionContentView(animationCollection.animationCollectionForType(WI.Animation.Type.WebAnimation)));
+            createSection("css-animation", WI.UIString("CSS Animations"), new WI.AnimationCollectionContentView(animationCollection.animationCollectionForType(WI.Animation.Type.CSSAnimation)));
+            createSection("css-transition", WI.UIString("CSS Transitions"), new WI.AnimationCollectionContentView(animationCollection.animationCollectionForType(WI.Animation.Type.CSSTransition)));
+
+            this.element.addEventListener("click", this._handleClick.bind(this));
+        } else
+            this.addSubview(this._canvasOverviewContentView);
+
+        let dropZoneView = new WI.DropZoneView(this);
+        dropZoneView.text = WI.UIString("Import Recording");
+        dropZoneView.targetElement = this.element;
+        this.addSubview(dropZoneView);
+    }
+
+    // DropZoneView delegate
+
+    dropZoneShouldAppearForDragEvent(dropZone, event)
+    {
+        return event.dataTransfer.types.includes("Files");
+    }
+
+    dropZoneHandleDrop(dropZone, event)
+    {
+        let files = event.dataTransfer.files;
+        if (files.length !== 1) {
+            InspectorFrontendHost.beep();
+            return;
+        }
+
+        WI.FileUtilities.readJSON(files, (result) => WI.canvasManager.processJSON(result));
+    }
+
+    // Private
+
+    _handleRefreshButtonClicked()
+    {
+        for (let overviewView of this._overviewViews)
+            overviewView.handleRefreshButtonClicked();
+    }
+
+    _handleShowGridButtonClicked(event)
+    {
+        WI.settings.showImageGrid.value = !this._showGridButtonNavigationItem.activated;
+    }
+
+    _handleShowImageGridSettingChanged()
+    {
+        this._showGridButtonNavigationItem.activated = !!WI.settings.showImageGrid.value;
+    }
+
+    _handleImportButtonNavigationItemClicked(event)
+    {
+        WI.FileUtilities.importJSON((result) => WI.canvasManager.processJSON(result), {multiple: true});
+    }
+
+    _handleOverviewViewSelectedItemChanged(event)
+    {
+        if (!event.target.selectedItem)
+            return;
+
+        for (let overviewView of this._overviewViews) {
+            if (overviewView !== event.target && overviewView.selectionEnabled)
+                overviewView.selectedItem = null;
+        }
+    }
+
+    _handleOverviewViewSupplementalRepresentedObjectsDidChange(evnet)
+    {
+        this.dispatchEventToListeners(WI.ContentView.Event.SupplementalRepresentedObjectsDidChange);
+    }
+
+    _handleClick(event)
+    {
+        if (event.target !== this.element)
+            return;
+
+        for (let overviewView of this._overviewViews) {
+            if (overviewView.selectionEnabled)
+                overviewView.selectedItem = null;
+        }
+    }
+};
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 Apple Inc. All rights reserved.
+ * Copyright (C) 2020 Apple Inc. All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
  * THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-.content-view.tab.canvas .navigation-bar > .item .canvas-overview .icon {
+.content-view.tab.graphics .navigation-bar > .item .canvas-overview .icon {
     content: url(../Images/CanvasOverview.svg);
 }
 
-.content-view.tab.canvas .navigation-bar > .item .canvas.canvas-2d .icon {
+.content-view.tab.graphics .navigation-bar > .item .canvas.canvas-2d .icon {
     content: url(../Images/Canvas2D.svg);
 }
 
-.content-view.tab.canvas .navigation-bar > .item .canvas:matches(.webgl, .webgl2, .webgpu, .webmetal) .icon {
+.content-view.tab.graphics .navigation-bar > .item .canvas:matches(.webgl, .webgl2, .webgpu, .webmetal) .icon {
     content: url(../Images/Canvas3D.svg);
 }
 
-.content-view.tab.canvas .navigation-bar > .item .recording > .icon {
+.content-view.tab.graphics .navigation-bar > .item .recording > .icon {
     content: url(../Images/Recording.svg);
 }
 
-.content-view.tab.canvas .navigation-bar > .item .shader-program > .icon {
+.content-view.tab.graphics .navigation-bar > .item .shader-program > .icon {
     content: image-set(url(../Images/DocumentGL.png) 1x, url(../Images/DocumentGL@2x.png) 2x);
 }
 
 @media (prefers-color-scheme: dark) {
-    .content-view.tab.canvas .navigation-bar > .item .canvas-overview .icon {
+    .content-view.tab.graphics .navigation-bar > .item .graphics-overview .icon {
         filter: invert(60%);
     }
 
-    .content-view.tab.canvas .navigation-bar > .item .canvas .icon,
-    .content-view.tab.canvas .navigation-bar > .item .recording > .icon {
+    .content-view.tab.graphics .navigation-bar > .item .canvas .icon,
+    .content-view.tab.graphics .navigation-bar > .item .recording > .icon {
         filter: invert();
     }
 }
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 Apple Inc. All rights reserved.
+ * Copyright (C) 2020 Apple Inc. All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
  * THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-WI.CanvasTabContentView = class CanvasTabContentView extends WI.ContentBrowserTabContentView
+WI.GraphicsTabContentView = class GraphicsTabContentView extends WI.ContentBrowserTabContentView
 {
-    constructor(representedObject)
+    constructor()
     {
-        console.assert(!representedObject || representedObject instanceof WI.Canvas);
-
-        let tabBarItem = WI.GeneralTabBarItem.fromTabInfo(WI.CanvasTabContentView.tabInfo());
+        let tabBarItem = WI.GeneralTabBarItem.fromTabInfo(WI.GraphicsTabContentView.tabInfo());
 
         const navigationSidebarPanelConstructor = WI.CanvasSidebarPanel;
-        const detailsSidebarPanelConstructors = [WI.RecordingStateDetailsSidebarPanel, WI.RecordingTraceDetailsSidebarPanel, WI.CanvasDetailsSidebarPanel];
+        const detailsSidebarPanelConstructors = [
+            WI.RecordingStateDetailsSidebarPanel,
+            WI.RecordingTraceDetailsSidebarPanel,
+            WI.CanvasDetailsSidebarPanel,
+            WI.AnimationDetailsSidebarPanel,
+        ];
         const disableBackForward = true;
-        super("canvas", ["canvas"], tabBarItem, navigationSidebarPanelConstructor, detailsSidebarPanelConstructors, disableBackForward);
-
-        this._canvasCollection = new WI.CanvasCollection;
+        super("graphics", ["graphics"], tabBarItem, navigationSidebarPanelConstructor, detailsSidebarPanelConstructors, disableBackForward);
 
-        this._canvasTreeOutline = new WI.TreeOutline;
-        this._canvasTreeOutline.allowsRepeatSelection = true;
-        this._canvasTreeOutline.addEventListener(WI.TreeOutline.Event.SelectionDidChange, this._canvasTreeOutlineSelectionDidChange, this);
+        this._canvasesTreeOutline = new WI.TreeOutline;
+        this._canvasesTreeOutline.allowsRepeatSelection = true;
+        this._canvasesTreeOutline.addEventListener(WI.TreeOutline.Event.SelectionDidChange, this._handleOverviewTreeOutlineSelectionDidChange, this);
 
-        this._overviewTreeElement = new WI.GeneralTreeElement("canvas-overview", WI.UIString("Overview"), null, this._canvasCollection);
-        this._canvasTreeOutline.appendChild(this._overviewTreeElement);
+        this._canvasesTreeElement = new WI.GeneralTreeElement("canvas-overview", WI.UIString("Overview"), null, {[WI.ContentView.isViewableSymbol]: true});
+        this._canvasesTreeOutline.appendChild(this._canvasesTreeElement);
 
-        this._savedRecordingsTreeElement = new WI.FolderTreeElement(WI.UIString("Saved Recordings"), WI.RecordingCollection);
-        this._savedRecordingsTreeElement.hidden = true;
-        this._overviewTreeElement.appendChild(this._savedRecordingsTreeElement);
+        this._savedCanvasRecordingsTreeElement = new WI.FolderTreeElement(WI.UIString("Saved Recordings"));
+        this._savedCanvasRecordingsTreeElement.hidden = true;
+        this._canvasesTreeElement.appendChild(this._savedCanvasRecordingsTreeElement);
 
         this._recordShortcut = new WI.KeyboardShortcut(null, WI.KeyboardShortcut.Key.Space, this._handleSpace.bind(this));
         this._recordShortcut.implicitlyPreventsDefault = false;
@@ -58,31 +59,33 @@ WI.CanvasTabContentView = class CanvasTabContentView extends WI.ContentBrowserTa
         this._recordSingleFrameShortcut.disabled = true;
 
         WI.canvasManager.enable();
+        WI.animationManager.enable();
     }
 
     static tabInfo()
     {
         return {
-            image: "Images/Canvas.svg",
-            title: WI.UIString("Canvas"),
+            image: "Images/Graphics.svg",
+            title: WI.UIString("Graphics"),
         };
     }
 
     static isTabAllowed()
     {
-        return InspectorBackend.hasDomain("Canvas");
+        return InspectorBackend.hasDomain("Canvas")
+            || InspectorBackend.hasDomain("Animation");
     }
 
     // Public
 
     treeElementForRepresentedObject(representedObject)
     {
-        return this._canvasTreeOutline.findTreeElement(representedObject);
+        return this._canvasesTreeOutline.findTreeElement(representedObject);
     }
 
     get type()
     {
-        return WI.CanvasTabContentView.Type;
+        return WI.GraphicsTabContentView.Type;
     }
 
     get supportsSplitContentBrowser()
@@ -95,35 +98,31 @@ WI.CanvasTabContentView = class CanvasTabContentView extends WI.ContentBrowserTa
         return true;
     }
 
-    canShowRepresentedObject(representedObject)
-    {
-        return representedObject instanceof WI.Canvas
-            || representedObject instanceof WI.CanvasCollection
-            || representedObject instanceof WI.Recording
-            || representedObject instanceof WI.ShaderProgram;
-    }
-
-    shown()
+    showRepresentedObject(representedObject, cookie)
     {
-        super.shown();
-
-        this._recordShortcut.disabled = false;
-        this._recordSingleFrameShortcut.disabled = false;
+        if (representedObject instanceof WI.Collection) {
+            this.contentBrowser.showContentView(this._overviewContentView);
+            return;
+        }
 
-        if (!this.contentBrowser.currentContentView)
-            this.showRepresentedObject(this._canvasCollection);
+        super.showRepresentedObject(representedObject, cookie);
     }
 
-    hidden()
+    canShowRepresentedObject(representedObject)
     {
-        this._recordShortcut.disabled = true;
-        this._recordSingleFrameShortcut.disabled = true;
-
-        super.hidden();
+        return representedObject instanceof WI.CanvasCollection
+            || representedObject instanceof WI.Canvas
+            || representedObject instanceof WI.RecordingCollection
+            || representedObject instanceof WI.Recording
+            || representedObject instanceof WI.ShaderProgramCollection
+            || representedObject instanceof WI.ShaderProgram
+            || representedObject instanceof WI.AnimationCollection
+            || representedObject instanceof WI.Animation;
     }
 
     closed()
     {
+        WI.animationManager.disable();
         WI.canvasManager.disable();
 
         super.closed();
@@ -150,42 +149,55 @@ WI.CanvasTabContentView = class CanvasTabContentView extends WI.ContentBrowserTa
     {
         super.attached();
 
-        WI.canvasManager.addEventListener(WI.CanvasManager.Event.CanvasAdded, this._handleCanvasAdded, this);
-        WI.canvasManager.addEventListener(WI.CanvasManager.Event.CanvasRemoved, this._handleCanvasRemoved, this);
+        WI.canvasManager.canvasCollection.addEventListener(WI.Collection.Event.ItemAdded, this._handleCanvasAdded, this);
+        WI.canvasManager.canvasCollection.addEventListener(WI.Collection.Event.ItemRemoved, this._handleCanvasRemoved, this);
         WI.canvasManager.addEventListener(WI.CanvasManager.Event.RecordingSaved, this._handleRecordingSavedOrStopped, this);
         WI.Canvas.addEventListener(WI.Canvas.Event.RecordingStopped, this._handleRecordingSavedOrStopped, this);
 
-        let canvases = WI.canvasManager.canvases;
-
-        for (let canvas of this._canvasCollection) {
-            if (!canvases.includes(canvas))
+        for (let child of this._canvasesTreeElement.children) {
+            let canvas = child.representedObject;
+            if (canvas instanceof WI.Canvas && !WI.canvasManager.canvasCollection.has(canvas))
                 this._removeCanvas(canvas);
         }
 
-        for (let canvas of canvases) {
-            if (!this._canvasCollection.has(canvas))
+        for (let canvas of WI.canvasManager.canvasCollection) {
+            if (!this._canvasesTreeOutline.findTreeElement(canvas))
                 this._addCanvas(canvas);
         }
 
-        this._savedRecordingsTreeElement.removeChildren();
-        for (let recording of WI.canvasManager.savedRecordings)
-            this._addRecording(recording, {suppressShowRecording: true});
+        for (let recording of WI.canvasManager.savedRecordings) {
+            if (!this._canvasesTreeOutline.findTreeElement(recording))
+                this._addRecording(recording, {suppressShowRecording: true});
+        }
+
+        this._recordShortcut.disabled = false;
+        this._recordSingleFrameShortcut.disabled = false;
     }
 
     detached()
     {
-        WI.Canvas.removeEventListener(null, null, this);
-        WI.canvasManager.removeEventListener(null, null, this);
+        this._recordShortcut.disabled = true;
+        this._recordSingleFrameShortcut.disabled = true;
+
+        WI.Canvas.removeEventListener(WI.Canvas.Event.RecordingStopped, this._handleRecordingSavedOrStopped, this);
+        WI.canvasManager.removeEventListener(WI.CanvasManager.Event.RecordingSaved, this._handleRecordingSavedOrStopped, this);
+        WI.canvasManager.canvasCollection.removeEventListener(WI.Collection.Event.ItemAdded, this._handleCanvasAdded, this);
+        WI.canvasManager.canvasCollection.removeEventListener(WI.Collection.Event.ItemRemoved, this._handleCanvasRemoved, this);
 
         super.detached();
     }
 
+    initialLayout()
+    {
+        this._overviewContentView = new WI.GraphicsOverviewContentView;
+        this.contentBrowser.showContentView(this._overviewContentView);
+    }
+
     // Private
 
     _addCanvas(canvas)
     {
-        this._overviewTreeElement.appendChild(new WI.CanvasTreeElement(canvas));
-        this._canvasCollection.add(canvas);
+        this._canvasesTreeElement.appendChild(new WI.CanvasTreeElement(canvas));
 
         const options = {
             suppressShowRecording: true,
@@ -197,18 +209,16 @@ WI.CanvasTabContentView = class CanvasTabContentView extends WI.ContentBrowserTa
 
     _removeCanvas(canvas)
     {
-        let treeElement = this._canvasTreeOutline.findTreeElement(canvas);
+        let treeElement = this._canvasesTreeOutline.findTreeElement(canvas);
         console.assert(treeElement, "Missing tree element for canvas.", canvas);
 
         const suppressNotification = true;
         treeElement.deselect(suppressNotification);
-        this._overviewTreeElement.removeChild(treeElement);
-
-        this._canvasCollection.remove(canvas);
+        this._canvasesTreeElement.removeChild(treeElement);
 
         let currentContentView = this.contentBrowser.currentContentView;
         if (currentContentView instanceof WI.CanvasContentView)
-            WI.showRepresentedObject(this._canvasCollection);
+            WI.showRepresentedObject(WI.canvasManager.canvasCollection);
         else if (currentContentView instanceof WI.RecordingContentView && canvas.recordingCollection.has(currentContentView.representedObject))
             this.contentBrowser.updateHierarchicalPathForCurrentContentView();
 
@@ -224,8 +234,8 @@ WI.CanvasTabContentView = class CanvasTabContentView extends WI.ContentBrowserTa
         if (!recording.source) {
             const subtitle = null;
             let recordingTreeElement = new WI.GeneralTreeElement(["recording"], recording.displayName, subtitle, recording);
-            this._savedRecordingsTreeElement.hidden = false;
-            this._savedRecordingsTreeElement.appendChild(recordingTreeElement);
+            this._savedCanvasRecordingsTreeElement.hidden = false;
+            this._savedCanvasRecordingsTreeElement.appendChild(recordingTreeElement);
         }
 
         if (!options.suppressShowRecording)
@@ -234,20 +244,27 @@ WI.CanvasTabContentView = class CanvasTabContentView extends WI.ContentBrowserTa
 
     _handleCanvasAdded(event)
     {
-        this._addCanvas(event.data.canvas);
+        this._addCanvas(event.data.item);
     }
 
     _handleCanvasRemoved(event)
     {
-        this._removeCanvas(event.data.canvas);
+        this._removeCanvas(event.data.item);
     }
 
-    _canvasTreeOutlineSelectionDidChange(event)
+    _handleOverviewTreeOutlineSelectionDidChange(event)
     {
-        let selectedElement = this._canvasTreeOutline.selectedTreeElement;
+        let selectedElement = this._canvasesTreeOutline.selectedTreeElement;
         if (!selectedElement)
             return;
 
+        switch (selectedElement) {
+        case this._canvasesTreeElement:
+        case this._savedCanvasRecordingsTreeElement:
+            this.contentBrowser.showContentView(this._overviewContentView);
+            return;
+        }
+
         let representedObject = selectedElement.representedObject;
         if (!this.canShowRepresentedObject(representedObject)) {
             console.assert(false, "Unexpected representedObject.", representedObject);
@@ -295,4 +312,4 @@ WI.CanvasTabContentView = class CanvasTabContentView extends WI.ContentBrowserTa
     }
 };
 
-WI.CanvasTabContentView.Type = "canvas";
+WI.GraphicsTabContentView.Type = "graphics";
index ce31af4..10cd64a 100644 (file)
@@ -209,12 +209,12 @@ body.docked:matches(.right, .left) #navigation-sidebar.collapsed > .resizer {
     margin-bottom: 15px;