Bug 1432016 - Part 2 - Move descriptionHeightWorkaround and some other methods to the PanelView class. r=Gijs draft
authorPaolo Amadini <paolo.mozmail@amadzone.org>
Mon, 22 Jan 2018 12:58:10 +0000
changeset 723060 e7c86579a97c8211089896af68e30f908ef3ba0e
parent 723059 90d8912b3ff0351d3e362e052f02aac60f2669ba
child 723061 25f0378c51b6d5a08fca22b561e239a245244e0a
push id96314
push userpaolo.mozmail@amadzone.org
push dateMon, 22 Jan 2018 13:20:54 +0000
reviewersGijs
bugs1432016
milestone59.0a1
Bug 1432016 - Part 2 - Move descriptionHeightWorkaround and some other methods to the PanelView class. r=Gijs MozReview-Commit-ID: 59fUuB35Ygy
browser/base/content/browser.js
browser/base/content/test/performance/browser_appmenu_reflows.js
browser/components/customizableui/CustomizableWidgets.jsm
browser/components/customizableui/PanelMultiView.jsm
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -36,16 +36,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
   FormValidationHandler: "resource:///modules/FormValidationHandler.jsm",
   LanguagePrompt: "resource://gre/modules/LanguagePrompt.jsm",
   LightweightThemeManager: "resource://gre/modules/LightweightThemeManager.jsm",
   Log: "resource://gre/modules/Log.jsm",
   LoginManagerParent: "resource://gre/modules/LoginManagerParent.jsm",
   NewTabUtils: "resource://gre/modules/NewTabUtils.jsm",
   PageActions: "resource:///modules/PageActions.jsm",
   PageThumbs: "resource://gre/modules/PageThumbs.jsm",
+  PanelView: "resource:///modules/PanelMultiView.jsm",
   PluralForm: "resource://gre/modules/PluralForm.jsm",
   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
   ProcessHangMonitor: "resource:///modules/ProcessHangMonitor.jsm",
   PromiseUtils: "resource://gre/modules/PromiseUtils.jsm",
   ReaderMode: "resource://gre/modules/ReaderMode.jsm",
   ReaderParent: "resource:///modules/ReaderParent.jsm",
   RecentWindow: "resource:///modules/RecentWindow.jsm",
   SafeBrowsing: "resource://gre/modules/SafeBrowsing.jsm",
