Web Inspector: Add Canvas tab and CanvasOverviewContentView
authormattbaker@apple.com <mattbaker@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 6 Oct 2017 23:31:42 +0000 (23:31 +0000)
committermattbaker@apple.com <mattbaker@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 6 Oct 2017 23:31:42 +0000 (23:31 +0000)
https://bugs.webkit.org/show_bug.cgi?id=177604
<rdar://problem/34714650>

Reviewed by Devin Rousso.

Source/WebInspectorUI:

This patch adds experimental feature support for the Canvas tab. Initially
the tab provides only an overview of the canvases in the page, and will
exist side-by-side with the existing experimental Canvas UI.

* Localizations/en.lproj/localizedStrings.js:
* UserInterface/Base/Main.js:
(WI.contentLoaded):
* UserInterface/Base/Setting.js:

* UserInterface/Images/Canvas.svg: Added.
* UserInterface/Images/CanvasOverview.svg: Added.
* UserInterface/Main.html:
Add new art and canvas UI classes.

* UserInterface/Models/Canvas.js:
(WI.Canvas.requestNode):
(WI.Canvas.prototype.requestContent):
(WI.Canvas.prototype.requestCSSCanvasClientNodes):
(WI.Canvas.prototype.requestSize.calculateSize.getAttributeValue):
(WI.Canvas.prototype.requestSize.calculateSize):
(WI.Canvas.prototype.requestSize.getPropertyValue):
(WI.Canvas.prototype.requestSize):
Use promises to retrieve canvas data asynchronously.

* UserInterface/Models/CollectionTypes.js: Added.
(WI.CanvasCollection):
New location for concrete collection types. Having a class to type check
makes using a collection as a represented object a bit simpler.

* UserInterface/Views/CanvasContentView.css:
(.content-view.canvas:not(.tab)):
(.content-view.canvas:not(.tab) > .preview):
(.content-view.canvas:not(.tab) > .preview > img):
(.content-view.canvas:not(.tab) > :matches(header, footer)):
(.content-view.canvas): Deleted.
(.content-view.canvas > .preview): Deleted.
(.content-view.canvas > .preview > img): Deleted.
During the transition to the new Canvas tab, CanvasContentView needs to
support being shown as a full-size content view, and as an item in a
CollectionContentView. Hide header and footer elements by default.

* UserInterface/Views/CanvasContentView.js:
(WI.CanvasContentView):
(WI.CanvasContentView.prototype.refresh):
(WI.CanvasContentView.prototype.initialLayout):
(WI.CanvasContentView.prototype.layout):
(WI.CanvasContentView.prototype.shown):
(WI.CanvasContentView.prototype.attached):
(WI.CanvasContentView.prototype.detached):
(WI.CanvasContentView.prototype._showError):
(WI.CanvasContentView.prototype._refreshPixelSize):
(WI.CanvasContentView.prototype._showGridButtonClicked):
(WI.CanvasContentView.prototype._updateImageGrid):
(WI.CanvasContentView.prototype._updateMemoryCost):
(WI.CanvasContentView.prototype._updatePixelSize):
(WI.CanvasContentView.prototype._updateRecordNavigationItem):
(WI.CanvasContentView.prototype.hidden): Deleted.
(WI.CanvasContentView.prototype.closed): Deleted.
(WI.CanvasContentView.prototype._showPreview): Deleted.
Added new UI for display in the Canvas overview. These elements are always
created, but only appear when the canvas is viewed as a "card".

Canvas previews are no longer shown as soon as they are available from
the backend. Instead, once the canvas content is ready a layout is scheduled.
This guarantees that refreshing all canvases at once causes no flicker,
and introduces no perceptible delay.

Finally, the "Cancel recording" tooltip has been renamed "Stop recording",
to match the behavior of the command.

* UserInterface/Views/CanvasDetailsSidebarPanel.js:
(WI.CanvasDetailsSidebarPanel.prototype._refreshSourceSection.this._canvas.requestNode.): Deleted.
Canvas.prototype.requestNode now returns a promise.

* UserInterface/Views/CanvasOverviewContentView.css: Added.
(.content-view.canvas-overview):
(.content-view.canvas-overview .content-view.canvas):
(.content-view.canvas-overview .content-view.canvas.selected:not(.is-recording)):
(.content-view.canvas-overview .content-view.canvas > :matches(header, footer)):
(.content-view.canvas-overview .content-view.canvas > header):
(.content-view.canvas-overview .content-view.canvas.is-recording > header):
(.content-view.canvas-overview .content-view.canvas > header > .titles,):
(.content-view.canvas-overview .content-view.canvas > header > .titles > .title):
(.content-view.canvas-overview .content-view.canvas > header > .titles > .subtitle,):
(.content-view.canvas-overview .content-view.canvas > header .subtitle::before):
(.content-view.canvas-overview .content-view.canvas.is-recording > header > .titles > .title):
(.content-view.canvas-overview .content-view.canvas.is-recording > header > .titles > .subtitle):
(.content-view.canvas-overview .content-view.canvas.is-recording > header > .navigation-bar > .item):
(.content-view.canvas-overview .content-view.canvas > header > .navigation-bar):
(.content-view.canvas-overview .content-view.canvas:not(:hover, .is-recording) > header > .navigation-bar):
(.content-view.canvas-overview .content-view.canvas:not(.is-recording) > header > .navigation-bar > .item.record-start-stop.disabled):
(.content-view.canvas-overview .content-view.canvas:not(.is-recording) > header > .navigation-bar > .item.record-start-stop):
(.content-view.canvas-overview .content-view.canvas:not(.is-recording) > header > .navigation-bar > .item.record-start-stop:not(.disabled):hover):
(.content-view.canvas-overview .content-view.canvas:not(.is-recording) > header > .navigation-bar > .item.record-start-stop:not(.disabled):active):
(.content-view.canvas-overview .content-view.canvas > .preview):
(.content-view.canvas-overview .content-view.canvas > .preview > img):
(.content-view.canvas-overview .content-view.canvas > .preview > .message-text-view):
(.content-view.canvas-overview .content-view.canvas > footer):
(.content-view.canvas-overview .content-view.canvas > footer .memory-cost):
Add header, navigation bar, and footer styles to CanvasContentView when
it is being shown as an item in a CollectionContentView.

* UserInterface/Views/CanvasOverviewContentView.js: Added.
(WI.CanvasOverviewContentView):
(WI.CanvasOverviewContentView.prototype.get navigationItems):
(WI.CanvasOverviewContentView.prototype.get selectionPathComponents):
(WI.CanvasOverviewContentView.prototype.hidden):
(WI.CanvasOverviewContentView.prototype.contentViewAdded):
(WI.CanvasOverviewContentView.prototype.contentViewRemoved):
(WI.CanvasOverviewContentView.prototype.attached):
(WI.CanvasOverviewContentView.prototype.detached):
(WI.CanvasOverviewContentView.prototype._refreshPreviews):
(WI.CanvasOverviewContentView.prototype._selectedPathComponentChanged):
(WI.CanvasOverviewContentView.prototype._showGridButtonClicked):
(WI.CanvasOverviewContentView.prototype._supplementalRepresentedObjectsDidChange.createCanvasPathComponent):
(WI.CanvasOverviewContentView.prototype._supplementalRepresentedObjectsDidChange):
(WI.CanvasOverviewContentView.prototype._updateNavigationItems):
(WI.CanvasOverviewContentView.prototype._updateShowImageGrid):
(WI.CanvasOverviewContentView.prototype._contentViewMouseEnter):
(WI.CanvasOverviewContentView.prototype._contentViewMouseLeave):
The overview extends CollectionContentView, adding buttons for global canvas actions
(refresh all and show/hide grid for all), and maintains a non-visible
outline of CanvasTreeElements to facilitate display of the hierarchical
path in the navigation bar.

* UserInterface/Views/CanvasTabContentView.css: Added.
(.content-view.tab.canvas .navigation-bar > .item > .hierarchical-path-component > .icon):
(.content-view.tab.canvas .navigation-bar > .item > .canvas-overview > .icon):
(.content-view.tab.canvas .navigation-bar > .item .canvas .icon):

* UserInterface/Views/CanvasTabContentView.js: Added.
(WI.CanvasTabContentView):
(WI.CanvasTabContentView.tabInfo):
(WI.CanvasTabContentView.isTabAllowed):
(WI.CanvasTabContentView.prototype.get type):
(WI.CanvasTabContentView.prototype.get supportsSplitContentBrowser):
(WI.CanvasTabContentView.prototype.canShowRepresentedObject):
(WI.CanvasTabContentView.prototype.shown):
(WI.CanvasTabContentView.prototype.treeElementForRepresentedObject):
(WI.CanvasTabContentView.prototype.restoreFromCookie):
(WI.CanvasTabContentView.prototype.saveStateToCookie):
(WI.CanvasTabContentView.prototype.attached):
(WI.CanvasTabContentView.prototype.detached):
(WI.CanvasTabContentView.prototype._canvasAdded):
(WI.CanvasTabContentView.prototype._canvasRemoved):
(WI.CanvasTabContentView.prototype._overviewPathComponentClicked):
(WI.CanvasTabContentView.prototype._mainResourceDidChange):

