Web Inspector: create Recording tab for displaying recordings
authordrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 1 Aug 2017 23:00:46 +0000 (23:00 +0000)
committerdrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 1 Aug 2017 23:00:46 +0000 (23:00 +0000)
https://bugs.webkit.org/show_bug.cgi?id=174484

Reviewed by Joseph Pecoraro.

Source/WebInspectorUI:

* Localizations/en.lproj/localizedStrings.js:
* UserInterface/Main.html:
* UserInterface/Test.html:

* UserInterface/Controllers/CanvasManager.js:
(WebInspector.CanvasManager.prototype.recordingFinished):
* UserInterface/Models/Canvas.js:
(WebInspector.Canvas.prototype.toggleRecording):

* UserInterface/Models/Recording.js:
(WebInspector.Recording):
(WebInspector.Recording.synthesizeError):
(WebInspector.Recording.prototype.get actions):
(WebInspector.Recording.prototype.get source):
(WebInspector.Recording.prototype.set source):
(WebInspector.Recording.prototype.swizzle):

* UserInterface/Models/RecordingAction.js:
(WebInspector.RecordingAction):
(WebInspector.RecordingAction.isFunctionForType):
(WebInspector.RecordingAction.get name):
(WebInspector.RecordingAction.get parameters):
(WebInspector.RecordingAction.prototype.get valid):
(WebInspector.RecordingAction.prototype.set valid):
(WebInspector.RecordingAction.get isFunction):
(WebInspector.RecordingAction.get isGetter):
(WebInspector.RecordingAction.get isVisual):
(WebInspector.RecordingAction.get stateModifiers):
(WebInspector.RecordingAction.prototype.swizzle):
(WebInspector.RecordingAction.prototype.parameterSwizzleTypeForTypeAtIndex):

* UserInterface/Models/RecordingInitialStateAction.js: Added.
(WebInspector.RecordingInitialStateAction):

* UserInterface/Views/RecordingTabContentView.js: Added.
(WebInspector.RecordingTabContentView):
(WebInspector.RecordingTabContentView.tabInfo):
(WebInspector.RecordingTabContentView.prototype.get type):
(WebInspector.RecordingTabContentView.prototype.canShowRepresentedObject):
(WebInspector.RecordingTabContentView.prototype.showRepresentedObject):
(WebInspector.RecordingTabContentView.prototype.restoreStateFromCookie):
(WebInspector.RecordingTabContentView.prototype.saveStateToCookie):
(WebInspector.RecordingTabContentView.prototype.closed):
(WebInspector.RecordingTabContentView.prototype._updateActionIndex):
(WebInspector.RecordingTabContentView.prototype._scrubberNavigationItemValueChanged):
(WebInspector.RecordingTabContentView.prototype._navigationSidebarImport):
(WebInspector.RecordingTabContentView.prototype._navigationSidebarTreeOutlineSelectionChanged):

* UserInterface/Views/RecordingContentView.js: Added.
(WebInspector.RecordingContentView):
(WebInspector.RecordingContentView.prototype.get navigationItems):
(WebInspector.RecordingContentView.prototype.updateActionIndex):
(WebInspector.RecordingContentView.prototype.shown):
(WebInspector.RecordingContentView.prototype.get supplementalRepresentedObjects):
(WebInspector.RecordingContentView.prototype._generateContentCanvas2D):
(WebInspector.RecordingContentView.prototype._applyAction):
(WebInspector.RecordingContentView.prototype._updateImageGrid):
(WebInspector.RecordingContentView.prototype._showGridButtonClicked):
* UserInterface/Views/RecordingContentView.css: Copied from Source/WebInspectorUI/UserInterface/Views/CanvasContentView.css.
(.content-view:not(.tab).recording):
(.content-view:not(.tab).recording > .preview-container):
(.content-view:not(.tab).recording canvas):

* UserInterface/Views/RecordingNavigationSidebarPanel.js: Added.
(WebInspector.RecordingNavigationSidebarPanel):
(WebInspector.RecordingNavigationSidebarPanel.disallowInstanceForClass):
(WebInspector.RecordingNavigationSidebarPanel.prototype.set recording):
(WebInspector.RecordingNavigationSidebarPanel.prototype.updateActionIndex):
(WebInspector.RecordingNavigationSidebarPanel.prototype.initialLayout):
(WebInspector.RecordingNavigationSidebarPanel.prototype._importNavigationItemClicked):
(WebInspector.RecordingNavigationSidebarPanel.prototype._exportNavigationItemClicked):
* UserInterface/Views/RecordingNavigationSidebarPanel.css: Added.
(.sidebar > .panel.navigation.recording > :matches(.content, .empty-content-placeholder)):
(.sidebar > .panel.navigation.recording > .content > .tree-outline):
(.sidebar > .panel.navigation.recording > .content > .tree-outline > .item.parent:not(.action, .selected).expanded):
(.sidebar > .panel.navigation.recording > .content > .tree-outline .item.action:not(.initial-state)::before):
(body[dir=ltr] .sidebar > .panel.navigation.recording > .content > .tree-outline .item.action:not(.initial-state)::before):
(body[dir=rtl] .sidebar > .panel.navigation.recording > .content > .tree-outline .item.action:not(.initial-state)::before):
(.sidebar > .panel.navigation.recording > .content > .tree-outline[data-indent="2"] .item.action:not(.initial-state)::before):
(.sidebar > .panel.navigation.recording > .content > .tree-outline[data-indent="3"] .item.action:not(.initial-state)::before):
(.sidebar > .panel.navigation.recording > .content > .tree-outline[data-indent="4"] .item.action:not(.initial-state)::before):
(.sidebar > .panel.navigation.recording > .content > .tree-outline[data-indent="5"] .item.action:not(.initial-state)::before):
(.sidebar > .panel.navigation.recording > .content > .tree-outline[data-indent="6"] .item.action:not(.initial-state)::before):
(.sidebar > .panel.navigation.recording > .content > .tree-outline[data-indent="7"] .item.action:not(.initial-state)::before):
(.sidebar > .panel.navigation.recording > .content .action > .icon):
(.sidebar > .panel.navigation.recording > .content .action.function > .icon):
(.sidebar > .panel.navigation.recording > .content .action.attribute.getter > .icon):
(.sidebar > .panel.navigation.recording > .content .tree-outline:matches(:focus, .force-focus) .action.attribute.getter.selected > .icon):
(.sidebar > .panel.navigation.recording > .content .action.attribute.boolean > .icon):
(.sidebar > .panel.navigation.recording > .content .action.attribute.number > .icon):
(.sidebar > .panel.navigation.recording > .content .action.attribute.object > .icon):
(.sidebar > .panel.navigation.recording > .content .action.attribute.string > .icon):
(.sidebar > .panel.navigation.recording > .content > .tree-outline > .item.parent:not(.action) > .icon):
(.sidebar > .panel.navigation.recording > .content .action:matches(.invalid, .missing) > .icon):
(body[dir=ltr] .sidebar > .panel.navigation.recording > .content .action:not(.initial-state) > .icon):
(body[dir=rtl] .sidebar > .panel.navigation.recording > .content .action:not(.initial-state) > .icon):
(.sidebar > .panel.navigation.recording > .content .action.visual:not(.selected, .invalid)):
(.sidebar > .panel.navigation.recording > .content .action:not(.selected, .initial-state) > .titles .parameter.swizzled):
(.sidebar > .panel.navigation.recording > .content .action.invalid:not(.selected, .initial-state) > .titles .name,):

* UserInterface/Views/RecordingStateDetailsSidebarPanel.js: Added.
(WebInspector.RecordingStateDetailsSidebarPanel):
(WebInspector.RecordingStateDetailsSidebarPanel.disallowInstanceForClass):
(WebInspector.RecordingStateDetailsSidebarPanel.prototype.inspect):
(WebInspector.RecordingStateDetailsSidebarPanel.prototype.set recording):
(WebInspector.RecordingStateDetailsSidebarPanel.prototype.updateActionIndex):
(WebInspector.RecordingStateDetailsSidebarPanel.prototype._generateDetailsCanvas2D):
* UserInterface/Views/RecordingStateDetailsSidebarPanel.css: Added.
(.sidebar > .panel.details.recording-state > .content > .data-grid):
(.sidebar > .panel.details.recording-state > .content > .data-grid tr.modified):
(.sidebar > .panel.details.recording-state > .content > .data-grid tr:not(.selected).non-standard):
(.sidebar > .panel.details.recording-state > .content > .data-grid tr:not(.selected) .unavailable):

* UserInterface/Views/RecordingActionTreeElement.js: Added.
(WebInspector.RecordingActionTreeElement):
(WebInspector.RecordingActionTreeElement._generateDOM):
(WebInspector.RecordingActionTreeElement.prototype.get index):
(WebInspector.RecordingActionTreeElement.get filterableData):
(WebInspector.RecordingActionTreeElement.prototype.get filterableData):
(WebInspector.RecordingActionTreeElement.prototype.onattach):
(WebInspector.RecordingActionTreeElement.prototype.populateContextMenu):

* UserInterface/Base/Main.js:
(WebInspector.contentLoaded):
(WebInspector.instanceForClass):

* UserInterface/Base/FileUtilites.js: Added.
(WebInspector.saveDataToFile): Moved from UserInterface/Base/Main.js
(WebInspector.loadDataFromFile):

* UserInterface/Views/ButtonNavigationItem.js:
(WebInspector.ButtonNavigationItem.prototype._mouseClicked):
Send the native event as part of the data.

* UserInterface/Views/CanvasContentView.js:
(WebInspector.CanvasContentView):
(WebInspector.CanvasContentView.prototype.get navigationItems):
(WebInspector.CanvasContentView.prototype.initialLayout):
(WebInspector.CanvasContentView.prototype.closed):
(WebInspector.CanvasContentView.prototype._toggleRecording):
(WebInspector.CanvasContentView.prototype._recordingFinished):
* UserInterface/Views/CanvasContentView.css:
(.content-view.canvas):

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

* UserInterface/Views/ContentBrowser.js:
(WebInspector.ContentBrowser):
(WebInspector.ContentBrowser.prototype._updateContentViewNavigationItems):
* UserInterface/Views/ContentBrowserTabContentView.js:
(WebInspector.ContentBrowserTabContentView):
* UserInterface/Views/ContentView.js:
(WebInspector.ContentView.createFromRepresentedObject):
(WebInspector.ContentView.isViewable):
* UserInterface/Views/ScrubberNavigationItem.js: Added.
(WebInspector.ScrubberNavigationItem):
(WebInspector.ScrubberNavigationItem.prototype.get value):
(WebInspector.ScrubberNavigationItem.prototype.set value):
(WebInspector.ScrubberNavigationItem.prototype.get min):
(WebInspector.ScrubberNavigationItem.prototype.set min):
(WebInspector.ScrubberNavigationItem.prototype.get max):
(WebInspector.ScrubberNavigationItem.prototype.set max):
(WebInspector.ScrubberNavigationItem.prototype.set disabled):
(WebInspector.ScrubberNavigationItem.prototype.get additionalClassNames):
(WebInspector.ScrubberNavigationItem.prototype._sliderChanged):
* UserInterface/Views/ScrubberNavigationItem.css: Added.
(.navigation-bar .item.scrubber):
(.navigation-bar .item.scrubber > input):
(.navigation-bar .item.scrubber > input[disabled]):
Allow the flexible space to be replaced with a navigation item at construction.

* UserInterface/Views/DataGridNode.js:
(WebInspector.DataGridNode):
(WebInspector.DataGridNode.prototype.get element):
Allow a list of CSS classes to be added to the node's element.

* UserInterface/Views/SettingsTabContentView.js:
(WebInspector.SettingsTabContentView.prototype.initialLayout):
(WebInspector.SettingsTabContentView.prototype._createExperimentalSettingsView.listenForChange):
(WebInspector.SettingsTabContentView.prototype._createExperimentalSettingsView):
(WebInspector.SettingsTabContentView.prototype._createDebugSettingsView):
Move experimental settings to their own panel.

* UserInterface/Views/TabContentView.js:
(WebInspector.TabContentView.prototype.get navigationSidebarPanel):
Save the NavigationSidebarPanel after it's constructed.

* UserInterface/Base/Utilities.js:
(Number.countDigits):

* UserInterface/Images/Recording.svg: Added.
* UserInterface/Images/RenderingFrame.svg: Renamed from Source/WebInspectorUI/UserInterface/Images/TimelineRecordRenderingFrame.svg.
* UserInterface/Images/gtk/RenderingFrame.svg: Renamed from Source/WebInspectorUI/UserInterface/Images/gtk/TimelineRecordRenderingFrame.svg.
* UserInterface/Views/TimelineIcons.css:
(.rendering-frame-record .icon):

* UserInterface/Views/DOMTreeOutline.css:
(@keyframes node-state-changed):
* UserInterface/Views/Main.css:
(:matches(img, canvas).show-grid):
(img.show-grid): Deleted.
* UserInterface/Views/NavigationBar.css:
(.navigation-bar):
* UserInterface/Views/NetworkSidebarPanel.css:
(.sidebar > .panel.navigation.network.network-grid-content-view-showing > .content > .tree-outline):
* UserInterface/Views/TimelineTabContentView.css:
(.timeline.tab.content-view .navigation-bar > .item.radio):
* UserInterface/Views/TimelineView.css:
(.panel.navigation.timeline.timeline-recording-content-view-showing > .content > .tree-outline):
* UserInterface/Views/Variables.css:
(:root):
Create CSS variables for the DOM modification flash color and the color striping.

LayoutTests:

* inspector/unit-tests/number-utilities-expected.txt:
* inspector/unit-tests/number-utilities.html:

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