@@ -7133,31 +7134,29 @@ var gIdentityHandler = {
   get _identityBox() {
     delete this._identityBox;
     return this._identityBox = document.getElementById("identity-box");
   },
   get _identityPopupMultiView() {
     delete this._identityPopupMultiView;
     return this._identityPopupMultiView = document.getElementById("identity-popup-multiView");
   },
+  get _identityPopupMainView() {
+    delete this._identityPopupMainView;
+    return this._identityPopupMainView = document.getElementById("identity-popup-mainView");
+  },
   get _identityPopupContentHosts() {
     delete this._identityPopupContentHosts;
-    let selector = ".identity-popup-host";
-    return this._identityPopupContentHosts = [
-      ...this._identityPopupMultiView._mainView.querySelectorAll(selector),
-      ...document.querySelectorAll(selector)
-    ];
+    return this._identityPopupContentHosts =
+      [...document.querySelectorAll(".identity-popup-host")];
   },
   get _identityPopupContentHostless() {
     delete this._identityPopupContentHostless;
-    let selector = ".identity-popup-hostless";
-    return this._identityPopupContentHostless = [
-      ...this._identityPopupMultiView._mainView.querySelectorAll(selector),
-      ...document.querySelectorAll(selector)
-    ];
+    return this._identityPopupContentHostless =
+      [...document.querySelectorAll(".identity-popup-hostless")];
   },
   get _identityPopupContentOwner() {
     delete this._identityPopupContentOwner;
     return this._identityPopupContentOwner =
       document.getElementById("identity-popup-content-owner");
   },
   get _identityPopupContentSupp() {
     delete this._identityPopupContentSupp;
@@ -7389,17 +7388,18 @@ var gIdentityHandler = {
       this._identityBox.setAttribute("sharing", sharing);
     else
       this._identityBox.removeAttribute("sharing");
 
     this._sharingState = tab._sharingState;
 
     if (this._identityPopup.state == "open") {
       this.updateSitePermissions();
-      this._identityPopupMultiView.descriptionHeightWorkaround();
+      PanelView.forNode(this._identityPopupMainView)
+               .descriptionHeightWorkaround();
     }
   },
 
   /**
    * Attempt to provide proper IDN treatment for host names
    */
   getEffectiveHost() {
     if (!this._IDNService)
@@ -8038,17 +8038,18 @@ var gIdentityHandler = {
           }
         }
         browser.messageManager.sendAsyncMessage("webrtc:StopSharing", windowId);
         webrtcUI.forgetActivePermissionsFromBrowser(gBrowser.selectedBrowser);
       }
       SitePermissions.remove(gBrowser.currentURI, aPermission.id, browser);
 
       this._permissionReloadHint.removeAttribute("hidden");
-      this._identityPopupMultiView.descriptionHeightWorkaround();
+      PanelView.forNode(this._identityPopupMainView)
+               .descriptionHeightWorkaround();
 
       // Set telemetry values for clearing a permission
       let histogram = Services.telemetry.getKeyedHistogramById("WEB_PERMISSION_CLEARED");
 
       let permissionType = 0;
       if (aPermission.state == SitePermissions.ALLOW &&
           aPermission.scope == SitePermissions.SCOPE_PERSISTENT) {
         // 1 : clear permanently allowed permission
--- a/browser/base/content/test/performance/browser_appmenu_reflows.js
+++ b/browser/base/content/test/performance/browser_appmenu_reflows.js
@@ -53,16 +53,17 @@ const EXPECTED_APPMENU_SUBVIEW_REFLOWS =
    *
    * If we add more views where this is necessary, we may need to duplicate
    * these expected reflows further. Bug 1392340 is on file to remove the
    * reflows completely when opening subviews.
    */
   {
     stack: [
       "descriptionHeightWorkaround@resource:///modules/PanelMultiView.jsm",
+      "set current@resource:///modules/PanelMultiView.jsm",
       "hideAllViewsExcept@resource:///modules/PanelMultiView.jsm",
     ],
 
     times: 1, // This number should only ever go down - never up.
   },
 
   {
     stack: [
--- a/browser/components/customizableui/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/CustomizableWidgets.jsm
@@ -9,16 +9,17 @@ this.EXPORTED_SYMBOLS = ["CustomizableWi
 
 Cu.import("resource:///modules/CustomizableUI.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/AppConstants.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   BrowserUITelemetry: "resource:///modules/BrowserUITelemetry.jsm",
+  PanelView: "resource:///modules/PanelMultiView.jsm",
   PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
   PlacesUIUtils: "resource:///modules/PlacesUIUtils.jsm",
   RecentlyClosedTabsAndWindowsMenuUtils: "resource:///modules/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.jsm",
   ShortcutUtils: "resource://gre/modules/ShortcutUtils.jsm",
   CharsetMenu: "resource://gre/modules/CharsetMenu.jsm",
   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
   SyncedTabs: "resource://services-sync/SyncedTabs.jsm",
 });
@@ -361,18 +362,18 @@ const CustomizableWidgets = [
           }
           if (paginationInfo && paginationInfo.clientId == client.id) {
             this._appendClient(client, fragment, paginationInfo.maxTabs);
           } else {
             this._appendClient(client, fragment);
           }
         }
         this._tabsList.appendChild(fragment);
-        let panelView = this._tabsList.closest("panelview");
-        panelView.panelMultiView.descriptionHeightWorkaround(panelView);
+        PanelView.forNode(this._tabsList.closest("panelview"))
+                 .descriptionHeightWorkaround();
       }).catch(err => {
         Cu.reportError(err);
       }).then(() => {
         // an observer for tests.
         Services.obs.notifyObservers(null, "synced-tabs-menu:test:tabs-updated");
       });
     },
     _clearTabList() {
--- a/browser/components/customizableui/PanelMultiView.jsm
+++ b/browser/components/customizableui/PanelMultiView.jsm
@@ -59,16 +59,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 const TRANSITION_PHASES = Object.freeze({
   START: 1,
   PREPARE: 2,
   TRANSITION: 3,
   END: 4
 });
 
 let gNodeToObjectMap = new WeakMap();
+let gMultiLineElementsMap = new WeakMap();
 
 /**
  * Allows associating an object to a node lazily using a weak map.
  *
  * Classes deriving from this one may be easily converted to Custom Elements,
  * although they would lose the ability of being associated lazily.
  */
 this.AssociatedToNode = class {
@@ -105,29 +106,45 @@ this.AssociatedToNode = class {
    * nsIDOMWindowUtils for the window of this node.
    */
   get _dwu() {
     if (this.__dwu)
       return this.__dwu;
     return this.__dwu = this.window.QueryInterface(Ci.nsIInterfaceRequestor)
                                    .getInterface(Ci.nsIDOMWindowUtils);
   }
+
+  /**
+   * Dispatches a custom event on this element.
+   *
+   * @param  {String}    eventName Name of the event to dispatch.
+   * @param  {Object}    [detail]  Event detail object. Optional.
+   * @param  {Boolean}   cancelable If the event can be canceled.
+   * @return {Boolean} `true` if the event was canceled by an event handler, `false`
+   *                   otherwise.
+   */
+  dispatchCustomEvent(eventName, detail, cancelable = false) {
+    let event = new this.window.CustomEvent(eventName, {
+      detail,
+      bubbles: true,
+      cancelable,
+    });
+    this.node.dispatchEvent(event);
+    return event.defaultPrevented;
+  }
 };
 
 /**
  * This is associated to <panelmultiview> elements by the panelUI.xml binding.
  */
 this.PanelMultiView = class extends this.AssociatedToNode {
   get _panel() {
     return this.node.parentNode;
   }
 
-  get showingSubView() {
-    return this._showingSubView;
-  }
   get _mainViewId() {
     return this.node.getAttribute("mainViewId");
   }
   get _mainView() {
     return this.document.getElementById(this._mainViewId);
   }
 
   get _transitioning() {
@@ -177,27 +194,23 @@ this.PanelMultiView = class extends this
   get currentShowPromise() {
     return this._currentShowPromise || Promise.resolve();
   }
   get _keyNavigationMap() {
     if (!this.__keyNavigationMap)
       this.__keyNavigationMap = new Map();
     return this.__keyNavigationMap;
   }
-  get _multiLineElementsMap() {
-    if (!this.__multiLineElementsMap)
-      this.__multiLineElementsMap = new WeakMap();
-    return this.__multiLineElementsMap;
-  }
 
   connect() {
     this.knownViews = new Set(this.node.getElementsByTagName("panelview"));
     this.openViews = [];
     this._mainViewHeight = 0;
-    this.__transitioning = this._ignoreMutations = this._showingSubView = false;
+    this.__transitioning = false;
+    this.showingSubView = false;
 
     const {document, window} = this;
 
     this._viewContainer =
       document.getAnonymousElementByAttribute(this.node, "anonid", "viewContainer");
     this._viewStack =
       document.getAnonymousElementByAttribute(this.node, "anonid", "viewStack");
     this._offscreenViewStack =
@@ -213,35 +226,25 @@ this.PanelMultiView = class extends this
     this._panel.addEventListener("popuphidden", this);
     this._panel.addEventListener("popupshown", this);
     let cs = window.getComputedStyle(document.documentElement);
     // Set CSS-determined attributes now to prevent a layout flush when we do
     // it when transitioning between panels.
     this._dir = cs.direction;
     this.showMainView();
 
-    this._showingSubView = false;
-
     // Proxy these public properties and methods, as used elsewhere by various
     // parts of the browser, to this instance.
-    ["_mainView", "ignoreMutations", "showingSubView"].forEach(property => {
-      Object.defineProperty(this.node, property, {
-        enumerable: true,
-        get: () => this[property],
-        set: (val) => this[property] = val
-      });
-    });
-    ["goBack", "descriptionHeightWorkaround", "showMainView",
-     "showSubView"].forEach(method => {
+    ["goBack", "showMainView", "showSubView"].forEach(method => {
       Object.defineProperty(this.node, method, {
         enumerable: true,
         value: (...args) => this[method](...args)
       });
     });
-    ["current", "currentShowPromise"].forEach(property => {
+    ["current", "currentShowPromise", "showingSubView"].forEach(property => {
       Object.defineProperty(this.node, property, {
         enumerable: true,
         get: () => this[property]
       });
     });
   }
 
   destructor() {
@@ -285,56 +288,16 @@ this.PanelMultiView = class extends this
     let subviews = Array.from(viewNodeContainer.childNodes);
     for (let subview of subviews) {
       // XBL lists the 'children' XBL element explicitly. :-(
       if (subview.nodeName != "children")
         this._panelViewCache.appendChild(subview);
     }
   }
 
-  _setHeader(viewNode, titleText) {
-    // If the header already exists, update or remove it as requested.
-    let header = viewNode.firstChild;
-    if (header && header.classList.contains("panel-header")) {
-      if (titleText) {
-        header.querySelector("label").setAttribute("value", titleText);
-      } else {
-        header.remove();
-      }
-      return;
-    }
-
-    // The header doesn't exist, only create it if needed.
-    if (!titleText) {
-      return;
-    }
-
-    header = this.document.createElement("box");
-    header.classList.add("panel-header");
-
-    let backButton = this.document.createElement("toolbarbutton");
-    backButton.className =
-      "subviewbutton subviewbutton-iconic subviewbutton-back";
-    backButton.setAttribute("closemenu", "none");
-    backButton.setAttribute("tabindex", "0");
-    backButton.setAttribute("tooltip",
-      this.node.getAttribute("data-subviewbutton-tooltip"));
-    backButton.addEventListener("command", () => {
-      // The panelmultiview element may change if the view is reused.
-      viewNode.panelMultiView.goBack();
-      backButton.blur();
-    });
-
-    let label = this.document.createElement("label");
-    label.setAttribute("value", titleText);
-
-    header.append(backButton, label);
-    viewNode.prepend(header);
-  }
-
   goBack() {
     let previous = this.openViews.pop();
     let current = this._currentSubView;
     return this.showSubView(current, null, previous);
   }
 
   /**
    * Checks whether it is possible to navigate backwards currently. Returns
@@ -361,34 +324,29 @@ this.PanelMultiView = class extends this
    * @param {panelview} [theOne] The panelview DOM node to ensure is visible.
    *                             Optional.
    */
   hideAllViewsExcept(theOne = null) {
     for (let panelview of this.knownViews) {
       // When the panelview was already reparented, don't interfere any more.
       if (panelview == theOne || !this.node || panelview.panelMultiView != this.node)
         continue;
-      if (panelview.hasAttribute("current"))
-        this._dispatchViewEvent(panelview, "ViewHiding");
-      panelview.removeAttribute("current");
+      PanelView.forNode(panelview).current = false;
     }
 
     this._viewShowing = null;
 
     if (!this.node || !theOne)
       return;
 
     if (!this.openViews.includes(theOne))
       this.openViews.push(theOne);
-    if (!theOne.hasAttribute("current")) {
-      theOne.setAttribute("current", true);
-      this.descriptionHeightWorkaround(theOne);
-      this._dispatchViewEvent(theOne, "ViewShown");
-    }
-    this._showingSubView = theOne.id != this._mainViewId;
+
+    PanelView.forNode(theOne).current = true;
+    this.showingSubView = theOne.id != this._mainViewId;
   }
 
   showSubView(aViewId, aAnchor, aPreviousView) {
     this._currentShowPromise = (async () => {
       // Support passing in the node directly.
       let viewNode = typeof aViewId == "string" ? this.node.querySelector("#" + aViewId) : aViewId;
       if (!viewNode) {
         viewNode = this.document.getElementById(aViewId);
@@ -396,24 +354,25 @@ this.PanelMultiView = class extends this
           this._viewStack.appendChild(viewNode);
         } else {
           throw new Error(`Subview ${aViewId} doesn't exist!`);
         }
       } else if (viewNode.parentNode == this._panelViewCache) {
         this._viewStack.appendChild(viewNode);
       }
 
+      let nextPanelView = PanelView.forNode(viewNode);
       this.knownViews.add(viewNode);
 
       viewNode.panelMultiView = this.node;
 
       let reverse = !!aPreviousView;
       if (!reverse) {
-        this._setHeader(viewNode, viewNode.getAttribute("title") ||
-                                  (aAnchor && aAnchor.getAttribute("label")));
+        nextPanelView.headerText = viewNode.getAttribute("title") ||
+                                   (aAnchor && aAnchor.getAttribute("label"));
       }
 
       let previousViewNode = aPreviousView || this._currentSubView;
       // If the panelview to show is the same as the previous one, the 'ViewShowing'
       // event has already been dispatched. Don't do it twice.
       let showingSameView = viewNode == previousViewNode;
       let playTransition = (!!previousViewNode && !showingSameView && this._panel.state == "open");
       let isMainView = viewNode.id == this._mainViewId;
@@ -430,20 +389,17 @@ this.PanelMultiView = class extends this
         this._mainViewHeight = previousRect.height;
         this._viewContainer.style.minHeight = this._mainViewHeight + "px";
       }
 
       this._viewShowing = viewNode;
       // Because the 'mainview' attribute may be out-of-sync, due to view node
       // reparenting in combination with ephemeral PanelMultiView instances,
       // this is the best place to correct it (just before showing).
-      if (isMainView)
-        viewNode.setAttribute("mainview", true);
-      else
-        viewNode.removeAttribute("mainview");
+      nextPanelView.mainview = isMainView;
 
       if (aAnchor) {
         viewNode.classList.add("PanelUI-subView");
       }
       if (!isMainView && this._mainViewWidth)
         viewNode.style.maxWidth = viewNode.style.minWidth = this._mainViewWidth + "px";
 
       if (!showingSameView || !viewNode.hasAttribute("current")) {
@@ -451,17 +407,17 @@ this.PanelMultiView = class extends this
         // to lazily populate the subview with things or perhaps even cancel this
         // whole operation.
         let detail = {
           blockers: new Set(),
           addBlocker(promise) {
             this.blockers.add(promise);
           }
         };
-        let cancel = this._dispatchViewEvent(viewNode, "ViewShowing", aAnchor, detail);
+        let cancel = nextPanelView.dispatchCustomEvent("ViewShowing", detail, true);
         if (detail.blockers.size) {
           try {
             let results = await Promise.all(detail.blockers);
             cancel = cancel || results.some(val => val === false);
           } catch (e) {
             Cu.reportError(e);
             cancel = true;
           }
@@ -508,16 +464,18 @@ this.PanelMultiView = class extends this
     // There's absolutely no need to show off our epic animation skillz when
     // the panel's not even open.
     if (this._panel.state != "open") {
       return;
     }
 
     const {window, document} = this;
 
+    let nextPanelView = PanelView.forNode(viewNode);
+
     if (this._autoResizeWorkaroundTimer)
       window.clearTimeout(this._autoResizeWorkaroundTimer);
 
     let details = this._transitionDetails = {
       phase: TRANSITION_PHASES.START,
       previousViewNode, viewNode, reverse, anchor
     };
 
@@ -557,17 +515,17 @@ this.PanelMultiView = class extends this
       let oldSibling = viewNode.nextSibling || null;
       this._offscreenViewStack.style.minHeight =
         this._viewContainer.style.height;
       this._offscreenViewStack.appendChild(viewNode);
       viewNode.setAttribute("in-transition", true);
 
       // Now that the subview is visible, we can check the height of the
       // description elements it contains.
-      this.descriptionHeightWorkaround(viewNode);
+      nextPanelView.descriptionHeightWorkaround();
 
       viewRect = await BrowserUtils.promiseLayoutFlushed(this.document, "layout", () => {
         return this._dwu.getBoundsWithoutFlushing(viewNode);
       });
 
       try {
         this._viewStack.insertBefore(viewNode, oldSibling);
       } catch (ex) {
@@ -707,45 +665,16 @@ this.PanelMultiView = class extends this
       // We force 'display: none' on the previous view node to make sure that it
       // doesn't cause an annoying flicker whilst resetting the styles above.
       previousViewNode.style.display = "none";
       await BrowserUtils.promiseLayoutFlushed(this.document, "layout", () => {});
       previousViewNode.style.removeProperty("display");
     }
   }
 
-  /**
-   * Helper method to emit an event on a panelview, whilst also making sure that
-   * the correct method is called on CustomizableWidget instances.
-   *
-   * @param  {panelview} viewNode  Target of the event to dispatch.
-   * @param  {String}    eventName Name of the event to dispatch.
-   * @param  {DOMNode}   [anchor]  Node where the panel is anchored to. Optional.
-   * @param  {Object}    [detail]  Event detail object. Optional.
-   * @return {Boolean} `true` if the event was canceled by an event handler, `false`
-   *                   otherwise.
-   */
-  _dispatchViewEvent(viewNode, eventName, anchor, detail) {
-    let cancel = false;
-    if (eventName != "PanelMultiViewHidden") {
-      // Don't need to do this for PanelMultiViewHidden event
-      CustomizableUI.ensureSubviewListeners(viewNode);
-    }
-
-    let evt = new this.window.CustomEvent(eventName, {
-      detail,
-      bubbles: true,
-      cancelable: eventName == "ViewShowing"
-    });
-    viewNode.dispatchEvent(evt);
-    if (!cancel)
-      cancel = evt.defaultPrevented;
-    return cancel;
-  }
-
   _calculateMaxHeight() {
     // While opening the panel, we have to limit the maximum height of any
     // view based on the space that will be available. We cannot just use
     // window.screen.availTop and availHeight because these may return an
     // incorrect value when the window spans multiple screens.
     let anchorBox = this._panel.anchorNode.boxObject;
     let screen = this._screenManager.screenForRect(anchorBox.screenX,
                                                    anchorBox.screenY,
@@ -815,17 +744,17 @@ this.PanelMultiView = class extends this
           this._viewStack.style.maxHeight = maxHeight + "px";
           this._offscreenViewStack.style.maxHeight = maxHeight + "px";
         }
         break;
       }
       case "popupshown":
         // Now that the main view is visible, we can check the height of the
         // description elements it contains.
-        this.descriptionHeightWorkaround();
+        PanelView.forNode(this._mainView).descriptionHeightWorkaround();
         break;
       case "popuphidden": {
         // WebExtensions consumers can hide the popup from viewshowing, or
         // mid-transition, which disrupts our state:
         this._viewShowing = null;
         this._transitioning = false;
         this.node.removeAttribute("panelopen");
         this.showMainView();
@@ -845,17 +774,17 @@ this.PanelMultiView = class extends this
         // when the popup is opened again, e.g. through touch mode sizing.
         this._mainViewHeight = 0;
         this._mainViewWidth = 0;
         this._viewContainer.style.removeProperty("min-height");
         this._viewStack.style.removeProperty("max-height");
         this._viewContainer.style.removeProperty("min-width");
         this._viewContainer.style.removeProperty("max-width");
 
-        this._dispatchViewEvent(this.node, "PanelMultiViewHidden");
+        this.dispatchCustomEvent("PanelMultiViewHidden");
         break;
       }
     }
   }
 
   /**
    * Based on going up or down, select the previous or next focusable button
    * in the current view.
@@ -1038,33 +967,112 @@ this.PanelMultiView = class extends this
    * @param {panelview} view the view in which to update keyboard focus.
    */
   _updateKeyboardFocus(view) {
     let navMap = this._keyNavigationMap.get(view);
     if (navMap && navMap.selected && navMap.selected.get()) {
       navMap.selected.get().focus();
     }
   }
+};
+
+/**
+ * This is associated to <panelview> elements.
+ */
+this.PanelView = class extends this.AssociatedToNode {
+  /**
+   * The "mainview" attribute is set before the panel is opened when this view
+   * is displayed as the main view, and is removed before the <panelview> is
+   * displayed as a subview. The same view element can be displayed as a main
+   * view and as a subview at different times.
+   */
+  set mainview(value) {
+    if (value) {
+      this.node.setAttribute("mainview", true);
+    } else {
+      this.node.removeAttribute("mainview");
+    }
+  }
+
+  set current(value) {
+    if (value) {
+      if (!this.node.hasAttribute("current")) {
+        this.node.setAttribute("current", true);
+        this.descriptionHeightWorkaround();
+        this.dispatchCustomEvent("ViewShown");
+      }
+    } else if (this.node.hasAttribute("current")) {
+      this.dispatchCustomEvent("ViewHiding");
+      this.node.removeAttribute("current");
+    }
+  }
+
+  /**
+   * Adds a header with the given title, or removes it if the title is empty.
+   */
+  set headerText(value) {
+    // If the header already exists, update or remove it as requested.
+    let header = this.node.firstChild;
+    if (header && header.classList.contains("panel-header")) {
+      if (value) {
+        header.querySelector("label").setAttribute("value", value);
+      } else {
+        header.remove();
+      }
+      return;
+    }
+
+    // The header doesn't exist, only create it if needed.
+    if (!value) {
+      return;
+    }
+
+    header = this.document.createElement("box");
+    header.classList.add("panel-header");
+
+    let backButton = this.document.createElement("toolbarbutton");
+    backButton.className =
+      "subviewbutton subviewbutton-iconic subviewbutton-back";
+    backButton.setAttribute("closemenu", "none");
+    backButton.setAttribute("tabindex", "0");
+    backButton.setAttribute("tooltip",
+      this.node.getAttribute("data-subviewbutton-tooltip"));
+    backButton.addEventListener("command", () => {
+      // The panelmultiview element may change if the view is reused.
+      this.node.panelMultiView.goBack();
+      backButton.blur();
+    });
+
+    let label = this.document.createElement("label");
+    label.setAttribute("value", value);
+
+    header.append(backButton, label);
+    this.node.prepend(header);
+  }
+
+  /**
+   * Also make sure that the correct method is called on CustomizableWidget.
+   */
+  dispatchCustomEvent(...args) {
+    CustomizableUI.ensureSubviewListeners(this.node);
+    super.dispatchCustomEvent(...args);
+  }
 
   /**
    * If the main view or a subview contains wrapping elements, the attribute
    * "descriptionheightworkaround" should be set on the view to force all the
    * wrapping "description", "label" or "toolbarbutton" elements to a fixed
    * height. If the attribute is set and the visibility, contents, or width
    * of any of these elements changes, this function should be called to
    * refresh the calculated heights.
    *
    * This may trigger a synchronous layout.
-   *
-   * @param viewNode
-   *        Indicates the node to scan for descendant elements. This is the main
-   *        view if omitted.
    */
-  descriptionHeightWorkaround(viewNode = this._mainView) {
-    if (!viewNode || !viewNode.hasAttribute("descriptionheightworkaround")) {
+  descriptionHeightWorkaround() {
+    if (!this.node.hasAttribute("descriptionheightworkaround")) {
       // This view does not require the workaround.
       return;
     }
 
     // We batch DOM changes together in order to reduce synchronous layouts.
     // First we reset any change we may have made previously. The first time
     // this is called, and in the best case scenario, this has no effect.
     let items = [];
@@ -1072,26 +1080,26 @@ this.PanelMultiView = class extends this
     // and also don't have a value attribute can be multiline (if their
     // text content is long enough).
     let isMultiline = ":not(:-moz-any([hidden],[value],:empty))";
     let selector = [
       "description" + isMultiline,
       "label" + isMultiline,
       "toolbarbutton[wrap]:not([hidden])",
     ].join(",");
-    for (let element of viewNode.querySelectorAll(selector)) {
+    for (let element of this.node.querySelectorAll(selector)) {
       // Ignore items in hidden containers.
       if (element.closest("[hidden]")) {
         continue;
       }
       // Take the label for toolbarbuttons; it only exists on those elements.
       element = element.labelElement || element;
 
       let bounds = element.getBoundingClientRect();
-      let previous = this._multiLineElementsMap.get(element);
+      let previous = gMultiLineElementsMap.get(element);
       // We don't need to (re-)apply the workaround for invisible elements or
       // on elements we've seen before and haven't changed in the meantime.
       if (!bounds.width || !bounds.height ||
           (previous && element.textContent == previous.textContent &&
                        bounds.width == previous.bounds.width)) {
         continue;
       }
 
@@ -1107,26 +1115,21 @@ this.PanelMultiView = class extends this
     // We now read the computed style to store the height of any element that
     // may contain wrapping text.
     for (let item of items) {
       item.bounds = item.element.getBoundingClientRect();
     }
 
     // Now we can make all the necessary DOM changes at once.
     for (let { element, bounds } of items) {
-      this._multiLineElementsMap.set(element, { bounds, textContent: element.textContent });
+      gMultiLineElementsMap.set(element, { bounds, textContent: element.textContent });
       element.style.height = bounds.height + "px";
     }
   }
-};
 
-/**
- * This is associated to <panelview> elements.
- */
-this.PanelView = class extends this.AssociatedToNode {
   /**
    * Retrieves the button elements that can be used for navigation using the
    * keyboard, that is all enabled buttons including the back button if visible.
    *
    * @return {Array}
    */
   getNavigableElements() {
     let buttons = Array.from(this.node.querySelectorAll(".subviewbutton:not([disabled])"));