Changeset 237808 in webkit
- Timestamp:
- Nov 5, 2018 9:34:48 AM (5 years ago)
- Location:
- trunk
- Files:
-
- 1 added
- 12 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/LayoutTests/ChangeLog
r237804 r237808 1 2018-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 1 14 2018-11-05 Thibault Saunier <tsaunier@igalia.com> 2 15 -
trunk/LayoutTests/inspector/canvas/recording-2d.html
r237777 r237808 520 520 async function logStates(recording) { 521 521 async function compare(index, expected) { 522 let s wizzledState = await recording._swizzleState(recording.initialState.states[index]);523 InspectorTest.expectEqual(s wizzledState["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.`) 524 524 } 525 525 -
trunk/LayoutTests/inspector/canvas/resources/recording-utilities.js
r237670 r237808 1 1 TestPage.registerInitializer(() => { 2 2 function log(object, indent) { 3 for (let key of Object.keys(object)) { 4 let value = object[key]; 3 for (let [name, value] of object) { 5 4 if (typeof value === "string") 6 5 value = sanitizeURL(value); 7 6 else if (Array.isArray(value) && value[0] instanceof DOMMatrix) 8 7 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)); 10 9 } 11 10 } … … 15 14 16 15 InspectorTest.log(" attributes:"); 17 log( recording.initialState.attributes, " ");16 log(Object.entries(recording.initialState.attributes), " "); 18 17 19 18 let currentState = recording.initialState.states.lastValue; 20 19 if (currentState) { 21 20 InspectorTest.log(" current state:"); 22 let s wizzledState = await recording._swizzleState(currentState);23 log(s wizzledState, " ");21 let state = await WI.RecordingState.swizzleInitialState(recording, currentState); 22 log(state, " "); 24 23 } 25 24 26 25 InspectorTest.log(" parameters:"); 27 log( recording.initialState.parameters, " ");26 log(Object.entries(recording.initialState.parameters), " "); 28 27 29 28 InspectorTest.log(" content: " + JSON.stringify(recording.initialState.content)); -
trunk/Source/WebInspectorUI/ChangeLog
r237777 r237808 1 2018-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 1 58 2018-11-03 Devin Rousso <drousso@apple.com> 2 59 -
trunk/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
r237746 r237808 55 55 localizedStrings["%s delay"] = "%s delay"; 56 56 localizedStrings["%s interval"] = "%s interval"; 57 localizedStrings["(Action %s)"] = "(Action %s)"; 57 58 localizedStrings["(Disk)"] = "(Disk)"; 58 59 localizedStrings["(Index)"] = "(Index)"; … … 253 254 localizedStrings["Create a new tab"] = "Create a new tab"; 254 255 localizedStrings["Current"] = "Current"; 256 localizedStrings["Current State"] = "Current State"; 255 257 localizedStrings["Custom"] = "Custom"; 256 258 localizedStrings["DNS"] = "DNS"; … … 746 748 localizedStrings["Running the “%s“ audit"] = "Running the “%s“ audit"; 747 749 localizedStrings["Samples"] = "Samples"; 750 localizedStrings["Save %d"] = "Save %d"; 748 751 localizedStrings["Save File"] = "Save File"; 749 752 localizedStrings["Save Selected"] = "Save Selected"; 750 753 localizedStrings["Save configuration"] = "Save configuration"; 754 localizedStrings["Saved States"] = "Saved States"; 751 755 localizedStrings["Scheduling:"] = "Scheduling:"; 752 756 localizedStrings["Scheme"] = "Scheme"; -
trunk/Source/WebInspectorUI/UserInterface/Main.html
r237665 r237808 414 414 <script src="Models/RecordingFrame.js"></script> 415 415 <script src="Models/RecordingInitialStateAction.js"></script> 416 <script src="Models/RecordingState.js"></script> 416 417 <script src="Models/Redirect.js"></script> 417 418 <script src="Models/RenderingFrameTimelineRecord.js"></script> -
trunk/Source/WebInspectorUI/UserInterface/Models/Recording.js
r237670 r237808 376 376 this._processContext.drawImage(initialContent, 0, 0); 377 377 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); 388 381 389 382 // 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) { 391 384 this._processContext.save(); 392 this._processStates.push(WI.Recording Action.deriveCurrentState(this._type, this._processContext));385 this._processStates.push(WI.RecordingState.fromContext(this._type, this._processContext)); 393 386 } 394 387 } … … 456 449 this._processContext = null; 457 450 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 yet466 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 else487 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;524 451 } 525 452 }; -
trunk/Source/WebInspectorUI/UserInterface/Models/RecordingAction.js
r237777 r237808 136 136 } 137 137 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 184 138 static _prototypeForType(type) 185 139 { … … 262 216 263 217 if (recording.type === WI.Recording.Type.Canvas2D) { 264 let currentState = WI.Recording Action.deriveCurrentState(recording.type, context);218 let currentState = WI.RecordingState.fromContext(recording.type, context, {source: this}); 265 219 console.assert(currentState); 266 220 … … 275 229 let lastState = null; 276 230 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); 281 236 } 282 237 } -
trunk/Source/WebInspectorUI/UserInterface/Test.html
r237665 r237808 175 175 <script src="Models/RecordingFrame.js"></script> 176 176 <script src="Models/RecordingInitialStateAction.js"></script> 177 <script src="Models/RecordingState.js"></script> 177 178 <script src="Models/Redirect.js"></script> 178 179 <script src="Models/RenderingFrameTimelineRecord.js"></script> -
trunk/Source/WebInspectorUI/UserInterface/Views/RecordingContentView.js
r237777 r237808 230 230 231 231 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); 249 233 250 234 ++saveCount; -
trunk/Source/WebInspectorUI/UserInterface/Views/RecordingStateDetailsSidebarPanel.css
r236237 r237808 28 28 } 29 29 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 { 31 36 background-color: var(--value-changed-highlight); 32 37 } 33 38 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 { 35 40 opacity: 0.5; 36 41 } 37 42 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 { 39 44 color: grey; 40 45 } 41 46 42 .sidebar > .panel.details.recording-state > .content >.data-grid .inline-swatch {47 .sidebar > .panel.details.recording-state > .content .data-grid .inline-swatch { 43 48 vertical-align: -1px; 44 49 } 45 50 46 51 @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 { 48 53 color: var(--green-highlight-text-color); 49 54 } -
trunk/Source/WebInspectorUI/UserInterface/Views/RecordingStateDetailsSidebarPanel.js
r237198 r237808 33 33 this._action = null; 34 34 35 this._dataGrid = null;35 this._dataGrids = []; 36 36 } 37 37 … … 79 79 get scrollElement() 80 80 { 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(); 82 96 } 83 97 … … 86 100 _generateDetailsCanvas2D(action) 87 101 { 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({ 90 116 name: {title: WI.UIString("Name")}, 91 117 value: {title: WI.UIString("Value")}, 92 118 }); 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; 93 211 } 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); 106 220 } 107 221 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); 168 225 } 169 226 };
Note: See TracChangeset
for help on using the changeset viewer.