Web Inspector: add listing of Canvases/Programs/Recordings to the NavigationSidebar
authormattbaker@apple.com <mattbaker@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 9 Feb 2018 00:52:28 +0000 (00:52 +0000)
committermattbaker@apple.com <mattbaker@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 9 Feb 2018 00:52:28 +0000 (00:52 +0000)
https://bugs.webkit.org/show_bug.cgi?id=178744
<rdar://problem/35374379>

Reviewed by Devin Rousso.

* Localizations/en.lproj/localizedStrings.js:

* UserInterface/Images/Canvas2D.svg:
* UserInterface/Images/Canvas3D.svg:
* UserInterface/Images/Recording.svg:
Update canvas icons to be monochrome. Simplified the recording icon.

* UserInterface/Main.html:

* UserInterface/Models/RecordingAction.js:
(WI.RecordingAction.prototype.get state):
(WI.RecordingAction.prototype.set state):
Allow (2D) snapshot state to be associated with the action. Used by
RecordingActionDetailsSidebarPanel to retrieve the snapshot state.

* UserInterface/Views/CanvasContentView.css:
(.content-view.canvas:not(.tab)):
(.content-view.canvas:not(.tab) > .progress): Deleted.
(.content-view.canvas:not(.tab) > .progress > .frame-count): Deleted.

* UserInterface/Views/CanvasContentView.js:
(WI.CanvasContentView):
(WI.CanvasContentView.prototype.get navigationItems):
(WI.CanvasContentView.prototype.layout):
(WI.CanvasContentView.prototype.shown):
(WI.CanvasContentView.prototype._recordingStarted):
(WI.CanvasContentView.prototype._recordingProgress):
(WI.CanvasContentView.prototype._recordingStopped):
(WI.CanvasContentView.prototype._updateRecordNavigationItem):
(WI.CanvasContentView.prototype._updateProgressView):
Replace progress UI with a reusable ProgressView class.
When in the overview, clicking the CanvasContentView shows a dedicated
CanvasContentView for inspecting shaders and recordings. This behavior
is controlled by CollectionContentView, so we need to prevent it when
clicking inside the header and footer elements, which contain clickable UI.

* UserInterface/Views/CanvasDetailsSidebarPanel.js:
(WI.CanvasDetailsSidebarPanel.prototype.inspect):

* UserInterface/Views/CanvasOverviewContentView.css:
(.content-view.canvas-overview .content-view.canvas):
(.content-view.canvas-overview .content-view.canvas.is-recording):
(.content-view.canvas-overview .content-view.canvas > :matches(header, footer)):
(.content-view.canvas-overview .content-view.canvas > header):
(.content-view.canvas-overview .content-view.canvas.is-recording > header):
(.content-view.canvas-overview .content-view.canvas > header > .navigation-bar):
(.content-view.canvas-overview .content-view.canvas:matches(:hover, .is-recording) > header > .navigation-bar):
(.content-view.canvas-overview .content-view.canvas.is-recording > .progress-view,):
(.content-view.canvas-overview .content-view.canvas.is-recording > .preview):
(.content-view.canvas-overview .content-view.canvas > :matches(header, .progress, .preview, footer)): Deleted.
(.content-view.canvas-overview .content-view.canvas.selected > :matches(.progress, .preview, footer),): Deleted.
(.content-view.canvas-overview .content-view.canvas:not(:hover, .is-recording, .selected) > header > .navigation-bar): Deleted.
(.content-view.canvas-overview .content-view.canvas > :matches(.progress, .preview)): Deleted.
(.content-view.canvas-overview .content-view.canvas > .preview): Deleted.
(.content-view.canvas-overview .content-view.canvas > .progress ~ .preview): Deleted.
Clean up styles, and remove selection styles as canvases are no longer selectable in the overview.

* UserInterface/Views/CanvasOverviewContentView.js:
(WI.CanvasOverviewContentView):
(WI.CanvasOverviewContentView.prototype.get navigationItems):
(WI.CanvasOverviewContentView.prototype.attached):
(WI.CanvasOverviewContentView.prototype.detached):
(WI.CanvasOverviewContentView.prototype.get selectionPathComponents): Deleted.
(WI.CanvasOverviewContentView.prototype._changeSelectedItemVertically): Deleted.
(WI.CanvasOverviewContentView.prototype._changeSelectedItemHorizontally): Deleted.
(WI.CanvasOverviewContentView.prototype._selectionPathComponentsChanged): Deleted.
(WI.CanvasOverviewContentView.prototype._handleUp): Deleted.
(WI.CanvasOverviewContentView.prototype._handleRight): Deleted.
(WI.CanvasOverviewContentView.prototype._handleDown): Deleted.
(WI.CanvasOverviewContentView.prototype._handleLeft): Deleted.
(WI.CanvasOverviewContentView.prototype._handleSpace): Deleted.
(WI.CanvasOverviewContentView.prototype._supplementalRepresentedObjectsDidChange): Deleted.
Disable canvas selection. Remove logic for supplemental represented objects,
path components, and selection keyboard shortcuts.

* UserInterface/Views/CanvasSidebarPanel.css: Added.
(.sidebar > .panel.navigation.canvas > .content):
(.sidebar > .panel.navigation.canvas > .navigation-bar > .item.record-start-stop.disabled):
(.sidebar > .panel.navigation.canvas > .content > .tree-outline .item.canvas.canvas-2d .icon):
(.sidebar > .panel.navigation.canvas > .content > .tree-outline .item.canvas.webgl .icon):
(.sidebar > .panel.navigation.canvas > .content > .navigation-bar):
(.sidebar > .panel.navigation.canvas.has-recordings > .content > .tree-outline.canvas):
(.sidebar > .panel.navigation.canvas:not(.has-recordings) > .filter-bar,):
(.sidebar > .panel.navigation.canvas > .content > .tree-outline .item.recording > .icon):
(.sidebar > .panel.navigation.canvas > .content > .tree-outline .item.shader-program > .icon):
(.sidebar > .panel.navigation.canvas > .content > .tree-outline .item.folder-icon > .icon):
(.sidebar > .panel.navigation.canvas > .content > .tree-outline .item.folder-icon > .status):

* UserInterface/Views/CanvasSidebarPanel.js: Added.
(WI.CanvasSidebarPanel):
(WI.CanvasSidebarPanel.prototype.get canvas):
(WI.CanvasSidebarPanel.prototype.set canvas):
(WI.CanvasSidebarPanel.prototype.set recording):
(WI.CanvasSidebarPanel.prototype.set action):
(WI.CanvasSidebarPanel.prototype.shown):
(WI.CanvasSidebarPanel.prototype.hidden):
(WI.CanvasSidebarPanel.prototype.hasCustomFilters):
(WI.CanvasSidebarPanel.prototype.matchTreeElementAgainstCustomFilters):
(WI.CanvasSidebarPanel.prototype.initialLayout):
(WI.CanvasSidebarPanel.prototype._recordingAdded):
(WI.CanvasSidebarPanel.prototype._recordingRemoved):
(WI.CanvasSidebarPanel.prototype._scopeBarSelectionChanged):
(WI.CanvasSidebarPanel.prototype._toggleRecording):
(WI.CanvasSidebarPanel.prototype._currentRepresentedObjectsDidChange):
(WI.CanvasSidebarPanel.prototype._treeOutlineSelectionDidChange):
(WI.CanvasSidebarPanel.prototype._canvasChanged):
(WI.CanvasSidebarPanel.prototype._recordingChanged):
(WI.CanvasSidebarPanel.prototype._updateRecordNavigationItem):
(WI.CanvasSidebarPanel.prototype._updateRecordingScopeBar):
Add new navigation sidebar, split into two sections. The upper section
contains a tree with a single element for the current canvas, and child
elements for any shader programs. The maximum height of this section is 50%
of the sidebar's height. The lower section contains a tree for the selected
recording, and a scope bar for choosing between recordings.

* UserInterface/Views/CanvasTabContentView.css:
(.content-view.tab.canvas .navigation-bar > .item .canvas-overview .icon):
(.content-view.tab.canvas .navigation-bar > .item .canvas.canvas-2d .icon):
(.content-view.tab.canvas .navigation-bar > .item .canvas.webgl .icon):
(.content-view.tab.canvas .navigation-bar > .item .shader-program > .icon):
(.content-view.tab.canvas .navigation-bar > .item > .hierarchical-path-component > .icon): Deleted.
(.content-view.tab.canvas .navigation-bar > .item .canvas .icon): Deleted.

* UserInterface/Views/CanvasTabContentView.js:
(WI.CanvasTabContentView):
(WI.CanvasTabContentView.prototype.canShowRepresentedObject):
(WI.CanvasTabContentView.prototype.attached):
(WI.CanvasTabContentView.prototype._addCanvas):
(WI.CanvasTabContentView.prototype._removeCanvas):
(WI.CanvasTabContentView.prototype._canvasTreeOutlineSelectionDidChange):
(WI.CanvasTabContentView.prototype._recordingAdded):
(WI.CanvasTabContentView.prototype._handleSpace):
(WI.CanvasTabContentView.prototype.showRepresentedObject): Deleted.
(WI.CanvasTabContentView.prototype._navigationSidebarTreeOutlineSelectionChanged): Deleted.
(WI.CanvasTabContentView.prototype._recordingActionIndexChanged): Deleted.
(WI.CanvasTabContentView.prototype._updateActionIndex): Deleted.
The canvas tab now maintains a tree outline of all canvases, with an
"Overview" element as the root. The Overview element is always the first
item of content browser's hierarchical path.

* UserInterface/Views/CanvasTreeElement.js:
(WI.CanvasTreeElement.createRecordingTreeElement):
(WI.CanvasTreeElement):
(WI.CanvasTreeElement.prototype.onattach):
(WI.CanvasTreeElement.prototype.onpopulate):
(WI.CanvasTreeElement.prototype._updateStatus):
(WI.CanvasTreeElement.prototype.ondetach): Deleted.
Make it possible to not show recordings under the Canvas element.
Create `isRecording` status element (spinner).

* UserInterface/Views/CollectionContentView.js:
(WI.CollectionContentView.prototype.shown):
(WI.CollectionContentView.prototype.hidden):
Child ContentViews need to be updated when the collection's visibility changes.

* UserInterface/Views/ContentView.js:
(WI.ContentView.isViewable):

* UserInterface/Views/ProgressView.css: Added.
(.progress-view):
(.progress-view > .titles):
(.progress-view > .titles > .title):
(.progress-view > .titles > .subtitle):
(.progress-view > .titles > .subtitle::before):
(.progress-view > .indeterminate-progress-spinner):

* UserInterface/Views/ProgressView.js: Added.
(WI.ProgressView):
(WI.ProgressView.prototype.get title):
(WI.ProgressView.prototype.set title):
(WI.ProgressView.prototype.get subtitle):
(WI.ProgressView.prototype.set subtitle):
(WI.ProgressView.prototype.get visible):
(WI.ProgressView.prototype.set visible):
(WI.ProgressView.prototype.initialLayout):
(WI.ProgressView.prototype._updateTitles):
New view class (not a ContentView) for showing a generic progress message,
with a title, subtitle, and progress spinner.

* UserInterface/Views/RecordingContentView.css:
(.content-view:not(.tab).recording > .preview-container):
Remove unnecessary styles.

* UserInterface/Views/RecordingContentView.js:
(WI.RecordingContentView):
(WI.RecordingContentView.prototype.get navigationItems):
(WI.RecordingContentView.prototype.get supplementalRepresentedObjects):
(WI.RecordingContentView.prototype.updateActionIndex):
(WI.RecordingContentView.prototype.get saveData):
(WI.RecordingContentView.prototype._exportRecording):
Relocate the recording export logic and UI.
(WI.RecordingContentView.prototype.async._generateContentCanvas2D):
(WI.RecordingContentView.prototype.async._generateContentCanvasWebGL):
(WI.RecordingContentView.prototype._sliderChanged):
Refactor logic for notifying the rest of the UI of changes to the action slider.
The selected action is now exposed as a supplemental represented object, and a
corresponding SupplementalRepresentedObjectsDidChange event.

* UserInterface/Views/RecordingStateDetailsSidebarPanel.js:
(WI.RecordingStateDetailsSidebarPanel.prototype.inspect):
(WI.RecordingStateDetailsSidebarPanel.prototype.set action):
(WI.RecordingStateDetailsSidebarPanel.prototype._generateDetailsCanvas2D):
(WI.RecordingStateDetailsSidebarPanel):
(WI.RecordingStateDetailsSidebarPanel.prototype.updateAction): Deleted.

* UserInterface/Views/RecordingTraceDetailsSidebarPanel.js:
(WI.RecordingTraceDetailsSidebarPanel.prototype.inspect):
(WI.RecordingTraceDetailsSidebarPanel.prototype.set action):
(WI.RecordingTraceDetailsSidebarPanel):
(WI.RecordingTraceDetailsSidebarPanel.prototype.updateAction): Deleted.
Now that the selected action is exposed to the UI as a supplemental
represented object, details sidebars can be more decoupled from the
canvas tab, and be notified of changes to the selection via `inspect()`.

* UserInterface/Views/ResourceIcons.css:
(.canvas > .icon): Deleted.
(.shader-program .icon): Deleted.

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