* UserInterface/Views/CollectionContentView.js:
(WI.CollectionContentView):
(WI.CollectionContentView.prototype.setSelectedItem):
(WI.CollectionContentView.prototype.addContentViewForItem):
(WI.CollectionContentView.prototype.removeContentViewForItem):
(WI.CollectionContentView.prototype.initialLayout):
(WI.CollectionContentView.prototype._showContentPlaceholder):
(WI.CollectionContentView.prototype._hideContentPlaceholder):
Placeholder content should be created lazily, and shown after a slight delay
to give represented objects a chance to load. Make sure to call the
shown or hidden method after adding or removing a content view.

* UserInterface/Views/SettingsTabContentView.js:
(WI.SettingsTabContentView.prototype._createExperimentalSettingsView):

* UserInterface/Views/Variables.css:
(:root):
(body.window-inactive *):

* UserInterface/Views/View.js:
(WI.View.fromElement):

LayoutTests:

Add test for new static function View.fromElement.

* inspector/view/basics-expected.txt:
* inspector/view/basics.html:

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

23 files changed:
LayoutTests/ChangeLog
LayoutTests/inspector/view/basics-expected.txt
LayoutTests/inspector/view/basics.html
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
Source/WebInspectorUI/UserInterface/Base/Main.js
Source/WebInspectorUI/UserInterface/Base/Setting.js
Source/WebInspectorUI/UserInterface/Images/Canvas.svg [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Images/CanvasOverview.svg [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Main.html
Source/WebInspectorUI/UserInterface/Models/Canvas.js
Source/WebInspectorUI/UserInterface/Models/CollectionTypes.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/CanvasContentView.css
Source/WebInspectorUI/UserInterface/Views/CanvasContentView.js
Source/WebInspectorUI/UserInterface/Views/CanvasDetailsSidebarPanel.js
Source/WebInspectorUI/UserInterface/Views/CanvasOverviewContentView.css [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/CanvasOverviewContentView.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/CanvasTabContentView.css [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/CanvasTabContentView.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/CollectionContentView.js
Source/WebInspectorUI/UserInterface/Views/SettingsTabContentView.js
Source/WebInspectorUI/UserInterface/Views/Variables.css
Source/WebInspectorUI/UserInterface/Views/View.js

index 349c165..ff03d50 100644 (file)
@@ -1,3 +1,16 @@
+2017-10-06  Matt Baker  <mattbaker@apple.com>
+
+        Web Inspector: Add Canvas tab and CanvasOverviewContentView
+        https://bugs.webkit.org/show_bug.cgi?id=177604
+        <rdar://problem/34714650>
+
+        Reviewed by Devin Rousso.
+
+        Add test for new static function View.fromElement.
+
+        * inspector/view/basics-expected.txt:
+        * inspector/view/basics.html:
+
 2017-10-06  Ryan Haddad  <ryanhaddad@apple.com>
 
         LayoutTest http/tests/loading/resourceLoadStatistics/partitioned-cookies-with-and-without-user-interaction.html is a flaky failure
index 215839e..2502868 100644 (file)
@@ -50,3 +50,9 @@ PASS: View added to a detached parent should not be attached.
 PASS: Attaching a view to the root causes descendent views to be attached.
 PASS: Detaching a view from the root causes descendent views to be detached.
 
+-- Running test case: View.fromElement
+PASS: Should be able to lookup an existing view from its element.
+PASS: Should return null for an element not associated with any view.
+PASS: Should return null for non-element.
+PASS: Should return null for null element.
+
index cc0877e..b277824 100644 (file)
@@ -148,6 +148,19 @@ function test()
         }
     });
 
+    suite.addTestCase({
+        name: "View.fromElement",
+        test() {
+            let view = new WI.View;
+            InspectorTest.expectEqual(WI.View.fromElement(view.element), view, "Should be able to lookup an existing view from its element.");
+            InspectorTest.expectNull(WI.View.fromElement(document.createElement("div")), "Should return null for an element not associated with any view.");
+            InspectorTest.expectNull(WI.View.fromElement({}), "Should return null for non-element.");
+            InspectorTest.expectNull(WI.View.fromElement(null), "Should return null for null element.");
+
+            return true;
+        }
+    });
+
     suite.runTestCasesAndFinish();
 }
 </script>
