Bug 1568157 - Part 2: Replace `toolbox.{inspector,walker,selection,highlighter}`...
[gecko.git] / devtools / client / inspector / inspector.js
blob0d6b9a69d817c0b4738a9b014f0082bbb97cd4e4
1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 /* global window, BrowserLoader */
9 "use strict";
11 const Services = require("Services");
12 const promise = require("promise");
13 const EventEmitter = require("devtools/shared/event-emitter");
14 const { executeSoon } = require("devtools/shared/DevToolsUtils");
15 const { Toolbox } = require("devtools/client/framework/toolbox");
16 const ReflowTracker = require("devtools/client/inspector/shared/reflow-tracker");
17 const Store = require("devtools/client/inspector/store");
18 const InspectorStyleChangeTracker = require("devtools/client/inspector/shared/style-change-tracker");
20 // Use privileged promise in panel documents to prevent having them to freeze
21 // during toolbox destruction. See bug 1402779.
22 const Promise = require("Promise");
24 loader.lazyRequireGetter(
25   this,
26   "initCssProperties",
27   "devtools/shared/fronts/css-properties",
28   true
30 loader.lazyRequireGetter(
31   this,
32   "HTMLBreadcrumbs",
33   "devtools/client/inspector/breadcrumbs",
34   true
36 loader.lazyRequireGetter(
37   this,
38   "KeyShortcuts",
39   "devtools/client/shared/key-shortcuts"
41 loader.lazyRequireGetter(
42   this,
43   "InspectorSearch",
44   "devtools/client/inspector/inspector-search",
45   true
47 loader.lazyRequireGetter(
48   this,
49   "ToolSidebar",
50   "devtools/client/inspector/toolsidebar",
51   true
53 loader.lazyRequireGetter(
54   this,
55   "MarkupView",
56   "devtools/client/inspector/markup/markup"
58 loader.lazyRequireGetter(
59   this,
60   "HighlightersOverlay",
61   "devtools/client/inspector/shared/highlighters-overlay"
63 loader.lazyRequireGetter(
64   this,
65   "ExtensionSidebar",
66   "devtools/client/inspector/extensions/extension-sidebar"
68 loader.lazyRequireGetter(
69   this,
70   "saveScreenshot",
71   "devtools/shared/screenshot/save"
74 loader.lazyImporter(
75   this,
76   "DeferredTask",
77   "resource://gre/modules/DeferredTask.jsm"
80 const { LocalizationHelper, localizeMarkup } = require("devtools/shared/l10n");
81 const INSPECTOR_L10N = new LocalizationHelper(
82   "devtools/client/locales/inspector.properties"
85 // Sidebar dimensions
86 const INITIAL_SIDEBAR_SIZE = 350;
88 // How long we wait to debounce resize events
89 const LAZY_RESIZE_INTERVAL_MS = 200;
91 // If the toolbox's width is smaller than the given amount of pixels, the sidebar
92 // automatically switches from 'landscape/horizontal' to 'portrait/vertical' mode.
93 const PORTRAIT_MODE_WIDTH_THRESHOLD = 700;
94 // If the toolbox's width docked to the side is smaller than the given amount of pixels,
95 // the sidebar automatically switches from 'landscape/horizontal' to 'portrait/vertical'
96 // mode.
97 const SIDE_PORTAIT_MODE_WIDTH_THRESHOLD = 1000;
99 const THREE_PANE_ENABLED_PREF = "devtools.inspector.three-pane-enabled";
100 const THREE_PANE_ENABLED_SCALAR = "devtools.inspector.three_pane_enabled";
101 const THREE_PANE_CHROME_ENABLED_PREF =
102   "devtools.inspector.chrome.three-pane-enabled";
103 const TELEMETRY_EYEDROPPER_OPENED = "devtools.toolbar.eyedropper.opened";
104 const TELEMETRY_SCALAR_NODE_SELECTION_COUNT =
105   "devtools.inspector.node_selection_count";
108  * Represents an open instance of the Inspector for a tab.
109  * The inspector controls the breadcrumbs, the markup view, and the sidebar
110  * (computed view, rule view, font view and animation inspector).
112  * Events:
113  * - ready
114  *      Fired when the inspector panel is opened for the first time and ready to
115  *      use
116  * - new-root
117  *      Fired after a new root (navigation to a new page) event was fired by
118  *      the walker, and taken into account by the inspector (after the markup
119  *      view has been reloaded)
120  * - markuploaded
121  *      Fired when the markup-view frame has loaded
122  * - breadcrumbs-updated
123  *      Fired when the breadcrumb widget updates to a new node
124  * - boxmodel-view-updated
125  *      Fired when the box model updates to a new node
126  * - markupmutation
127  *      Fired after markup mutations have been processed by the markup-view
128  * - computed-view-refreshed
129  *      Fired when the computed rules view updates to a new node
130  * - computed-view-property-expanded
131  *      Fired when a property is expanded in the computed rules view
132  * - computed-view-property-collapsed
133  *      Fired when a property is collapsed in the computed rules view
134  * - computed-view-sourcelinks-updated
135  *      Fired when the stylesheet source links have been updated (when switching
136  *      to source-mapped files)
137  * - rule-view-refreshed
138  *      Fired when the rule view updates to a new node
139  * - rule-view-sourcelinks-updated
140  *      Fired when the stylesheet source links have been updated (when switching
141  *      to source-mapped files)
142  */
143 function Inspector(toolbox) {
144   EventEmitter.decorate(this);
146   this._toolbox = toolbox;
147   this._target = toolbox.target;
148   this.panelDoc = window.document;
149   this.panelWin = window;
150   this.panelWin.inspector = this;
151   this.telemetry = toolbox.telemetry;
152   this.store = Store();
154   // Map [panel id => panel instance]
155   // Stores all the instances of sidebar panels like rule view, computed view, ...
156   this._panels = new Map();
158   this._clearSearchResultsLabel = this._clearSearchResultsLabel.bind(this);
159   this._handleRejectionIfNotDestroyed = this._handleRejectionIfNotDestroyed.bind(
160     this
161   );
162   this._onBeforeNavigate = this._onBeforeNavigate.bind(this);
163   this._onMarkupFrameLoad = this._onMarkupFrameLoad.bind(this);
164   this._updateSearchResultsLabel = this._updateSearchResultsLabel.bind(this);
166   this.onDetached = this.onDetached.bind(this);
167   this.onHostChanged = this.onHostChanged.bind(this);
168   this.onMarkupLoaded = this.onMarkupLoaded.bind(this);
169   this.onNewSelection = this.onNewSelection.bind(this);
170   this.onNewRoot = this.onNewRoot.bind(this);
171   this.onPanelWindowResize = this.onPanelWindowResize.bind(this);
172   this.onShowBoxModelHighlighterForNode = this.onShowBoxModelHighlighterForNode.bind(
173     this
174   );
175   this.onSidebarHidden = this.onSidebarHidden.bind(this);
176   this.onSidebarResized = this.onSidebarResized.bind(this);
177   this.onSidebarSelect = this.onSidebarSelect.bind(this);
178   this.onSidebarShown = this.onSidebarShown.bind(this);
179   this.onSidebarToggle = this.onSidebarToggle.bind(this);
180   this.handleThreadPaused = this.handleThreadPaused.bind(this);
181   this.handleThreadResumed = this.handleThreadResumed.bind(this);
184 Inspector.prototype = {
185   /**
186    * InspectorPanel.open() is effectively an asynchronous constructor.
187    * Set any attributes or listeners that rely on the document being loaded or fronts
188    * from the InspectorFront and Target here.
189    */
190   async init() {
191     // Localize all the nodes containing a data-localization attribute.
192     localizeMarkup(this.panelDoc);
194     // When replaying, we need to listen to changes in the target's pause state.
195     if (this.target.isReplayEnabled()) {
196       let dbg = this._toolbox.getPanel("jsdebugger");
197       if (!dbg) {
198         dbg = await this._toolbox.loadTool("jsdebugger");
199       }
200       this._replayResumed = !dbg.isPaused();
202       this.target.threadFront.on("paused", this.handleThreadPaused);
203       this.target.threadFront.on("resumed", this.handleThreadResumed);
204     }
206     await this.initInspectorFront();
208     this.target.on("will-navigate", this._onBeforeNavigate);
210     await Promise.all([
211       this._getCssProperties(),
212       this._getPageStyle(),
213       this._getDefaultSelection(),
214       this._getAccessibilityFront(),
215       this._getChangesFront(),
216     ]);
218     // Store the URL of the target page prior to navigation in order to ensure
219     // telemetry counts in the Grid Inspector are not double counted on reload.
220     this.previousURL = this.target.url;
221     this.reflowTracker = new ReflowTracker(this.target);
222     this.styleChangeTracker = new InspectorStyleChangeTracker(this);
224     this._markupBox = this.panelDoc.getElementById("markup-box");
226     return this._deferredOpen();
227   },
229   async initInspectorFront() {
230     this.inspectorFront = await this.target.getFront("inspector");
231     this.highlighter = this.inspectorFront.highlighter;
232     this.selection = this.inspectorFront.selection;
233     this.walker = this.inspectorFront.walker;
234   },
236   get toolbox() {
237     return this._toolbox;
238   },
240   get highlighters() {
241     if (!this._highlighters) {
242       this._highlighters = new HighlightersOverlay(this);
243     }
245     return this._highlighters;
246   },
248   get isHighlighterReady() {
249     return !!this._highlighters;
250   },
252   get is3PaneModeEnabled() {
253     if (this.target.chrome) {
254       if (!this._is3PaneModeChromeEnabled) {
255         this._is3PaneModeChromeEnabled = Services.prefs.getBoolPref(
256           THREE_PANE_CHROME_ENABLED_PREF
257         );
258       }
260       return this._is3PaneModeChromeEnabled;
261     }
263     if (!this._is3PaneModeEnabled) {
264       this._is3PaneModeEnabled = Services.prefs.getBoolPref(
265         THREE_PANE_ENABLED_PREF
266       );
267     }
269     return this._is3PaneModeEnabled;
270   },
272   set is3PaneModeEnabled(value) {
273     if (this.target.chrome) {
274       this._is3PaneModeChromeEnabled = value;
275       Services.prefs.setBoolPref(
276         THREE_PANE_CHROME_ENABLED_PREF,
277         this._is3PaneModeChromeEnabled
278       );
279     } else {
280       this._is3PaneModeEnabled = value;
281       Services.prefs.setBoolPref(
282         THREE_PANE_ENABLED_PREF,
283         this._is3PaneModeEnabled
284       );
285     }
286   },
288   get search() {
289     if (!this._search) {
290       this._search = new InspectorSearch(
291         this,
292         this.searchBox,
293         this.searchClearButton
294       );
295     }
297     return this._search;
298   },
300   get cssProperties() {
301     return this._cssProperties.cssProperties;
302   },
304   /**
305    * Handle promise rejections for various asynchronous actions, and only log errors if
306    * the inspector panel still exists.
307    * This is useful to silence useless errors that happen when the inspector is closed
308    * while still initializing (and making protocol requests).
309    */
310   _handleRejectionIfNotDestroyed: function(e) {
311     if (!this._destroyed) {
312       console.error(e);
313     }
314   },
316   _deferredOpen: async function() {
317     const onMarkupLoaded = this.once("markuploaded");
318     this._initMarkup();
319     this.isReady = false;
321     // Set the node front so that the markup and sidebar panels will have the selected
322     // nodeFront ready when they're initialized.
323     if (this._defaultNode) {
324       this.selection.setNodeFront(this._defaultNode, {
325         reason: "inspector-open",
326       });
327     }
329     // Setup the splitter before the sidebar is displayed so, we don't miss any events.
330     this.setupSplitter();
332     // We can display right panel with: tab bar, markup view and breadbrumb. Right after
333     // the splitter set the right and left panel sizes, in order to avoid resizing it
334     // during load of the inspector.
335     this.panelDoc.getElementById("inspector-main-content").style.visibility =
336       "visible";
338     // Setup the sidebar panels.
339     await this.setupSidebar();
341     await onMarkupLoaded;
342     this.isReady = true;
344     // All the components are initialized. Take care of the remaining initialization
345     // and setup.
346     this.breadcrumbs = new HTMLBreadcrumbs(this);
347     this.setupExtensionSidebars();
348     this.setupSearchBox();
349     await this.setupToolbar();
351     this.onNewSelection();
353     this.walker.on("new-root", this.onNewRoot);
354     this.toolbox.on("host-changed", this.onHostChanged);
355     this.selection.on("new-node-front", this.onNewSelection);
356     this.selection.on("detached-front", this.onDetached);
358     // Log the 3 pane inspector setting on inspector open. The question we want to answer
359     // is:
360     // "What proportion of users use the 3 pane vs 2 pane inspector on inspector open?"
361     this.telemetry.keyedScalarAdd(
362       THREE_PANE_ENABLED_SCALAR,
363       this.is3PaneModeEnabled,
364       1
365     );
367     this.emit("ready");
368     return this;
369   },
371   _onBeforeNavigate: function() {
372     this._defaultNode = null;
373     this.selection.setNodeFront(null);
374     this._destroyMarkup();
375     this._pendingSelection = null;
376   },
378   _getCssProperties: function() {
379     return initCssProperties(this.toolbox).then(cssProperties => {
380       this._cssProperties = cssProperties;
381     }, this._handleRejectionIfNotDestroyed);
382   },
384   _getAccessibilityFront: async function() {
385     this.accessibilityFront = await this.target.getFront("accessibility");
386     return this.accessibilityFront;
387   },
389   _getChangesFront: async function() {
390     // Get the Changes front, then call a method on it, which will instantiate
391     // the ChangesActor. We want the ChangesActor to be guaranteed available before
392     // the user makes any changes.
393     this.changesFront = await this.toolbox.target.getFront("changes");
394     await this.changesFront.start();
395     return this.changesFront;
396   },
398   _getDefaultSelection: function() {
399     // This may throw if the document is still loading and we are
400     // refering to a dead about:blank document
401     return this._getDefaultNodeForSelection().catch(
402       this._handleRejectionIfNotDestroyed
403     );
404   },
406   _getPageStyle: function() {
407     return this.inspectorFront.getPageStyle().then(pageStyle => {
408       this.pageStyle = pageStyle;
409     }, this._handleRejectionIfNotDestroyed);
410   },
412   /**
413    * Return a promise that will resolve to the default node for selection.
414    */
415   _getDefaultNodeForSelection: function() {
416     if (this._defaultNode) {
417       return this._defaultNode;
418     }
419     const walker = this.walker;
420     let rootNode = null;
421     const pendingSelection = this._pendingSelection;
423     // A helper to tell if the target has or is about to navigate.
424     // this._pendingSelection changes on "will-navigate" and "new-root" events.
425     // When replaying, if the target is unpaused then we consider it to be
426     // navigating so that its tree will not be constructed.
427     const hasNavigated = () => {
428       return pendingSelection !== this._pendingSelection || this._replayResumed;
429     };
431     // If available, set either the previously selected node or the body
432     // as default selected, else set documentElement
433     return walker
434       .getRootNode()
435       .then(node => {
436         if (hasNavigated()) {
437           return promise.reject(
438             "navigated; resolution of _defaultNode aborted"
439           );
440         }
442         rootNode = node;
443         if (this.selectionCssSelector) {
444           return walker.querySelector(rootNode, this.selectionCssSelector);
445         }
446         return null;
447       })
448       .then(front => {
449         if (hasNavigated()) {
450           return promise.reject(
451             "navigated; resolution of _defaultNode aborted"
452           );
453         }
455         if (front) {
456           return front;
457         }
458         return walker.querySelector(rootNode, "body");
459       })
460       .then(front => {
461         if (hasNavigated()) {
462           return promise.reject(
463             "navigated; resolution of _defaultNode aborted"
464           );
465         }
467         if (front) {
468           return front;
469         }
470         return this.walker.documentElement();
471       })
472       .then(node => {
473         if (hasNavigated()) {
474           return promise.reject(
475             "navigated; resolution of _defaultNode aborted"
476           );
477         }
478         this._defaultNode = node;
479         return node;
480       });
481   },
483   /**
484    * Target getter.
485    */
486   get target() {
487     return this._target;
488   },
490   /**
491    * Target setter.
492    */
493   set target(value) {
494     this._target = value;
495   },
497   /**
498    * Hooks the searchbar to show result and auto completion suggestions.
499    */
500   setupSearchBox: function() {
501     this.searchBox = this.panelDoc.getElementById("inspector-searchbox");
502     this.searchClearButton = this.panelDoc.getElementById(
503       "inspector-searchinput-clear"
504     );
505     this.searchResultsContainer = this.panelDoc.getElementById(
506       "inspector-searchlabel-container"
507     );
508     this.searchResultsLabel = this.panelDoc.getElementById(
509       "inspector-searchlabel"
510     );
512     this.searchBox.addEventListener(
513       "focus",
514       () => {
515         this.search.on("search-cleared", this._clearSearchResultsLabel);
516         this.search.on("search-result", this._updateSearchResultsLabel);
517       },
518       { once: true }
519     );
521     this.createSearchBoxShortcuts();
522   },
524   createSearchBoxShortcuts() {
525     this.searchboxShortcuts = new KeyShortcuts({
526       window: this.panelDoc.defaultView,
527       // The inspector search shortcuts need to be available from everywhere in the
528       // inspector, and the inspector uses iframes (markupview, sidepanel webextensions).
529       // Use the chromeEventHandler as the target to catch events from all frames.
530       target: this.toolbox.getChromeEventHandler(),
531     });
532     const key = INSPECTOR_L10N.getStr("inspector.searchHTML.key");
533     this.searchboxShortcuts.on(key, event => {
534       // Prevent overriding same shortcut from the computed/rule views
535       if (
536         event.target.closest("#sidebar-panel-ruleview") ||
537         event.target.closest("#sidebar-panel-computedview")
538       ) {
539         return;
540       }
541       event.preventDefault();
542       this.searchBox.focus();
543     });
544   },
546   get searchSuggestions() {
547     return this.search.autocompleter;
548   },
550   _clearSearchResultsLabel: function(result) {
551     return this._updateSearchResultsLabel(result, true);
552   },
554   _updateSearchResultsLabel: function(result, clear = false) {
555     let str = "";
556     if (!clear) {
557       if (result) {
558         str = INSPECTOR_L10N.getFormatStr(
559           "inspector.searchResultsCount2",
560           result.resultsIndex + 1,
561           result.resultsLength
562         );
563       } else {
564         str = INSPECTOR_L10N.getStr("inspector.searchResultsNone");
565       }
567       this.searchResultsContainer.hidden = false;
568     } else {
569       this.searchResultsContainer.hidden = true;
570     }
572     this.searchResultsLabel.textContent = str;
573   },
575   get React() {
576     return this._toolbox.React;
577   },
579   get ReactDOM() {
580     return this._toolbox.ReactDOM;
581   },
583   get ReactRedux() {
584     return this._toolbox.ReactRedux;
585   },
587   get browserRequire() {
588     return this._toolbox.browserRequire;
589   },
591   get InspectorTabPanel() {
592     if (!this._InspectorTabPanel) {
593       this._InspectorTabPanel = this.React.createFactory(
594         this.browserRequire(
595           "devtools/client/inspector/components/InspectorTabPanel"
596         )
597       );
598     }
599     return this._InspectorTabPanel;
600   },
602   get InspectorSplitBox() {
603     if (!this._InspectorSplitBox) {
604       this._InspectorSplitBox = this.React.createFactory(
605         this.browserRequire(
606           "devtools/client/shared/components/splitter/SplitBox"
607         )
608       );
609     }
610     return this._InspectorSplitBox;
611   },
613   get TabBar() {
614     if (!this._TabBar) {
615       this._TabBar = this.React.createFactory(
616         this.browserRequire("devtools/client/shared/components/tabs/TabBar")
617       );
618     }
619     return this._TabBar;
620   },
622   /**
623    * Check if the inspector should use the landscape mode.
624    *
625    * @return {Boolean} true if the inspector should be in landscape mode.
626    */
627   useLandscapeMode: function() {
628     if (!this.panelDoc) {
629       return true;
630     }
632     const splitterBox = this.panelDoc.getElementById("inspector-splitter-box");
633     const { width } = window.windowUtils.getBoundsWithoutFlushing(splitterBox);
635     return this.is3PaneModeEnabled &&
636       (this.toolbox.hostType == Toolbox.HostType.LEFT ||
637         this.toolbox.hostType == Toolbox.HostType.RIGHT)
638       ? width > SIDE_PORTAIT_MODE_WIDTH_THRESHOLD
639       : width > PORTRAIT_MODE_WIDTH_THRESHOLD;
640   },
642   /**
643    * Build Splitter located between the main and side area of
644    * the Inspector panel.
645    */
646   setupSplitter: function() {
647     const { width, height, splitSidebarWidth } = this.getSidebarSize();
649     this.sidebarSplitBoxRef = this.React.createRef();
651     const splitter = this.InspectorSplitBox({
652       className: "inspector-sidebar-splitter",
653       initialWidth: width,
654       initialHeight: height,
655       minSize: "10%",
656       maxSize: "80%",
657       splitterSize: 1,
658       endPanelControl: true,
659       startPanel: this.InspectorTabPanel({
660         id: "inspector-main-content",
661       }),
662       endPanel: this.InspectorSplitBox({
663         initialWidth: splitSidebarWidth,
664         minSize: 10,
665         maxSize: "80%",
666         splitterSize: this.is3PaneModeEnabled ? 1 : 0,
667         endPanelControl: this.is3PaneModeEnabled,
668         startPanel: this.InspectorTabPanel({
669           id: "inspector-rules-container",
670         }),
671         endPanel: this.InspectorTabPanel({
672           id: "inspector-sidebar-container",
673         }),
674         ref: this.sidebarSplitBoxRef,
675       }),
676       vert: this.useLandscapeMode(),
677       onControlledPanelResized: this.onSidebarResized,
678     });
680     this.splitBox = this.ReactDOM.render(
681       splitter,
682       this.panelDoc.getElementById("inspector-splitter-box")
683     );
685     this.panelWin.addEventListener("resize", this.onPanelWindowResize, true);
686   },
688   _onLazyPanelResize: async function() {
689     // We can be called on a closed window because of the deferred task.
690     if (window.closed) {
691       return;
692     }
694     this.splitBox.setState({ vert: this.useLandscapeMode() });
695     this.emit("inspector-resize");
696   },
698   /**
699    * If Toolbox width is less than 600 px, the splitter changes its mode
700    * to `horizontal` to support portrait view.
701    */
702   onPanelWindowResize: function() {
703     if (this.toolbox.currentToolId !== "inspector") {
704       return;
705     }
707     if (!this._lazyResizeHandler) {
708       this._lazyResizeHandler = new DeferredTask(
709         this._onLazyPanelResize.bind(this),
710         LAZY_RESIZE_INTERVAL_MS,
711         0
712       );
713     }
714     this._lazyResizeHandler.arm();
715   },
717   getSidebarSize: function() {
718     let width;
719     let height;
720     let splitSidebarWidth;
722     // Initialize splitter size from preferences.
723     try {
724       width = Services.prefs.getIntPref("devtools.toolsidebar-width.inspector");
725       height = Services.prefs.getIntPref(
726         "devtools.toolsidebar-height.inspector"
727       );
728       splitSidebarWidth = Services.prefs.getIntPref(
729         "devtools.toolsidebar-width.inspector.splitsidebar"
730       );
731     } catch (e) {
732       // Set width and height of the splitter. Only one
733       // value is really useful at a time depending on the current
734       // orientation (vertical/horizontal).
735       // Having both is supported by the splitter component.
736       width = this.is3PaneModeEnabled
737         ? INITIAL_SIDEBAR_SIZE * 2
738         : INITIAL_SIDEBAR_SIZE;
739       height = INITIAL_SIDEBAR_SIZE;
740       splitSidebarWidth = INITIAL_SIDEBAR_SIZE;
741     }
743     return { width, height, splitSidebarWidth };
744   },
746   onSidebarHidden: function() {
747     // Store the current splitter size to preferences.
748     const state = this.splitBox.state;
749     Services.prefs.setIntPref(
750       "devtools.toolsidebar-width.inspector",
751       state.width
752     );
753     Services.prefs.setIntPref(
754       "devtools.toolsidebar-height.inspector",
755       state.height
756     );
757     Services.prefs.setIntPref(
758       "devtools.toolsidebar-width.inspector.splitsidebar",
759       this.sidebarSplitBoxRef.current.state.width
760     );
761   },
763   onSidebarResized: function(width, height) {
764     this.toolbox.emit("inspector-sidebar-resized", { width, height });
765   },
767   onSidebarSelect: function(toolId) {
768     // Save the currently selected sidebar panel
769     Services.prefs.setCharPref("devtools.inspector.activeSidebar", toolId);
771     // Then forces the panel creation by calling getPanel
772     // (This allows lazy loading the panels only once we select them)
773     this.getPanel(toolId);
775     this.toolbox.emit("inspector-sidebar-select", toolId);
776   },
778   onSidebarShown: function() {
779     const { width, height, splitSidebarWidth } = this.getSidebarSize();
780     this.splitBox.setState({ width, height });
781     this.sidebarSplitBoxRef.current.setState({ width: splitSidebarWidth });
782   },
784   async onSidebarToggle() {
785     this.is3PaneModeEnabled = !this.is3PaneModeEnabled;
786     await this.setupToolbar();
787     await this.addRuleView({ skipQueue: true });
788   },
790   /**
791    * Sets the inspector sidebar split box state. Shows the splitter inside the sidebar
792    * split box, specifies the end panel control and resizes the split box width depending
793    * on the width of the toolbox.
794    */
795   setSidebarSplitBoxState() {
796     const toolboxWidth = this.panelDoc.getElementById("inspector-splitter-box")
797       .clientWidth;
799     // Get the inspector sidebar's (right panel in horizontal mode or bottom panel in
800     // vertical mode) width.
801     const sidebarWidth = this.splitBox.state.width;
802     // This variable represents the width of the right panel in horizontal mode or
803     // bottom-right panel in vertical mode width in 3 pane mode.
804     let sidebarSplitboxWidth;
806     if (this.useLandscapeMode()) {
807       // Whether or not doubling the inspector sidebar's (right panel in horizontal mode
808       // or bottom panel in vertical mode) width will be bigger than half of the
809       // toolbox's width.
810       const canDoubleSidebarWidth = sidebarWidth * 2 < toolboxWidth / 2;
812       // Resize the main split box's end panel that contains the middle and right panel.
813       // Attempts to resize the main split box's end panel to be double the size of the
814       // existing sidebar's width when switching to 3 pane mode. However, if the middle
815       // and right panel's width together is greater than half of the toolbox's width,
816       // split all 3 panels to be equally sized by resizing the end panel to be 2/3 of
817       // the current toolbox's width.
818       this.splitBox.setState({
819         width: canDoubleSidebarWidth
820           ? sidebarWidth * 2
821           : (toolboxWidth * 2) / 3,
822       });
824       // In landscape/horizontal mode, set the right panel back to its original
825       // inspector sidebar width if we can double the sidebar width. Otherwise, set
826       // the width of the right panel to be 1/3 of the toolbox's width since all 3
827       // panels will be equally sized.
828       sidebarSplitboxWidth = canDoubleSidebarWidth
829         ? sidebarWidth
830         : toolboxWidth / 3;
831     } else {
832       // In portrait/vertical mode, set the bottom-right panel to be 1/2 of the
833       // toolbox's width.
834       sidebarSplitboxWidth = toolboxWidth / 2;
835     }
837     // Show the splitter inside the sidebar split box. Sets the width of the inspector
838     // sidebar and specify that the end (right in horizontal or bottom-right in
839     // vertical) panel of the sidebar split box should be controlled when resizing.
840     this.sidebarSplitBoxRef.current.setState({
841       endPanelControl: true,
842       splitterSize: 1,
843       width: sidebarSplitboxWidth,
844     });
845   },
847   /**
848    * Adds the rule view to the middle (in landscape/horizontal mode) or bottom-left panel
849    * (in portrait/vertical mode) or inspector sidebar depending on whether or not it is 3
850    * pane mode. The default tab specifies whether or not the rule view should be selected.
851    * The defaultTab defaults to the rule view when reverting to the 2 pane mode and the
852    * rule view is being merged back into the inspector sidebar from middle/bottom-left
853    * panel. Otherwise, we specify the default tab when handling the sidebar setup.
854    *
855    * @params {String} defaultTab
856    *         Thie id of the default tab for the sidebar.
857    */
858   async addRuleView({ defaultTab = "ruleview", skipQueue = false } = {}) {
859     const ruleViewSidebar = this.sidebarSplitBoxRef.current.startPanelContainer;
861     if (this.is3PaneModeEnabled) {
862       // Convert to 3 pane mode by removing the rule view from the inspector sidebar
863       // and adding the rule view to the middle (in landscape/horizontal mode) or
864       // bottom-left (in portrait/vertical mode) panel.
865       ruleViewSidebar.style.display = "block";
867       this.setSidebarSplitBoxState();
869       // Force the rule view panel creation by calling getPanel
870       this.getPanel("ruleview");
872       await this.sidebar.removeTab("ruleview");
874       this.ruleViewSideBar.addExistingTab(
875         "ruleview",
876         INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"),
877         true
878       );
880       this.ruleViewSideBar.show();
881     } else {
882       // Removes the rule view from the 3 pane mode and adds the rule view to the main
883       // inspector sidebar.
884       ruleViewSidebar.style.display = "none";
886       // Set the width of the split box (right panel in horziontal mode and bottom panel
887       // in vertical mode) to be the width of the inspector sidebar.
888       const splitterBox = this.panelDoc.getElementById(
889         "inspector-splitter-box"
890       );
891       this.splitBox.setState({
892         width: this.useLandscapeMode()
893           ? this.sidebarSplitBoxRef.current.state.width
894           : splitterBox.clientWidth,
895       });
897       // Hide the splitter to prevent any drag events in the sidebar split box and
898       // specify that the end (right panel in horziontal mode or bottom panel in vertical
899       // mode) panel should be uncontrolled when resizing.
900       this.sidebarSplitBoxRef.current.setState({
901         endPanelControl: false,
902         splitterSize: 0,
903       });
905       this.ruleViewSideBar.hide();
906       await this.ruleViewSideBar.removeTab("ruleview");
908       if (skipQueue) {
909         this.sidebar.addExistingTab(
910           "ruleview",
911           INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"),
912           defaultTab == "ruleview",
913           0
914         );
915       } else {
916         this.sidebar.queueExistingTab(
917           "ruleview",
918           INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"),
919           defaultTab == "ruleview",
920           0
921         );
922       }
923     }
925     this.emit("ruleview-added");
926   },
928   /**
929    * Lazily get and create panel instances displayed in the sidebar
930    */
931   getPanel: function(id) {
932     if (this._panels.has(id)) {
933       return this._panels.get(id);
934     }
936     let panel;
937     switch (id) {
938       case "animationinspector":
939         const AnimationInspector = this.browserRequire(
940           "devtools/client/inspector/animation/animation"
941         );
942         panel = new AnimationInspector(this, this.panelWin);
943         break;
944       case "boxmodel":
945         // box-model isn't a panel on its own, it used to, now it is being used by
946         // the layout view which retrieves an instance via getPanel.
947         const BoxModel = require("devtools/client/inspector/boxmodel/box-model");
948         panel = new BoxModel(this, this.panelWin);
949         break;
950       case "changesview":
951         const ChangesView = this.browserRequire(
952           "devtools/client/inspector/changes/ChangesView"
953         );
954         panel = new ChangesView(this, this.panelWin);
955         break;
956       case "computedview":
957         const { ComputedViewTool } = this.browserRequire(
958           "devtools/client/inspector/computed/computed"
959         );
960         panel = new ComputedViewTool(this, this.panelWin);
961         break;
962       case "fontinspector":
963         const FontInspector = this.browserRequire(
964           "devtools/client/inspector/fonts/fonts"
965         );
966         panel = new FontInspector(this, this.panelWin);
967         break;
968       case "layoutview":
969         const LayoutView = this.browserRequire(
970           "devtools/client/inspector/layout/layout"
971         );
972         panel = new LayoutView(this, this.panelWin);
973         break;
974       case "newruleview":
975         const RulesView = this.browserRequire(
976           "devtools/client/inspector/rules/new-rules"
977         );
978         panel = new RulesView(this, this.panelWin);
979         break;
980       case "ruleview":
981         const {
982           RuleViewTool,
983         } = require("devtools/client/inspector/rules/rules");
984         panel = new RuleViewTool(this, this.panelWin);
985         break;
986       default:
987         // This is a custom panel or a non lazy-loaded one.
988         return null;
989     }
991     if (panel) {
992       this._panels.set(id, panel);
993     }
995     return panel;
996   },
998   /**
999    * Build the sidebar.
1000    */
1001   async setupSidebar() {
1002     const sidebar = this.panelDoc.getElementById("inspector-sidebar");
1003     const options = {
1004       showAllTabsMenu: true,
1005       sidebarToggleButton: {
1006         collapsed: !this.is3PaneModeEnabled,
1007         collapsePaneTitle: INSPECTOR_L10N.getStr("inspector.hideThreePaneMode"),
1008         expandPaneTitle: INSPECTOR_L10N.getStr("inspector.showThreePaneMode"),
1009         onClick: this.onSidebarToggle,
1010       },
1011     };
1013     this.sidebar = new ToolSidebar(sidebar, this, "inspector", options);
1014     this.sidebar.on("select", this.onSidebarSelect);
1016     const ruleSideBar = this.panelDoc.getElementById("inspector-rules-sidebar");
1017     this.ruleViewSideBar = new ToolSidebar(ruleSideBar, this, "inspector", {
1018       hideTabstripe: true,
1019     });
1021     // defaultTab may also be an empty string or a tab id that doesn't exist anymore
1022     // (e.g. it was a tab registered by an addon that has been uninstalled).
1023     let defaultTab = Services.prefs.getCharPref(
1024       "devtools.inspector.activeSidebar"
1025     );
1027     if (this.is3PaneModeEnabled && defaultTab === "ruleview") {
1028       defaultTab = "layoutview";
1029     }
1031     // Append all side panels
1033     await this.addRuleView({ defaultTab });
1035     // Inspector sidebar panels in order of appearance.
1036     const sidebarPanels = [
1037       {
1038         id: "layoutview",
1039         title: INSPECTOR_L10N.getStr("inspector.sidebar.layoutViewTitle2"),
1040       },
1041       {
1042         id: "computedview",
1043         title: INSPECTOR_L10N.getStr("inspector.sidebar.computedViewTitle"),
1044       },
1045       {
1046         id: "changesview",
1047         title: INSPECTOR_L10N.getStr("inspector.sidebar.changesViewTitle"),
1048       },
1049       {
1050         id: "fontinspector",
1051         title: INSPECTOR_L10N.getStr("inspector.sidebar.fontInspectorTitle"),
1052       },
1053       {
1054         id: "animationinspector",
1055         title: INSPECTOR_L10N.getStr(
1056           "inspector.sidebar.animationInspectorTitle"
1057         ),
1058       },
1059     ];
1061     if (
1062       Services.prefs.getBoolPref("devtools.inspector.new-rulesview.enabled")
1063     ) {
1064       sidebarPanels.push({
1065         id: "newruleview",
1066         title: INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"),
1067       });
1068     }
1070     for (const { id, title } of sidebarPanels) {
1071       // The Computed panel is not a React-based panel. We pick its element container from
1072       // the DOM and wrap it in a React component (InspectorTabPanel) so it behaves like
1073       // other panels when using the Inspector's tool sidebar.
1074       if (id === "computedview") {
1075         this.sidebar.queueExistingTab(id, title, defaultTab === id);
1076       } else {
1077         // When `panel` is a function, it is called when the tab should render. It is
1078         // expected to return a React component to populate the tab's content area.
1079         // Calling this method on-demand allows us to lazy-load the requested panel.
1080         this.sidebar.queueTab(
1081           id,
1082           title,
1083           {
1084             props: {
1085               id,
1086               title,
1087             },
1088             panel: () => {
1089               return this.getPanel(id).provider;
1090             },
1091           },
1092           defaultTab === id
1093         );
1094       }
1095     }
1097     this.sidebar.addAllQueuedTabs();
1099     // Persist splitter state in preferences.
1100     this.sidebar.on("show", this.onSidebarShown);
1101     this.sidebar.on("hide", this.onSidebarHidden);
1102     this.sidebar.on("destroy", this.onSidebarHidden);
1104     this.sidebar.show();
1105   },
1107   /**
1108    * Setup any extension sidebar already registered to the toolbox when the inspector.
1109    * has been created for the first time.
1110    */
1111   setupExtensionSidebars() {
1112     for (const [sidebarId, { title }] of this.toolbox
1113       .inspectorExtensionSidebars) {
1114       this.addExtensionSidebar(sidebarId, { title });
1115     }
1116   },
1118   /**
1119    * Create a side-panel tab controlled by an extension
1120    * using the devtools.panels.elements.createSidebarPane and sidebar object API
1121    *
1122    * @param {String} id
1123    *        An unique id for the sidebar tab.
1124    * @param {Object} options
1125    * @param {String} options.title
1126    *        The tab title
1127    */
1128   addExtensionSidebar: function(id, { title }) {
1129     if (this._panels.has(id)) {
1130       throw new Error(
1131         `Cannot create an extension sidebar for the existent id: ${id}`
1132       );
1133     }
1135     const extensionSidebar = new ExtensionSidebar(this, { id, title });
1137     // TODO(rpl): pass some extension metadata (e.g. extension name and icon) to customize
1138     // the render of the extension title (e.g. use the icon in the sidebar and show the
1139     // extension name in a tooltip).
1140     this.addSidebarTab(id, title, extensionSidebar.provider, false);
1142     this._panels.set(id, extensionSidebar);
1144     // Emit the created ExtensionSidebar instance to the listeners registered
1145     // on the toolbox by the "devtools.panels.elements" WebExtensions API.
1146     this.toolbox.emit(`extension-sidebar-created-${id}`, extensionSidebar);
1147   },
1149   /**
1150    * Remove and destroy a side-panel tab controlled by an extension (e.g. when the
1151    * extension has been disable/uninstalled while the toolbox and inspector were
1152    * still open).
1153    *
1154    * @param {String} id
1155    *        The id of the sidebar tab to destroy.
1156    */
1157   removeExtensionSidebar: function(id) {
1158     if (!this._panels.has(id)) {
1159       throw new Error(`Unable to find a sidebar panel with id "${id}"`);
1160     }
1162     const panel = this._panels.get(id);
1164     if (!(panel instanceof ExtensionSidebar)) {
1165       throw new Error(
1166         `The sidebar panel with id "${id}" is not an ExtensionSidebar`
1167       );
1168     }
1170     this._panels.delete(id);
1171     this.sidebar.removeTab(id);
1172     panel.destroy();
1173   },
1175   /**
1176    * Register a side-panel tab. This API can be used outside of
1177    * DevTools (e.g. from an extension) as well as by DevTools
1178    * code base.
1179    *
1180    * @param {string} tab uniq id
1181    * @param {string} title tab title
1182    * @param {React.Component} panel component. See `InspectorPanelTab` as an example.
1183    * @param {boolean} selected true if the panel should be selected
1184    */
1185   addSidebarTab: function(id, title, panel, selected) {
1186     this.sidebar.addTab(id, title, panel, selected);
1187   },
1189   /**
1190    * Method to check whether the document is a HTML document and
1191    * pickColorFromPage method is available or not.
1192    *
1193    * @return {Boolean} true if the eyedropper highlighter is supported by the current
1194    *         document.
1195    */
1196   async supportsEyeDropper() {
1197     try {
1198       return await this.inspectorFront.supportsHighlighters();
1199     } catch (e) {
1200       console.error(e);
1201       return false;
1202     }
1203   },
1205   async setupToolbar() {
1206     this.teardownToolbar();
1208     // Setup the add-node button.
1209     this.addNode = this.addNode.bind(this);
1210     this.addNodeButton = this.panelDoc.getElementById(
1211       "inspector-element-add-button"
1212     );
1213     this.addNodeButton.addEventListener("click", this.addNode);
1215     // Setup the eye-dropper icon if we're in an HTML document and we have actor support.
1216     const canShowEyeDropper = await this.supportsEyeDropper();
1218     // Bail out if the inspector was destroyed in the meantime and panelDoc is no longer
1219     // available.
1220     if (!this.panelDoc) {
1221       return;
1222     }
1224     if (canShowEyeDropper) {
1225       this.onEyeDropperDone = this.onEyeDropperDone.bind(this);
1226       this.onEyeDropperButtonClicked = this.onEyeDropperButtonClicked.bind(
1227         this
1228       );
1229       this.eyeDropperButton = this.panelDoc.getElementById(
1230         "inspector-eyedropper-toggle"
1231       );
1232       this.eyeDropperButton.disabled = false;
1233       this.eyeDropperButton.title = INSPECTOR_L10N.getStr(
1234         "inspector.eyedropper.label"
1235       );
1236       this.eyeDropperButton.addEventListener(
1237         "click",
1238         this.onEyeDropperButtonClicked
1239       );
1240     } else {
1241       const eyeDropperButton = this.panelDoc.getElementById(
1242         "inspector-eyedropper-toggle"
1243       );
1244       eyeDropperButton.disabled = true;
1245       eyeDropperButton.title = INSPECTOR_L10N.getStr(
1246         "eyedropper.disabled.title"
1247       );
1248     }
1250     this.emit("inspector-toolbar-updated");
1251   },
1253   teardownToolbar: function() {
1254     if (this.addNodeButton) {
1255       this.addNodeButton.removeEventListener("click", this.addNode);
1256       this.addNodeButton = null;
1257     }
1259     if (this.eyeDropperButton) {
1260       this.eyeDropperButton.removeEventListener(
1261         "click",
1262         this.onEyeDropperButtonClicked
1263       );
1264       this.eyeDropperButton = null;
1265     }
1266   },
1268   /**
1269    * Reset the inspector on new root mutation.
1270    */
1271   onNewRoot: function() {
1272     // Record new-root timing for telemetry
1273     this._newRootStart = this.panelWin.performance.now();
1275     this._defaultNode = null;
1276     this.selection.setNodeFront(null);
1277     this._destroyMarkup();
1279     const onNodeSelected = defaultNode => {
1280       // Cancel this promise resolution as a new one had
1281       // been queued up.
1282       if (this._pendingSelection != onNodeSelected) {
1283         return;
1284       }
1285       this._pendingSelection = null;
1286       this.selection.setNodeFront(defaultNode, { reason: "navigateaway" });
1288       this.once("markuploaded", this.onMarkupLoaded);
1289       this._initMarkup();
1291       // Setup the toolbar again, since its content may depend on the current document.
1292       this.setupToolbar();
1293     };
1294     this._pendingSelection = onNodeSelected;
1295     this._getDefaultNodeForSelection().then(
1296       onNodeSelected,
1297       this._handleRejectionIfNotDestroyed
1298     );
1299   },
1301   /**
1302    * When replaying, reset the inspector whenever the target pauses.
1303    */
1304   handleThreadPaused() {
1305     this._replayResumed = false;
1306     this.onNewRoot();
1307   },
1309   /**
1310    * When replaying, reset the inspector whenever the target resumes.
1311    */
1312   handleThreadResumed() {
1313     this._replayResumed = true;
1314     this.onNewRoot();
1315   },
1317   /**
1318    * Handler for "markuploaded" event fired on a new root mutation and after the markup
1319    * view is initialized. Expands the current selected node and restores the saved
1320    * highlighter state.
1321    */
1322   async onMarkupLoaded() {
1323     if (!this.markup) {
1324       return;
1325     }
1327     const onExpand = this.markup.expandNode(this.selection.nodeFront);
1329     // Restore the highlighter states prior to emitting "new-root".
1330     if (this._highlighters) {
1331       await Promise.all([
1332         this.highlighters.restoreFlexboxState(),
1333         this.highlighters.restoreGridState(),
1334       ]);
1335     }
1337     this.emit("new-root");
1339     // Wait for full expand of the selected node in order to ensure
1340     // the markup view is fully emitted before firing 'reloaded'.
1341     // 'reloaded' is used to know when the panel is fully updated
1342     // after a page reload.
1343     await onExpand;
1345     this.emit("reloaded");
1347     // Record the time between new-root event and inspector fully loaded.
1348     if (this._newRootStart) {
1349       // Only log the timing when inspector is not destroyed and is in foreground.
1350       if (this.toolbox && this.toolbox.currentToolId == "inspector") {
1351         const delay = this.panelWin.performance.now() - this._newRootStart;
1352         const telemetryKey = "DEVTOOLS_INSPECTOR_NEW_ROOT_TO_RELOAD_DELAY_MS";
1353         const histogram = this.telemetry.getHistogramById(telemetryKey);
1354         histogram.add(delay);
1355       }
1356       delete this._newRootStart;
1357     }
1358   },
1360   _selectionCssSelector: null,
1362   /**
1363    * Set the currently selected node unique css selector.
1364    * Will store the current target url along with it to allow pre-selection at
1365    * reload
1366    */
1367   set selectionCssSelector(cssSelector = null) {
1368     if (this._destroyed) {
1369       return;
1370     }
1372     this._selectionCssSelector = {
1373       selector: cssSelector,
1374       url: this._target.url,
1375     };
1376   },
1378   /**
1379    * Get the current selection unique css selector if any, that is, if a node
1380    * is actually selected and that node has been selected while on the same url
1381    */
1382   get selectionCssSelector() {
1383     if (
1384       this._selectionCssSelector &&
1385       this._selectionCssSelector.url === this._target.url
1386     ) {
1387       return this._selectionCssSelector.selector;
1388     }
1389     return null;
1390   },
1392   /**
1393    * On any new selection made by the user, store the unique css selector
1394    * of the selected node so it can be restored after reload of the same page
1395    */
1396   updateSelectionCssSelector() {
1397     if (this.selection.isElementNode()) {
1398       this.selection.nodeFront.getUniqueSelector().then(selector => {
1399         this.selectionCssSelector = selector;
1400       }, this._handleRejectionIfNotDestroyed);
1401     }
1402   },
1404   /**
1405    * Can a new HTML element be inserted into the currently selected element?
1406    * @return {Boolean}
1407    */
1408   canAddHTMLChild: function() {
1409     const selection = this.selection;
1411     // Don't allow to insert an element into these elements. This should only
1412     // contain elements where walker.insertAdjacentHTML has no effect.
1413     const invalidTagNames = ["html", "iframe"];
1415     return (
1416       selection.isHTMLNode() &&
1417       selection.isElementNode() &&
1418       !selection.isPseudoElementNode() &&
1419       !selection.isAnonymousNode() &&
1420       !invalidTagNames.includes(selection.nodeFront.nodeName.toLowerCase())
1421     );
1422   },
1424   /**
1425    * Update the state of the add button in the toolbar depending on the current selection.
1426    */
1427   updateAddElementButton() {
1428     const btn = this.panelDoc.getElementById("inspector-element-add-button");
1429     if (this.canAddHTMLChild()) {
1430       btn.removeAttribute("disabled");
1431     } else {
1432       btn.setAttribute("disabled", "true");
1433     }
1434   },
1436   /**
1437    * Handler for the "host-changed" event from the toolbox. Resets the inspector
1438    * sidebar sizes when the toolbox host type changes.
1439    */
1440   async onHostChanged() {
1441     // Eagerly call our resize handling code to process the fact that we
1442     // switched hosts. If we don't do this, we'll wait for resize events + 200ms
1443     // to have passed, which causes the old layout to noticeably show up in the
1444     // new host, followed by the updated one.
1445     await this._onLazyPanelResize();
1446     // Note that we may have been destroyed by now, especially in tests, so we
1447     // need to check if that's happened before touching anything else.
1448     if (!this.target || !this.is3PaneModeEnabled) {
1449       return;
1450     }
1452     // When changing hosts, the toolbox chromeEventHandler might change, for instance when
1453     // switching from docked to window hosts. Recreate the searchbox shortcuts.
1454     this.searchboxShortcuts.destroy();
1455     this.createSearchBoxShortcuts();
1457     this.setSidebarSplitBoxState();
1458   },
1460   /**
1461    * When a new node is selected.
1462    */
1463   onNewSelection: function(value, reason) {
1464     if (reason === "selection-destroy") {
1465       return;
1466     }
1468     this.updateAddElementButton();
1469     this.updateSelectionCssSelector();
1471     const selfUpdate = this.updating("inspector-panel");
1472     executeSoon(() => {
1473       try {
1474         selfUpdate(this.selection.nodeFront);
1475         this.telemetry.scalarAdd(TELEMETRY_SCALAR_NODE_SELECTION_COUNT, 1);
1476       } catch (ex) {
1477         console.error(ex);
1478       }
1479     });
1480   },
1482   /**
1483    * Delay the "inspector-updated" notification while a tool
1484    * is updating itself.  Returns a function that must be
1485    * invoked when the tool is done updating with the node
1486    * that the tool is viewing.
1487    */
1488   updating: function(name) {
1489     if (
1490       this._updateProgress &&
1491       this._updateProgress.node != this.selection.nodeFront
1492     ) {
1493       this.cancelUpdate();
1494     }
1496     if (!this._updateProgress) {
1497       // Start an update in progress.
1498       const self = this;
1499       this._updateProgress = {
1500         node: this.selection.nodeFront,
1501         outstanding: new Set(),
1502         checkDone: function() {
1503           if (this !== self._updateProgress) {
1504             return;
1505           }
1506           // Cancel update if there is no `selection` anymore.
1507           // It can happen if the inspector panel is already destroyed.
1508           if (!self.selection || this.node !== self.selection.nodeFront) {
1509             self.cancelUpdate();
1510             return;
1511           }
1512           if (this.outstanding.size !== 0) {
1513             return;
1514           }
1516           self._updateProgress = null;
1517           self.emit("inspector-updated", name);
1518         },
1519       };
1520     }
1522     const progress = this._updateProgress;
1523     const done = function() {
1524       progress.outstanding.delete(done);
1525       progress.checkDone();
1526     };
1527     progress.outstanding.add(done);
1528     return done;
1529   },
1531   /**
1532    * Cancel notification of inspector updates.
1533    */
1534   cancelUpdate: function() {
1535     this._updateProgress = null;
1536   },
1538   /**
1539    * When a node is deleted, select its parent node or the defaultNode if no
1540    * parent is found (may happen when deleting an iframe inside which the
1541    * node was selected).
1542    */
1543   onDetached: function(parentNode) {
1544     this.breadcrumbs.cutAfter(this.breadcrumbs.indexOf(parentNode));
1545     const nodeFront = parentNode ? parentNode : this._defaultNode;
1546     this.selection.setNodeFront(nodeFront, { reason: "detached" });
1547   },
1549   /**
1550    * Destroy the inspector.
1551    */
1552   destroy: function() {
1553     if (this._destroyed) {
1554       return;
1555     }
1556     this._destroyed = true;
1558     this._target.threadFront.off("paused", this.handleThreadPaused);
1559     this._target.threadFront.off("resumed", this.handleThreadResumed);
1561     if (this.walker) {
1562       this.walker.off("new-root", this.onNewRoot);
1563       this.pageStyle = null;
1564     }
1566     this.cancelUpdate();
1568     this.sidebar.destroy();
1570     this.panelWin.removeEventListener("resize", this.onPanelWindowResize, true);
1571     this.selection.off("new-node-front", this.onNewSelection);
1572     this.selection.off("detached-front", this.onDetached);
1573     this.sidebar.off("select", this.onSidebarSelect);
1574     this.sidebar.off("show", this.onSidebarShown);
1575     this.sidebar.off("hide", this.onSidebarHidden);
1576     this.sidebar.off("destroy", this.onSidebarHidden);
1577     this.target.off("will-navigate", this._onBeforeNavigate);
1579     for (const [, panel] of this._panels) {
1580       panel.destroy();
1581     }
1582     this._panels.clear();
1584     if (this._highlighters) {
1585       this._highlighters.destroy();
1586       this._highlighters = null;
1587     }
1589     if (this._markupFrame) {
1590       this._markupFrame.removeEventListener(
1591         "load",
1592         this._onMarkupFrameLoad,
1593         true
1594       );
1595     }
1597     if (this._search) {
1598       this._search.destroy();
1599       this._search = null;
1600     }
1602     this.sidebar.destroy();
1603     if (this.ruleViewSideBar) {
1604       this.ruleViewSideBar.destroy();
1605     }
1606     this._destroyMarkup();
1608     this.teardownToolbar();
1610     this.breadcrumbs.destroy();
1611     this.reflowTracker.destroy();
1612     this.styleChangeTracker.destroy();
1613     this.searchboxShortcuts.destroy();
1615     this._is3PaneModeChromeEnabled = null;
1616     this._is3PaneModeEnabled = null;
1617     this._markupBox = null;
1618     this._markupFrame = null;
1619     this._target = null;
1620     this._toolbox = null;
1621     this.breadcrumbs = null;
1622     this.panelDoc = null;
1623     this.panelWin.inspector = null;
1624     this.panelWin = null;
1625     this.resultsLength = null;
1626     this.searchBox = null;
1627     this.show3PaneTooltip = null;
1628     this.sidebar = null;
1629     this.store = null;
1630     this.telemetry = null;
1631   },
1633   _initMarkup: function() {
1634     if (!this._markupFrame) {
1635       this._markupFrame = this.panelDoc.createElement("iframe");
1636       this._markupFrame.setAttribute(
1637         "aria-label",
1638         INSPECTOR_L10N.getStr("inspector.panelLabel.markupView")
1639       );
1640       this._markupFrame.setAttribute("flex", "1");
1641       // This is needed to enable tooltips inside the iframe document.
1642       this._markupFrame.setAttribute("tooltip", "aHTMLTooltip");
1644       this._markupBox.style.visibility = "hidden";
1645       this._markupBox.appendChild(this._markupFrame);
1647       this._markupFrame.addEventListener("load", this._onMarkupFrameLoad, true);
1648       this._markupFrame.setAttribute("src", "markup/markup.xhtml");
1649     } else {
1650       this._onMarkupFrameLoad();
1651     }
1652   },
1654   _onMarkupFrameLoad: function() {
1655     this._markupFrame.removeEventListener(
1656       "load",
1657       this._onMarkupFrameLoad,
1658       true
1659     );
1660     this._markupFrame.contentWindow.focus();
1661     this._markupBox.style.visibility = "visible";
1662     this.markup = new MarkupView(this, this._markupFrame, this._toolbox.win);
1663     this.emit("markuploaded");
1664   },
1666   _destroyMarkup: function() {
1667     let destroyPromise;
1669     if (this.markup) {
1670       destroyPromise = this.markup.destroy();
1671       this.markup = null;
1672     } else {
1673       destroyPromise = promise.resolve();
1674     }
1676     this._markupBox.style.visibility = "hidden";
1678     return destroyPromise;
1679   },
1681   onEyeDropperButtonClicked: function() {
1682     this.eyeDropperButton.classList.contains("checked")
1683       ? this.hideEyeDropper()
1684       : this.showEyeDropper();
1685   },
1687   startEyeDropperListeners: function() {
1688     this.inspectorFront.once("color-pick-canceled", this.onEyeDropperDone);
1689     this.inspectorFront.once("color-picked", this.onEyeDropperDone);
1690     this.walker.once("new-root", this.onEyeDropperDone);
1691   },
1693   stopEyeDropperListeners: function() {
1694     this.inspectorFront.off("color-pick-canceled", this.onEyeDropperDone);
1695     this.inspectorFront.off("color-picked", this.onEyeDropperDone);
1696     this.walker.off("new-root", this.onEyeDropperDone);
1697   },
1699   onEyeDropperDone: function() {
1700     this.eyeDropperButton.classList.remove("checked");
1701     this.stopEyeDropperListeners();
1702   },
1704   /**
1705    * Show the eyedropper on the page.
1706    * @return {Promise} resolves when the eyedropper is visible.
1707    */
1708   showEyeDropper: function() {
1709     // The eyedropper button doesn't exist, most probably because the actor doesn't
1710     // support the pickColorFromPage, or because the page isn't HTML.
1711     if (!this.eyeDropperButton) {
1712       return null;
1713     }
1715     this.telemetry.scalarSet(TELEMETRY_EYEDROPPER_OPENED, 1);
1716     this.eyeDropperButton.classList.add("checked");
1717     this.startEyeDropperListeners();
1718     return this.inspectorFront
1719       .pickColorFromPage({ copyOnSelect: true })
1720       .catch(console.error);
1721   },
1723   /**
1724    * Hide the eyedropper.
1725    * @return {Promise} resolves when the eyedropper is hidden.
1726    */
1727   hideEyeDropper: function() {
1728     // The eyedropper button doesn't exist, most probably  because the page isn't HTML.
1729     if (!this.eyeDropperButton) {
1730       return null;
1731     }
1733     this.eyeDropperButton.classList.remove("checked");
1734     this.stopEyeDropperListeners();
1735     return this.inspectorFront.cancelPickColorFromPage().catch(console.error);
1736   },
1738   /**
1739    * Create a new node as the last child of the current selection, expand the
1740    * parent and select the new node.
1741    */
1742   async addNode() {
1743     if (!this.canAddHTMLChild()) {
1744       return;
1745     }
1747     const html = "<div></div>";
1749     // Insert the html and expect a childList markup mutation.
1750     const onMutations = this.once("markupmutation");
1751     await this.walker.insertAdjacentHTML(
1752       this.selection.nodeFront,
1753       "beforeEnd",
1754       html
1755     );
1756     await onMutations;
1758     // Expand the parent node.
1759     this.markup.expandNode(this.selection.nodeFront);
1760   },
1762   /**
1763    * Toggle a pseudo class.
1764    */
1765   togglePseudoClass: function(pseudo) {
1766     if (this.selection.isElementNode()) {
1767       const node = this.selection.nodeFront;
1768       if (node.hasPseudoClassLock(pseudo)) {
1769         return this.walker.removePseudoClassLock(node, pseudo, {
1770           parents: true,
1771         });
1772       }
1774       const hierarchical = pseudo == ":hover" || pseudo == ":active";
1775       return this.walker.addPseudoClassLock(node, pseudo, {
1776         parents: hierarchical,
1777       });
1778     }
1779     return promise.resolve();
1780   },
1782   /**
1783    * Initiate screenshot command on selected node.
1784    */
1785   async screenshotNode() {
1786     // Bug 1332936 - it's possible to call `screenshotNode` while the BoxModel highlighter
1787     // is still visible, therefore showing it in the picture.
1788     // To avoid that, we have to hide it before taking the screenshot. The `hideBoxModel`
1789     // will do that, calling `hide` for the highlighter only if previously shown.
1790     await this.highlighter.hideBoxModel();
1792     const clipboardEnabled = Services.prefs.getBoolPref(
1793       "devtools.screenshot.clipboard.enabled"
1794     );
1795     const args = {
1796       file: true,
1797       nodeActorID: this.selection.nodeFront.actorID,
1798       clipboard: clipboardEnabled,
1799     };
1800     const screenshotFront = await this.target.getFront("screenshot");
1801     const screenshot = await screenshotFront.capture(args);
1802     await saveScreenshot(this.panelWin, args, screenshot);
1803   },
1805   /**
1806    * Returns an object containing the shared handler functions used in the box
1807    * model and grid React components.
1808    */
1809   getCommonComponentProps() {
1810     return {
1811       setSelectedNode: this.selection.setNodeFront,
1812       onShowBoxModelHighlighterForNode: this.onShowBoxModelHighlighterForNode,
1813     };
1814   },
1816   /**
1817    * Shows the box-model highlighter on the element corresponding to the provided
1818    * NodeFront.
1819    *
1820    * @param  {NodeFront} nodeFront
1821    *         The node to highlight.
1822    * @param  {Object} options
1823    *         Options passed to the highlighter actor.
1824    */
1825   onShowBoxModelHighlighterForNode(nodeFront, options) {
1826     const toolbox = this.toolbox;
1827     toolbox.highlighter.highlight(nodeFront, options);
1828   },
1830   async inspectNodeActor(nodeActor, inspectFromAnnotation) {
1831     const nodeFront = await this.walker.gripToNodeFront({ actor: nodeActor });
1832     if (!nodeFront) {
1833       console.error(
1834         "The object cannot be linked to the inspector, the " +
1835           "corresponding nodeFront could not be found."
1836       );
1837       return false;
1838     }
1840     const isAttached = await this.walker.isInDOMTree(nodeFront);
1841     if (!isAttached) {
1842       console.error("Selected DOMNode is not attached to the document tree.");
1843       return false;
1844     }
1846     await this.selection.setNodeFront(nodeFront, {
1847       reason: inspectFromAnnotation,
1848     });
1849     return true;
1850   },
1853 exports.Inspector = Inspector;