26 files changed:
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
Source/WebInspectorUI/UserInterface/Images/Canvas2D.svg
Source/WebInspectorUI/UserInterface/Images/Canvas3D.svg
Source/WebInspectorUI/UserInterface/Images/Recording.svg
Source/WebInspectorUI/UserInterface/Main.html
Source/WebInspectorUI/UserInterface/Models/RecordingAction.js
Source/WebInspectorUI/UserInterface/Views/CanvasContentView.css
Source/WebInspectorUI/UserInterface/Views/CanvasContentView.js
Source/WebInspectorUI/UserInterface/Views/CanvasDetailsSidebarPanel.js
Source/WebInspectorUI/UserInterface/Views/CanvasOverviewContentView.css
Source/WebInspectorUI/UserInterface/Views/CanvasOverviewContentView.js
Source/WebInspectorUI/UserInterface/Views/CanvasSidebarPanel.css [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/CanvasSidebarPanel.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/CanvasTabContentView.css
Source/WebInspectorUI/UserInterface/Views/CanvasTabContentView.js
Source/WebInspectorUI/UserInterface/Views/CanvasTreeElement.js
Source/WebInspectorUI/UserInterface/Views/CollectionContentView.js
Source/WebInspectorUI/UserInterface/Views/ContentView.js
Source/WebInspectorUI/UserInterface/Views/ProgressView.css [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/ProgressView.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/RecordingContentView.css
Source/WebInspectorUI/UserInterface/Views/RecordingContentView.js
Source/WebInspectorUI/UserInterface/Views/RecordingStateDetailsSidebarPanel.js
Source/WebInspectorUI/UserInterface/Views/RecordingTraceDetailsSidebarPanel.js
Source/WebInspectorUI/UserInterface/Views/ResourceIcons.css

index 06cb5c4..7d65011 100644 (file)
@@ -1,3 +1,229 @@
+2018-02-08  Matt Baker  <mattbaker@apple.com>
+
+        Web Inspector: add listing of Canvases/Programs/Recordings to the NavigationSidebar
+        https://bugs.webkit.org/show_bug.cgi?id=178744
+        <rdar://problem/35374379>
+
+        Reviewed by Devin Rousso.
+
+        * Localizations/en.lproj/localizedStrings.js:
+
+        * UserInterface/Images/Canvas2D.svg:
+        * UserInterface/Images/Canvas3D.svg:
+        * UserInterface/Images/Recording.svg:
+        Update canvas icons to be monochrome. Simplified the recording icon.
+
+        * UserInterface/Main.html:
+
+        * UserInterface/Models/RecordingAction.js:
+        (WI.RecordingAction.prototype.get state):
+        (WI.RecordingAction.prototype.set state):
+        Allow (2D) snapshot state to be associated with the action. Used by
+        RecordingActionDetailsSidebarPanel to retrieve the snapshot state.
+
+        * UserInterface/Views/CanvasContentView.css:
+        (.content-view.canvas:not(.tab)):
+        (.content-view.canvas:not(.tab) > .progress): Deleted.
+        (.content-view.canvas:not(.tab) > .progress > .frame-count): Deleted.
+
+        * UserInterface/Views/CanvasContentView.js:
+        (WI.CanvasContentView):
+        (WI.CanvasContentView.prototype.get navigationItems):
+        (WI.CanvasContentView.prototype.layout):
+        (WI.CanvasContentView.prototype.shown):
+        (WI.CanvasContentView.prototype._recordingStarted):
+        (WI.CanvasContentView.prototype._recordingProgress):
+        (WI.CanvasContentView.prototype._recordingStopped):
+        (WI.CanvasContentView.prototype._updateRecordNavigationItem):
+        (WI.CanvasContentView.prototype._updateProgressView):
+        Replace progress UI with a reusable ProgressView class.
+        When in the overview, clicking the CanvasContentView shows a dedicated
+        CanvasContentView for inspecting shaders and recordings. This behavior
+        is controlled by CollectionContentView, so we need to prevent it when
+        clicking inside the header and footer elements, which contain clickable UI.
+
+        * UserInterface/Views/CanvasDetailsSidebarPanel.js:
+        (WI.CanvasDetailsSidebarPanel.prototype.inspect):
+
+        * UserInterface/Views/CanvasOverviewContentView.css:
+        (.content-view.canvas-overview .content-view.canvas):
+        (.content-view.canvas-overview .content-view.canvas.is-recording):
+        (.content-view.canvas-overview .content-view.canvas > :matches(header, footer)):
+        (.content-view.canvas-overview .content-view.canvas > header):
+        (.content-view.canvas-overview .content-view.canvas.is-recording > header):
+        (.content-view.canvas-overview .content-view.canvas > header > .navigation-bar):
+        (.content-view.canvas-overview .content-view.canvas:matches(:hover, .is-recording) > header > .navigation-bar):
+        (.content-view.canvas-overview .content-view.canvas.is-recording > .progress-view,):
+        (.content-view.canvas-overview .content-view.canvas.is-recording > .preview):
+        (.content-view.canvas-overview .content-view.canvas > :matches(header, .progress, .preview, footer)): Deleted.
+        (.content-view.canvas-overview .content-view.canvas.selected > :matches(.progress, .preview, footer),): Deleted.
+        (.content-view.canvas-overview .content-view.canvas:not(:hover, .is-recording, .selected) > header > .navigation-bar): Deleted.
+        (.content-view.canvas-overview .content-view.canvas > :matches(.progress, .preview)): Deleted.
+        (.content-view.canvas-overview .content-view.canvas > .preview): Deleted.
+        (.content-view.canvas-overview .content-view.canvas > .progress ~ .preview): Deleted.
+        Clean up styles, and remove selection styles as canvases are no longer selectable in the overview.
+
+        * UserInterface/Views/CanvasOverviewContentView.js:
+        (WI.CanvasOverviewContentView):
+        (WI.CanvasOverviewContentView.prototype.get navigationItems):
+        (WI.CanvasOverviewContentView.prototype.attached):
+        (WI.CanvasOverviewContentView.prototype.detached):
+        (WI.CanvasOverviewContentView.prototype.get selectionPathComponents): Deleted.
+        (WI.CanvasOverviewContentView.prototype._changeSelectedItemVertically): Deleted.
+        (WI.CanvasOverviewContentView.prototype._changeSelectedItemHorizontally): Deleted.
+        (WI.CanvasOverviewContentView.prototype._selectionPathComponentsChanged): Deleted.
+        (WI.CanvasOverviewContentView.prototype._handleUp): Deleted.
+        (WI.CanvasOverviewContentView.prototype._handleRight): Deleted.
+        (WI.CanvasOverviewContentView.prototype._handleDown): Deleted.
+        (WI.CanvasOverviewContentView.prototype._handleLeft): Deleted.
+        (WI.CanvasOverviewContentView.prototype._handleSpace): Deleted.
+        (WI.CanvasOverviewContentView.prototype._supplementalRepresentedObjectsDidChange): Deleted.
+        Disable canvas selection. Remove logic for supplemental represented objects,
+        path components, and selection keyboard shortcuts.
+
+        * UserInterface/Views/CanvasSidebarPanel.css: Added.
+        (.sidebar > .panel.navigation.canvas > .content):
+        (.sidebar > .panel.navigation.canvas > .navigation-bar > .item.record-start-stop.disabled):
+        (.sidebar > .panel.navigation.canvas > .content > .tree-outline .item.canvas.canvas-2d .icon):
+        (.sidebar > .panel.navigation.canvas > .content > .tree-outline .item.canvas.webgl .icon):
+        (.sidebar > .panel.navigation.canvas > .content > .navigation-bar):
+        (.sidebar > .panel.navigation.canvas.has-recordings > .content > .tree-outline.canvas):
+        (.sidebar > .panel.navigation.canvas:not(.has-recordings) > .filter-bar,):
+        (.sidebar > .panel.navigation.canvas > .content > .tree-outline .item.recording > .icon):
+        (.sidebar > .panel.navigation.canvas > .content > .tree-outline .item.shader-program > .icon):
+        (.sidebar > .panel.navigation.canvas > .content > .tree-outline .item.folder-icon > .icon):
+        (.sidebar > .panel.navigation.canvas > .content > .tree-outline .item.folder-icon > .status):
+
+        * UserInterface/Views/CanvasSidebarPanel.js: Added.
+        (WI.CanvasSidebarPanel):
+        (WI.CanvasSidebarPanel.prototype.get canvas):
+        (WI.CanvasSidebarPanel.prototype.set canvas):
+        (WI.CanvasSidebarPanel.prototype.set recording):
+        (WI.CanvasSidebarPanel.prototype.set action):
+        (WI.CanvasSidebarPanel.prototype.shown):
+        (WI.CanvasSidebarPanel.prototype.hidden):
+        (WI.CanvasSidebarPanel.prototype.hasCustomFilters):
+        (WI.CanvasSidebarPanel.prototype.matchTreeElementAgainstCustomFilters):
+        (WI.CanvasSidebarPanel.prototype.initialLayout):
+        (WI.CanvasSidebarPanel.prototype._recordingAdded):
+        (WI.CanvasSidebarPanel.prototype._recordingRemoved):
+        (WI.CanvasSidebarPanel.prototype._scopeBarSelectionChanged):
+        (WI.CanvasSidebarPanel.prototype._toggleRecording):
+        (WI.CanvasSidebarPanel.prototype._currentRepresentedObjectsDidChange):
+        (WI.CanvasSidebarPanel.prototype._treeOutlineSelectionDidChange):
+        (WI.CanvasSidebarPanel.prototype._canvasChanged):
+        (WI.CanvasSidebarPanel.prototype._recordingChanged):
+        (WI.CanvasSidebarPanel.prototype._updateRecordNavigationItem):
+        (WI.CanvasSidebarPanel.prototype._updateRecordingScopeBar):
+        Add new navigation sidebar, split into two sections. The upper section
+        contains a tree with a single element for the current canvas, and child
+        elements for any shader programs. The maximum height of this section is 50%
+        of the sidebar's height. The lower section contains a tree for the selected
+        recording, and a scope bar for choosing between recordings.
+
+        * UserInterface/Views/CanvasTabContentView.css:
+        (.content-view.tab.canvas .navigation-bar > .item .canvas-overview .icon):
+        (.content-view.tab.canvas .navigation-bar > .item .canvas.canvas-2d .icon):
+        (.content-view.tab.canvas .navigation-bar > .item .canvas.webgl .icon):
+        (.content-view.tab.canvas .navigation-bar > .item .shader-program > .icon):
+        (.content-view.tab.canvas .navigation-bar > .item > .hierarchical-path-component > .icon): Deleted.
+        (.content-view.tab.canvas .navigation-bar > .item .canvas .icon): Deleted.
+
+        * UserInterface/Views/CanvasTabContentView.js:
+        (WI.CanvasTabContentView):
+        (WI.CanvasTabContentView.prototype.canShowRepresentedObject):
+        (WI.CanvasTabContentView.prototype.attached):
+        (WI.CanvasTabContentView.prototype._addCanvas):
+        (WI.CanvasTabContentView.prototype._removeCanvas):
+        (WI.CanvasTabContentView.prototype._canvasTreeOutlineSelectionDidChange):
+        (WI.CanvasTabContentView.prototype._recordingAdded):
+        (WI.CanvasTabContentView.prototype._handleSpace):
+        (WI.CanvasTabContentView.prototype.showRepresentedObject): Deleted.
+        (WI.CanvasTabContentView.prototype._navigationSidebarTreeOutlineSelectionChanged): Deleted.
+        (WI.CanvasTabContentView.prototype._recordingActionIndexChanged): Deleted.
+        (WI.CanvasTabContentView.prototype._updateActionIndex): Deleted.
+        The canvas tab now maintains a tree outline of all canvases, with an
+        "Overview" element as the root. The Overview element is always the first
+        item of content browser's hierarchical path.
+
+        * UserInterface/Views/CanvasTreeElement.js:
+        (WI.CanvasTreeElement.createRecordingTreeElement):
+        (WI.CanvasTreeElement):
+        (WI.CanvasTreeElement.prototype.onattach):
+        (WI.CanvasTreeElement.prototype.onpopulate):
+        (WI.CanvasTreeElement.prototype._updateStatus):
+        (WI.CanvasTreeElement.prototype.ondetach): Deleted.
+        Make it possible to not show recordings under the Canvas element.
+        Create `isRecording` status element (spinner).
+
+        * UserInterface/Views/CollectionContentView.js:
+        (WI.CollectionContentView.prototype.shown):
+        (WI.CollectionContentView.prototype.hidden):
+        Child ContentViews need to be updated when the collection's visibility changes.
+
+        * UserInterface/Views/ContentView.js:
+        (WI.ContentView.isViewable):
+
+        * UserInterface/Views/ProgressView.css: Added.
+        (.progress-view):
+        (.progress-view > .titles):
+        (.progress-view > .titles > .title):
+        (.progress-view > .titles > .subtitle):
+        (.progress-view > .titles > .subtitle::before):
+        (.progress-view > .indeterminate-progress-spinner):
+
+        * UserInterface/Views/ProgressView.js: Added.
+        (WI.ProgressView):
+        (WI.ProgressView.prototype.get title):
+        (WI.ProgressView.prototype.set title):
+        (WI.ProgressView.prototype.get subtitle):
+        (WI.ProgressView.prototype.set subtitle):
+        (WI.ProgressView.prototype.get visible):
+        (WI.ProgressView.prototype.set visible):
+        (WI.ProgressView.prototype.initialLayout):
+        (WI.ProgressView.prototype._updateTitles):
+        New view class (not a ContentView) for showing a generic progress message,
+        with a title, subtitle, and progress spinner.
+
+        * UserInterface/Views/RecordingContentView.css:
+        (.content-view:not(.tab).recording > .preview-container):
+        Remove unnecessary styles.
+
+        * UserInterface/Views/RecordingContentView.js:
+        (WI.RecordingContentView):
+        (WI.RecordingContentView.prototype.get navigationItems):
+        (WI.RecordingContentView.prototype.get supplementalRepresentedObjects):
+        (WI.RecordingContentView.prototype.updateActionIndex):
+        (WI.RecordingContentView.prototype.get saveData):
+        (WI.RecordingContentView.prototype._exportRecording):
+        Relocate the recording export logic and UI.
+        (WI.RecordingContentView.prototype.async._generateContentCanvas2D):
+        (WI.RecordingContentView.prototype.async._generateContentCanvasWebGL):
+        (WI.RecordingContentView.prototype._sliderChanged):
+        Refactor logic for notifying the rest of the UI of changes to the action slider.
+        The selected action is now exposed as a supplemental represented object, and a
+        corresponding SupplementalRepresentedObjectsDidChange event.
+
+        * UserInterface/Views/RecordingStateDetailsSidebarPanel.js:
+        (WI.RecordingStateDetailsSidebarPanel.prototype.inspect):
+        (WI.RecordingStateDetailsSidebarPanel.prototype.set action):
+        (WI.RecordingStateDetailsSidebarPanel.prototype._generateDetailsCanvas2D):
+        (WI.RecordingStateDetailsSidebarPanel):
+        (WI.RecordingStateDetailsSidebarPanel.prototype.updateAction): Deleted.
+
+        * UserInterface/Views/RecordingTraceDetailsSidebarPanel.js:
+        (WI.RecordingTraceDetailsSidebarPanel.prototype.inspect):
+        (WI.RecordingTraceDetailsSidebarPanel.prototype.set action):
+        (WI.RecordingTraceDetailsSidebarPanel):
+        (WI.RecordingTraceDetailsSidebarPanel.prototype.updateAction): Deleted.
+        Now that the selected action is exposed to the UI as a supplemental
+        represented object, details sidebars can be more decoupled from the
+        canvas tab, and be notified of changes to the selection via `inspect()`.
+
+        * UserInterface/Views/ResourceIcons.css:
+        (.canvas > .icon): Deleted.
+        (.shader-program .icon): Deleted.
+
 2018-02-08  Nikita Vasilyev  <nvasilyev@apple.com>
 
         Web Inspector: Styles: Typing value and quickly moving focus away may display outdated value in UI
index b1da5dc..ae452ba 100644 (file)
@@ -168,7 +168,6 @@ localizedStrings["Cancel comparison"] = "Cancel comparison";
 localizedStrings["Canvas"] = "Canvas";
 localizedStrings["Canvas %d"] = "Canvas %d";
 localizedStrings["Canvas %s"] = "Canvas %s";
-localizedStrings["Canvas Overview"] = "Canvas Overview";
 localizedStrings["Canvases"] = "Canvases";
 localizedStrings["Cap"] = "Cap";
 localizedStrings["Caps"] = "Caps";
@@ -402,6 +401,7 @@ localizedStrings["Experimental"] = "Experimental";
 localizedStrings["Expires"] = "Expires";
 localizedStrings["Export"] = "Export";
 localizedStrings["Export HAR"] = "Export HAR";
+localizedStrings["Export recording (%s)"] = "Export recording (%s)";
 localizedStrings["Expression"] = "Expression";
 localizedStrings["Extension Scripts"] = "Extension Scripts";
 localizedStrings["Extensions"] = "Extensions";
@@ -496,6 +496,8 @@ localizedStrings["Image Size"] = "Image Size";
 localizedStrings["Images"] = "Images";
 localizedStrings["Immediate Pause Requested"] = "Immediate Pause Requested";
 localizedStrings["Import"] = "Import";
+localizedStrings["Import recording from file"] = "Import recording from file";
+localizedStrings["Imported Recordings"] = "Imported Recordings";
 localizedStrings["Incomplete"] = "Incomplete";
 localizedStrings["Indent"] = "Indent";
 localizedStrings["Indent width:"] = "Indent width:";
@@ -905,7 +907,7 @@ localizedStrings["Stalled"] = "Stalled";
 localizedStrings["Start Time"] = "Start Time";
 localizedStrings["Start element selection (%s)"] = "Start element selection (%s)";
 localizedStrings["Start recording (%s)\nCreate new recording (%s)"] = "Start recording (%s)\nCreate new recording (%s)";
-localizedStrings["Start recording canvas actions. Shift-click to record a single frame."] = "Start recording canvas actions. Shift-click to record a single frame.";
+localizedStrings["Start recording canvas actions.\nShift-click to record a single frame."] = "Start recording canvas actions.\nShift-click to record a single frame.";
 localizedStrings["Start to Finish"] = "Start to Finish";
 localizedStrings["State"] = "State";
 localizedStrings["Status"] = "Status";
index 21ee92a..5bfd932 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- Copyright © 2017 Apple Inc. All rights reserved. -->
-<svg viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg">
-    <polygon fill="rgb(242, 97, 97)" stroke="white" stroke-width="0.5" points="9,5.226497308103743 14,13.88675134594813 4,13.88675134594813" />
-    <circle fill="rgb(97, 242, 97)" stroke="white" stroke-width="0.5" cx="10" cy="5" r="4" />
-    <rect fill="rgb(92, 140, 229)" stroke="white" stroke-width="0.5" x="1.5" y="3" width="8" height="8" />
+<svg xmlns="http://www.w3.org/2000/svg" id="root" version="1.1" viewBox="0 0 16 16">
+    <rect x="1.5" y="1.5" width="13" height="13" rx="1.5" ry="1.5" fill="#fff" stroke="#4d4d4d"/>
+    <path d="M11,9,9,11,6,8,3,11v1.3a.7.7,0,0,0,.7.7h8.6a.7.7,0,0,0,.7-.7V11Z" fill="#666"/>
+    <circle cx="10.5" cy="5.5" r="1.75" fill="#666"/>
 </svg>
index ba182cf..7c288a6 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- Copyright © 2017 Apple Inc. All rights reserved. -->
-<svg viewBox="0 0 14 16" version="1.1" xmlns="http://www.w3.org/2000/svg">
-    <path fill="rgb(92, 140, 229)" stroke="white" stroke-width="0.5" stroke-linejoin="round" stroke-linecap="square" d="M 0.5 4.29857047 L 7.09714095 7.59714095 L 7.09714095 15.0189245 L 0.5 11.720354 L 0.5 4.29857047 Z" />
-    <path fill="rgb(242, 97, 97)" stroke="white" stroke-width="0.5" stroke-linejoin="round" stroke-linecap="square" d="M 13.697 4.299 L 7.097 7.597 L 7.097 15.019 L 13.697 11.72 L 13.697 4.299 Z"/>
-    <path fill="rgb(97, 242, 97)" stroke="white" stroke-width="0.5" stroke-linejoin="round" d="M 0.5 4.29857047 L 7.09714095 1 L 13.6908901 4.29857047 L 7.09714095 7.59714095 L 0.5 4.29857047 Z"/>
+<svg xmlns="http://www.w3.org/2000/svg" id="root" version="1.1" viewBox="0 0 16 16">
+    <rect x="1.5" y="1.5" width="13" height="13" rx="1.5" ry="1.5" fill="#fff" stroke="#4d4d4d"/>
+    <polygon points="12.109 10.361 8.451 12.469 8 12.731 7.549 12.469 3.891 10.361 3.891 5.63 4.207 5.45 7.982 3.269 11.775 5.45 12.1 5.639 12.1 6.315 12.109 10.361" fill="#ccc" stroke="#666"/>
+    <path d="M12.1,5.639,8,8,3.891,5.63M8,8v4.731" fill="none" stroke="#666"/>
 </svg>
index 0f32a3d..665b41d 100644 (file)
@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- Copyright © 2017 Apple Inc. All rights reserved. -->
 <svg xmlns="http://www.w3.org/2000/svg" id="root" version="1.1" viewBox="0 0 16 16">
-    <path d="M 6.5 8 A 1.5 1.5 0 1 1 5 6.5 1.5 1.5 0 0 1 6.5 8 Z M 5 9.5 h 6 m 0 -3 A 1.5 1.5 0 1 0 12.5 8 1.5 1.5 0 0 0 11 6.5 Z" fill="none" stroke="currentColor"/>
-    <path d="M1.5 3.729 v 8.542 a 1.127 1.127 0 0 0 1 1.22 h 11 a 1.127 1.127 0 0 0 1 -1.22 V 3.729 a 1.127 1.127 0 0 0 -1 -1.22 H 2.5 A 1.127 1.127 0 0 0 1.5 3.729 Z" fill="none" stroke="currentColor"/>
+    <circle cx="4" cy="8" r="2.5" fill="none" stroke="rgb(26, 26, 26)" stroke-width="1.2"/>
+    <circle cx="12" cy="8" r="2.5" fill="none" stroke="rgb(26, 26, 26)" stroke-width="1.2"/>
+    <line x1="4" y1="10.5" x2="12" y2="10.5" fill="none" stroke="rgb(26, 26, 26)"/>
 </svg>
index e15acf2..44aa693 100644 (file)
@@ -46,6 +46,7 @@
     <link rel="stylesheet" href="Views/CanvasContentView.css">
     <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/ChartDetailsSectionRow.css">
     <link rel="stylesheet" href="Views/CheckboxNavigationItem.css">
     <link rel="stylesheet" href="Views/ProbeDetailsSidebarPanel.css">
     <link rel="stylesheet" href="Views/ProbeSetDataGrid.css">
     <link rel="stylesheet" href="Views/ProfileView.css">
+    <link rel="stylesheet" href="Views/ProgressView.css">
     <link rel="stylesheet" href="Views/QuickConsole.css">
     <link rel="stylesheet" href="Views/RadioButtonNavigationItem.css">
     <link rel="stylesheet" href="Views/RecordingActionTreeElement.css">
     <link rel="stylesheet" href="Views/RecordingContentView.css">
     <link rel="stylesheet" href="Views/RecordingStateDetailsSidebarPanel.css">
     <link rel="stylesheet" href="Views/RecordingTraceDetailsSidebarPanel.css">
-    <link rel="stylesheet" href="Views/RecordingNavigationSidebarPanel.css">
     <link rel="stylesheet" href="Views/RenderingFrameTimelineOverviewGraph.css">
     <link rel="stylesheet" href="Views/RenderingFrameTimelineView.css">
     <link rel="stylesheet" href="Views/Resizer.css">
     <script src="Views/CanvasContentView.js"></script>
     <script src="Views/CanvasDetailsSidebarPanel.js"></script>
     <script src="Views/CanvasOverviewContentView.js"></script>
+    <script src="Views/CanvasSidebarPanel.js"></script>
     <script src="Views/CanvasTreeElement.js"></script>
     <script src="Views/ChartDetailsSectionRow.js"></script>
     <script src="Views/CheckboxNavigationItem.js"></script>
     <script src="Views/ProfileNodeDataGridNode.js"></script>
     <script src="Views/ProfileNodeTreeElement.js"></script>
     <script src="Views/ProfileView.js"></script>
+    <script src="Views/ProgressView.js"></script>
     <script src="Views/QuickConsole.js"></script>
     <script src="Views/QuickConsoleNavigationBar.js"></script>
     <script src="Views/RadioButtonNavigationItem.js"></script>
     <script src="Views/RecordingActionTreeElement.js"></script>
     <script src="Views/RecordingContentView.js"></script>
-    <script src="Views/RecordingNavigationSidebarPanel.js"></script>
     <script src="Views/RecordingStateDetailsSidebarPanel.js"></script>
     <script src="Views/RecordingTraceDetailsSidebarPanel.js"></script>
     <script src="Views/RenderingFrameTimelineDataGridNode.js"></script>
index bd0afa2..27db60f 100644 (file)
@@ -99,6 +99,9 @@ WI.RecordingAction = class RecordingAction extends WI.Object
     get hasVisibleEffect() { return this._hasVisibleEffect; }
     get stateModifiers() { return this._stateModifiers; }
 
+    get state() { return this._state; }
+    set state(state) { this._state = state; }
+
     markInvalid()
     {
         let wasValid = this._valid;
index 38a0ac6..4eecb69 100644 (file)
 
 .content-view.canvas:not(.tab) {
     background-color: hsl(0, 0%, 90%);
-
-    --progress-padding: 8px;
-}
-
-.content-view.canvas:not(.tab) > .progress {
-    padding: var(--progress-padding) 0;
-    text-align: center;
-    color: var(--text-color-gray-medium);
-}
-
-.content-view.canvas:not(.tab) > .progress > .frame-count {
-    color: var(--text-color-gray-dark);
 }
 
 .content-view.canvas:not(.tab) > .preview {
     padding: 15px;
 }
 
+.content-view.canvas:not(.tab) {
+    display: flex;
+    flex-direction: column;
+}
+
 .content-view.canvas:not(.tab) > .preview > img {
     max-width: 100%;
     max-height: 100%;
index 66095f5..4ecb401 100644 (file)
@@ -33,7 +33,7 @@ WI.CanvasContentView = class CanvasContentView extends WI.ContentView
 
         this.element.classList.add("canvas");
 
-        this._recordingProgressElement = null;
+        this._progressView = null;
         this._previewContainerElement = null;
         this._previewImageElement = null;
         this._errorElement = null;
@@ -45,7 +45,7 @@ WI.CanvasContentView = class CanvasContentView extends WI.ContentView
         this._recordingOptionElementMap = new WeakMap;
 
         if (this.representedObject.contextType === WI.Canvas.ContextType.Canvas2D || this.representedObject.contextType === WI.Canvas.ContextType.WebGL) {
-            const toolTip = WI.UIString("Start recording canvas actions. Shift-click to record a single frame.");
+            const toolTip = WI.UIString("Start recording canvas actions.\nShift-click to record a single frame.");
             const altToolTip = WI.UIString("Stop recording canvas actions");
             this._recordButtonNavigationItem = new WI.ToggleButtonNavigationItem("record-start-stop", toolTip, altToolTip, "Images/Record.svg", "Images/Stop.svg", 13, 13);
             this._recordButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.High;
@@ -66,10 +66,9 @@ WI.CanvasContentView = class CanvasContentView extends WI.ContentView
 
     get navigationItems()
     {
-        let navigationItems = [this._refreshButtonNavigationItem, this._showGridButtonNavigationItem];
-        if (this._recordButtonNavigationItem)
-            navigationItems.unshift(this._recordButtonNavigationItem);
-        return navigationItems;
+        // The toggle recording NavigationItem isn't added to the ContentBrowser's NavigationBar.
+        // It's added to the "quick access" NavigationBar shown when hovering the canvas in the overview.
+        return [this._refreshButtonNavigationItem, this._showGridButtonNavigationItem];
     }
 
     refresh()
@@ -97,6 +96,8 @@ WI.CanvasContentView = class CanvasContentView extends WI.ContentView
         super.initialLayout();
 
         let header = this.element.appendChild(document.createElement("header"));
+        header.addEventListener("click", (event) => { event.stopPropagation(); });
+
         let titles = header.appendChild(document.createElement("div"));
         titles.className = "titles";
 
@@ -120,6 +121,7 @@ WI.CanvasContentView = class CanvasContentView extends WI.ContentView
         this._previewContainerElement.className = "preview";
 
         let footer = this.element.appendChild(document.createElement("footer"));
+        footer.addEventListener("click", (event) => { event.stopPropagation(); });
 
         this._recordingSelectContainer = footer.appendChild(document.createElement("div"));
         this._recordingSelectContainer.classList.add("recordings", "hidden");
@@ -178,6 +180,7 @@ WI.CanvasContentView = class CanvasContentView extends WI.ContentView
         this.refresh();
 
         this._updateRecordNavigationItem();
+        this._updateProgressView();
     }
 
     attached()
@@ -262,16 +265,7 @@ WI.CanvasContentView = class CanvasContentView extends WI.ContentView
     _recordingStarted(event)
     {
         this._updateRecordNavigationItem();
-
-        if (!this.representedObject.isRecording)
-            return;
-
-        if (!this._recordingProgressElement) {
-            this._recordingProgressElement = this._previewContainerElement.insertAdjacentElement("beforebegin", document.createElement("div"));
-            this._recordingProgressElement.className = "progress";
-        }
-
-        this._recordingProgressElement.textContent = WI.UIString("Waiting for frames…");
+        this._updateProgressView();
     }
 
     _recordingProgress(event)
@@ -280,19 +274,7 @@ WI.CanvasContentView = class CanvasContentView extends WI.ContentView
         if (canvas !== this.representedObject)
             return;
 
-        this._recordingProgressElement.removeChildren();
-
-        let frameCountElement = this._recordingProgressElement.appendChild(document.createElement("span"));
-        frameCountElement.className = "frame-count";
-
-        let frameString = frameCount === 1 ? WI.UIString("%d Frame") : WI.UIString("%d Frames");
-        frameCountElement.textContent = frameString.format(frameCount);
-
-        this._recordingProgressElement.append(" ");
-
-        let bufferUsedElement = this._recordingProgressElement.appendChild(document.createElement("span"));
-        bufferUsedElement.className = "buffer-used";
-        bufferUsedElement.textContent = "(" + Number.bytesToString(bufferUsed) + ")";
+        this._updateProgressView(frameCount, bufferUsed);
     }
 
     _recordingStopped(event)
@@ -303,13 +285,10 @@ WI.CanvasContentView = class CanvasContentView extends WI.ContentView
         if (canvas !== this.representedObject)
             return;
 
+        this._updateProgressView();
+
         if (recording)
             this._addRecording(recording);
-
-        if (this._recordingProgressElement) {
-            this._recordingProgressElement.remove();
-            this._recordingProgressElement = null;
-        }
     }
 
     _handleRecordingSelectElementChange(event)
@@ -388,6 +367,35 @@ WI.CanvasContentView = class CanvasContentView extends WI.ContentView
         this._recordButtonNavigationItem.enabled = isRecording || !WI.canvasManager.recordingCanvas;
         this._recordButtonNavigationItem.toggled = isRecording;
 
+        this._refreshButtonNavigationItem.enabled = !isRecording;
+
         this.element.classList.toggle("is-recording", isRecording);
     }
+
+    _updateProgressView(frameCount, bufferUsed)
+    {
+        if (!this.representedObject.isRecording) {
+            if (this._progressView && this._progressView.parentView) {
+                this.removeSubview(this._progressView);
+                this._progressView = null;
+            }
+            return;
+        }
+
+        if (!this._progressView) {
+            this._progressView = new WI.ProgressView;
+            this.element.insertBefore(this._progressView.element, this._previewContainerElement);
+            this.addSubview(this._progressView);
+        }
+
+        let title;
+        if (frameCount) {
+            let formatString = frameCount === 1 ? WI.UIString("%d Frame") : WI.UIString("%d Frames");
+            title = formatString.format(frameCount);
+        } else
+            title = WI.UIString("Waiting for frames…")
+
+        this._progressView.title = title;
+        this._progressView.subtitle = bufferUsed ? Number.bytesToString(bufferUsed) : "";
+    }
 };