index ec61e3d..258a176 100644 (file)
@@ -1,3 +1,181 @@
+2017-10-06  Matt Baker  <mattbaker@apple.com>
+
+        Web Inspector: Add Canvas tab and CanvasOverviewContentView
+        https://bugs.webkit.org/show_bug.cgi?id=177604
+        <rdar://problem/34714650>
+
+        Reviewed by Devin Rousso.
+
+        This patch adds experimental feature support for the Canvas tab. Initially
+        the tab provides only an overview of the canvases in the page, and will
+        exist side-by-side with the existing experimental Canvas UI.
+
+        * Localizations/en.lproj/localizedStrings.js:
+        * UserInterface/Base/Main.js:
+        (WI.contentLoaded):
+        * UserInterface/Base/Setting.js:
+
+        * UserInterface/Images/Canvas.svg: Added.
+        * UserInterface/Images/CanvasOverview.svg: Added.
+        * UserInterface/Main.html:
+        Add new art and canvas UI classes.
+
+        * UserInterface/Models/Canvas.js:
+        (WI.Canvas.requestNode):
+        (WI.Canvas.prototype.requestContent):
+        (WI.Canvas.prototype.requestCSSCanvasClientNodes):
+        (WI.Canvas.prototype.requestSize.calculateSize.getAttributeValue):
+        (WI.Canvas.prototype.requestSize.calculateSize):
+        (WI.Canvas.prototype.requestSize.getPropertyValue):
+        (WI.Canvas.prototype.requestSize):
+        Use promises to retrieve canvas data asynchronously.
+
+        * UserInterface/Models/CollectionTypes.js: Added.
+        (WI.CanvasCollection):
+        New location for concrete collection types. Having a class to type check
+        makes using a collection as a represented object a bit simpler.
+
+        * UserInterface/Views/CanvasContentView.css:
+        (.content-view.canvas:not(.tab)):
+        (.content-view.canvas:not(.tab) > .preview):
+        (.content-view.canvas:not(.tab) > .preview > img):
+        (.content-view.canvas:not(.tab) > :matches(header, footer)):
+        (.content-view.canvas): Deleted.
+        (.content-view.canvas > .preview): Deleted.
+        (.content-view.canvas > .preview > img): Deleted.
+        During the transition to the new Canvas tab, CanvasContentView needs to
+        support being shown as a full-size content view, and as an item in a
+        CollectionContentView. Hide header and footer elements by default.
+
+        * UserInterface/Views/CanvasContentView.js:
+        (WI.CanvasContentView):
+        (WI.CanvasContentView.prototype.refresh):
+        (WI.CanvasContentView.prototype.initialLayout):
+        (WI.CanvasContentView.prototype.layout):
+        (WI.CanvasContentView.prototype.shown):
+        (WI.CanvasContentView.prototype.attached):
+        (WI.CanvasContentView.prototype.detached):
+        (WI.CanvasContentView.prototype._showError):
+        (WI.CanvasContentView.prototype._refreshPixelSize):
+        (WI.CanvasContentView.prototype._showGridButtonClicked):
+        (WI.CanvasContentView.prototype._updateImageGrid):
+        (WI.CanvasContentView.prototype._updateMemoryCost):
+        (WI.CanvasContentView.prototype._updatePixelSize):
+        (WI.CanvasContentView.prototype._updateRecordNavigationItem):
+        (WI.CanvasContentView.prototype.hidden): Deleted.
+        (WI.CanvasContentView.prototype.closed): Deleted.
+        (WI.CanvasContentView.prototype._showPreview): Deleted.
+        Added new UI for display in the Canvas overview. These elements are always
+        created, but only appear when the canvas is viewed as a "card".
+
+        Canvas previews are no longer shown as soon as they are available from
+        the backend. Instead, once the canvas content is ready a layout is scheduled.
+        This guarantees that refreshing all canvases at once causes no flicker,
+        and introduces no perceptible delay.
+
+        Finally, the "Cancel recording" tooltip has been renamed "Stop recording",
+        to match the behavior of the command.
+
+        * UserInterface/Views/CanvasDetailsSidebarPanel.js:
+        (WI.CanvasDetailsSidebarPanel.prototype._refreshSourceSection.this._canvas.requestNode.): Deleted.
+        Canvas.prototype.requestNode now returns a promise.
+
+        * UserInterface/Views/CanvasOverviewContentView.css: Added.
+        (.content-view.canvas-overview):
+        (.content-view.canvas-overview .content-view.canvas):
+        (.content-view.canvas-overview .content-view.canvas.selected:not(.is-recording)):
+        (.content-view.canvas-overview .content-view.canvas > :matches(header, footer)):
+        (.content-view.canvas-overview .content-view.canvas > header):
+        (.content-view.canvas-overview .content-view.canvas.is-recording > header):
+        (.content-view.canvas-overview .content-view.canvas > header > .titles,):
+        (.content-view.canvas-overview .content-view.canvas > header > .titles > .title):
+        (.content-view.canvas-overview .content-view.canvas > header > .titles > .subtitle,):
+        (.content-view.canvas-overview .content-view.canvas > header .subtitle::before):
+        (.content-view.canvas-overview .content-view.canvas.is-recording > header > .titles > .title):
+        (.content-view.canvas-overview .content-view.canvas.is-recording > header > .titles > .subtitle):
+        (.content-view.canvas-overview .content-view.canvas.is-recording > header > .navigation-bar > .item):
+        (.content-view.canvas-overview .content-view.canvas > header > .navigation-bar):
+        (.content-view.canvas-overview .content-view.canvas:not(:hover, .is-recording) > header > .navigation-bar):
+        (.content-view.canvas-overview .content-view.canvas:not(.is-recording) > header > .navigation-bar > .item.record-start-stop.disabled):
+        (.content-view.canvas-overview .content-view.canvas:not(.is-recording) > header > .navigation-bar > .item.record-start-stop):
+        (.content-view.canvas-overview .content-view.canvas:not(.is-recording) > header > .navigation-bar > .item.record-start-stop:not(.disabled):hover):
+        (.content-view.canvas-overview .content-view.canvas:not(.is-recording) > header > .navigation-bar > .item.record-start-stop:not(.disabled):active):
+        (.content-view.canvas-overview .content-view.canvas > .preview):
+        (.content-view.canvas-overview .content-view.canvas > .preview > img):
+        (.content-view.canvas-overview .content-view.canvas > .preview > .message-text-view):
+        (.content-view.canvas-overview .content-view.canvas > footer):
+        (.content-view.canvas-overview .content-view.canvas > footer .memory-cost):
+        Add header, navigation bar, and footer styles to CanvasContentView when
+        it is being shown as an item in a CollectionContentView.
+
+        * UserInterface/Views/CanvasOverviewContentView.js: Added.
+        (WI.CanvasOverviewContentView):
+        (WI.CanvasOverviewContentView.prototype.get navigationItems):
+        (WI.CanvasOverviewContentView.prototype.get selectionPathComponents):
+        (WI.CanvasOverviewContentView.prototype.hidden):
+        (WI.CanvasOverviewContentView.prototype.contentViewAdded):
+        (WI.CanvasOverviewContentView.prototype.contentViewRemoved):
+        (WI.CanvasOverviewContentView.prototype.attached):
+        (WI.CanvasOverviewContentView.prototype.detached):
+        (WI.CanvasOverviewContentView.prototype._refreshPreviews):
+        (WI.CanvasOverviewContentView.prototype._selectedPathComponentChanged):
+        (WI.CanvasOverviewContentView.prototype._showGridButtonClicked):
+        (WI.CanvasOverviewContentView.prototype._supplementalRepresentedObjectsDidChange.createCanvasPathComponent):
+        (WI.CanvasOverviewContentView.prototype._supplementalRepresentedObjectsDidChange):
+        (WI.CanvasOverviewContentView.prototype._updateNavigationItems):
+        (WI.CanvasOverviewContentView.prototype._updateShowImageGrid):
+        (WI.CanvasOverviewContentView.prototype._contentViewMouseEnter):
+        (WI.CanvasOverviewContentView.prototype._contentViewMouseLeave):
+        The overview extends CollectionContentView, adding buttons for global canvas actions
+        (refresh all and show/hide grid for all), and maintains a non-visible
+        outline of CanvasTreeElements to facilitate display of the hierarchical
+        path in the navigation bar.
+
+        * UserInterface/Views/CanvasTabContentView.css: Added.
+        (.content-view.tab.canvas .navigation-bar > .item > .hierarchical-path-component > .icon):
+        (.content-view.tab.canvas .navigation-bar > .item > .canvas-overview > .icon):
+        (.content-view.tab.canvas .navigation-bar > .item .canvas .icon):
+
+        * UserInterface/Views/CanvasTabContentView.js: Added.
+        (WI.CanvasTabContentView):
+        (WI.CanvasTabContentView.tabInfo):
+        (WI.CanvasTabContentView.isTabAllowed):
+        (WI.CanvasTabContentView.prototype.get type):
+        (WI.CanvasTabContentView.prototype.get supportsSplitContentBrowser):
+        (WI.CanvasTabContentView.prototype.canShowRepresentedObject):
+        (WI.CanvasTabContentView.prototype.shown):
+        (WI.CanvasTabContentView.prototype.treeElementForRepresentedObject):
+        (WI.CanvasTabContentView.prototype.restoreFromCookie):
+        (WI.CanvasTabContentView.prototype.saveStateToCookie):
+        (WI.CanvasTabContentView.prototype.attached):
+        (WI.CanvasTabContentView.prototype.detached):
+        (WI.CanvasTabContentView.prototype._canvasAdded):
+        (WI.CanvasTabContentView.prototype._canvasRemoved):
+        (WI.CanvasTabContentView.prototype._overviewPathComponentClicked):
+        (WI.CanvasTabContentView.prototype._mainResourceDidChange):
+
+        * UserInterface/Views/CollectionContentView.js:
+        (WI.CollectionContentView):
+        (WI.CollectionContentView.prototype.setSelectedItem):
+        (WI.CollectionContentView.prototype.addContentViewForItem):
+        (WI.CollectionContentView.prototype.removeContentViewForItem):
+        (WI.CollectionContentView.prototype.initialLayout):
+        (WI.CollectionContentView.prototype._showContentPlaceholder):
+        (WI.CollectionContentView.prototype._hideContentPlaceholder):
+        Placeholder content should be created lazily, and shown after a slight delay
+        to give represented objects a chance to load. Make sure to call the
+        shown or hidden method after adding or removing a content view.
+
+        * UserInterface/Views/SettingsTabContentView.js:
+        (WI.SettingsTabContentView.prototype._createExperimentalSettingsView):
+
+        * UserInterface/Views/Variables.css:
+        (:root):
+        (body.window-inactive *):
+
+        * UserInterface/Views/View.js:
+        (WI.View.fromElement):
+
 2017-10-06  Joseph Pecoraro  <pecoraro@apple.com>
 
         Web Inspector: Network Tab - Headers Detail View
index 18707e8..915a249 100644 (file)
@@ -156,10 +156,10 @@ 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";
+localizedStrings["Canvas Overview"] = "Canvas Overview";
 localizedStrings["Canvas:"] = "Canvas:";
 localizedStrings["Canvases"] = "Canvases";
 localizedStrings["Cap"] = "Cap";
@@ -355,6 +355,7 @@ localizedStrings["Element overlaps other compositing element"] = "Element overla
 localizedStrings["Elements"] = "Elements";
 localizedStrings["Enable Breakpoint"] = "Enable Breakpoint";
 localizedStrings["Enable Breakpoints"] = "Enable Breakpoints";
+localizedStrings["Enable Canvas Tab"] = "Enable Canvas Tab";
 localizedStrings["Enable Layers Tab"] = "Enable Layers Tab";
 localizedStrings["Enable Program"] = "Enable Program";
 localizedStrings["Enable all breakpoints (%s)"] = "Enable all breakpoints (%s)";
@@ -605,6 +606,7 @@ localizedStrings["No Response Headers"] = "No Response Headers";
 localizedStrings["No Results Found"] = "No Results Found";
 localizedStrings["No Search Results"] = "No Search Results";
 localizedStrings["No Watch Expressions"] = "No Watch Expressions";
+localizedStrings["No canvas contexts found"] = "No canvas contexts found";
 localizedStrings["No matching ARIA role"] = "No matching ARIA role";
 localizedStrings["No preview available"] = "No preview available";
 localizedStrings["No request headers"] = "No request headers";
@@ -703,6 +705,7 @@ localizedStrings["Recording error: %s"] = "Recording error: %s";
 localizedStrings["Reference Issue"] = "Reference Issue";
 localizedStrings["Reflection"] = "Reflection";
 localizedStrings["Refresh"] = "Refresh";
+localizedStrings["Refresh all"] = "Refresh all";
 localizedStrings["Refresh watch expressions"] = "Refresh watch expressions";
 localizedStrings["Region announced in its entirety."] = "Region announced in its entirety.";
 localizedStrings["Regular Expression"] = "Regular Expression";
@@ -723,7 +726,6 @@ 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";
@@ -823,6 +825,7 @@ localizedStrings["Show page resources"] = "Show page resources";
 localizedStrings["Show shadow DOM nodes"] = "Show shadow DOM nodes";
 localizedStrings["Show the details sidebar (%s)"] = "Show the details sidebar (%s)";
 localizedStrings["Show the navigation sidebar (%s)"] = "Show the navigation sidebar (%s)";
+localizedStrings["Show transparency grid"] = "Show transparency grid";
 localizedStrings["Show type information"] = "Show type information";
 localizedStrings["Show warnings logged to the Console"] = "Show warnings logged to the Console";
 localizedStrings["Show:"] = "Show:";
@@ -854,6 +857,7 @@ localizedStrings["Stalled"] = "Stalled";
 localizedStrings["Start Time"] = "Start Time";
 localizedStrings["Start element selection (%s)"] = "Start element selection (%s)";
 localizedStrings["Start recording (%s)\nCreate new recording (%s)"] = "Start recording (%s)\nCreate new recording (%s)";
