Bug 1354159 - Part 2 - Introduce a new Places view type, PlacesPanelview, which can visualize query results inside panelview nodes. r?mak,Gijs draft
authorMike de Boer <mdeboer@mozilla.com>
Thu, 29 Jun 2017 17:57:24 -0700
changeset 602440 1be20e6a69d8c87e8bd9234d9228a03aff5f518d
parent 602439 9c20523a0ff6ffea605f72dd7027840e3216f769
child 602441 290917305e7934e1400e05f570bf3aea27acfccf
push id66444
push usermdeboer@mozilla.com
push dateFri, 30 Jun 2017 01:01:10 +0000
reviewersmak, Gijs
bugs1354159
milestone56.0a1
Bug 1354159 - Part 2 - Introduce a new Places view type, PlacesPanelview, which can visualize query results inside panelview nodes. r?mak,Gijs MozReview-Commit-ID: Ft1RC7dsqKD
browser/components/customizableui/PanelMultiView.jsm
browser/components/places/PlacesUIUtils.jsm
browser/components/places/content/browserPlacesViews.js
--- a/browser/components/customizableui/PanelMultiView.jsm
+++ b/browser/components/customizableui/PanelMultiView.jsm
@@ -200,16 +200,25 @@ this.PanelMultiView = class {
                                    .getInterface(Ci.nsIDOMWindowUtils);
   }
   get _screenManager() {
     if (this.__screenManager)
       return this.__screenManager;
     return this.__screenManager = Cc["@mozilla.org/gfx/screenmanager;1"]
                                     .getService(Ci.nsIScreenManager);
   }
+  /**
+   * Getter that returns the currently visible subview OR the subview that is
+   * about to be shown whilst a 'ViewShowing' event is being dispatched.
+   *
+   * @return {panelview}
+   */
+  get current() {
+    return this._viewShowing || this._currentSubView
+  }
   get _currentSubView() {
     return this.panelViews ? this.panelViews.currentView : this.__currentSubView;
   }
   set _currentSubView(panel) {
     if (this.panelViews)
       this.panelViews.currentView = panel;
     else
       this.__currentSubView = panel;
@@ -290,16 +299,20 @@ this.PanelMultiView = class {
     });
     ["goBack", "descriptionHeightWorkaround", "setMainView", "showMainView",
      "showSubView"].forEach(method => {
       Object.defineProperty(this.node, method, {
         enumerable: true,
         value: (...args) => this[method](...args)
       });
     });