index 5f2545d..80db9ac 100644 (file)
@@ -55,7 +55,7 @@ WI.CanvasDetailsSidebarPanel = class CanvasDetailsSidebarPanel extends WI.Detail
 
         this.canvas = objects.find((object) => object instanceof WI.Canvas);
 
-        return true;
+        return !!this.canvas;
     }
 
     get canvas()
index c1c3a27..563eac1 100644 (file)
     background-color: white;
 }
 
-.content-view.canvas-overview .content-view.canvas > :matches(header, .progress, .preview, footer) {
+.content-view.canvas-overview .content-view.canvas {
     border: 1px solid var(--border-color);
+    cursor: pointer;
 }
 
-.content-view.canvas-overview .content-view.canvas.selected > :matches(.progress, .preview, footer),
-.content-view.canvas-overview .content-view.canvas.selected:not(.is-recording) > header {
-    border-color: var(--selected-background-color);
+.content-view.canvas-overview .content-view.canvas.is-recording {
+    border-color: red;
 }
 
 .content-view.canvas-overview .content-view.canvas > :matches(header, footer) {
     align-items: center;
     padding: 0 6px;
     height: var(--navigation-bar-height);
+    cursor: default;
 }
 
 .content-view.canvas-overview .content-view.canvas > header {
     font-size: 13px;
-    border-bottom: none;
 }
 
 .content-view.canvas-overview .content-view.canvas.is-recording > header {
     background-color: red;
-    border-color: red;
 }
 
 .content-view.canvas-overview .content-view.canvas > header > .titles,
 .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:not(:hover, .is-recording, .selected) > header > .navigation-bar {
-    visibility: hidden;
+.content-view.canvas-overview .content-view.canvas:matches(:hover, .is-recording) > header > .navigation-bar {
+    opacity: 1;
+    transition: opacity 200ms ease-in-out;
 }
 
 .content-view.canvas-overview .content-view.canvas:not(.is-recording) > header > .navigation-bar > .item.record-start-stop.disabled {
     filter: brightness(80%);
 }
 
-.content-view.canvas-overview .content-view.canvas > :matches(.progress, .preview) {
-    border-top: none;
-    border-bottom: none;
-}
-
+.content-view.canvas-overview .content-view.canvas.is-recording > .progress-view,
 .content-view.canvas-overview .content-view.canvas > .preview {
-    height: var(--preview-height);
-
-    --preview-height: 280px;
+    height: 280px;
 }
 
-.content-view.canvas-overview .content-view.canvas > .progress ~ .preview {
-    /* Keep the height of each CanvasContentView constant by subtracting the padding-top, */
-    /* padding-bottom, and text-height (1em) from the previously set height. */
-    height: calc(var(--preview-height) - 1em - (2 * var(--progress-padding)));
+.content-view.canvas-overview .content-view.canvas.is-recording > .preview {
+    display: none;
 }
 
 .content-view.canvas-overview .content-view.canvas > .preview > img {
index 8a8bd1a..fbd577e 100644 (file)
@@ -45,6 +45,11 @@ WI.CanvasOverviewContentView = class CanvasOverviewContentView extends WI.Collec
 
         this.element.classList.add("canvas-overview");
 
+        this._importButtonNavigationItem = new WI.ButtonNavigationItem("import-recording", WI.UIString("Import"), "Images/Import.svg", 15, 15);
+        this._importButtonNavigationItem.toolTip = WI.UIString("Import recording from file");
+        this._importButtonNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText;
+        this._importButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, () => { WI.canvasManager.importRecording(); });
+
         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);
@@ -53,54 +58,13 @@ WI.CanvasOverviewContentView = class CanvasOverviewContentView extends WI.Collec
         this._showGridButtonNavigationItem.activated = !!WI.settings.showImageGrid.value;
         this._showGridButtonNavigationItem.enabled = false;
         this._showGridButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._showGridButtonClicked, this);
-
-        this.selectionEnabled = true;
-
-        this._keyboardShortcuts = [
-            new WI.KeyboardShortcut(null, WI.KeyboardShortcut.Key.Up, this._handleUp.bind(this)),
-            new WI.KeyboardShortcut(null, WI.KeyboardShortcut.Key.Right, this._handleRight.bind(this)),
-            new WI.KeyboardShortcut(null, WI.KeyboardShortcut.Key.Down, this._handleDown.bind(this)),
-            new WI.KeyboardShortcut(null, WI.KeyboardShortcut.Key.Left, this._handleLeft.bind(this)),
-        ];
-
-        let recordShortcut = new WI.KeyboardShortcut(null, WI.KeyboardShortcut.Key.Space, this._handleSpace.bind(this));
-        recordShortcut.implicitlyPreventsDefault = false;
-        this._keyboardShortcuts.push(recordShortcut);
-
-        let recordSingleFrameShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.Shift, WI.KeyboardShortcut.Key.Space, this._handleSpace.bind(this));
-        recordSingleFrameShortcut.implicitlyPreventsDefault = false;
-        this._keyboardShortcuts.push(recordSingleFrameShortcut);
-
-        for (let shortcut of this._keyboardShortcuts)
-            shortcut.disabled = true;
     }
 
     // Public
 
     get navigationItems()
     {
-        return [this._refreshButtonNavigationItem, this._showGridButtonNavigationItem];
-    }
-
-    get selectionPathComponents()
-    {
-        let components = [];
-
-        if (this.supplementalRepresentedObjects.length) {
-            let [canvas] = this.supplementalRepresentedObjects;
-            let tabContentView = WI.tabBrowser.selectedTabContentView;
-            if (tabContentView) {
-                let treeElement = tabContentView.treeElementForRepresentedObject(canvas);
-                console.assert(treeElement);
-                if (treeElement) {
-                    let pathComponent = new WI.GeneralTreeElementPathComponent(treeElement);
-                    pathComponent.addEventListener(WI.HierarchicalPathComponent.Event.SiblingWasSelected, this._selectionPathComponentsChanged, this);
-                    components.push(pathComponent);
-                }
-            }
-        }
-
-        return components;
+        return [this._importButtonNavigationItem, new WI.DividerNavigationItem, this._refreshButtonNavigationItem, this._showGridButtonNavigationItem];
     }
 
     hidden()