+localizedStrings["Start recording canvas actions. Shift-click to record a single frame."] = "Start recording canvas actions. Shift-click to record a single frame.";
 localizedStrings["State"] = "State";
 localizedStrings["Status"] = "Status";
 localizedStrings["Step"] = "Step";
@@ -865,6 +869,7 @@ localizedStrings["Stop Recording"] = "Stop Recording";
 localizedStrings["Stop element selection (%s)"] = "Stop element selection (%s)";
 localizedStrings["Stop recording"] = "Stop recording";
 localizedStrings["Stop recording (%s)"] = "Stop recording (%s)";
+localizedStrings["Stop recording canvas actions"] = "Stop recording canvas actions";
 localizedStrings["Storage"] = "Storage";
 localizedStrings["Stroke"] = "Stroke";
 localizedStrings["Style"] = "Style";
index 88e9de7..2c87a55 100644 (file)
@@ -427,6 +427,7 @@ WI.contentLoaded = function()
     // These tabs are always available for selecting, modulo isTabAllowed().
     // Other tabs may be engineering-only or toggled at runtime if incomplete.
     let productionTabClasses = [
+        WI.CanvasTabContentView,
         WI.ConsoleTabContentView,
         WI.DebuggerTabContentView,
         WI.ElementsTabContentView,
@@ -1106,6 +1107,9 @@ WI.tabContentViewClassForRepresentedObject = function(representedObject)
         representedObject instanceof WI.IndexedDatabaseObjectStoreIndex)
         return WI.ResourcesTabContentView;
 
+    if (representedObject instanceof WI.CanvasCollection)
+        return WI.CanvasTabContentView;
+
     if (representedObject instanceof WI.Recording)
         return WI.RecordingTabContentView;
 
index 571aa69..037c4ec 100644 (file)
@@ -128,6 +128,7 @@ WI.settings = {
     // Experimental
     experimentalShowCanvasContextsInResources: new WI.Setting("experimental-show-canvas-contexts-in-resources", false),
     experimentalSpreadsheetStyleEditor: new WI.Setting("experimental-spreadsheet-style-editor", false),
+    experimentalEnableCanvasTab: new WI.Setting("experimental-enable-canvas-tab", false),
     experimentalEnableLayersTab: new WI.Setting("experimental-enable-layers-tab", false),
     experimentalEnableNewNetworkTab: new WI.Setting("experimental-enable-new-network-tab", false),
 };
diff --git a/Source/WebInspectorUI/UserInterface/Images/Canvas.svg b/Source/WebInspectorUI/UserInterface/Images/Canvas.svg
new file mode 100644 (file)
index 0000000..dbd657d
--- /dev/null
@@ -0,0 +1,6 @@
+<?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">
+    <rect x="1.5" y="2.5" width="13" height="11" fill="none" stroke="currentColor"/>
+    <polygon points="11 8 9 11 6 7 3 11 3 12 13 12 13 11 11 8"/>
+</svg>
diff --git a/Source/WebInspectorUI/UserInterface/Images/CanvasOverview.svg b/Source/WebInspectorUI/UserInterface/Images/CanvasOverview.svg
new file mode 100644 (file)
index 0000000..76b72d3
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright © 2017 Apple Inc. All rights reserved. -->
+<svg xmlns="http://www.w3.org/2000/svg" id="root" version="1.1" viewBox="0 0 16 16">
+    <rect x="0.5" y="2.5" width="13" height="10" fill="none" stroke="currentColor"/>
+    <polyline points="2 14.5 15.5 14.5 15.5 4" fill="none" stroke="currentColor"/>
+    <polygon points="10 7 8 10 5 6 2 10 2 11 12 11 12 10 10 7"/>
+</svg>
index 77a70ae..96a16f8 100644 (file)
@@ -47,6 +47,8 @@
     <link rel="stylesheet" href="Views/CallFrameView.css">
     <link rel="stylesheet" href="Views/CanvasContentView.css">
     <link rel="stylesheet" href="Views/CanvasDetailsSidebarPanel.css">
+    <link rel="stylesheet" href="Views/CanvasOverviewContentView.css">
+    <link rel="stylesheet" href="Views/CanvasTabContentView.css">
     <link rel="stylesheet" href="Views/ChartDetailsSectionRow.css">
     <link rel="stylesheet" href="Views/CircleChart.css">
     <link rel="stylesheet" href="Views/ClusterContentView.css">
     <script src="Models/Collection.js"></script>
     <script src="Models/CollectionEntry.js"></script>
     <script src="Models/CollectionEntryPreview.js"></script>
+    <script src="Models/CollectionTypes.js"></script>
     <script src="Models/Color.js"></script>
     <script src="Models/ConsoleCommandResultMessage.js"></script>
     <script src="Models/CookieStorageObject.js"></script>
     <script src="Views/TimelineRecordTreeElement.js"></script>
     <script src="Views/TimelineTreeElement.js"></script>
 
+    <script src="Views/CanvasTabContentView.js"></script>
     <script src="Views/ConsoleTabContentView.js"></script>
     <script src="Views/DebuggerTabContentView.js"></script>
     <script src="Views/ElementsTabContentView.js"></script>
     <script src="Views/CallFrameView.js"></script>
     <script src="Views/CanvasContentView.js"></script>
     <script src="Views/CanvasDetailsSidebarPanel.js"></script>
+    <script src="Views/CanvasOverviewContentView.js"></script>
     <script src="Views/CanvasTreeElement.js"></script>
     <script src="Views/ChartDetailsSectionRow.js"></script>
     <script src="Views/CircleChart.js"></script>
index 33edcc8..d98ff47 100644 (file)
@@ -45,6 +45,8 @@ WI.Canvas = class Canvas extends WI.Object
         this._shaderProgramCollection = new WI.Collection(WI.Collection.TypeVerifier.ShaderProgram);
 
         this._nextShaderProgramDisplayNumber = 1;
+
+        this._requestNodePromise = null;
     }
 
     // Static
@@ -144,36 +146,29 @@ WI.Canvas = class Canvas extends WI.Object
         return WI.UIString("Canvas %d").format(this._uniqueDisplayNameNumber);
     }
 
-    requestNode(callback)
+    requestNode()
     {
-        if (this._domNode) {
-            callback(this._domNode);
-            return;
+        if (!this._requestNodePromise) {
+            this._requestNodePromise = new Promise((resolve, reject) => {
+                WI.domTreeManager.ensureDocument();
+
+                CanvasAgent.requestNode(this._identifier).then((result) => {
+                    this._domNode = WI.domTreeManager.nodeForId(result.nodeId);
+                    if (!this._domNode) {
+                        reject(`No DOM node for identifier: ${result.nodeId}.`);
+                        return;
+                    }
+                    resolve(this._domNode);
+                }).catch(reject);
+            });
         }
 
-        WI.domTreeManager.ensureDocument();
-
-        CanvasAgent.requestNode(this._identifier, (error, nodeId) => {
-            if (error) {
-                callback(null);
-                return;
-            }
-
-            this._domNode = WI.domTreeManager.nodeForId(nodeId);
-            callback(this._domNode);
-        });
+        return this._requestNodePromise;
     }
 
-    requestContent(callback)
+    requestContent()
     {
-        CanvasAgent.requestContent(this._identifier, (error, content) => {
-            if (error) {
-                callback(null);
-                return;
-            }
-
-            callback(content);
-        });
+        return CanvasAgent.requestContent(this._identifier).then((result) => result.content).catch((error) => console.error(error));
     }
 
     requestCSSCanvasClientNodes(callback)
@@ -198,11 +193,62 @@ WI.Canvas = class Canvas extends WI.Object
 
             clientNodeIds = Array.isArray(clientNodeIds) ? clientNodeIds : [];
             this._cssCanvasClientNodes = clientNodeIds.map((clientNodeId) => WI.domTreeManager.nodeForId(clientNodeId));
-
             callback(this._cssCanvasClientNodes);
         });
     }
 