46 files changed:
LayoutTests/ChangeLog
LayoutTests/inspector/unit-tests/number-utilities-expected.txt
LayoutTests/inspector/unit-tests/number-utilities.html
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
Source/WebInspectorUI/UserInterface/Base/FileUtilities.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Base/Main.js
Source/WebInspectorUI/UserInterface/Base/Utilities.js
Source/WebInspectorUI/UserInterface/Controllers/CanvasManager.js
Source/WebInspectorUI/UserInterface/Images/Recording.svg [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Images/RenderingFrame.svg [moved from Source/WebInspectorUI/UserInterface/Images/TimelineRecordRenderingFrame.svg with 100% similarity]
Source/WebInspectorUI/UserInterface/Images/gtk/RenderingFrame.svg [moved from Source/WebInspectorUI/UserInterface/Images/gtk/TimelineRecordRenderingFrame.svg with 100% similarity]
Source/WebInspectorUI/UserInterface/Main.html
Source/WebInspectorUI/UserInterface/Models/Canvas.js
Source/WebInspectorUI/UserInterface/Models/Recording.js
Source/WebInspectorUI/UserInterface/Models/RecordingAction.js
Source/WebInspectorUI/UserInterface/Models/RecordingInitialStateAction.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Test.html
Source/WebInspectorUI/UserInterface/Views/ButtonNavigationItem.js
Source/WebInspectorUI/UserInterface/Views/CanvasContentView.css
Source/WebInspectorUI/UserInterface/Views/CanvasContentView.js
Source/WebInspectorUI/UserInterface/Views/CanvasDetailsSidebarPanel.js
Source/WebInspectorUI/UserInterface/Views/ContentBrowser.js
Source/WebInspectorUI/UserInterface/Views/ContentBrowserTabContentView.js
Source/WebInspectorUI/UserInterface/Views/ContentView.js
Source/WebInspectorUI/UserInterface/Views/DOMTreeOutline.css
Source/WebInspectorUI/UserInterface/Views/DataGridNode.js
Source/WebInspectorUI/UserInterface/Views/Main.css
Source/WebInspectorUI/UserInterface/Views/NavigationBar.css
Source/WebInspectorUI/UserInterface/Views/NetworkSidebarPanel.css
Source/WebInspectorUI/UserInterface/Views/RecordingActionTreeElement.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/RecordingContentView.css [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/RecordingContentView.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/RecordingNavigationSidebarPanel.css [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/RecordingNavigationSidebarPanel.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/RecordingStateDetailsSidebarPanel.css [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/RecordingStateDetailsSidebarPanel.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/RecordingTabContentView.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/ScrubberNavigationItem.css [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/ScrubberNavigationItem.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/SettingsTabContentView.js
Source/WebInspectorUI/UserInterface/Views/TabContentView.js
Source/WebInspectorUI/UserInterface/Views/TimelineIcons.css
Source/WebInspectorUI/UserInterface/Views/TimelineTabContentView.css
Source/WebInspectorUI/UserInterface/Views/TimelineView.css
Source/WebInspectorUI/UserInterface/Views/Variables.css

index b00320b..360e1cb 100644 (file)
@@ -1,3 +1,13 @@
+2017-08-01  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: create Recording tab for displaying recordings
+        https://bugs.webkit.org/show_bug.cgi?id=174484
+
+        Reviewed by Joseph Pecoraro.
+
+        * inspector/unit-tests/number-utilities-expected.txt:
+        * inspector/unit-tests/number-utilities.html:
+
 2017-08-01  Zalan Bujtas  <zalan@apple.com>
 
         REGRESSION (r217197): New Yorker website hangs for a long time on load, lots of blank tiles
index 5ee71aa..8c0fee6 100644 (file)
@@ -63,3 +63,25 @@ PASS: padding of 2 with one decimal should add no zeros
 PASS: padding of 3 with one decimal should add no zeros
 PASS: padding of 4 with one decimal should add one zero
 
+-- Running test case: Number.countDigits
+PASS: 0 should have 1 digit
+PASS: -0 should have 1 digit
+PASS: 10 should have 2 digits
+PASS: -10 should have 2 digits
+PASS: 100 should have 3 digits
+PASS: -100 should have 3 digits
+PASS: 1000 should have 4 digits
+PASS: -1000 should have 4 digits
+PASS: 10000 should have 5 digits
+PASS: -10000 should have 5 digits
+PASS: 100000 should have 6 digits
+PASS: -100000 should have 6 digits
+PASS: 1000000 should have 7 digits
+PASS: -1000000 should have 7 digits
+PASS: 10000000 should have 8 digits
+PASS: -10000000 should have 8 digits
+PASS: 100000000 should have 9 digits
+PASS: -100000000 should have 9 digits
+PASS: 1000000000 should have 10 digits
+PASS: -1000000000 should have 10 digits
+
index e459dc0..8ef3282 100644 (file)
@@ -113,6 +113,22 @@ function test()
         }
     });
 
+    suite.addTestCase({
+        name: "Number.countDigits",
+        test() {
+            InspectorTest.expectEqual(Number.countDigits(0), 1, "0 should have 1 digit");
+            InspectorTest.expectEqual(Number.countDigits(-0), 1, "-0 should have 1 digit");
+            for (let i = 1; i < 10; ++i) {
+                let digits = i + 1;
+                let num = 10 ** i;
+                InspectorTest.expectEqual(Number.countDigits(num), digits, `${num} should have ${digits} digits`);
+                InspectorTest.expectEqual(Number.countDigits(-num), digits, `${-num} should have ${digits} digits`);
+            }
+
+            return true;
+        }
+    });
+
     suite.runTestCasesAndFinish();
 }
 </script>
index f17424d..18c5b4e 100644 (file)
@@ -1,3 +1,224 @@
+2017-08-01  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: create Recording tab for displaying recordings
+        https://bugs.webkit.org/show_bug.cgi?id=174484
+
+        Reviewed by Joseph Pecoraro.
+
+        * Localizations/en.lproj/localizedStrings.js:
+        * UserInterface/Main.html:
+        * UserInterface/Test.html:
+
+        * UserInterface/Controllers/CanvasManager.js:
+        (WebInspector.CanvasManager.prototype.recordingFinished):
+        * UserInterface/Models/Canvas.js:
+        (WebInspector.Canvas.prototype.toggleRecording):
+
+        * UserInterface/Models/Recording.js:
+        (WebInspector.Recording):
+        (WebInspector.Recording.synthesizeError):
+        (WebInspector.Recording.prototype.get actions):
+        (WebInspector.Recording.prototype.get source):
+        (WebInspector.Recording.prototype.set source):
+        (WebInspector.Recording.prototype.swizzle):
+
+        * UserInterface/Models/RecordingAction.js:
+        (WebInspector.RecordingAction):
+        (WebInspector.RecordingAction.isFunctionForType):
+        (WebInspector.RecordingAction.get name):
+        (WebInspector.RecordingAction.get parameters):
+        (WebInspector.RecordingAction.prototype.get valid):
+        (WebInspector.RecordingAction.prototype.set valid):
+        (WebInspector.RecordingAction.get isFunction):
+        (WebInspector.RecordingAction.get isGetter):
+        (WebInspector.RecordingAction.get isVisual):
+        (WebInspector.RecordingAction.get stateModifiers):
+        (WebInspector.RecordingAction.prototype.swizzle):
+        (WebInspector.RecordingAction.prototype.parameterSwizzleTypeForTypeAtIndex):
+
+        * UserInterface/Models/RecordingInitialStateAction.js: Added.
+        (WebInspector.RecordingInitialStateAction):
+
+        * UserInterface/Views/RecordingTabContentView.js: Added.
+        (WebInspector.RecordingTabContentView):
+        (WebInspector.RecordingTabContentView.tabInfo):
+        (WebInspector.RecordingTabContentView.prototype.get type):
+        (WebInspector.RecordingTabContentView.prototype.canShowRepresentedObject):
+        (WebInspector.RecordingTabContentView.prototype.showRepresentedObject):
+        (WebInspector.RecordingTabContentView.prototype.restoreStateFromCookie):
+        (WebInspector.RecordingTabContentView.prototype.saveStateToCookie):
+        (WebInspector.RecordingTabContentView.prototype.closed):
+        (WebInspector.RecordingTabContentView.prototype._updateActionIndex):
+        (WebInspector.RecordingTabContentView.prototype._scrubberNavigationItemValueChanged):
+        (WebInspector.RecordingTabContentView.prototype._navigationSidebarImport):
+        (WebInspector.RecordingTabContentView.prototype._navigationSidebarTreeOutlineSelectionChanged):
+
+        * UserInterface/Views/RecordingContentView.js: Added.
+        (WebInspector.RecordingContentView):
+        (WebInspector.RecordingContentView.prototype.get navigationItems):
+        (WebInspector.RecordingContentView.prototype.updateActionIndex):
+        (WebInspector.RecordingContentView.prototype.shown):
+        (WebInspector.RecordingContentView.prototype.get supplementalRepresentedObjects):
+        (WebInspector.RecordingContentView.prototype._generateContentCanvas2D):
+        (WebInspector.RecordingContentView.prototype._applyAction):
+        (WebInspector.RecordingContentView.prototype._updateImageGrid):
+        (WebInspector.RecordingContentView.prototype._showGridButtonClicked):
+        * UserInterface/Views/RecordingContentView.css: Copied from Source/WebInspectorUI/UserInterface/Views/CanvasContentView.css.
+        (.content-view:not(.tab).recording):
+        (.content-view:not(.tab).recording > .preview-container):
+        (.content-view:not(.tab).recording canvas):
+
+        * UserInterface/Views/RecordingNavigationSidebarPanel.js: Added.
+        (WebInspector.RecordingNavigationSidebarPanel):
+        (WebInspector.RecordingNavigationSidebarPanel.disallowInstanceForClass):
+        (WebInspector.RecordingNavigationSidebarPanel.prototype.set recording):
+        (WebInspector.RecordingNavigationSidebarPanel.prototype.updateActionIndex):
+        (WebInspector.RecordingNavigationSidebarPanel.prototype.initialLayout):
+        (WebInspector.RecordingNavigationSidebarPanel.prototype._importNavigationItemClicked):
+        (WebInspector.RecordingNavigationSidebarPanel.prototype._exportNavigationItemClicked):
+        * UserInterface/Views/RecordingNavigationSidebarPanel.css: Added.
+        (.sidebar > .panel.navigation.recording > :matches(.content, .empty-content-placeholder)):
+        (.sidebar > .panel.navigation.recording > .content > .tree-outline):
+        (.sidebar > .panel.navigation.recording > .content > .tree-outline > .item.parent:not(.action, .selected).expanded):
+        (.sidebar > .panel.navigation.recording > .content > .tree-outline .item.action:not(.initial-state)::before):
+        (body[dir=ltr] .sidebar > .panel.navigation.recording > .content > .tree-outline .item.action:not(.initial-state)::before):
+        (body[dir=rtl] .sidebar > .panel.navigation.recording > .content > .tree-outline .item.action:not(.initial-state)::before):
+        (.sidebar > .panel.navigation.recording > .content > .tree-outline[data-indent="2"] .item.action:not(.initial-state)::before):
+        (.sidebar > .panel.navigation.recording > .content > .tree-outline[data-indent="3"] .item.action:not(.initial-state)::before):
+        (.sidebar > .panel.navigation.recording > .content > .tree-outline[data-indent="4"] .item.action:not(.initial-state)::before):
+        (.sidebar > .panel.navigation.recording > .content > .tree-outline[data-indent="5"] .item.action:not(.initial-state)::before):
+        (.sidebar > .panel.navigation.recording > .content > .tree-outline[data-indent="6"] .item.action:not(.initial-state)::before):
+        (.sidebar > .panel.navigation.recording > .content > .tree-outline[data-indent="7"] .item.action:not(.initial-state)::before):
+        (.sidebar > .panel.navigation.recording > .content .action > .icon):
+        (.sidebar > .panel.navigation.recording > .content .action.function > .icon):
+        (.sidebar > .panel.navigation.recording > .content .action.attribute.getter > .icon):
+        (.sidebar > .panel.navigation.recording > .content .tree-outline:matches(:focus, .force-focus) .action.attribute.getter.selected > .icon):
+        (.sidebar > .panel.navigation.recording > .content .action.attribute.boolean > .icon):
+        (.sidebar > .panel.navigation.recording > .content .action.attribute.number > .icon):
+        (.sidebar > .panel.navigation.recording > .content .action.attribute.object > .icon):
+        (.sidebar > .panel.navigation.recording > .content .action.attribute.string > .icon):
+        (.sidebar > .panel.navigation.recording > .content > .tree-outline > .item.parent:not(.action) > .icon):
+        (.sidebar > .panel.navigation.recording > .content .action:matches(.invalid, .missing) > .icon):
+        (body[dir=ltr] .sidebar > .panel.navigation.recording > .content .action:not(.initial-state) > .icon):
+        (body[dir=rtl] .sidebar > .panel.navigation.recording > .content .action:not(.initial-state) > .icon):
+        (.sidebar > .panel.navigation.recording > .content .action.visual:not(.selected, .invalid)):
+        (.sidebar > .panel.navigation.recording > .content .action:not(.selected, .initial-state) > .titles .parameter.swizzled):
+        (.sidebar > .panel.navigation.recording > .content .action.invalid:not(.selected, .initial-state) > .titles .name,):
+
+        * UserInterface/Views/RecordingStateDetailsSidebarPanel.js: Added.
+        (WebInspector.RecordingStateDetailsSidebarPanel):
+        (WebInspector.RecordingStateDetailsSidebarPanel.disallowInstanceForClass):
+        (WebInspector.RecordingStateDetailsSidebarPanel.prototype.inspect):
+        (WebInspector.RecordingStateDetailsSidebarPanel.prototype.set recording):
+        (WebInspector.RecordingStateDetailsSidebarPanel.prototype.updateActionIndex):
+        (WebInspector.RecordingStateDetailsSidebarPanel.prototype._generateDetailsCanvas2D):
+        * UserInterface/Views/RecordingStateDetailsSidebarPanel.css: Added.
+        (.sidebar > .panel.details.recording-state > .content > .data-grid):
+        (.sidebar > .panel.details.recording-state > .content > .data-grid tr.modified):
+        (.sidebar > .panel.details.recording-state > .content > .data-grid tr:not(.selected).non-standard):
+        (.sidebar > .panel.details.recording-state > .content > .data-grid tr:not(.selected) .unavailable):
+
+        * UserInterface/Views/RecordingActionTreeElement.js: Added.
+        (WebInspector.RecordingActionTreeElement):
+        (WebInspector.RecordingActionTreeElement._generateDOM):
+        (WebInspector.RecordingActionTreeElement.prototype.get index):
+        (WebInspector.RecordingActionTreeElement.get filterableData):
+        (WebInspector.RecordingActionTreeElement.prototype.get filterableData):
+        (WebInspector.RecordingActionTreeElement.prototype.onattach):
+        (WebInspector.RecordingActionTreeElement.prototype.populateContextMenu):
+
+        * UserInterface/Base/Main.js:
+        (WebInspector.contentLoaded):
+        (WebInspector.instanceForClass):
+
+        * UserInterface/Base/FileUtilites.js: Added.
+        (WebInspector.saveDataToFile): Moved from UserInterface/Base/Main.js
+        (WebInspector.loadDataFromFile):
+
+        * UserInterface/Views/ButtonNavigationItem.js:
+        (WebInspector.ButtonNavigationItem.prototype._mouseClicked):
+        Send the native event as part of the data.
+
+        * UserInterface/Views/CanvasContentView.js:
+        (WebInspector.CanvasContentView):
+        (WebInspector.CanvasContentView.prototype.get navigationItems):
+        (WebInspector.CanvasContentView.prototype.initialLayout):
+        (WebInspector.CanvasContentView.prototype.closed):
+        (WebInspector.CanvasContentView.prototype._toggleRecording):
+        (WebInspector.CanvasContentView.prototype._recordingFinished):
+        * UserInterface/Views/CanvasContentView.css:
+        (.content-view.canvas):
+
+        * UserInterface/Views/CanvasDetailsSidebarPanel.js:
+        (WebInspector.CanvasDetailsSidebarPanel.prototype.inspect):
+
+        * UserInterface/Views/ContentBrowser.js:
+        (WebInspector.ContentBrowser):
+        (WebInspector.ContentBrowser.prototype._updateContentViewNavigationItems):
+        * UserInterface/Views/ContentBrowserTabContentView.js:
+        (WebInspector.ContentBrowserTabContentView):
+        * UserInterface/Views/ContentView.js:
+        (WebInspector.ContentView.createFromRepresentedObject):
+        (WebInspector.ContentView.isViewable):
+        * UserInterface/Views/ScrubberNavigationItem.js: Added.
+        (WebInspector.ScrubberNavigationItem):
+        (WebInspector.ScrubberNavigationItem.prototype.get value):
+        (WebInspector.ScrubberNavigationItem.prototype.set value):
+        (WebInspector.ScrubberNavigationItem.prototype.get min):
+        (WebInspector.ScrubberNavigationItem.prototype.set min):
+        (WebInspector.ScrubberNavigationItem.prototype.get max):
+        (WebInspector.ScrubberNavigationItem.prototype.set max):
+        (WebInspector.ScrubberNavigationItem.prototype.set disabled):
+        (WebInspector.ScrubberNavigationItem.prototype.get additionalClassNames):
+        (WebInspector.ScrubberNavigationItem.prototype._sliderChanged):
+        * UserInterface/Views/ScrubberNavigationItem.css: Added.
+        (.navigation-bar .item.scrubber):
+        (.navigation-bar .item.scrubber > input):
+        (.navigation-bar .item.scrubber > input[disabled]):
+        Allow the flexible space to be replaced with a navigation item at construction.
+
+        * UserInterface/Views/DataGridNode.js:
+        (WebInspector.DataGridNode):
+        (WebInspector.DataGridNode.prototype.get element):
+        Allow a list of CSS classes to be added to the node's element.
+
+        * UserInterface/Views/SettingsTabContentView.js:
+        (WebInspector.SettingsTabContentView.prototype.initialLayout):
+        (WebInspector.SettingsTabContentView.prototype._createExperimentalSettingsView.listenForChange):
+        (WebInspector.SettingsTabContentView.prototype._createExperimentalSettingsView):
+        (WebInspector.SettingsTabContentView.prototype._createDebugSettingsView):
+        Move experimental settings to their own panel.
+
+        * UserInterface/Views/TabContentView.js:
+        (WebInspector.TabContentView.prototype.get navigationSidebarPanel):
+        Save the NavigationSidebarPanel after it's constructed.
+
+        * UserInterface/Base/Utilities.js:
+        (Number.countDigits):
+
+        * UserInterface/Images/Recording.svg: Added.
+        * UserInterface/Images/RenderingFrame.svg: Renamed from Source/WebInspectorUI/UserInterface/Images/TimelineRecordRenderingFrame.svg.
+        * UserInterface/Images/gtk/RenderingFrame.svg: Renamed from Source/WebInspectorUI/UserInterface/Images/gtk/TimelineRecordRenderingFrame.svg.
+        * UserInterface/Views/TimelineIcons.css:
+        (.rendering-frame-record .icon):
+
+        * UserInterface/Views/DOMTreeOutline.css:
+        (@keyframes node-state-changed):
+        * UserInterface/Views/Main.css:
+        (:matches(img, canvas).show-grid):
+        (img.show-grid): Deleted.
+        * UserInterface/Views/NavigationBar.css:
+        (.navigation-bar):
+        * UserInterface/Views/NetworkSidebarPanel.css:
+        (.sidebar > .panel.navigation.network.network-grid-content-view-showing > .content > .tree-outline):
+        * UserInterface/Views/TimelineTabContentView.css:
+        (.timeline.tab.content-view .navigation-bar > .item.radio):
+        * UserInterface/Views/TimelineView.css:
+        (.panel.navigation.timeline.timeline-recording-content-view-showing > .content > .tree-outline):
+        * UserInterface/Views/Variables.css:
+        (:root):
+        Create CSS variables for the DOM modification flash color and the color striping.
+
 2017-07-28  Devin Rousso  <drousso@apple.com>
 
         Web Inspector: Cleanup unused/invalid parameters for TreeElements
index 7268f5d..fbdd57a 100644 (file)
@@ -149,6 +149,7 @@ localizedStrings["Call Trees"] = "Call Trees";
 localizedStrings["Calls"] = "Calls";
 localizedStrings["Cancel Automatic Continue"] = "Cancel Automatic Continue";
 localizedStrings["Cancel comparison"] = "Cancel comparison";
+localizedStrings["Cancel recording"] = "Cancel recording";
 localizedStrings["Canvas"] = "Canvas";
 localizedStrings["Canvas %d"] = "Canvas %d";
 localizedStrings["Canvas %s"] = "Canvas %s";
@@ -231,6 +232,7 @@ localizedStrings["Continue script execution (%s or %s)"] = "Continue script exec
 localizedStrings["Continue to Here"] = "Continue to Here";
 localizedStrings["Controls"] = "Controls";
 localizedStrings["Cookies"] = "Cookies";
+localizedStrings["Copy Action"] = "Copy Action";
 localizedStrings["Copy Link Address"] = "Copy Link Address";
 localizedStrings["Copy Path to Property"] = "Copy Path to Property";
 localizedStrings["Copy Row"] = "Copy Row";
@@ -371,7 +373,9 @@ localizedStrings["Exception with thrown value: %s"] = "Exception with thrown val
 localizedStrings["Expand All"] = "Expand All";
 localizedStrings["Expand columns"] = "Expand columns";
 localizedStrings["Expanded"] = "Expanded";
+localizedStrings["Experimental"] = "Experimental";
 localizedStrings["Expires"] = "Expires";
+localizedStrings["Export"] = "Export";
 localizedStrings["Expression"] = "Expression";
 localizedStrings["Extension Scripts"] = "Extension Scripts";
 localizedStrings["Extra Scripts"] = "Extra Scripts";
@@ -386,6 +390,7 @@ localizedStrings["File or Resource"] = "File or Resource";
 localizedStrings["Filename"] = "Filename";
 localizedStrings["Fill"] = "Fill";
 localizedStrings["Fill Mode"] = "Fill Mode";
+localizedStrings["Filter Actions"] = "Filter Actions";
 localizedStrings["Filter Console Log"] = "Filter Console Log";
 localizedStrings["Filter List"] = "Filter List";
 localizedStrings["Filter Records"] = "Filter Records";
@@ -463,6 +468,8 @@ localizedStrings["Image"] = "Image";
 localizedStrings["Image Size"] = "Image Size";
 localizedStrings["Images"] = "Images";
 localizedStrings["Immediate Pause Requested"] = "Immediate Pause Requested";
+localizedStrings["Import"] = "Import";
+localizedStrings["Incomplete"] = "Incomplete";
 localizedStrings["Indent"] = "Indent";
 localizedStrings["Indent width:"] = "Indent width:";
 localizedStrings["Index"] = "Index";
@@ -471,6 +478,7 @@ localizedStrings["Indexed Databases"] = "Indexed Databases";
 localizedStrings["Info: "] = "Info: ";
 localizedStrings["Inherited From: "] = "Inherited From: ";
 localizedStrings["Inherited from %s"] = "Inherited from %s";
+localizedStrings["Initial State"] = "Initial State";
 localizedStrings["Initial Velocity"] = "Initial Velocity";
 localizedStrings["Initiated"] = "Initiated";
 localizedStrings["Initiator"] = "Initiator";
@@ -580,6 +588,7 @@ localizedStrings["No Preview Available"] = "No Preview Available";
 localizedStrings["No Properties"] = "No Properties";
 localizedStrings["No Properties \u2014 Click to Edit"] = "No Properties \u2014 Click to Edit";
 localizedStrings["No Query Parameters"] = "No Query Parameters";
+localizedStrings["No Recording Data"] = "No Recording Data";
 localizedStrings["No Request Headers"] = "No Request Headers";
 localizedStrings["No Response Headers"] = "No Response Headers";
 localizedStrings["No Results Found"] = "No Results Found";
@@ -668,7 +677,9 @@ localizedStrings["Range Issue"] = "Range Issue";
 localizedStrings["Readonly"] = "Readonly";
 localizedStrings["Reasons for compositing:"] = "Reasons for compositing:";
 localizedStrings["Recently Closed Tabs"] = "Recently Closed Tabs";
+localizedStrings["Recording"] = "Recording";
 localizedStrings["Recording Timeline Data"] = "Recording Timeline Data";
+localizedStrings["Recording error: %s"] = "Recording error: %s";
 localizedStrings["Reference Issue"] = "Reference Issue";
 localizedStrings["Reflection"] = "Reflection";
 localizedStrings["Refresh"] = "Refresh";
@@ -676,6 +687,7 @@ localizedStrings["Refresh watch expressions"] = "Refresh watch expressions";
 localizedStrings["Region Flow"] = "Region Flow";
 localizedStrings["Region announced in its entirety."] = "Region announced in its entirety.";
 localizedStrings["Regular Expression"] = "Regular Expression";
+localizedStrings["Reload Web Inspector"] = "Reload Web Inspector";
 localizedStrings["Reload this page (%s)\nReload ignoring cache (%s)"] = "Reload this page (%s)\nReload ignoring cache (%s)";
 localizedStrings["Removals"] = "Removals";
 localizedStrings["Remove Watch Expression"] = "Remove Watch Expression";
@@ -692,6 +704,7 @@ localizedStrings["Request"] = "Request";
 localizedStrings["Request & Response"] = "Request & Response";
 localizedStrings["Request Data"] = "Request Data";
 localizedStrings["Request Headers"] = "Request Headers";
+localizedStrings["Request recording of actions. Shift-click to record a single frame."] = "Request recording of actions. Shift-click to record a single frame.";
 localizedStrings["Requesting: %s"] = "Requesting: %s";
 localizedStrings["Required"] = "Required";
 localizedStrings["Reset"] = "Reset";
@@ -810,6 +823,7 @@ localizedStrings["Specificity: (%d, %d, %d)"] = "Specificity: (%d, %d, %d)";
 localizedStrings["Specificity: No value for selected element"] = "Specificity: No value for selected element";
 localizedStrings["Spelling"] = "Spelling";
 localizedStrings["Spread"] = "Spread";
+localizedStrings["Spreadsheet Style Editor"] = "Spreadsheet Style Editor";
 localizedStrings["Spring"] = "Spring";
 localizedStrings["Stalled"] = "Stalled";
 localizedStrings["Start Time"] = "Start Time";
@@ -835,6 +849,7 @@ localizedStrings["Style rule"] = "Style rule";
 localizedStrings["Styles"] = "Styles";
 localizedStrings["Styles Editing:"] = "Styles Editing:";
 localizedStrings["Styles Invalidated"] = "Styles Invalidated";
+localizedStrings["Styles Panel:"] = "Styles Panel:";
 localizedStrings["Styles Recalculated"] = "Styles Recalculated";
 localizedStrings["Styles \u2014 Computed"] = "Styles \u2014 Computed";
 localizedStrings["Styles \u2014 Rules"] = "Styles \u2014 Rules";
@@ -946,6 +961,7 @@ localizedStrings["Z-Index"] = "Z-Index";
 localizedStrings["Zoom:"] = "Zoom:";
 localizedStrings["computed"] = "computed";
 localizedStrings["default"] = "default";
+localizedStrings["for changes to take effect"] = "for changes to take effect";
 localizedStrings["key"] = "key";
 localizedStrings["line "] = "line ";
 localizedStrings["originally %s"] = "originally %s";
@@ -955,5 +971,8 @@ localizedStrings["spaces"] = "spaces";
 localizedStrings["time before stopping"] = "time before stopping";
 localizedStrings["times before stopping"] = "times before stopping";
 localizedStrings["toggle"] = "toggle";
+localizedStrings["unsupported version."] = "unsupported version.";
 localizedStrings["value"] = "value";
 localizedStrings["“%s” Profile Recorded"] = "“%s” Profile Recorded";
+localizedStrings["“%s” is invalid."] = "“%s” is invalid.";
+localizedStrings["“%s” threw an error."] = "“%s” threw an error.";
diff --git a/Source/WebInspectorUI/UserInterface/Base/FileUtilities.js b/Source/WebInspectorUI/UserInterface/Base/FileUtilities.js
new file mode 100644 (file)
index 0000000..94e716e
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+WebInspector.saveDataToFile = function(saveData, forceSaveAs)
+{
+    console.assert(saveData);
+    if (!saveData)
+        return;
+
+    if (typeof saveData.customSaveHandler === "function") {
+        saveData.customSaveHandler(forceSaveAs);
+        return;
+    }
+
+    console.assert(saveData.content);
+    if (!saveData.content)
+        return;
+
+    let url = saveData.url || "";
+    let suggestedName = parseURL(url).lastPathComponent;
+    if (!suggestedName) {
+        suggestedName = WebInspector.UIString("Untitled");
+        let dataURLTypeMatch = /^data:([^;]+)/.exec(url);
+        if (dataURLTypeMatch)
+            suggestedName += WebInspector.fileExtensionForMIMEType(dataURLTypeMatch[1]) || "";
+    }
+
+    if (typeof saveData.content === "string") {
+        const base64Encoded = saveData.base64Encoded || false;
+        InspectorFrontendHost.save(suggestedName, saveData.content, base64Encoded, forceSaveAs || saveData.forceSaveAs);
+        return;
+    }
+
+    let fileReader = new FileReader;
+    fileReader.readAsDataURL(saveData.content);
+    fileReader.addEventListener("loadend", () => {
+        let dataURLComponents = parseDataURL(fileReader.result);
+
+        const base64Encoded = true;
+        InspectorFrontendHost.save(suggestedName, dataURLComponents.data, base64Encoded, forceSaveAs || saveData.forceSaveAs);
+    });
+};
+
+WebInspector.loadDataFromFile = function(callback)
+{
+    let inputElement = document.createElement("input");
+    inputElement.type = "file";
+    inputElement.addEventListener("change", (event) => {
+        if (!inputElement.files.length) {
+            callback(null);
+            return;
+        }
+
+        let reader = new FileReader;
+        reader.addEventListener("loadend", (event) => {
+            callback(reader.result);
+        });
+        reader.readAsText(inputElement.files[0]);
+    });
+    inputElement.click();
+};
index 5de7b4d..e7a82f7 100644 (file)
@@ -435,6 +435,7 @@ WebInspector.contentLoaded = function()
         WebInspector.ElementsTabContentView,
         WebInspector.NetworkTabContentView,
         WebInspector.NewTabContentView,
+        WebInspector.RecordingTabContentView,
         WebInspector.ResourcesTabContentView,
         WebInspector.SearchTabContentView,
         WebInspector.SettingsTabContentView,
@@ -507,10 +508,14 @@ WebInspector.contentLoaded = function()
 // This function returns a lazily constructed instance of a class scoped to this WebInspector
 // instance. In the unlikely event that we ever need to construct multiple WebInspector instances
 // this allows us to scope objects within the WebInspector.
+// Classes can prevent usage of this function via a static `disallowInstanceForClass` function that
+// returns true. It is then their responsibility to ensure that the returned value is tracked.
 // Currently it is only used for sidebars.
 WebInspector.instanceForClass = function(constructor)
 {
     console.assert(typeof constructor === "function");
+    if (typeof constructor.disallowInstanceForClass === "function" && constructor.disallowInstanceForClass())
+        return new constructor;
 
     let key = `__${constructor.name}`;
     if (!WebInspector[key])
@@ -860,46 +865,6 @@ WebInspector.close = function()
     InspectorFrontendHost.closeWindow();
 };
 
-WebInspector.saveDataToFile = function(saveData, forceSaveAs)
-{
-    console.assert(saveData);
-    if (!saveData)
-        return;
-
-    if (typeof saveData.customSaveHandler === "function") {
-        saveData.customSaveHandler(forceSaveAs);
-        return;
-    }
-
-    console.assert(saveData.content);
-    if (!saveData.content)
-        return;
-
-    let url = saveData.url || "";
-    let suggestedName = parseURL(url).lastPathComponent;
-    if (!suggestedName) {
-        suggestedName = WebInspector.UIString("Untitled");
-        let dataURLTypeMatch = /^data:([^;]+)/.exec(url);
-        if (dataURLTypeMatch)
-            suggestedName += WebInspector.fileExtensionForMIMEType(dataURLTypeMatch[1]) || "";
-    }
-
-    if (typeof saveData.content === "string") {
-        const base64Encoded = saveData.base64Encoded || false;
-        InspectorFrontendHost.save(suggestedName, saveData.content, base64Encoded, forceSaveAs || saveData.forceSaveAs);
-        return;
-    }
-
-    let fileReader = new FileReader;
-    fileReader.readAsDataURL(saveData.content);
-    fileReader.addEventListener("loadend", () => {
-        let dataURLComponents = parseDataURL(fileReader.result);
-
-        const base64Encoded = true;
-        InspectorFrontendHost.save(suggestedName, dataURLComponents.data, base64Encoded, forceSaveAs || saveData.forceSaveAs);
-    });
-};
-
 WebInspector.isConsoleFocused = function()
 {
     return this.quickConsole.prompt.focused;
@@ -1119,6 +1084,9 @@ WebInspector.tabContentViewClassForRepresentedObject = function(representedObjec
         representedObject instanceof WebInspector.IndexedDatabaseObjectStoreIndex)
         return WebInspector.ResourcesTabContentView;
 
+    if (representedObject instanceof WebInspector.Recording)
+        return WebInspector.RecordingTabContentView;
+
     return null;
 };
 
index aa0a27d..5025285 100644 (file)
@@ -1128,6 +1128,18 @@ Object.defineProperty(Number, "zeroPad",
     },
 });
 