@@ -133,22 +97,12 @@ WI.CanvasOverviewContentView = class CanvasOverviewContentView extends WI.Collec
         super.attached();
 
         WI.settings.showImageGrid.addEventListener(WI.Setting.Event.Changed, this._updateShowImageGrid, this);
-
-        this.addEventListener(WI.ContentView.Event.SupplementalRepresentedObjectsDidChange, this._supplementalRepresentedObjectsDidChange, this);
-
-        for (let shortcut of this._keyboardShortcuts)
-            shortcut.disabled = false;
     }
 
     detached()
     {
         WI.settings.showImageGrid.removeEventListener(null, null, this);
 
-        this.removeEventListener(null, null, this);
-
-        for (let shortcut of this._keyboardShortcuts)
-            shortcut.disabled = true;
-
         super.detached();
     }
 
@@ -165,43 +119,6 @@ WI.CanvasOverviewContentView = class CanvasOverviewContentView extends WI.Collec
             canvasContentView.refresh();
     }
 
-    _changeSelectedItemVertically(shift)
-    {
-        let itemElementWidth = this.element.firstElementChild.offsetWidth + (2 * this._itemMargin);
-        let itemsPerRow = Math.floor(this.element.offsetWidth / itemElementWidth);
-
-        let items = Array.from(this.representedObject.items);
-        let index = items.indexOf(this._selectedItem);
-        if (index === -1)
-            index = shift < 0 ? items.length + 1 : itemsPerRow;
-
-        index += shift * itemsPerRow;
-        if (index < 0)
-            index = items.length + index;
-
-        this.setSelectedItem(items[index % items.length]);
-    }
-
-    _changeSelectedItemHorizontally(shift)
-    {
-        let itemElementWidth = this.element.firstElementChild.offsetWidth + (2 * this._itemMargin);
-        let itemsPerRow = Math.floor(this.element.offsetWidth / itemElementWidth);
-
-        let items = Array.from(this.representedObject.items);
-        let index = items.indexOf(this._selectedItem);
-        if (index === -1)
-            index = shift >= 0 ? itemsPerRow - 1 : 0;
-
-        let selectedRow = Math.floor(index / itemsPerRow);
-        index += shift;
-        if (index < selectedRow * itemsPerRow)
-            index += itemsPerRow;
-        else if (index >= (selectedRow + 1) * itemsPerRow)
-            index -= itemsPerRow;
-
-        this.setSelectedItem(items[index]);
-    }
-
     _updateNavigationItems()
     {
         let hasItems = !!this.representedObject.items.size;
@@ -209,70 +126,16 @@ WI.CanvasOverviewContentView = class CanvasOverviewContentView extends WI.Collec
         this._showGridButtonNavigationItem.enabled = hasItems;
     }
 
