|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 const MAX_ORDINAL = 99; |
|
8 const ZOOM_PREF = "devtools.toolbox.zoomValue"; |
|
9 const MIN_ZOOM = 0.5; |
|
10 const MAX_ZOOM = 2; |
|
11 |
|
12 let {Cc, Ci, Cu} = require("chrome"); |
|
13 let {Promise: promise} = require("resource://gre/modules/Promise.jsm"); |
|
14 let EventEmitter = require("devtools/toolkit/event-emitter"); |
|
15 let Telemetry = require("devtools/shared/telemetry"); |
|
16 let HUDService = require("devtools/webconsole/hudservice"); |
|
17 |
|
18 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
19 Cu.import("resource://gre/modules/Services.jsm"); |
|
20 Cu.import("resource:///modules/devtools/gDevTools.jsm"); |
|
21 Cu.import("resource:///modules/devtools/scratchpad-manager.jsm"); |
|
22 Cu.import("resource:///modules/devtools/DOMHelpers.jsm"); |
|
23 Cu.import("resource://gre/modules/Task.jsm"); |
|
24 |
|
25 loader.lazyGetter(this, "Hosts", () => require("devtools/framework/toolbox-hosts").Hosts); |
|
26 |
|
27 loader.lazyImporter(this, "CommandUtils", "resource:///modules/devtools/DeveloperToolbar.jsm"); |
|
28 |
|
29 loader.lazyGetter(this, "toolboxStrings", () => { |
|
30 let bundle = Services.strings.createBundle("chrome://browser/locale/devtools/toolbox.properties"); |
|
31 return (name, ...args) => { |
|
32 try { |
|
33 if (!args.length) { |
|
34 return bundle.GetStringFromName(name); |
|
35 } |
|
36 return bundle.formatStringFromName(name, args, args.length); |
|
37 } catch (ex) { |
|
38 Services.console.logStringMessage("Error reading '" + name + "'"); |
|
39 return null; |
|
40 } |
|
41 }; |
|
42 }); |
|
43 |
|
44 loader.lazyGetter(this, "Selection", () => require("devtools/framework/selection").Selection); |
|
45 loader.lazyGetter(this, "InspectorFront", () => require("devtools/server/actors/inspector").InspectorFront); |
|
46 |
|
47 /** |
|
48 * A "Toolbox" is the component that holds all the tools for one specific |
|
49 * target. Visually, it's a document that includes the tools tabs and all |
|
50 * the iframes where the tool panels will be living in. |
|
51 * |
|
52 * @param {object} target |
|
53 * The object the toolbox is debugging. |
|
54 * @param {string} selectedTool |
|
55 * Tool to select initially |
|
56 * @param {Toolbox.HostType} hostType |
|
57 * Type of host that will host the toolbox (e.g. sidebar, window) |
|
58 * @param {object} hostOptions |
|
59 * Options for host specifically |
|
60 */ |
|
61 function Toolbox(target, selectedTool, hostType, hostOptions) { |
|
62 this._target = target; |
|
63 this._toolPanels = new Map(); |
|
64 this._telemetry = new Telemetry(); |
|
65 |
|
66 this._toolRegistered = this._toolRegistered.bind(this); |
|
67 this._toolUnregistered = this._toolUnregistered.bind(this); |
|
68 this._refreshHostTitle = this._refreshHostTitle.bind(this); |
|
69 this._splitConsoleOnKeypress = this._splitConsoleOnKeypress.bind(this) |
|
70 this.destroy = this.destroy.bind(this); |
|
71 this.highlighterUtils = new ToolboxHighlighterUtils(this); |
|
72 this._highlighterReady = this._highlighterReady.bind(this); |
|
73 this._highlighterHidden = this._highlighterHidden.bind(this); |
|
74 |
|
75 this._target.on("close", this.destroy); |
|
76 |
|
77 if (!hostType) { |
|
78 hostType = Services.prefs.getCharPref(this._prefs.LAST_HOST); |
|
79 } |
|
80 if (!selectedTool) { |
|
81 selectedTool = Services.prefs.getCharPref(this._prefs.LAST_TOOL); |
|
82 } |
|
83 if (!gDevTools.getToolDefinition(selectedTool)) { |
|
84 selectedTool = "webconsole"; |
|
85 } |
|
86 this._defaultToolId = selectedTool; |
|
87 |
|
88 this._host = this._createHost(hostType, hostOptions); |
|
89 |
|
90 EventEmitter.decorate(this); |
|
91 |
|
92 this._target.on("navigate", this._refreshHostTitle); |
|
93 this.on("host-changed", this._refreshHostTitle); |
|
94 this.on("select", this._refreshHostTitle); |
|
95 |
|
96 gDevTools.on("tool-registered", this._toolRegistered); |
|
97 gDevTools.on("tool-unregistered", this._toolUnregistered); |
|
98 } |
|
99 exports.Toolbox = Toolbox; |
|
100 |
|
101 /** |
|
102 * The toolbox can be 'hosted' either embedded in a browser window |
|
103 * or in a separate window. |
|
104 */ |
|
105 Toolbox.HostType = { |
|
106 BOTTOM: "bottom", |
|
107 SIDE: "side", |
|
108 WINDOW: "window", |
|
109 CUSTOM: "custom" |
|
110 }; |
|
111 |
|
112 Toolbox.prototype = { |
|
113 _URL: "chrome://browser/content/devtools/framework/toolbox.xul", |
|
114 |
|
115 _prefs: { |
|
116 LAST_HOST: "devtools.toolbox.host", |
|
117 LAST_TOOL: "devtools.toolbox.selectedTool", |
|
118 SIDE_ENABLED: "devtools.toolbox.sideEnabled" |
|
119 }, |
|
120 |
|
121 currentToolId: null, |
|
122 |
|
123 /** |
|
124 * Returns a *copy* of the _toolPanels collection. |
|
125 * |
|
126 * @return {Map} panels |
|
127 * All the running panels in the toolbox |
|
128 */ |
|
129 getToolPanels: function() { |
|
130 return new Map(this._toolPanels); |
|
131 }, |
|
132 |
|
133 /** |
|
134 * Access the panel for a given tool |
|
135 */ |
|
136 getPanel: function(id) { |
|
137 return this._toolPanels.get(id); |
|
138 }, |
|
139 |
|
140 /** |
|
141 * This is a shortcut for getPanel(currentToolId) because it is much more |
|
142 * likely that we're going to want to get the panel that we've just made |
|
143 * visible |
|
144 */ |
|
145 getCurrentPanel: function() { |
|
146 return this._toolPanels.get(this.currentToolId); |
|
147 }, |
|
148 |
|
149 /** |
|
150 * Get/alter the target of a Toolbox so we're debugging something different. |
|
151 * See Target.jsm for more details. |
|
152 * TODO: Do we allow |toolbox.target = null;| ? |
|
153 */ |
|
154 get target() { |
|
155 return this._target; |
|
156 }, |
|
157 |
|
158 /** |
|
159 * Get/alter the host of a Toolbox, i.e. is it in browser or in a separate |
|
160 * tab. See HostType for more details. |
|
161 */ |
|
162 get hostType() { |
|
163 return this._host.type; |
|
164 }, |
|
165 |
|
166 /** |
|
167 * Get the iframe containing the toolbox UI. |
|
168 */ |
|
169 get frame() { |
|
170 return this._host.frame; |
|
171 }, |
|
172 |
|
173 /** |
|
174 * Shortcut to the document containing the toolbox UI |
|
175 */ |
|
176 get doc() { |
|
177 return this.frame.contentDocument; |
|
178 }, |
|
179 |
|
180 /** |
|
181 * Get current zoom level of toolbox |
|
182 */ |
|
183 get zoomValue() { |
|
184 return parseFloat(Services.prefs.getCharPref(ZOOM_PREF)); |
|
185 }, |
|
186 |
|
187 /** |
|
188 * Get the toolbox highlighter front. Note that it may not always have been |
|
189 * initialized first. Use `initInspector()` if needed. |
|
190 */ |
|
191 get highlighter() { |
|
192 if (this.highlighterUtils.isRemoteHighlightable) { |
|
193 return this._highlighter; |
|
194 } else { |
|
195 return null; |
|
196 } |
|
197 }, |
|
198 |
|
199 /** |
|
200 * Get the toolbox's inspector front. Note that it may not always have been |
|
201 * initialized first. Use `initInspector()` if needed. |
|
202 */ |
|
203 get inspector() { |
|
204 return this._inspector; |
|
205 }, |
|
206 |
|
207 /** |
|
208 * Get the toolbox's walker front. Note that it may not always have been |
|
209 * initialized first. Use `initInspector()` if needed. |
|
210 */ |
|
211 get walker() { |
|
212 return this._walker; |
|
213 }, |
|
214 |
|
215 /** |
|
216 * Get the toolbox's node selection. Note that it may not always have been |
|
217 * initialized first. Use `initInspector()` if needed. |
|
218 */ |
|
219 get selection() { |
|
220 return this._selection; |
|
221 }, |
|
222 |
|
223 /** |
|
224 * Get the toggled state of the split console |
|
225 */ |
|
226 get splitConsole() { |
|
227 return this._splitConsole; |
|
228 }, |
|
229 |
|
230 /** |
|
231 * Open the toolbox |
|
232 */ |
|
233 open: function() { |
|
234 let deferred = promise.defer(); |
|
235 |
|
236 return this._host.create().then(iframe => { |
|
237 let deferred = promise.defer(); |
|
238 |
|
239 let domReady = () => { |
|
240 this.isReady = true; |
|
241 |
|
242 let closeButton = this.doc.getElementById("toolbox-close"); |
|
243 closeButton.addEventListener("command", this.destroy, true); |
|
244 |
|
245 this._buildDockButtons(); |
|
246 this._buildOptions(); |
|
247 this._buildTabs(); |
|
248 this._buildButtons(); |
|
249 this._addKeysToWindow(); |
|
250 this._addToolSwitchingKeys(); |
|
251 this._addZoomKeys(); |
|
252 this._loadInitialZoom(); |
|
253 |
|
254 this._telemetry.toolOpened("toolbox"); |
|
255 |
|
256 this.selectTool(this._defaultToolId).then(panel => { |
|
257 this.emit("ready"); |
|
258 deferred.resolve(); |
|
259 }); |
|
260 }; |
|
261 |
|
262 // Load the toolbox-level actor fronts and utilities now |
|
263 this._target.makeRemote().then(() => { |
|
264 iframe.setAttribute("src", this._URL); |
|
265 let domHelper = new DOMHelpers(iframe.contentWindow); |
|
266 domHelper.onceDOMReady(domReady); |
|
267 }); |
|
268 |
|
269 return deferred.promise; |
|
270 }); |
|
271 }, |
|
272 |
|
273 _buildOptions: function() { |
|
274 let key = this.doc.getElementById("toolbox-options-key"); |
|
275 key.addEventListener("command", () => { |
|
276 this.selectTool("options"); |
|
277 }, true); |
|
278 }, |
|
279 |
|
280 _isResponsiveModeActive: function() { |
|
281 let responsiveModeActive = false; |
|
282 if (this.target.isLocalTab) { |
|
283 let tab = this.target.tab; |
|
284 let browserWindow = tab.ownerDocument.defaultView; |
|
285 let responsiveUIManager = browserWindow.ResponsiveUI.ResponsiveUIManager; |
|
286 responsiveModeActive = responsiveUIManager.isActiveForTab(tab); |
|
287 } |
|
288 return responsiveModeActive; |
|
289 }, |
|
290 |
|
291 _splitConsoleOnKeypress: function(e) { |
|
292 let responsiveModeActive = this._isResponsiveModeActive(); |
|
293 if (e.keyCode === e.DOM_VK_ESCAPE && !responsiveModeActive) { |
|
294 this.toggleSplitConsole(); |
|
295 } |
|
296 }, |
|
297 |
|
298 _addToolSwitchingKeys: function() { |
|
299 let nextKey = this.doc.getElementById("toolbox-next-tool-key"); |
|
300 nextKey.addEventListener("command", this.selectNextTool.bind(this), true); |
|
301 let prevKey = this.doc.getElementById("toolbox-previous-tool-key"); |
|
302 prevKey.addEventListener("command", this.selectPreviousTool.bind(this), true); |
|
303 |
|
304 // Split console uses keypress instead of command so the event can be |
|
305 // cancelled with stopPropagation on the keypress, and not preventDefault. |
|
306 this.doc.addEventListener("keypress", this._splitConsoleOnKeypress, false); |
|
307 }, |
|
308 |
|
309 /** |
|
310 * Make sure that the console is showing up properly based on all the |
|
311 * possible conditions. |
|
312 * 1) If the console tab is selected, then regardless of split state |
|
313 * it should take up the full height of the deck, and we should |
|
314 * hide the deck and splitter. |
|
315 * 2) If the console tab is not selected and it is split, then we should |
|
316 * show the splitter, deck, and console. |
|
317 * 3) If the console tab is not selected and it is *not* split, |
|
318 * then we should hide the console and splitter, and show the deck |
|
319 * at full height. |
|
320 */ |
|
321 _refreshConsoleDisplay: function() { |
|
322 let deck = this.doc.getElementById("toolbox-deck"); |
|
323 let webconsolePanel = this.doc.getElementById("toolbox-panel-webconsole"); |
|
324 let splitter = this.doc.getElementById("toolbox-console-splitter"); |
|
325 let openedConsolePanel = this.currentToolId === "webconsole"; |
|
326 |
|
327 if (openedConsolePanel) { |
|
328 deck.setAttribute("collapsed", "true"); |
|
329 splitter.setAttribute("hidden", "true"); |
|
330 webconsolePanel.removeAttribute("collapsed"); |
|
331 } else { |
|
332 deck.removeAttribute("collapsed"); |
|
333 if (this._splitConsole) { |
|
334 webconsolePanel.removeAttribute("collapsed"); |
|
335 splitter.removeAttribute("hidden"); |
|
336 } else { |
|
337 webconsolePanel.setAttribute("collapsed", "true"); |
|
338 splitter.setAttribute("hidden", "true"); |
|
339 } |
|
340 } |
|
341 }, |
|
342 |
|
343 /** |
|
344 * Wire up the listeners for the zoom keys. |
|
345 */ |
|
346 _addZoomKeys: function() { |
|
347 let inKey = this.doc.getElementById("toolbox-zoom-in-key"); |
|
348 inKey.addEventListener("command", this.zoomIn.bind(this), true); |
|
349 |
|
350 let inKey2 = this.doc.getElementById("toolbox-zoom-in-key2"); |
|
351 inKey2.addEventListener("command", this.zoomIn.bind(this), true); |
|
352 |
|
353 let outKey = this.doc.getElementById("toolbox-zoom-out-key"); |
|
354 outKey.addEventListener("command", this.zoomOut.bind(this), true); |
|
355 |
|
356 let resetKey = this.doc.getElementById("toolbox-zoom-reset-key"); |
|
357 resetKey.addEventListener("command", this.zoomReset.bind(this), true); |
|
358 }, |
|
359 |
|
360 /** |
|
361 * Set zoom on toolbox to whatever the last setting was. |
|
362 */ |
|
363 _loadInitialZoom: function() { |
|
364 this.setZoom(this.zoomValue); |
|
365 }, |
|
366 |
|
367 /** |
|
368 * Increase zoom level of toolbox window - make things bigger. |
|
369 */ |
|
370 zoomIn: function() { |
|
371 this.setZoom(this.zoomValue + 0.1); |
|
372 }, |
|
373 |
|
374 /** |
|
375 * Decrease zoom level of toolbox window - make things smaller. |
|
376 */ |
|
377 zoomOut: function() { |
|
378 this.setZoom(this.zoomValue - 0.1); |
|
379 }, |
|
380 |
|
381 /** |
|
382 * Reset zoom level of the toolbox window. |
|
383 */ |
|
384 zoomReset: function() { |
|
385 this.setZoom(1); |
|
386 }, |
|
387 |
|
388 /** |
|
389 * Set zoom level of the toolbox window. |
|
390 * |
|
391 * @param {number} zoomValue |
|
392 * Zoom level e.g. 1.2 |
|
393 */ |
|
394 setZoom: function(zoomValue) { |
|
395 // cap zoom value |
|
396 zoomValue = Math.max(zoomValue, MIN_ZOOM); |
|
397 zoomValue = Math.min(zoomValue, MAX_ZOOM); |
|
398 |
|
399 let contViewer = this.frame.docShell.contentViewer; |
|
400 let docViewer = contViewer.QueryInterface(Ci.nsIMarkupDocumentViewer); |
|
401 |
|
402 docViewer.fullZoom = zoomValue; |
|
403 |
|
404 Services.prefs.setCharPref(ZOOM_PREF, zoomValue); |
|
405 }, |
|
406 |
|
407 /** |
|
408 * Adds the keys and commands to the Toolbox Window in window mode. |
|
409 */ |
|
410 _addKeysToWindow: function() { |
|
411 if (this.hostType != Toolbox.HostType.WINDOW) { |
|
412 return; |
|
413 } |
|
414 |
|
415 let doc = this.doc.defaultView.parent.document; |
|
416 |
|
417 for (let [id, toolDefinition] of gDevTools.getToolDefinitionMap()) { |
|
418 // Prevent multiple entries for the same tool. |
|
419 if (!toolDefinition.key || doc.getElementById("key_" + id)) { |
|
420 continue; |
|
421 } |
|
422 |
|
423 let toolId = id; |
|
424 let key = doc.createElement("key"); |
|
425 |
|
426 key.id = "key_" + toolId; |
|
427 |
|
428 if (toolDefinition.key.startsWith("VK_")) { |
|
429 key.setAttribute("keycode", toolDefinition.key); |
|
430 } else { |
|
431 key.setAttribute("key", toolDefinition.key); |
|
432 } |
|
433 |
|
434 key.setAttribute("modifiers", toolDefinition.modifiers); |
|
435 key.setAttribute("oncommand", "void(0);"); // needed. See bug 371900 |
|
436 key.addEventListener("command", () => { |
|
437 this.selectTool(toolId).then(() => this.fireCustomKey(toolId)); |
|
438 }, true); |
|
439 doc.getElementById("toolbox-keyset").appendChild(key); |
|
440 } |
|
441 |
|
442 // Add key for toggling the browser console from the detached window |
|
443 if (!doc.getElementById("key_browserconsole")) { |
|
444 let key = doc.createElement("key"); |
|
445 key.id = "key_browserconsole"; |
|
446 |
|
447 key.setAttribute("key", toolboxStrings("browserConsoleCmd.commandkey")); |
|
448 key.setAttribute("modifiers", "accel,shift"); |
|
449 key.setAttribute("oncommand", "void(0)"); // needed. See bug 371900 |
|
450 key.addEventListener("command", () => { |
|
451 HUDService.toggleBrowserConsole(); |
|
452 }, true); |
|
453 doc.getElementById("toolbox-keyset").appendChild(key); |
|
454 } |
|
455 }, |
|
456 |
|
457 /** |
|
458 * Handle any custom key events. Returns true if there was a custom key binding run |
|
459 * @param {string} toolId |
|
460 * Which tool to run the command on (skip if not current) |
|
461 */ |
|
462 fireCustomKey: function(toolId) { |
|
463 let toolDefinition = gDevTools.getToolDefinition(toolId); |
|
464 |
|
465 if (toolDefinition.onkey && |
|
466 ((this.currentToolId === toolId) || |
|
467 (toolId == "webconsole" && this.splitConsole))) { |
|
468 toolDefinition.onkey(this.getCurrentPanel(), this); |
|
469 } |
|
470 }, |
|
471 |
|
472 /** |
|
473 * Build the buttons for changing hosts. Called every time |
|
474 * the host changes. |
|
475 */ |
|
476 _buildDockButtons: function() { |
|
477 let dockBox = this.doc.getElementById("toolbox-dock-buttons"); |
|
478 |
|
479 while (dockBox.firstChild) { |
|
480 dockBox.removeChild(dockBox.firstChild); |
|
481 } |
|
482 |
|
483 if (!this._target.isLocalTab) { |
|
484 return; |
|
485 } |
|
486 |
|
487 let closeButton = this.doc.getElementById("toolbox-close"); |
|
488 if (this.hostType == Toolbox.HostType.WINDOW) { |
|
489 closeButton.setAttribute("hidden", "true"); |
|
490 } else { |
|
491 closeButton.removeAttribute("hidden"); |
|
492 } |
|
493 |
|
494 let sideEnabled = Services.prefs.getBoolPref(this._prefs.SIDE_ENABLED); |
|
495 |
|
496 for (let type in Toolbox.HostType) { |
|
497 let position = Toolbox.HostType[type]; |
|
498 if (position == this.hostType || |
|
499 position == Toolbox.HostType.CUSTOM || |
|
500 (!sideEnabled && position == Toolbox.HostType.SIDE)) { |
|
501 continue; |
|
502 } |
|
503 |
|
504 let button = this.doc.createElement("toolbarbutton"); |
|
505 button.id = "toolbox-dock-" + position; |
|
506 button.className = "toolbox-dock-button"; |
|
507 button.setAttribute("tooltiptext", toolboxStrings("toolboxDockButtons." + |
|
508 position + ".tooltip")); |
|
509 button.addEventListener("command", () => { |
|
510 this.switchHost(position); |
|
511 }); |
|
512 |
|
513 dockBox.appendChild(button); |
|
514 } |
|
515 }, |
|
516 |
|
517 /** |
|
518 * Add tabs to the toolbox UI for registered tools |
|
519 */ |
|
520 _buildTabs: function() { |
|
521 for (let definition of gDevTools.getToolDefinitionArray()) { |
|
522 this._buildTabForTool(definition); |
|
523 } |
|
524 }, |
|
525 |
|
526 /** |
|
527 * Add buttons to the UI as specified in the devtools.toolbox.toolbarSpec pref |
|
528 */ |
|
529 _buildButtons: function() { |
|
530 this._buildPickerButton(); |
|
531 |
|
532 if (!this.target.isLocalTab) { |
|
533 return; |
|
534 } |
|
535 |
|
536 let spec = CommandUtils.getCommandbarSpec("devtools.toolbox.toolbarSpec"); |
|
537 let environment = CommandUtils.createEnvironment(this, '_target'); |
|
538 this._requisition = CommandUtils.createRequisition(environment); |
|
539 let buttons = CommandUtils.createButtons(spec, this._target, |
|
540 this.doc, this._requisition); |
|
541 let container = this.doc.getElementById("toolbox-buttons"); |
|
542 buttons.forEach(container.appendChild.bind(container)); |
|
543 this.setToolboxButtonsVisibility(); |
|
544 }, |
|
545 |
|
546 /** |
|
547 * Adding the element picker button is done here unlike the other buttons |
|
548 * since we want it to work for remote targets too |
|
549 */ |
|
550 _buildPickerButton: function() { |
|
551 this._pickerButton = this.doc.createElement("toolbarbutton"); |
|
552 this._pickerButton.id = "command-button-pick"; |
|
553 this._pickerButton.className = "command-button command-button-invertable"; |
|
554 this._pickerButton.setAttribute("tooltiptext", toolboxStrings("pickButton.tooltip")); |
|
555 |
|
556 let container = this.doc.querySelector("#toolbox-buttons"); |
|
557 container.appendChild(this._pickerButton); |
|
558 |
|
559 this._togglePicker = this.highlighterUtils.togglePicker.bind(this.highlighterUtils); |
|
560 this._pickerButton.addEventListener("command", this._togglePicker, false); |
|
561 }, |
|
562 |
|
563 /** |
|
564 * Return all toolbox buttons (command buttons, plus any others that were |
|
565 * added manually). |
|
566 */ |
|
567 get toolboxButtons() { |
|
568 // White-list buttons that can be toggled to prevent adding prefs for |
|
569 // addons that have manually inserted toolbarbuttons into DOM. |
|
570 return [ |
|
571 "command-button-pick", |
|
572 "command-button-splitconsole", |
|
573 "command-button-responsive", |
|
574 "command-button-paintflashing", |
|
575 "command-button-tilt", |
|
576 "command-button-scratchpad", |
|
577 "command-button-eyedropper" |
|
578 ].map(id => { |
|
579 let button = this.doc.getElementById(id); |
|
580 // Some buttons may not exist inside of Browser Toolbox |
|
581 if (!button) { |
|
582 return false; |
|
583 } |
|
584 return { |
|
585 id: id, |
|
586 button: button, |
|
587 label: button.getAttribute("tooltiptext"), |
|
588 visibilityswitch: "devtools." + id + ".enabled" |
|
589 } |
|
590 }).filter(button=>button); |
|
591 }, |
|
592 |
|
593 /** |
|
594 * Ensure the visibility of each toolbox button matches the |
|
595 * preference value. Simply hide buttons that are preffed off. |
|
596 */ |
|
597 setToolboxButtonsVisibility: function() { |
|
598 this.toolboxButtons.forEach(buttonSpec => { |
|
599 let {visibilityswitch, id, button}=buttonSpec; |
|
600 let on = true; |
|
601 try { |
|
602 on = Services.prefs.getBoolPref(visibilityswitch); |
|
603 } catch (ex) { } |
|
604 |
|
605 if (button) { |
|
606 if (on) { |
|
607 button.removeAttribute("hidden"); |
|
608 } else { |
|
609 button.setAttribute("hidden", "true"); |
|
610 } |
|
611 } |
|
612 }); |
|
613 }, |
|
614 |
|
615 /** |
|
616 * Build a tab for one tool definition and add to the toolbox |
|
617 * |
|
618 * @param {string} toolDefinition |
|
619 * Tool definition of the tool to build a tab for. |
|
620 */ |
|
621 _buildTabForTool: function(toolDefinition) { |
|
622 if (!toolDefinition.isTargetSupported(this._target)) { |
|
623 return; |
|
624 } |
|
625 |
|
626 let tabs = this.doc.getElementById("toolbox-tabs"); |
|
627 let deck = this.doc.getElementById("toolbox-deck"); |
|
628 |
|
629 let id = toolDefinition.id; |
|
630 |
|
631 if (toolDefinition.ordinal == undefined || toolDefinition.ordinal < 0) { |
|
632 toolDefinition.ordinal = MAX_ORDINAL; |
|
633 } |
|
634 |
|
635 let radio = this.doc.createElement("radio"); |
|
636 // The radio element is not being used in the conventional way, thus |
|
637 // the devtools-tab class replaces the radio XBL binding with its base |
|
638 // binding (the control-item binding). |
|
639 radio.className = "devtools-tab"; |
|
640 radio.id = "toolbox-tab-" + id; |
|
641 radio.setAttribute("toolid", id); |
|
642 radio.setAttribute("ordinal", toolDefinition.ordinal); |
|
643 radio.setAttribute("tooltiptext", toolDefinition.tooltip); |
|
644 if (toolDefinition.invertIconForLightTheme) { |
|
645 radio.setAttribute("icon-invertable", "true"); |
|
646 } |
|
647 |
|
648 radio.addEventListener("command", () => { |
|
649 this.selectTool(id); |
|
650 }); |
|
651 |
|
652 // spacer lets us center the image and label, while allowing cropping |
|
653 let spacer = this.doc.createElement("spacer"); |
|
654 spacer.setAttribute("flex", "1"); |
|
655 radio.appendChild(spacer); |
|
656 |
|
657 if (toolDefinition.icon) { |
|
658 let image = this.doc.createElement("image"); |
|
659 image.className = "default-icon"; |
|
660 image.setAttribute("src", |
|
661 toolDefinition.icon || toolDefinition.highlightedicon); |
|
662 radio.appendChild(image); |
|
663 // Adding the highlighted icon image |
|
664 image = this.doc.createElement("image"); |
|
665 image.className = "highlighted-icon"; |
|
666 image.setAttribute("src", |
|
667 toolDefinition.highlightedicon || toolDefinition.icon); |
|
668 radio.appendChild(image); |
|
669 } |
|
670 |
|
671 if (toolDefinition.label) { |
|
672 let label = this.doc.createElement("label"); |
|
673 label.setAttribute("value", toolDefinition.label) |
|
674 label.setAttribute("crop", "end"); |
|
675 label.setAttribute("flex", "1"); |
|
676 radio.appendChild(label); |
|
677 radio.setAttribute("flex", "1"); |
|
678 } |
|
679 |
|
680 if (!toolDefinition.bgTheme) { |
|
681 toolDefinition.bgTheme = "theme-toolbar"; |
|
682 } |
|
683 let vbox = this.doc.createElement("vbox"); |
|
684 vbox.className = "toolbox-panel " + toolDefinition.bgTheme; |
|
685 |
|
686 // There is already a container for the webconsole frame. |
|
687 if (!this.doc.getElementById("toolbox-panel-" + id)) { |
|
688 vbox.id = "toolbox-panel-" + id; |
|
689 } |
|
690 |
|
691 // If there is no tab yet, or the ordinal to be added is the largest one. |
|
692 if (tabs.childNodes.length == 0 || |
|
693 +tabs.lastChild.getAttribute("ordinal") <= toolDefinition.ordinal) { |
|
694 tabs.appendChild(radio); |
|
695 deck.appendChild(vbox); |
|
696 } else { |
|
697 // else, iterate over all the tabs to get the correct location. |
|
698 Array.some(tabs.childNodes, (node, i) => { |
|
699 if (+node.getAttribute("ordinal") > toolDefinition.ordinal) { |
|
700 tabs.insertBefore(radio, node); |
|
701 deck.insertBefore(vbox, deck.childNodes[i]); |
|
702 return true; |
|
703 } |
|
704 return false; |
|
705 }); |
|
706 } |
|
707 |
|
708 this._addKeysToWindow(); |
|
709 }, |
|
710 |
|
711 /** |
|
712 * Ensure the tool with the given id is loaded. |
|
713 * |
|
714 * @param {string} id |
|
715 * The id of the tool to load. |
|
716 */ |
|
717 loadTool: function(id) { |
|
718 if (id === "inspector" && !this._inspector) { |
|
719 return this.initInspector().then(() => { |
|
720 return this.loadTool(id); |
|
721 }); |
|
722 } |
|
723 |
|
724 let deferred = promise.defer(); |
|
725 let iframe = this.doc.getElementById("toolbox-panel-iframe-" + id); |
|
726 |
|
727 if (iframe) { |
|
728 let panel = this._toolPanels.get(id); |
|
729 if (panel) { |
|
730 deferred.resolve(panel); |
|
731 } else { |
|
732 this.once(id + "-ready", panel => { |
|
733 deferred.resolve(panel); |
|
734 }); |
|
735 } |
|
736 return deferred.promise; |
|
737 } |
|
738 |
|
739 let definition = gDevTools.getToolDefinition(id); |
|
740 if (!definition) { |
|
741 deferred.reject(new Error("no such tool id "+id)); |
|
742 return deferred.promise; |
|
743 } |
|
744 |
|
745 iframe = this.doc.createElement("iframe"); |
|
746 iframe.className = "toolbox-panel-iframe"; |
|
747 iframe.id = "toolbox-panel-iframe-" + id; |
|
748 iframe.setAttribute("flex", 1); |
|
749 iframe.setAttribute("forceOwnRefreshDriver", ""); |
|
750 iframe.tooltip = "aHTMLTooltip"; |
|
751 iframe.style.visibility = "hidden"; |
|
752 |
|
753 let vbox = this.doc.getElementById("toolbox-panel-" + id); |
|
754 vbox.appendChild(iframe); |
|
755 |
|
756 let onLoad = () => { |
|
757 // Prevent flicker while loading by waiting to make visible until now. |
|
758 iframe.style.visibility = "visible"; |
|
759 |
|
760 let built = definition.build(iframe.contentWindow, this); |
|
761 promise.resolve(built).then((panel) => { |
|
762 this._toolPanels.set(id, panel); |
|
763 this.emit(id + "-ready", panel); |
|
764 gDevTools.emit(id + "-ready", this, panel); |
|
765 deferred.resolve(panel); |
|
766 }, console.error); |
|
767 }; |
|
768 |
|
769 iframe.setAttribute("src", definition.url); |
|
770 |
|
771 // Depending on the host, iframe.contentWindow is not always |
|
772 // defined at this moment. If it is not defined, we use an |
|
773 // event listener on the iframe DOM node. If it's defined, |
|
774 // we use the chromeEventHandler. We can't use a listener |
|
775 // on the DOM node every time because this won't work |
|
776 // if the (xul chrome) iframe is loaded in a content docshell. |
|
777 if (iframe.contentWindow) { |
|
778 let domHelper = new DOMHelpers(iframe.contentWindow); |
|
779 domHelper.onceDOMReady(onLoad); |
|
780 } else { |
|
781 let callback = () => { |
|
782 iframe.removeEventListener("DOMContentLoaded", callback); |
|
783 onLoad(); |
|
784 } |
|
785 iframe.addEventListener("DOMContentLoaded", callback); |
|
786 } |
|
787 |
|
788 return deferred.promise; |
|
789 }, |
|
790 |
|
791 /** |
|
792 * Switch to the tool with the given id |
|
793 * |
|
794 * @param {string} id |
|
795 * The id of the tool to switch to |
|
796 */ |
|
797 selectTool: function(id) { |
|
798 let selected = this.doc.querySelector(".devtools-tab[selected]"); |
|
799 if (selected) { |
|
800 selected.removeAttribute("selected"); |
|
801 } |
|
802 |
|
803 let tab = this.doc.getElementById("toolbox-tab-" + id); |
|
804 tab.setAttribute("selected", "true"); |
|
805 |
|
806 if (this.currentToolId == id) { |
|
807 // re-focus tool to get key events again |
|
808 this.focusTool(id); |
|
809 |
|
810 // Return the existing panel in order to have a consistent return value. |
|
811 return promise.resolve(this._toolPanels.get(id)); |
|
812 } |
|
813 |
|
814 if (!this.isReady) { |
|
815 throw new Error("Can't select tool, wait for toolbox 'ready' event"); |
|
816 } |
|
817 |
|
818 tab = this.doc.getElementById("toolbox-tab-" + id); |
|
819 |
|
820 if (tab) { |
|
821 if (this.currentToolId) { |
|
822 this._telemetry.toolClosed(this.currentToolId); |
|
823 } |
|
824 this._telemetry.toolOpened(id); |
|
825 } else { |
|
826 throw new Error("No tool found"); |
|
827 } |
|
828 |
|
829 let tabstrip = this.doc.getElementById("toolbox-tabs"); |
|
830 |
|
831 // select the right tab, making 0th index the default tab if right tab not |
|
832 // found |
|
833 let index = 0; |
|
834 let tabs = tabstrip.childNodes; |
|
835 for (let i = 0; i < tabs.length; i++) { |
|
836 if (tabs[i] === tab) { |
|
837 index = i; |
|
838 break; |
|
839 } |
|
840 } |
|
841 tabstrip.selectedItem = tab; |
|
842 |
|
843 // and select the right iframe |
|
844 let deck = this.doc.getElementById("toolbox-deck"); |
|
845 deck.selectedIndex = index; |
|
846 |
|
847 this.currentToolId = id; |
|
848 this._refreshConsoleDisplay(); |
|
849 if (id != "options") { |
|
850 Services.prefs.setCharPref(this._prefs.LAST_TOOL, id); |
|
851 } |
|
852 |
|
853 return this.loadTool(id).then(panel => { |
|
854 // focus the tool's frame to start receiving key events |
|
855 this.focusTool(id); |
|
856 |
|
857 this.emit("select", id); |
|
858 this.emit(id + "-selected", panel); |
|
859 return panel; |
|
860 }); |
|
861 }, |
|
862 |
|
863 /** |
|
864 * Focus a tool's panel by id |
|
865 * @param {string} id |
|
866 * The id of tool to focus |
|
867 */ |
|
868 focusTool: function(id) { |
|
869 let iframe = this.doc.getElementById("toolbox-panel-iframe-" + id); |
|
870 iframe.focus(); |
|
871 }, |
|
872 |
|
873 /** |
|
874 * Focus split console's input line |
|
875 */ |
|
876 focusConsoleInput: function() { |
|
877 let hud = this.getPanel("webconsole").hud; |
|
878 if (hud && hud.jsterm) { |
|
879 hud.jsterm.inputNode.focus(); |
|
880 } |
|
881 }, |
|
882 |
|
883 /** |
|
884 * Toggles the split state of the webconsole. If the webconsole panel |
|
885 * is already selected, then this command is ignored. |
|
886 */ |
|
887 toggleSplitConsole: function() { |
|
888 let openedConsolePanel = this.currentToolId === "webconsole"; |
|
889 |
|
890 // Don't allow changes when console is open, since it could be confusing |
|
891 if (!openedConsolePanel) { |
|
892 this._splitConsole = !this._splitConsole; |
|
893 this._refreshConsoleDisplay(); |
|
894 this.emit("split-console"); |
|
895 |
|
896 if (this._splitConsole) { |
|
897 this.loadTool("webconsole").then(() => { |
|
898 this.focusConsoleInput(); |
|
899 }); |
|
900 } |
|
901 } |
|
902 }, |
|
903 |
|
904 /** |
|
905 * Loads the tool next to the currently selected tool. |
|
906 */ |
|
907 selectNextTool: function() { |
|
908 let selected = this.doc.querySelector(".devtools-tab[selected]"); |
|
909 let next = selected.nextSibling || selected.parentNode.firstChild; |
|
910 let tool = next.getAttribute("toolid"); |
|
911 return this.selectTool(tool); |
|
912 }, |
|
913 |
|
914 /** |
|
915 * Loads the tool just left to the currently selected tool. |
|
916 */ |
|
917 selectPreviousTool: function() { |
|
918 let selected = this.doc.querySelector(".devtools-tab[selected]"); |
|
919 let previous = selected.previousSibling || selected.parentNode.lastChild; |
|
920 let tool = previous.getAttribute("toolid"); |
|
921 return this.selectTool(tool); |
|
922 }, |
|
923 |
|
924 /** |
|
925 * Highlights the tool's tab if it is not the currently selected tool. |
|
926 * |
|
927 * @param {string} id |
|
928 * The id of the tool to highlight |
|
929 */ |
|
930 highlightTool: function(id) { |
|
931 let tab = this.doc.getElementById("toolbox-tab-" + id); |
|
932 tab && tab.setAttribute("highlighted", "true"); |
|
933 }, |
|
934 |
|
935 /** |
|
936 * De-highlights the tool's tab. |
|
937 * |
|
938 * @param {string} id |
|
939 * The id of the tool to unhighlight |
|
940 */ |
|
941 unhighlightTool: function(id) { |
|
942 let tab = this.doc.getElementById("toolbox-tab-" + id); |
|
943 tab && tab.removeAttribute("highlighted"); |
|
944 }, |
|
945 |
|
946 /** |
|
947 * Raise the toolbox host. |
|
948 */ |
|
949 raise: function() { |
|
950 this._host.raise(); |
|
951 }, |
|
952 |
|
953 /** |
|
954 * Refresh the host's title. |
|
955 */ |
|
956 _refreshHostTitle: function() { |
|
957 let toolName; |
|
958 let toolDef = gDevTools.getToolDefinition(this.currentToolId); |
|
959 if (toolDef) { |
|
960 toolName = toolDef.label; |
|
961 } else { |
|
962 // no tool is selected |
|
963 toolName = toolboxStrings("toolbox.defaultTitle"); |
|
964 } |
|
965 let title = toolboxStrings("toolbox.titleTemplate", |
|
966 toolName, this.target.url || this.target.name); |
|
967 this._host.setTitle(title); |
|
968 }, |
|
969 |
|
970 /** |
|
971 * Create a host object based on the given host type. |
|
972 * |
|
973 * Warning: some hosts require that the toolbox target provides a reference to |
|
974 * the attached tab. Not all Targets have a tab property - make sure you correctly |
|
975 * mix and match hosts and targets. |
|
976 * |
|
977 * @param {string} hostType |
|
978 * The host type of the new host object |
|
979 * |
|
980 * @return {Host} host |
|
981 * The created host object |
|
982 */ |
|
983 _createHost: function(hostType, options) { |
|
984 if (!Hosts[hostType]) { |
|
985 throw new Error("Unknown hostType: " + hostType); |
|
986 } |
|
987 |
|
988 // clean up the toolbox if its window is closed |
|
989 let newHost = new Hosts[hostType](this.target.tab, options); |
|
990 newHost.on("window-closed", this.destroy); |
|
991 return newHost; |
|
992 }, |
|
993 |
|
994 /** |
|
995 * Switch to a new host for the toolbox UI. E.g. |
|
996 * bottom, sidebar, separate window. |
|
997 * |
|
998 * @param {string} hostType |
|
999 * The host type of the new host object |
|
1000 */ |
|
1001 switchHost: function(hostType) { |
|
1002 if (hostType == this._host.type || !this._target.isLocalTab) { |
|
1003 return null; |
|
1004 } |
|
1005 |
|
1006 let newHost = this._createHost(hostType); |
|
1007 return newHost.create().then(iframe => { |
|
1008 // change toolbox document's parent to the new host |
|
1009 iframe.QueryInterface(Ci.nsIFrameLoaderOwner); |
|
1010 iframe.swapFrameLoaders(this.frame); |
|
1011 |
|
1012 this._host.off("window-closed", this.destroy); |
|
1013 this.destroyHost(); |
|
1014 |
|
1015 this._host = newHost; |
|
1016 |
|
1017 if (this.hostType != Toolbox.HostType.CUSTOM) { |
|
1018 Services.prefs.setCharPref(this._prefs.LAST_HOST, this._host.type); |
|
1019 } |
|
1020 |
|
1021 this._buildDockButtons(); |
|
1022 this._addKeysToWindow(); |
|
1023 |
|
1024 this.emit("host-changed"); |
|
1025 }); |
|
1026 }, |
|
1027 |
|
1028 /** |
|
1029 * Handler for the tool-registered event. |
|
1030 * @param {string} event |
|
1031 * Name of the event ("tool-registered") |
|
1032 * @param {string} toolId |
|
1033 * Id of the tool that was registered |
|
1034 */ |
|
1035 _toolRegistered: function(event, toolId) { |
|
1036 let tool = gDevTools.getToolDefinition(toolId); |
|
1037 this._buildTabForTool(tool); |
|
1038 }, |
|
1039 |
|
1040 /** |
|
1041 * Handler for the tool-unregistered event. |
|
1042 * @param {string} event |
|
1043 * Name of the event ("tool-unregistered") |
|
1044 * @param {string|object} toolId |
|
1045 * Definition or id of the tool that was unregistered. Passing the |
|
1046 * tool id should be avoided as it is a temporary measure. |
|
1047 */ |
|
1048 _toolUnregistered: function(event, toolId) { |
|
1049 if (typeof toolId != "string") { |
|
1050 toolId = toolId.id; |
|
1051 } |
|
1052 |
|
1053 if (this._toolPanels.has(toolId)) { |
|
1054 let instance = this._toolPanels.get(toolId); |
|
1055 instance.destroy(); |
|
1056 this._toolPanels.delete(toolId); |
|
1057 } |
|
1058 |
|
1059 let radio = this.doc.getElementById("toolbox-tab-" + toolId); |
|
1060 let panel = this.doc.getElementById("toolbox-panel-" + toolId); |
|
1061 |
|
1062 if (radio) { |
|
1063 if (this.currentToolId == toolId) { |
|
1064 let nextToolName = null; |
|
1065 if (radio.nextSibling) { |
|
1066 nextToolName = radio.nextSibling.getAttribute("toolid"); |
|
1067 } |
|
1068 if (radio.previousSibling) { |
|
1069 nextToolName = radio.previousSibling.getAttribute("toolid"); |
|
1070 } |
|
1071 if (nextToolName) { |
|
1072 this.selectTool(nextToolName); |
|
1073 } |
|
1074 } |
|
1075 radio.parentNode.removeChild(radio); |
|
1076 } |
|
1077 |
|
1078 if (panel) { |
|
1079 panel.parentNode.removeChild(panel); |
|
1080 } |
|
1081 |
|
1082 if (this.hostType == Toolbox.HostType.WINDOW) { |
|
1083 let doc = this.doc.defaultView.parent.document; |
|
1084 let key = doc.getElementById("key_" + toolId); |
|
1085 if (key) { |
|
1086 key.parentNode.removeChild(key); |
|
1087 } |
|
1088 } |
|
1089 }, |
|
1090 |
|
1091 /** |
|
1092 * Initialize the inspector/walker/selection/highlighter fronts. |
|
1093 * Returns a promise that resolves when the fronts are initialized |
|
1094 */ |
|
1095 initInspector: function() { |
|
1096 if (!this._initInspector) { |
|
1097 this._initInspector = Task.spawn(function*() { |
|
1098 this._inspector = InspectorFront(this._target.client, this._target.form); |
|
1099 this._walker = yield this._inspector.getWalker(); |
|
1100 this._selection = new Selection(this._walker); |
|
1101 |
|
1102 if (this.highlighterUtils.isRemoteHighlightable) { |
|
1103 let autohide = !gDevTools.testing; |
|
1104 |
|
1105 this.walker.on("highlighter-ready", this._highlighterReady); |
|
1106 this.walker.on("highlighter-hide", this._highlighterHidden); |
|
1107 |
|
1108 this._highlighter = yield this._inspector.getHighlighter(autohide); |
|
1109 } |
|
1110 }.bind(this)); |
|
1111 } |
|
1112 return this._initInspector; |
|
1113 }, |
|
1114 |
|
1115 /** |
|
1116 * Destroy the inspector/walker/selection fronts |
|
1117 * Returns a promise that resolves when the fronts are destroyed |
|
1118 */ |
|
1119 destroyInspector: function() { |
|
1120 if (this._destroying) { |
|
1121 return this._destroying; |
|
1122 } |
|
1123 |
|
1124 if (!this._inspector) { |
|
1125 return promise.resolve(); |
|
1126 } |
|
1127 |
|
1128 let outstanding = () => { |
|
1129 return Task.spawn(function*() { |
|
1130 yield this.highlighterUtils.stopPicker(); |
|
1131 yield this._inspector.destroy(); |
|
1132 if (this._highlighter) { |
|
1133 yield this._highlighter.destroy(); |
|
1134 } |
|
1135 if (this._selection) { |
|
1136 this._selection.destroy(); |
|
1137 } |
|
1138 |
|
1139 if (this.walker) { |
|
1140 this.walker.off("highlighter-ready", this._highlighterReady); |
|
1141 this.walker.off("highlighter-hide", this._highlighterHidden); |
|
1142 } |
|
1143 |
|
1144 this._inspector = null; |
|
1145 this._highlighter = null; |
|
1146 this._selection = null; |
|
1147 this._walker = null; |
|
1148 }.bind(this)); |
|
1149 }; |
|
1150 |
|
1151 // Releasing the walker (if it has been created) |
|
1152 // This can fail, but in any case, we want to continue destroying the |
|
1153 // inspector/highlighter/selection |
|
1154 let walker = (this._destroying = this._walker) ? |
|
1155 this._walker.release() : |
|
1156 promise.resolve(); |
|
1157 return walker.then(outstanding, outstanding); |
|
1158 }, |
|
1159 |
|
1160 /** |
|
1161 * Get the toolbox's notification box |
|
1162 * |
|
1163 * @return The notification box element. |
|
1164 */ |
|
1165 getNotificationBox: function() { |
|
1166 return this.doc.getElementById("toolbox-notificationbox"); |
|
1167 }, |
|
1168 |
|
1169 /** |
|
1170 * Destroy the current host, and remove event listeners from its frame. |
|
1171 * |
|
1172 * @return {promise} to be resolved when the host is destroyed. |
|
1173 */ |
|
1174 destroyHost: function() { |
|
1175 this.doc.removeEventListener("keypress", |
|
1176 this._splitConsoleOnKeypress, false); |
|
1177 return this._host.destroy(); |
|
1178 }, |
|
1179 |
|
1180 /** |
|
1181 * Remove all UI elements, detach from target and clear up |
|
1182 */ |
|
1183 destroy: function() { |
|
1184 // If several things call destroy then we give them all the same |
|
1185 // destruction promise so we're sure to destroy only once |
|
1186 if (this._destroyer) { |
|
1187 return this._destroyer; |
|
1188 } |
|
1189 |
|
1190 this._target.off("navigate", this._refreshHostTitle); |
|
1191 this.off("select", this._refreshHostTitle); |
|
1192 this.off("host-changed", this._refreshHostTitle); |
|
1193 |
|
1194 gDevTools.off("tool-registered", this._toolRegistered); |
|
1195 gDevTools.off("tool-unregistered", this._toolUnregistered); |
|
1196 |
|
1197 let outstanding = []; |
|
1198 for (let [id, panel] of this._toolPanels) { |
|
1199 try { |
|
1200 outstanding.push(panel.destroy()); |
|
1201 } catch (e) { |
|
1202 // We don't want to stop here if any panel fail to close. |
|
1203 console.error("Panel " + id + ":", e); |
|
1204 } |
|
1205 } |
|
1206 |
|
1207 // Destroying the walker and inspector fronts |
|
1208 outstanding.push(this.destroyInspector()); |
|
1209 // Removing buttons |
|
1210 outstanding.push(() => { |
|
1211 this._pickerButton.removeEventListener("command", this._togglePicker, false); |
|
1212 this._pickerButton = null; |
|
1213 let container = this.doc.getElementById("toolbox-buttons"); |
|
1214 while (container.firstChild) { |
|
1215 container.removeChild(container.firstChild); |
|
1216 } |
|
1217 }); |
|
1218 // Remove the host UI |
|
1219 outstanding.push(this.destroyHost()); |
|
1220 |
|
1221 if (this.target.isLocalTab) { |
|
1222 this._requisition.destroy(); |
|
1223 } |
|
1224 this._telemetry.destroy(); |
|
1225 |
|
1226 return this._destroyer = promise.all(outstanding).then(() => { |
|
1227 // Targets need to be notified that the toolbox is being torn down. |
|
1228 // This is done after other destruction tasks since it may tear down |
|
1229 // fronts and the debugger transport which earlier destroy methods may |
|
1230 // require to complete. |
|
1231 if (!this._target) { |
|
1232 return null; |
|
1233 } |
|
1234 let target = this._target; |
|
1235 this._target = null; |
|
1236 target.off("close", this.destroy); |
|
1237 return target.destroy(); |
|
1238 }).then(() => { |
|
1239 this.emit("destroyed"); |
|
1240 // Free _host after the call to destroyed in order to let a chance |
|
1241 // to destroyed listeners to still query toolbox attributes |
|
1242 this._host = null; |
|
1243 this._toolPanels.clear(); |
|
1244 }).then(null, console.error); |
|
1245 }, |
|
1246 |
|
1247 _highlighterReady: function() { |
|
1248 this.emit("highlighter-ready"); |
|
1249 }, |
|
1250 |
|
1251 _highlighterHidden: function() { |
|
1252 this.emit("highlighter-hide"); |
|
1253 }, |
|
1254 }; |
|
1255 |
|
1256 /** |
|
1257 * The ToolboxHighlighterUtils is what you should use for anything related to |
|
1258 * node highlighting and picking. |
|
1259 * It encapsulates the logic to connecting to the HighlighterActor. |
|
1260 */ |
|
1261 function ToolboxHighlighterUtils(toolbox) { |
|
1262 this.toolbox = toolbox; |
|
1263 this._onPickerNodeHovered = this._onPickerNodeHovered.bind(this); |
|
1264 this._onPickerNodePicked = this._onPickerNodePicked.bind(this); |
|
1265 this.stopPicker = this.stopPicker.bind(this); |
|
1266 } |
|
1267 |
|
1268 ToolboxHighlighterUtils.prototype = { |
|
1269 /** |
|
1270 * Indicates whether the highlighter actor exists on the server. |
|
1271 */ |
|
1272 get isRemoteHighlightable() { |
|
1273 return this.toolbox._target.client.traits.highlightable; |
|
1274 }, |
|
1275 |
|
1276 /** |
|
1277 * Start/stop the element picker on the debuggee target. |
|
1278 */ |
|
1279 togglePicker: function() { |
|
1280 if (this._isPicking) { |
|
1281 return this.stopPicker(); |
|
1282 } else { |
|
1283 return this.startPicker(); |
|
1284 } |
|
1285 }, |
|
1286 |
|
1287 _onPickerNodeHovered: function(res) { |
|
1288 this.toolbox.emit("picker-node-hovered", res.node); |
|
1289 }, |
|
1290 |
|
1291 _onPickerNodePicked: function(res) { |
|
1292 this.toolbox.selection.setNodeFront(res.node, "picker-node-picked"); |
|
1293 this.stopPicker(); |
|
1294 }, |
|
1295 |
|
1296 /** |
|
1297 * Start the element picker on the debuggee target. |
|
1298 * This will request the inspector actor to start listening for mouse/touch |
|
1299 * events on the target to highlight the hovered/picked element. |
|
1300 * Depending on the server-side capabilities, this may fire events when nodes |
|
1301 * are hovered. |
|
1302 * @return A promise that resolves when the picker has started or immediately |
|
1303 * if it is already started |
|
1304 */ |
|
1305 startPicker: function() { |
|
1306 if (this._isPicking) { |
|
1307 return promise.resolve(); |
|
1308 } |
|
1309 |
|
1310 let deferred = promise.defer(); |
|
1311 |
|
1312 let done = () => { |
|
1313 this._isPicking = true; |
|
1314 this.toolbox.emit("picker-started"); |
|
1315 this.toolbox.on("select", this.stopPicker); |
|
1316 deferred.resolve(); |
|
1317 }; |
|
1318 |
|
1319 promise.all([ |
|
1320 this.toolbox.initInspector(), |
|
1321 this.toolbox.selectTool("inspector") |
|
1322 ]).then(() => { |
|
1323 this.toolbox._pickerButton.setAttribute("checked", "true"); |
|
1324 |
|
1325 if (this.isRemoteHighlightable) { |
|
1326 this.toolbox.walker.on("picker-node-hovered", this._onPickerNodeHovered); |
|
1327 this.toolbox.walker.on("picker-node-picked", this._onPickerNodePicked); |
|
1328 |
|
1329 this.toolbox.highlighter.pick().then(done); |
|
1330 } else { |
|
1331 return this.toolbox.walker.pick().then(node => { |
|
1332 this.toolbox.selection.setNodeFront(node, "picker-node-picked").then(() => { |
|
1333 this.stopPicker(); |
|
1334 done(); |
|
1335 }); |
|
1336 }); |
|
1337 } |
|
1338 }); |
|
1339 |
|
1340 return deferred.promise; |
|
1341 }, |
|
1342 |
|
1343 /** |
|
1344 * Stop the element picker |
|
1345 * @return A promise that resolves when the picker has stopped or immediately |
|
1346 * if it is already stopped |
|
1347 */ |
|
1348 stopPicker: function() { |
|
1349 if (!this._isPicking) { |
|
1350 return promise.resolve(); |
|
1351 } |
|
1352 |
|
1353 let deferred = promise.defer(); |
|
1354 |
|
1355 let done = () => { |
|
1356 this.toolbox.emit("picker-stopped"); |
|
1357 this.toolbox.off("select", this.stopPicker); |
|
1358 deferred.resolve(); |
|
1359 }; |
|
1360 |
|
1361 this.toolbox.initInspector().then(() => { |
|
1362 this._isPicking = false; |
|
1363 this.toolbox._pickerButton.removeAttribute("checked"); |
|
1364 if (this.isRemoteHighlightable) { |
|
1365 this.toolbox.highlighter.cancelPick().then(done); |
|
1366 this.toolbox.walker.off("picker-node-hovered", this._onPickerNodeHovered); |
|
1367 this.toolbox.walker.off("picker-node-picked", this._onPickerNodePicked); |
|
1368 } else { |
|
1369 this.toolbox.walker.cancelPick().then(done); |
|
1370 } |
|
1371 }); |
|
1372 |
|
1373 return deferred.promise; |
|
1374 }, |
|
1375 |
|
1376 /** |
|
1377 * Show the box model highlighter on a node, given its NodeFront (this type |
|
1378 * of front is normally returned by the WalkerActor). |
|
1379 * @return a promise that resolves to the nodeFront when the node has been |
|
1380 * highlit |
|
1381 */ |
|
1382 highlightNodeFront: function(nodeFront, options={}) { |
|
1383 let deferred = promise.defer(); |
|
1384 |
|
1385 // If the remote highlighter exists on the target, use it |
|
1386 if (this.isRemoteHighlightable) { |
|
1387 this.toolbox.initInspector().then(() => { |
|
1388 this.toolbox.highlighter.showBoxModel(nodeFront, options).then(() => { |
|
1389 this.toolbox.emit("node-highlight", nodeFront); |
|
1390 deferred.resolve(nodeFront); |
|
1391 }); |
|
1392 }); |
|
1393 } |
|
1394 // Else, revert to the "older" version of the highlighter in the walker |
|
1395 // actor |
|
1396 else { |
|
1397 this.toolbox.walker.highlight(nodeFront).then(() => { |
|
1398 this.toolbox.emit("node-highlight", nodeFront); |
|
1399 deferred.resolve(nodeFront); |
|
1400 }); |
|
1401 } |
|
1402 |
|
1403 return deferred.promise; |
|
1404 }, |
|
1405 |
|
1406 /** |
|
1407 * This is a convenience method in case you don't have a nodeFront but a |
|
1408 * valueGrip. This is often the case with VariablesView properties. |
|
1409 * This method will simply translate the grip into a nodeFront and call |
|
1410 * highlightNodeFront |
|
1411 * @return a promise that resolves to the nodeFront when the node has been |
|
1412 * highlit |
|
1413 */ |
|
1414 highlightDomValueGrip: function(valueGrip, options={}) { |
|
1415 return this._translateGripToNodeFront(valueGrip).then(nodeFront => { |
|
1416 if (nodeFront) { |
|
1417 return this.highlightNodeFront(nodeFront, options); |
|
1418 } else { |
|
1419 return promise.reject(); |
|
1420 } |
|
1421 }); |
|
1422 }, |
|
1423 |
|
1424 _translateGripToNodeFront: function(grip) { |
|
1425 return this.toolbox.initInspector().then(() => { |
|
1426 return this.toolbox.walker.getNodeActorFromObjectActor(grip.actor); |
|
1427 }); |
|
1428 }, |
|
1429 |
|
1430 /** |
|
1431 * Hide the highlighter. |
|
1432 * @return a promise that resolves when the highlighter is hidden |
|
1433 */ |
|
1434 unhighlight: function(forceHide=false) { |
|
1435 let unhighlightPromise; |
|
1436 forceHide = forceHide || !gDevTools.testing; |
|
1437 |
|
1438 if (forceHide && this.isRemoteHighlightable && this.toolbox.highlighter) { |
|
1439 // If the remote highlighter exists on the target, use it |
|
1440 unhighlightPromise = this.toolbox.highlighter.hideBoxModel(); |
|
1441 } else { |
|
1442 // If not, no need to unhighlight as the older highlight method uses a |
|
1443 // setTimeout to hide itself |
|
1444 unhighlightPromise = promise.resolve(); |
|
1445 } |
|
1446 |
|
1447 return unhighlightPromise.then(() => { |
|
1448 this.toolbox.emit("node-unhighlight"); |
|
1449 }); |
|
1450 } |
|
1451 }; |