+Object.defineProperty(Number, "countDigits",
+{
+    value(num)
+    {
+        if (num === 0)
+            return 1;
+
+        num = Math.abs(num);
+        return Math.floor(Math.log(num) * Math.LOG10E) + 1;
+    }
+});
+
 Object.defineProperty(Number.prototype, "maxDecimals",
 {
     value(decimals)
index f4a238b..26044c0 100644 (file)
@@ -106,6 +106,8 @@ WebInspector.CanvasManager = class CanvasManager extends WebInspector.Object
             return;
 
         let recording = WebInspector.Recording.fromPayload(recordingPayload);
+        recording.source = canvas;
+
         this.dispatchEventToListeners(WebInspector.CanvasManager.Event.RecordingFinished, {canvas, recording});
     }
 
diff --git a/Source/WebInspectorUI/UserInterface/Images/Recording.svg b/Source/WebInspectorUI/UserInterface/Images/Recording.svg
new file mode 100644 (file)
index 0000000..8b6cdea
--- /dev/null
@@ -0,0 +1,9 @@
+<?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 fill="currentColor" d="M 13 1 L 3 1 C 1.898438 1 1 1.898438 1 3 L 1 13 C 1 14.101562 1.898438 15 3 15 L 13 15 C 14.101562 15 15 14.101562 15 13 L 15 3 C 15 1.898438 14.101562 1 13 1 M 13 2 C 13.550781 2 14 2.449219 14 3 L 14 13 C 14 13.550781 13.550781 14 13 14 L 3 14 C 2.449219 14 2 13.550781 2 13 L 2 3 C 2 2.449219 2.449219 2 3 2 L 13 2"/>
+    <rect fill="currentColor" x="4.25" y="4.5" width="7.5" height="1"/>
+    <rect fill="currentColor" x="4.25" y="6.5" width="7.5" height="1"/>
+    <rect fill="currentColor" x="4.25" y="8.5" width="7.5" height="1"/>
+    <rect fill="currentColor" x="4.25" y="10.5" width="7.5" height="1"/>
+</svg>
index c87b1d0..b8ede32 100644 (file)
     <link rel="stylesheet" href="Views/ProfileView.css">
     <link rel="stylesheet" href="Views/QuickConsole.css">
     <link rel="stylesheet" href="Views/RadioButtonNavigationItem.css">
+    <link rel="stylesheet" href="Views/RecordingContentView.css">
+    <link rel="stylesheet" href="Views/RecordingStateDetailsSidebarPanel.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">
     <link rel="stylesheet" href="Views/ScriptContentView.css">
     <link rel="stylesheet" href="Views/ScriptDetailsTimelineView.css">
     <link rel="stylesheet" href="Views/ScriptTimelineOverviewGraph.css">
+    <link rel="stylesheet" href="Views/ScrubberNavigationItem.css">
     <link rel="stylesheet" href="Views/SearchBar.css">
     <link rel="stylesheet" href="Views/SearchIcons.css">
     <link rel="stylesheet" href="Views/SearchSidebarPanel.css">
     <script src="Base/DOMUtilities.js"></script>
     <script src="Base/EventListener.js"></script>
     <script src="Base/EventListenerSet.js"></script>
+    <script src="Base/FileUtilities.js"></script>
     <script src="Base/ImageUtilities.js"></script>
     <script src="Base/LoadLocalizedStrings.js"></script>
     <script src="Base/MIMETypeUtilities.js"></script>
     <script src="Models/Recording.js"></script>
     <script src="Models/RecordingAction.js"></script>
     <script src="Models/RecordingFrame.js"></script>
+    <script src="Models/RecordingInitialStateAction.js"></script>
     <script src="Models/RenderingFrameTimelineRecord.js"></script>
     <script src="Models/Resource.js"></script>
     <script src="Models/ResourceCollection.js"></script>
     <script src="Views/ConsoleTabContentView.js"></script>
     <script src="Views/DebuggerTabContentView.js"></script>
     <script src="Views/ElementsTabContentView.js"></script>
+    <script src="Views/RecordingTabContentView.js"></script>
     <script src="Views/ResourceTreeElement.js"></script>
     <script src="Views/ResourcesTabContentView.js"></script>
     <script src="Views/SearchTabContentView.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/RenderingFrameTimelineDataGridNode.js"></script>
     <script src="Views/RenderingFrameTimelineOverviewGraph.js"></script>
     <script src="Views/RenderingFrameTimelineView.js"></script>
     <script src="Views/ScriptTimelineDataGridNode.js"></script>
     <script src="Views/ScriptTimelineOverviewGraph.js"></script>
     <script src="Views/ScriptTreeElement.js"></script>
+    <script src="Views/ScrubberNavigationItem.js"></script>
     <script src="Views/SearchBar.js"></script>
     <script src="Views/SearchResultTreeElement.js"></script>
     <script src="Views/SearchSidebarPanel.js"></script>
index efcb6ca..42d6f84 100644 (file)
@@ -194,6 +194,14 @@ WebInspector.Canvas = class Canvas extends WebInspector.Object
         });
     }
 