-    _selectionPathComponentsChanged(event)
-    {
-        let pathComponent = event.data.pathComponent;
-        if (pathComponent.representedObject instanceof WI.Canvas)
-            this.setSelectedItem(pathComponent.representedObject);
-        else if (pathComponent.representedObject instanceof WI.Recording)
-            WI.showRepresentedObject(pathComponent.representedObject);
-    }
-
     _showGridButtonClicked(event)
     {
         WI.settings.showImageGrid.value = !this._showGridButtonNavigationItem.activated;
     }
 
-    _handleUp(event)
-    {
-        this._changeSelectedItemVertically(-1);
-    }
-
-    _handleRight(event)
-    {
-        let shift = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL ? -1 : 1;
-        this._changeSelectedItemHorizontally(shift);
-    }
-
-    _handleDown(event)
-    {
-        this._changeSelectedItemVertically(1);
-    }
-
-    _handleLeft(event)
-    {
-        let shift = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL ? 1 : -1;
-        this._changeSelectedItemHorizontally(shift);
-    }
-
-    _handleSpace(event)
-    {
-        if (WI.isEventTargetAnEditableField(event))
-            return;
-
-        if (!this._selectedItem)
-            return;
-
-        if (this._selectedItem.isRecording)
-            WI.canvasManager.stopRecording();
-        else if (!WI.canvasManager.recordingCanvas) {
-            let singleFrame = !!event.shiftKey;
-            WI.canvasManager.startRecording(this._selectedItem, singleFrame);
-        }
-
-        event.preventDefault();
-    }
-
     _updateShowImageGrid()
     {
         this._showGridButtonNavigationItem.activated = !!WI.settings.showImageGrid.value;
     }
 
-    _supplementalRepresentedObjectsDidChange()
-    {
-        this.dispatchEventToListeners(WI.ContentView.Event.SelectionPathComponentsDidChange);
-    }
-
     _contentViewMouseEnter(event)
     {
         let contentView = WI.View.fromElement(event.target);
diff --git a/Source/WebInspectorUI/UserInterface/Views/CanvasSidebarPanel.css b/Source/WebInspectorUI/UserInterface/Views/CanvasSidebarPanel.css
new file mode 100644 (file)
index 0000000..db5b068
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2018 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.navigation.canvas > .content {
+    top: var(--navigation-bar-height);
+}
+
+.sidebar > .panel.navigation.canvas > .navigation-bar > .item.record-start-stop.disabled {
+    filter: grayscale();
+    opacity: 0.5;
+}
+
+.sidebar > .panel.navigation.canvas > .content > .tree-outline .item.canvas.canvas-2d .icon {
+    content: url(../Images/Canvas2D.svg);
+}
+
+.sidebar > .panel.navigation.canvas > .content > .tree-outline .item.canvas.webgl .icon {
+    content: url(../Images/Canvas3D.svg);
+}
+
+.sidebar > .panel.navigation.canvas > .content > .navigation-bar {
+    border-top: 1px solid var(--border-color);
+}
+
+.sidebar > .panel.navigation.canvas.has-recordings > .content > .tree-outline.canvas {
+    max-height: 50%;
+    overflow-y: scroll;
+}
+
+.sidebar > .panel.navigation.canvas:not(.has-recordings) > .filter-bar,
+.sidebar > .panel.navigation.canvas:not(.has-recordings) > .content > .navigation-bar {
+    display: none;
+}
+
+.sidebar > .panel.navigation.canvas > .content > .tree-outline .item.recording > .icon {
+    content: url(../Images/Recording.svg);
+}
+
+.sidebar > .panel.navigation.canvas > .content > .tree-outline .item.shader-program > .icon {
+    content: image-set(url(../Images/DocumentGL.png) 1x, url(../Images/DocumentGL@2x.png) 2x);
+}
+
+.sidebar > .panel.navigation.canvas > .content > .tree-outline .item.folder-icon > .icon {
+    content: url(../Images/RenderingFrame.svg);
+}
+
+.sidebar > .panel.navigation.canvas > .content > .tree-outline .item.folder-icon > .status {
+    line-height: 16px;
+}
diff --git a/Source/WebInspectorUI/UserInterface/Views/CanvasSidebarPanel.js b/Source/WebInspectorUI/UserInterface/Views/CanvasSidebarPanel.js
new file mode 100644 (file)
index 0000000..e194e90
--- /dev/null
@@ -0,0 +1,385 @@
+/*
+ * Copyright (C) 2018 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.CanvasSidebarPanel = class CanvasSidebarPanel extends WI.NavigationSidebarPanel
+{
+    constructor()
+    {
+        super("canvas", WI.UIString("Canvas"));
+
+        this._canvas = null;
+        this._recording = null;
+
+        this._navigationBar = new WI.NavigationBar;
+        this._scopeBar = null;
+
+        const toolTip = WI.UIString("Start recording canvas actions.\nShift-click to record a single frame.");
+        const altToolTip = WI.UIString("Stop recording canvas actions");
+        this._recordButtonNavigationItem = new WI.ToggleButtonNavigationItem("record-start-stop", toolTip, altToolTip, "Images/Record.svg", "Images/Stop.svg", 13, 13);
+        this._recordButtonNavigationItem.enabled = false;
+        this._recordButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._toggleRecording, this);
+        this._navigationBar.addNavigationItem(this._recordButtonNavigationItem);
+
+        this.addSubview(this._navigationBar);
+
+        const suppressFiltering = true;
+        this._canvasTreeOutline = this.createContentTreeOutline(suppressFiltering);
+        this._canvasTreeOutline.element.classList.add("canvas");
+
+        this._recordingNavigationBar = new WI.NavigationBar;
+        this.contentView.addSubview(this._recordingNavigationBar);
+
+        this._recordingTreeOutline = this.contentTreeOutline;
+        this.contentView.element.appendChild(this._recordingTreeOutline.element);
+
+        this._canvasTreeOutline.addEventListener(WI.TreeOutline.Event.SelectionDidChange, this._treeOutlineSelectionDidChange, this);
+        this._recordingTreeOutline.addEventListener(WI.TreeOutline.Event.SelectionDidChange, this._treeOutlineSelectionDidChange, this);
+
+        WI.canvasManager.addEventListener(WI.CanvasManager.Event.RecordingStarted, this._updateRecordNavigationItem, this);
+        WI.canvasManager.addEventListener(WI.CanvasManager.Event.RecordingStopped, this._updateRecordNavigationItem, this);
+    }
+
+    // Public
+
+    get canvas()
+    {
+        return this._canvas;
+    }
+
+    set canvas(canvas)
+    {
+        if (this._canvas === canvas)
+            return;
+
+        if (this._canvas)
+            this._canvas.recordingCollection.removeEventListener(null, null, this);
+
+        this._canvas = canvas;
+        if (this._canvas) {
+            this._canvas.recordingCollection.addEventListener(WI.Collection.Event.ItemAdded, this._recordingAdded, this);
+            this._canvas.recordingCollection.addEventListener(WI.Collection.Event.ItemRemoved, this._recordingRemoved, this);
+        }
+
+        this._canvasChanged();
+        this._updateRecordNavigationItem();
+        this._updateRecordingScopeBar();
+    }
+
+    set recording(recording)
+    {
+        if (recording === this._recording)
+            return;
+
+        if (recording)
+            this.canvas = recording.source;
+
+        this._recording = recording;
+        this._recordingChanged();
+    }
+
+    set action(action)
+    {
+        if (!this._recording)
+            return;
+
+        let treeElement = this._recordingTreeOutline.findTreeElement(action);
+        console.assert(treeElement, "Missing tree element for recording action.", action);
+        if (!treeElement)
+            return;
+
+        this._recording[WI.CanvasSidebarPanel.SelectedActionSymbol] = action;
+
+        const omitFocus = false;
+        const selectedByUser = false;
+        treeElement.revealAndSelect(omitFocus, selectedByUser);
+    }
+
+    shown()
+    {
+        super.shown();
+
+        this.contentBrowser.addEventListener(WI.ContentBrowser.Event.CurrentRepresentedObjectsDidChange, this._currentRepresentedObjectsDidChange, this);
+    }
+
+    hidden()
+    {
+        this.contentBrowser.removeEventListener(null, null, this);
+
+        super.hidden();
+    }
+
+    // Protected
+
+    hasCustomFilters()
+    {
+        return true;
+    }
+
+    matchTreeElementAgainstCustomFilters(treeElement)
+    {
+        // Keep recording frame tree elements.
+        if (treeElement instanceof WI.FolderTreeElement)
+            return true;
+
+        // Always show the Initial State tree element.
+        if (treeElement instanceof WI.RecordingActionTreeElement && treeElement.representedObject instanceof WI.RecordingInitialStateAction)
+            return true;
+
+        return super.matchTreeElementAgainstCustomFilters(treeElement);
+    }
+
+    initialLayout()
+    {
+        super.initialLayout();
+
+        let filterFunction = (treeElement) => {
+            if (!(treeElement.representedObject instanceof WI.RecordingAction))
+                return false;
+
+            return treeElement.representedObject.isVisual || treeElement.representedObject instanceof WI.RecordingInitialStateAction;
+        };
+
+        const activatedByDefault = false;
+        const defaultToolTip = WI.UIString("Only show visual actions");
+        const activatedToolTip = WI.UIString("Show all actions");
+        this.filterBar.addFilterBarButton("recording-show-visual-only", filterFunction, activatedByDefault, defaultToolTip, activatedToolTip, "Images/Paint.svg", 15, 15);
+    }
+
+    // Private
+
+    _recordingAdded(event)
+    {
+        this.recording = event.data.item;
+
+        this._updateRecordNavigationItem();
+        this._updateRecordingScopeBar();
+    }
+
+    _recordingRemoved(event)
+    {
+        let recording = event.data.item;
+        if (recording === this.recording)
+            this.recording = this._canvas ? this._canvas.recordingCollection.toArray().lastValue : null;
+
+        this._updateRecordingScopeBar();
+    }
+
+    _scopeBarSelectionChanged()
+    {
+        let selectedScopeBarItem = this._scopeBar.selectedItems[0];
+        this.recording = selectedScopeBarItem.__recording || null;
+    }
+
+    _toggleRecording(event)
+    {
+        if (!this._canvas)
+            return;
+
+        if (this._canvas.isRecording)
+            WI.canvasManager.stopRecording();
+        else if (!WI.canvasManager.recordingCanvas) {
+            let singleFrame = event.data.nativeEvent.shiftKey;
+            WI.canvasManager.startRecording(this._canvas, singleFrame);
+        }
+    }
+
+    _currentRepresentedObjectsDidChange(event)
+    {
+        let objects = this.contentBrowser.currentRepresentedObjects;
+
+        let canvas = objects.find((object) => object instanceof WI.Canvas);
+        if (canvas) {
+            this.canvas = canvas;
+            return;
+        }
+
+        let shaderProgram = objects.find((object) => object instanceof WI.ShaderProgram);
+        if (shaderProgram) {
+            this.canvas = shaderProgram.canvas;
+            let treeElement = this._canvasTreeOutline.findTreeElement(shaderProgram);
+            const omitFocus = false;
+            const selectedByUser = false;
+            treeElement.revealAndSelect(omitFocus, selectedByUser);
+            return;
+        }
+
+        let recording = objects.find((object) => object instanceof WI.Recording);
+        if (recording) {
+            this.canvas = recording.source;
+            this.recording = recording;
+            this.action = objects.find((object) => object instanceof WI.RecordingAction);
+            return;
+        }
+
+        this.canvas = null;
+    }
+
+    _treeOutlineSelectionDidChange(event)
+    {
+        let treeElement = event.data.selectedElement;
+        if (!treeElement)
+            return;
+
+        if ((treeElement instanceof WI.CanvasTreeElement) || (treeElement instanceof WI.ShaderProgramTreeElement)) {
+            this.showDefaultContentViewForTreeElement(treeElement);
+            return;
+        }
+
+        if (treeElement instanceof WI.FolderTreeElement)
+            treeElement = treeElement.children.lastValue;
+
+        if (!(treeElement instanceof WI.RecordingActionTreeElement))
+            return;
+
+        console.assert(this._recording, "Missing recording for action tree element.", treeElement);
+        this._recording[WI.CanvasSidebarPanel.SelectedActionSymbol] = treeElement.representedObject;
+
+        let recordingContentView = this.contentBrowser.showContentViewForRepresentedObject(this._recording);
+        if (recordingContentView)
+            recordingContentView.updateActionIndex(treeElement.index);
+    }
+
+    _canvasChanged()
+    {
+        this._canvasTreeOutline.removeChildren();
+
+        if (!this._canvas)
+            return;
+
+        const showRecordings = false;
+        let canvasTreeElement = new WI.CanvasTreeElement(this._canvas, showRecordings);
+        canvasTreeElement.expanded = true;
+        this._canvasTreeOutline.appendChild(canvasTreeElement);
+
+        const omitFocus = false;
+        const selectedByUser = false;
+        canvasTreeElement.revealAndSelect(omitFocus, selectedByUser);
+
+        if (WI.Canvas.ContextType.Canvas2D || this._canvas.contextType === WI.Canvas.ContextType.WebGL)
+            this._recordButtonNavigationItem.enabled = true;
+
+        let defaultSelectedRecording = null;
+        if (this._canvas.recordingCollection.items.size)
+            defaultSelectedRecording = this._canvas.recordingCollection.toArray().lastValue;
+
+        this.recording = defaultSelectedRecording;
+    }
+
+    _recordingChanged()
+    {
+        this._recordingTreeOutline.removeChildren();
+
+        if (!this._recording)
+            return;
+
+        let recording = this._recording;
+
+        this._recording.actions.then((actions) => {
+            if (recording !== this._recording)
+                return;
+
+            this._recordingTreeOutline.element.dataset.indent = Number.countDigits(actions.length);
+
+            if (actions[0] instanceof WI.RecordingInitialStateAction)
+                this._recordingTreeOutline.appendChild(new WI.RecordingActionTreeElement(actions[0], 0, this._recording.type));
+
+            let cumulativeActionIndex = 1;
+            this._recording.frames.forEach((frame, frameIndex) => {
+                let folder = new WI.FolderTreeElement(WI.UIString("Frame %d").format((frameIndex + 1).toLocaleString()));
+                this._recordingTreeOutline.appendChild(folder);
+
+                for (let i = 0; i < frame.actions.length; ++i)
+                    folder.appendChild(new WI.RecordingActionTreeElement(frame.actions[i], cumulativeActionIndex + i, this._recording.type));
+
+                if (!isNaN(frame.duration)) {
+                    const higherResolution = true;
+                    folder.status = Number.secondsToString(frame.duration / 1000, higherResolution);
+                }
+
+                if (frame.incomplete)
+                    folder.subtitle = WI.UIString("Incomplete");
+
+                if (this._recording.frames.length === 1)
+                    folder.expand();
+
+                cumulativeActionIndex += frame.actions.length;
+            });
+
+            if (this._scopeBar) {
+                let scopeBarItem = this._scopeBar.item(this._recording.displayName);
+                console.assert(scopeBarItem, "Missing scopeBarItem for recording.", this._recording);
+                scopeBarItem.selected = true;
+            }
+
+            this.action = this._recording[WI.CanvasSidebarPanel.SelectedActionSymbol] || actions[0];
+        });
+    }
+
+    _updateRecordNavigationItem()
+    {
+        if (!this._canvas || !(this._canvas.contextType === WI.Canvas.ContextType.Canvas2D || this._canvas.contextType === WI.Canvas.ContextType.WebGL)) {
+            this._recordButtonNavigationItem.enabled = false;
+            return;
+        }
+
+        let isRecording = this._canvas.isRecording;
+        this._recordButtonNavigationItem.enabled = isRecording || !WI.canvasManager.recordingCanvas;
+        this._recordButtonNavigationItem.toggled = isRecording;
+    }
+
+    _updateRecordingScopeBar()
+    {
+        if (this._scopeBar) {
+            this._recordingNavigationBar.removeNavigationItem(this._scopeBar);
+            this._scopeBar = null;
+        }
+
+        if (!this._canvas || !this._canvas.recordingCollection.items.size) {
+            this.element.classList.remove("has-recordings");
+            return;
+        }
+
+        let scopeBarItems = [];
+        let selectedScopeBarItem = null;
+        for (let recording of this._canvas.recordingCollection.items) {
+            let scopeBarItem = new WI.ScopeBarItem(recording.displayName, recording.displayName);
+            if (recording === this._recording)
+                selectedScopeBarItem = scopeBarItem;
+            scopeBarItem.__recording = recording;
+            scopeBarItems.push(scopeBarItem);
+        }
+
+        if (!selectedScopeBarItem)
+            selectedScopeBarItem = scopeBarItems[0];
+
+        this._scopeBar = new WI.ScopeBar("canvas-recordinga-scope-bar", scopeBarItems, selectedScopeBarItem, true);
+        this._scopeBar.addEventListener(WI.ScopeBar.Event.SelectionChanged, this._scopeBarSelectionChanged, this);
+        this._recordingNavigationBar.insertNavigationItem(this._scopeBar, 0);
+
+        this.element.classList.add("has-recordings");
+    }
+};
+
+WI.CanvasSidebarPanel.SelectedActionSymbol = Symbol("selected-action");
index 0233ab9..27cb204 100644 (file)
  * THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-.content-view.tab.canvas .navigation-bar > .item > .hierarchical-path-component > .icon {
-    opacity: 0.7;
+.content-view.tab.canvas .navigation-bar > .item .canvas-overview .icon {
+    content: url(../Images/CanvasOverview.svg);
 }
 
-/* FIXME: this can be removed once <https://webkit.org/b/177606> is complete. */
-.content-view.tab.canvas .navigation-bar > .item .canvas .icon {
-    content: url(../Images/Canvas.svg);
+.content-view.tab.canvas .navigation-bar > .item .canvas.canvas-2d .icon {
+    content: url(../Images/Canvas2D.svg);
+}
+
+.content-view.tab.canvas .navigation-bar > .item .canvas.webgl .icon {
+    content: url(../Images/Canvas3D.svg);
 }
 
 .content-view.tab.canvas .navigation-bar > .item .recording > .icon {
     content: url(../Images/Recording.svg);
 }
+
+.content-view.tab.canvas .navigation-bar > .item .shader-program > .icon {
+    content: image-set(url(../Images/DocumentGL.png) 1x, url(../Images/DocumentGL@2x.png) 2x);
+}
index 8364b95..af99aba 100644 (file)
@@ -31,7 +31,7 @@ WI.CanvasTabContentView = class CanvasTabContentView extends WI.ContentBrowserTa
 
         let tabBarItem = WI.GeneralTabBarItem.fromTabInfo(WI.CanvasTabContentView.tabInfo());
 
-        const navigationSidebarPanelConstructor = WI.RecordingNavigationSidebarPanel;
+        const navigationSidebarPanelConstructor = WI.CanvasSidebarPanel;
         const detailsSidebarPanelConstructors = [WI.RecordingStateDetailsSidebarPanel, WI.RecordingTraceDetailsSidebarPanel, WI.CanvasDetailsSidebarPanel];
         const disableBackForward = true;
         super("canvas", ["canvas"], tabBarItem, navigationSidebarPanelConstructor, detailsSidebarPanelConstructors, disableBackForward);
@@ -41,19 +41,18 @@ WI.CanvasTabContentView = class CanvasTabContentView extends WI.ContentBrowserTa
         this._canvasTreeOutline = new WI.TreeOutline;
         this._canvasTreeOutline.addEventListener(WI.TreeOutline.Event.SelectionDidChange, this._canvasTreeOutlineSelectionDidChange, this);
 
-        const leftArrow = "Images/BackForwardArrows.svg#left-arrow-mask";
-        const rightArrow = "Images/BackForwardArrows.svg#right-arrow-mask";
-        let isRTL = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL;
-        let backButtonImage = isRTL ? rightArrow : leftArrow;
-        this._overviewNavigationItem = new WI.ButtonNavigationItem("canvas-overview", WI.UIString("Canvas Overview"), backButtonImage, 8, 13);
-        this._overviewNavigationItem.hidden = true;
-        this._overviewNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.High;
-        this._overviewNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, () => { this.showRepresentedObject(this._canvasCollection); });
+        this._overviewTreeElement = new WI.GeneralTreeElement("canvas-overview", WI.UIString("Overview"), null, this._canvasCollection);
+        this._canvasTreeOutline.appendChild(this._overviewTreeElement);
 
-        this.contentBrowser.navigationBar.insertNavigationItem(this._overviewNavigationItem, 2);
-        this.contentBrowser.navigationBar.insertNavigationItem(new WI.DividerNavigationItem, 3);
+        this._importedRecordingsTreeElement = new WI.FolderTreeElement(WI.UIString("Imported Recordings"));
+        this._importedRecordingsTreeElement.hidden = true;
+        this._overviewTreeElement.appendChild(this._importedRecordingsTreeElement);
 
-        this.navigationSidebarPanel.contentTreeOutline.addEventListener(WI.TreeOutline.Event.SelectionDidChange, this._navigationSidebarTreeOutlineSelectionChanged, this);
+        this._recordShortcut = new WI.KeyboardShortcut(null, WI.KeyboardShortcut.Key.Space, this._handleSpace.bind(this));
+        this._recordShortcut.implicitlyPreventsDefault = false;
+
+        this._recordSingleFrameShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.Shift, WI.KeyboardShortcut.Key.Space, this._handleSpace.bind(this));
+        this._recordSingleFrameShortcut.implicitlyPreventsDefault = false;
     }
 
     static tabInfo()
