Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
michael@0 | 1 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
michael@0 | 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 4 | "use strict"; |
michael@0 | 5 | |
michael@0 | 6 | const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; |
michael@0 | 7 | |
michael@0 | 8 | Cu.import("resource://gre/modules/Services.jsm"); |
michael@0 | 9 | Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
michael@0 | 10 | Cu.import("resource:///modules/devtools/SideMenuWidget.jsm"); |
michael@0 | 11 | Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); |
michael@0 | 12 | |
michael@0 | 13 | const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require; |
michael@0 | 14 | const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise; |
michael@0 | 15 | const EventEmitter = require("devtools/toolkit/event-emitter"); |
michael@0 | 16 | const { CallWatcherFront } = require("devtools/server/actors/call-watcher"); |
michael@0 | 17 | const { CanvasFront } = require("devtools/server/actors/canvas"); |
michael@0 | 18 | |
michael@0 | 19 | XPCOMUtils.defineLazyModuleGetter(this, "Task", |
michael@0 | 20 | "resource://gre/modules/Task.jsm"); |
michael@0 | 21 | |
michael@0 | 22 | XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", |
michael@0 | 23 | "resource://gre/modules/PluralForm.jsm"); |
michael@0 | 24 | |
michael@0 | 25 | XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", |
michael@0 | 26 | "resource://gre/modules/FileUtils.jsm"); |
michael@0 | 27 | |
michael@0 | 28 | XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", |
michael@0 | 29 | "resource://gre/modules/NetUtil.jsm"); |
michael@0 | 30 | |
michael@0 | 31 | XPCOMUtils.defineLazyModuleGetter(this, "DevToolsUtils", |
michael@0 | 32 | "resource://gre/modules/devtools/DevToolsUtils.jsm"); |
michael@0 | 33 | |
michael@0 | 34 | // The panel's window global is an EventEmitter firing the following events: |
michael@0 | 35 | const EVENTS = { |
michael@0 | 36 | // When the UI is reset from tab navigation. |
michael@0 | 37 | UI_RESET: "CanvasDebugger:UIReset", |
michael@0 | 38 | |
michael@0 | 39 | // When all the animation frame snapshots are removed by the user. |
michael@0 | 40 | SNAPSHOTS_LIST_CLEARED: "CanvasDebugger:SnapshotsListCleared", |
michael@0 | 41 | |
michael@0 | 42 | // When an animation frame snapshot starts/finishes being recorded. |
michael@0 | 43 | SNAPSHOT_RECORDING_STARTED: "CanvasDebugger:SnapshotRecordingStarted", |
michael@0 | 44 | SNAPSHOT_RECORDING_FINISHED: "CanvasDebugger:SnapshotRecordingFinished", |
michael@0 | 45 | |
michael@0 | 46 | // When an animation frame snapshot was selected and all its data displayed. |
michael@0 | 47 | SNAPSHOT_RECORDING_SELECTED: "CanvasDebugger:SnapshotRecordingSelected", |
michael@0 | 48 | |
michael@0 | 49 | // After all the function calls associated with an animation frame snapshot |
michael@0 | 50 | // are displayed in the UI. |
michael@0 | 51 | CALL_LIST_POPULATED: "CanvasDebugger:CallListPopulated", |
michael@0 | 52 | |
michael@0 | 53 | // After the stack associated with a call in an animation frame snapshot |
michael@0 | 54 | // is displayed in the UI. |
michael@0 | 55 | CALL_STACK_DISPLAYED: "CanvasDebugger:CallStackDisplayed", |
michael@0 | 56 | |
michael@0 | 57 | // After a screenshot associated with a call in an animation frame snapshot |
michael@0 | 58 | // is displayed in the UI. |
michael@0 | 59 | CALL_SCREENSHOT_DISPLAYED: "CanvasDebugger:ScreenshotDisplayed", |
michael@0 | 60 | |
michael@0 | 61 | // After all the thumbnails associated with an animation frame snapshot |
michael@0 | 62 | // are displayed in the UI. |
michael@0 | 63 | THUMBNAILS_DISPLAYED: "CanvasDebugger:ThumbnailsDisplayed", |
michael@0 | 64 | |
michael@0 | 65 | // When a source is shown in the JavaScript Debugger at a specific location. |
michael@0 | 66 | SOURCE_SHOWN_IN_JS_DEBUGGER: "CanvasDebugger:SourceShownInJsDebugger", |
michael@0 | 67 | SOURCE_NOT_FOUND_IN_JS_DEBUGGER: "CanvasDebugger:SourceNotFoundInJsDebugger" |
michael@0 | 68 | }; |
michael@0 | 69 | |
michael@0 | 70 | const HTML_NS = "http://www.w3.org/1999/xhtml"; |
michael@0 | 71 | const STRINGS_URI = "chrome://browser/locale/devtools/canvasdebugger.properties" |
michael@0 | 72 | |
michael@0 | 73 | const SNAPSHOT_START_RECORDING_DELAY = 10; // ms |
michael@0 | 74 | const SNAPSHOT_DATA_EXPORT_MAX_BLOCK = 1000; // ms |
michael@0 | 75 | const SNAPSHOT_DATA_DISPLAY_DELAY = 10; // ms |
michael@0 | 76 | const SCREENSHOT_DISPLAY_DELAY = 100; // ms |
michael@0 | 77 | const STACK_FUNC_INDENTATION = 14; // px |
michael@0 | 78 | |
michael@0 | 79 | // This identifier string is simply used to tentatively ascertain whether or not |
michael@0 | 80 | // a JSON loaded from disk is actually something generated by this tool or not. |
michael@0 | 81 | // It isn't, of course, a definitive verification, but a Good Enoughâ„¢ |
michael@0 | 82 | // approximation before continuing the import. Don't localize this. |
michael@0 | 83 | const CALLS_LIST_SERIALIZER_IDENTIFIER = "Recorded Animation Frame Snapshot"; |
michael@0 | 84 | const CALLS_LIST_SERIALIZER_VERSION = 1; |
michael@0 | 85 | const CALLS_LIST_SLOW_SAVE_DELAY = 100; // ms |
michael@0 | 86 | |
michael@0 | 87 | /** |
michael@0 | 88 | * The current target and the Canvas front, set by this tool's host. |
michael@0 | 89 | */ |
michael@0 | 90 | let gToolbox, gTarget, gFront; |
michael@0 | 91 | |
michael@0 | 92 | /** |
michael@0 | 93 | * Initializes the canvas debugger controller and views. |
michael@0 | 94 | */ |
michael@0 | 95 | function startupCanvasDebugger() { |
michael@0 | 96 | return promise.all([ |
michael@0 | 97 | EventsHandler.initialize(), |
michael@0 | 98 | SnapshotsListView.initialize(), |
michael@0 | 99 | CallsListView.initialize() |
michael@0 | 100 | ]); |
michael@0 | 101 | } |
michael@0 | 102 | |
michael@0 | 103 | /** |
michael@0 | 104 | * Destroys the canvas debugger controller and views. |
michael@0 | 105 | */ |
michael@0 | 106 | function shutdownCanvasDebugger() { |
michael@0 | 107 | return promise.all([ |
michael@0 | 108 | EventsHandler.destroy(), |
michael@0 | 109 | SnapshotsListView.destroy(), |
michael@0 | 110 | CallsListView.destroy() |
michael@0 | 111 | ]); |
michael@0 | 112 | } |
michael@0 | 113 | |
michael@0 | 114 | /** |
michael@0 | 115 | * Functions handling target-related lifetime events. |
michael@0 | 116 | */ |
michael@0 | 117 | let EventsHandler = { |
michael@0 | 118 | /** |
michael@0 | 119 | * Listen for events emitted by the current tab target. |
michael@0 | 120 | */ |
michael@0 | 121 | initialize: function() { |
michael@0 | 122 | this._onTabNavigated = this._onTabNavigated.bind(this); |
michael@0 | 123 | gTarget.on("will-navigate", this._onTabNavigated); |
michael@0 | 124 | gTarget.on("navigate", this._onTabNavigated); |
michael@0 | 125 | }, |
michael@0 | 126 | |
michael@0 | 127 | /** |
michael@0 | 128 | * Remove events emitted by the current tab target. |
michael@0 | 129 | */ |
michael@0 | 130 | destroy: function() { |
michael@0 | 131 | gTarget.off("will-navigate", this._onTabNavigated); |
michael@0 | 132 | gTarget.off("navigate", this._onTabNavigated); |
michael@0 | 133 | }, |
michael@0 | 134 | |
michael@0 | 135 | /** |
michael@0 | 136 | * Called for each location change in the debugged tab. |
michael@0 | 137 | */ |
michael@0 | 138 | _onTabNavigated: function(event) { |
michael@0 | 139 | if (event != "will-navigate") { |
michael@0 | 140 | return; |
michael@0 | 141 | } |
michael@0 | 142 | // Make sure the backend is prepared to handle <canvas> contexts. |
michael@0 | 143 | gFront.setup({ reload: false }); |
michael@0 | 144 | |
michael@0 | 145 | // Reset UI. |
michael@0 | 146 | SnapshotsListView.empty(); |
michael@0 | 147 | CallsListView.empty(); |
michael@0 | 148 | |
michael@0 | 149 | $("#record-snapshot").removeAttribute("checked"); |
michael@0 | 150 | $("#record-snapshot").removeAttribute("disabled"); |
michael@0 | 151 | $("#record-snapshot").hidden = false; |
michael@0 | 152 | |
michael@0 | 153 | $("#reload-notice").hidden = true; |
michael@0 | 154 | $("#empty-notice").hidden = false; |
michael@0 | 155 | $("#import-notice").hidden = true; |
michael@0 | 156 | |
michael@0 | 157 | $("#debugging-pane-contents").hidden = true; |
michael@0 | 158 | $("#screenshot-container").hidden = true; |
michael@0 | 159 | $("#snapshot-filmstrip").hidden = true; |
michael@0 | 160 | |
michael@0 | 161 | window.emit(EVENTS.UI_RESET); |
michael@0 | 162 | } |
michael@0 | 163 | }; |
michael@0 | 164 | |
michael@0 | 165 | /** |
michael@0 | 166 | * Functions handling the recorded animation frame snapshots UI. |
michael@0 | 167 | */ |
michael@0 | 168 | let SnapshotsListView = Heritage.extend(WidgetMethods, { |
michael@0 | 169 | /** |
michael@0 | 170 | * Initialization function, called when the tool is started. |
michael@0 | 171 | */ |
michael@0 | 172 | initialize: function() { |
michael@0 | 173 | this.widget = new SideMenuWidget($("#snapshots-list"), { |
michael@0 | 174 | showArrows: true |
michael@0 | 175 | }); |
michael@0 | 176 | |
michael@0 | 177 | this._onSelect = this._onSelect.bind(this); |
michael@0 | 178 | this._onClearButtonClick = this._onClearButtonClick.bind(this); |
michael@0 | 179 | this._onRecordButtonClick = this._onRecordButtonClick.bind(this); |
michael@0 | 180 | this._onImportButtonClick = this._onImportButtonClick.bind(this); |
michael@0 | 181 | this._onSaveButtonClick = this._onSaveButtonClick.bind(this); |
michael@0 | 182 | |
michael@0 | 183 | this.emptyText = L10N.getStr("noSnapshotsText"); |
michael@0 | 184 | this.widget.addEventListener("select", this._onSelect, false); |
michael@0 | 185 | }, |
michael@0 | 186 | |
michael@0 | 187 | /** |
michael@0 | 188 | * Destruction function, called when the tool is closed. |
michael@0 | 189 | */ |
michael@0 | 190 | destroy: function() { |
michael@0 | 191 | this.widget.removeEventListener("select", this._onSelect, false); |
michael@0 | 192 | }, |
michael@0 | 193 | |
michael@0 | 194 | /** |
michael@0 | 195 | * Adds a snapshot entry to this container. |
michael@0 | 196 | * |
michael@0 | 197 | * @return object |
michael@0 | 198 | * The newly inserted item. |
michael@0 | 199 | */ |
michael@0 | 200 | addSnapshot: function() { |
michael@0 | 201 | let contents = document.createElement("hbox"); |
michael@0 | 202 | contents.className = "snapshot-item"; |
michael@0 | 203 | |
michael@0 | 204 | let thumbnail = document.createElementNS(HTML_NS, "canvas"); |
michael@0 | 205 | thumbnail.className = "snapshot-item-thumbnail"; |
michael@0 | 206 | thumbnail.width = CanvasFront.THUMBNAIL_HEIGHT; |
michael@0 | 207 | thumbnail.height = CanvasFront.THUMBNAIL_HEIGHT; |
michael@0 | 208 | |
michael@0 | 209 | let title = document.createElement("label"); |
michael@0 | 210 | title.className = "plain snapshot-item-title"; |
michael@0 | 211 | title.setAttribute("value", |
michael@0 | 212 | L10N.getFormatStr("snapshotsList.itemLabel", this.itemCount + 1)); |
michael@0 | 213 | |
michael@0 | 214 | let calls = document.createElement("label"); |
michael@0 | 215 | calls.className = "plain snapshot-item-calls"; |
michael@0 | 216 | calls.setAttribute("value", |
michael@0 | 217 | L10N.getStr("snapshotsList.loadingLabel")); |
michael@0 | 218 | |
michael@0 | 219 | let save = document.createElement("label"); |
michael@0 | 220 | save.className = "plain snapshot-item-save"; |
michael@0 | 221 | save.addEventListener("click", this._onSaveButtonClick, false); |
michael@0 | 222 | |
michael@0 | 223 | let spacer = document.createElement("spacer"); |
michael@0 | 224 | spacer.setAttribute("flex", "1"); |
michael@0 | 225 | |
michael@0 | 226 | let footer = document.createElement("hbox"); |
michael@0 | 227 | footer.className = "snapshot-item-footer"; |
michael@0 | 228 | footer.appendChild(save); |
michael@0 | 229 | |
michael@0 | 230 | let details = document.createElement("vbox"); |
michael@0 | 231 | details.className = "snapshot-item-details"; |
michael@0 | 232 | details.appendChild(title); |
michael@0 | 233 | details.appendChild(calls); |
michael@0 | 234 | details.appendChild(spacer); |
michael@0 | 235 | details.appendChild(footer); |
michael@0 | 236 | |
michael@0 | 237 | contents.appendChild(thumbnail); |
michael@0 | 238 | contents.appendChild(details); |
michael@0 | 239 | |
michael@0 | 240 | // Append a recorded snapshot item to this container. |
michael@0 | 241 | return this.push([contents], { |
michael@0 | 242 | attachment: { |
michael@0 | 243 | // The snapshot and function call actors, along with the thumbnails |
michael@0 | 244 | // will be available as soon as recording finishes. |
michael@0 | 245 | actor: null, |
michael@0 | 246 | calls: null, |
michael@0 | 247 | thumbnails: null, |
michael@0 | 248 | screenshot: null |
michael@0 | 249 | } |
michael@0 | 250 | }); |
michael@0 | 251 | }, |
michael@0 | 252 | |
michael@0 | 253 | /** |
michael@0 | 254 | * Customizes a shapshot in this container. |
michael@0 | 255 | * |
michael@0 | 256 | * @param Item snapshotItem |
michael@0 | 257 | * An item inserted via `SnapshotsListView.addSnapshot`. |
michael@0 | 258 | * @param object snapshotActor |
michael@0 | 259 | * The frame snapshot actor received from the backend. |
michael@0 | 260 | * @param object snapshotOverview |
michael@0 | 261 | * Additional data about the snapshot received from the backend. |
michael@0 | 262 | */ |
michael@0 | 263 | customizeSnapshot: function(snapshotItem, snapshotActor, snapshotOverview) { |
michael@0 | 264 | // Make sure the function call actors are stored on the item, |
michael@0 | 265 | // to be used when populating the CallsListView. |
michael@0 | 266 | snapshotItem.attachment.actor = snapshotActor; |
michael@0 | 267 | let functionCalls = snapshotItem.attachment.calls = snapshotOverview.calls; |
michael@0 | 268 | let thumbnails = snapshotItem.attachment.thumbnails = snapshotOverview.thumbnails; |
michael@0 | 269 | let screenshot = snapshotItem.attachment.screenshot = snapshotOverview.screenshot; |
michael@0 | 270 | |
michael@0 | 271 | let lastThumbnail = thumbnails[thumbnails.length - 1]; |
michael@0 | 272 | let { width, height, flipped, pixels } = lastThumbnail; |
michael@0 | 273 | |
michael@0 | 274 | let thumbnailNode = $(".snapshot-item-thumbnail", snapshotItem.target); |
michael@0 | 275 | thumbnailNode.setAttribute("flipped", flipped); |
michael@0 | 276 | drawImage(thumbnailNode, width, height, pixels, { centered: true }); |
michael@0 | 277 | |
michael@0 | 278 | let callsNode = $(".snapshot-item-calls", snapshotItem.target); |
michael@0 | 279 | let drawCalls = functionCalls.filter(e => CanvasFront.DRAW_CALLS.has(e.name)); |
michael@0 | 280 | |
michael@0 | 281 | let drawCallsStr = PluralForm.get(drawCalls.length, |
michael@0 | 282 | L10N.getStr("snapshotsList.drawCallsLabel")); |
michael@0 | 283 | let funcCallsStr = PluralForm.get(functionCalls.length, |
michael@0 | 284 | L10N.getStr("snapshotsList.functionCallsLabel")); |
michael@0 | 285 | |
michael@0 | 286 | callsNode.setAttribute("value", |
michael@0 | 287 | drawCallsStr.replace("#1", drawCalls.length) + ", " + |
michael@0 | 288 | funcCallsStr.replace("#1", functionCalls.length)); |
michael@0 | 289 | |
michael@0 | 290 | let saveNode = $(".snapshot-item-save", snapshotItem.target); |
michael@0 | 291 | saveNode.setAttribute("disabled", !!snapshotItem.isLoadedFromDisk); |
michael@0 | 292 | saveNode.setAttribute("value", snapshotItem.isLoadedFromDisk |
michael@0 | 293 | ? L10N.getStr("snapshotsList.loadedLabel") |
michael@0 | 294 | : L10N.getStr("snapshotsList.saveLabel")); |
michael@0 | 295 | |
michael@0 | 296 | // Make sure there's always a selected item available. |
michael@0 | 297 | if (!this.selectedItem) { |
michael@0 | 298 | this.selectedIndex = 0; |
michael@0 | 299 | } |
michael@0 | 300 | }, |
michael@0 | 301 | |
michael@0 | 302 | /** |
michael@0 | 303 | * The select listener for this container. |
michael@0 | 304 | */ |
michael@0 | 305 | _onSelect: function({ detail: snapshotItem }) { |
michael@0 | 306 | if (!snapshotItem) { |
michael@0 | 307 | return; |
michael@0 | 308 | } |
michael@0 | 309 | let { calls, thumbnails, screenshot } = snapshotItem.attachment; |
michael@0 | 310 | |
michael@0 | 311 | $("#reload-notice").hidden = true; |
michael@0 | 312 | $("#empty-notice").hidden = true; |
michael@0 | 313 | $("#import-notice").hidden = false; |
michael@0 | 314 | |
michael@0 | 315 | $("#debugging-pane-contents").hidden = true; |
michael@0 | 316 | $("#screenshot-container").hidden = true; |
michael@0 | 317 | $("#snapshot-filmstrip").hidden = true; |
michael@0 | 318 | |
michael@0 | 319 | Task.spawn(function*() { |
michael@0 | 320 | // Wait for a few milliseconds between presenting the function calls, |
michael@0 | 321 | // screenshot and thumbnails, to allow each component being |
michael@0 | 322 | // sequentially drawn. This gives the illusion of snappiness. |
michael@0 | 323 | |
michael@0 | 324 | yield DevToolsUtils.waitForTime(SNAPSHOT_DATA_DISPLAY_DELAY); |
michael@0 | 325 | CallsListView.showCalls(calls); |
michael@0 | 326 | $("#debugging-pane-contents").hidden = false; |
michael@0 | 327 | $("#import-notice").hidden = true; |
michael@0 | 328 | |
michael@0 | 329 | yield DevToolsUtils.waitForTime(SNAPSHOT_DATA_DISPLAY_DELAY); |
michael@0 | 330 | CallsListView.showThumbnails(thumbnails); |
michael@0 | 331 | $("#snapshot-filmstrip").hidden = false; |
michael@0 | 332 | |
michael@0 | 333 | yield DevToolsUtils.waitForTime(SNAPSHOT_DATA_DISPLAY_DELAY); |
michael@0 | 334 | CallsListView.showScreenshot(screenshot); |
michael@0 | 335 | $("#screenshot-container").hidden = false; |
michael@0 | 336 | |
michael@0 | 337 | window.emit(EVENTS.SNAPSHOT_RECORDING_SELECTED); |
michael@0 | 338 | }); |
michael@0 | 339 | }, |
michael@0 | 340 | |
michael@0 | 341 | /** |
michael@0 | 342 | * The click listener for the "clear" button in this container. |
michael@0 | 343 | */ |
michael@0 | 344 | _onClearButtonClick: function() { |
michael@0 | 345 | Task.spawn(function*() { |
michael@0 | 346 | SnapshotsListView.empty(); |
michael@0 | 347 | CallsListView.empty(); |
michael@0 | 348 | |
michael@0 | 349 | $("#reload-notice").hidden = true; |
michael@0 | 350 | $("#empty-notice").hidden = true; |
michael@0 | 351 | $("#import-notice").hidden = true; |
michael@0 | 352 | |
michael@0 | 353 | if (yield gFront.isInitialized()) { |
michael@0 | 354 | $("#empty-notice").hidden = false; |
michael@0 | 355 | } else { |
michael@0 | 356 | $("#reload-notice").hidden = false; |
michael@0 | 357 | } |
michael@0 | 358 | |
michael@0 | 359 | $("#debugging-pane-contents").hidden = true; |
michael@0 | 360 | $("#screenshot-container").hidden = true; |
michael@0 | 361 | $("#snapshot-filmstrip").hidden = true; |
michael@0 | 362 | |
michael@0 | 363 | window.emit(EVENTS.SNAPSHOTS_LIST_CLEARED); |
michael@0 | 364 | }); |
michael@0 | 365 | }, |
michael@0 | 366 | |
michael@0 | 367 | /** |
michael@0 | 368 | * The click listener for the "record" button in this container. |
michael@0 | 369 | */ |
michael@0 | 370 | _onRecordButtonClick: function() { |
michael@0 | 371 | Task.spawn(function*() { |
michael@0 | 372 | $("#record-snapshot").setAttribute("checked", "true"); |
michael@0 | 373 | $("#record-snapshot").setAttribute("disabled", "true"); |
michael@0 | 374 | |
michael@0 | 375 | // Insert a "dummy" snapshot item in the view, to hint that recording |
michael@0 | 376 | // has now started. However, wait for a few milliseconds before actually |
michael@0 | 377 | // starting the recording, since that might block rendering and prevent |
michael@0 | 378 | // the dummy snapshot item from being drawn. |
michael@0 | 379 | let snapshotItem = this.addSnapshot(); |
michael@0 | 380 | |
michael@0 | 381 | // If this is the first item, immediately show the "Loading…" notice. |
michael@0 | 382 | if (this.itemCount == 1) { |
michael@0 | 383 | $("#empty-notice").hidden = true; |
michael@0 | 384 | $("#import-notice").hidden = false; |
michael@0 | 385 | } |
michael@0 | 386 | |
michael@0 | 387 | yield DevToolsUtils.waitForTime(SNAPSHOT_START_RECORDING_DELAY); |
michael@0 | 388 | window.emit(EVENTS.SNAPSHOT_RECORDING_STARTED); |
michael@0 | 389 | |
michael@0 | 390 | let snapshotActor = yield gFront.recordAnimationFrame(); |
michael@0 | 391 | let snapshotOverview = yield snapshotActor.getOverview(); |
michael@0 | 392 | this.customizeSnapshot(snapshotItem, snapshotActor, snapshotOverview); |
michael@0 | 393 | |
michael@0 | 394 | $("#record-snapshot").removeAttribute("checked"); |
michael@0 | 395 | $("#record-snapshot").removeAttribute("disabled"); |
michael@0 | 396 | |
michael@0 | 397 | window.emit(EVENTS.SNAPSHOT_RECORDING_FINISHED); |
michael@0 | 398 | }.bind(this)); |
michael@0 | 399 | }, |
michael@0 | 400 | |
michael@0 | 401 | /** |
michael@0 | 402 | * The click listener for the "import" button in this container. |
michael@0 | 403 | */ |
michael@0 | 404 | _onImportButtonClick: function() { |
michael@0 | 405 | let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); |
michael@0 | 406 | fp.init(window, L10N.getStr("snapshotsList.saveDialogTitle"), Ci.nsIFilePicker.modeOpen); |
michael@0 | 407 | fp.appendFilter(L10N.getStr("snapshotsList.saveDialogJSONFilter"), "*.json"); |
michael@0 | 408 | fp.appendFilter(L10N.getStr("snapshotsList.saveDialogAllFilter"), "*.*"); |
michael@0 | 409 | |
michael@0 | 410 | if (fp.show() != Ci.nsIFilePicker.returnOK) { |
michael@0 | 411 | return; |
michael@0 | 412 | } |
michael@0 | 413 | |
michael@0 | 414 | let channel = NetUtil.newChannel(fp.file); |
michael@0 | 415 | channel.contentType = "text/plain"; |
michael@0 | 416 | |
michael@0 | 417 | NetUtil.asyncFetch(channel, (inputStream, status) => { |
michael@0 | 418 | if (!Components.isSuccessCode(status)) { |
michael@0 | 419 | console.error("Could not import recorded animation frame snapshot file."); |
michael@0 | 420 | return; |
michael@0 | 421 | } |
michael@0 | 422 | try { |
michael@0 | 423 | let string = NetUtil.readInputStreamToString(inputStream, inputStream.available()); |
michael@0 | 424 | var data = JSON.parse(string); |
michael@0 | 425 | } catch (e) { |
michael@0 | 426 | console.error("Could not read animation frame snapshot file."); |
michael@0 | 427 | return; |
michael@0 | 428 | } |
michael@0 | 429 | if (data.fileType != CALLS_LIST_SERIALIZER_IDENTIFIER) { |
michael@0 | 430 | console.error("Unrecognized animation frame snapshot file."); |
michael@0 | 431 | return; |
michael@0 | 432 | } |
michael@0 | 433 | |
michael@0 | 434 | // Add a `isLoadedFromDisk` flag on everything to avoid sending invalid |
michael@0 | 435 | // requests to the backend, since we're not dealing with actors anymore. |
michael@0 | 436 | let snapshotItem = this.addSnapshot(); |
michael@0 | 437 | snapshotItem.isLoadedFromDisk = true; |
michael@0 | 438 | data.calls.forEach(e => e.isLoadedFromDisk = true); |
michael@0 | 439 | |
michael@0 | 440 | // Create array buffers from the parsed pixel arrays. |
michael@0 | 441 | for (let thumbnail of data.thumbnails) { |
michael@0 | 442 | let thumbnailPixelsArray = thumbnail.pixels.split(","); |
michael@0 | 443 | thumbnail.pixels = new Uint32Array(thumbnailPixelsArray); |
michael@0 | 444 | } |
michael@0 | 445 | let screenshotPixelsArray = data.screenshot.pixels.split(","); |
michael@0 | 446 | data.screenshot.pixels = new Uint32Array(screenshotPixelsArray); |
michael@0 | 447 | |
michael@0 | 448 | this.customizeSnapshot(snapshotItem, data.calls, data); |
michael@0 | 449 | }); |
michael@0 | 450 | }, |
michael@0 | 451 | |
michael@0 | 452 | /** |
michael@0 | 453 | * The click listener for the "save" button of each item in this container. |
michael@0 | 454 | */ |
michael@0 | 455 | _onSaveButtonClick: function(e) { |
michael@0 | 456 | let snapshotItem = this.getItemForElement(e.target); |
michael@0 | 457 | |
michael@0 | 458 | let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); |
michael@0 | 459 | fp.init(window, L10N.getStr("snapshotsList.saveDialogTitle"), Ci.nsIFilePicker.modeSave); |
michael@0 | 460 | fp.appendFilter(L10N.getStr("snapshotsList.saveDialogJSONFilter"), "*.json"); |
michael@0 | 461 | fp.appendFilter(L10N.getStr("snapshotsList.saveDialogAllFilter"), "*.*"); |
michael@0 | 462 | fp.defaultString = "snapshot.json"; |
michael@0 | 463 | |
michael@0 | 464 | // Start serializing all the function call actors for the specified snapshot, |
michael@0 | 465 | // while the nsIFilePicker dialog is being opened. Snappy. |
michael@0 | 466 | let serialized = Task.spawn(function*() { |
michael@0 | 467 | let data = { |
michael@0 | 468 | fileType: CALLS_LIST_SERIALIZER_IDENTIFIER, |
michael@0 | 469 | version: CALLS_LIST_SERIALIZER_VERSION, |
michael@0 | 470 | calls: [], |
michael@0 | 471 | thumbnails: [], |
michael@0 | 472 | screenshot: null |
michael@0 | 473 | }; |
michael@0 | 474 | let functionCalls = snapshotItem.attachment.calls; |
michael@0 | 475 | let thumbnails = snapshotItem.attachment.thumbnails; |
michael@0 | 476 | let screenshot = snapshotItem.attachment.screenshot; |
michael@0 | 477 | |
michael@0 | 478 | // Prepare all the function calls for serialization. |
michael@0 | 479 | yield DevToolsUtils.yieldingEach(functionCalls, (call, i) => { |
michael@0 | 480 | let { type, name, file, line, argsPreview, callerPreview } = call; |
michael@0 | 481 | return call.getDetails().then(({ stack }) => { |
michael@0 | 482 | data.calls[i] = { |
michael@0 | 483 | type: type, |
michael@0 | 484 | name: name, |
michael@0 | 485 | file: file, |
michael@0 | 486 | line: line, |
michael@0 | 487 | stack: stack, |
michael@0 | 488 | argsPreview: argsPreview, |
michael@0 | 489 | callerPreview: callerPreview |
michael@0 | 490 | }; |
michael@0 | 491 | }); |
michael@0 | 492 | }); |
michael@0 | 493 | |
michael@0 | 494 | // Prepare all the thumbnails for serialization. |
michael@0 | 495 | yield DevToolsUtils.yieldingEach(thumbnails, (thumbnail, i) => { |
michael@0 | 496 | let { index, width, height, flipped, pixels } = thumbnail; |
michael@0 | 497 | data.thumbnails.push({ |
michael@0 | 498 | index: index, |
michael@0 | 499 | width: width, |
michael@0 | 500 | height: height, |
michael@0 | 501 | flipped: flipped, |
michael@0 | 502 | pixels: Array.join(pixels, ",") |
michael@0 | 503 | }); |
michael@0 | 504 | }); |
michael@0 | 505 | |
michael@0 | 506 | // Prepare the screenshot for serialization. |
michael@0 | 507 | let { index, width, height, flipped, pixels } = screenshot; |
michael@0 | 508 | data.screenshot = { |
michael@0 | 509 | index: index, |
michael@0 | 510 | width: width, |
michael@0 | 511 | height: height, |
michael@0 | 512 | flipped: flipped, |
michael@0 | 513 | pixels: Array.join(pixels, ",") |
michael@0 | 514 | }; |
michael@0 | 515 | |
michael@0 | 516 | let string = JSON.stringify(data); |
michael@0 | 517 | let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. |
michael@0 | 518 | createInstance(Ci.nsIScriptableUnicodeConverter); |
michael@0 | 519 | |
michael@0 | 520 | converter.charset = "UTF-8"; |
michael@0 | 521 | return converter.convertToInputStream(string); |
michael@0 | 522 | }); |
michael@0 | 523 | |
michael@0 | 524 | // Open the nsIFilePicker and wait for the function call actors to finish |
michael@0 | 525 | // being serialized, in order to save the generated JSON data to disk. |
michael@0 | 526 | fp.open({ done: result => { |
michael@0 | 527 | if (result == Ci.nsIFilePicker.returnCancel) { |
michael@0 | 528 | return; |
michael@0 | 529 | } |
michael@0 | 530 | let footer = $(".snapshot-item-footer", snapshotItem.target); |
michael@0 | 531 | let save = $(".snapshot-item-save", snapshotItem.target); |
michael@0 | 532 | |
michael@0 | 533 | // Show a throbber and a "Saving…" label if serializing isn't immediate. |
michael@0 | 534 | setNamedTimeout("call-list-save", CALLS_LIST_SLOW_SAVE_DELAY, () => { |
michael@0 | 535 | footer.setAttribute("saving", ""); |
michael@0 | 536 | save.setAttribute("disabled", "true"); |
michael@0 | 537 | save.setAttribute("value", L10N.getStr("snapshotsList.savingLabel")); |
michael@0 | 538 | }); |
michael@0 | 539 | |
michael@0 | 540 | serialized.then(inputStream => { |
michael@0 | 541 | let outputStream = FileUtils.openSafeFileOutputStream(fp.file); |
michael@0 | 542 | |
michael@0 | 543 | NetUtil.asyncCopy(inputStream, outputStream, status => { |
michael@0 | 544 | if (!Components.isSuccessCode(status)) { |
michael@0 | 545 | console.error("Could not save recorded animation frame snapshot file."); |
michael@0 | 546 | } |
michael@0 | 547 | clearNamedTimeout("call-list-save"); |
michael@0 | 548 | footer.removeAttribute("saving"); |
michael@0 | 549 | save.removeAttribute("disabled"); |
michael@0 | 550 | save.setAttribute("value", L10N.getStr("snapshotsList.saveLabel")); |
michael@0 | 551 | }); |
michael@0 | 552 | }); |
michael@0 | 553 | }}); |
michael@0 | 554 | } |
michael@0 | 555 | }); |
michael@0 | 556 | |
michael@0 | 557 | /** |
michael@0 | 558 | * Functions handling details about a single recorded animation frame snapshot |
michael@0 | 559 | * (the calls list, rendering preview, thumbnails filmstrip etc.). |
michael@0 | 560 | */ |
michael@0 | 561 | let CallsListView = Heritage.extend(WidgetMethods, { |
michael@0 | 562 | /** |
michael@0 | 563 | * Initialization function, called when the tool is started. |
michael@0 | 564 | */ |
michael@0 | 565 | initialize: function() { |
michael@0 | 566 | this.widget = new SideMenuWidget($("#calls-list")); |
michael@0 | 567 | this._slider = $("#calls-slider"); |
michael@0 | 568 | this._searchbox = $("#calls-searchbox"); |
michael@0 | 569 | this._filmstrip = $("#snapshot-filmstrip"); |
michael@0 | 570 | |
michael@0 | 571 | this._onSelect = this._onSelect.bind(this); |
michael@0 | 572 | this._onSlideMouseDown = this._onSlideMouseDown.bind(this); |
michael@0 | 573 | this._onSlideMouseUp = this._onSlideMouseUp.bind(this); |
michael@0 | 574 | this._onSlide = this._onSlide.bind(this); |
michael@0 | 575 | this._onSearch = this._onSearch.bind(this); |
michael@0 | 576 | this._onScroll = this._onScroll.bind(this); |
michael@0 | 577 | this._onExpand = this._onExpand.bind(this); |
michael@0 | 578 | this._onStackFileClick = this._onStackFileClick.bind(this); |
michael@0 | 579 | this._onThumbnailClick = this._onThumbnailClick.bind(this); |
michael@0 | 580 | |
michael@0 | 581 | this.widget.addEventListener("select", this._onSelect, false); |
michael@0 | 582 | this._slider.addEventListener("mousedown", this._onSlideMouseDown, false); |
michael@0 | 583 | this._slider.addEventListener("mouseup", this._onSlideMouseUp, false); |
michael@0 | 584 | this._slider.addEventListener("change", this._onSlide, false); |
michael@0 | 585 | this._searchbox.addEventListener("input", this._onSearch, false); |
michael@0 | 586 | this._filmstrip.addEventListener("wheel", this._onScroll, false); |
michael@0 | 587 | }, |
michael@0 | 588 | |
michael@0 | 589 | /** |
michael@0 | 590 | * Destruction function, called when the tool is closed. |
michael@0 | 591 | */ |
michael@0 | 592 | destroy: function() { |
michael@0 | 593 | this.widget.removeEventListener("select", this._onSelect, false); |
michael@0 | 594 | this._slider.removeEventListener("mousedown", this._onSlideMouseDown, false); |
michael@0 | 595 | this._slider.removeEventListener("mouseup", this._onSlideMouseUp, false); |
michael@0 | 596 | this._slider.removeEventListener("change", this._onSlide, false); |
michael@0 | 597 | this._searchbox.removeEventListener("input", this._onSearch, false); |
michael@0 | 598 | this._filmstrip.removeEventListener("wheel", this._onScroll, false); |
michael@0 | 599 | }, |
michael@0 | 600 | |
michael@0 | 601 | /** |
michael@0 | 602 | * Populates this container with a list of function calls. |
michael@0 | 603 | * |
michael@0 | 604 | * @param array functionCalls |
michael@0 | 605 | * A list of function call actors received from the backend. |
michael@0 | 606 | */ |
michael@0 | 607 | showCalls: function(functionCalls) { |
michael@0 | 608 | this.empty(); |
michael@0 | 609 | |
michael@0 | 610 | for (let i = 0, len = functionCalls.length; i < len; i++) { |
michael@0 | 611 | let call = functionCalls[i]; |
michael@0 | 612 | |
michael@0 | 613 | let view = document.createElement("vbox"); |
michael@0 | 614 | view.className = "call-item-view devtools-monospace"; |
michael@0 | 615 | view.setAttribute("flex", "1"); |
michael@0 | 616 | |
michael@0 | 617 | let contents = document.createElement("hbox"); |
michael@0 | 618 | contents.className = "call-item-contents"; |
michael@0 | 619 | contents.setAttribute("align", "center"); |
michael@0 | 620 | contents.addEventListener("dblclick", this._onExpand); |
michael@0 | 621 | view.appendChild(contents); |
michael@0 | 622 | |
michael@0 | 623 | let index = document.createElement("label"); |
michael@0 | 624 | index.className = "plain call-item-index"; |
michael@0 | 625 | index.setAttribute("flex", "1"); |
michael@0 | 626 | index.setAttribute("value", i + 1); |
michael@0 | 627 | |
michael@0 | 628 | let gutter = document.createElement("hbox"); |
michael@0 | 629 | gutter.className = "call-item-gutter"; |
michael@0 | 630 | gutter.appendChild(index); |
michael@0 | 631 | contents.appendChild(gutter); |
michael@0 | 632 | |
michael@0 | 633 | // Not all function calls have a caller that was stringified (e.g. |
michael@0 | 634 | // context calls have a "gl" or "ctx" caller preview). |
michael@0 | 635 | if (call.callerPreview) { |
michael@0 | 636 | let context = document.createElement("label"); |
michael@0 | 637 | context.className = "plain call-item-context"; |
michael@0 | 638 | context.setAttribute("value", call.callerPreview); |
michael@0 | 639 | contents.appendChild(context); |
michael@0 | 640 | |
michael@0 | 641 | let separator = document.createElement("label"); |
michael@0 | 642 | separator.className = "plain call-item-separator"; |
michael@0 | 643 | separator.setAttribute("value", "."); |
michael@0 | 644 | contents.appendChild(separator); |
michael@0 | 645 | } |
michael@0 | 646 | |
michael@0 | 647 | let name = document.createElement("label"); |
michael@0 | 648 | name.className = "plain call-item-name"; |
michael@0 | 649 | name.setAttribute("value", call.name); |
michael@0 | 650 | contents.appendChild(name); |
michael@0 | 651 | |
michael@0 | 652 | let argsPreview = document.createElement("label"); |
michael@0 | 653 | argsPreview.className = "plain call-item-args"; |
michael@0 | 654 | argsPreview.setAttribute("crop", "end"); |
michael@0 | 655 | argsPreview.setAttribute("flex", "100"); |
michael@0 | 656 | // Getters and setters are displayed differently from regular methods. |
michael@0 | 657 | if (call.type == CallWatcherFront.METHOD_FUNCTION) { |
michael@0 | 658 | argsPreview.setAttribute("value", "(" + call.argsPreview + ")"); |
michael@0 | 659 | } else { |
michael@0 | 660 | argsPreview.setAttribute("value", " = " + call.argsPreview); |
michael@0 | 661 | } |
michael@0 | 662 | contents.appendChild(argsPreview); |
michael@0 | 663 | |
michael@0 | 664 | let location = document.createElement("label"); |
michael@0 | 665 | location.className = "plain call-item-location"; |
michael@0 | 666 | location.setAttribute("value", getFileName(call.file) + ":" + call.line); |
michael@0 | 667 | location.setAttribute("crop", "start"); |
michael@0 | 668 | location.setAttribute("flex", "1"); |
michael@0 | 669 | location.addEventListener("mousedown", this._onExpand); |
michael@0 | 670 | contents.appendChild(location); |
michael@0 | 671 | |
michael@0 | 672 | // Append a function call item to this container. |
michael@0 | 673 | this.push([view], { |
michael@0 | 674 | staged: true, |
michael@0 | 675 | attachment: { |
michael@0 | 676 | actor: call |
michael@0 | 677 | } |
michael@0 | 678 | }); |
michael@0 | 679 | |
michael@0 | 680 | // Highlight certain calls that are probably more interesting than |
michael@0 | 681 | // everything else, making it easier to quickly glance over them. |
michael@0 | 682 | if (CanvasFront.DRAW_CALLS.has(call.name)) { |
michael@0 | 683 | view.setAttribute("draw-call", ""); |
michael@0 | 684 | } |
michael@0 | 685 | if (CanvasFront.INTERESTING_CALLS.has(call.name)) { |
michael@0 | 686 | view.setAttribute("interesting-call", ""); |
michael@0 | 687 | } |
michael@0 | 688 | } |
michael@0 | 689 | |
michael@0 | 690 | // Flushes all the prepared function call items into this container. |
michael@0 | 691 | this.commit(); |
michael@0 | 692 | window.emit(EVENTS.CALL_LIST_POPULATED); |
michael@0 | 693 | |
michael@0 | 694 | // Resetting the function selection slider's value (shown in this |
michael@0 | 695 | // container's toolbar) would trigger a selection event, which should be |
michael@0 | 696 | // ignored in this case. |
michael@0 | 697 | this._ignoreSliderChanges = true; |
michael@0 | 698 | this._slider.value = 0; |
michael@0 | 699 | this._slider.max = functionCalls.length - 1; |
michael@0 | 700 | this._ignoreSliderChanges = false; |
michael@0 | 701 | }, |
michael@0 | 702 | |
michael@0 | 703 | /** |
michael@0 | 704 | * Displays an image in the rendering preview of this container, generated |
michael@0 | 705 | * for the specified draw call in the recorded animation frame snapshot. |
michael@0 | 706 | * |
michael@0 | 707 | * @param array screenshot |
michael@0 | 708 | * A single "snapshot-image" instance received from the backend. |
michael@0 | 709 | */ |
michael@0 | 710 | showScreenshot: function(screenshot) { |
michael@0 | 711 | let { index, width, height, flipped, pixels } = screenshot; |
michael@0 | 712 | |
michael@0 | 713 | let screenshotNode = $("#screenshot-image"); |
michael@0 | 714 | screenshotNode.setAttribute("flipped", flipped); |
michael@0 | 715 | drawBackground("screenshot-rendering", width, height, pixels); |
michael@0 | 716 | |
michael@0 | 717 | let dimensionsNode = $("#screenshot-dimensions"); |
michael@0 | 718 | dimensionsNode.setAttribute("value", ~~width + " x " + ~~height); |
michael@0 | 719 | |
michael@0 | 720 | window.emit(EVENTS.CALL_SCREENSHOT_DISPLAYED); |
michael@0 | 721 | }, |
michael@0 | 722 | |
michael@0 | 723 | /** |
michael@0 | 724 | * Populates this container's footer with a list of thumbnails, one generated |
michael@0 | 725 | * for each draw call in the recorded animation frame snapshot. |
michael@0 | 726 | * |
michael@0 | 727 | * @param array thumbnails |
michael@0 | 728 | * An array of "snapshot-image" instances received from the backend. |
michael@0 | 729 | */ |
michael@0 | 730 | showThumbnails: function(thumbnails) { |
michael@0 | 731 | while (this._filmstrip.hasChildNodes()) { |
michael@0 | 732 | this._filmstrip.firstChild.remove(); |
michael@0 | 733 | } |
michael@0 | 734 | for (let thumbnail of thumbnails) { |
michael@0 | 735 | this.appendThumbnail(thumbnail); |
michael@0 | 736 | } |
michael@0 | 737 | |
michael@0 | 738 | window.emit(EVENTS.THUMBNAILS_DISPLAYED); |
michael@0 | 739 | }, |
michael@0 | 740 | |
michael@0 | 741 | /** |
michael@0 | 742 | * Displays an image in the thumbnails list of this container, generated |
michael@0 | 743 | * for the specified draw call in the recorded animation frame snapshot. |
michael@0 | 744 | * |
michael@0 | 745 | * @param array thumbnail |
michael@0 | 746 | * A single "snapshot-image" instance received from the backend. |
michael@0 | 747 | */ |
michael@0 | 748 | appendThumbnail: function(thumbnail) { |
michael@0 | 749 | let { index, width, height, flipped, pixels } = thumbnail; |
michael@0 | 750 | |
michael@0 | 751 | let thumbnailNode = document.createElementNS(HTML_NS, "canvas"); |
michael@0 | 752 | thumbnailNode.setAttribute("flipped", flipped); |
michael@0 | 753 | thumbnailNode.width = Math.max(CanvasFront.THUMBNAIL_HEIGHT, width); |
michael@0 | 754 | thumbnailNode.height = Math.max(CanvasFront.THUMBNAIL_HEIGHT, height); |
michael@0 | 755 | drawImage(thumbnailNode, width, height, pixels, { centered: true }); |
michael@0 | 756 | |
michael@0 | 757 | thumbnailNode.className = "filmstrip-thumbnail"; |
michael@0 | 758 | thumbnailNode.onmousedown = e => this._onThumbnailClick(e, index); |
michael@0 | 759 | thumbnailNode.setAttribute("index", index); |
michael@0 | 760 | this._filmstrip.appendChild(thumbnailNode); |
michael@0 | 761 | }, |
michael@0 | 762 | |
michael@0 | 763 | /** |
michael@0 | 764 | * Sets the currently highlighted thumbnail in this container. |
michael@0 | 765 | * A screenshot will always correlate to a thumbnail in the filmstrip, |
michael@0 | 766 | * both being identified by the same 'index' of the context function call. |
michael@0 | 767 | * |
michael@0 | 768 | * @param number index |
michael@0 | 769 | * The context function call's index. |
michael@0 | 770 | */ |
michael@0 | 771 | set highlightedThumbnail(index) { |
michael@0 | 772 | let currHighlightedThumbnail = $(".filmstrip-thumbnail[index='" + index + "']"); |
michael@0 | 773 | if (currHighlightedThumbnail == null) { |
michael@0 | 774 | return; |
michael@0 | 775 | } |
michael@0 | 776 | |
michael@0 | 777 | let prevIndex = this._highlightedThumbnailIndex |
michael@0 | 778 | let prevHighlightedThumbnail = $(".filmstrip-thumbnail[index='" + prevIndex + "']"); |
michael@0 | 779 | if (prevHighlightedThumbnail) { |
michael@0 | 780 | prevHighlightedThumbnail.removeAttribute("highlighted"); |
michael@0 | 781 | } |
michael@0 | 782 | |
michael@0 | 783 | currHighlightedThumbnail.setAttribute("highlighted", ""); |
michael@0 | 784 | currHighlightedThumbnail.scrollIntoView(); |
michael@0 | 785 | this._highlightedThumbnailIndex = index; |
michael@0 | 786 | }, |
michael@0 | 787 | |
michael@0 | 788 | /** |
michael@0 | 789 | * Gets the currently highlighted thumbnail in this container. |
michael@0 | 790 | * @return number |
michael@0 | 791 | */ |
michael@0 | 792 | get highlightedThumbnail() { |
michael@0 | 793 | return this._highlightedThumbnailIndex; |
michael@0 | 794 | }, |
michael@0 | 795 | |
michael@0 | 796 | /** |
michael@0 | 797 | * The select listener for this container. |
michael@0 | 798 | */ |
michael@0 | 799 | _onSelect: function({ detail: callItem }) { |
michael@0 | 800 | if (!callItem) { |
michael@0 | 801 | return; |
michael@0 | 802 | } |
michael@0 | 803 | |
michael@0 | 804 | // Some of the stepping buttons don't make sense specifically while the |
michael@0 | 805 | // last function call is selected. |
michael@0 | 806 | if (this.selectedIndex == this.itemCount - 1) { |
michael@0 | 807 | $("#resume").setAttribute("disabled", "true"); |
michael@0 | 808 | $("#step-over").setAttribute("disabled", "true"); |
michael@0 | 809 | $("#step-out").setAttribute("disabled", "true"); |
michael@0 | 810 | } else { |
michael@0 | 811 | $("#resume").removeAttribute("disabled"); |
michael@0 | 812 | $("#step-over").removeAttribute("disabled"); |
michael@0 | 813 | $("#step-out").removeAttribute("disabled"); |
michael@0 | 814 | } |
michael@0 | 815 | |
michael@0 | 816 | // Correlate the currently selected item with the function selection |
michael@0 | 817 | // slider's value. Avoid triggering a redundant selection event. |
michael@0 | 818 | this._ignoreSliderChanges = true; |
michael@0 | 819 | this._slider.value = this.selectedIndex; |
michael@0 | 820 | this._ignoreSliderChanges = false; |
michael@0 | 821 | |
michael@0 | 822 | // Can't generate screenshots for function call actors loaded from disk. |
michael@0 | 823 | // XXX: Bug 984844. |
michael@0 | 824 | if (callItem.attachment.actor.isLoadedFromDisk) { |
michael@0 | 825 | return; |
michael@0 | 826 | } |
michael@0 | 827 | |
michael@0 | 828 | // To keep continuous selection buttery smooth (for example, while pressing |
michael@0 | 829 | // the DOWN key or moving the slider), only display the screenshot after |
michael@0 | 830 | // any kind of user input stops. |
michael@0 | 831 | setConditionalTimeout("screenshot-display", SCREENSHOT_DISPLAY_DELAY, () => { |
michael@0 | 832 | return !this._isSliding; |
michael@0 | 833 | }, () => { |
michael@0 | 834 | let frameSnapshot = SnapshotsListView.selectedItem.attachment.actor |
michael@0 | 835 | let functionCall = callItem.attachment.actor; |
michael@0 | 836 | frameSnapshot.generateScreenshotFor(functionCall).then(screenshot => { |
michael@0 | 837 | this.showScreenshot(screenshot); |
michael@0 | 838 | this.highlightedThumbnail = screenshot.index; |
michael@0 | 839 | }); |
michael@0 | 840 | }); |
michael@0 | 841 | }, |
michael@0 | 842 | |
michael@0 | 843 | /** |
michael@0 | 844 | * The mousedown listener for the call selection slider. |
michael@0 | 845 | */ |
michael@0 | 846 | _onSlideMouseDown: function() { |
michael@0 | 847 | this._isSliding = true; |
michael@0 | 848 | }, |
michael@0 | 849 | |
michael@0 | 850 | /** |
michael@0 | 851 | * The mouseup listener for the call selection slider. |
michael@0 | 852 | */ |
michael@0 | 853 | _onSlideMouseUp: function() { |
michael@0 | 854 | this._isSliding = false; |
michael@0 | 855 | }, |
michael@0 | 856 | |
michael@0 | 857 | /** |
michael@0 | 858 | * The change listener for the call selection slider. |
michael@0 | 859 | */ |
michael@0 | 860 | _onSlide: function() { |
michael@0 | 861 | // Avoid performing any operations when programatically changing the value. |
michael@0 | 862 | if (this._ignoreSliderChanges) { |
michael@0 | 863 | return; |
michael@0 | 864 | } |
michael@0 | 865 | let selectedFunctionCallIndex = this.selectedIndex = this._slider.value; |
michael@0 | 866 | |
michael@0 | 867 | // While sliding, immediately show the most relevant thumbnail for a |
michael@0 | 868 | // function call, for a nice diff-like animation effect between draws. |
michael@0 | 869 | let thumbnails = SnapshotsListView.selectedItem.attachment.thumbnails; |
michael@0 | 870 | let thumbnail = getThumbnailForCall(thumbnails, selectedFunctionCallIndex); |
michael@0 | 871 | |
michael@0 | 872 | // Avoid drawing and highlighting if the selected function call has the |
michael@0 | 873 | // same thumbnail as the last one. |
michael@0 | 874 | if (thumbnail.index == this.highlightedThumbnail) { |
michael@0 | 875 | return; |
michael@0 | 876 | } |
michael@0 | 877 | // If a thumbnail wasn't found (e.g. the backend avoids creating thumbnails |
michael@0 | 878 | // when rendering offscreen), simply defer to the first available one. |
michael@0 | 879 | if (thumbnail.index == -1) { |
michael@0 | 880 | thumbnail = thumbnails[0]; |
michael@0 | 881 | } |
michael@0 | 882 | |
michael@0 | 883 | let { index, width, height, flipped, pixels } = thumbnail; |
michael@0 | 884 | this.highlightedThumbnail = index; |
michael@0 | 885 | |
michael@0 | 886 | let screenshotNode = $("#screenshot-image"); |
michael@0 | 887 | screenshotNode.setAttribute("flipped", flipped); |
michael@0 | 888 | drawBackground("screenshot-rendering", width, height, pixels); |
michael@0 | 889 | }, |
michael@0 | 890 | |
michael@0 | 891 | /** |
michael@0 | 892 | * The input listener for the calls searchbox. |
michael@0 | 893 | */ |
michael@0 | 894 | _onSearch: function(e) { |
michael@0 | 895 | let lowerCaseSearchToken = this._searchbox.value.toLowerCase(); |
michael@0 | 896 | |
michael@0 | 897 | this.filterContents(e => { |
michael@0 | 898 | let call = e.attachment.actor; |
michael@0 | 899 | let name = call.name.toLowerCase(); |
michael@0 | 900 | let file = call.file.toLowerCase(); |
michael@0 | 901 | let line = call.line.toString().toLowerCase(); |
michael@0 | 902 | let args = call.argsPreview.toLowerCase(); |
michael@0 | 903 | |
michael@0 | 904 | return name.contains(lowerCaseSearchToken) || |
michael@0 | 905 | file.contains(lowerCaseSearchToken) || |
michael@0 | 906 | line.contains(lowerCaseSearchToken) || |
michael@0 | 907 | args.contains(lowerCaseSearchToken); |
michael@0 | 908 | }); |
michael@0 | 909 | }, |
michael@0 | 910 | |
michael@0 | 911 | /** |
michael@0 | 912 | * The wheel listener for the filmstrip that contains all the thumbnails. |
michael@0 | 913 | */ |
michael@0 | 914 | _onScroll: function(e) { |
michael@0 | 915 | this._filmstrip.scrollLeft += e.deltaX; |
michael@0 | 916 | }, |
michael@0 | 917 | |
michael@0 | 918 | /** |
michael@0 | 919 | * The click/dblclick listener for an item or location url in this container. |
michael@0 | 920 | * When expanding an item, it's corresponding call stack will be displayed. |
michael@0 | 921 | */ |
michael@0 | 922 | _onExpand: function(e) { |
michael@0 | 923 | let callItem = this.getItemForElement(e.target); |
michael@0 | 924 | let view = $(".call-item-view", callItem.target); |
michael@0 | 925 | |
michael@0 | 926 | // If the call stack nodes were already created, simply re-show them |
michael@0 | 927 | // or jump to the corresponding file and line in the Debugger if a |
michael@0 | 928 | // location link was clicked. |
michael@0 | 929 | if (view.hasAttribute("call-stack-populated")) { |
michael@0 | 930 | let isExpanded = view.getAttribute("call-stack-expanded") == "true"; |
michael@0 | 931 | |
michael@0 | 932 | // If clicking on the location, jump to the Debugger. |
michael@0 | 933 | if (e.target.classList.contains("call-item-location")) { |
michael@0 | 934 | let { file, line } = callItem.attachment.actor; |
michael@0 | 935 | viewSourceInDebugger(file, line); |
michael@0 | 936 | return; |
michael@0 | 937 | } |
michael@0 | 938 | // Otherwise hide the call stack. |
michael@0 | 939 | else { |
michael@0 | 940 | view.setAttribute("call-stack-expanded", !isExpanded); |
michael@0 | 941 | $(".call-item-stack", view).hidden = isExpanded; |
michael@0 | 942 | return; |
michael@0 | 943 | } |
michael@0 | 944 | } |
michael@0 | 945 | |
michael@0 | 946 | let list = document.createElement("vbox"); |
michael@0 | 947 | list.className = "call-item-stack"; |
michael@0 | 948 | view.setAttribute("call-stack-populated", ""); |
michael@0 | 949 | view.setAttribute("call-stack-expanded", "true"); |
michael@0 | 950 | view.appendChild(list); |
michael@0 | 951 | |
michael@0 | 952 | /** |
michael@0 | 953 | * Creates a function call nodes in this container for a stack. |
michael@0 | 954 | */ |
michael@0 | 955 | let display = stack => { |
michael@0 | 956 | for (let i = 1; i < stack.length; i++) { |
michael@0 | 957 | let call = stack[i]; |
michael@0 | 958 | |
michael@0 | 959 | let contents = document.createElement("hbox"); |
michael@0 | 960 | contents.className = "call-item-stack-fn"; |
michael@0 | 961 | contents.style.MozPaddingStart = (i * STACK_FUNC_INDENTATION) + "px"; |
michael@0 | 962 | |
michael@0 | 963 | let name = document.createElement("label"); |
michael@0 | 964 | name.className = "plain call-item-stack-fn-name"; |
michael@0 | 965 | name.setAttribute("value", "↳ " + call.name + "()"); |
michael@0 | 966 | contents.appendChild(name); |
michael@0 | 967 | |
michael@0 | 968 | let spacer = document.createElement("spacer"); |
michael@0 | 969 | spacer.setAttribute("flex", "100"); |
michael@0 | 970 | contents.appendChild(spacer); |
michael@0 | 971 | |
michael@0 | 972 | let location = document.createElement("label"); |
michael@0 | 973 | location.className = "plain call-item-stack-fn-location"; |
michael@0 | 974 | location.setAttribute("value", getFileName(call.file) + ":" + call.line); |
michael@0 | 975 | location.setAttribute("crop", "start"); |
michael@0 | 976 | location.setAttribute("flex", "1"); |
michael@0 | 977 | location.addEventListener("mousedown", e => this._onStackFileClick(e, call)); |
michael@0 | 978 | contents.appendChild(location); |
michael@0 | 979 | |
michael@0 | 980 | list.appendChild(contents); |
michael@0 | 981 | } |
michael@0 | 982 | |
michael@0 | 983 | window.emit(EVENTS.CALL_STACK_DISPLAYED); |
michael@0 | 984 | }; |
michael@0 | 985 | |
michael@0 | 986 | // If this animation snapshot is loaded from disk, there are no corresponding |
michael@0 | 987 | // backend actors available and the data is immediately available. |
michael@0 | 988 | let functionCall = callItem.attachment.actor; |
michael@0 | 989 | if (functionCall.isLoadedFromDisk) { |
michael@0 | 990 | display(functionCall.stack); |
michael@0 | 991 | } |
michael@0 | 992 | // ..otherwise we need to request the function call stack from the backend. |
michael@0 | 993 | else { |
michael@0 | 994 | callItem.attachment.actor.getDetails().then(fn => display(fn.stack)); |
michael@0 | 995 | } |
michael@0 | 996 | }, |
michael@0 | 997 | |
michael@0 | 998 | /** |
michael@0 | 999 | * The click listener for a location link in the call stack. |
michael@0 | 1000 | * |
michael@0 | 1001 | * @param string file |
michael@0 | 1002 | * The url of the source owning the function. |
michael@0 | 1003 | * @param number line |
michael@0 | 1004 | * The line of the respective function. |
michael@0 | 1005 | */ |
michael@0 | 1006 | _onStackFileClick: function(e, { file, line }) { |
michael@0 | 1007 | viewSourceInDebugger(file, line); |
michael@0 | 1008 | }, |
michael@0 | 1009 | |
michael@0 | 1010 | /** |
michael@0 | 1011 | * The click listener for a thumbnail in the filmstrip. |
michael@0 | 1012 | * |
michael@0 | 1013 | * @param number index |
michael@0 | 1014 | * The function index in the recorded animation frame snapshot. |
michael@0 | 1015 | */ |
michael@0 | 1016 | _onThumbnailClick: function(e, index) { |
michael@0 | 1017 | this.selectedIndex = index; |
michael@0 | 1018 | }, |
michael@0 | 1019 | |
michael@0 | 1020 | /** |
michael@0 | 1021 | * The click listener for the "resume" button in this container's toolbar. |
michael@0 | 1022 | */ |
michael@0 | 1023 | _onResume: function() { |
michael@0 | 1024 | // Jump to the next draw call in the recorded animation frame snapshot. |
michael@0 | 1025 | let drawCall = getNextDrawCall(this.items, this.selectedItem); |
michael@0 | 1026 | if (drawCall) { |
michael@0 | 1027 | this.selectedItem = drawCall; |
michael@0 | 1028 | return; |
michael@0 | 1029 | } |
michael@0 | 1030 | |
michael@0 | 1031 | // If there are no more draw calls, just jump to the last context call. |
michael@0 | 1032 | this._onStepOut(); |
michael@0 | 1033 | }, |
michael@0 | 1034 | |
michael@0 | 1035 | /** |
michael@0 | 1036 | * The click listener for the "step over" button in this container's toolbar. |
michael@0 | 1037 | */ |
michael@0 | 1038 | _onStepOver: function() { |
michael@0 | 1039 | this.selectedIndex++; |
michael@0 | 1040 | }, |
michael@0 | 1041 | |
michael@0 | 1042 | /** |
michael@0 | 1043 | * The click listener for the "step in" button in this container's toolbar. |
michael@0 | 1044 | */ |
michael@0 | 1045 | _onStepIn: function() { |
michael@0 | 1046 | if (this.selectedIndex == -1) { |
michael@0 | 1047 | this._onResume(); |
michael@0 | 1048 | return; |
michael@0 | 1049 | } |
michael@0 | 1050 | let callItem = this.selectedItem; |
michael@0 | 1051 | let { file, line } = callItem.attachment.actor; |
michael@0 | 1052 | viewSourceInDebugger(file, line); |
michael@0 | 1053 | }, |
michael@0 | 1054 | |
michael@0 | 1055 | /** |
michael@0 | 1056 | * The click listener for the "step out" button in this container's toolbar. |
michael@0 | 1057 | */ |
michael@0 | 1058 | _onStepOut: function() { |
michael@0 | 1059 | this.selectedIndex = this.itemCount - 1; |
michael@0 | 1060 | } |
michael@0 | 1061 | }); |
michael@0 | 1062 | |
michael@0 | 1063 | /** |
michael@0 | 1064 | * Localization convenience methods. |
michael@0 | 1065 | */ |
michael@0 | 1066 | let L10N = new ViewHelpers.L10N(STRINGS_URI); |
michael@0 | 1067 | |
michael@0 | 1068 | /** |
michael@0 | 1069 | * Convenient way of emitting events from the panel window. |
michael@0 | 1070 | */ |
michael@0 | 1071 | EventEmitter.decorate(this); |
michael@0 | 1072 | |
michael@0 | 1073 | /** |
michael@0 | 1074 | * DOM query helpers. |
michael@0 | 1075 | */ |
michael@0 | 1076 | function $(selector, target = document) target.querySelector(selector); |
michael@0 | 1077 | function $all(selector, target = document) target.querySelectorAll(selector); |
michael@0 | 1078 | |
michael@0 | 1079 | /** |
michael@0 | 1080 | * Helper for getting an nsIURL instance out of a string. |
michael@0 | 1081 | */ |
michael@0 | 1082 | function nsIURL(url, store = nsIURL.store) { |
michael@0 | 1083 | if (store.has(url)) { |
michael@0 | 1084 | return store.get(url); |
michael@0 | 1085 | } |
michael@0 | 1086 | let uri = Services.io.newURI(url, null, null).QueryInterface(Ci.nsIURL); |
michael@0 | 1087 | store.set(url, uri); |
michael@0 | 1088 | return uri; |
michael@0 | 1089 | } |
michael@0 | 1090 | |
michael@0 | 1091 | // The cache used in the `nsIURL` function. |
michael@0 | 1092 | nsIURL.store = new Map(); |
michael@0 | 1093 | |
michael@0 | 1094 | /** |
michael@0 | 1095 | * Gets the fileName part of a string which happens to be an URL. |
michael@0 | 1096 | */ |
michael@0 | 1097 | function getFileName(url) { |
michael@0 | 1098 | try { |
michael@0 | 1099 | let { fileName } = nsIURL(url); |
michael@0 | 1100 | return fileName || "/"; |
michael@0 | 1101 | } catch (e) { |
michael@0 | 1102 | // This doesn't look like a url, or nsIURL can't handle it. |
michael@0 | 1103 | return ""; |
michael@0 | 1104 | } |
michael@0 | 1105 | } |
michael@0 | 1106 | |
michael@0 | 1107 | /** |
michael@0 | 1108 | * Gets an image data object containing a buffer large enough to hold |
michael@0 | 1109 | * width * height pixels. |
michael@0 | 1110 | * |
michael@0 | 1111 | * This method avoids allocating memory and tries to reuse a common buffer |
michael@0 | 1112 | * as much as possible. |
michael@0 | 1113 | * |
michael@0 | 1114 | * @param number w |
michael@0 | 1115 | * The desired image data storage width. |
michael@0 | 1116 | * @param number h |
michael@0 | 1117 | * The desired image data storage height. |
michael@0 | 1118 | * @return ImageData |
michael@0 | 1119 | * The requested image data buffer. |
michael@0 | 1120 | */ |
michael@0 | 1121 | function getImageDataStorage(ctx, w, h) { |
michael@0 | 1122 | let storage = getImageDataStorage.cache; |
michael@0 | 1123 | if (storage && storage.width == w && storage.height == h) { |
michael@0 | 1124 | return storage; |
michael@0 | 1125 | } |
michael@0 | 1126 | return getImageDataStorage.cache = ctx.createImageData(w, h); |
michael@0 | 1127 | } |
michael@0 | 1128 | |
michael@0 | 1129 | // The cache used in the `getImageDataStorage` function. |
michael@0 | 1130 | getImageDataStorage.cache = null; |
michael@0 | 1131 | |
michael@0 | 1132 | /** |
michael@0 | 1133 | * Draws image data into a canvas. |
michael@0 | 1134 | * |
michael@0 | 1135 | * This method makes absolutely no assumptions about the canvas element |
michael@0 | 1136 | * dimensions, or pre-existing rendering. It's a dumb proxy that copies pixels. |
michael@0 | 1137 | * |
michael@0 | 1138 | * @param HTMLCanvasElement canvas |
michael@0 | 1139 | * The canvas element to put the image data into. |
michael@0 | 1140 | * @param number width |
michael@0 | 1141 | * The image data width. |
michael@0 | 1142 | * @param number height |
michael@0 | 1143 | * The image data height. |
michael@0 | 1144 | * @param pixels |
michael@0 | 1145 | * An array buffer view of the image data. |
michael@0 | 1146 | * @param object options |
michael@0 | 1147 | * Additional options supported by this operation: |
michael@0 | 1148 | * - centered: specifies whether the image data should be centered |
michael@0 | 1149 | * when copied in the canvas; this is useful when the |
michael@0 | 1150 | * supplied pixels don't completely cover the canvas. |
michael@0 | 1151 | */ |
michael@0 | 1152 | function drawImage(canvas, width, height, pixels, options = {}) { |
michael@0 | 1153 | let ctx = canvas.getContext("2d"); |
michael@0 | 1154 | |
michael@0 | 1155 | // FrameSnapshot actors return "snapshot-image" type instances with just an |
michael@0 | 1156 | // empty pixel array if the source image is completely transparent. |
michael@0 | 1157 | if (pixels.length <= 1) { |
michael@0 | 1158 | ctx.clearRect(0, 0, canvas.width, canvas.height); |
michael@0 | 1159 | return; |
michael@0 | 1160 | } |
michael@0 | 1161 | |
michael@0 | 1162 | let arrayBuffer = new Uint8Array(pixels.buffer); |
michael@0 | 1163 | let imageData = getImageDataStorage(ctx, width, height); |
michael@0 | 1164 | imageData.data.set(arrayBuffer); |
michael@0 | 1165 | |
michael@0 | 1166 | if (options.centered) { |
michael@0 | 1167 | let left = (canvas.width - width) / 2; |
michael@0 | 1168 | let top = (canvas.height - height) / 2; |
michael@0 | 1169 | ctx.putImageData(imageData, left, top); |
michael@0 | 1170 | } else { |
michael@0 | 1171 | ctx.putImageData(imageData, 0, 0); |
michael@0 | 1172 | } |
michael@0 | 1173 | } |
michael@0 | 1174 | |
michael@0 | 1175 | /** |
michael@0 | 1176 | * Draws image data into a canvas, and sets that as the rendering source for |
michael@0 | 1177 | * an element with the specified id as the -moz-element background image. |
michael@0 | 1178 | * |
michael@0 | 1179 | * @param string id |
michael@0 | 1180 | * The id of the -moz-element background image. |
michael@0 | 1181 | * @param number width |
michael@0 | 1182 | * The image data width. |
michael@0 | 1183 | * @param number height |
michael@0 | 1184 | * The image data height. |
michael@0 | 1185 | * @param pixels |
michael@0 | 1186 | * An array buffer view of the image data. |
michael@0 | 1187 | */ |
michael@0 | 1188 | function drawBackground(id, width, height, pixels) { |
michael@0 | 1189 | let canvas = document.createElementNS(HTML_NS, "canvas"); |
michael@0 | 1190 | canvas.width = width; |
michael@0 | 1191 | canvas.height = height; |
michael@0 | 1192 | |
michael@0 | 1193 | drawImage(canvas, width, height, pixels); |
michael@0 | 1194 | document.mozSetImageElement(id, canvas); |
michael@0 | 1195 | |
michael@0 | 1196 | // Used in tests. Not emitting an event because this shouldn't be "interesting". |
michael@0 | 1197 | if (window._onMozSetImageElement) { |
michael@0 | 1198 | window._onMozSetImageElement(pixels); |
michael@0 | 1199 | } |
michael@0 | 1200 | } |
michael@0 | 1201 | |
michael@0 | 1202 | /** |
michael@0 | 1203 | * Iterates forward to find the next draw call in a snapshot. |
michael@0 | 1204 | */ |
michael@0 | 1205 | function getNextDrawCall(calls, call) { |
michael@0 | 1206 | for (let i = calls.indexOf(call) + 1, len = calls.length; i < len; i++) { |
michael@0 | 1207 | let nextCall = calls[i]; |
michael@0 | 1208 | let name = nextCall.attachment.actor.name; |
michael@0 | 1209 | if (CanvasFront.DRAW_CALLS.has(name)) { |
michael@0 | 1210 | return nextCall; |
michael@0 | 1211 | } |
michael@0 | 1212 | } |
michael@0 | 1213 | return null; |
michael@0 | 1214 | } |
michael@0 | 1215 | |
michael@0 | 1216 | /** |
michael@0 | 1217 | * Iterates backwards to find the most recent screenshot for a function call |
michael@0 | 1218 | * in a snapshot loaded from disk. |
michael@0 | 1219 | */ |
michael@0 | 1220 | function getScreenshotFromCallLoadedFromDisk(calls, call) { |
michael@0 | 1221 | for (let i = calls.indexOf(call); i >= 0; i--) { |
michael@0 | 1222 | let prevCall = calls[i]; |
michael@0 | 1223 | let screenshot = prevCall.screenshot; |
michael@0 | 1224 | if (screenshot) { |
michael@0 | 1225 | return screenshot; |
michael@0 | 1226 | } |
michael@0 | 1227 | } |
michael@0 | 1228 | return CanvasFront.INVALID_SNAPSHOT_IMAGE; |
michael@0 | 1229 | } |
michael@0 | 1230 | |
michael@0 | 1231 | /** |
michael@0 | 1232 | * Iterates backwards to find the most recent thumbnail for a function call. |
michael@0 | 1233 | */ |
michael@0 | 1234 | function getThumbnailForCall(thumbnails, index) { |
michael@0 | 1235 | for (let i = thumbnails.length - 1; i >= 0; i--) { |
michael@0 | 1236 | let thumbnail = thumbnails[i]; |
michael@0 | 1237 | if (thumbnail.index <= index) { |
michael@0 | 1238 | return thumbnail; |
michael@0 | 1239 | } |
michael@0 | 1240 | } |
michael@0 | 1241 | return CanvasFront.INVALID_SNAPSHOT_IMAGE; |
michael@0 | 1242 | } |
michael@0 | 1243 | |
michael@0 | 1244 | /** |
michael@0 | 1245 | * Opens/selects the debugger in this toolbox and jumps to the specified |
michael@0 | 1246 | * file name and line number. |
michael@0 | 1247 | */ |
michael@0 | 1248 | function viewSourceInDebugger(url, line) { |
michael@0 | 1249 | let showSource = ({ DebuggerView }) => { |
michael@0 | 1250 | if (DebuggerView.Sources.containsValue(url)) { |
michael@0 | 1251 | DebuggerView.setEditorLocation(url, line, { noDebug: true }).then(() => { |
michael@0 | 1252 | window.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER); |
michael@0 | 1253 | }, () => { |
michael@0 | 1254 | window.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER); |
michael@0 | 1255 | }); |
michael@0 | 1256 | } |
michael@0 | 1257 | } |
michael@0 | 1258 | |
michael@0 | 1259 | // If the Debugger was already open, switch to it and try to show the |
michael@0 | 1260 | // source immediately. Otherwise, initialize it and wait for the sources |
michael@0 | 1261 | // to be added first. |
michael@0 | 1262 | let debuggerAlreadyOpen = gToolbox.getPanel("jsdebugger"); |
michael@0 | 1263 | gToolbox.selectTool("jsdebugger").then(({ panelWin: dbg }) => { |
michael@0 | 1264 | if (debuggerAlreadyOpen) { |
michael@0 | 1265 | showSource(dbg); |
michael@0 | 1266 | } else { |
michael@0 | 1267 | dbg.once(dbg.EVENTS.SOURCES_ADDED, () => showSource(dbg)); |
michael@0 | 1268 | } |
michael@0 | 1269 | }); |
michael@0 | 1270 | } |