+    toggleRecording(flag, singleFrame, callback)
+    {
+        if (flag)
+            CanvasAgent.requestRecording(this._identifier, singleFrame, callback);
+        else
+            CanvasAgent.cancelRecording(this._identifier, callback);
+    }
+
     saveIdentityToCookie(cookie)
     {
         cookie[WebInspector.Canvas.FrameURLCookieKey] = this._frame.url.hash;
index e0704a0..17c758b 100644 (file)
@@ -32,6 +32,30 @@ WebInspector.Recording = class Recording
         this._initialState = initialState;
         this._frames = frames;
         this._data = data;
+
+        this._actions = [new WebInspector.RecordingInitialStateAction].concat(...this._frames.map((frame) => frame.actions));
+        this._swizzle = [];
+        this._source = null;
+
+        for (let frame of this._frames) {
+            for (let action of frame.actions) {
+                action.swizzle(this);
+
+                let prototype = null;
+                if (this._type === WebInspector.Recording.Type.Canvas2D)
+                    prototype = CanvasRenderingContext2D.prototype;
+
+                if (prototype) {
+                    let validName = action.name in prototype;
+                    let validFunction = !action.isFunction || typeof prototype[action.name] === "function";
+                    if (!validName || !validFunction) {
+                        action.valid = false;
+
+                        WebInspector.Recording.synthesizeError(WebInspector.UIString("“%s” is invalid.").format(action.name));
+                    }
+                }
+            }
+        }
     }
 
     // Static
@@ -73,6 +97,17 @@ WebInspector.Recording = class Recording
         return new WebInspector.Recording(payload.version, type, payload.initialState, frames, payload.data);
     }
 
+    static synthesizeError(message)
+    {
+        const target = WebInspector.mainTarget;
+        const source = WebInspector.ConsoleMessage.MessageSource.Other;
+        const level = WebInspector.ConsoleMessage.MessageLevel.Error;
+        let consoleMessage = new WebInspector.ConsoleMessage(target, source, level, WebInspector.UIString("Recording error: %s").format(message));
+        consoleMessage.shouldRevealConsole = true;
+
+        WebInspector.consoleLogViewController.appendConsoleMessage(consoleMessage);
+    }
+
     // Public
 
     get type() { return this._type; }
@@ -80,6 +115,68 @@ WebInspector.Recording = class Recording
     get frames() { return this._frames; }
     get data() { return this._data; }
 
+    get actions() { return this._actions; }
+
+    get source() { return this._source; }
+    set source(source) { this._source = source; }
+
+    swizzle(index, type)
+    {
+        if (typeof this._swizzle[index] !== "object")
+            this._swizzle[index] = {};
+
+        if (!(type in this._swizzle[index])) {
+            try {
+                let data = this._data[index];
+                switch (type) {
+                case WebInspector.Recording.Swizzle.CanvasStyle:
+                    if (Array.isArray(data)) {
+                        let context = document.createElement("canvas").getContext("2d");
+
+                        let canvasStyle = this.swizzle(data[0], WebInspector.Recording.Swizzle.String);
+                        if (canvasStyle === "linear-gradient" || canvasStyle === "radial-gradient") {
+                            this._swizzle[index][type] = canvasStyle === "radial-gradient" ? context.createRadialGradient(...data[1]) : context.createLinearGradient(...data[1]);
+                            for (let stop of data[2]) {
+                                let color = this.swizzle(stop[1], WebInspector.Recording.Swizzle.String);
+                                this._swizzle[index][type].addColorStop(stop[0], color);
+                            }
+                        } else if (canvasStyle === "pattern") {
+                            let image = this.swizzle(data[1], WebInspector.Recording.Swizzle.Image);
+                            let repeat = this.swizzle(data[2], WebInspector.Recording.Swizzle.String);
+                            this._swizzle[index][type] = context.createPattern(image, repeat);
+                        }
+                    } else if (typeof data === "string")
+                        this._swizzle[index][type] = data;
+                    break;
+                case WebInspector.Recording.Swizzle.Element:
+                    this._swizzle[index][type] = WebInspector.Recording.Swizzle.Invalid;
+                    break;
+                case WebInspector.Recording.Swizzle.Image:
+                    this._swizzle[index][type] = new Image;
+                    this._swizzle[index][type].src = data;
+                    break;
+                case WebInspector.Recording.Swizzle.ImageData:
+                    this._swizzle[index][type] = new ImageData(new Uint8ClampedArray(data[0]), parseInt(data[1]), parseInt(data[2]));
+                    break;
+                case WebInspector.Recording.Swizzle.Path2D:
+                    this._swizzle[index][type] = new Path2D(data);
+                    break;
+                case WebInspector.Recording.Swizzle.String:
+                    if (typeof data === "string")
+                        this._swizzle[index][type] = data;
+                    break;
+                }
+            } catch (e) {
+                this._swizzle[index][type] = WebInspector.Recording.Swizzle.Invalid;
+            }
+
+            if (!(type in this._swizzle[index]))
+                this._swizzle[index][type] = WebInspector.Recording.Swizzle.Invalid;
+        }
+
+        return this._swizzle[index][type];
+    }
+
     toJSON()
     {
         let initialState = {};
@@ -103,3 +200,13 @@ WebInspector.Recording = class Recording
 WebInspector.Recording.Type = {
     Canvas2D: "canvas-2d",
 };
+
+WebInspector.Recording.Swizzle = {
+    CanvasStyle: "CanvasStyle",
+    Element: "Element",
+    Image: "Image",
+    ImageData: "ImageData",
+    Path2D: "Path2D",
+    String: "String",
+    Invalid: Symbol("invalid"),
+};
index 12fc946..dd3dce7 100644 (file)
@@ -27,8 +27,18 @@ WebInspector.RecordingAction = class RecordingAction
 {
     constructor(name, parameters)
     {
-        this._name = name;
-        this._parameters = parameters;
+        this._payloadName = name;
+        this._payloadParameters = parameters;
+
+        this._name = "";
+        this._parameters = [];
+
+        this._valid = true;
+
+        this._isFunction = false;
+        this._isGetter = false;
+        this._isVisual = false;
+        this._stateModifiers = [];
     }
 
     // Static
@@ -48,13 +58,320 @@ WebInspector.RecordingAction = class RecordingAction
         return new WebInspector.RecordingAction(...payload);
     }
 
+    static isFunctionForType(type, name)
+    {
+        let functionNames = WebInspector.RecordingAction._functionNames[type];
+        if (!functionNames)
+            return false;
+
+        return functionNames.includes(name);
+    }
+
     // Public
 
-    get name() { return this._resolvedName; }
-    get parameters() { return this._resolvedParameters; }
+    get name() { return this._name; }
+    get parameters() { return this._parameters; }
+
+    get valid() { return this._valid; }
+    set valid(valid) { this._valid = !!valid; }
+
+    get isFunction() { return this._isFunction; }
+    get isGetter() { return this._isGetter; }
+    get isVisual() { return this._isVisual; }
+    get stateModifiers() { return this._stateModifiers; }
+
+    swizzle(recording)
+    {
+        this._name = recording.swizzle(this._payloadName, WebInspector.Recording.Swizzle.String);
+
+        this._parameters = this._payloadParameters.map((item, i) => {
+            let type = this.parameterSwizzleTypeForTypeAtIndex(recording.type, i);
+            if (!type)
+                return item;
+
+            let swizzled = recording.swizzle(item, type);
+            if (swizzled === WebInspector.Recording.Swizzle.Invalid)
+                this._valid = false;
+
+            return swizzled;
+        });
+
+        this._isFunction = WebInspector.RecordingAction.isFunctionForType(recording.type, this._name);
+        this._isGetter = !this._isFunction && !this._parameters.length;
+
+        let visualNames = WebInspector.RecordingAction._visualNames[recording.type];
+        this._isVisual = visualNames ? visualNames.includes(this._name) : false;
+
+        this._stateModifiers = [this._name];
+        let stateModifiers = WebInspector.RecordingAction._stateModifiers[recording.type];
+        if (stateModifiers) {
+            let modifiedByAction = stateModifiers[this._name];
+            if (modifiedByAction)
+                this._stateModifiers = this._stateModifiers.concat(modifiedByAction);
+        }
+    }
+
+    parameterSwizzleTypeForTypeAtIndex(type, index)
+    {
+        let functionNames = WebInspector.RecordingAction._parameterSwizzleTypeForTypeAtIndex[type];
+        if (!functionNames)
+            return null;
+
+        let parameterLengths = functionNames[this._name];
+        if (!parameterLengths)
+            return null;
+
+        let argumentSwizzleTypes = parameterLengths[this._payloadParameters.length];
+        if (!argumentSwizzleTypes)
+            return null;
+
+        return argumentSwizzleTypes[index] || null;
+    }
 
     toJSON()
     {
-        return [this._name, this._parameters];
+        return [this._payloadName, this._payloadParameters];
     }
 };