@@ -88,31 +87,12 @@ WI.CanvasTabContentView = class CanvasTabContentView extends WI.ContentBrowserTa
 
     canShowRepresentedObject(representedObject)
     {
-        return representedObject instanceof WI.CanvasCollection
+        return representedObject instanceof WI.Canvas
+            || representedObject instanceof WI.CanvasCollection
             || representedObject instanceof WI.Recording
             || representedObject instanceof WI.ShaderProgram;
     }
 
-    showRepresentedObject(representedObject, cookie)
-    {
-        super.showRepresentedObject(representedObject, cookie);
-
-        this.navigationSidebarPanel.recording = null;
-
-        if (representedObject instanceof WI.CanvasCollection || representedObject instanceof WI.ShaderProgram) {
-            this._overviewNavigationItem.hidden = true;
-            return;
-        }
-
-        if (representedObject instanceof WI.Recording) {
-            this._overviewNavigationItem.hidden = false;
-            this.navigationSidebarPanel.recording = representedObject;
-            return;
-        }
-
-        console.assert(false, "Should not be reached.");
-    }
-
     shown()
     {
         super.shown();
@@ -141,7 +121,6 @@ WI.CanvasTabContentView = class CanvasTabContentView extends WI.ContentBrowserTa
         WI.canvasManager.addEventListener(WI.CanvasManager.Event.CanvasRemoved, this._handleCanvasRemoved, this);
         WI.canvasManager.addEventListener(WI.CanvasManager.Event.RecordingImported, this._recordingImportedOrStopped, this);
         WI.canvasManager.addEventListener(WI.CanvasManager.Event.RecordingStopped, this._recordingImportedOrStopped, this);
-        WI.RecordingContentView.addEventListener(WI.RecordingContentView.Event.RecordingActionIndexChanged, this._recordingActionIndexChanged, this);
 
         let canvases = new Set(Array.from(this._canvasCollection.items).concat(WI.canvasManager.canvases));
 
@@ -168,7 +147,7 @@ WI.CanvasTabContentView = class CanvasTabContentView extends WI.ContentBrowserTa
 
     _addCanvas(canvas)
     {
-        this._canvasTreeOutline.appendChild(new WI.CanvasTreeElement(canvas));
+        this._overviewTreeElement.appendChild(new WI.CanvasTreeElement(canvas));
         this._canvasCollection.add(canvas);
 
         for (let recording of canvas.recordingCollection.items)
@@ -177,19 +156,11 @@ WI.CanvasTabContentView = class CanvasTabContentView extends WI.ContentBrowserTa
 
     _removeCanvas(canvas)
     {
-        // Move all existing recordings for the removed canvas to be imported recordings, as the
-        // recording's source is no longer valid.
-        for (let recording of canvas.recordingCollection.items) {
-            recording.source = null;
-            recording.createDisplayName();
-
-            const subtitle = null;
-            this._canvasTreeOutline.appendChild(new WI.GeneralTreeElement(["recording"], recording.displayName, subtitle, recording));
-        }
+        // FIXME: Create tree elements/cards for recordings belonging to the removed canvas.
 
         let treeElement = this._canvasTreeOutline.findTreeElement(canvas);
         console.assert(treeElement, "Missing tree element for canvas.", canvas);
-        this._canvasTreeOutline.removeChild(treeElement);
+        this._overviewTreeElement.removeChild(treeElement);
         this._canvasCollection.remove(canvas);
 
         let currentContentView = this.contentBrowser.currentContentView;
@@ -214,22 +185,12 @@ WI.CanvasTabContentView = class CanvasTabContentView extends WI.ContentBrowserTa
             return;
 
         let representedObject = selectedElement.representedObject;
-
-        if (this.canShowRepresentedObject(representedObject)) {
-            this.showRepresentedObject(representedObject);
-
-            if (representedObject instanceof WI.Recording)
-                this._updateActionIndex(0);
-            return;
-        }
-
-        if (representedObject instanceof WI.Canvas) {
-            this.showRepresentedObject(this._canvasCollection);
-            this.contentBrowser.currentContentView.setSelectedItem(representedObject);
+        if (!this.canShowRepresentedObject(representedObject)) {
+            console.assert(false, "Unexpected representedObject.", representedObject);
             return;
         }
 
-        console.assert(false, "Unexpected representedObject.", representedObject);
+        this.showRepresentedObject(representedObject);
     }
 
     _recordingImportedOrStopped(event)
@@ -243,57 +204,39 @@ WI.CanvasTabContentView = class CanvasTabContentView extends WI.ContentBrowserTa
         });
     }
 