+    requestSize()
+    {
+        function calculateSize(domNode) {
+            function getAttributeValue(name) {
+                let value = Number(domNode.getAttribute(name));
+                if (!Number.isInteger(value) || value < 0)
+                    return NaN;
+                return value;
+            }
+
+            return {
+                width: getAttributeValue("width"),
+                height: getAttributeValue("height")
+            };
+        }
+
+        function getPropertyValue(remoteObject, name) {
+            return new Promise((resolve, reject) => {
+                remoteObject.getProperty(name, (error, result) => {
+                    if (error) {
+                        reject(error);
+                        return;
+                    }
+                    resolve(result);
+                });
+            });
+        }
+
+        return this.requestNode().then((domNode) => {
+            let size = calculateSize(domNode);
+            if (!isNaN(size.width) && !isNaN(size.height))
+                return size;
+
+            // Since the "width" and "height" properties of canvas elements are more than just
+            // attributes, we need to invoke the getter for each to get the actual value.
+            //  - https://html.spec.whatwg.org/multipage/canvas.html#attr-canvas-width
+            //  - https://html.spec.whatwg.org/multipage/canvas.html#attr-canvas-height
+            let remoteObject = null;
+            return WI.RemoteObject.resolveNode(domNode).then((object) => {
+                remoteObject = object;
+                return Promise.all([getPropertyValue(object, "width"), getPropertyValue(object, "height")]);
+            }).then((values) => {
+                let width = values[0].value;
+                let height = values[1].value;
+                values[0].release();
+                values[1].release();
+                remoteObject.release();
+                return {width, height};
+            });
+        });
+    }
+
     saveIdentityToCookie(cookie)
     {
         cookie[WI.Canvas.FrameURLCookieKey] = this._frame.url.hash;
diff --git a/Source/WebInspectorUI/UserInterface/Models/CollectionTypes.js b/Source/WebInspectorUI/UserInterface/Models/CollectionTypes.js
new file mode 100644 (file)
index 0000000..da494a3
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+
+WI.CanvasCollection = class CanvasCollection extends WI.Collection
+{
+    constructor(canvases)
+    {
+        super((item) => item instanceof WI.Canvas);
+
+        for (let canvas of canvases)
+            this.add(canvas);
+    }
+};
index 13e672c..e7b9be4 100644 (file)
  * THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-.content-view.canvas {
+.content-view.canvas:not(.tab) {
     background-color: hsl(0, 0%, 90%);
 }
 
-.content-view.canvas > .preview {
+.content-view.canvas:not(.tab) > .preview {
     display: flex;
     justify-content: center;
     align-items: center;
     padding: 15px;
 }
 
-.content-view.canvas > .preview > img {
+.content-view.canvas:not(.tab) > .preview > img {
     max-width: 100%;
     max-height: 100%;
 }
 
+.content-view.canvas:not(.tab) > :matches(header, footer) {
+    display: none;
+}
+
 .navigation-bar > .item.canvas-record.disabled {
     filter: grayscale();
     opacity: 0.5;
index 9ff1b90..f9222ad 100644 (file)
@@ -36,18 +36,23 @@ WI.CanvasContentView = class CanvasContentView extends WI.ContentView
         this._previewContainerElement = null;
         this._previewImageElement = null;
         this._errorElement = null;
-
-        if (representedObject.contextType === WI.Canvas.ContextType.Canvas2D || representedObject.contextType === WI.Canvas.ContextType.WebGL) {
-            const toolTip = WI.UIString("Request recording of actions. Shift-click to record a single frame.");
-            const altToolTip = WI.UIString("Cancel recording");
-            this._recordButtonNavigationItem = new WI.ToggleButtonNavigationItem("canvas-record", toolTip, altToolTip, "Images/Record.svg", "Images/Stop.svg", 13, 13);
+        this._memoryCostElement = null;
+        this._pendingContent = null;
+        this._pixelSize = null;
+        this._pixelSizeElement = null;
+        this._canvasNode = null;
+
+        if (this.representedObject.contextType === WI.Canvas.ContextType.Canvas2D || this.representedObject.contextType === WI.Canvas.ContextType.WebGL) {
+            const toolTip = WI.UIString("Start recording canvas actions. Shift-click to record a single frame.");
+            const altToolTip = WI.UIString("Stop recording canvas actions");
+            this._recordButtonNavigationItem = new WI.ToggleButtonNavigationItem("record-start-stop", toolTip, altToolTip, "Images/Record.svg", "Images/Stop.svg", 13, 13);
             this._recordButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.High;
             this._recordButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._toggleRecording, this);
         }
 
-        this._refreshButtonNavigationItem = new WI.ButtonNavigationItem("canvas-refresh", WI.UIString("Refresh"), "Images/ReloadFull.svg", 13, 13);
+        this._refreshButtonNavigationItem = new WI.ButtonNavigationItem("refresh", WI.UIString("Refresh"), "Images/ReloadFull.svg", 13, 13);
         this._refreshButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
-        this._refreshButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._showPreview, this);
+        this._refreshButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this.refresh, this);
 
         this._showGridButtonNavigationItem = new WI.ActivateButtonNavigationItem("show-grid", WI.UIString("Show Grid"), WI.UIString("Hide Grid"), "Images/NavigationItemCheckers.svg", 13, 13);
         this._showGridButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._showGridButtonClicked, this);
@@ -55,7 +60,7 @@ WI.CanvasContentView = class CanvasContentView extends WI.ContentView
         this._showGridButtonNavigationItem.activated = !!WI.settings.showImageGrid.value;
     }
 
-    // Protected
+    // Public
 
     get navigationItems()
     {
@@ -65,40 +70,146 @@ WI.CanvasContentView = class CanvasContentView extends WI.ContentView
         return navigationItems;
     }
 
+    refresh()
+    {
+        this._pendingContent = null;
+
+        this.representedObject.requestContent().then((content) => {
+            this._pendingContent = content;
+            this.needsLayout();
+        })
+        .catch(() => {
+            this._showError();
+        });
+    }
+
+    // Protected
+
     initialLayout()
     {
         super.initialLayout();
 
-        WI.canvasManager.addEventListener(WI.CanvasManager.Event.RecordingStarted, this._recordingStarted, this);
-        WI.canvasManager.addEventListener(WI.CanvasManager.Event.RecordingStopped, this._recordingStopped, this);
+        let header = this.element.appendChild(document.createElement("header"));
+        let titles = header.appendChild(document.createElement("div"));
+        titles.className = "titles";
+
+        let title = titles.appendChild(document.createElement("span"));
+        title.className = "title";
+        title.textContent = this.representedObject.displayName;
+
+        let subtitle = titles.appendChild(document.createElement("span"));
+        subtitle.className = "subtitle";
+        subtitle.textContent = WI.Canvas.displayNameForContextType(this.representedObject.contextType);
+
+        let navigationBar = new WI.NavigationBar;
+        if (this._recordButtonNavigationItem) {
+            navigationBar.addNavigationItem(this._recordButtonNavigationItem);
+            navigationBar.addNavigationItem(new WI.DividerNavigationItem);
+        }
+        navigationBar.addNavigationItem(this._refreshButtonNavigationItem);
+
+        header.append(navigationBar.element);
+
+        this._previewContainerElement = this.element.appendChild(document.createElement("div"));
+        this._previewContainerElement.className = "preview";
+
+        let footer = this.element.appendChild(document.createElement("footer"));
+        let metrics = footer.appendChild(document.createElement("div"));
+        this._pixelSizeElement = metrics.appendChild(document.createElement("span"));
+        this._pixelSizeElement.className = "pixel-size";
+        this._memoryCostElement = metrics.appendChild(document.createElement("span"));
+        this._memoryCostElement.className = "memory-cost";
+    }
+
+    layout()
+    {
+        super.layout();
+
+        if (!this._pendingContent)
+            return;
+
+        if (this._errorElement) {
+            this._errorElement.remove();
+            this._errorElement = null;
+        }
+
+        if (!this._previewImageElement) {
+            this._previewImageElement = document.createElement("img");
+            this._previewImageElement.addEventListener("error", this._showError.bind(this));
+        }
+
+        this._previewImageElement.src = this._pendingContent;
+        this._pendingContent = null;
+
+        if (!this._previewImageElement.parentNode)
+            this._previewContainerElement.appendChild(this._previewImageElement);
+
+        this._updateImageGrid();
+
+        this._refreshPixelSize();
+        this._updateMemoryCost();
     }
 
     shown()
     {
         super.shown();
 
-        this._showPreview();
-        this._updateRecordNavigationItem();
+        this.refresh();
 
-        WI.settings.showImageGrid.addEventListener(WI.Setting.Event.Changed, this._updateImageGrid, this);
+        this._updateRecordNavigationItem();
     }
 
-    hidden()
+    attached()
     {
-        WI.settings.showImageGrid.removeEventListener(WI.Setting.Event.Changed, this._updateImageGrid, this);
+        super.attached();
+
+        this.representedObject.addEventListener(WI.Canvas.Event.MemoryChanged, this._updateMemoryCost, this);
 
-        super.hidden();
+        this.representedObject.requestNode().then((node) => {
+            console.assert(!this._canvasNode || this._canvasNode === node);
+            if (this._canvasNode === node)
+                return;
+
+            this._canvasNode = node;
+            this._canvasNode.addEventListener(WI.DOMNode.Event.AttributeModified, this._refreshPixelSize, this);
+            this._canvasNode.addEventListener(WI.DOMNode.Event.AttributeRemoved, this._refreshPixelSize, this);
+        });
+
+        WI.canvasManager.addEventListener(WI.CanvasManager.Event.RecordingStarted, this._recordingStarted, this);
+        WI.canvasManager.addEventListener(WI.CanvasManager.Event.RecordingStopped, this._recordingStopped, this);
+
+        WI.settings.showImageGrid.addEventListener(WI.Setting.Event.Changed, this._updateImageGrid, this);
     }
 
-    closed()
+    detached()
     {
+        this.representedObject.removeEventListener(null, null, this);
+
+        if (this._canvasNode) {
+            this._canvasNode.removeEventListener(null, null, this);
+            this._canvasNode = null;
+        }
+
         WI.canvasManager.removeEventListener(null, null, this);
+        WI.settings.showImageGrid.removeEventListener(null, null, this);
 
-        super.closed();
+        super.detached();
     }
 
     // Private
 
+    _showError()
+    {
+        console.assert(!this._errorElement, "Error element already exists.");
+
+        if (this._previewImageElement)
+            this._previewImageElement.remove();
+
+        const isError = true;
+        this._errorElement = WI.createMessageTextView(WI.UIString("No Preview Available"), isError);
+        this._previewContainerElement.appendChild(this._errorElement);
+    }
+
     _toggleRecording(event)
     {
         if (this.representedObject.isRecording)
@@ -129,69 +240,67 @@ WI.CanvasContentView = class CanvasContentView extends WI.ContentView
         WI.showRepresentedObject(event.data.recording);
     }
 
-    _showPreview()
+    _refreshPixelSize()
     {
-        let showError = () => {
-            if (this._previewContainerElement)
-                this._previewContainerElement.remove();
-
-            if (!this._errorElement) {
-                const isError = true;
-                this._errorElement = WI.createMessageTextView(WI.UIString("No Preview Available"), isError);
-            }
-
-            this.element.appendChild(this._errorElement);
-        };
-
-        if (!this._previewContainerElement) {
-            this._previewContainerElement = this.element.appendChild(document.createElement("div"));
-            this._previewContainerElement.classList.add("preview");
-        }
+        this._pixelSize = null;
 
-        this.representedObject.requestContent((content) => {
-            if (!content) {
-                showError();
-                return;
-            }
-
-            if (this._errorElement)
-                this._errorElement.remove();
+        this.representedObject.requestSize().then((size) => {
+            this._pixelSize = size;
+            this._updatePixelSize();
+        }).catch((error) => {
+            this._updatePixelSize();
+        });
+    }
 
-            if (!this._previewImageElement) {
-                this._previewImageElement = document.createElement("img");
-                this._previewImageElement.addEventListener("error", showError);
-            }
+    _showGridButtonClicked()
+    {
+        WI.settings.showImageGrid.value = !this._showGridButtonNavigationItem.activated;
+    }
 
-            this._previewImageElement.src = content;
-            this._previewContainerElement.appendChild(this._previewImageElement);
+    _updateImageGrid()
+    {
+        let activated = WI.settings.showImageGrid.value;
+        this._showGridButtonNavigationItem.activated = activated;
 
-            this._updateImageGrid();
-        });
+        if (this._previewImageElement)
+            this._previewImageElement.classList.toggle("show-grid", activated);
     }
 
-    _updateRecordNavigationItem()
+    _updateMemoryCost()
     {
-        if (!this._recordButtonNavigationItem)
+        if (!this._memoryCostElement)
             return;
 
-        this._recordButtonNavigationItem.enabled = this.representedObject.isRecording || !WI.canvasManager.recordingCanvas;
-        this._recordButtonNavigationItem.toggled = this.representedObject.isRecording;
+        let memoryCost = this.representedObject.memoryCost;
+        if (isNaN(memoryCost))
+            this._memoryCostElement.textContent = emDash;
+        else {
+            const higherResolution = false;
+            let bytesString = Number.bytesToString(memoryCost, higherResolution);
+            this._memoryCostElement.textContent = `(${bytesString})`;
+        }
     }
 
-    _updateImageGrid()
+    _updatePixelSize()
     {
-        if (!this._previewImageElement)
+        if (!this._pixelSizeElement)
             return;
 
-        let activated = WI.settings.showImageGrid.value;
-        this._showGridButtonNavigationItem.activated = activated;
-        this._previewImageElement.classList.toggle("show-grid", activated);
+        if (this._pixelSize)
+            this._pixelSizeElement.textContent = `${this._pixelSize.width} ${multiplicationSign} ${this._pixelSize.height}`;
+        else
+            this._pixelSizeElement.textContent = emDash;
     }
 
-    _showGridButtonClicked(event)
+    _updateRecordNavigationItem()
     {
-        WI.settings.showImageGrid.value = !this._showGridButtonNavigationItem.activated;
+        if (!this._recordButtonNavigationItem)
+            return;
 
-        this._updateImageGrid();
+        let isRecording = this.representedObject.isRecording;
+        this._recordButtonNavigationItem.enabled = isRecording || !WI.canvasManager.recordingCanvas;
+        this._recordButtonNavigationItem.toggled = isRecording;
+
+        this.element.classList.toggle("is-recording", isRecording);
     }
 };
