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.
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/. */
5 "use strict";
7 const MAX_ORDINAL = 99;
8 const ZOOM_PREF = "devtools.toolbox.zoomValue";
9 const MIN_ZOOM = 0.5;
10 const MAX_ZOOM = 2;
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");
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");
25 loader.lazyGetter(this, "Hosts", () => require("devtools/framework/toolbox-hosts").Hosts);
27 loader.lazyImporter(this, "CommandUtils", "resource:///modules/devtools/DeveloperToolbar.jsm");
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 });
44 loader.lazyGetter(this, "Selection", () => require("devtools/framework/selection").Selection);
45 loader.lazyGetter(this, "InspectorFront", () => require("devtools/server/actors/inspector").InspectorFront);
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();
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);
75 this._target.on("close", this.destroy);
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;
88 this._host = this._createHost(hostType, hostOptions);
90 EventEmitter.decorate(this);
92 this._target.on("navigate", this._refreshHostTitle);
93 this.on("host-changed", this._refreshHostTitle);
94 this.on("select", this._refreshHostTitle);
96 gDevTools.on("tool-registered", this._toolRegistered);
97 gDevTools.on("tool-unregistered", this._toolUnregistered);
98 }
99 exports.Toolbox = Toolbox;
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 };
112 Toolbox.prototype = {
113 _URL: "chrome://browser/content/devtools/framework/toolbox.xul",
115 _prefs: {
116 LAST_HOST: "devtools.toolbox.host",
117 LAST_TOOL: "devtools.toolbox.selectedTool",
118 SIDE_ENABLED: "devtools.toolbox.sideEnabled"
119 },
121 currentToolId: null,
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 },
133 /**
134 * Access the panel for a given tool
135 */
136 getPanel: function(id) {
137 return this._toolPanels.get(id);
138 },
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 },
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 },
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 },
166 /**
167 * Get the iframe containing the toolbox UI.
168 */
169 get frame() {
170 return this._host.frame;
171 },
173 /**
174 * Shortcut to the document containing the toolbox UI
175 */
176 get doc() {
177 return this.frame.contentDocument;
178 },
180 /**
181 * Get current zoom level of toolbox
182 */
183 get zoomValue() {
184 return parseFloat(Services.prefs.getCharPref(ZOOM_PREF));
185 },
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 },
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 },
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 },
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 },
223 /**
224 * Get the toggled state of the split console
225 */
226 get splitConsole() {
227 return this._splitConsole;
228 },
230 /**
231 * Open the toolbox
232 */
233 open: function() {
234 let deferred = promise.defer();
236 return this._host.create().then(iframe => {
237 let deferred = promise.defer();
239 let domReady = () => {
240 this.isReady = true;
242 let closeButton = this.doc.getElementById("toolbox-close");
243 closeButton.addEventListener("command", this.destroy, true);
245 this._buildDockButtons();
246 this._buildOptions();
247 this._buildTabs();
248 this._buildButtons();
249 this._addKeysToWindow();
250 this._addToolSwitchingKeys();
251 this._addZoomKeys();
252 this._loadInitialZoom();
254 this._telemetry.toolOpened("toolbox");
256 this.selectTool(this._defaultToolId).then(panel => {
257 this.emit("ready");
258 deferred.resolve();
259 });
260 };
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 });
269 return deferred.promise;
270 });
271 },
273 _buildOptions: function() {
274 let key = this.doc.getElementById("toolbox-options-key");
275 key.addEventListener("command", () => {
276 this.selectTool("options");
277 }, true);
278 },
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 },
291 _splitConsoleOnKeypress: function(e) {
292 let responsiveModeActive = this._isResponsiveModeActive();
293 if (e.keyCode === e.DOM_VK_ESCAPE && !responsiveModeActive) {
294 this.toggleSplitConsole();
295 }
296 },
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);
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 },
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";
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 },
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);
350 let inKey2 = this.doc.getElementById("toolbox-zoom-in-key2");
351 inKey2.addEventListener("command", this.zoomIn.bind(this), true);
353 let outKey = this.doc.getElementById("toolbox-zoom-out-key");
354 outKey.addEventListener("command", this.zoomOut.bind(this), true);
356 let resetKey = this.doc.getElementById("toolbox-zoom-reset-key");
357 resetKey.addEventListener("command", this.zoomReset.bind(this), true);
358 },
360 /**
361 * Set zoom on toolbox to whatever the last setting was.
362 */
363 _loadInitialZoom: function() {
364 this.setZoom(this.zoomValue);
365 },
367 /**
368 * Increase zoom level of toolbox window - make things bigger.
369 */
370 zoomIn: function() {
371 this.setZoom(this.zoomValue + 0.1);
372 },
374 /**
375 * Decrease zoom level of toolbox window - make things smaller.
376 */
377 zoomOut: function() {
378 this.setZoom(this.zoomValue - 0.1);
379 },
381 /**
382 * Reset zoom level of the toolbox window.
383 */
384 zoomReset: function() {
385 this.setZoom(1);
386 },
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);
399 let contViewer = this.frame.docShell.contentViewer;
400 let docViewer = contViewer.QueryInterface(Ci.nsIMarkupDocumentViewer);
402 docViewer.fullZoom = zoomValue;
404 Services.prefs.setCharPref(ZOOM_PREF, zoomValue);
405 },
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 }
415 let doc = this.doc.defaultView.parent.document;
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 }
423 let toolId = id;
424 let key = doc.createElement("key");
426 key.id = "key_" + toolId;
428 if (toolDefinition.key.startsWith("VK_")) {
429 key.setAttribute("keycode", toolDefinition.key);
430 } else {
431 key.setAttribute("key", toolDefinition.key);
432 }
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 }
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";
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 },
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);
465 if (toolDefinition.onkey &&
466 ((this.currentToolId === toolId) ||
467 (toolId == "webconsole" && this.splitConsole))) {
468 toolDefinition.onkey(this.getCurrentPanel(), this);
469 }
470 },
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");
479 while (dockBox.firstChild) {
480 dockBox.removeChild(dockBox.firstChild);
481 }
483 if (!this._target.isLocalTab) {
484 return;
485 }
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 }
494 let sideEnabled = Services.prefs.getBoolPref(this._prefs.SIDE_ENABLED);
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 }
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 });
513 dockBox.appendChild(button);
514 }
515 },
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 },
526 /**
527 * Add buttons to the UI as specified in the devtools.toolbox.toolbarSpec pref
528 */
529 _buildButtons: function() {
530 this._buildPickerButton();
532 if (!this.target.isLocalTab) {
533 return;
534 }
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 },
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"));
556 let container = this.doc.querySelector("#toolbox-buttons");
557 container.appendChild(this._pickerButton);
559 this._togglePicker = this.highlighterUtils.togglePicker.bind(this.highlighterUtils);
560 this._pickerButton.addEventListener("command", this._togglePicker, false);
561 },
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 },
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) { }
605 if (button) {
606 if (on) {
607 button.removeAttribute("hidden");
608 } else {
609 button.setAttribute("hidden", "true");
610 }
611 }
612 });
613 },
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 }
626 let tabs = this.doc.getElementById("toolbox-tabs");
627 let deck = this.doc.getElementById("toolbox-deck");
629 let id = toolDefinition.id;
631 if (toolDefinition.ordinal == undefined || toolDefinition.ordinal < 0) {
632 toolDefinition.ordinal = MAX_ORDINAL;
633 }
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 }
648 radio.addEventListener("command", () => {
649 this.selectTool(id);
650 });
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);
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 }
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 }
680 if (!toolDefinition.bgTheme) {
681 toolDefinition.bgTheme = "theme-toolbar";
682 }
683 let vbox = this.doc.createElement("vbox");
684 vbox.className = "toolbox-panel " + toolDefinition.bgTheme;
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 }
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 }
708 this._addKeysToWindow();
709 },
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 }
724 let deferred = promise.defer();
725 let iframe = this.doc.getElementById("toolbox-panel-iframe-" + id);
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 }
739 let definition = gDevTools.getToolDefinition(id);
740 if (!definition) {
741 deferred.reject(new Error("no such tool id "+id));
742 return deferred.promise;
743 }
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";
753 let vbox = this.doc.getElementById("toolbox-panel-" + id);
754 vbox.appendChild(iframe);
756 let onLoad = () => {
757 // Prevent flicker while loading by waiting to make visible until now.
758 iframe.style.visibility = "visible";
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 };
769 iframe.setAttribute("src", definition.url);
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 }
788 return deferred.promise;
789 },
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 }
803 let tab = this.doc.getElementById("toolbox-tab-" + id);
804 tab.setAttribute("selected", "true");
806 if (this.currentToolId == id) {
807 // re-focus tool to get key events again
808 this.focusTool(id);
810 // Return the existing panel in order to have a consistent return value.
811 return promise.resolve(this._toolPanels.get(id));
812 }
814 if (!this.isReady) {
815 throw new Error("Can't select tool, wait for toolbox 'ready' event");
816 }
818 tab = this.doc.getElementById("toolbox-tab-" + id);
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 }
829 let tabstrip = this.doc.getElementById("toolbox-tabs");
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;
843 // and select the right iframe
844 let deck = this.doc.getElementById("toolbox-deck");
845 deck.selectedIndex = index;
847 this.currentToolId = id;
848 this._refreshConsoleDisplay();
849 if (id != "options") {
850 Services.prefs.setCharPref(this._prefs.LAST_TOOL, id);
851 }
853 return this.loadTool(id).then(panel => {
854 // focus the tool's frame to start receiving key events
855 this.focusTool(id);
857 this.emit("select", id);
858 this.emit(id + "-selected", panel);
859 return panel;
860 });
861 },
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 },
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 },
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";
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");
896 if (this._splitConsole) {
897 this.loadTool("webconsole").then(() => {
898 this.focusConsoleInput();
899 });
900 }
901 }
902 },
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 },
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 },
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 },
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 },
946 /**
947 * Raise the toolbox host.
948 */
949 raise: function() {
950 this._host.raise();
951 },
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 },
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 }
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 },
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 }
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);
1012 this._host.off("window-closed", this.destroy);
1013 this.destroyHost();
1015 this._host = newHost;
1017 if (this.hostType != Toolbox.HostType.CUSTOM) {
1018 Services.prefs.setCharPref(this._prefs.LAST_HOST, this._host.type);
1019 }
1021 this._buildDockButtons();
1022 this._addKeysToWindow();
1024 this.emit("host-changed");
1025 });
1026 },
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 },
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 }
1053 if (this._toolPanels.has(toolId)) {
1054 let instance = this._toolPanels.get(toolId);
1055 instance.destroy();
1056 this._toolPanels.delete(toolId);
1057 }
1059 let radio = this.doc.getElementById("toolbox-tab-" + toolId);
1060 let panel = this.doc.getElementById("toolbox-panel-" + toolId);
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 }
1078 if (panel) {
1079 panel.parentNode.removeChild(panel);
1080 }
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 },
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);
1102 if (this.highlighterUtils.isRemoteHighlightable) {
1103 let autohide = !gDevTools.testing;
1105 this.walker.on("highlighter-ready", this._highlighterReady);
1106 this.walker.on("highlighter-hide", this._highlighterHidden);
1108 this._highlighter = yield this._inspector.getHighlighter(autohide);
1109 }
1110 }.bind(this));
1111 }
1112 return this._initInspector;
1113 },
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 }
1124 if (!this._inspector) {
1125 return promise.resolve();
1126 }
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 }
1139 if (this.walker) {
1140 this.walker.off("highlighter-ready", this._highlighterReady);
1141 this.walker.off("highlighter-hide", this._highlighterHidden);
1142 }
1144 this._inspector = null;
1145 this._highlighter = null;
1146 this._selection = null;
1147 this._walker = null;
1148 }.bind(this));
1149 };
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 },
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 },
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 },
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 }
1190 this._target.off("navigate", this._refreshHostTitle);
1191 this.off("select", this._refreshHostTitle);
1192 this.off("host-changed", this._refreshHostTitle);
1194 gDevTools.off("tool-registered", this._toolRegistered);
1195 gDevTools.off("tool-unregistered", this._toolUnregistered);
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 }
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());
1221 if (this.target.isLocalTab) {
1222 this._requisition.destroy();
1223 }
1224 this._telemetry.destroy();
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 },
1247 _highlighterReady: function() {
1248 this.emit("highlighter-ready");
1249 },
1251 _highlighterHidden: function() {
1252 this.emit("highlighter-hide");
1253 },
1254 };
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 }
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 },
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 },
1287 _onPickerNodeHovered: function(res) {
1288 this.toolbox.emit("picker-node-hovered", res.node);
1289 },
1291 _onPickerNodePicked: function(res) {
1292 this.toolbox.selection.setNodeFront(res.node, "picker-node-picked");
1293 this.stopPicker();
1294 },
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 }
1310 let deferred = promise.defer();
1312 let done = () => {
1313 this._isPicking = true;
1314 this.toolbox.emit("picker-started");
1315 this.toolbox.on("select", this.stopPicker);
1316 deferred.resolve();
1317 };
1319 promise.all([
1320 this.toolbox.initInspector(),
1321 this.toolbox.selectTool("inspector")
1322 ]).then(() => {
1323 this.toolbox._pickerButton.setAttribute("checked", "true");
1325 if (this.isRemoteHighlightable) {
1326 this.toolbox.walker.on("picker-node-hovered", this._onPickerNodeHovered);
1327 this.toolbox.walker.on("picker-node-picked", this._onPickerNodePicked);
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 });
1340 return deferred.promise;
1341 },
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 }
1353 let deferred = promise.defer();
1355 let done = () => {
1356 this.toolbox.emit("picker-stopped");
1357 this.toolbox.off("select", this.stopPicker);
1358 deferred.resolve();
1359 };
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 });
1373 return deferred.promise;
1374 },
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();
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 }
1403 return deferred.promise;
1404 },
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 },
1424 _translateGripToNodeFront: function(grip) {
1425 return this.toolbox.initInspector().then(() => {
1426 return this.toolbox.walker.getNodeActorFromObjectActor(grip.actor);
1427 });
1428 },
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;
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 }
1447 return unhighlightPromise.then(() => {
1448 this.toolbox.emit("node-unhighlight");
1449 });
1450 }
1451 };