+
+// This object instructs the frontend as to how to reconstruct deduplicated objects found in the
+// "data" section of a recording payload. It will only swizzle values if they are of the expected
+// type in the right index of the version of the action (determined by the number of parameters) for
+// the recording type. Since a recording can be created by importing a JSON file, this is used to
+// make sure that inputs are only considered valid if they conform to the structure defined below.
+// 
+// For Example:
+//
+// IDL:
+//
+//     void foo(optional DOMString s = "bar");
+//     void foo(DOMPath path, optional DOMString s = "bar");
+//     void foo(float a, float b, float c, float d);
+//
+// Swizzle Entries:
+//
+//     - For the 1 parameter version, the parameter at index 0 needs to be swizzled as a string
+//     - For the 2 parameter version, the parameters need to be swizzled as a Path and String
+//     - For the 4 parameter version, numbers don't need to be swizzled, so it is not included
+//
+//     "foo": {
+//         1: {0: String}
+//         2: {0: Path2D, 1: String}
+//     }
+
+{
+    let {CanvasStyle, Element, Image, ImageData, Path2D, String} = WebInspector.Recording.Swizzle;
+
+    WebInspector.RecordingAction._parameterSwizzleTypeForTypeAtIndex = {
+        [WebInspector.Recording.Type.Canvas2D]: {
+            "clip": {
+                1: {0: String},
+                2: {0: Path2D, 1: String},
+            },
+            "createImageData": {
+                1: {0: ImageData},
+            },
+            "createPattern": {
+                2: {0: Image, 1: String},
+            },
+            "direction": {
+                1: {0: String},
+            },
+            "drawImage": {
+                3: {0: Image},
+                5: {0: Image},
+                9: {0: Image},
+            },
+            "drawImageFromRect": {
+                10: {0: Image, 9: String},
+            },
+            "drawFocusIfNeeded": {
+                1: {0: Element},
+                2: {0: Path2D, 1: Element},
+            },
+            "fill": {
+                1: {0: String},
+                2: {0: Path2D, 1: String},
+            },
+            "fillStyle": {
+                1: {0: CanvasStyle},
+            },
+            "fillText": {
+                3: {0: String},
+                4: {0: String},
+            },
+            "font": {
+                1: {0: String},
+            },
+            "globalCompositeOperation": {
+                1: {0: String},
+            },
+            "imageSmoothingQuality": {
+                1: {0: String},
+            },
+            "isPointInPath": {
+                4: {0: Path2D, 3: String},
+            },
+            "isPointInStroke": {
+                3: {0: Path2D, 3: String},
+            },
+            "lineCap": {
+                1: {0: String},
+            },
+            "lineJoin": {
+                1: {0: String},
+            },
+            "measureText": {
+                1: {0: String},
+            },
+            "putImageData": {
+                3: {0: ImageData},
+                7: {0: ImageData},
+            },
+            "setCompositeOperation": {
+                1: {0: String},
+            },
+            "setFillColor": {
+                1: {0: String},
+                2: {0: String},
+            },
+            "setLineCap": {
+                1: {0: String},
+            },
+            "setLineJoin": {
+                1: {0: String},
+            },
+            "setShadow": {
+                4: {3: String},
+                5: {3: String},
+            },
+            "setStrokeColor": {
+                1: {0: String},
+                2: {0: String},
+            },
+            "shadowColor": {
+                1: {0: String},
+            },
+            "stroke": {
+                1: {0: Path2D},
+            },
+            "strokeStyle": {
+                1: {0: CanvasStyle},
+            },
+            "strokeText": {
+                3: {0: String},
+                4: {0: String},
+            },
+            "textAlign": {
+                1: {0: String},
+            },
+            "textBaseline": {
+                1: {0: String},
+            },
+            "webkitPutImageData": {
+                3: {0: ImageData},
+                7: {0: ImageData},
+            },
+        },
+    };
+}
+
+WebInspector.RecordingAction._functionNames = {
+    [WebInspector.Recording.Type.Canvas2D]: [
+        "arc",
+        "arcTo",
+        "beginPath",
+        "bezierCurveTo",
+        "clearRect",
+        "clearShadow",
+        "clip",
+        "clip",
+        "closePath",
+        "commit",
+        "createImageData",
+        "createLinearGradient",
+        "createPattern",
+        "createRadialGradient",
+        "drawFocusIfNeeded",
+        "drawImage",
+        "drawImageFromRect",
+        "ellipse",
+        "fill",
+        "fill",
+        "fillRect",
+        "fillText",
+        "getImageData",
+        "getLineDash",
+        "isPointInPath",
+        "isPointInPath",
+        "isPointInStroke",
+        "isPointInStroke",
+        "lineTo",
+        "measureText",
+        "moveTo",
+        "putImageData",
+        "quadraticCurveTo",
+        "rect",
+        "resetTransform",
+        "restore",
+        "rotate",
+        "save",
+        "scale",
+        "setAlpha",
+        "setCompositeOperation",
+        "setFillColor",
+        "setLineCap",
+        "setLineDash",
+        "setLineJoin",
+        "setLineWidth",
+        "setMiterLimit",
+        "setShadow",
+        "setStrokeColor",
+        "setTransform",
+        "stroke",
+        "stroke",
+        "strokeRect",
+        "strokeText",
+        "transform",
+        "translate",
+        "webkitGetImageDataHD",
+        "webkitPutImageDataHD",
+    ],
+};
+
+WebInspector.RecordingAction._visualNames = {
+    [WebInspector.Recording.Type.Canvas2D]: [
+        "clearRect",
+        "drawFocusIfNeeded",
+        "drawImage",
+        "drawImageFromRect",
+        "fill",
+        "fillRect",
+        "fillText",
+        "putImageData",
+        "stroke",
+        "strokeRect",
+        "strokeText",
+        "webkitPutImageDataHD",
+    ],
+};
+
+WebInspector.RecordingAction._stateModifiers = {
+    [WebInspector.Recording.Type.Canvas2D]: {
+        clearShadow: ["shadowOffsetX", "shadowOffsetY", "shadowBlur", "shadowColor"],
+        resetTransform: ["transform"],
+        rotate: ["transform"],
+        scale: ["transform"],
+        setAlpha: ["globalAlpha"],
+        setCompositeOperation: ["globalCompositeOperation"],
+        setFillColor: ["fillStyle"],
+        setLineCap: ["lineCap"],
+        setLineJoin: ["lineJoin"],
+        setLineWidth: ["lineWidth"],
+        setMiterLimit: ["miterLimit"],
+        setShadow: ["shadowOffsetX", "shadowOffsetY", "shadowBlur", "shadowColor"],
+        setStrokeColor: ["strokeStyle"],
+        setTransform: ["transform"],
+        translate: ["transform"],
+    },
+};
diff --git a/Source/WebInspectorUI/UserInterface/Models/RecordingInitialStateAction.js b/Source/WebInspectorUI/UserInterface/Models/RecordingInitialStateAction.js
new file mode 100644 (file)
index 0000000..ac07a48
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+WebInspector.RecordingInitialStateAction = class RecordingInitialStateAction extends WebInspector.RecordingAction
+{
+    constructor()
+    {
+        super();
+
+        this._name = WebInspector.UIString("Initial State");
+
+        this._valid = false;
+    }
+};
index 6473ef5..813e55d 100644 (file)
     <script src="Models/Recording.js"></script>
     <script src="Models/RecordingAction.js"></script>
     <script src="Models/RecordingFrame.js"></script>
+    <script src="Models/RecordingInitialStateAction.js"></script>
     <script src="Models/RenderingFrameTimelineRecord.js"></script>
     <script src="Models/Resource.js"></script>
     <script src="Models/ResourceCollection.js"></script>
index 8848f9d..531cdb8 100644 (file)
@@ -128,7 +128,7 @@ WebInspector.ButtonNavigationItem = class ButtonNavigationItem extends WebInspec
     {
         if (!this.enabled)
             return;
-        this.dispatchEventToListeners(WebInspector.ButtonNavigationItem.Event.Clicked);
+        this.dispatchEventToListeners(WebInspector.ButtonNavigationItem.Event.Clicked, {nativeEvent: event});
     }
 };
 
index 6d11ee6..6b55e42 100644 (file)
  * THE POSSIBILITY OF SUCH DAMAGE.
  */
 
+.content-view.canvas {
+    background-color: hsl(0, 0%, 90%);
+}
+
 .content-view.canvas > .preview {
     display: flex;
     justify-content: center;
index c9c7cb3..d96b344 100644 (file)
@@ -37,6 +37,13 @@ WebInspector.CanvasContentView = class CanvasContentView extends WebInspector.Co
         this._previewImageElement = null;
         this._errorElement = null;
 
+        if (representedObject.contextType === WebInspector.Canvas.ContextType.Canvas2D) {
+            const toolTip = WebInspector.UIString("Request recording of actions. Shift-click to record a single frame.");
+            const altToolTip = WebInspector.UIString("Cancel recording");
+            this._recordButtonNavigationItem = new WebInspector.ToggleButtonNavigationItem("canvas-record", toolTip, altToolTip, "Images/Record.svg", "Images/Stop.svg", 13, 13);
+            this._recordButtonNavigationItem.addEventListener(WebInspector.ButtonNavigationItem.Event.Clicked, this._toggleRecording, this);
+        }
+
         this._refreshButtonNavigationItem = new WebInspector.ButtonNavigationItem("canvas-refresh", WebInspector.UIString("Refresh"), "Images/ReloadFull.svg", 13, 13);
         this._refreshButtonNavigationItem.addEventListener(WebInspector.ButtonNavigationItem.Event.Clicked, this._showPreview, this);
 
@@ -49,7 +56,17 @@ WebInspector.CanvasContentView = class CanvasContentView extends WebInspector.Co
 
     get navigationItems()
     {
-        return [this._refreshButtonNavigationItem, this._showGridButtonNavigationItem];
+        let navigationItems = [this._refreshButtonNavigationItem, this._showGridButtonNavigationItem];
+        if (this._recordButtonNavigationItem)
+            navigationItems.unshift(this._recordButtonNavigationItem);
+        return navigationItems;
+    }
+
+    initialLayout()
+    {
+        super.initialLayout();
+
+        WebInspector.canvasManager.addEventListener(WebInspector.CanvasManager.Event.RecordingFinished, this._recordingFinished, this);
     }
 
     shown()
@@ -68,8 +85,37 @@ WebInspector.CanvasContentView = class CanvasContentView extends WebInspector.Co
         super.hidden();
     }
 
+    closed()
+    {
+        WebInspector.canvasManager.removeEventListener(null, null, this);
+
+        super.closed();
+    }
+
     // Private
 