-    _navigationSidebarTreeOutlineSelectionChanged(event)
-    {
-        if (!event.data.selectedElement)
-            return;
-
-        let recordingContentView = this.contentBrowser.currentContentView;
-        if (!(recordingContentView instanceof WI.RecordingContentView))
-            return;
-
-        let selectedTreeElement = event.data.selectedElement;
-        if (selectedTreeElement instanceof WI.FolderTreeElement)
-            selectedTreeElement = selectedTreeElement.children.lastValue;
-
-        this._updateActionIndex(selectedTreeElement.index, {suppressNavigationSidebarUpdate: true});
-    }
-
     _recordingAdded(recording, options = {})
     {
         if (!recording.source) {
             const subtitle = null;
             let recordingTreeElement = new WI.GeneralTreeElement(["recording"], recording.displayName, subtitle, recording);
-            this._canvasTreeOutline.appendChild(recordingTreeElement);
+            this._importedRecordingsTreeElement.hidden = false;
+            this._importedRecordingsTreeElement.appendChild(recordingTreeElement);
         }
 
-        if (!options.suppressShowRecording) {
+        if (!options.suppressShowRecording)
             this.showRepresentedObject(recording);
-            this._updateActionIndex(0, {suppressNavigationSidebarUpdate: true});
-        }
     }
 
-    _recordingActionIndexChanged(event)
+    _handleSpace(event)
     {
-        if (event.target !== this.contentBrowser.currentContentView)
+        if (WI.isEventTargetAnEditableField(event))
             return;
 
-        this._updateActionIndex(event.data.index);
-    }
+        if (!this.navigationSidebarPanel)
+            return;
 
-    _updateActionIndex(index, options = {})
-    {
-        options.actionCompletedCallback = (action, context) => {
-            for (let detailsSidebarPanel of this.detailsSidebarPanels) {
-                if (detailsSidebarPanel.updateAction)
-                    detailsSidebarPanel.updateAction(action, context, options);
-            }
-        };
+        let canvas = this.navigationSidebarPanel.canvas;
+        if (!canvas)
+            return;
 
-        if (!options.suppressNavigationSidebarUpdate)
-            this.navigationSidebarPanel.updateActionIndex(index, options);
+        if (canvas.isRecording)
+            WI.canvasManager.stopRecording();
+        else if (!WI.canvasManager.recordingCanvas) {
+            let singleFrame = !!event.shiftKey;
+            WI.canvasManager.startRecording(canvas, singleFrame);
+        }
 
-        this.contentBrowser.currentContentView.updateActionIndex(index, options);
+        event.preventDefault();
     }
 };
 
index 29f3d93..6631cda 100644 (file)
 
 WI.CanvasTreeElement = class CanvasTreeElement extends WI.FolderizedTreeElement
 {
-    constructor(representedObject)
+    constructor(representedObject, showRecordings = true)
     {
         console.assert(representedObject instanceof WI.Canvas);
 
-        const subtitle = null;
+        let subtitle = WI.Canvas.displayNameForContextType(representedObject.contextType);
         super(["canvas", representedObject.contextType], representedObject.displayName, subtitle, representedObject);
 
         this.registerFolderizeSettings("shader-programs", WI.UIString("Shader Programs"), this.representedObject.shaderProgramCollection, WI.ShaderProgramTreeElement);
 
-        function createRecordingTreeElement(recording) {
-            return new WI.GeneralTreeElement(["recording"], recording.displayName, subtitle, recording);
+        WI.canvasManager.addEventListener(WI.CanvasManager.Event.RecordingStarted, this._updateStatus, this);
+        WI.canvasManager.addEventListener(WI.CanvasManager.Event.RecordingStopped, this._updateStatus, this);
+
+        this.representedObject.shaderProgramCollection.addEventListener(WI.Collection.Event.ItemAdded, this._handleItemAdded, this);
+        this.representedObject.shaderProgramCollection.addEventListener(WI.Collection.Event.ItemRemoved, this._handleItemRemoved, this);
+
+        this._showRecordings = showRecordings;
+        if (this._showRecordings) {
+            function createRecordingTreeElement(recording) {
+                return new WI.GeneralTreeElement(["recording"], recording.displayName, null, recording);
+            }
+            this.registerFolderizeSettings("recordings", WI.UIString("Recordings"), this.representedObject.recordingCollection, createRecordingTreeElement);
+
+            this.representedObject.recordingCollection.addEventListener(WI.Collection.Event.ItemAdded, this._handleItemAdded, this);
+            this.representedObject.recordingCollection.addEventListener(WI.Collection.Event.ItemRemoved, this._handleItemRemoved, this);
         }
-        this.registerFolderizeSettings("recordings", WI.UIString("Recordings"), this.representedObject.recordingCollection, createRecordingTreeElement);
     }
 
     // Protected
@@ -46,29 +58,12 @@ WI.CanvasTreeElement = class CanvasTreeElement extends WI.FolderizedTreeElement
     {
         super.onattach();
 
-        this.representedObject.shaderProgramCollection.addEventListener(WI.Collection.Event.ItemAdded, this._handleItemAdded, this);
-        this.representedObject.shaderProgramCollection.addEventListener(WI.Collection.Event.ItemRemoved, this._handleItemRemoved, this);
-
-        this.representedObject.recordingCollection.addEventListener(WI.Collection.Event.ItemAdded, this._handleItemAdded, this);
-        this.representedObject.recordingCollection.addEventListener(WI.Collection.Event.ItemRemoved, this._handleItemRemoved, this);
-
         this.element.addEventListener("mouseover", this._handleMouseOver.bind(this));
         this.element.addEventListener("mouseout", this._handleMouseOut.bind(this));
 
         this.onpopulate();
     }
 
-    ondetach()
-    {
-        this.representedObject.shaderProgramCollection.removeEventListener(WI.Collection.Event.ItemAdded, this._handleItemAdded, this);
-        this.representedObject.shaderProgramCollection.removeEventListener(WI.Collection.Event.ItemRemoved, this._handleItemRemoved, this);
-
-        this.representedObject.recordingCollection.removeEventListener(WI.Collection.Event.ItemAdded, this._handleItemAdded, this);
-        this.representedObject.recordingCollection.removeEventListener(WI.Collection.Event.ItemRemoved, this._handleItemRemoved, this);
-
-        super.ondetach();
-    }
-
     onpopulate()
     {
         super.onpopulate();
@@ -83,8 +78,10 @@ WI.CanvasTreeElement = class CanvasTreeElement extends WI.FolderizedTreeElement
         for (let program of this.representedObject.shaderProgramCollection.items)
             this.addChildForRepresentedObject(program);
 
-        for (let recording of this.representedObject.recordingCollection.items)
-            this.addChildForRepresentedObject(recording);
+        if (this._showRecordings) {
+            for (let recording of this.representedObject.recordingCollection.items)
+                this.addChildForRepresentedObject(recording);
+        }
     }
 
     populateContextMenu(contextMenu, event)
@@ -137,4 +134,18 @@ WI.CanvasTreeElement = class CanvasTreeElement extends WI.FolderizedTreeElement
     {
         WI.domTreeManager.hideDOMNodeHighlight();
     }
+
+    _updateStatus()
+    {
+        if (this.representedObject.isRecording) {
+            if (!this.status || !this.status[WI.CanvasTreeElement.SpinnerSymbol]) {
+                let spinner = new WI.IndeterminateProgressSpinner;
+                this.status = spinner.element;
+                this.status[WI.CanvasTreeElement.SpinnerSymbol] = true;
+            }
+        } else {
+            if (this.status && this.status[WI.CanvasTreeElement.SpinnerSymbol])
+                this.status = "";
+        }
+    }
 };
index 1209f00..e4817b9 100644 (file)
@@ -51,6 +51,22 @@ WI.CollectionContentView = class CollectionContentView extends WI.ContentView
         return [];
     }
 
+    shown()
+    {
+        super.shown();
+
+        for (let contentView of this._contentViewMap.values())
+            contentView.shown();
+    }
+
+    hidden()
+    {
+        for (let contentView of this._contentViewMap.values())
+            contentView.hidden();
+
+        super.hidden();
+    }
+
     get selectionEnabled()
     {
         return this._selectionEnabled;
index 459e8cd..4050b7c 100644 (file)
@@ -255,6 +255,8 @@ 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)
diff --git a/Source/WebInspectorUI/UserInterface/Views/ProgressView.css b/Source/WebInspectorUI/UserInterface/Views/ProgressView.css
new file mode 100644 (file)
index 0000000..e67b7ca
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+
+.progress-view {
+    display: flex;
+    flex-shrink: 0;
+    flex-direction: column;
+    justify-content: center;
+    font-size: var(--message-text-view-font-size);
+    text-align: center;
+}
+
+.progress-view > .titles {
+    padding: 15px 0;
+}
+
+.progress-view > .titles > .title {
+    color: var(--text-color-gray-dark);
+}
+
+.progress-view > .titles > .subtitle {
+    color: var(--text-color-gray-medium);
+}
+
+.progress-view > .titles > .subtitle::before {
+    content: "\2002\2012\2002";
+}
+
+.progress-view > .indeterminate-progress-spinner {
+    flex-shrink: 0;
+    width: 24px;
+    height: 24px;
+    margin: 0 auto;
+}
diff --git a/Source/WebInspectorUI/UserInterface/Views/ProgressView.js b/Source/WebInspectorUI/UserInterface/Views/ProgressView.js
new file mode 100644 (file)
index 0000000..5e14059
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2018 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.ProgressView = class ProgressView extends WI.View
+{
+    constructor(title, subtitle)
+    {
+        super();
+
+        this.element.classList.add("progress-view");
+
+        this._title = title || "";
+        this._subtitle = subtitle || "";
+
+        this._titlesElement = null;
+        this._titleElement = null;
+        this._subtitleElement = null;
+    }
+
+    // Public
+
+    get title()
+    {
+        return this._title;
+    }
+
+    set title(title)
+    {
+        title = title || "";
+        if (this._title === title)
+            return;
+
+        this._title = title;
+        this._updateTitles();
+    }
+
+    get subtitle()
+    {
+        return this._subtitle;
+    }
+
+    set subtitle(subtitle)
+    {
+        subtitle = subtitle || "";
+        if (this._subtitle === subtitle)
+            return;
+
+        this._subtitle = subtitle;
+        this._updateTitles();
+    }
+
+    get visible() { return this._visible; }
+
+    set visible(x)
+    {
+        if (this._visible === x)
+            return;
+
+        // FIXME: remove once <https://webkit.org/b/150741> is fixed.
+        this._visible = x;
+        this.element.classList.toggle("hidden", !this._visible);
+    }
+
+    // Protected
+
+    initialLayout()
+    {
+        super.initialLayout();
+
+        this._titlesElement = this.element.appendChild(document.createElement("div"));
+        this._titlesElement.className = "titles";
+        this._titleElement = this._titlesElement.appendChild(document.createElement("span"));
+        this._titleElement.className = "title";
+        this._subtitleElement = this._titlesElement.appendChild(document.createElement("span"));
+        this._subtitleElement.className = "subtitle";
+
+        let spinner = new WI.IndeterminateProgressSpinner;
+        this.element.appendChild(spinner.element);
+
+        this._updateTitles();
+    }
+
+    _updateTitles()
+    {
+        if (!this._titlesElement)
+            return;
+
+        this._titleElement.textContent = this._title;
+        this._titleElement.classList.toggle("hidden", !this._title);
+
+        this._subtitleElement.textContent = this._subtitle;
+        this._subtitleElement.classList.toggle("hidden", !this._subtitle);
+    }
+};
index a2074ea..d224b38 100644 (file)
@@ -67,9 +67,6 @@
     flex: 1;
     justify-content: center;
     align-items: center;
-    position: relative;
-    width: 100%;
-    height: 100%;
 }
 
 .content-view:not(.tab).recording :matches(img, canvas) {
index ce30755..f0ea465 100644 (file)
@@ -32,6 +32,7 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
         super(representedObject);
 
         this._index = NaN;
+        this._action = null;
         this._snapshots = [];
         this._initialContent = null;
         this._throttler = this.throttle(200);
@@ -53,6 +54,12 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
             this._showGridButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
             this._showGridButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._showGridButtonClicked, this);
             this._showGridButtonNavigationItem.activated = !!WI.settings.showImageGrid.value;
+
+            this._exportButtonNavigationItem = new WI.ButtonNavigationItem("export-recording", WI.UIString("Export"), "Images/Export.svg", 15, 15);
+            this._exportButtonNavigationItem.toolTip = WI.UIString("Export recording (%s)").format(WI.saveKeyboardShortcut.displayName);
+            this._exportButtonNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText;
+            this._exportButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.High;
+            this._exportButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, () => { this._exportRecording(); });
         }
     }
 
@@ -88,16 +95,23 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
     {
         let isCanvas2D = this.representedObject.type === WI.Recording.Type.Canvas2D;
         let isCanvasWebGL = this.representedObject.type === WI.Recording.Type.CanvasWebGL;
-        if (isCanvas2D || isCanvasWebGL) {
-            let navigationItems = [this._showGridButtonNavigationItem];
-            if (isCanvas2D && WI.RecordingContentView.supportsCanvasPathDebugging())
-                navigationItems.unshift(this._showPathButtonNavigationItem);
-            return navigationItems;
-        }
-        return [];
+        if (!isCanvas2D && !isCanvasWebGL)
+            return [];
+
+        let navigationItems = [this._exportButtonNavigationItem, new WI.DividerNavigationItem];
+        if (isCanvas2D && WI.RecordingContentView.supportsCanvasPathDebugging())
+            navigationItems.push(this._showPathButtonNavigationItem);
+
+        navigationItems.push(this._showGridButtonNavigationItem);
+        return navigationItems;
     }
 
-    updateActionIndex(index, options = {})
+    get supplementalRepresentedObjects()
+    {
+        return this._action ? [this._action] : [];
+    }
+
+    updateActionIndex(index)
     {
         if (!this.representedObject)
             return;
@@ -114,9 +128,9 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
             this._updateSliderValue();
 
             if (this.representedObject.type === WI.Recording.Type.Canvas2D)
-                this._throttler._generateContentCanvas2D(index, actions, options);
+                this._throttler._generateContentCanvas2D(index, actions);
             else if (this.representedObject.type === WI.Recording.Type.CanvasWebGL)
-                this._throttler._generateContentCanvasWebGL(index, actions, options);
+                this._throttler._generateContentCanvasWebGL(index, actions);
         });
     }
 
