Changeset 237808 in webkit


Ignore:
Timestamp:
Nov 5, 2018 9:34:48 AM (5 years ago)
Author:
Devin Rousso
Message:

Web Inspector: show save/restore stack for recorded 2D Canvases
https://bugs.webkit.org/show_bug.cgi?id=175283
<rdar://problem/34040756>

Reviewed by Matt Baker.

Source/WebInspectorUI:

Instead of using plain objects for holding the WI.Recording's state, use a model object
so that more data can be passed around. When visualizing the state, show any previously
saved states in a WI.DetailsSection underneath the current state. If there are no saved
states (meaning there is only the current state), don't use a WI.DetailsSection.

  • UserInterface/Models/RecordingState.js: Added.

(WI.RecordingState):
(WI.RecordingState.fromContext):
(WI.RecordingState.async swizzleInitialState):
(WI.RecordingState.prototype.get source):
(WI.RecordingState.prototype.has):
(WI.RecordingState.prototype.get return):
(WI.RecordingState.prototype.toJSON):
(WI.RecordingState.prototype.[Symbol.iterator]):

  • UserInterface/Models/Recording.js:

(WI.Recording.prototype.async _process):
(WI.Recording.prototype.async _swizzleState): Deleted.

  • UserInterface/Models/RecordingAction.js:

(WI.RecordingAction.prototype.process):
(WI.RecordingAction.deriveCurrentState): Deleted.

  • UserInterface/Views/RecordingContentView.js:

(WI.RecordingContentView.prototype._generateContentCanvas2D):

  • UserInterface/Views/RecordingStateDetailsSidebarPanel.js:

(WI.RecordingStateDetailsSidebarPanel):
(WI.RecordingStateDetailsSidebarPanel.prototype.get scrollElement):
(WI.RecordingStateDetailsSidebarPanel.prototype.sizeDidChange): Added.
(WI.RecordingStateDetailsSidebarPanel.prototype._generateDetailsCanvas2D):
(WI.RecordingStateDetailsSidebarPanel.prototype._generateDetailsCanvas2D.isColorProperty): Deleted.
(WI.RecordingStateDetailsSidebarPanel.prototype._generateDetailsCanvas2D.createInlineSwatch): Deleted.

  • UserInterface/Views/RecordingStateDetailsSidebarPanel.css:

(.sidebar > .panel.details.recording-state > .content .details-section > .header .source): Added.
(.sidebar > .panel.details.recording-state > .content .data-grid tr.modified): Added.
(.sidebar > .panel.details.recording-state > .content .data-grid tr:not(.selected).non-standard .name-column): Added.
(.sidebar > .panel.details.recording-state > .content .data-grid tr:not(.selected) .unavailable): Added.
(.sidebar > .panel.details.recording-state > .content .data-grid .inline-swatch): Added.
(.sidebar > .panel.details.recording-state > .content > .data-grid tr.modified): Deleted.
(.sidebar > .panel.details.recording-state > .content > .data-grid tr:not(.selected).non-standard): Deleted.
(.sidebar > .panel.details.recording-state > .content > .data-grid tr:not(.selected) .unavailable): Deleted.
(.sidebar > .panel.details.recording-state > .content > .data-grid .inline-swatch): Deleted.

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

LayoutTests:

  • inspector/canvas/recording-2d.html:
  • inspector/canvas/resources/recording-utilities.js:

(TestPage.registerInitializer.log):
(TestPage.registerInitializer.async logRecording):

Location:
trunk
Files:
1 added
12 edited

Legend:

Unmodified
Added
Removed
  • trunk/LayoutTests/ChangeLog

    r237804 r237808  
     12018-11-05  Devin Rousso  <drousso@apple.com>
     2
     3        Web Inspector: show save/restore stack for recorded 2D Canvases
     4        https://bugs.webkit.org/show_bug.cgi?id=175283
     5        <rdar://problem/34040756>
     6
     7        Reviewed by Matt Baker.
     8
     9        * inspector/canvas/recording-2d.html:
     10        * inspector/canvas/resources/recording-utilities.js:
     11        (TestPage.registerInitializer.log):
     12        (TestPage.registerInitializer.async logRecording):
     13
    1142018-11-05  Thibault Saunier  <tsaunier@igalia.com>
    215
  • trunk/LayoutTests/inspector/canvas/recording-2d.html

    r237777 r237808  
    520520            async function logStates(recording) {
    521521                async function compare(index, expected) {
    522                     let swizzledState = await recording._swizzleState(recording.initialState.states[index]);
    523                     InspectorTest.expectEqual(swizzledState["fillStyle"], expected, `State ${index} should match expected fillStyle value.`)
     522                    let state = await WI.RecordingState.swizzleInitialState(recording, recording.initialState.states[index]);
     523                    InspectorTest.expectEqual(state.get("fillStyle"), expected, `State ${index} should match expected fillStyle value.`)
    524524                }
    525525
  • trunk/LayoutTests/inspector/canvas/resources/recording-utilities.js

    r237670 r237808  
    11TestPage.registerInitializer(() => {
    22    function log(object, indent) {
    3         for (let key of Object.keys(object)) {
    4             let value = object[key];
     3        for (let [name, value] of object) {
    54            if (typeof value === "string")
    65                value = sanitizeURL(value);
    76            else if (Array.isArray(value) && value[0] instanceof DOMMatrix)
    87                value[0] = [value[0].a, value[0].b, value[0].c, value[0].d, value[0].e, value[0].f];
    9             InspectorTest.log(indent + key + ": " + JSON.stringify(value));
     8            InspectorTest.log(indent + name + ": " + JSON.stringify(value));
    109        }
    1110    }
     
    1514
    1615        InspectorTest.log("  attributes:");
    17         log(recording.initialState.attributes, "    ");
     16        log(Object.entries(recording.initialState.attributes), "    ");
    1817
    1918        let currentState = recording.initialState.states.lastValue;
    2019        if (currentState) {
    2120            InspectorTest.log("  current state:");
    22             let swizzledState = await recording._swizzleState(currentState);
    23             log(swizzledState, "    ");
     21            let state = await WI.RecordingState.swizzleInitialState(recording, currentState);
     22            log(state, "    ");
    2423        }
    2524
    2625        InspectorTest.log("  parameters:");
    27         log(recording.initialState.parameters, "    ");
     26        log(Object.entries(recording.initialState.parameters), "    ");
    2827
    2928        InspectorTest.log("  content: " + JSON.stringify(recording.initialState.content));
  • trunk/Source/WebInspectorUI/ChangeLog

    r237777 r237808  
     12018-11-05  Devin Rousso  <drousso@apple.com>
     2
     3        Web Inspector: show save/restore stack for recorded 2D Canvases
     4        https://bugs.webkit.org/show_bug.cgi?id=175283
     5        <rdar://problem/34040756>
     6
     7        Reviewed by Matt Baker.
     8
     9        Instead of using plain objects for holding the `WI.Recording`'s state, use a model object
     10        so that more data can be passed around. When visualizing the state, show any previously
     11        saved states in a `WI.DetailsSection` underneath the current state. If there are no saved
     12        states (meaning there is only the current state), don't use a `WI.DetailsSection`.
     13
     14        * UserInterface/Models/RecordingState.js: Added.
     15        (WI.RecordingState):
     16        (WI.RecordingState.fromContext):
     17        (WI.RecordingState.async swizzleInitialState):
     18        (WI.RecordingState.prototype.get source):
     19        (WI.RecordingState.prototype.has):
     20        (WI.RecordingState.prototype.get return):
     21        (WI.RecordingState.prototype.toJSON):
     22        (WI.RecordingState.prototype.[Symbol.iterator]):
     23
     24        * UserInterface/Models/Recording.js:
     25        (WI.Recording.prototype.async _process):
     26        (WI.Recording.prototype.async _swizzleState): Deleted.
     27
     28        * UserInterface/Models/RecordingAction.js:
     29        (WI.RecordingAction.prototype.process):
     30        (WI.RecordingAction.deriveCurrentState): Deleted.
     31
     32        * UserInterface/Views/RecordingContentView.js:
     33        (WI.RecordingContentView.prototype._generateContentCanvas2D):
     34
     35        * UserInterface/Views/RecordingStateDetailsSidebarPanel.js:
     36        (WI.RecordingStateDetailsSidebarPanel):
     37        (WI.RecordingStateDetailsSidebarPanel.prototype.get scrollElement):
     38        (WI.RecordingStateDetailsSidebarPanel.prototype.sizeDidChange): Added.
     39        (WI.RecordingStateDetailsSidebarPanel.prototype._generateDetailsCanvas2D):
     40        (WI.RecordingStateDetailsSidebarPanel.prototype._generateDetailsCanvas2D.isColorProperty): Deleted.
     41        (WI.RecordingStateDetailsSidebarPanel.prototype._generateDetailsCanvas2D.createInlineSwatch): Deleted.
     42        * UserInterface/Views/RecordingStateDetailsSidebarPanel.css:
     43        (.sidebar > .panel.details.recording-state > .content .details-section > .header .source): Added.
     44        (.sidebar > .panel.details.recording-state > .content .data-grid tr.modified): Added.
     45        (.sidebar > .panel.details.recording-state > .content .data-grid tr:not(.selected).non-standard .name-column): Added.
     46        (.sidebar > .panel.details.recording-state > .content .data-grid tr:not(.selected) .unavailable): Added.
     47        (.sidebar > .panel.details.recording-state > .content .data-grid .inline-swatch): Added.
     48        (.sidebar > .panel.details.recording-state > .content > .data-grid tr.modified): Deleted.
     49        (.sidebar > .panel.details.recording-state > .content > .data-grid tr:not(.selected).non-standard): Deleted.
     50        (.sidebar > .panel.details.recording-state > .content > .data-grid tr:not(.selected) .unavailable): Deleted.
     51        (.sidebar > .panel.details.recording-state > .content > .data-grid .inline-swatch): Deleted.
     52
     53        * UserInterface/Main.html:
     54        * UserInterface/Test.html:
     55
     56        * Localizations/en.lproj/localizedStrings.js:
     57
    1582018-11-03  Devin Rousso  <drousso@apple.com>
    259
  • trunk/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js

    r237746 r237808  
    5555localizedStrings["%s delay"] = "%s delay";
    5656localizedStrings["%s interval"] = "%s interval";
     57localizedStrings["(Action %s)"] = "(Action %s)";
    5758localizedStrings["(Disk)"] = "(Disk)";
    5859localizedStrings["(Index)"] = "(Index)";
     
    253254localizedStrings["Create a new tab"] = "Create a new tab";
    254255localizedStrings["Current"] = "Current";
     256localizedStrings["Current State"] = "Current State";
    255257localizedStrings["Custom"] = "Custom";
    256258localizedStrings["DNS"] = "DNS";
     
    746748localizedStrings["Running the “%s“ audit"] = "Running the “%s“ audit";
    747749localizedStrings["Samples"] = "Samples";
     750localizedStrings["Save %d"] = "Save %d";
    748751localizedStrings["Save File"] = "Save File";
    749752localizedStrings["Save Selected"] = "Save Selected";
    750753localizedStrings["Save configuration"] = "Save configuration";
     754localizedStrings["Saved States"] = "Saved States";
    751755localizedStrings["Scheduling:"] = "Scheduling:";
    752756localizedStrings["Scheme"] = "Scheme";
  • trunk/Source/WebInspectorUI/UserInterface/Main.html

    r237665 r237808  
    414414    <script src="Models/RecordingFrame.js"></script>
    415415    <script src="Models/RecordingInitialStateAction.js"></script>
     416    <script src="Models/RecordingState.js"></script>
    416417    <script src="Models/Redirect.js"></script>
    417418    <script src="Models/RenderingFrameTimelineRecord.js"></script>
  • trunk/Source/WebInspectorUI/UserInterface/Models/Recording.js

    r237670 r237808  
    376376                this._processContext.drawImage(initialContent, 0, 0);
    377377
    378                 for (let state of this._initialState.states) {
    379                     let swizzledState = await this._swizzleState(state);
    380                     for (let [key, value] of Object.entries(swizzledState)) {
    381                         try {
    382                             if (WI.RecordingAction.isFunctionForType(this._type, key))
    383                                 this._processContext[key](...value);
    384                             else
    385                                 this._processContext[key] = value;
    386                         } catch { }
    387                     }
     378                for (let initialState of this._initialState.states) {
     379                    let state = await WI.RecordingState.swizzleInitialState(this, initialState);
     380                    state.apply(this._type, this._processContext);
    388381
    389382                    // The last state represents the current state, which should not be saved.
    390                     if (state !== this._initialState.states.lastValue) {
     383                    if (initialState !== this._initialState.states.lastValue) {
    391384                        this._processContext.save();
    392                         this._processStates.push(WI.RecordingAction.deriveCurrentState(this._type, this._processContext));
     385                        this._processStates.push(WI.RecordingState.fromContext(this._type, this._processContext));
    393386                    }
    394387                }
     
    456449        this._processContext = null;
    457450        this._processing = false;
    458     }
    459 
    460     async _swizzleState(state)
    461     {
    462         let swizzledState = {};
    463 
    464         for (let [key, value] of Object.entries(state)) {
    465             // COMPATIBILITY (iOS 12.0): Recording.InitialState.states did not exist yet
    466             let keyIndex = parseInt(key);
    467             if (!isNaN(keyIndex))
    468                 key = await this.swizzle(keyIndex, WI.Recording.Swizzle.String);
    469 
    470             switch (key) {
    471             case "setTransform":
    472                 value = [await this.swizzle(value, WI.Recording.Swizzle.DOMMatrix)];
    473                 break;
    474 
    475             case "fillStyle":
    476             case "strokeStyle":
    477                     let [gradient, pattern, string] = await Promise.all([
    478                         this.swizzle(value, WI.Recording.Swizzle.CanvasGradient),
    479                         this.swizzle(value, WI.Recording.Swizzle.CanvasPattern),
    480                         this.swizzle(value, WI.Recording.Swizzle.String),
    481                     ]);
    482                     if (gradient && !pattern)
    483                         value = gradient;
    484                     else if (pattern && !gradient)
    485                         value = pattern;
    486                     else
    487                         value = string;
    488                 break;
    489 
    490             case "direction":
    491             case "font":
    492             case "globalCompositeOperation":
    493             case "imageSmoothingQuality":
    494             case "lineCap":
    495             case "lineJoin":
    496             case "shadowColor":
    497             case "textAlign":
    498             case "textBaseline":
    499                 value = await this.swizzle(value, WI.Recording.Swizzle.String);
    500                 break;
    501 
    502             case "globalAlpha":
    503             case "lineWidth":
    504             case "miterLimit":
    505             case "shadowOffsetX":
    506             case "shadowOffsetY":
    507             case "shadowBlur":
    508             case "lineDashOffset":
    509                 value = await this.swizzle(value, WI.Recording.Swizzle.Number);
    510                 break;
    511 
    512             case "setPath":
    513                 value = [await this.swizzle(value[0], WI.Recording.Swizzle.Path2D)];
    514                 break;
    515             }
    516 
    517             if (value === undefined || (Array.isArray(value) && value.includes(undefined)))
    518                 continue;
    519 
    520             swizzledState[key] = value;
    521         }
    522 
    523         return swizzledState;
    524451    }
    525452};
  • trunk/Source/WebInspectorUI/UserInterface/Models/RecordingAction.js

    r237777 r237808  
    136136    }
    137137
    138     static deriveCurrentState(type, context)
    139     {
    140         if (type === WI.Recording.Type.Canvas2D) {
    141             let matrix = context.getTransform();
    142 
    143             let state = {};
    144 
    145             if (WI.ImageUtilities.supportsCanvasPathDebugging()) {
    146                 state.currentX = context.currentX;
    147                 state.currentY = context.currentY;
    148             }
    149 
    150             state.direction = context.direction;
    151             state.fillStyle = context.fillStyle;
    152             state.font = context.font;
    153             state.globalAlpha = context.globalAlpha;
    154             state.globalCompositeOperation = context.globalCompositeOperation;
    155             state.imageSmoothingEnabled = context.imageSmoothingEnabled;
    156             state.imageSmoothingQuality = context.imageSmoothingQuality;
    157             state.lineCap = context.lineCap;
    158             state.lineDash = context.getLineDash();
    159             state.lineDashOffset = context.lineDashOffset;
    160             state.lineJoin = context.lineJoin;
    161             state.lineWidth = context.lineWidth;
    162             state.miterLimit = context.miterLimit;
    163             state.shadowBlur = context.shadowBlur;
    164             state.shadowColor = context.shadowColor;
    165             state.shadowOffsetX = context.shadowOffsetX;
    166             state.shadowOffsetY = context.shadowOffsetY;
    167             state.strokeStyle = context.strokeStyle;
    168             state.textAlign = context.textAlign;
    169             state.textBaseline = context.textBaseline;
    170             state.transform = [matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f];
    171             state.webkitImageSmoothingEnabled = context.webkitImageSmoothingEnabled;
    172             state.webkitLineDash = context.webkitLineDash;
    173             state.webkitLineDashOffset = context.webkitLineDashOffset;
    174 
    175             if (WI.ImageUtilities.supportsCanvasPathDebugging())
    176                 state.setPath = [context.getPath()];
    177 
    178             return state;
    179         }
    180 
    181         return null;
    182     }
    183 
    184138    static _prototypeForType(type)
    185139    {
     
    262216
    263217        if (recording.type === WI.Recording.Type.Canvas2D) {
    264             let currentState = WI.RecordingAction.deriveCurrentState(recording.type, context);
     218            let currentState = WI.RecordingState.fromContext(recording.type, context, {source: this});
    265219            console.assert(currentState);
    266220
     
    275229            let lastState = null;
    276230            if (lastAction) {
    277                 lastState = lastAction.states.lastValue;
    278                 for (let key in currentState) {
    279                     if (!(key in lastState) || (currentState[key] !== lastState[key] && !Object.shallowEqual(currentState[key], lastState[key])))
    280                         this._stateModifiers.add(key);
     231                let previousState = lastAction.states.lastValue;
     232                for (let [name, value] of currentState) {
     233                    let previousValue = previousState.get(name);
     234                    if (value !== previousValue && !Object.shallowEqual(value, previousValue))
     235                        this._stateModifiers.add(name);
    281236                }
    282237            }
  • trunk/Source/WebInspectorUI/UserInterface/Test.html

    r237665 r237808  
    175175    <script src="Models/RecordingFrame.js"></script>
    176176    <script src="Models/RecordingInitialStateAction.js"></script>
     177    <script src="Models/RecordingState.js"></script>
    177178    <script src="Models/Redirect.js"></script>
    178179    <script src="Models/RenderingFrameTimelineRecord.js"></script>
  • trunk/Source/WebInspectorUI/UserInterface/Views/RecordingContentView.js

    r237777 r237808  
    230230
    231231            for (let state of snapshot.states) {
    232                 for (let name in state) {
    233                     if (!(name in snapshot.context))
    234                         continue;
    235 
    236                     // Skip internal state used for path debugging.
    237                     if (name === "currentX" || name === "currentY")
    238                         continue;
    239 
    240                     try {
    241                         if (WI.RecordingAction.isFunctionForType(this.representedObject.type, name))
    242                             snapshot.context[name](...state[name]);
    243                         else
    244                             snapshot.context[name] = state[name];
    245                     } catch {
    246                         delete state[name];
    247                     }
    248                 }
     232                state.apply(this.representedObject.type, snapshot.context);
    249233
    250234                ++saveCount;
  • trunk/Source/WebInspectorUI/UserInterface/Views/RecordingStateDetailsSidebarPanel.css

    r236237 r237808  
    2828}
    2929
    30 .sidebar > .panel.details.recording-state > .content > .data-grid tr.modified {
     30.sidebar > .panel.details.recording-state > .content .details-section > .header .source {
     31    -webkit-margin-start: 4px;
     32    color: var(--text-color-gray-medium);
     33}
     34
     35.sidebar > .panel.details.recording-state > .content .data-grid tr.modified {
    3136    background-color: var(--value-changed-highlight);
    3237}
    3338
    34 .sidebar > .panel.details.recording-state > .content > .data-grid tr:not(.selected).non-standard {
     39.sidebar > .panel.details.recording-state > .content .data-grid tr:not(.selected).non-standard .name-column {
    3540    opacity: 0.5;
    3641}
    3742
    38 .sidebar > .panel.details.recording-state > .content > .data-grid tr:not(.selected) .unavailable {
     43.sidebar > .panel.details.recording-state > .content .data-grid tr:not(.selected) .unavailable {
    3944    color: grey;
    4045}
    4146
    42 .sidebar > .panel.details.recording-state > .content > .data-grid .inline-swatch {
     47.sidebar > .panel.details.recording-state > .content .data-grid .inline-swatch {
    4348    vertical-align: -1px;
    4449}
    4550
    4651@media (prefers-dark-interface) {
    47     .sidebar > .panel.details.recording-state > .content > .data-grid tr.modified {
     52    .sidebar > .panel.details.recording-state > .content .data-grid tr.modified {
    4853        color: var(--green-highlight-text-color);
    4954    }
  • trunk/Source/WebInspectorUI/UserInterface/Views/RecordingStateDetailsSidebarPanel.js

    r237198 r237808  
    3333        this._action = null;
    3434
    35         this._dataGrid = null;
     35        this._dataGrids = [];
    3636    }
    3737
     
    7979    get scrollElement()
    8080    {
    81         return this._dataGrid.scrollContainer;
     81        if (this._dataGrids.length === 1)
     82            return this._dataGrids[0].scrollContainer;
     83        return super.scrollElement;
     84    }
     85
     86    sizeDidChange()
     87    {
     88        super.sizeDidChange();
     89
     90        if (this._dataGrids.length === 1)
     91            return;
     92
     93        // FIXME: <https://webkit.org/b/152269> Web Inspector: Convert DetailsSection classes to use View
     94        for (let dataGrid of this._dataGrids)
     95            dataGrid.sizeDidChange();
    8296    }
    8397
     
    86100    _generateDetailsCanvas2D(action)
    87101    {
    88         if (!this._dataGrid) {
    89             this._dataGrid = new WI.DataGrid({
     102        if (this._dataGrids.length === 1)
     103            this.contentView.removeSubview(this._dataGrids[0]);
     104
     105        this.contentView.element.removeChildren();
     106
     107        this._dataGrids = [];
     108
     109        let currentState = action.states.lastValue;
     110        console.assert(currentState);
     111        if (!currentState)
     112            return;
     113
     114        let createStateDataGrid = (state) => {
     115            let dataGrid = new WI.DataGrid({
    90116                name: {title: WI.UIString("Name")},
    91117                value: {title: WI.UIString("Value")},
    92118            });
     119            this._dataGrids.push(dataGrid);
     120
     121            for (let [name, value] of state) {
     122                // Skip internal state used for path debugging.
     123                if (name === "setPath")
     124                    continue;
     125
     126                if (typeof value === "object") {
     127                    let isGradient = value instanceof CanvasGradient;
     128                    let isPattern = value instanceof CanvasPattern;
     129                    if (isGradient || isPattern) {
     130                        let textElement = document.createElement("span");
     131                        textElement.classList.add("unavailable");
     132
     133                        let image = null;
     134                        if (isGradient) {
     135                            textElement.textContent = WI.unlocalizedString("CanvasGradient");
     136                            image = WI.ImageUtilities.imageFromCanvasGradient(value, 100, 100);
     137                        } else if (isPattern) {
     138                            textElement.textContent = WI.unlocalizedString("CanvasPattern");
     139                            image = value.__image;
     140                        }
     141
     142                        let fragment = document.createDocumentFragment();
     143                        if (image) {
     144                            let swatch = new WI.InlineSwatch(WI.InlineSwatch.Type.Image, image);
     145                            fragment.appendChild(swatch.element);
     146                        }
     147                        fragment.appendChild(textElement);
     148                        value = fragment;
     149                    } else {
     150                        if (value instanceof DOMMatrix)
     151                            value = [value.a, value.b, value.c, value.d, value.e, value.f];
     152
     153                        value = JSON.stringify(value);
     154                    }
     155                } else if (name === "fillStyle" || name === "strokeStyle" || name === "shadowColor") {
     156                    let color = WI.Color.fromString(value);
     157                    const readOnly = true;
     158                    let swatch = new WI.InlineSwatch(WI.InlineSwatch.Type.Color, color, readOnly);
     159                    value = document.createElement("span");
     160                    value.append(swatch.element, color.toString());
     161                }
     162
     163                let classNames = [];
     164                if (state === currentState && !action.isGetter && action.stateModifiers.has(name))
     165                    classNames.push("modified");
     166                if (name.startsWith("webkit"))
     167                    classNames.push("non-standard");
     168
     169                const hasChildren = false;
     170                dataGrid.appendChild(new WI.DataGridNode({name, value}, hasChildren, classNames));
     171            }
     172
     173            dataGrid.updateLayoutIfNeeded();
     174            return dataGrid;
     175        };
     176
     177        let createStateSection = (state, index = NaN) => {
     178            let isCurrentState = isNaN(index);
     179
     180            let dataGrid = createStateDataGrid(state);
     181            let row = new WI.DetailsSectionDataGridRow(dataGrid);
     182            let group = new WI.DetailsSectionGroup([row]);
     183
     184            let identifier = isCurrentState ? "recording-current-state" : `recording-saved-state-${index + 1}`;
     185            const title = null;
     186            const optionsElement = null;
     187            let defaultCollapsedSettingValue = !isCurrentState;
     188            let section = new WI.DetailsSection(identifier, title, [group], optionsElement, defaultCollapsedSettingValue);
     189
     190            if (isCurrentState)
     191                section.title = WI.UIString("Current State");
     192            else {
     193                section.title = WI.UIString("Save %d").format(index + 1);
     194
     195                if (state.source) {
     196                    let sourceIndex = this._recording.actions.indexOf(state.source);
     197                    if (sourceIndex >= 0) {
     198                        let sourceElement = section.titleElement.appendChild(document.createElement("span"));
     199                        sourceElement.classList.add("source");
     200                        sourceElement.textContent = WI.UIString("(Action %s)").format(sourceIndex);
     201                    }
     202                }
     203            }
     204
     205            return section;
     206        };
     207
     208        if (action.states.length === 1) {
     209            this.contentView.addSubview(createStateDataGrid(currentState));
     210            return;
    93211        }
    94         if (!this._dataGrid.parentView)
    95             this.contentView.addSubview(this._dataGrid);
    96 
    97         this._dataGrid.removeChildren();
    98 
    99         let currentState = action.states.lastValue;
    100         console.assert(currentState);
    101         if (!currentState)
    102             return;
    103 
    104         function isColorProperty(name) {
    105             return name === "fillStyle" || name === "strokeStyle" || name === "shadowColor";
     212
     213        let currentStateSection = createStateSection(currentState);
     214        this.contentView.element.appendChild(currentStateSection.element);
     215
     216        let savedStateSections = [];
     217        for (let i = action.states.length - 2; i >= 0; --i) {
     218            let savedStateSection = createStateSection(action.states[i], i);
     219            savedStateSections.push(savedStateSection);
    106220        }
    107221
    108         function createInlineSwatch(value) {
    109             let color = WI.Color.fromString(value);
    110             if (!color)
    111                 return null;
    112 
    113             const readOnly = true;
    114             return new WI.InlineSwatch(WI.InlineSwatch.Type.Color, color, readOnly);
    115         }
    116 
    117         for (let name in currentState) {
    118             // Skip internal state used for path debugging.
    119             if (name === "setPath")
    120                 continue;
    121 
    122             let value = currentState[name];
    123             if (typeof value === "object") {
    124                 let isGradient = value instanceof CanvasGradient;
    125                 let isPattern = value instanceof CanvasPattern;
    126                 if (isGradient || isPattern) {
    127                     let textElement = document.createElement("span");
    128                     textElement.classList.add("unavailable");
    129 
    130                     let image = null;
    131                     if (isGradient) {
    132                         textElement.textContent = WI.unlocalizedString("CanvasGradient");
    133                         image = WI.ImageUtilities.imageFromCanvasGradient(value, 100, 100);
    134                     } else if (isPattern) {
    135                         textElement.textContent = WI.unlocalizedString("CanvasPattern");
    136                         image = value.__image;
    137                     }
    138 
    139                     let fragment = document.createDocumentFragment();
    140                     if (image) {
    141                         let swatch = new WI.InlineSwatch(WI.InlineSwatch.Type.Image, image);
    142                         fragment.appendChild(swatch.element);
    143                     }
    144                     fragment.appendChild(textElement);
    145                     value = fragment;
    146                 } else {
    147                     if (value instanceof DOMMatrix)
    148                         value = [value.a, value.b, value.c, value.d, value.e, value.f];
    149 
    150                     value = JSON.stringify(value);
    151                 }
    152             } else if (isColorProperty(name)) {
    153                 let swatch = createInlineSwatch(value);
    154                 let label = swatch.value.toString();
    155                 value = document.createElement("span");
    156                 value.append(swatch.element, label);
    157             }
    158 
    159             let classNames = [];
    160             if (!action.isGetter && action.stateModifiers.has(name))
    161                 classNames.push("modified");
    162             if (name.startsWith("webkit"))
    163                 classNames.push("non-standard");
    164 
    165             const hasChildren = false;
    166             this._dataGrid.appendChild(new WI.DataGridNode({name, value}, hasChildren, classNames));
    167         }
     222        let savedStatesGroup = new WI.DetailsSectionGroup(savedStateSections);
     223        let savedStatesSection = new WI.DetailsSection("recording-saved-states", WI.UIString("Saved States"), [savedStatesGroup]);
     224        this.contentView.element.appendChild(savedStatesSection.element);
    168225    }
    169226};
Note: See TracChangeset for help on using the changeset viewer.