Bug 1439358 - Part 5 - Handle panel hiding during ViewShowing events. r=Gijs draft
authorPaolo Amadini <paolo.mozmail@amadzone.org>
Thu, 22 Feb 2018 17:11:25 +0000
changeset 758542 31184a13b82c1f8fd60b6551a65aae6e4e11a8fe
parent 758480 c481e7957c2372f8613b3cac9ecc61563d60c4e6
child 758543 5c90cbbc4b194ec4bb4e28f50082097371e3086d
push id100095
push userpaolo.mozmail@amadzone.org
push dateThu, 22 Feb 2018 17:13:44 +0000
reviewersGijs
bugs1439358
milestone60.0a1
Bug 1439358 - Part 5 - Handle panel hiding during ViewShowing events. r=Gijs The ViewHiding event is now dispatched consistently, regardless of whether the ViewShowing event is canceled or the panel is closed during the event. This is done by a new _openView helper, while the logic that is specific to each of the showMainView, showSubView, and goBack functions has been moved out of the _showView function. MozReview-Commit-ID: 5WvW6THWbyb
browser/components/customizableui/PanelMultiView.jsm
--- a/browser/components/customizableui/PanelMultiView.jsm
+++ b/browser/components/customizableui/PanelMultiView.jsm
@@ -588,50 +588,99 @@ this.PanelMultiView = class extends this
    * @param viewIdOrNode
    *        DOM element or string ID of the <panelview> to display.
    * @param anchor
    *        DOM element that triggered the subview, which will be highlighted
    *        and whose "label" attribute will be used for the title of the
    *        subview when a "title" attribute is not specified.
    */
   showSubView(viewIdOrNode, anchor) {
+    this._showSubView(viewIdOrNode, anchor).catch(Cu.reportError);
+  }
+  async _showSubView(viewIdOrNode, anchor) {
     let viewNode = typeof viewIdOrNode == "string" ?
                    this.document.getElementById(viewIdOrNode) : viewIdOrNode;
     if (!viewNode) {
       Cu.reportError(new Error(`Subview ${viewIdOrNode} doesn't exist.`));
       return;
     }
 
+    let prevPanelView = this.openViews[this.openViews.length - 1];
     let nextPanelView = PanelView.forNode(viewNode);
     if (this.openViews.includes(nextPanelView)) {
       Cu.reportError(new Error(`Subview ${viewNode.id} is already open.`));
       return;
     }
+    if (!(await this._openView(nextPanelView))) {
+      return;
+    }
 
-    this._showView(nextPanelView, anchor);
+    prevPanelView.captureKnownSize();
+
+    // The main view of a panel can be a subview in another one. Make sure to
+    // reset all the properties that may be set on a subview.
+    nextPanelView.mainview = false;
+    // The header may change based on how the subview was opened.
+    nextPanelView.headerText = viewNode.getAttribute("title") ||
+                               (anchor && anchor.getAttribute("label"));
+    // The constrained width of subviews may also vary between panels.
+    nextPanelView.minMaxWidth = prevPanelView.knownWidth;
+
+    if (anchor) {
+      viewNode.classList.add("PanelUI-subView");
+    }
+
+    await this._transitionViews(prevPanelView.node, viewNode, false, anchor);
   }
 
   /**
    * Navigates backwards by sliding out the most recent subview.
    */
   goBack() {
+    this._goBack().catch(Cu.reportError);
+  }
+  async _goBack() {
     if (this.openViews.length < 2) {
       // This may be called by keyboard navigation or external code when only
       // the main view is open.
       return;
     }
 
-    this._showView(this.openViews[this.openViews.length - 2], null, true);
+    let prevPanelView = this.openViews[this.openViews.length - 1];
+    let nextPanelView = this.openViews[this.openViews.length - 2];
+
+    prevPanelView.captureKnownSize();
+    await this._transitionViews(prevPanelView.node, nextPanelView.node, true);
+
+    this.openViews.pop();
   }
 