@@ -150,15 +164,7 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
 
     get saveData()
     {
-        let filename = this.representedObject.displayName;
-        if (!filename.endsWith(".json"))
-            filename += ".json";
-
-        return {
-            url: "web-inspector:///" + encodeURI(filename),
-            content: JSON.stringify(this.representedObject.toJSON()),
-            forceSaveAs: true,
-        };
+        return {customSaveHandler: () => { this._exportRecording(); }};
     }
 
     // Protected
@@ -191,14 +197,31 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
 
     // Private
 
-    async _generateContentCanvas2D(index, actions, options = {})
+    _exportRecording()
+    {
+        if (!this.representedObject) {
+            InspectorFrontendHost.beep();
+            return;
+        }
+
+        let filename = this.representedObject.displayName;
+        let url = "web-inspector:///" + encodeURI(filename) + ".json";
+
+        WI.saveDataToFile({
+            url,
+            content: JSON.stringify(this.representedObject.toJSON()),
+            forceSaveAs: true,
+        });
+    }
+
+    async _generateContentCanvas2D(index, actions)
     {
         let imageLoad = (event) => {
             // Loading took too long and the current action index has already changed.
             if (index !== this._index)
                 return;
 
-            this._generateContentCanvas2D(index, actions, options);
+            this._generateContentCanvas2D(index, actions);
         };
 
         let initialState = this.representedObject.initialState;
@@ -228,6 +251,10 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
                 if (!(name in snapshot.context))
                     continue;
 
+                // Skip internal state used for path debugging.
+                if (name === "currentX" || name === "currentY")
+                    continue;
+
                 try {
                     if (WI.RecordingAction.isFunctionForType(this.representedObject.type, name))
                         snapshot.context[name](...snapshot.state[name]);
@@ -304,11 +331,43 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
             } else if (this._pathContext)
                 this._pathContext.canvas.remove();
 
-            callback();
+            let state = {
+                currentX: snapshot.context.currentX,
+                currentY: snapshot.context.currentY,
+                direction: snapshot.context.direction,
+                fillStyle: snapshot.context.fillStyle,
+                font: snapshot.context.font,
+                globalAlpha: snapshot.context.globalAlpha,
+                globalCompositeOperation: snapshot.context.globalCompositeOperation,
+                imageSmoothingEnabled: snapshot.context.imageSmoothingEnabled,
+                imageSmoothingQuality: snapshot.context.imageSmoothingQuality,
+                lineCap: snapshot.context.lineCap,
+                lineDash: snapshot.context.getLineDash(),
+                lineDashOffset: snapshot.context.lineDashOffset,
+                lineJoin: snapshot.context.lineJoin,
+                lineWidth: snapshot.context.lineWidth,
+                miterLimit: snapshot.context.miterLimit,
+                shadowBlur: snapshot.context.shadowBlur,
+                shadowColor: snapshot.context.shadowColor,
+                shadowOffsetX: snapshot.context.shadowOffsetX,
+                shadowOffsetY: snapshot.context.shadowOffsetY,
+                strokeStyle: snapshot.context.strokeStyle,
+                textAlign: snapshot.context.textAlign,
+                textBaseline: snapshot.context.textBaseline,
+                transform: snapshot.context.getTransform(),
+                webkitImageSmoothingEnabled: snapshot.context.webkitImageSmoothingEnabled,
+                webkitLineDash: snapshot.context.webkitLineDash,
+                webkitLineDashOffset: snapshot.context.webkitLineDashOffset,
+            };
+
+            if (WI.RecordingContentView.supportsCanvasPathDebugging())
+                state.setPath = [snapshot.context.getPath()];
 
             snapshot.context.restore();
             while (saveCount-- > 0)
                 snapshot.context.restore();
+
+            return state;
         };
 
         if (!snapshot) {
@@ -384,37 +443,7 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
                 startIndex = this._snapshots[lastSnapshotIndex].index;
             }
 
-            applyActions(startIndex, snapshot.index - 1, () => {
-                snapshot.state = {
-                    direction: snapshot.context.direction,
-                    fillStyle: snapshot.context.fillStyle,
-                    font: snapshot.context.font,
-                    globalAlpha: snapshot.context.globalAlpha,
-                    globalCompositeOperation: snapshot.context.globalCompositeOperation,
-                    imageSmoothingEnabled: snapshot.context.imageSmoothingEnabled,
-                    imageSmoothingQuality: snapshot.context.imageSmoothingQuality,
-                    lineCap: snapshot.context.lineCap,
-                    lineDashOffset: snapshot.context.lineDashOffset,
-                    lineJoin: snapshot.context.lineJoin,
-                    lineWidth: snapshot.context.lineWidth,
-                    miterLimit: snapshot.context.miterLimit,
-                    setLineDash: [snapshot.context.getLineDash()],
-                    setTransform: [snapshot.context.getTransform()],
-                    shadowBlur: snapshot.context.shadowBlur,
-                    shadowColor: snapshot.context.shadowColor,
-                    shadowOffsetX: snapshot.context.shadowOffsetX,
-                    shadowOffsetY: snapshot.context.shadowOffsetY,
-                    strokeStyle: snapshot.context.strokeStyle,
-                    textAlign: snapshot.context.textAlign,
-                    textBaseline: snapshot.context.textBaseline,
-                    webkitImageSmoothingEnabled: snapshot.context.webkitImageSmoothingEnabled,
-                    webkitLineDash: snapshot.context.webkitLineDash,
-                    webkitLineDashOffset: snapshot.context.webkitLineDashOffset,
-                };
-
-                if (WI.RecordingContentView.supportsCanvasPathDebugging())
-                    snapshot.state.setPath = [snapshot.context.getPath()];
-            });
+            snapshot.state = applyActions(startIndex, snapshot.index - 1);
 
             snapshot.content = new Image;
             snapshot.content.src = snapshot.element.toDataURL();
@@ -430,23 +459,27 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
                 --indexOfLastBeginPathAction;
         }
 
-        applyActions(snapshot.index, this._index, () => {
-            if (options.actionCompletedCallback)
-                options.actionCompletedCallback(actions[this._index], snapshot.context);
-        });
+        this._action = actions[this._index];
+
+        let state = applyActions(snapshot.index, this._index);
+        console.assert(!this._action.state || Object.shallowEqual(this._action.state, state));
+        if (!this._action.state)
+            this._action.state = state;
+
+        this.dispatchEventToListeners(WI.ContentView.Event.SupplementalRepresentedObjectsDidChange);
 
         this._previewContainer.appendChild(snapshot.element);
         this._updateImageGrid();
     }
 
-    async _generateContentCanvasWebGL(index, actions, options = {})
+    async _generateContentCanvasWebGL(index, actions)
     {
         let imageLoad = (event) => {
             // Loading took too long and the current action index has already changed.
             if (index !== this._index)
                 return;
 
-            this._generateContentCanvasWebGL(index, actions, options);
+            this._generateContentCanvasWebGL(index, actions);
         };
 
         let initialState = this.representedObject.initialState;
@@ -481,8 +514,7 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
             this._updateImageGrid();
         }
 
-        if (options.actionCompletedCallback)
-            options.actionCompletedCallback(actions[this._index]);
+        this.dispatchEventToListeners(WI.ContentView.Event.SupplementalRepresentedObjectsDidChange);
     }
 
     _updateCanvasPath()
@@ -545,12 +577,8 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
         if (visualActionIndex !== -1)
             index = this.representedObject.visualActionIndexes[visualActionIndex];
 
-        this.dispatchEventToListeners(WI.RecordingContentView.Event.RecordingActionIndexChanged, {index});
+        this.updateActionIndex(index);
     }
 };
 
 WI.RecordingContentView.SnapshotInterval = 5000;
-
-WI.RecordingContentView.Event = {
-    RecordingActionIndexChanged: "recording-action-index-changed",
-};
index dabd630..37bcbca 100644 (file)
@@ -43,8 +43,9 @@ WI.RecordingStateDetailsSidebarPanel = class RecordingStateDetailsSidebarPanel e
             objects = [objects];
 
         this.recording = objects.find((object) => object instanceof WI.Recording && object.type === WI.Recording.Type.Canvas2D);
+        this.action = objects.find((object) => object instanceof WI.RecordingAction);
 
-        return !!this._recording;
+        return this._recording && this._action;
     }
 
     set recording(recording)
@@ -59,22 +60,23 @@ WI.RecordingStateDetailsSidebarPanel = class RecordingStateDetailsSidebarPanel e
             this.contentView.removeSubview(subview);
     }
 
-    updateAction(action, context, options = {})
+    set action(action)
     {
+        console.assert(!action || action instanceof WI.RecordingAction);
         if (!this._recording || action === this._action)
             return;
 
         this._action = action;
 
-        if (this._recording.type === WI.Recording.Type.Canvas2D)
-            this._generateDetailsCanvas2D(action, context, options);
+        if (this._action && this._recording.type === WI.Recording.Type.Canvas2D)
+            this._generateDetailsCanvas2D(this._action);
 
         this.updateLayoutIfNeeded();
     }
 
     // Private
 
-    _generateDetailsCanvas2D(action, context, options = {})
+    _generateDetailsCanvas2D(action)
     {
         if (!this._dataGrid) {
             this._dataGrid = new WI.DataGrid({
@@ -87,41 +89,10 @@ WI.RecordingStateDetailsSidebarPanel = class RecordingStateDetailsSidebarPanel e
 
         this._dataGrid.removeChildren();
 
-        if (!context)
+        console.assert(action.state);
+        if (!action.state)
             return;
 
-        let state = {};
-
-        if (WI.RecordingContentView.supportsCanvasPathDebugging()) {
-            state.currentX = context.currentX;
-            state.currentY = context.currentY;
-        }
-
-        state.direction = context.direction;
-        state.fillStyle = context.fillStyle;
-        state.font = context.font;
-        state.globalAlpha = context.globalAlpha;
-        state.globalCompositeOperation = context.globalCompositeOperation;
-        state.imageSmoothingEnabled = context.imageSmoothingEnabled;
-        state.imageSmoothingQuality = context.imageSmoothingQuality;
-        state.lineCap = context.lineCap;
-        state.lineDash = context.getLineDash();
-        state.lineDashOffset = context.lineDashOffset;
-        state.lineJoin = context.lineJoin;
-        state.lineWidth = context.lineWidth;
-        state.miterLimit = context.miterLimit;
-        state.shadowBlur = context.shadowBlur;
-        state.shadowColor = context.shadowColor;
-        state.shadowOffsetX = context.shadowOffsetX;
-        state.shadowOffsetY = context.shadowOffsetY;
-        state.strokeStyle = context.strokeStyle;
-        state.textAlign = context.textAlign;
-        state.textBaseline = context.textBaseline;
-        state.transform = context.getTransform();
-        state.webkitImageSmoothingEnabled = context.webkitImageSmoothingEnabled;
-        state.webkitLineDash = context.webkitLineDash;
-        state.webkitLineDashOffset = context.webkitLineDashOffset;
-
         function isColorProperty(name) {
             return name === "fillStyle" || name === "strokeStyle" || name === "shadowColor";
         }
@@ -135,8 +106,12 @@ WI.RecordingStateDetailsSidebarPanel = class RecordingStateDetailsSidebarPanel e
             return new WI.InlineSwatch(WI.InlineSwatch.Type.Color, color, readOnly);
         }
 
-        for (let name in state) {
-            let value = state[name];
+        for (let name in action.state) {
+            // Skip internal state used for path debugging.
+            if (name === "setPath")
+                continue;
+
+            let value = action.state[name];
             if (typeof value === "object") {
                 let isGradient = value instanceof CanvasGradient;
                 let isPattern = value instanceof CanvasPattern;
index 3d6f144..8ccf827 100644 (file)
@@ -46,8 +46,9 @@ WI.RecordingTraceDetailsSidebarPanel = class RecordingTraceDetailsSidebarPanel e
             objects = [objects];
 
         this.recording = objects.find((object) => object instanceof WI.Recording);
+        this.action = objects.find((object) => object instanceof WI.RecordingAction);
 
-        return !!this._recording;
+        return this._recording && this._action;
     }
 
     set recording(recording)
@@ -61,8 +62,9 @@ WI.RecordingTraceDetailsSidebarPanel = class RecordingTraceDetailsSidebarPanel e
         this.contentView.element.removeChildren();
     }
 
-    updateAction(action, context, options = {})
+    set action(action)
     {
+        console.assert(!action || action instanceof WI.RecordingAction);
         if (!this._recording || action === this._action)
             return;
 
@@ -70,6 +72,9 @@ WI.RecordingTraceDetailsSidebarPanel = class RecordingTraceDetailsSidebarPanel e
 
         this.contentView.element.removeChildren();
 
+        if (!this._action)
+            return;
+
         let trace = this._action.trace;
         this._backtraceTreeController.callFrames = trace;
 
index e9ebdce..d1544dc 100644 (file)
     content: url(../Images/Beacon.svg);
 }
 
-.canvas > .icon {
-    content: url(../Images/Canvas.svg);
-}
-
-.shader-program .icon {
-    content: image-set(url(../Images/DocumentGL.png) 1x, url(../Images/DocumentGL@2x.png) 2x);
-}
-
 .large .resource-icon .icon {
     content: image-set(url(../Images/DocumentGenericLarge.png) 1x, url(../Images/DocumentGenericLarge@2x.png) 2x);
 }