+    _toggleRecording(event)
+    {
+        let toggled = this._recordButtonNavigationItem.toggled;
+        let singleFrame = event.data.nativeEvent.shiftKey;
+        this.representedObject.toggleRecording(!toggled, singleFrame, (error) => {
+            console.assert(!error);
+
+            this._recordButtonNavigationItem.toggled = !toggled;
+        });
+    }
+
+    _recordingFinished(event)
+    {
+        if (event.data.canvas !== this.representedObject)
+            return;
+
+        if (this._recordButtonNavigationItem)
+            this._recordButtonNavigationItem.toggled = false;
+
+        WebInspector.showRepresentedObject(event.data.recording);
+    }
+
     _showPreview()
     {
         let showError = () => {
index c8419c3..cadd43a 100644 (file)
@@ -42,6 +42,8 @@ WebInspector.CanvasDetailsSidebarPanel = class CanvasDetailsSidebarPanel extends
         if (!(objects instanceof Array))
             objects = [objects];
 
+        objects = objects.map((object) => object instanceof WebInspector.Recording ? object.source : object);
+
         this.canvas = objects.find((object) => object instanceof WebInspector.Canvas);
 
         return !!this._canvas;
index 62ecb79..57d638c 100644 (file)
@@ -25,7 +25,7 @@
 
 WebInspector.ContentBrowser = class ContentBrowser extends WebInspector.View
 {
-    constructor(element, delegate, disableBackForward, disableFindBanner)
+    constructor(element, delegate, disableBackForward, disableFindBanner, flexibleNavigationItem)
     {
         super(element);
 
@@ -79,8 +79,8 @@ WebInspector.ContentBrowser = class ContentBrowser extends WebInspector.View
 
         this._contentViewSelectionPathNavigationItem = new WebInspector.HierarchicalPathNavigationItem;
 
-        this._dividingFlexibleSpaceNavigationItem = new WebInspector.FlexibleSpaceNavigationItem;
-        this._navigationBar.addNavigationItem(this._dividingFlexibleSpaceNavigationItem);
+        this._flexibleNavigationItem = flexibleNavigationItem || new WebInspector.FlexibleSpaceNavigationItem;
+        this._navigationBar.addNavigationItem(this._flexibleNavigationItem);
 
         WebInspector.ContentView.addEventListener(WebInspector.ContentView.Event.SelectionPathComponentsDidChange, this._contentViewSelectionPathComponentDidChange, this);
         WebInspector.ContentView.addEventListener(WebInspector.ContentView.Event.SupplementalRepresentedObjectsDidChange, this._contentViewSupplementalRepresentedObjectsDidChange, this);
@@ -392,7 +392,7 @@ WebInspector.ContentBrowser = class ContentBrowser extends WebInspector.View
         this._removeAllNavigationItems();
 
         let navigationBar = this.navigationBar;
-        let insertionIndex = navigationBar.navigationItems.indexOf(this._dividingFlexibleSpaceNavigationItem) + 1;
+        let insertionIndex = navigationBar.navigationItems.indexOf(this._flexibleNavigationItem) + 1;
         console.assert(insertionIndex >= 0);
 
         // Keep track of items we'll be adding to the navigation bar.
index 8040d3d..bc75f18 100644 (file)
 
 WebInspector.ContentBrowserTabContentView = class ContentBrowserTabContentView extends WebInspector.TabContentView
 {
-    constructor(identifier, styleClassNames, tabBarItem, navigationSidebarPanelConstructor, detailsSidebarPanelConstructors, disableBackForward)
+    constructor(identifier, styleClassNames, tabBarItem, navigationSidebarPanelConstructor, detailsSidebarPanelConstructors, disableBackForward, flexibleNavigationItem)
     {
         if (typeof styleClassNames === "string")
             styleClassNames = [styleClassNames];
 
         styleClassNames.push("content-browser");
 
-        var contentBrowser = new WebInspector.ContentBrowser(null, null, disableBackForward);
+        var contentBrowser = new WebInspector.ContentBrowser(null, null, disableBackForward, false, flexibleNavigationItem);
 
         super(identifier, styleClassNames, tabBarItem, navigationSidebarPanelConstructor, detailsSidebarPanelConstructors);
 
index eed7adb..f14d63c 100644 (file)
@@ -158,6 +158,9 @@ WebInspector.ContentView = class ContentView extends WebInspector.View
         if (representedObject instanceof WebInspector.HeapSnapshotProxy || representedObject instanceof WebInspector.HeapSnapshotDiffProxy)
             return new WebInspector.HeapSnapshotClusterContentView(representedObject, extraArguments);
 
+        if (representedObject instanceof WebInspector.Recording)
+            return new WebInspector.RecordingContentView(representedObject, extraArguments);
+
         if (representedObject instanceof WebInspector.Collection)
             return new WebInspector.CollectionContentView(representedObject, extraArguments);
 
@@ -280,6 +283,8 @@ WebInspector.ContentView = class ContentView extends WebInspector.View
             return true;
         if (representedObject instanceof WebInspector.HeapSnapshotProxy || representedObject instanceof WebInspector.HeapSnapshotDiffProxy)
             return true;
+        if (representedObject instanceof WebInspector.Recording)
+            return true;
         if (representedObject instanceof WebInspector.Collection)
             return true;
         if (typeof representedObject === "string" || representedObject instanceof String)
index 962f6bb..ee1576e 100644 (file)
@@ -289,7 +289,7 @@ body[dir=rtl] .tree-outline.dom li.parent.shadow::after {
 }
 
 @keyframes node-state-changed {
-    from { background-color: hsla(83, 100%, 48%, 0.4); }
+    from { background-color: var(--value-changed-highlight); }
 }
 
 .node-state-changed {
index 1095929..a64d069 100644 (file)
@@ -25,7 +25,7 @@
 
 WebInspector.DataGridNode = class DataGridNode extends WebInspector.Object
 {
-    constructor(data, hasChildren)
+    constructor(data, hasChildren, classNames)
     {
         super();
 
@@ -42,6 +42,7 @@ WebInspector.DataGridNode = class DataGridNode extends WebInspector.Object
         this.previousSibling = null;
         this.nextSibling = null;
         this.disclosureToggleWidth = 10;
+        this._classNames = classNames || [];
     }
 
     get hidden()
@@ -100,6 +101,7 @@ WebInspector.DataGridNode = class DataGridNode extends WebInspector.Object
             this._element.classList.add("revealed");
         if (this._hidden)
             this._element.classList.add("hidden");
+        this._element.classList.add(...this._classNames);
 
         this.createCells();
         return this._element;
index 7a82af9..6c61104 100644 (file)
@@ -368,7 +368,7 @@ body[dir=rtl] .go-to-arrow {
     }
 }
 
-img.show-grid {
+:matches(img, canvas).show-grid {
     background-image: linear-gradient(315deg, transparent 75%, hsl(0, 0%, 95%) 75%),
                       linear-gradient(45deg, transparent 75%, hsl(0, 0%, 95%) 75%),
                       linear-gradient(315deg, hsl(0, 0%, 95%) 25%, transparent 25%),
index f5d9c45..19ed5de 100644 (file)
@@ -26,6 +26,7 @@
 .navigation-bar {
     display: flex;
     justify-content: center;
+    align-items: center;
     flex-wrap: wrap;
 
     border-bottom: 1px solid var(--border-color);
index 5920158..60f7b7d 100644 (file)
@@ -92,8 +92,7 @@
 }
 
 .sidebar > .panel.navigation.network.network-grid-content-view-showing > .content > .tree-outline {
-    background-image: linear-gradient(to bottom, transparent, transparent 50%, hsla(0, 0%, 0%, 0.03) 50%, hsla(0, 0%, 0%, 0.03));
-    background-size: 100% 40px;
+    background: var(--transparent-stripe-background-gradient);
 }
 
 .sidebar > .panel.navigation.network .tree-outline > .preserved:not(.selected) > :not(.status) {
diff --git a/Source/WebInspectorUI/UserInterface/Views/RecordingActionTreeElement.js b/Source/WebInspectorUI/UserInterface/Views/RecordingActionTreeElement.js
new file mode 100644 (file)
index 0000000..aaa72f9
--- /dev/null
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+WebInspector.RecordingActionTreeElement = class RecordingActionTreeElement extends WebInspector.GeneralTreeElement
+{
+    constructor(representedObject, index, recordingType)
+    {
+        console.assert(representedObject instanceof WebInspector.RecordingAction);
+
+        let {classNames, copyText, titleFragment} = WebInspector.RecordingActionTreeElement._generateDOM(representedObject, recordingType);
+
+        const subtitle = null;
+        super(classNames, titleFragment, subtitle, representedObject);
+
+        this._index = index;
+        this._copyText = copyText;
+    }
+
+    // Static
+
+    static _generateDOM(recordingAction, recordingType)
+    {
+        let classNames = ["action"];
+        let copyText = recordingAction.name;
+
+        let isInitialState = recordingAction instanceof WebInspector.RecordingInitialStateAction;
+        if (recordingAction.isFunction)
+            classNames.push("function");
+        else if (!isInitialState) {
+            classNames.push("attribute");
+            if (recordingAction.isGetter)
+                classNames.push("getter");
+            else
+                classNames.push(typeof recordingAction.parameters[0]);
+        }
+
+        if (recordingAction.isVisual)
+            classNames.push("visual");
+
+        if (!recordingAction.valid)
+            classNames.push("invalid");
+
+        let titleFragment = document.createDocumentFragment();
+
+        let nameContainer = titleFragment.appendChild(document.createElement("span"));
+        nameContainer.classList.add("name");
+        nameContainer.textContent = recordingAction.name;
+
+        if (!recordingAction.isGetter && !isInitialState) {
+            let hasMissingParameter = false;
+
+            if (recordingAction.isFunction) {
+                titleFragment.append("(");
+                copyText += "(";
+            } else {
+                titleFragment.append(" = ");
+                copyText = " = ";
+            }
+
+            for (let i = 0; i < recordingAction.parameters.length; ++i) {
+                let parameter = recordingAction.parameters[i];
+
+                if (i) {
+                    titleFragment.append(", ");
+                    copyText += ", ";
+                }
+
+                let parameterElement = titleFragment.appendChild(document.createElement("span"));
+                parameterElement.classList.add("parameter");
+
+                let swizzleType = recordingAction.parameterSwizzleTypeForTypeAtIndex(recordingType, i);
+                if (swizzleType && typeof parameter !== "string") {
+                    parameterElement.classList.add("swizzled");
+                    if (parameter === WebInspector.Recording.Swizzle.Invalid) {
+                        parameterElement.classList.add("missing");
+
+                        hasMissingParameter = true;
+                    }
+
+                    if (parameter instanceof CanvasGradient)
+                        parameterElement.textContent = WebInspector.unlocalizedString("Gradient");
+                    else if (parameter instanceof CanvasPattern)
+                        parameterElement.textContent = WebInspector.unlocalizedString("Pattern");
+                    else
+                        parameterElement.textContent = swizzleType;
+
+                    copyText += parameterElement.textContent;
+                } else if (!isNaN(parameter) && parameter !== null) {
+                    parameterElement.textContent = parameter.maxDecimals(2);
+                    copyText += parameter;
+                } else {
+                    parameterElement.textContent = JSON.stringify(parameter);
+                    copyText += parameterElement.textContent;
+                }
+
+                if (!recordingAction.isFunction)
+                    break;
+            }
+
+            if (recordingAction.isFunction) {
+                titleFragment.append(")");
+                copyText += ")";
+            }
+
+            if (hasMissingParameter)
+                classNames.push("missing");
+        } else if (isInitialState)
+            classNames.push("initial-state");
+
+        return {
+            classNames,
+            copyText,
+            titleFragment,
+        };
+    }
+
+    // Public
+
+    get index() { return this._index; }
+
+    get filterableData()
+    {
+        let text = [];
+
+        function getText(stringOrElement) {
+            if (typeof stringOrElement === "string")
+                text.push(stringOrElement);
+            else if (stringOrElement instanceof Node)
+                text.push(stringOrElement.textContent);
+        }
+
+        getText(this._mainTitleElement || this._mainTitle);
+        getText(this._subtitleElement || this._subtitle);
+
+        return {text};
+    }
+
+    // Protected
+
+    onattach()
+    {
+        super.onattach();
+
+        this.element.dataset.index = this._index.toLocaleString();
+    }
+
+    populateContextMenu(contextMenu, event)
+    {
+        contextMenu.appendItem(WebInspector.UIString("Copy Action"), () => {
+            InspectorFrontendHost.copyText("context." + this._copyText + ";");
+        });
+
+        contextMenu.appendSeparator();
+
+        super.populateContextMenu(contextMenu, event);
+    }
+};
diff --git a/Source/WebInspectorUI/UserInterface/Views/RecordingContentView.css b/Source/WebInspectorUI/UserInterface/Views/RecordingContentView.css
new file mode 100644 (file)
index 0000000..a062eaf
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2017 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+.content-view:not(.tab).recording {
+    padding: 15px;
+    background-color: hsl(0, 0%, 90%);
+}
+
+.content-view:not(.tab).recording > .preview-container {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    position: relative;
+    width: 100%;
+    height: 100%;
+}
+
+.content-view:not(.tab).recording canvas {
+    max-width: 100%;
+    max-height: 100%;
+}
diff --git a/Source/WebInspectorUI/UserInterface/Views/RecordingContentView.js b/Source/WebInspectorUI/UserInterface/Views/RecordingContentView.js
new file mode 100644 (file)
index 0000000..33bd5bf
--- /dev/null
@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+WebInspector.RecordingContentView = class RecordingContentView extends WebInspector.ContentView
+{
+    constructor(representedObject)
+    {
+        console.assert(representedObject instanceof WebInspector.Recording);
+
+        super(representedObject);
+
+        this._index = NaN;
+        this._snapshots = [];
+        this._initialContent = null;
+
+        this.element.classList.add("recording", this.representedObject.type);
+
+        if (this.representedObject.type === WebInspector.Recording.Type.Canvas2D) {
+            this._showGridButtonNavigationItem = new WebInspector.ActivateButtonNavigationItem("show-grid", WebInspector.UIString("Show Grid"), WebInspector.UIString("Hide Grid"), "Images/NavigationItemCheckers.svg", 13, 13);
+            this._showGridButtonNavigationItem.addEventListener(WebInspector.ButtonNavigationItem.Event.Clicked, this._showGridButtonClicked, this);
+            this._showGridButtonNavigationItem.activated = !!WebInspector.settings.showImageGrid.value;
+        }
+
+        this._previewContainer = this.element.appendChild(document.createElement("div"));
+        this._previewContainer.classList.add("preview-container");
+}
+
+    // Public
+
+    get navigationItems()
+    {
+        if (this.representedObject.type === WebInspector.Recording.Type.Canvas2D)
+            return [this._showGridButtonNavigationItem];
+        return [];
+    }
+
+    updateActionIndex(index, options = {})
+    {
+        console.assert(!this.representedObject || (index >= 0 && index < this.representedObject.actions.length));
+        if (!this.representedObject || index < 0 || index >= this.representedObject.actions.length || index === this._index)
+            return;
+
+        this._index = index;
+
+        if (this.representedObject.type === WebInspector.Recording.Type.Canvas2D)
+            this._generateContentCanvas2D(index, options);
+    }
+
+    // Protected
+
+    shown()
+    {
+        super.shown();
+
+        if (this.representedObject.type === WebInspector.Recording.Type.Canvas2D)
+            this._updateImageGrid();
+    }
+
+    get supplementalRepresentedObjects()
+    {
+        let supplementalRepresentedObjects = super.supplementalRepresentedObjects;
+        if (this.representedObject)
+            supplementalRepresentedObjects.push(this.representedObject);
+        return supplementalRepresentedObjects;
+    }
+
+    // Private
+
+    _generateContentCanvas2D(index, options = {})
+    {
+        let imageLoad = (event) => {
+            // Loading took too long and the current action index has already changed.
+            if (index !== this._index)
+                return;
+
+            this._generateContentCanvas2D(index, options);
+        };
+
+        let initialState = this.representedObject.initialState;
+        if (initialState.content && !this._initialContent) {
+            this._initialContent = new Image;
+            this._initialContent.src = initialState.content;
+            this._initialContent.addEventListener("load", imageLoad);
+            return;
+        }
+
+        let snapshotIndex = Math.floor(index / WebInspector.RecordingContentView.SnapshotInterval);
+        let snapshot = this._snapshots[snapshotIndex];
+
+        let actions = this.representedObject.actions;
+        let applyActions = (from, to, callback) => {
+            let saveCount = 0;
+            snapshot.context.save();
+
+            if (snapshot.content) {
+                snapshot.context.clearRect(0, 0, snapshot.element.width, snapshot.element.height);
+                snapshot.context.drawImage(snapshot.content, 0, 0);
+            }
+
+            for (let name in snapshot.state) {
+                if (!(name in snapshot.context))
+                    continue;
+
+                try {
+                    if (WebInspector.RecordingAction.functionForType(this.representedObject.type, name))
+                        snapshot.context[name](...snapshot.state[name]);
+                    else
+                        snapshot.context[name] = snapshot.state[name];
+                } catch (e) {
+                    delete snapshot.state[name];
+                }
+            }
+
+            for (let i = from; i <= to; ++i) {
+                if (actions[i].name === "save")
+                    ++saveCount;
+                else if (actions[i].name === "restore") {
+                    if (!saveCount) // Only attempt to restore if save has been called.
+                        continue;
+                    --saveCount;
+                }
+
+                this._applyAction(snapshot.context, actions[i]);
+            }
+
+            callback();
+
+            snapshot.context.restore();
+            while (saveCount-- > 0)
+                snapshot.context.restore();
+        };
+
+        if (!snapshot) {
+            snapshot = this._snapshots[snapshotIndex] = {};
+            snapshot.index = snapshotIndex * WebInspector.RecordingContentView.SnapshotInterval;
+            while (snapshot.index && actions[snapshot.index].name !== "beginPath")
+                --snapshot.index;
+
+            snapshot.element = document.createElement("canvas");
+            snapshot.context = snapshot.element.getContext("2d", ...initialState.parameters);
+            if ("width" in initialState.attributes)
+                snapshot.element.width = initialState.attributes.width;
+            if ("height" in initialState.attributes)
+                snapshot.element.height = initialState.attributes.height;
+
+            let lastSnapshotIndex = snapshotIndex;
+            while (--lastSnapshotIndex >= 0) {
+                if (this._snapshots[--lastSnapshotIndex])
+                    break;
+            }
+
+            let startIndex = 0;
+            if (lastSnapshotIndex < 0) {
+                snapshot.content = this._initialContent;
+                snapshot.state = {};
+
+                for (let key in initialState.attributes) {
+                    let value = initialState.attributes[key];
+                    if (key === "strokeStyle" || key === "fillStyle")
+                        value = this.representedObject.swizzle(value, WebInspector.Recording.Swizzle.CanvasStyle);
+
+                    if (value === WebInspector.Recording.Swizzle.Invalid)
+                        continue;
+
+                    snapshot.state[key] = value;
+                }
+            } else {
+                snapshot.content = this._snapshots[lastSnapshotIndex].content;
+                snapshot.state = this._snapshots[lastSnapshotIndex].state;
+                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,
+                };
+            });
+
+            snapshot.content = new Image;
+            snapshot.content.src = snapshot.element.toDataURL();
+            snapshot.content.addEventListener("load", imageLoad);
+            return;
+        }
+
+        this._previewContainer.removeChildren();
+
+        applyActions(snapshot.index, this._index, () => {
+            if (options.onCompleteCallback)
+                options.onCompleteCallback(snapshot.context);
+        });
+
+        this._previewContainer.appendChild(snapshot.element);
+        this._updateImageGrid();
+    }
+
+    _applyAction(context, action, options = {})
+    {
+        if (!action.valid)
+            return;
+
+        if (action.parameters.includes(WebInspector.Recording.Swizzle.Invalid))
+            return;
+
+        try {
+            let name = options.nameOverride || action.name;
+            if (action.isFunction)
+                context[name](...action.parameters);
+            else {
+                if (action.isGetter)
+                    context[name];
+                else
+                    context[name] = action.parameters[0];
+            }
+        } catch (e) {
+            WebInspector.Recording.synthesizeError(WebInspector.UIString("“%s” threw an error.").format(action.name));
+
+            action.valid = false;
+        }
+    }
+
+    _updateImageGrid()
+    {
+        let activated = WebInspector.settings.showImageGrid.value;
+        this._showGridButtonNavigationItem.activated = activated;
+
+        let snapshotIndex = Math.floor(this._index / WebInspector.RecordingContentView.SnapshotInterval);
+        if (!isNaN(this._index) && this._snapshots[snapshotIndex])
+            this._snapshots[snapshotIndex].element.classList.toggle("show-grid", activated);
+    }
+
+    _showGridButtonClicked(event)
+    {
+        WebInspector.settings.showImageGrid.value = !this._showGridButtonNavigationItem.activated;
+
+        this._updateImageGrid();
+    }
+};
+
+WebInspector.RecordingContentView.SnapshotInterval = 5000;
diff --git a/Source/WebInspectorUI/UserInterface/Views/RecordingNavigationSidebarPanel.css b/Source/WebInspectorUI/UserInterface/Views/RecordingNavigationSidebarPanel.css
new file mode 100644 (file)
index 0000000..78507c3
--- /dev/null
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2017 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.recording > :matches(.content, .empty-content-placeholder) {
+    top: var(--navigation-bar-height);
+}
+
+.sidebar > .panel.navigation.recording > .content > .tree-outline {
+    min-height: 100%;
+    background: var(--transparent-stripe-background-gradient);
+}
+
+.sidebar > .panel.navigation.recording > .content > .tree-outline > .item.parent:not(.action, .selected).expanded {
+    background-color: white;
+}
+
+.sidebar > .panel.navigation.recording > .content > .tree-outline .item.action:not(.initial-state)::before {
+    position: relative;
+    top: 3px;
+    content: attr(data-index);
+    font-family: Menlo, monospace;
+    text-align: end;
+}
+
+body[dir=ltr] .sidebar > .panel.navigation.recording > .content > .tree-outline .item.action:not(.initial-state)::before {
+    float: left;
+    margin-right: var(--tree-outline-icon-margin-end);
+    margin-left: 0;
+}
+
+body[dir=rtl] .sidebar > .panel.navigation.recording > .content > .tree-outline .item.action:not(.initial-state)::before {
+    float: right;
+    margin-right: 0;
+    margin-left: var(--tree-outline-icon-margin-end);
+}
+
+.sidebar > .panel.navigation.recording > .content > .tree-outline[data-indent="2"] .item.action:not(.initial-state)::before {
+    min-width: 1.2em;
+}
+
+.sidebar > .panel.navigation.recording > .content > .tree-outline[data-indent="3"] .item.action:not(.initial-state)::before {
+    min-width: 1.9em;
+}
+
+.sidebar > .panel.navigation.recording > .content > .tree-outline[data-indent="4"] .item.action:not(.initial-state)::before {
+    min-width: 3.1em;
+}
+
+.sidebar > .panel.navigation.recording > .content > .tree-outline[data-indent="5"] .item.action:not(.initial-state)::before {
+    min-width: 3.7em;
+}
+
+.sidebar > .panel.navigation.recording > .content > .tree-outline[data-indent="6"] .item.action:not(.initial-state)::before {
+    min-width: 4.2em;
+}
+
+.sidebar > .panel.navigation.recording > .content > .tree-outline[data-indent="7"] .item.action:not(.initial-state)::before {
+    min-width: 5.4em;
+}
+
+.sidebar > .panel.navigation.recording > .content .action > .icon {
+    content: url("../Images/Source.svg");
+}
+
+.sidebar > .panel.navigation.recording > .content .action.function > .icon {
+    content: url(../Images/Function.svg);
+}
+
+.sidebar > .panel.navigation.recording > .content .action.attribute.getter > .icon {
+    content: url(../Images/Eye.svg);
+}
+
+.sidebar > .panel.navigation.recording > .content .tree-outline:matches(:focus, .force-focus) .action.attribute.getter.selected > .icon {
+    filter: invert();
+}
+
+.sidebar > .panel.navigation.recording > .content .action.attribute.boolean > .icon {
+    content: url(../Images/TypeBoolean.svg);
+}
+
+.sidebar > .panel.navigation.recording > .content .action.attribute.number > .icon {
+    content: url(../Images/TypeNumber.svg);
+}
+
+.sidebar > .panel.navigation.recording > .content .action.attribute.object > .icon {
+    content: url(../Images/TypeObject.svg);
+}
+
+.sidebar > .panel.navigation.recording > .content .action.attribute.string > .icon {
+    content: url(../Images/TypeString.svg);
+}
+
+.sidebar > .panel.navigation.recording > .content > .tree-outline > .item.parent:not(.action) > .icon {
+    content: url(../Images/RenderingFrame.svg);
+}
+
+.sidebar > .panel.navigation.recording > .content .action:matches(.invalid, .missing) > .icon {
+    filter: grayscale();
+}
+
+body[dir=ltr] .sidebar > .panel.navigation.recording > .content .action:not(.initial-state) > .icon {
+    margin-left: 0;
+}
+
+body[dir=rtl] .sidebar > .panel.navigation.recording > .content .action:not(.initial-state) > .icon {
+    margin-right: 0;
+}
+
+.sidebar > .panel.navigation.recording > .content .action.visual:not(.selected, .invalid) {
+    background-color: var(--value-visual-highlight);
+}
+
+.sidebar > .panel.navigation.recording > .content .action:not(.selected, .initial-state) > .titles .parameter.swizzled {
+    color: grey;
+}
+
+.sidebar > .panel.navigation.recording > .content .action.invalid:not(.selected, .initial-state) > .titles .name,
+.sidebar > .panel.navigation.recording > .content .action.missing:not(.selected, .initial-state) > .titles .parameter.swizzled.missing {
+    color: red;
+}
diff --git a/Source/WebInspectorUI/UserInterface/Views/RecordingNavigationSidebarPanel.js b/Source/WebInspectorUI/UserInterface/Views/RecordingNavigationSidebarPanel.js
new file mode 100644 (file)
index 0000000..d822bc4
--- /dev/null
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+
+WebInspector.RecordingNavigationSidebarPanel = class RecordingNavigationSidebarPanel extends WebInspector.NavigationSidebarPanel
+{
+    constructor()
+    {
+        super("recording", WebInspector.UIString("Recording"));
+
+        this.contentTreeOutline.customIndent = true;
+
+        this.filterBar.placeholder = WebInspector.UIString("Filter Actions");
+
+        this.recording = null;
+
+        this._importButton = null;
+        this._exportButton = null;
+    }
+
+    // Static
+
+    static disallowInstanceForClass()
+    {
+        return true;
+    }
+
+    // Public
+
+    set recording(recording)
+    {
+        if (recording === this._recording)
+            return;
+
+        this.contentTreeOutline.removeChildren();
+
+        this._recording = recording;
+
+        if (this._recording) {
+            this.contentTreeOutline.element.dataset.indent = Number.countDigits(this._recording.actions.length);
+
+            if (this._recording.actions[0] instanceof WebInspector.RecordingInitialStateAction)
+                this.contentTreeOutline.appendChild(new WebInspector.RecordingActionTreeElement(this._recording.actions[0], 0, this._recording.type));
+
+            let cumulativeActionIndex = 1;
+            this._recording.frames.forEach((frame, frameIndex) => {
+                let folder = new WebInspector.FolderTreeElement(WebInspector.UIString("Frame %d").format((frameIndex + 1).toLocaleString()));
+                this.contentTreeOutline.appendChild(folder);
+
+                for (let i = 0; i < frame.actions.length; ++i)
+                    folder.appendChild(new WebInspector.RecordingActionTreeElement(frame.actions[i], cumulativeActionIndex + i, this._recording.type));
+
+                if (frame.incomplete)
+                    folder.subtitle = WebInspector.UIString("Incomplete");
+
+                if (this._recording.frames.length === 1)
+                    folder.expand();
+
+                cumulativeActionIndex += frame.actions.length;
+            });
+        }
+
+        this.updateEmptyContentPlaceholder(WebInspector.UIString("No Recording Data"));
+
+        if (this._exportButton)
+            this._exportButton.disabled = !this.contentTreeOutline.children.length;
+    }
+
+    updateActionIndex(index, options = {})
+    {
+        console.assert(!this._recording || (index >= 0 && index < this._recording.actions.length));
+        if (!this._recording || index < 0 || index >= this._recording.actions.length)
+            return;
+
+        let treeOutline = this.contentTreeOutline;
+        if (!(this._recording.actions[0] instanceof WebInspector.RecordingInitialStateAction) || index) {
+            treeOutline = treeOutline.children[0];
+            while (index > treeOutline.children.length) {
+                index -= treeOutline.children.length;
+                treeOutline = treeOutline.nextSibling;
+            }
+
+            // Must subtract one from the final result since the initial state is considered index 0.
+            --index;
+        }
+
+        let treeElementToSelect = treeOutline.children[index];
+        if (treeElementToSelect.parent && !treeElementToSelect.parent.expanded)
+            treeElementToSelect = treeElementToSelect.parent;
+
+        const omitFocus = false;
+        const selectedByUser = false;
+        let suppressOnSelect = !(treeElementToSelect instanceof WebInspector.FolderTreeElement);
+        const suppressOnDeselect = true;
+        treeElementToSelect.revealAndSelect(omitFocus, selectedByUser, suppressOnSelect, suppressOnDeselect);
+    }
+
+    // Protected
+
+    initialLayout()
+    {
+        super.initialLayout();
+
+        const role = "button";
+
+        const importLabel = WebInspector.UIString("Import");
+        let importNavigationItem = new WebInspector.NavigationItem("recording-import", role, importLabel);
+
+        this._importButton = importNavigationItem.element.appendChild(document.createElement("button"));
+        this._importButton.textContent = importLabel;
+        this._importButton.addEventListener("click", this._importNavigationItemClicked.bind(this));
+
+        const exportLabel = WebInspector.UIString("Export");
+        let exportNavigationItem = new WebInspector.NavigationItem("recording-export", role, exportLabel);
+
+        this._exportButton = exportNavigationItem.element.appendChild(document.createElement("button"));
+        this._exportButton.textContent = exportLabel;
+        this._exportButton.disabled = !this.contentTreeOutline.children.length;
+        this._exportButton.addEventListener("click", this._exportNavigationItemClicked.bind(this));
+
+        const element = null;
+        this.addSubview(new WebInspector.NavigationBar(element, [importNavigationItem, exportNavigationItem]));
+    }
+
+    // Private
+
+    _importNavigationItemClicked(event)
+    {
+        WebInspector.loadDataFromFile((data) => {
+            if (!data)
+                return;
+
+            let payload = null;
+            try {
+                payload = JSON.parse(data);
+            } catch (e) {
+                WebInspector.Recording.synthesizeError(e);
+                return;
+            }
+
+            this.dispatchEventToListeners(WebInspector.RecordingNavigationSidebarPanel.Event.Import, {payload});
+        });
+    }
+
+    _exportNavigationItemClicked(event)
+    {
+        if (!this._recording)
+            return;
+
+        const forceSaveAs = true;
+        WebInspector.saveDataToFile({
+            url: "web-inspector:///Recording.json",
+            content: JSON.stringify(this._recording.toJSON()),
+        }, forceSaveAs);
+    }
+};
+
+WebInspector.RecordingNavigationSidebarPanel.Event = {
+    Import: "recording-navigation-sidebar-panel-import",
+};
diff --git a/Source/WebInspectorUI/UserInterface/Views/RecordingStateDetailsSidebarPanel.css b/Source/WebInspectorUI/UserInterface/Views/RecordingStateDetailsSidebarPanel.css
new file mode 100644 (file)
index 0000000..a8c0c81
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2017 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+.sidebar > .panel.details.recording-state > .content > .data-grid {
+    min-height: 100%;
+}
+
+.sidebar > .panel.details.recording-state > .content > .data-grid tr.modified {
+    background-color: var(--value-changed-highlight);
+}
+
+.sidebar > .panel.details.recording-state > .content > .data-grid tr:not(.selected).non-standard {
+    opacity: 0.5;
+}
+
+.sidebar > .panel.details.recording-state > .content > .data-grid tr:not(.selected) .unavailable {
+    color: grey;
+}
diff --git a/Source/WebInspectorUI/UserInterface/Views/RecordingStateDetailsSidebarPanel.js b/Source/WebInspectorUI/UserInterface/Views/RecordingStateDetailsSidebarPanel.js
new file mode 100644 (file)
index 0000000..088f99d
--- /dev/null
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+WebInspector.RecordingStateDetailsSidebarPanel = class RecordingStateDetailsSidebarPanel extends WebInspector.DetailsSidebarPanel
+{
+    constructor()
+    {
+        super("recording-state", WebInspector.UIString("State"));
+
+        this._recording = null;
+        this._index = NaN;
+
+        this._dataGrid = null;
+    }
+
+    // Static
+
+    static disallowInstanceForClass()
+    {
+        return true;
+    }
+
+    // Public
+
+    inspect(objects)
+    {
+        if (!(objects instanceof Array))
+            objects = [objects];
+
+        this.recording = objects.find((object) => object instanceof WebInspector.Recording && object.type === WebInspector.Recording.Type.Canvas2D);
+
+        return !!this._recording;
+    }
+
+    set recording(recording)
+    {
+        if (recording === this._recording)
+            return;
+
+        this._recording = recording;
+        this._index = NaN;
+
+        for (let subview of this.contentView.subviews)
+            this.contentView.removeSubview(subview);
+    }
+
+    updateActionIndex(index, context, options = {})
+    {
+        console.assert(!this._recording || (index >= 0 && index < this._recording.actions.length));
+        if (!this._recording || index < 0 || index > this._recording.actions.length || index === this._index)
+            return;
+
+        this._index = index;
+
+        if (this._recording.type === WebInspector.Recording.Type.Canvas2D)
+            this._generateDetailsCanvas2D(context, options);
+
+        this.updateLayoutIfNeeded();
+    }
+
+    // Private
+
+    _generateDetailsCanvas2D(context, options = {})
+    {
+        if (!this._dataGrid) {
+            this._dataGrid = new WebInspector.DataGrid({
+                name: {title: WebInspector.UIString("Name")},
+                value: {title: WebInspector.UIString("Value")},
+            });
+        }
+        if (!this._dataGrid.parentView)
+            this.contentView.addSubview(this._dataGrid);
+
+        this._dataGrid.removeChildren();
+
+        if (!context)
+            return;
+
+        let state = {
+            direction: context.direction,
+            fillStyle: context.fillStyle,
+            font: context.font,
+            globalAlpha: context.globalAlpha,
+            globalCompositeOperation: context.globalCompositeOperation,
+            imageSmoothingEnabled: context.imageSmoothingEnabled,
+            imageSmoothingQuality: context.imageSmoothingQuality,
+            lineCap: context.lineCap,
+            lineDash: context.getLineDash(),
+            lineDashOffset: context.lineDashOffset,
+            lineJoin: context.lineJoin,
+            lineWidth: context.lineWidth,
+            miterLimit: context.miterLimit,
+            shadowBlur: context.shadowBlur,
+            shadowColor: context.shadowColor,
+            shadowOffsetX: context.shadowOffsetX,
+            shadowOffsetY: context.shadowOffsetY,
+            strokeStyle: context.strokeStyle,
+            textAlign: context.textAlign,
+            textBaseline: context.textBaseline,
+            transform: context.getTransform(),
+            webkitImageSmoothingEnabled: context.webkitImageSmoothingEnabled,
+            webkitLineDash: context.webkitLineDash,
+            webkitLineDashOffset: context.webkitLineDashOffset,
+        };
+
+        let action = this._recording.actions[this._index];
+        for (let name in state) {
+            let value = state[name];
+            if (typeof value === "object") {
+                let isGradient = value instanceof CanvasGradient;
+                let isPattern = value instanceof CanvasPattern;
+                if (isGradient || isPattern) {
+                    value = document.createElement("span");
+                    value.classList.add("unavailable");
+                    if (isGradient)
+                        value.textContent = WebInspector.unlocalizedString("Gradient");
+                    else if (isPattern)
+                        value.textContent = WebInspector.unlocalizedString("Pattern");
+                } else {
+                    if (value instanceof DOMMatrix)
+                        value = [value.a, value.b, value.c, value.d, value.e, value.f];
+
+                    value = JSON.stringify(value);
+                }
+            }
+
+            let classNames = [];
+            if (!action.isGetter && action.stateModifiers.includes(name))
+                classNames.push("modified");
+            if (name.startsWith("webkit"))
+                classNames.push("non-standard");
+
+            const hasChildren = false;
+            this._dataGrid.appendChild(new WebInspector.DataGridNode({name, value}, hasChildren, classNames));
+        }
+    }
+};
diff --git a/Source/WebInspectorUI/UserInterface/Views/RecordingTabContentView.js b/Source/WebInspectorUI/UserInterface/Views/RecordingTabContentView.js
new file mode 100644 (file)
index 0000000..7492487
--- /dev/null
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+WebInspector.RecordingTabContentView = class RecordingTabContentView extends WebInspector.ContentBrowserTabContentView
+{
+    constructor()
+    {
+        let {image, title} = WebInspector.RecordingTabContentView.tabInfo();
+        let tabBarItem = new WebInspector.GeneralTabBarItem(image, title);
+
+        const navigationSidebarPanelConstructor = WebInspector.RecordingNavigationSidebarPanel;
+        const detailsSidebarPanelConstructors = [WebInspector.RecordingStateDetailsSidebarPanel, WebInspector.CanvasDetailsSidebarPanel];
+        const disableBackForward = true;
+        let flexibleNavigationItem = new WebInspector.ScrubberNavigationItem;
+        super("recording", "recording", tabBarItem, navigationSidebarPanelConstructor, detailsSidebarPanelConstructors, disableBackForward, flexibleNavigationItem);
+
+        this._visualActionIndexes = [];
+
+        this._scrubberNavigationItem = flexibleNavigationItem;
+        this._scrubberNavigationItem.value = 0;
+        this._scrubberNavigationItem.disabled = true;
+        this._scrubberNavigationItem.addEventListener(WebInspector.ScrubberNavigationItem.Event.ValueChanged, this._scrubberNavigationItemValueChanged, this);
+
+        this.navigationSidebarPanel.addEventListener(WebInspector.RecordingNavigationSidebarPanel.Event.Import, this._navigationSidebarImport, this);
+        this.navigationSidebarPanel.contentTreeOutline.addEventListener(WebInspector.TreeOutline.Event.SelectionDidChange, this._navigationSidebarTreeOutlineSelectionChanged, this);
+
+        this._recording = null;
+    }
+
+    // Static
+
+    static tabInfo()
+    {
+        return {
+            image: "Images/Recording.svg",
+            title: WebInspector.UIString("Recording"),
+        };
+    }
+
+    // Public
+
+    get type()
+    {
+        return WebInspector.RecordingTabContentView.Type;
+    }
+
+    canShowRepresentedObject(representedObject)
+    {
+        // Once a recording has been loaded for this tab, do not allow another one to be loaded.
+        // This will cause new tabs to be opened for each recording, which is the desired behavior.
+        if (this._recording)
+            return false;
+
+        return representedObject instanceof WebInspector.Recording;
+    }
+
+    showRepresentedObject(representedObject, cookie)
+    {
+        super.showRepresentedObject(representedObject, cookie);
+
+        this._recording = representedObject;
+
+        this._visualActionIndexes = [];
+        representedObject.actions.forEach((action, i) => {
+            if (action.isVisual)
+                this._visualActionIndexes.push(i);
+        });
+
+        this._scrubberNavigationItem.value = 0;
+        this._scrubberNavigationItem.min = 0;
+        this._scrubberNavigationItem.max = representedObject.actions.length - 1;
+        this._scrubberNavigationItem.disabled = false;
+
+        this.navigationSidebarPanel.recording = representedObject;
+
+        this._updateActionIndex(this._scrubberNavigationItem.value);
+    }
+
+    // Protected
+
+    restoreStateFromCookie(restorationType)
+    {
+        // Don't attempt to do anything to this tab.
+    }
+
+    saveStateToCookie(cookie)
+    {
+        // Don't attempt to do anything to this tab.
+    }
+
+    closed()
+    {
+        super.closed();
+
+        for (let detailsSidebarPanel of this.detailsSidebarPanels)
+            detailsSidebarPanel.recording = null;
+
+        this.navigationSidebarPanel.recording = null;
+    }
+
+    // Private
+
+    _updateActionIndex(index, options = {})
+    {
+        this._scrubberNavigationItem.value = index;
+
+        options.onCompleteCallback = (context) => {
+            for (let detailsSidebarPanel of this.detailsSidebarPanels) {
+                if (detailsSidebarPanel.updateActionIndex)
+                    detailsSidebarPanel.updateActionIndex(index, context, options);
+            }
+        };
+
+        this.contentBrowser.currentContentView.updateActionIndex(index, options);
+
+        // This must be placed last, as it is possible for the update of the NavigationSidebarPanel
+        // to trigger another update of the index, such as if the selected index is not expanded.
+        if (!options.suppressNavigationUpdate)
+            this.navigationSidebarPanel.updateActionIndex(index, options);
+    }
+
+    _scrubberNavigationItemValueChanged(event)
+    {
+        for (let i = 0; i <= this._visualActionIndexes.length; ++i) {
+            if (this._visualActionIndexes[i] < this._scrubberNavigationItem.value)
+                continue;
+
+            let min = i ? this._visualActionIndexes[i - 1] : this._scrubberNavigationItem.min;
+            let max = i < this._visualActionIndexes.length ? this._visualActionIndexes[i] : this._scrubberNavigationItem.max;
+            this._updateActionIndex(this._scrubberNavigationItem.value >= (min + max) / 2 ? max : min);
+            return;
+        }
+    }
+
+    _navigationSidebarImport(event)
+    {
+        let recording = WebInspector.Recording.fromPayload(event.data.payload);
+        if (!recording) {
+            WebInspector.Recording.synthesizeError(WebInspector.UIString("unsupported version."));
+            return;
+        }
+
+        this.showRepresentedObject(recording);
+    }
+
+    _navigationSidebarTreeOutlineSelectionChanged(event)
+    {
+        // Ignore deselect events.
+        if (!event.data.selectedElement)
+            return;
+
+        let options = {suppressNavigationUpdate: true};
+        let selectedTreeElement = this.navigationSidebarPanel.contentTreeOutline.selectedTreeElement;
+        if (selectedTreeElement instanceof WebInspector.FolderTreeElement)
+            selectedTreeElement = selectedTreeElement.children.lastValue;
+        this._updateActionIndex(selectedTreeElement.index, options);
+    }
+};
+
+WebInspector.RecordingTabContentView.Type = "recording";
diff --git a/Source/WebInspectorUI/UserInterface/Views/ScrubberNavigationItem.css b/Source/WebInspectorUI/UserInterface/Views/ScrubberNavigationItem.css
new file mode 100644 (file)
index 0000000..4a2120a
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+.navigation-bar .item.scrubber {
+    padding: 0 4px;
+}
+
+.navigation-bar .item.scrubber > input {
+    width: 100%;
+}
+
+.navigation-bar .item.scrubber > input[disabled] {
+    opacity: 0.5;
+    pointer-events: none;
+} 
diff --git a/Source/WebInspectorUI/UserInterface/Views/ScrubberNavigationItem.js b/Source/WebInspectorUI/UserInterface/Views/ScrubberNavigationItem.js
new file mode 100644 (file)
index 0000000..efd643a
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+WebInspector.ScrubberNavigationItem = class ScrubberNavigationItem extends WebInspector.FlexibleSpaceNavigationItem
+{
+    constructor(identifier)
+    {
+        super(identifier);
+
+        this._sliderElement = this._element.appendChild(document.createElement("input"));
+        this._sliderElement.type = "range";
+        this._sliderElement.addEventListener("input", this._sliderChanged.bind(this));
+    }
+
+    // Public
+
+    get value() { return parseInt(this._sliderElement.value); }
+    set value(value) { this._sliderElement.value = value; }
+
+    get min() { return parseInt(this._sliderElement.min); }
+    set min(min) { this._sliderElement.min = min; }
+
+    get max() { return parseInt(this._sliderElement.max); }
+    set max(max) { this._sliderElement.max = max; }
+
+    get disabled()
+    {
+        return this._sliderElement.disabled;
+    }
+
+    set disabled(flag)
+    {
+        this._sliderElement.disabled = !!flag;
+    }
+
+    // Protected
+
+    get additionalClassNames()
+    {
+        return super.additionalClassNames.concat(["scrubber"]);
+    }
+
+    // Private
+
+    _sliderChanged(event)
+    {
+        this.dispatchEventToListeners(WebInspector.ScrubberNavigationItem.Event.ValueChanged);
+    }
+};
+
+WebInspector.ScrubberNavigationItem.Event = {
+    ValueChanged: "slider-navigation-item-value-changed",
+};
index cc34f58..68a34a5 100644 (file)
@@ -161,6 +161,7 @@ WebInspector.SettingsTabContentView = class SettingsTabContentView extends WebIn
         this.addSubview(this._navigationBar);
 
         this._createGeneralSettingsView();
+        this._createExperimentalSettingsView();
 
         WebInspector.notifications.addEventListener(WebInspector.Notification.DebugUIEnabledDidChange, this._updateDebugSettingsViewVisibility, this);
         this._updateDebugSettingsViewVisibility();
@@ -226,6 +227,42 @@ WebInspector.SettingsTabContentView = class SettingsTabContentView extends WebIn
         this.addSettingsView(generalSettingsView);
     }
 
+    _createExperimentalSettingsView()
+    {
+        let experimentalSettingsView = new WebInspector.SettingsView("experimental", WebInspector.UIString("Experimental"));
+
+        if (window.CanvasAgent) {
+            experimentalSettingsView.addSetting(WebInspector.UIString("Canvas:"), WebInspector.settings.experimentalShowCanvasContextsInResources, WebInspector.UIString("Show Contexts in Resources Tab"));
+
+            experimentalSettingsView.addSeparator();
+        }
+
+        experimentalSettingsView.addSetting(WebInspector.UIString("Styles Panel:"), WebInspector.settings.experimentalSpreadsheetStyleEditor, WebInspector.UIString("Spreadsheet Style Editor"));
+
+        experimentalSettingsView.addSeparator();
+
+        let reloadInspectorButton = document.createElement("button");
+        reloadInspectorButton.textContent = WebInspector.UIString("Reload Web Inspector");
+        reloadInspectorButton.addEventListener("click", () => { window.location.reload(); });
+
+        let reloadInspectorContainerElement = experimentalSettingsView.addCenteredContainer(reloadInspectorButton, WebInspector.UIString("for changes to take effect"));
+        reloadInspectorContainerElement.classList.add("hidden");
+
+        function listenForChange(setting) {
+            let initialValue = setting.value;
+            setting.addEventListener(WebInspector.Setting.Event.Changed, () => {
+                reloadInspectorContainerElement.classList.toggle("hidden", initialValue === setting.value);
+            });
+        }
+
+        if (window.CanvasAgent)
+            listenForChange(WebInspector.settings.experimentalShowCanvasContextsInResources);
+
+        listenForChange(WebInspector.settings.experimentalSpreadsheetStyleEditor);
+
+        this.addSettingsView(experimentalSettingsView);
+    }
+
     _createDebugSettingsView()
     {
         if (this._debugSettingsView)
@@ -260,29 +297,6 @@ WebInspector.SettingsTabContentView = class SettingsTabContentView extends WebIn
         layoutDirectionEditor.value = WebInspector.settings.layoutDirection.value;
         layoutDirectionEditor.addEventListener(WebInspector.SettingEditor.Event.ValueDidChange, () => { WebInspector.setLayoutDirection(layoutDirectionEditor.value); });
 
-        if (window.CanvasAgent) {
-            this._debugSettingsView.addSeparator();
-
-            this._debugSettingsView.addSetting(WebInspector.UIString("Canvas:"), WebInspector.settings.experimentalShowCanvasContextsInResources, WebInspector.UIString("Show Contexts in Resources Tab"));
-        }
-
-        this._debugSettingsView.addSeparator();
-        this._debugSettingsView.addSetting(WebInspector.unlocalizedString("Styles Panel:"), WebInspector.settings.experimentalSpreadsheetStyleEditor, WebInspector.unlocalizedString("Spreadsheet Style Editor"));
-
-        this._debugSettingsView.addSeparator();
-
-        let reloadInspectorButton = document.createElement("button");
-        reloadInspectorButton.textContent = WebInspector.unlocalizedString("Reload Web Inspector");
-        reloadInspectorButton.addEventListener("click", () => { window.location.reload(); });
-
-        let reloadInspectorContainerElement = this._debugSettingsView.addCenteredContainer(reloadInspectorButton, WebInspector.unlocalizedString("for changes to take effect"));
-        reloadInspectorContainerElement.classList.add("hidden");
-
-        let experimentalSpreadsheetStyleEditorInitialValue = WebInspector.settings.experimentalSpreadsheetStyleEditor.value;
-        WebInspector.settings.experimentalSpreadsheetStyleEditor.addEventListener(WebInspector.Setting.Event.Changed, () => {
-            reloadInspectorContainerElement.classList.toggle("hidden", experimentalSpreadsheetStyleEditorInitialValue === WebInspector.settings.experimentalSpreadsheetStyleEditor.value);
-        });
-
         this.addSettingsView(this._debugSettingsView);
     }
 
index 636b283..82aa506 100644 (file)
@@ -177,7 +177,9 @@ WebInspector.TabContentView = class TabContentView extends WebInspector.ContentV
     {
         if (!this._navigationSidebarPanelConstructor)
             return null;
-        return WebInspector.instanceForClass(this._navigationSidebarPanelConstructor);
+        if (!this._navigationSidebarPanel)
+            this._navigationSidebarPanel = WebInspector.instanceForClass(this._navigationSidebarPanelConstructor);
+        return this._navigationSidebarPanel;
     }
 
     get navigationSidebarCollapsedSetting() { return this._navigationSidebarCollapsedSetting; }
index 9cac987..8f7398d 100644 (file)
@@ -74,7 +74,7 @@
 }
 
 .rendering-frame-record .icon {
-    content: url(../Images/TimelineRecordRenderingFrame.svg);
+    content: url(../Images/RenderingFrame.svg);
 }
 
 .api-record .icon {
index f7a3d4b..345ca9e 100644 (file)
@@ -42,6 +42,5 @@
 }
 
 .panel.navigation.timeline.timeline-recording-content-view-showing > .content > .tree-outline {
-    background-image: linear-gradient(to bottom, transparent, transparent 50%, hsla(0, 0%, 0%, 0.03) 50%, hsla(0, 0%, 0%, 0.03));
-    background-size: 100% 40px;
+    background: var(--transparent-stripe-background-gradient);
 }
index 498b44e..95db84b 100644 (file)
@@ -61,6 +61,9 @@
     --text-color-gray-medium: hsl(0, 0%, 50%);
     --error-text-color: hsl(0, 86%, 47%);
 
+    --value-changed-highlight: hsla(83, 100%, 48%, 0.4);
+    --value-visual-highlight: hsla(60, 100%, 50%, 0.4);
+
     --breakpoint-fill-color: hsl(212, 45%, 54%);
     --breakpoint-stroke-color: hsl(212, 45%, 48%);
     --breakpoint-disabled-fill-color: hsl(212, 40%, 84%);
@@ -91,6 +94,7 @@
 
     --even-zebra-stripe-row-background-color: white;
     --odd-zebra-stripe-row-background-color: hsl(0, 0%, 95%);
+    --transparent-stripe-background-gradient: linear-gradient(to bottom, transparent, transparent 50%, hsla(0, 0%, 0%, 0.03) 50%, hsla(0, 0%, 0%, 0.03)) top left / 100% 40px;
 
     --toolbar-height: 36px;
     --tab-bar-height: 24px;