+    Object.defineProperty(this.node, "current", {
+      enumerable: true,
+      get: () => this.current
+    });
   }
 
   destructor() {
     // Guard against re-entrancy.
     if (!this.node)
       return;
 
     if (this._mainView) {
@@ -315,16 +328,17 @@ this.PanelMultiView = class {
       this._moveOutKids(this._viewStack);
       this.panelViews.clear();
     } else {
       this._clickCapturer.removeEventListener("click", this);
     }
     this._panel.removeEventListener("popupshowing", this);
     this._panel.removeEventListener("popupshown", this);
     this._panel.removeEventListener("popuphidden", this);
+    this.node.dispatchEvent(new this.window.CustomEvent("destructed"));
     this.node = this._clickCapturer = this._viewContainer = this._mainViewContainer =
       this._subViews = this._viewStack = this.__dwu = this._panelViewCache = null;
   }
 
   /**
    * Remove any child subviews into the panelViewCache, to ensure
    * they remain usable even if this panelmultiview instance is removed
    * from the DOM.
@@ -339,16 +353,26 @@ this.PanelMultiView = class {
     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);
     }
   }
 
+  _placeSubView(viewNode) {
+    if (this.panelViews) {
+      this._viewStack.appendChild(viewNode);
+      if (!this.panelViews.includes(viewNode))
+        this.panelViews.push(viewNode);
+    } else {
+      this._subViews.appendChild(viewNode);
+    }
+  }
+
   goBack(target) {
     let [current, previous] = this.panelViews.back();
     return this.showSubView(current, target, previous);
   }
 
   /**
    * Checks whether it is possible to navigate backwards currently. Returns
    * false if this is the panelmultiview's mainview, true otherwise.
@@ -404,25 +428,22 @@ this.PanelMultiView = class {
   showSubView(aViewId, aAnchor, aPreviousView) {
     const {document, window} = this;
     return (async () => {
       // Support passing in the node directly.
       let viewNode = typeof aViewId == "string" ? this.node.querySelector("#" + aViewId) : aViewId;
       if (!viewNode) {
         viewNode = document.getElementById(aViewId);
         if (viewNode) {
-          if (this.panelViews) {
-            this._viewStack.appendChild(viewNode);
-            this.panelViews.push(viewNode);
-          } else {
-            this._subViews.appendChild(viewNode);
-          }
+          this._placeSubView(viewNode);
         } else {
           throw new Error(`Subview ${aViewId} doesn't exist!`);
         }
+      } else if (viewNode.parentNode == this._panelViewCache) {
+        this._placeSubView(viewNode);
       }
 
       let reverse = !!aPreviousView;
       let previousViewNode = aPreviousView || this._currentSubView;
       let playTransition = (!!previousViewNode && previousViewNode != viewNode);
 
       let dwu, previousRect;
       if (playTransition || this.panelViews) {
@@ -467,16 +488,17 @@ this.PanelMultiView = class {
           if (custWidget.onInit)
             custWidget.onInit(aAnchor);
           custWidget.onViewShowing({ target: viewNode, preventDefault: () => cancel = true, detail });
         }
       }
       if (this.panelViews && this._mainViewWidth)
         viewNode.style.maxWidth = viewNode.style.minWidth = this._mainViewWidth + "px";
 
+      this._viewShowing = viewNode;
       let evt = new window.CustomEvent("ViewShowing", { bubbles: true, cancelable: true, detail });
       viewNode.dispatchEvent(evt);
 
       if (!cancel)
         cancel = evt.defaultPrevented;
       if (detail.blockers.size) {
         try {
           let results = await Promise.all(detail.blockers);
@@ -486,16 +508,17 @@ this.PanelMultiView = class {
           cancel = true;
         }
       }
 
       if (cancel) {
         return;
       }
 
+      this._viewShowing = null;
       this._currentSubView = viewNode;
       viewNode.setAttribute("current", true);
       if (this.panelViews) {
         this.node.setAttribute("viewtype", "subview");
         if (!playTransition)
           this.descriptionHeightWorkaround(viewNode);
       }
 
--- a/browser/components/places/PlacesUIUtils.jsm
+++ b/browser/components/places/PlacesUIUtils.jsm
@@ -659,16 +659,20 @@ this.PlacesUIUtils = {
    * Returns the closet ancestor places view for the given DOM node
    * @param aNode
    *        a DOM node
    * @return the closet ancestor places view if exists, null otherwsie.
    */
   getViewForNode: function PUIU_getViewForNode(aNode) {
     let node = aNode;
 
+    if (node.localName == "panelview" && node._placesView) {
+      return node._placesView;
+    }
+
     // The view for a <menu> of which its associated menupopup is a places
     // view, is the menupopup.
     if (node.localName == "menu" && !node._placesNode &&
         node.lastChild._placesView)
       return node.lastChild._placesView;
 
     while (node instanceof Ci.nsIDOMElement) {
       if (node._placesView)
--- a/browser/components/places/content/browserPlacesViews.js
+++ b/browser/components/places/content/browserPlacesViews.js
@@ -7,20 +7,24 @@
 Components.utils.import("resource://gre/modules/AppConstants.jsm");
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
 
 /**
  * The base view implements everything that's common to the toolbar and
  * menu views.
  */
-function PlacesViewBase(aPlace, aOptions) {
-  this.place = aPlace;
+function PlacesViewBase(aPlace, aOptions = {}) {
+  if ("rootElt" in aOptions)
+    this._rootElt = aOptions.rootElt;
+  if ("viewElt" in aOptions)
+    this._viewElt = aOptions.viewElt;
   this.options = aOptions;
   this._controller = new PlacesController(this);
+  this.place = aPlace;
   this._viewElt.controllers.appendController(this._controller);
 }
 
 PlacesViewBase.prototype = {
   // The xul element that holds the entire view.
   _viewElt: null,
   get viewElt() {
     return this._viewElt;
@@ -228,16 +232,19 @@ PlacesViewBase.prototype = {
     return this.controller.buildContextMenu(aPopup);
   },
 
   destroyContextMenu: function PVB_destroyContextMenu(aPopup) {
     this._contextMenuShown = null;
   },
 
   _cleanPopup: function PVB_cleanPopup(aPopup, aDelay) {
+    // Ensure markers are here when `invalidateContainer` is called before the
+    // popup is shown, which may the case for panelviews, for example.
+    this._ensureMarkers(aPopup);
     // Remove Places nodes from the popup.
     let child = aPopup._startMarker;
     while (child.nextSibling != aPopup._endMarker) {
       let sibling = child.nextSibling;
       if (sibling._placesNode && !aDelay) {
         aPopup.removeChild(sibling);
       } else if (sibling._placesNode && aDelay) {
         // HACK (bug 733419): the popups originating from the OS X native
@@ -312,18 +319,18 @@ PlacesViewBase.prototype = {
     } else {
       aPopup.removeAttribute("emptyplacesresult");
       try {
         aPopup.removeChild(aPopup._emptyMenuitem);
       } catch (ex) {}
     }
   },
 
-  _createMenuItemForPlacesNode:
-  function PVB__createMenuItemForPlacesNode(aPlacesNode) {
+  _createDOMNodeForPlacesNode:
+  function PVB__createDOMNodeForPlacesNode(aPlacesNode) {
     this._domNodes.delete(aPlacesNode);
 
     let element;
     let type = aPlacesNode.type;
     if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
       element = document.createElement("menuseparator");
       element.setAttribute("class", "small-separator");
     } else {
@@ -387,17 +394,17 @@ PlacesViewBase.prototype = {
     if (!this._domNodes.has(aPlacesNode))
       this._domNodes.set(aPlacesNode, element);
 
     return element;
   },
 
   _insertNewItemToPopup:
   function PVB__insertNewItemToPopup(aNewChild, aPopup, aBefore) {
-    let element = this._createMenuItemForPlacesNode(aNewChild);
+    let element = this._createDOMNodeForPlacesNode(aNewChild);
     let before = aBefore || aPopup._endMarker;
 
     if (element.localName == "menuitem" || element.localName == "menu") {
       if (typeof this.options.extraClasses.entry == "string")
         element.classList.add(this.options.extraClasses.entry);
     }
 
     aPopup.insertBefore(element, before);
@@ -711,22 +718,33 @@ PlacesViewBase.prototype = {
           if (child.accessCount)
             this._getDOMNodeForPlacesNode(child).setAttribute("visited", true);
           else
             this._getDOMNodeForPlacesNode(child).removeAttribute("visited");
         }
       }, Components.utils.reportError);
   },
 
+  /**
+   * Checks whether the popup associated with the provided element is open.
+   * This method may be overridden by classes that extend this base class.
+   *
+   * @param  {nsIDOMElement} elt
+   * @return {Boolean}
+   */
+  _isPopupOpen(elt) {
+    return !!elt.parentNode.open;
+  },
+
   invalidateContainer: function PVB_invalidateContainer(aPlacesNode) {
     let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
     elt._built = false;
 
     // If the menupopup is open we should live-update it.
-    if (elt.parentNode.open)
+    if (this._isPopupOpen(elt))
       this._rebuildPopup(elt);
   },
 
   uninit: function PVB_uninit() {
     if (this._result) {
       this._result.removeObserver(this);
       this._resultNode.containerOpen = false;
       this._resultNode = null;
@@ -850,17 +868,17 @@ PlacesViewBase.prototype = {
 
     // _startMarker is an hidden menuseparator that lives before places nodes.
     aPopup._startMarker = document.createElement("menuseparator");
     aPopup._startMarker.hidden = true;
     aPopup.insertBefore(aPopup._startMarker, aPopup.firstChild);
 
     // _endMarker is a DOM node that lives after places nodes, specified with
     // the 'insertionPoint' option or will be a hidden menuseparator.
-    let node = ("insertionPoint" in this.options) ?
+    let node = this.options.insertionPoint ?
                aPopup.querySelector(this.options.insertionPoint) : null;
     if (node) {
       aPopup._endMarker = node;
     } else {
       aPopup._endMarker = document.createElement("menuseparator");
       aPopup._endMarker.hidden = true;
     }
     aPopup.appendChild(aPopup._endMarker);
@@ -907,24 +925,24 @@ PlacesViewBase.prototype = {
       if (!popup._built)
         this._rebuildPopup(popup);
 
       this._mayAddCommandsItems(popup);
     }
   },
 
   _addEventListeners:
-  function PVB__addEventListeners(aObject, aEventNames, aCapturing) {
+  function PVB__addEventListeners(aObject, aEventNames, aCapturing = false) {
     for (let i = 0; i < aEventNames.length; i++) {
       aObject.addEventListener(aEventNames[i], this, aCapturing);
     }
   },
 
   _removeEventListeners:
-  function PVB__removeEventListeners(aObject, aEventNames, aCapturing) {
+  function PVB__removeEventListeners(aObject, aEventNames, aCapturing = false) {
     for (let i = 0; i < aEventNames.length; i++) {
       aObject.removeEventListener(aEventNames[i], this, aCapturing);
     }
   },
 };
 
 function PlacesToolbar(aPlace) {
   let startTime = Date.now();
@@ -1953,8 +1971,201 @@ PlacesPanelMenuView.prototype = {
       this._rootElt.firstChild.remove();
     }
 
     for (let i = 0; i < this._resultNode.childCount; ++i) {
       this._insertNewItem(this._resultNode.getChild(i), null);
     }
   }
 };
+
+class PlacesPanelview extends PlacesViewBase {
+  constructor(container, panelview, place, options = {}) {
+    options.rootElt = container;
+    options.viewElt = panelview;
+    super(place, options);
+    this._viewElt._placesView = this;
+    // We're simulating a popup show, because a panelview may only be shown when
+    // its containing popup is already shown.
+    this._onPopupShowing({ originalTarget: this._viewElt });
+    this._addEventListeners(window, ["unload"]);
+    this._rootElt.setAttribute("context", "placesContext");
+  }
+
+  get events() {
+    if (this._events)
+      return this._events;
+    return this._events = ["command", "destructed", "dragend", "dragstart",
+      "ViewHiding", "ViewShowing", "ViewShown"];
+  }
+
+  get panel() {
+    return this.panelMultiView.parentNode;
+  }
+
+  get panelMultiView() {
+    return this._viewElt.panelMultiView;
+  }
+
+  handleEvent(event) {
+    switch (event.type) {
+      case "command":
+        this._onCommand(event);
+        break;
+      case "destructed":
+        this._onDestructed(event);
+        break;
+      case "dragend":
+        this._onDragEnd(event);
+        break;
+      case "dragstart":
+        this._onDragStart(event);
+        break;
+      case "unload":
+        this.uninit(event);
+        break;
+      case "ViewHiding":
+        this._onPopupHidden(event);
+        break;
+      case "ViewShowing":
+        this._onPopupShowing(event);
+        break;
+      case "ViewShown":
+        this._onViewShown(event);
+        break;
+    }
+  }
+
+  _onCommand(event) {
+    let button = event.originalTarget;
+    if (!button._placesNode)
+      return;
+
+    PlacesUIUtils.openNodeWithEvent(button._placesNode, event);
+  }
+
+  _onDestructed(event) {
+    // The panelmultiview is ephemeral, so let's keep an eye out when the root
+    // element is showing again.
+    this._removeEventListeners(event.target, this.events);
+    this._addEventListeners(this._viewElt, ["ViewShowing"]);
+  }
+
+  _onDragEnd() {
+    this._draggedElt = null;
+  }
+
+  _onDragStart(event) {
+    let draggedElt = event.originalTarget;
+    if (draggedElt.parentNode != this._rootElt || !draggedElt._placesNode)
+      return;
+
+    // Activate the view and cache the dragged element.
+    this._draggedElt = draggedElt._placesNode;
+    this._rootElt.focus();
+
+    this._controller.setDataTransfer(event);
+    event.stopPropagation();
+  }
+
+  uninit(event) {
+    this._removeEventListeners(this.panelMultiView, this.events);
+    this._removeEventListeners(this._viewElt, ["ViewShowing"]);
+    this._removeEventListeners(window, ["unload"]);
+    super.uninit(event);
+  }
+
+  _createDOMNodeForPlacesNode(placesNode) {
+    this._domNodes.delete(placesNode);
+
+    let element;
+    let type = placesNode.type;
+    if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
+      element = document.createElement("toolbarseparator");
+    } else {
+      if (type != Ci.nsINavHistoryResultNode.RESULT_TYPE_URI)
+        throw "Unexpected node";
+
+      element = document.createElement("toolbarbutton");
+      element.classList.add("subviewbutton", "subviewbutton-iconic", "subview-bookmark-item");
+      element.setAttribute("scheme", PlacesUIUtils.guessUrlSchemeForUI(placesNode.uri));
+      element.setAttribute("label", PlacesUIUtils.getBestTitle(placesNode));
+
+      let icon = placesNode.icon;
+      if (icon)
+        element.setAttribute("image", icon);
+    }
+
+    element._placesNode = placesNode;
+    if (!this._domNodes.has(placesNode))
+      this._domNodes.set(placesNode, element);
+
+    return element;
+  }
+
+  _setEmptyPopupStatus(panelview, empty = false) {
+    if (!panelview._emptyMenuitem) {
+      let label = PlacesUIUtils.getString("bookmarksMenuEmptyFolder");
+      panelview._emptyMenuitem = document.createElement("toolbarbutton");
+      panelview._emptyMenuitem.setAttribute("label", label);
+      panelview._emptyMenuitem.setAttribute("disabled", true);
+      panelview._emptyMenuitem.className = "subviewbutton";
+      if (typeof this.options.extraClasses.entry == "string")
+        panelview._emptyMenuitem.classList.add(this.options.extraClasses.entry);
+    }
+
+    if (empty) {
+      panelview.setAttribute("emptyplacesresult", "true");
+      // Don't add the menuitem if there is static content.
+      if (!panelview._startMarker.previousSibling &&
+          !panelview._endMarker.nextSibling)
+        panelview.insertBefore(panelview._emptyMenuitem, panelview._endMarker);
+    } else {
+      panelview.removeAttribute("emptyplacesresult");
+      try {
+        panelview.removeChild(panelview._emptyMenuitem);
+      } catch (ex) {}
+    }
+  }
+
+  _isPopupOpen() {
+    return this.panel.state == "open" && this.panelMultiView.current == this._viewElt;
+  }
+
+  _onPopupHidden(event) {
+    let panelview = event.originalTarget;
+    let placesNode = panelview._placesNode;
+    // Avoid handling ViewHiding of inner views
+    if (placesNode && PlacesUIUtils.getViewForNode(panelview) == this) {
+      // UI performance: folder queries are cheap, keep the resultnode open
+      // so we don't rebuild its contents whenever the popup is reopened.
+      // Though, we want to always close feed containers so their expiration
+      // status will be checked at next opening.
+      if (!PlacesUtils.nodeIsFolder(placesNode) ||
+          this.controller.hasCachedLivemarkInfo(placesNode)) {
+        placesNode.containerOpen = false;
+      }
+    }
+  }
+
+  _onPopupShowing(event) {
+    // If the event came from the root element, this is a sign that the panelmultiview
+    // was just instantiated (see `_onDestructed` above) or this is the first time
+    // we ever get here.
+    if (event.originalTarget == this._viewElt) {
+      this._removeEventListeners(this._viewElt, ["ViewShowing"]);
+      // Start listening for events from all panels inside the panelmultiview.
+      this._addEventListeners(this.panelMultiView, this.events);
+    }
+    super._onPopupShowing(event);
+  }
+
+  _onViewShown(event) {
+    if (event.originalTarget != this._viewElt)
+      return;
+
+    // Because PanelMultiView reparents the panelview internally, the controller
+    // may get lost. In that case we'll append it again, because we certainly
+    // need it later!
+    if (!this.controllers.getControllerCount() && this._controller)
+      this.controllers.appendController(this._controller);
+  }
+}