index 84b5719..3f1287e 100644 (file)
@@ -161,10 +161,7 @@ WI.CanvasDetailsSidebarPanel = class CanvasDetailsSidebarPanel extends WI.Detail
         this._heightRow.value = emDash;
         this._datachedRow.value = null;
 
-        this._canvas.requestNode((node) => {
-            if (!node)
-                return;
-
+        this._canvas.requestNode().then((node) => {
             if (node !== this._node) {
                 if (this._node) {
                     this._node.removeEventListener(WI.DOMNode.Event.AttributeModified, this._refreshSourceSection, this);
diff --git a/Source/WebInspectorUI/UserInterface/Views/CanvasOverviewContentView.css b/Source/WebInspectorUI/UserInterface/Views/CanvasOverviewContentView.css
new file mode 100644 (file)
index 0000000..bf1c14c
--- /dev/null
@@ -0,0 +1,139 @@
+/*
+ * 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.canvas-overview {
+    justify-content: center;
+    align-items: flex-start;
+    background-color: hsl(0, 0%, 90%);
+}
+
+.content-view.canvas-overview .content-view.canvas {
+    flex-grow: 0;
+    margin: 10px;
+    width: 400px;
+    background-color: white;
+    border: solid 1px var(--border-color);
+}
+
+.content-view.canvas-overview .content-view.canvas.selected:not(.is-recording) {
+    border-color: var(--selected-background-color);
+}
+
+.content-view.canvas-overview .content-view.canvas > :matches(header, footer) {
+    display: flex;
+    flex-direction: row;
+    flex-shrink: 0;
+    justify-content: space-between;
+    align-items: center;
+    padding: 0 6px;
+    height: var(--navigation-bar-height);
+}
+
+.content-view.canvas-overview .content-view.canvas > header {
+    font-size: 14px;
+}
+
+.content-view.canvas-overview .content-view.canvas.is-recording > header {
+    background-color: red;
+}
+
+.content-view.canvas-overview .content-view.canvas > header > .titles,
+.content-view.canvas-overview .content-view.canvas > footer > .size {
+    white-space: nowrap;
+}
+
+.content-view.canvas-overview .content-view.canvas > header > .titles > .title {
+    color: var(--text-color-gray-dark);
+}
+
+.content-view.canvas-overview .content-view.canvas > header > .titles > .subtitle,
+.content-view.canvas-overview .content-view.canvas > footer .memory-cost {
+    color: var(--text-color-gray-medium);
+}
+
+.content-view.canvas-overview .content-view.canvas > header .subtitle::before {
+    content: " \2014 ";
+}
+
+.content-view.canvas-overview .content-view.canvas.is-recording > header > .titles > .title {
+    color: white;
+}
+
+.content-view.canvas-overview .content-view.canvas.is-recording > header > .titles > .subtitle {
+    color: var(--selected-secondary-text-color);
+}
+
+.content-view.canvas-overview .content-view.canvas.is-recording > header > .navigation-bar > .item {
+    filter: brightness(0) invert();
+}
+
+.content-view.canvas-overview .content-view.canvas > header > .navigation-bar {
+    align-items: initial;
+    border: none;
+}
+
+.content-view.canvas-overview .content-view.canvas:not(:hover, .is-recording) > header > .navigation-bar {
+    visibility: hidden;
+}
+
+.content-view.canvas-overview .content-view.canvas:not(.is-recording) > header > .navigation-bar > .item.record-start-stop.disabled {
+    filter: grayscale();
+    opacity: 0.5;
+}
+
+.content-view.canvas-overview .content-view.canvas:not(.is-recording) > header > .navigation-bar > .item.record-start-stop {
+    /* Workaround for background image clipping issue on non-retina machines. See http://webkit.org/b/147346. */
+    filter: brightness(100%);
+}
+
+.content-view.canvas-overview .content-view.canvas:not(.is-recording) > header > .navigation-bar > .item.record-start-stop:not(.disabled):hover {
+    filter: brightness(95%);
+}
+
+.content-view.canvas-overview .content-view.canvas:not(.is-recording) > header > .navigation-bar > .item.record-start-stop:not(.disabled):active {
+    filter: brightness(80%);
+}
+
+.content-view.canvas-overview .content-view.canvas > .preview {
+    height: 280px;
+}
+
+.content-view.canvas-overview .content-view.canvas > .preview > img {
+    border-radius: 4px;
+    box-shadow: 1px 2px 6px rgba(0, 0, 0, 0.58);
+}
+
+.content-view.canvas-overview .content-view.canvas > .preview > .message-text-view {
+    position: static;
+}
+
+.content-view.canvas-overview .content-view.canvas > footer {
+    /* FIXME: this can be removed once <https://webkit.org/b/177606> is complete.*/
+    justify-content: flex-end;
+}
+
+.content-view.canvas-overview .content-view.canvas > footer .memory-cost {
+    -webkit-padding-start: 4px;
+}
diff --git a/Source/WebInspectorUI/UserInterface/Views/CanvasOverviewContentView.js b/Source/WebInspectorUI/UserInterface/Views/CanvasOverviewContentView.js
new file mode 100644 (file)
index 0000000..728c409
--- /dev/null
@@ -0,0 +1,204 @@
+/*
+ * 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.
+ */
+
+WI.CanvasOverviewContentView = class CanvasOverviewContentView extends WI.CollectionContentView
+{
+    constructor(representedObject)
+    {
+        console.assert(representedObject instanceof WI.CanvasCollection);
+
+        super(representedObject, WI.CanvasContentView, WI.UIString("No canvas contexts found"));
+
+        this.element.classList.add("canvas-overview");
+
+        this._canvasTreeOutline = new WI.TreeOutline;
+
+        this._refreshButtonNavigationItem = new WI.ButtonNavigationItem("refresh-all", WI.UIString("Refresh all"), "Images/ReloadFull.svg", 13, 13);
+        this._refreshButtonNavigationItem.disabled = true;
+        this._refreshButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._refreshPreviews, this);
+
+        this._showGridButtonNavigationItem = new WI.ActivateButtonNavigationItem("show-grid", WI.UIString("Show transparency grid"), WI.UIString("Hide Grid"), "Images/NavigationItemCheckers.svg", 13, 13);
+        this._showGridButtonNavigationItem.activated = !!WI.settings.showImageGrid.value;
+        this._showGridButtonNavigationItem.disabled = true;
+        this._showGridButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._showGridButtonClicked, this);
+
+        this._selectedCanvasPathComponent = null;
+
+        this.selectionEnabled = true;
+    }
+
+    // Public
+
+    get navigationItems()
+    {
+        return [this._refreshButtonNavigationItem, this._showGridButtonNavigationItem];
+    }
+
+    get selectionPathComponents()
+    {
+        let components = [];
+        let canvas = this.supplementalRepresentedObjects[0];
+        if (canvas) {
+            let treeElement = this._canvasTreeOutline.findTreeElement(canvas);
+            console.assert(treeElement);
+            if (treeElement) {
+                let pathComponent = new WI.GeneralTreeElementPathComponent(treeElement, canvas);
+                pathComponent.addEventListener(WI.HierarchicalPathComponent.Event.SiblingWasSelected, this._selectedPathComponentChanged, this);
+                components.push(pathComponent);
+            }
+        }
+
+        return components;
+    }
+
+    hidden()
+    {
+        WI.domTreeManager.hideDOMNodeHighlight();
+
+        super.hidden();
+    }
+
+    // Protected
+
+    contentViewAdded(contentView)
+    {
+        let canvas = contentView.representedObject;
+        let treeElement = this._canvasTreeOutline.findTreeElement(canvas);
+        console.assert(!treeElement, "Already added tree element for canvas.", canvas);
+        if (treeElement)
+            return;
+
+        treeElement = new WI.CanvasTreeElement(canvas);
+        this._canvasTreeOutline.appendChild(treeElement);
+
+        contentView.element.addEventListener("mouseenter", this._contentViewMouseEnter);
+        contentView.element.addEventListener("mouseleave", this._contentViewMouseLeave);
+    }
+
+    contentViewRemoved(contentView)
+    {
+        let canvas = contentView.representedObject;
+        let treeElement = this._canvasTreeOutline.findTreeElement(canvas);
+        console.assert(treeElement, "Missing tree element for canvas.", canvas);
+        if (!treeElement)
+            return;
+
+        this._canvasTreeOutline.removeChild(treeElement);
+
+        contentView.element.removeEventListener("mouseenter", this._contentViewMouseEnter);
+        contentView.element.removeEventListener("mouseleave", this._contentViewMouseLeave);
+    }
+
+    attached()
+    {
+        super.attached();
+
+        WI.settings.showImageGrid.addEventListener(WI.Setting.Event.Changed, this._updateShowImageGrid, this);
+
+        this.addEventListener(WI.ContentView.Event.SupplementalRepresentedObjectsDidChange, this._supplementalRepresentedObjectsDidChange, this);
+    }
+
+    detached()
+    {
+        WI.settings.showImageGrid.removeEventListener(null, null, this);
+
+        this.removeEventListener(null, null, this);
+
+        super.detached();
+    }
+
+    // Private
+
+    _refreshPreviews()
+    {
+        for (let canvasContentView of this.subviews)
+            canvasContentView.refresh();
+    }
+
+    _selectedPathComponentChanged(event)
+    {
+        let pathComponent = event.data.pathComponent;
+        if (pathComponent.representedObject instanceof WI.Canvas)
+            this.setSelectedItem(pathComponent.representedObject);
+    }
+
+    _showGridButtonClicked(event)
+    {
+        WI.settings.showImageGrid.value = !this._showGridButtonNavigationItem.activated;
+    }
+
+    _supplementalRepresentedObjectsDidChange()
+    {
+        function createCanvasPathComponent(canvas) {
+            return new WI.HierarchicalPathComponent(canvas.displayName, "canvas", canvas);
+        }
+
+        let currentCanvas = this.supplementalRepresentedObjects[0];
+        if (currentCanvas)
+            this._selectedCanvasPathComponent = createCanvasPathComponent(currentCanvas);
+        else
+            this._selectedCanvasPathComponent = null;
+
+        this.dispatchEventToListeners(WI.ContentView.Event.SelectionPathComponentsDidChange);
+    }
+
+    _updateNavigationItems()
+    {
+        let disabled = !this.representedObject.items.size;
+        this._refreshButtonNavigationItem.disabled = disabled;
+        this._showGridButtonNavigationItem.disabled = disabled;
+    }
+
+    _updateShowImageGrid()
+    {
+        this._showGridButtonNavigationItem.activated = !!WI.settings.showImageGrid.value;
+    }
+
+    _contentViewMouseEnter(event)
+    {
+        let contentView = WI.View.fromElement(event.target);
+        if (!(contentView instanceof WI.CanvasContentView))
+            return;
+
+        let canvas = contentView.representedObject;
+        if (canvas.cssCanvasName) {
+            canvas.requestCSSCanvasClientNodes((cssCanvasClientNodes) => {
+                WI.domTreeManager.highlightDOMNodeList(cssCanvasClientNodes.map((node) => node.id));
+            });
+            return;
+        }
+
+        canvas.requestNode().then((node) => {
+            if (!node || !node.ownerDocument)
+                return;
+            WI.domTreeManager.highlightDOMNode(node.id);
+        });
+    }
+
+    _contentViewMouseLeave(event)
+    {
+        WI.domTreeManager.hideDOMNodeHighlight();
+    }
+};
diff --git a/Source/WebInspectorUI/UserInterface/Views/CanvasTabContentView.css b/Source/WebInspectorUI/UserInterface/Views/CanvasTabContentView.css
new file mode 100644 (file)
index 0000000..cb04513
--- /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.
+ */
+
+.content-view.tab.canvas .navigation-bar > .item > .hierarchical-path-component > .icon {
+    opacity: 0.7;
+}
+
+.content-view.tab.canvas .navigation-bar > .item > .canvas-overview > .icon {
+    content: url(../Images/CanvasOverview.svg);
+}
+
+/* FIXME: this can be removed once <https://webkit.org/b/177606> is complete. */
+.content-view.tab.canvas .navigation-bar > .item .canvas .icon {
+    content: url(../Images/Canvas.svg);
+}
diff --git a/Source/WebInspectorUI/UserInterface/Views/CanvasTabContentView.js b/Source/WebInspectorUI/UserInterface/Views/CanvasTabContentView.js
new file mode 100644 (file)
index 0000000..dd794b7
--- /dev/null
@@ -0,0 +1,161 @@
+/*
+ * 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.
+ */
+
+WI.CanvasTabContentView = class CanvasTabContentView extends WI.ContentBrowserTabContentView
+{
+    constructor(representedObject)
+    {
+        console.assert(!representedObject || representedObject instanceof WI.Canvas);
+
+        let {image, title} = WI.CanvasTabContentView.tabInfo();
+        let tabBarItem = new WI.GeneralTabBarItem(image, title);
+
+        const navigationSidebarPanelConstructor = null;
+        const detailsSidebarPanelConstructors = [WI.RecordingStateDetailsSidebarPanel, WI.RecordingTraceDetailsSidebarPanel, WI.CanvasDetailsSidebarPanel];
+        const disableBackForward = true;
+        super("canvas", ["canvas"], tabBarItem, navigationSidebarPanelConstructor, detailsSidebarPanelConstructors, disableBackForward);
+
+        this._overviewPathComponent = new WI.HierarchicalPathComponent(WI.UIString("Canvas Overview"), "canvas-overview");
+        this._overviewPathComponent.addEventListener(WI.HierarchicalPathComponent.Event.Clicked, this._overviewPathComponentClicked, this);
+
+        this._canvasCollection = null;
+        this._canvasOverviewContentView = null;
+    }
+
+    static tabInfo()
+    {
+        return {
+            image: "Images/Canvas.svg",
+            title: WI.UIString("Canvas"),
+        };
+    }
+
+    static isTabAllowed()
+    {
+        // FIXME: remove experimental setting check once <https://webkit.org/b/175485> is complete.
+        return !!window.CanvasAgent && WI.settings.experimentalEnableCanvasTab.value;
+    }
+
+    // Public
+
+    get type()
+    {
+        return WI.CanvasTabContentView.Type;
+    }
+
+    get supportsSplitContentBrowser()
+    {
+        return true;
+    }
+
+    canShowRepresentedObject(representedObject)
+    {
+        return representedObject instanceof WI.CanvasCollection;
+    }
+
+    shown()
+    {
+        super.shown();
+
+        if (this.contentBrowser.currentContentView)
+            return;
+
+        this._canvasOverviewContentView = new WI.CanvasOverviewContentView(this._canvasCollection);
+        this.contentBrowser.showContentView(this._canvasOverviewContentView);
+    }
+
+    treeElementForRepresentedObject(representedObject)
+    {
+        if (!this._overviewTreeElement) {
+            const title = WI.UIString("Canvas Overview");
+            this._overviewTreeElement = new WI.GeneralTreeElement(["canvas-overview"], title, null, representedObject);
+        }
+
+        return this._overviewTreeElement;
+    }
+
+    restoreFromCookie(cookie)
+    {
+        // FIXME: implement once <https://webkit.org/b/177606> is complete.
+    }
+
+    saveStateToCookie(cookie)
+    {
+        // FIXME: implement once <https://webkit.org/b/177606> is complete.
+    }
+
+    // Protected
+
+    attached()
+    {
+        super.attached();
+
+        WI.canvasManager.addEventListener(WI.CanvasManager.Event.CanvasWasAdded, this._canvasAdded, this);
+        WI.canvasManager.addEventListener(WI.CanvasManager.Event.CanvasWasRemoved, this._canvasRemoved, this);
+        WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
+
+        this._canvasCollection = new WI.CanvasCollection(WI.canvasManager.canvases);
+    }
+
+    detached()
+    {
+        WI.canvasManager.removeEventListener(null, null, this);
+        WI.Frame.removeEventListener(null, null, this);
+
+        this._canvasCollection = null;
+
+        super.detached();
+    }
+
+    // Private
+
+    _canvasAdded(event)
+    {
+        let canvas = event.data.canvas;
+        this._canvasCollection.add(canvas);
+    }
+
+    _canvasRemoved(event)
+    {
+        let canvas = event.data.canvas;
+        this._canvasCollection.remove(canvas);
+    }
+
+    _overviewPathComponentClicked(event)
+    {
+        console.assert(this._canvasOverviewContentView);
+        this.contentBrowser.showContentView(this._canvasOverviewContentView);
+    }
+
+    _mainResourceDidChange(event)
+    {
+        if (!event.target.isMainFrame())
+            return;
+
+        this._canvasCollection.clear();
+    }
+};
+
+WI.CanvasTabContentView.Type = "canvas";
index f5506c5..3d06c8d 100644 (file)
@@ -33,7 +33,7 @@ WI.CollectionContentView = class CollectionContentView extends WI.ContentView
 
         this.element.classList.add("collection");
 
-        this._contentPlaceholder = new WI.TitleView(contentPlaceholderText || WI.CollectionContentView.titleForCollection(collection));
+        this._contentPlaceholderText = contentPlaceholderText || WI.CollectionContentView.titleForCollection(collection);
         this._contentViewConstructor = contentViewConstructor;
         this._contentViewMap = new Map;
         this._handleClickMap = new WeakMap;
@@ -86,6 +86,21 @@ WI.CollectionContentView = class CollectionContentView extends WI.ContentView
             this._selectItem(null);
     }
 
+    setSelectedItem(item)
+    {
+        console.assert(this._selectionEnabled, "Attempted to set selected item when selection is disabled.");
+        if (!this._selectionEnabled)
+            return;
+
+        let contentView = this._contentViewMap.get(item);
+        console.assert(contentView, "Missing contet view for item.", item);
+        if (!contentView)
+            return;
+
+        this._selectItem(item);
+        contentView.element.scrollIntoViewIfNeeded();
+    }
+
     // Protected
 
     addContentViewForItem(item)
@@ -98,8 +113,7 @@ WI.CollectionContentView = class CollectionContentView extends WI.ContentView
             return;
         }
 
-        if (this._contentPlaceholder.parentView)
-            this.removeSubview(this._contentPlaceholder);
+        this._hideContentPlaceholder();
 
         let contentView = new this._contentViewConstructor(item);
         console.assert(contentView instanceof WI.ContentView);
@@ -120,6 +134,8 @@ WI.CollectionContentView = class CollectionContentView extends WI.ContentView
 
         this.addSubview(contentView);
         this.contentViewAdded(contentView);
+
+        contentView.shown();
     }
 
     removeContentViewForItem(item)
