WebKit Bugzilla
Attachment 347468 Details for
Bug 185152
: Web Inspector: Canvas tab: allow recording processing to be stopped midway
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Requests
|
Help
|
New Account
|
Log In
Remember
[x]
|
Forgot Password
Login:
[x]
[patch]
Patch
bug-185152-20180819145959.patch (text/plain), 48.34 KB, created by
Devin Rousso
on 2018-08-19 15:00:00 PDT
(
hide
)
Description:
Patch
Filename:
MIME Type:
Creator:
Devin Rousso
Created:
2018-08-19 15:00:00 PDT
Size:
48.34 KB
patch
obsolete
>diff --git a/Source/WebInspectorUI/ChangeLog b/Source/WebInspectorUI/ChangeLog >index 1d0a6261761057810a38ccd216ac3007a8d8ce7a..c17224793aba5d432f0ff6bc40a01ee6bc4756df 100644 >--- a/Source/WebInspectorUI/ChangeLog >+++ b/Source/WebInspectorUI/ChangeLog >@@ -1,3 +1,86 @@ >+2018-08-19 Devin Rousso <drousso@apple.com> >+ >+ Web Inspector: Canvas tab: allow recording processing to be stopped midway >+ https://bugs.webkit.org/show_bug.cgi?id=185152 >+ >+ Reviewed by NOBODY (OOPS!). >+ >+ Previously, `WI.Recording` used a `WI.YieldableTask` to process every action in such a way >+ as to not block the UI. The downside to this approach was that it used a message view to >+ indicate the progress of this process, and prevented the user from viewing the `WI.Recording` >+ until that process was completed. >+ >+ This patch changes `WI.Recording` to instead use `async/await` and fire events whenever a >+ `WI.RecordingAction` (and `WI.RecordingFrame`) finished processing, allowing it to be added >+ to the recording `WI.TreeOutline` and selected by the user. Additionally, a pause/resume >+ button is added to the `WI.CanvasSidebarPanel` so the user has greater control over what >+ how much of the `WI.Recording` they want to process. >+ >+ * Localizations/en.lproj/localizedStrings.js: >+ >+ * UserInterface/Base/Utilities.js: >+ (Promise.delay) >+ Utility function for promisifying `setTimeout`. >+ >+ * UserInterface/Models/Recording.js: >+ (WI.Recording): >+ (WI.Recording.prototype.get processing): Added. >+ (WI.Recording.prototype.get ready): Added. >+ (WI.Recording.prototype.startProcessing): Added. >+ (WI.Recording.prototype.stopProcessing): Added. >+ (WI.Recording.prototype.async._process): Added. >+ (WI.Recording.prototype.process): Deleted. >+ (WI.Recording.prototype.async.yieldableTaskWillProcessItem): Deleted. >+ (WI.Recording.prototype.async.yieldableTaskDidFinish): Deleted. >+ >+ * UserInterface/Models/RecordingAction.js: >+ (WI.RecordingAction): >+ (WI.RecordingAction.prototype.get ready): Added. >+ (WI.RecordingAction.prototype.async.swizzle): >+ (WI.RecordingAction.prototype.apply): >+ >+ * UserInterface/Models/RecordingInitialStateAction.js: >+ (WI.RecordingInitialStateAction): >+ >+ * UserInterface/Views/CanvasSidebarPanel.js: >+ (WI.CanvasSidebarPanel): >+ (WI.CanvasSidebarPanel.prototype.set recording): >+ (WI.CanvasSidebarPanel.prototype.set action): >+ (WI.CanvasSidebarPanel.prototype._recordingAdded): >+ (WI.CanvasSidebarPanel.prototype._recordingRemoved): >+ (WI.CanvasSidebarPanel.prototype._currentRepresentedObjectsDidChange): >+ (WI.CanvasSidebarPanel.prototype._treeOutlineSelectionDidChange): >+ (WI.CanvasSidebarPanel.prototype._recordingChanged): >+ (WI.CanvasSidebarPanel.prototype._createRecordingFrameTreeElement): Added. >+ (WI.CanvasSidebarPanel.prototype._createRecordingActionTreeElement): Added. >+ * UserInterface/Views/CanvasSidebarPanel.css: >+ (.sidebar > .panel.navigation.canvas > .content > .recording-content > .tree-outline .item.processing .subtitle > progress): Added. >+ (.sidebar > .panel.navigation.canvas > .content > .recording-content > .tree-outline .item.processing .subtitle::before): Added. >+ (.sidebar > .panel.navigation.canvas > .content > .recording-content > .recording-processing-options): Added. >+ (.sidebar > .panel.navigation.canvas > .content > .recording-content > .recording-processing-options > .indeterminate-progress-spinner): Added. >+ (.sidebar > .panel.navigation.canvas > .content > .recording-content > .indeterminate-progress-spinner): Deleted. >+ >+ * UserInterface/Views/RecordingContentView.js: >+ (WI.RecordingContentView): >+ (WI.RecordingContentView.prototype.updateActionIndex): >+ (WI.RecordingContentView.prototype.initialLayout): >+ (WI.RecordingContentView.prototype._updateCanvasPath): >+ (WI.RecordingContentView.prototype._handleRecordingProcessedAction): Added. >+ (WI.RecordingContentView.prototype._updateProcessProgress): Deleted. >+ (WI.RecordingContentView.prototype._handleRecordingProcessedActionSwizzle): Deleted. >+ (WI.RecordingContentView.prototype._handleRecordingProcessedActionApply): Deleted. >+ >+ * UserInterface/Views/FolderTreeElement.js: >+ (WI.FolderTreeElement): >+ >+ * UserInterface/Views/GeneralTreeElement.js: >+ (WI.GeneralTreeElement.prototype.get statusElement): Added. >+ (WI.GeneralTreeElement.prototype._updateTitleElements): >+ >+ * UserInterface/Views/RecordingNavigationSidebarPanel.css: Removed. >+ * UserInterface/Views/RecordingNavigationSidebarPanel.js: Removed. >+ These files are no longer used since they were "merged" into `WI.CanvasSidebarPanel`. >+ > 2018-08-16 Devin Rousso <drousso@apple.com> > > Web Inspector: support breakpoints for arbitrary event names >diff --git a/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js b/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js >index aa25795db1be469139c29092f92150281f717ee2..4f13981e402942fc2353aad17ed046ad62c8147c 100644 >--- a/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js >+++ b/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js >@@ -568,7 +568,6 @@ localizedStrings["Live"] = "Live"; > localizedStrings["Live Size"] = "Live Size"; > localizedStrings["Load \u2014 %s"] = "Load \u2014 %s"; > localizedStrings["Load cancelled"] = "Load cancelled"; >-localizedStrings["Loading Recording"] = "Loading Recording"; > localizedStrings["Local File"] = "Local File"; > localizedStrings["Local Storage"] = "Local Storage"; > localizedStrings["Local Variables"] = "Local Variables"; >@@ -641,7 +640,6 @@ localizedStrings["No Preview Available"] = "No Preview Available"; > localizedStrings["No Properties"] = "No Properties"; > localizedStrings["No Properties \u2014 Click to Edit"] = "No Properties \u2014 Click to Edit"; > localizedStrings["No Query Parameters"] = "No Query Parameters"; >-localizedStrings["No Recording Data"] = "No Recording Data"; > localizedStrings["No Request Headers"] = "No Request Headers"; > localizedStrings["No Response Headers"] = "No Response Headers"; > localizedStrings["No Results Found"] = "No Results Found"; >@@ -704,6 +702,7 @@ localizedStrings["Parent"] = "Parent"; > localizedStrings["Partial Garbage Collection"] = "Partial Garbage Collection"; > localizedStrings["Passive"] = "Passive"; > localizedStrings["Path"] = "Path"; >+localizedStrings["Pause Processing"] = "Pause Processing"; > localizedStrings["Pause Reason"] = "Pause Reason"; > localizedStrings["Pause script execution (%s or %s)"] = "Pause script execution (%s or %s)"; > localizedStrings["Ping"] = "Ping"; >@@ -730,7 +729,6 @@ localizedStrings["Probe Expression"] = "Probe Expression"; > localizedStrings["Probe Sample Recorded"] = "Probe Sample Recorded"; > localizedStrings["Probes"] = "Probes"; > localizedStrings["Processing Instruction"] = "Processing Instruction"; >-localizedStrings["Processing Recording"] = "Processing Recording"; > localizedStrings["Program %d"] = "Program %d"; > localizedStrings["Properties"] = "Properties"; > localizedStrings["Property"] = "Property"; >@@ -798,6 +796,7 @@ localizedStrings["Response Headers"] = "Response Headers"; > localizedStrings["Response:"] = "Response:"; > localizedStrings["Restart (%s)"] = "Restart (%s)"; > localizedStrings["Restart animation"] = "Restart animation"; >+localizedStrings["Resume Processing"] = "Resume Processing"; > localizedStrings["Resume Thread"] = "Resume Thread"; > localizedStrings["Retained Size"] = "Retained Size"; > localizedStrings["Return type for anonymous function"] = "Return type for anonymous function"; >diff --git a/Source/WebInspectorUI/UserInterface/Base/Utilities.js b/Source/WebInspectorUI/UserInterface/Base/Utilities.js >index 8072ce1aace3fe539e792e39aafd88388ff7c9b2..6cc4618f84c21bd492c26d0d602cfd44ffe9f270 100644 >--- a/Source/WebInspectorUI/UserInterface/Base/Utilities.js >+++ b/Source/WebInspectorUI/UserInterface/Base/Utilities.js >@@ -1337,6 +1337,14 @@ Object.defineProperty(Array.prototype, "binaryIndexOf", > } > }); > >+Object.defineProperty(Promise, "delay", >+{ >+ value(delay) >+ { >+ return new Promise((resolve) => setTimeout(resolve, delay || 0)); >+ } >+}); >+ > (function() { > // The `debounce` function lets you call any function on an object with a delay > // and if the function keeps getting called, the delay gets reset. Since `debounce` >diff --git a/Source/WebInspectorUI/UserInterface/Models/Recording.js b/Source/WebInspectorUI/UserInterface/Models/Recording.js >index 23f9fd5d22825aa837f8a722ff4e675d5f7a5e2f..40e4b790c9a10dce86ad2bd986ec50557d88eb6c 100644 >--- a/Source/WebInspectorUI/UserInterface/Models/Recording.js >+++ b/Source/WebInspectorUI/UserInterface/Models/Recording.js >@@ -41,10 +41,8 @@ WI.Recording = class Recording extends WI.Object > this._visualActionIndexes = []; > this._source = null; > >- this._swizzleTask = null; >- this._applyTask = null; > this._processContext = null; >- this._processPromise = null; >+ this._processing = false; > } > > static fromPayload(payload, frames) >@@ -162,18 +160,33 @@ WI.Recording = class Recording extends WI.Object > get source() { return this._source; } > set source(source) { this._source = source; } > >- process() >+ get processing() { return this._processing; } >+ >+ get ready() > { >- if (!this._processPromise) { >- this._processPromise = new WI.WrappedPromise; >+ return this._actions.lastValue.ready; >+ } > >- let items = this._actions.map((action, index) => { return {action, index} }); >- this._swizzleTask = new WI.YieldableTask(this, items); >- this._applyTask = new WI.YieldableTask(this, items); >+ startProcessing() >+ { >+ console.assert(!this._processing, "Cannot start an already started process()."); >+ console.assert(!this.ready, "Cannot stop a completed process()."); >+ if (this._processing || this.ready) >+ return; > >- this._swizzleTask.start(); >- } >- return this._processPromise.promise; >+ this._processing = true; >+ >+ this._process(); >+ } >+ >+ stopProcessing() >+ { >+ console.assert(this._processing, "Cannot stop an already stopped process()."); >+ console.assert(!this.ready, "Cannot stop a completed process()."); >+ if (!this._processing || this.ready) >+ return; >+ >+ this._processing = false; > } > > createDisplayName(suggestedName) >@@ -332,29 +345,11 @@ WI.Recording = class Recording extends WI.Object > }; > } > >- // YieldableTask delegate >+ // Private > >- async yieldableTaskWillProcessItem(task, item) >+ async _process() > { >- if (task === this._swizzleTask) { >- await item.action.swizzle(this); >- >- this.dispatchEventToListeners(WI.Recording.Event.ProcessedActionSwizzle, {index: item.index}); >- } else if (task === this._applyTask) { >- item.action.process(this, this._processContext); >- >- if (item.action.isVisual) >- this._visualActionIndexes.push(item.index); >- >- this.dispatchEventToListeners(WI.Recording.Event.ProcessedActionApply, {index: item.index}); >- } >- } >- >- async yieldableTaskDidFinish(task) >- { >- if (task === this._swizzleTask) { >- this._swizzleTask = null; >- >+ if (!this._processContext) { > this._processContext = this.createContext(); > > if (this._type === WI.Recording.Type.Canvas2D) { >@@ -411,19 +406,61 @@ WI.Recording = class Recording extends WI.Object > } catch { } > } > } >+ } >+ >+ if (!this._actions[0].ready) { >+ this._actions[0].process(this, this._processContext); >+ this.dispatchEventToListeners(WI.Recording.Event.ProcessedAction, {action: this._actions[0], index: 0}); >+ } >+ >+ const workInterval = 10; >+ let startTime = Date.now(); >+ >+ let cumulativeActionIndex = 0; >+ for (let frameIndex = 0; frameIndex < this._frames.length; ++frameIndex) { >+ let frame = this._frames[frameIndex]; >+ >+ for (let actionIndex = 0; actionIndex < frame.actions.length; ++actionIndex) { >+ ++cumulativeActionIndex; >+ >+ let action = frame.actions[actionIndex]; >+ if (action.ready) >+ continue; >+ >+ await action.swizzle(this); >+ >+ action.process(this, this._processContext); >+ >+ if (action.isVisual) >+ this._visualActionIndexes.push(cumulativeActionIndex); >+ >+ if (!actionIndex) >+ this.dispatchEventToListeners(WI.Recording.Event.ProcessedFrame, {frame, index: frameIndex}); >+ >+ this.dispatchEventToListeners(WI.Recording.Event.ProcessedAction, {action, index: cumulativeActionIndex}); >+ >+ if (Date.now() - startTime > workInterval) { >+ await Promise.delay(); >+ >+ startTime = Date.now(); >+ } >+ >+ if (!this._processing) >+ return; >+ } > >- this._applyTask.start(); >- } else if (task === this._applyTask) { >- this._applyTask = null; >- this._processContext = null; >- this._processPromise.resolve(); >+ if (!this._processing) >+ return; > } >+ >+ this._processContext = null; >+ this._processing = false; > } > }; > > WI.Recording.Event = { >- ProcessedActionApply: "recording-processed-action-apply", >- ProcessedActionSwizzle: "recording-processed-action-swizzle", >+ ProcessedAction: "recording-processed-action", >+ ProcessedFrame: "recording-processed-frame", > }; > > WI.Recording._importedRecordingNameSet = new Set; >diff --git a/Source/WebInspectorUI/UserInterface/Models/RecordingAction.js b/Source/WebInspectorUI/UserInterface/Models/RecordingAction.js >index 0705d12517b609d4d6ba6015ddc03a42013d587f..238d2aa3e76d37962c8ab9d67b5c993dabe09144 100644 >--- a/Source/WebInspectorUI/UserInterface/Models/RecordingAction.js >+++ b/Source/WebInspectorUI/UserInterface/Models/RecordingAction.js >@@ -48,6 +48,9 @@ WI.RecordingAction = class RecordingAction extends WI.Object > > this._state = null; > this._stateModifiers = new Set; >+ >+ this._swizzled = false; >+ this._processed = false; > } > > // Static >@@ -108,8 +111,18 @@ WI.RecordingAction = class RecordingAction extends WI.Object > get state() { return this._state; } > get stateModifiers() { return this._stateModifiers; } > >+ get ready() >+ { >+ return this._swizzled && this._processed; >+ } >+ > process(recording, context) > { >+ console.assert(this._swizzled, "You must swizzle() before you can process()."); >+ console.assert(!this._processed, "You should only process() once."); >+ >+ this._processed = true; >+ > if (recording.type === WI.Recording.Type.CanvasWebGL) { > // We add each RecordingAction to the list of visualActionIndexes after it is processed. > if (this._valid && this._isVisual) { >@@ -187,8 +200,12 @@ WI.RecordingAction = class RecordingAction extends WI.Object > > async swizzle(recording) > { >- if (!this._valid) >+ console.assert(!this._swizzled, "You should only swizzle() once."); >+ >+ if (!this._valid) { >+ this._swizzled = true; > return; >+ } > > let swizzleParameter = (item, index) => { > return recording.swizzle(item, this._payloadSwizzleTypes[index]); >@@ -254,10 +271,15 @@ WI.RecordingAction = class RecordingAction extends WI.Object > this._stateModifiers.add(item); > } > } >+ >+ this._swizzled = true; > } > > apply(context, options = {}) > { >+ console.assert(this._swizzled, "You must swizzle() before you can apply()."); >+ console.assert(this._processed, "You must process() before you can apply()."); >+ > if (!this.valid) > return; > >diff --git a/Source/WebInspectorUI/UserInterface/Models/RecordingInitialStateAction.js b/Source/WebInspectorUI/UserInterface/Models/RecordingInitialStateAction.js >index d5498542246f8c77cdce91282b644751ca164f0c..af4afcd5d3e95eb709ffa8e79317ceafec6f4f36 100644 >--- a/Source/WebInspectorUI/UserInterface/Models/RecordingInitialStateAction.js >+++ b/Source/WebInspectorUI/UserInterface/Models/RecordingInitialStateAction.js >@@ -32,5 +32,7 @@ WI.RecordingInitialStateAction = class RecordingInitialStateAction extends WI.Re > this._name = WI.UIString("Initial State"); > > this._valid = false; >+ >+ this._swizzled = true; > } > }; >diff --git a/Source/WebInspectorUI/UserInterface/Views/CanvasSidebarPanel.css b/Source/WebInspectorUI/UserInterface/Views/CanvasSidebarPanel.css >index cab3dbc1f92ed909d9d0d3e967ee9a2df0c0a9cd..29aea41e5b916a2377bb920a93806f919eec0c2f 100644 >--- a/Source/WebInspectorUI/UserInterface/Views/CanvasSidebarPanel.css >+++ b/Source/WebInspectorUI/UserInterface/Views/CanvasSidebarPanel.css >@@ -72,6 +72,23 @@ > line-height: 16px; > } > >-.sidebar > .panel.navigation.canvas > .content > .recording-content > .indeterminate-progress-spinner { >- margin: 16px auto; >+.sidebar > .panel.navigation.canvas > .content > .recording-content > .tree-outline .item.processing .subtitle > progress { >+ width: 100%; >+ max-width: 100px; >+ margin: 2px 4px 0; >+} >+ >+.sidebar > .panel.navigation.canvas > .content > .recording-content > .tree-outline .item.processing .subtitle::before { >+ content: ""; >+} >+ >+.sidebar > .panel.navigation.canvas > .content > .recording-content > .recording-processing-options { >+ display: flex; >+ flex-direction: column; >+ align-items: center; >+ margin: 16px 0; >+} >+ >+.sidebar > .panel.navigation.canvas > .content > .recording-content > .recording-processing-options > .indeterminate-progress-spinner { >+ margin-bottom: 4px; > } >diff --git a/Source/WebInspectorUI/UserInterface/Views/CanvasSidebarPanel.js b/Source/WebInspectorUI/UserInterface/Views/CanvasSidebarPanel.js >index 75c30b5320eec0e3d47e7a03f9c427e1dcc143ea..b83f85974a23d825dfbd9a9f8a1e1373b0931199 100644 >--- a/Source/WebInspectorUI/UserInterface/Views/CanvasSidebarPanel.js >+++ b/Source/WebInspectorUI/UserInterface/Views/CanvasSidebarPanel.js >@@ -68,8 +68,9 @@ WI.CanvasSidebarPanel = class CanvasSidebarPanel extends WI.NavigationSidebarPan > WI.canvasManager.addEventListener(WI.CanvasManager.Event.RecordingStarted, this._updateRecordNavigationItem, this); > WI.canvasManager.addEventListener(WI.CanvasManager.Event.RecordingStopped, this._updateRecordNavigationItem, this); > >- this._recordingProcessPromise = null; >- this._recordingProcessSpinner = null; >+ this._recordingProcessingOptionsContainer = null; >+ >+ this._selectedRecordingActionIndex = NaN; > } > > // Public >@@ -103,16 +104,30 @@ WI.CanvasSidebarPanel = class CanvasSidebarPanel extends WI.NavigationSidebarPan > if (recording === this._recording) > return; > >+ if (this._recording) { >+ this._recording.removeEventListener(WI.Recording.Event.ProcessedAction, this._handleRecordingProcessedAction, this); >+ this._recording.removeEventListener(WI.Recording.Event.ProcessedFrame, this._handleRecordingProcessedFrame, this); >+ } >+ > if (recording) > this.canvas = recording.source; > > this._recording = recording; >+ >+ if (this._recording) { >+ this._recording.addEventListener(WI.Recording.Event.ProcessedAction, this._handleRecordingProcessedAction, this); >+ this._recording.addEventListener(WI.Recording.Event.ProcessedFrame, this._handleRecordingProcessedFrame, this); >+ } >+ > this._recordingChanged(); > } > > set action(action) > { >- if (!this._recording || this._recordingProcessPromise) >+ if (!this._recording) >+ return; >+ >+ if (action === this._recording.actions[this._selectedRecordingActionIndex]) > return; > > let selectedTreeElement = this._recordingTreeOutline.selectedTreeElement; >@@ -138,6 +153,8 @@ WI.CanvasSidebarPanel = class CanvasSidebarPanel extends WI.NavigationSidebarPan > const omitFocus = false; > const selectedByUser = false; > treeElement.revealAndSelect(omitFocus, selectedByUser); >+ >+ this._selectedRecordingActionIndex = this._recording.actions.indexOf(action); > } > > shown() >@@ -204,19 +221,19 @@ WI.CanvasSidebarPanel = class CanvasSidebarPanel extends WI.NavigationSidebarPan > > _recordingAdded(event) > { >- this.recording = event.data.item; >- > this._updateRecordNavigationItem(); > this._updateRecordingScopeBar(); >+ >+ this.recording = event.data.item; > } > > _recordingRemoved(event) > { >+ this._updateRecordingScopeBar(); >+ > let recording = event.data.item; > if (recording === this.recording) > this.recording = this._canvas ? Array.from(this._canvas.recordingCollection).lastValue : null; >- >- this._updateRecordingScopeBar(); > } > > _scopeBarSelectionChanged() >@@ -260,8 +277,8 @@ WI.CanvasSidebarPanel = class CanvasSidebarPanel extends WI.NavigationSidebarPan > > let recording = objects.find((object) => object instanceof WI.Recording); > if (recording) { >- recording[WI.CanvasSidebarPanel.SelectedActionSymbol] = objects.find((object) => object instanceof WI.RecordingAction); > this.recording = recording; >+ this.action = objects.find((object) => object instanceof WI.RecordingAction); > return; > } > >@@ -294,8 +311,11 @@ WI.CanvasSidebarPanel = class CanvasSidebarPanel extends WI.NavigationSidebarPan > > const onlyExisting = true; > let recordingContentView = this.contentBrowser.contentViewForRepresentedObject(this._recording, onlyExisting); >- if (recordingContentView) >- recordingContentView.updateActionIndex(treeElement.index); >+ if (!recordingContentView) >+ return; >+ >+ this._selectedRecordingActionIndex = treeElement.index; >+ recordingContentView.updateActionIndex(this._selectedRecordingActionIndex); > } > > _canvasChanged() >@@ -326,66 +346,86 @@ WI.CanvasSidebarPanel = class CanvasSidebarPanel extends WI.NavigationSidebarPan > { > this._recordingTreeOutline.removeChildren(); > >- if (!this._recording) >+ if (!this._recording) { >+ if (this._recordingProcessingOptionsContainer) { >+ this._recordingProcessingOptionsContainer.remove(); >+ this._recordingProcessingOptionsContainer = null; >+ } > return; >- >- if (!this._recordingProcessSpinner) { >- this._recordingProcessSpinner = new WI.IndeterminateProgressSpinner; >- this._recordingContentContainer.appendChild(this._recordingProcessSpinner.element); > } > >- this.contentBrowser.showContentViewForRepresentedObject(this._recording); >- >- let recording = this._recording; >- >- let promise = this._recording.process().then(() => { >- if (recording !== this._recording || promise !== this._recordingProcessPromise) >- return; >- >- if (this._recordingProcessSpinner) { >- this._recordingProcessSpinner.element.remove(); >- this._recordingProcessSpinner = null; >+ if (!this._recording.ready) { >+ if (!this._recording.processing) >+ this._recording.startProcessing(); >+ >+ if (!this._recordingProcessingOptionsContainer) { >+ this._recordingProcessingOptionsContainer = this._recordingContentContainer.appendChild(document.createElement("div")); >+ this._recordingProcessingOptionsContainer.classList.add("recording-processing-options"); >+ >+ let createPauseButton = () => { >+ let spinner = new WI.IndeterminateProgressSpinner; >+ this._recordingProcessingOptionsContainer.appendChild(spinner.element); >+ >+ let pauseButton = this._recordingProcessingOptionsContainer.appendChild(document.createElement("button")); >+ pauseButton.textContent = WI.UIString("Pause Processing"); >+ pauseButton.addEventListener("click", (event) => { >+ this._recording.stopProcessing(); >+ >+ spinner.element.remove(); >+ pauseButton.remove(); >+ createResumeButton(); >+ }); >+ }; >+ >+ let createResumeButton = () => { >+ let resumeButton = this._recordingProcessingOptionsContainer.appendChild(document.createElement("button")); >+ resumeButton.textContent = WI.UIString("Resume Processing"); >+ resumeButton.addEventListener("click", (event) => { >+ this._recording.startProcessing(); >+ >+ resumeButton.remove(); >+ createPauseButton(); >+ }); >+ }; >+ >+ if (this._recording.processing) >+ createPauseButton(); >+ else >+ createResumeButton(); > } >+ } > >- this._recordingTreeOutline.element.dataset.indent = Number.countDigits(this._recording.actions.length); >+ this.contentBrowser.showContentViewForRepresentedObject(this._recording); > >- if (this._recording.actions[0] instanceof WI.RecordingInitialStateAction) >- this._recordingTreeOutline.appendChild(new WI.RecordingActionTreeElement(this._recording.actions[0], 0, this._recording.type)); >+ if (this._scopeBar) { >+ let scopeBarItem = this._scopeBar.item(this._recording.displayName); >+ console.assert(scopeBarItem, "Missing scopeBarItem for recording.", this._recording); >+ scopeBarItem.selected = true; >+ } > >- let cumulativeActionIndex = 1; >- this._recording.frames.forEach((frame, frameIndex) => { >- let folder = new WI.FolderTreeElement(WI.UIString("Frame %d").format((frameIndex + 1).toLocaleString())); >- this._recordingTreeOutline.appendChild(folder); >+ if (this._recording.actions[0].ready) { >+ this._recordingTreeOutline.appendChild(new WI.RecordingActionTreeElement(this._recording.actions[0], 0, this._recording.type)); > >- for (let i = 0; i < frame.actions.length; ++i) >- folder.appendChild(new WI.RecordingActionTreeElement(frame.actions[i], cumulativeActionIndex + i, this._recording.type)); >+ if (!this._recording[WI.CanvasSidebarPanel.SelectedActionSymbol]) >+ this.action = this._recording.actions[0]; >+ } > >- if (!isNaN(frame.duration)) { >- const higherResolution = true; >- folder.status = Number.secondsToString(frame.duration / 1000, higherResolution); >- } >+ let cumulativeActionIndex = 0; >+ this._recording.frames.forEach((frame, frameIndex) => { >+ if (!frame.actions[0].ready) >+ return; > >- if (frame.incomplete) >- folder.subtitle = WI.UIString("Incomplete"); >+ let folder = this._createRecordingFrameTreeElement(frame, frameIndex, this._recordingTreeOutline); > >- if (this._recording.frames.length === 1) >- folder.expand(); >+ for (let action of frame.actions) { >+ if (!action.ready) >+ break; > >- cumulativeActionIndex += frame.actions.length; >- }); >+ ++cumulativeActionIndex; > >- if (this._scopeBar) { >- let scopeBarItem = this._scopeBar.item(this._recording.displayName); >- console.assert(scopeBarItem, "Missing scopeBarItem for recording.", this._recording); >- scopeBarItem.selected = true; >+ this._createRecordingActionTreeElement(action, cumulativeActionIndex, folder); > } >- >- this.action = this._recording[WI.CanvasSidebarPanel.SelectedActionSymbol] || this._recording.actions[0]; >- >- this._recordingProcessPromise = null; > }); >- >- this._recordingProcessPromise = promise; > } > > _updateRecordNavigationItem() >@@ -444,6 +484,79 @@ WI.CanvasSidebarPanel = class CanvasSidebarPanel extends WI.NavigationSidebarPan > this._scopeBar.addEventListener(WI.ScopeBar.Event.SelectionChanged, this._scopeBarSelectionChanged, this); > this._recordingNavigationBar.insertNavigationItem(this._scopeBar, 0); > } >+ >+ _createRecordingFrameTreeElement(frame, index, parent) >+ { >+ let folder = new WI.FolderTreeElement(WI.UIString("Frame %d").format((index + 1).toLocaleString()), frame); >+ >+ if (!isNaN(frame.duration)) { >+ const higherResolution = true; >+ folder.status = Number.secondsToString(frame.duration / 1000, higherResolution); >+ } >+ >+ parent.appendChild(folder); >+ >+ return folder; >+ } >+ >+ _createRecordingActionTreeElement(action, index, parent) >+ { >+ let treeElement = new WI.RecordingActionTreeElement(action, index, this._recording.type); >+ >+ parent.appendChild(treeElement); >+ >+ if (parent instanceof WI.FolderTreeElement && parent.representedObject instanceof WI.RecordingFrame) { >+ if (action !== parent.representedObject.actions.lastValue) { >+ parent.addClassName("processing"); >+ >+ if (!(parent.subtitle instanceof HTMLProgressElement)) >+ parent.subtitle = document.createElement("progress"); >+ >+ if (parent.statusElement) >+ parent.subtitle.style.setProperty("width", `calc(100% - ${parent.statusElement.offsetWidth + 4}px`); >+ >+ parent.subtitle.value = parent.representedObject.actions.indexOf(action) / parent.representedObject.actions.length; >+ } else { >+ parent.removeClassName("processing"); >+ if (parent.representedObject.incomplete) >+ parent.subtitle = WI.UIString("Incomplete"); >+ else >+ parent.subtitle = ""; >+ } >+ } >+ >+ if (action === this._recording[WI.CanvasSidebarPanel.SelectedActionSymbol]) >+ this.action = action; >+ >+ return treeElement; >+ } >+ >+ _handleRecordingProcessedAction(event) >+ { >+ let {action, index} = event.data; >+ >+ this._recordingTreeOutline.element.dataset.indent = Number.countDigits(index); >+ >+ let isInitialStateAction = !index; >+ >+ console.assert(isInitialStateAction || this._recordingTreeOutline.children.lastValue instanceof WI.FolderTreeElement, "There should be a WI.FolderTreeElement for the frame for this action."); >+ this._createRecordingActionTreeElement(action, index, isInitialStateAction ? this._recordingTreeOutline : this._recordingTreeOutline.children.lastValue); >+ >+ if (isInitialStateAction && !this._recording[WI.CanvasSidebarPanel.SelectedActionSymbol]) >+ this.action = action; >+ >+ if (action === this._recording.actions.lastValue && this._recordingProcessingOptionsContainer) { >+ this._recordingProcessingOptionsContainer.remove(); >+ this._recordingProcessingOptionsContainer = null; >+ } >+ } >+ >+ _handleRecordingProcessedFrame(event) >+ { >+ let {frame, index} = event.data; >+ >+ this._createRecordingFrameTreeElement(frame, index, this._recordingTreeOutline); >+ } > }; > > WI.CanvasSidebarPanel.SelectedActionSymbol = Symbol("selected-action"); >diff --git a/Source/WebInspectorUI/UserInterface/Views/FolderTreeElement.js b/Source/WebInspectorUI/UserInterface/Views/FolderTreeElement.js >index db099f3bd9bb40a8ebc51571ae10182e91d8244c..e05e2e037fe226d7e7d1116e82a82a17700cace1 100644 >--- a/Source/WebInspectorUI/UserInterface/Views/FolderTreeElement.js >+++ b/Source/WebInspectorUI/UserInterface/Views/FolderTreeElement.js >@@ -27,8 +27,6 @@ WI.FolderTreeElement = class FolderTreeElement extends WI.GeneralTreeElement > { > constructor(title, representedObject) > { >- console.assert(!representedObject || representedObject instanceof WI.Collection); >- > const classNames = [WI.FolderTreeElement.FolderIconStyleClassName]; > const subtitle = null; > super(classNames, title, subtitle, representedObject, {hasChildren: true}); >diff --git a/Source/WebInspectorUI/UserInterface/Views/GeneralTreeElement.js b/Source/WebInspectorUI/UserInterface/Views/GeneralTreeElement.js >index 4eb53d3df13d27e82c5f8c487ee963c32da12664..8626e83088ce7bffdbdc60fea1133d51f8cce4f5 100644 >--- a/Source/WebInspectorUI/UserInterface/Views/GeneralTreeElement.js >+++ b/Source/WebInspectorUI/UserInterface/Views/GeneralTreeElement.js >@@ -50,6 +50,11 @@ WI.GeneralTreeElement = class GeneralTreeElement extends WI.TreeElement > return this._iconElement; > } > >+ get statusElement() >+ { >+ return this._statusElement; >+ } >+ > get titlesElement() > { > this._createElementsIfNeeded(); >@@ -301,6 +306,7 @@ WI.GeneralTreeElement = class GeneralTreeElement extends WI.TreeElement > this._createSubtitleElementIfNeeded(); > this._subtitleElement.removeChildren(); > this._subtitleElement.appendChild(this._subtitle); >+ this._titlesElement.classList.remove(WI.GeneralTreeElement.NoSubtitleStyleClassName); > } else { > if (this._subtitleElement) > this._subtitleElement.textContent = ""; >diff --git a/Source/WebInspectorUI/UserInterface/Views/RecordingContentView.js b/Source/WebInspectorUI/UserInterface/Views/RecordingContentView.js >index 97d6c7b9650961504555a137792c6ce6e58e9936..4a8e010cfa0b96e9823907cdbd4965938e29bd86 100644 >--- a/Source/WebInspectorUI/UserInterface/Views/RecordingContentView.js >+++ b/Source/WebInspectorUI/UserInterface/Views/RecordingContentView.js >@@ -61,9 +61,6 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView > this._exportButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.High; > this._exportButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, () => { this._exportRecording(); }); > } >- >- this._processing = true; >- this._processMessageTextView = null; > } > > // Static >@@ -123,9 +120,6 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView > > this._index = index; > >- if (this._processing) >- return; >- > this._updateSliderValue(); > > if (this.representedObject.type === WI.Recording.Type.Canvas2D) >@@ -178,7 +172,7 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView > let previewHeader = this.element.appendChild(document.createElement("header")); > > let sliderContainer = previewHeader.appendChild(document.createElement("div")); >- sliderContainer.className = "slider-container hidden"; >+ sliderContainer.className = "slider-container"; > > this._previewContainer = this.element.appendChild(document.createElement("div")); > this._previewContainer.className = "preview-container"; >@@ -192,25 +186,7 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView > this._sliderElement.min = 0; > this._sliderElement.max = 0; > >- this.representedObject.addEventListener(WI.Recording.Event.ProcessedActionSwizzle, this._handleRecordingProcessedActionSwizzle, this); >- this.representedObject.addEventListener(WI.Recording.Event.ProcessedActionApply, this._handleRecordingProcessedActionApply, this); >- >- this.representedObject.process().then(() => { >- if (this._processMessageTextView) >- this._processMessageTextView.remove(); >- >- sliderContainer.classList.remove("hidden"); >- this._sliderElement.max = this.representedObject.visualActionIndexes.length; >- this._updateSliderValue(); >- >- this._processing = false; >- >- let index = this._index; >- if (!isNaN(index)) { >- this._index = NaN; >- this.updateActionIndex(index); >- } >- }); >+ this.representedObject.addEventListener(WI.Recording.Event.ProcessedAction, this._handleRecordingProcessedAction, this); > } > > // Private >@@ -454,7 +430,7 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView > { > let activated = WI.settings.showCanvasPath.value; > >- if (this._showPathButtonNavigationItem.activated !== activated && !this._processing) >+ if (this._showPathButtonNavigationItem.activated !== activated) > this._generateContentCanvas2D(this._index); > > this._showPathButtonNavigationItem.activated = activated; >@@ -486,18 +462,6 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView > this._sliderValueElement.textContent = WI.UIString("%d of %d").format(visualActionIndex, visualActionIndexes.length); > } > >- _updateProcessProgress(message, index) >- { >- if (this._processMessageTextView) >- this._processMessageTextView.remove(); >- >- this._processMessageTextView = WI.createMessageTextView(message); >- this.element.appendChild(this._processMessageTextView); >- >- this._processProgressElement = this._processMessageTextView.appendChild(document.createElement("progress")); >- this._processProgressElement.value = index / this.representedObject.actions.length; >- } >- > _showPathButtonClicked(event) > { > WI.settings.showCanvasPath.value = !this._showPathButtonNavigationItem.activated; >@@ -523,14 +487,10 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView > this.updateActionIndex(index); > } > >- _handleRecordingProcessedActionSwizzle(event) >+ _handleRecordingProcessedAction(event) > { >- this._updateProcessProgress(WI.UIString("Loading Recording"), event.data.index); >- } >- >- _handleRecordingProcessedActionApply(event) >- { >- this._updateProcessProgress(WI.UIString("Processing Recording"), event.data.index); >+ this._sliderElement.max = this.representedObject.visualActionIndexes.length; >+ this._updateSliderValue(); > } > }; > >diff --git a/Source/WebInspectorUI/UserInterface/Views/RecordingNavigationSidebarPanel.css b/Source/WebInspectorUI/UserInterface/Views/RecordingNavigationSidebarPanel.css >deleted file mode 100644 >index 9b6970e4774c842d2e410f4410ed10e4c316cdba..0000000000000000000000000000000000000000 >--- a/Source/WebInspectorUI/UserInterface/Views/RecordingNavigationSidebarPanel.css >+++ /dev/null >@@ -1,40 +0,0 @@ >-/* >- * Copyright (C) 2017 Apple Inc. All rights reserved. >- * >- * Redistribution and use in source and binary forms, with or without >- * modification, are permitted provided that the following conditions >- * are met: >- * 1. Redistributions of source code must retain the above copyright >- * notice, this list of conditions and the following disclaimer. >- * 2. Redistributions in binary form must reproduce the above copyright >- * notice, this list of conditions and the following disclaimer in the >- * documentation and/or other materials provided with the distribution. >- * >- * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' >- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, >- * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR >- * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS >- * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR >- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF >- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS >- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN >- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) >- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF >- * THE POSSIBILITY OF SUCH DAMAGE. >- */ >- >-.sidebar > .panel.navigation.recording > .content { >- top: var(--navigation-bar-height); >-} >- >-.sidebar > .panel.navigation.recording > .content > .tree-outline { >- min-height: 100%; >-} >- >-.sidebar > .panel.navigation.recording > .content > .tree-outline .item.folder-icon > .icon { >- content: url(../Images/RenderingFrame.svg); >-} >- >-.sidebar > .panel.navigation.recording > .content > .tree-outline .item.folder-icon > .status { >- line-height: 16px; >-} >diff --git a/Source/WebInspectorUI/UserInterface/Views/RecordingNavigationSidebarPanel.js b/Source/WebInspectorUI/UserInterface/Views/RecordingNavigationSidebarPanel.js >deleted file mode 100644 >index ee21753ce6d3c8db89cb58baaa7c4b8c14479344..0000000000000000000000000000000000000000 >--- a/Source/WebInspectorUI/UserInterface/Views/RecordingNavigationSidebarPanel.js >+++ /dev/null >@@ -1,188 +0,0 @@ >-/* >- * 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.RecordingNavigationSidebarPanel = class RecordingNavigationSidebarPanel extends WI.NavigationSidebarPanel >-{ >- constructor() >- { >- super("recording", WI.UIString("Recording")); >- >- this.contentTreeOutline.customIndent = true; >- this.contentTreeOutline.registerScrollVirtualizer(this.contentView.element, 20); >- >- this.recording = null; >- >- this._importButton = null; >- this._exportButton = null; >- } >- >- // Public >- >- set recording(recording) >- { >- if (recording === this._recording) >- return; >- >- this.contentTreeOutline.removeChildren(); >- >- this._recording = recording; >- >- this.updateEmptyContentPlaceholder(WI.UIString("No Recording Data")); >- >- if (!this._recording) { >- if (this._exportButton) >- this._exportButton.disabled = true; >- return; >- } >- >- this._recording.actions.then((actions) => { >- if (recording !== this._recording) >- return; >- >- this.contentTreeOutline.element.dataset.indent = Number.countDigits(actions.length); >- >- if (actions[0] instanceof WI.RecordingInitialStateAction) >- this.contentTreeOutline.appendChild(new WI.RecordingActionTreeElement(actions[0], 0, this._recording.type)); >- >- let cumulativeActionIndex = 1; >- this._recording.frames.forEach((frame, frameIndex) => { >- let folder = new WI.FolderTreeElement(WI.UIString("Frame %d").format((frameIndex + 1).toLocaleString())); >- this.contentTreeOutline.appendChild(folder); >- >- for (let i = 0; i < frame.actions.length; ++i) >- folder.appendChild(new WI.RecordingActionTreeElement(frame.actions[i], cumulativeActionIndex + i, this._recording.type)); >- >- if (!isNaN(frame.duration)) { >- const higherResolution = true; >- folder.status = Number.secondsToString(frame.duration / 1000, higherResolution); >- } >- >- if (frame.incomplete) >- folder.subtitle = WI.UIString("Incomplete"); >- >- if (this._recording.frames.length === 1) >- folder.expand(); >- >- cumulativeActionIndex += frame.actions.length; >- }); >- >- this._exportButton.disabled = !actions.length; >- >- let index = this._recording[WI.RecordingNavigationSidebarPanel.SelectedActionIndexSymbol] || 0; >- this.updateActionIndex(index); >- }); >- } >- >- updateActionIndex(index, options = {}) >- { >- if (!this._recording) >- return; >- >- this._recording.actions.then((actions) => { >- let recordingAction = actions[index]; >- console.assert(recordingAction, "Invalid recording action index.", index); >- if (!recordingAction) >- return; >- >- let treeElement = this.contentTreeOutline.findTreeElement(recordingAction); >- console.assert(treeElement, "Missing tree element for recording action.", recordingAction); >- if (!treeElement) >- return; >- >- this._recording[WI.RecordingNavigationSidebarPanel.SelectedActionIndexSymbol] = index; >- >- const omitFocus = false; >- const selectedByUser = false; >- const suppressOnSelect = true; >- const suppressOnDeselect = true; >- treeElement.revealAndSelect(omitFocus, selectedByUser, suppressOnSelect, suppressOnDeselect); >- }); >- } >- >- // Protected >- >- initialLayout() >- { >- super.initialLayout(); >- >- const role = "button"; >- >- const importLabel = WI.UIString("Import"); >- let importNavigationItem = new WI.NavigationItem("recording-import", role, importLabel); >- >- this._importButton = importNavigationItem.element.appendChild(document.createElement("button")); >- this._importButton.textContent = importLabel; >- this._importButton.addEventListener("click", () => { WI.canvasManager.importRecording(); }); >- >- const exportLabel = WI.UIString("Export"); >- let exportNavigationItem = new WI.NavigationItem("recording-export", role, exportLabel); >- >- this._exportButton = exportNavigationItem.element.appendChild(document.createElement("button")); >- this._exportButton.textContent = exportLabel; >- this._exportButton.disabled = !this.contentTreeOutline.children.length; >- this._exportButton.addEventListener("click", this._exportNavigationItemClicked.bind(this)); >- >- const element = null; >- this.addSubview(new WI.NavigationBar(element, [importNavigationItem, exportNavigationItem])); >- >- let filterFunction = (treeElement) => { >- if (!(treeElement instanceof WI.RecordingActionTreeElement)) >- return false; >- >- return treeElement.representedObject.isVisual; >- }; >- >- const activatedByDefault = false; >- const defaultToolTip = WI.UIString("Only show visual actions"); >- const activatedToolTip = WI.UIString("Show all actions"); >- this.filterBar.addFilterBarButton("recording-show-visual-only", filterFunction, activatedByDefault, defaultToolTip, activatedToolTip, "Images/Paint.svg", 15, 15); >- } >- >- matchTreeElementAgainstCustomFilters(treeElement) >- { >- // Keep recording frame tree elements. >- if (treeElement instanceof WI.FolderTreeElement) >- return true; >- >- return super.matchTreeElementAgainstCustomFilters(treeElement); >- } >- >- // Private >- >- _exportNavigationItemClicked(event) >- { >- if (!this._recording || !this.contentBrowser || !this.contentBrowser.currentContentView || !this.contentBrowser.currentContentView.supportsSave) >- return; >- >- const forceSaveAs = true; >- WI.saveDataToFile(this.contentBrowser.currentContentView.saveData, forceSaveAs); >- } >-}; >- >-WI.RecordingNavigationSidebarPanel.SelectedActionIndexSymbol = Symbol("selected-action-index"); >- >-WI.RecordingNavigationSidebarPanel.Event = { >- Import: "recording-navigation-sidebar-panel-import", >-};
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Formatted Diff
|
Diff
Attachments on
bug 185152
:
347468
|
347471
|
347536
|
347538