Bug 1428839 - Part 3 - Separate the set of known views from the stack of open views. r=Gijs draft
authorPaolo Amadini <paolo.mozmail@amadzone.org>
Thu, 18 Jan 2018 16:05:36 +0000
changeset 722192 06afc9fd6f106a974b35249ede9dd459f6360b27
parent 721629 c15ad57a3644635e346953889a95531f77c466e1
child 722193 c6fc658c774f3facc61ec4f4276a34f885cfbf8b
push id96081
push userpaolo.mozmail@amadzone.org
push dateThu, 18 Jan 2018 16:15:56 +0000
reviewersGijs
bugs1428839
milestone59.0a1
Bug 1428839 - Part 3 - Separate the set of known views from the stack of open views. r=Gijs This makes the code easier to follow and facilitates future refactoring, for example the set of known views can be removed entirely by making the clean up and navigation code use the stack of open views. The SlidingPanelView class can thus be removed, saving various lines of code. The class implemented a small optimization for garbage collection, that was already less effective because various other objects are created during each view transition anyways. MozReview-Commit-ID: Z4JJMklUMf
browser/components/customizableui/PanelMultiView.jsm
--- a/browser/components/customizableui/PanelMultiView.jsm
+++ b/browser/components/customizableui/PanelMultiView.jsm
@@ -1,12 +1,49 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+/**
+ * Allows a popup panel to host multiple subviews. The main view shown when the
+ * panel is opened may slide out to display a subview, which in turn may lead to
+ * other subviews in a cascade menu pattern.
+ *
+ * The <panel> element should contain a <panelmultiview> element. Views are
+ * declared using <panelview> elements that are usually children of the main
+ * <panelmultiview> element, although they don't need to be, as views can also
+ * be imported into the panel from other panels or popup sets.
+ *
+ * The main view can be declared using the mainViewId attribute, and specific
+ * subviews can slide in using the showSubView method. Backwards navigation can
+ * be done using the goBack method or through a button in the subview headers.
+ *
+ * This diagram shows how <panelview> nodes move during navigation:
+ *
+ *   In this <panelmultiview>     In other panels    Action
+ *             ┌───┬───┬───┐        ┌───┬───┐
+ *             │(A)│ B │ C │        │ D │ E │          Open panel
+ *             └───┴───┴───┘        └───┴───┘
+ *         ┌───┬───┬───┐            ┌───┬───┐
+ *         │ A │(C)│ B │            │ D │ E │          Show subview C
+ *         └───┴───┴───┘            └───┴───┘
+ *     ┌───┬───┬───┬───┐            ┌───┐
+ *     │ A │ C │(D)│ B │            │ E │              Show subview D
+ *     └───┴───┴───┴───┘            └───┘
+ *         ┌───┬───┬───┬───┐        ┌───┐
+ *         │ A │(C)│ D │ B │        │ E │              Go back
+ *         └───┴───┴───┴───┘        └───┘
+ *               │
+ *               └── Currently visible view
+ *
+ * If the <panelmultiview> element is "ephemeral", imported subviews will be
+ * moved out again to the element specified by the viewCacheId attribute, so
+ * that the panel element can be removed safely.
+ */
+
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["PanelMultiView"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
@@ -19,135 +56,16 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 const TRANSITION_PHASES = Object.freeze({
   START: 1,
   PREPARE: 2,
   TRANSITION: 3,
   END: 4
 });
 
 /**
- * Simple implementation of the sliding window pattern; panels are added to a
- * linked list, in-order, and the currently shown panel is remembered using a
- * marker. The marker shifts as navigation between panels is continued, where
- * the panel at index 0 is always the starting point:
- *           ┌────┬────┬────┬────┐
- *           │▓▓▓▓│    │    │    │ Start
- *           └────┴────┴────┴────┘
- *      ┌────┬────┬────┬────┐
- *      │    │▓▓▓▓│    │    │      Forward
- *      └────┴────┴────┴────┘
- * ┌────┬────┬────┬────┐
- * │    │    │▓▓▓▓│    │           Forward
- * └────┴────┴────┴────┘
- *      ┌────┬────┬────┬────┐
- *      │    │▓▓▓▓│    │    │      Back
- *      └────┴────┴────┴────┘
- */
-class SlidingPanelViews extends Array {
-  constructor() {
-    super();
-    this._marker = 0;
-  }
-
-  /**
-   * Get the index that points to the currently selected view.
-   *
-   * @return {Number}
-   */
-  get current() {
-    return this._marker;
-  }
-
-  /**
-   * Setter for the current index, which changes the order of elements and
-   * updates the internal marker for the currently selected view.
-   * We're manipulating the array directly to have it reflect the order of
-   * navigation, instead of continuously growing the array with the next selected
-   * view to keep memory usage within reasonable proportions. With this method,
-   * the data structure grows no larger than the number of panels inside the
-   * panelMultiView.
-   *
-   * @param  {Number} index Index of the item to move to the current position.
-   * @return {Number} The new marker index.
-   */
-  set current(index) {
-    if (index == this._marker) {
-      // Never change a winning team.
-      return index;
-    }
-    if (index == -1 || index > (this.length - 1)) {
-      throw new Error(`SlidingPanelViews :: index ${index} out of bounds`);
-    }
-
-    let view = this.splice(index, 1)[0];
-    if (this._marker > index) {
-      // Correct the current marker if the view-to-select was removed somewhere
-      // before it.
-      --this._marker;
-    }
-    // Then add the view-to-select right after the currently selected view.
-    this.splice(++this._marker, 0, view);
-    return this._marker;
-  }
-
-  /**
-   * Getter for the currently selected view node.
-   *
-   * @return {panelview}
-   */
-  get currentView() {
-    return this[this._marker];
-  }
-
-  /**
-   * Setter for the currently selected view node.
-   *
-   * @param  {panelview} view
-   * @return {Number} Index of the currently selected view.
-   */
-  set currentView(view) {
-    if (!view)
-      return this.current;
-    // This will throw an error if the view could not be found.
-    return this.current = this.indexOf(view);
-  }
-
-  /**
-   * Getter for the previous view, which is always positioned one position after
-   * the current view.
-   *
-   * @return {panelview}
-   */
-  get previousView() {
-    return this[this._marker + 1];
-  }
-
-  /**
-   * Going back is an explicit action on the data structure, moving the marker
-   * one step back.
-   *
-   * @return {Array} A list of two items: the newly selected view and the previous one.
-   */
-  back() {
-    if (this._marker > 0)
-      --this._marker;
-    return [this.currentView, this.previousView];
-  }
-
-  /**
-   * Reset the data structure to its original construct, removing all references
-   * to view nodes.
-   */
-  clear() {
-    this._marker = 0;
-    this.splice(0, this.length);
-  }
-}
-
-/**
  * This is the implementation of the panelUI.xml XBL binding, moved to this
  * module, to make it easier to fork the logic for the newer photon structure.
  * Goals are:
  * 1. to make it easier to programmatically extend the list of panels,
  * 2. allow for navigation between panels multiple levels deep and
  * 3. maintain the pre-photon structure with as little effort possible.
  *
  * @type {PanelMultiView}
@@ -195,24 +113,16 @@ this.PanelMultiView = class {
    * @return {Boolean} |true| when the 'ephemeral' attribute is set, which means
    *                   that this instance should be ready to be thrown away at
    *                   any time.
    */
   get _ephemeral() {
     return this.node.hasAttribute("ephemeral");
   }
 
-  get panelViews() {
-    if (this._panelViews)
-      return this._panelViews;
-
-    this._panelViews = new SlidingPanelViews();
-    this._panelViews.push(...this.node.getElementsByTagName("panelview"));
-    return this._panelViews;
-  }
   get _dwu() {
     if (this.__dwu)
       return this.__dwu;
     return this.__dwu = this.window.QueryInterface(Ci.nsIInterfaceRequestor)
                                    .getInterface(Ci.nsIDOMWindowUtils);
   }
   get _screenManager() {
     if (this.__screenManager)
@@ -224,20 +134,19 @@ this.PanelMultiView = class {
    * @return {panelview} the currently visible subview OR the subview that is
    *                     about to be shown whilst a 'ViewShowing' event is being
    *                     dispatched.
    */
   get current() {
     return this._viewShowing || this._currentSubView;
   }
   get _currentSubView() {
-    return this.panelViews.currentView;
-  }
-  set _currentSubView(panel) {
-    this.panelViews.currentView = panel;
+    // Peek the top of the stack, but fall back to the main view if the list of
+    // opened views is currently empty.
+    return this.openViews[this.openViews.length - 1] || this._mainView;
   }
   /**
    * @return {Promise} showSubView() returns a promise, which is kept here for
    *                   random access.
    */
   get currentShowPromise() {
     return this._currentShowPromise || Promise.resolve();
   }
@@ -254,17 +163,18 @@ this.PanelMultiView = class {
 
   constructor(xulNode, testMode = false) {
     this.node = xulNode;
     // If `testMode` is `true`, the consumer is only interested in accessing the
     // methods of this instance. (E.g. in unit tests.)
     if (testMode)
       return;
 
-    this._currentSubView = this._subViewObserver = null;
+    this.knownViews = new Set(this.node.getElementsByTagName("panelview"));
+    this.openViews = [];
     this._mainViewHeight = 0;
     this.__transitioning = this._ignoreMutations = this._showingSubView = false;
 
     const {document, window} = this;
 
     this._viewContainer =
       document.getAnonymousElementByAttribute(this.node, "anonid", "viewContainer");
     this._viewStack =
@@ -324,17 +234,16 @@ this.PanelMultiView = class {
     let mainView = this._mainView;
     if (mainView) {
       if (this._panelViewCache)
         this._panelViewCache.appendChild(mainView);
       mainView.removeAttribute("mainview");
     }
 
     this._moveOutKids(this._viewStack);
-    this.panelViews.clear();
     this._panel.removeEventListener("mousemove", this);
     this._panel.removeEventListener("popupshowing", this);
     this._panel.removeEventListener("popuppositioned", this);
     this._panel.removeEventListener("popupshown", this);
     this._panel.removeEventListener("popuphidden", this);
     this.window.removeEventListener("keydown", this);
     this.node = this._viewContainer = this._viewStack = this.__dwu =
       this._panelViewCache = this._transitionDetails = null;
@@ -396,61 +305,63 @@ this.PanelMultiView = class {
     let label = this.document.createElement("label");
     label.setAttribute("value", titleText);
 
     header.append(backButton, label);
     viewNode.prepend(header);
   }
 
   goBack() {
-    let [current, previous] = this.panelViews.back();
+    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
    * false if this is the panelmultiview's mainview, true otherwise.
    *
    * @param  {panelview} view View to check, defaults to the currently active view.
    * @return {Boolean}
    */
   _canGoBack(view = this._currentSubView) {
     return view.id != this._mainViewId;
   }
 
   showMainView() {
-    if (!this._mainViewId)
+    if (!this.node || !this._mainViewId)
       return Promise.resolve();
 
     return this.showSubView(this._mainView);
   }
 
   /**
    * Ensures that all the panelviews, that are currently part of this instance,
    * are hidden, except one specifically.
    *
    * @param {panelview} [theOne] The panelview DOM node to ensure is visible.
    *                             Optional.
    */
   hideAllViewsExcept(theOne = null) {
-    for (let panelview of this.panelViews) {
+    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");
     }
 
     this._viewShowing = null;
 
     if (!this.node || !theOne)
       return;
 
-    this._currentSubView = theOne;
+    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;
   }
 
@@ -464,18 +375,17 @@ this.PanelMultiView = class {
           this._viewStack.appendChild(viewNode);
         } else {
           throw new Error(`Subview ${aViewId} doesn't exist!`);
         }
       } else if (viewNode.parentNode == this._panelViewCache) {
         this._viewStack.appendChild(viewNode);
       }
 
-      if (!this.panelViews.includes(viewNode))
-        this.panelViews.push(viewNode);
+      this.knownViews.add(viewNode);
 
       viewNode.panelMultiView = this.node;
 
       let reverse = !!aPreviousView;
       if (!reverse) {
         this._setHeader(viewNode, viewNode.getAttribute("title") ||
                                   (aAnchor && aAnchor.getAttribute("label")));
       }