@@ -132,10 +148,15 @@ WI.CollectionContentView = class CollectionContentView extends WI.ContentView
         if (!contentView)
             return;
 
+        if (this._selectedItem === item)
+            this._selectItem(null);
+
         this.removeSubview(contentView);
         this._contentViewMap.delete(item);
         this.contentViewRemoved(contentView);
 
+        contentView.hidden();
+
         contentView.removeEventListener(null, null, this);
 
         let handleClick = this._handleClickMap.get(item);
@@ -146,11 +167,8 @@ WI.CollectionContentView = class CollectionContentView extends WI.ContentView
             this._handleClickMap.delete(item);
         }
 
-        if (this._selectedItem === item)
-            this._selectItem(null);
-
         if (!this.subviews.length)
-            this.addSubview(this._contentPlaceholder);
+            this._showContentPlaceholder();
     }
 
     contentViewAdded(contentView)
@@ -167,7 +185,7 @@ WI.CollectionContentView = class CollectionContentView extends WI.ContentView
     {
         let items = this.representedObject.items;
         if (!items.size || !this._contentViewConstructor) {
-            this.addSubview(this._contentPlaceholder);
+            this._showContentPlaceholder();
             return;
         }
 
@@ -251,4 +269,21 @@ WI.CollectionContentView = class CollectionContentView extends WI.ContentView
 
         this.dispatchEventToListeners(WI.ContentView.Event.SupplementalRepresentedObjectsDidChange);
     }