+  /**
+   * Prepares the main view before showing the panel.
+   */
   async showMainView() {
-    if (!this.node || !this._mainViewId)
+    if (!this.node || !this._mainViewId) {
+      return false;
+    }
+
+    let nextPanelView = PanelView.forNode(this._mainView);
+    if (!(await this._openView(nextPanelView))) {
       return false;
+    }
 
-    return this._showView(PanelView.forNode(this._mainView));
+    // The main view of a panel can be a subview in another one. Make sure to
+    // reset all the properties that may be set on a subview.
+    nextPanelView.mainview = true;
+    nextPanelView.headerText = "";
+    nextPanelView.minMaxWidth = 0;
+
+    await this._cleanupTransitionPhase();
+    this.hideAllViewsExcept(nextPanelView);
+
+    return true;
   }
 
   /**
    * Ensures that all the panelviews, that are currently part of this instance,
    * are hidden, except one specifically.
    *
    * @param {panelview} [nextPanelView]
    *        The PanelView object to ensure is visible. Optional.
@@ -646,84 +695,50 @@ this.PanelMultiView = class extends this
 
     if (!this.node || !nextPanelView)
       return;
 
     nextPanelView.current = true;
     this.showingSubView = nextPanelView.node.id != this._mainViewId;
   }
 
-  async _showView(nextPanelView, anchor, reverse) {
-    try {
-      let viewNode = nextPanelView.node;
-      this.knownViews.add(nextPanelView);
-
-      viewNode.panelMultiView = this.node;
-
-      let previousViewNode = this._currentSubView;
-
-      if (viewNode.parentNode != this._viewStack) {
-        this._viewStack.appendChild(viewNode);
-      }
-
-      // Emit the ViewShowing event so that the widget definition has a chance
-      // to lazily populate the subview with things or perhaps even cancel this
-      // whole operation.
-      if (await nextPanelView.dispatchAsyncEvent("ViewShowing")) {
-        return false;
-      }
-
-      if (!reverse) {
-        this.openViews.push(nextPanelView);
-      }
-
-      // 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 prevPanelView = PanelView.forNode(previousViewNode);
-      prevPanelView.captureKnownSize();
+  /**
+   * Opens the specified PanelView and dispatches the ViewShowing event, which
+   * can be used to populate the subview or cancel the operation.
+   *
+   * @resolves With true if the view was opened, false otherwise.
+   */
+  async _openView(panelView) {
+    if (panelView.node.parentNode != this._viewStack) {
+      this._viewStack.appendChild(panelView.node);
+    }
 
-      if (!reverse) {
-        // We are opening a new view, either because we are navigating forward
-        // or because we are showing the main view. Some properties of the view
-        // may vary between panels, so we make sure to update them every time.
-        // Firstly, make sure that the header matches how the view was opened.
-        nextPanelView.headerText = viewNode.getAttribute("title") ||
-                                   (anchor && anchor.getAttribute("label"));
-        // The main view of a panel can be a subview in another one.
-        let isMainView = viewNode.id == this._mainViewId;
-        nextPanelView.mainview = isMainView;
-        // The constrained width of subviews may also vary between panels.
-        nextPanelView.minMaxWidth = isMainView ? 0 : prevPanelView.knownWidth;
-      }
-
-      if (anchor) {
-        viewNode.classList.add("PanelUI-subView");
-      }
+    this.knownViews.add(panelView);
+    panelView.node.panelMultiView = this.node;
+    this.openViews.push(panelView);
 
-      // Now we have to transition the panel. If we've got an older transition
-      // still running, make sure to clean it up.
-      await this._cleanupTransitionPhase();
-      if (!showingSameView && this._panel.state == "open") {
-        await this._transitionViews(previousViewNode, viewNode, reverse, anchor);
-        nextPanelView.focusSelectedElement();
-      } else {
-        this.hideAllViewsExcept(nextPanelView);
-      }
+    let canceled = await panelView.dispatchAsyncEvent("ViewShowing");
 
-      if (reverse) {
-        this.openViews.pop();
-      }
-
-      return true;
-    } catch (ex) {
-      Cu.reportError(ex);
+    // The panel can be hidden while we are processing the ViewShowing event.
+    // This results in all the views being closed synchronously, and at this
+    // point the ViewHiding event has already been dispatched for all of them.
+    if (!this.openViews.length) {
       return false;
     }
+
+    // Check if the event requested cancellation but the panel is still open.
+    if (canceled) {
+      // Handlers for ViewShowing can't know if a different handler requested
+      // cancellation, so we dispatch an event to give a chance to clean up.
+      panelView.dispatchCustomEvent("ViewHiding");
+      this.openViews.pop();
+      return false;
+    }
+
+    return true;
   }
 
   /**
    * Apply a transition to 'slide' from the currently active view to the next
    * one.
    * Sliding the next subview in means that the previous panelview stays where it
    * is and the active panelview slides in from the left in LTR mode, right in
    * RTL mode.
@@ -733,16 +748,19 @@ this.PanelMultiView = class extends this
    * @param {panelview} viewNode         Node that will becode the active view,
    *                                     after the transition has finished.
    * @param {Boolean}   reverse          Whether we're navigation back to a
    *                                     previous view or forward to a next view.
    * @param {Element}   anchor           the anchor for which we're opening
    *                                     a new panelview, if any
    */
   async _transitionViews(previousViewNode, viewNode, reverse, anchor) {
+    // Clean up any previous transition that may be active at this point.
+    await this._cleanupTransitionPhase();
+
     // 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;
 
@@ -874,17 +892,23 @@ this.PanelMultiView = class extends this
         this._viewContainer.removeEventListener("transitioncancel", details.cancelListener);
         delete details.cancelListener;
         resolve();
       });
     });
 
     details.phase = TRANSITION_PHASES.END;
 
+    // This will complete the operation by removing any transition properties.
     await this._cleanupTransitionPhase(details);
+
+    // Focus the correct element, unless the view was closed in the meantime.
+    if (nextPanelView.node.panelMultiView == this.node) {
+      nextPanelView.focusSelectedElement();
+    }
   }
 
   /**
    * Attempt to clean up the attributes and properties set by `_transitionViews`
    * above. Which attributes and properties depends on the phase the transition
    * was left from - normally that'd be `TRANSITION_PHASES.END`.
    *
    * @param {Object} details Dictionary object containing details of the transition