+
+    _showContentPlaceholder()
+    {
+        if (!this._contentPlaceholder)
+            this._contentPlaceholder = new WI.TitleView(this._contentPlaceholderText);
+
+        if (!this._contentPlaceholder.parentView)
+            this.debounce(250).addSubview(this._contentPlaceholder);
+    }
+
+    _hideContentPlaceholder()
+    {
+        this.addSubview.cancelDebounce();
+
+        if (this._contentPlaceholder && this._contentPlaceholder.parentView)
+            this.removeSubview(this._contentPlaceholder);
+    }
 };
index fa3f517..17826aa 100644 (file)
@@ -240,7 +240,9 @@ WI.SettingsTabContentView = class SettingsTabContentView extends WI.TabContentVi
         let experimentalSettingsView = new WI.SettingsView("experimental", WI.UIString("Experimental"));
 
         if (window.CanvasAgent) {
-            experimentalSettingsView.addSetting(WI.UIString("Canvas:"), WI.settings.experimentalShowCanvasContextsInResources, WI.UIString("Show Contexts in Resources Tab"));
+            let canvasSettingsGroup = experimentalSettingsView.addGroup(WI.UIString("Canvas:"));
+            canvasSettingsGroup.addSetting(WI.settings.experimentalEnableCanvasTab, WI.UIString("Enable Canvas Tab"));
+            canvasSettingsGroup.addSetting(WI.settings.experimentalShowCanvasContextsInResources, WI.UIString("Show Contexts in Resources Tab"));
             experimentalSettingsView.addSeparator();
         }
 
@@ -296,6 +298,7 @@ WI.SettingsTabContentView = class SettingsTabContentView extends WI.TabContentVi
         listenForChange(WI.settings.experimentalSpreadsheetStyleEditor);
         listenForChange(WI.settings.experimentalEnableNewNetworkTab);
         listenForChange(WI.settings.experimentalEnableLayersTab);
+        listenForChange(WI.settings.experimentalEnableCanvasTab);
 
         this.addSettingsView(experimentalSettingsView);
     }
index caa9ad4..214f8cc 100644 (file)
@@ -61,6 +61,7 @@
 
     --sidebar-no-results-message-font-size: 16px;
 
+    --text-color-gray-dark: hsl(0, 0%, 20%);
     --text-color-gray-medium: hsl(0, 0%, 52%);
     --error-text-color: hsl(0, 86%, 47%);
 
@@ -123,4 +124,5 @@ body.window-inactive {
 
 body.window-inactive * {
     --border-color: hsl(0, 0%, 85%);
+    --border-color-dark: hsl(0, 0%, 72%);
 }
index 6e2ee5f..96b3c3f 100644 (file)
@@ -42,6 +42,16 @@ WI.View = class View extends WI.Object
 
     // Static
 
+    static fromElement(element)
+    {
+        if (!element || !(element instanceof HTMLElement))
+            return null;
+
+        if (element.__view instanceof WI.View)
+            return element.__view;
+        return null;
+    }
+
     static rootView()
     {
         if (!WI.View._rootView) {