browser/components/customizableui/content/panelUI.js

Wed, 31 Dec 2014 07:53:36 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:53:36 +0100
branch
TOR_BUG_3246
changeset 5
4ab42b5ab56c
permissions
-rw-r--r--

Correct small whitespace inconsistency, lost while renaming variables.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
     6                                   "resource:///modules/CustomizableUI.jsm");
     7 XPCOMUtils.defineLazyModuleGetter(this, "ScrollbarSampler",
     8                                   "resource:///modules/ScrollbarSampler.jsm");
     9 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
    10                                   "resource://gre/modules/Promise.jsm");
    11 XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils",
    12                                   "resource://gre/modules/ShortcutUtils.jsm");
    13 /**
    14  * Maintains the state and dispatches events for the main menu panel.
    15  */
    17 const PanelUI = {
    18   /** Panel events that we listen for. **/
    19   get kEvents() ["popupshowing", "popupshown", "popuphiding", "popuphidden"],
    20   /**
    21    * Used for lazily getting and memoizing elements from the document. Lazy
    22    * getters are set in init, and memoizing happens after the first retrieval.
    23    */
    24   get kElements() {
    25     return {
    26       contents: "PanelUI-contents",
    27       mainView: "PanelUI-mainView",
    28       multiView: "PanelUI-multiView",
    29       helpView: "PanelUI-helpView",
    30       menuButton: "PanelUI-menu-button",
    31       panel: "PanelUI-popup",
    32       scroller: "PanelUI-contents-scroller"
    33     };
    34   },
    36   _initialized: false,
    37   init: function() {
    38     for (let [k, v] of Iterator(this.kElements)) {
    39       // Need to do fresh let-bindings per iteration
    40       let getKey = k;
    41       let id = v;
    42       this.__defineGetter__(getKey, function() {
    43         delete this[getKey];
    44         return this[getKey] = document.getElementById(id);
    45       });
    46     }
    48     this.menuButton.addEventListener("mousedown", this);
    49     this.menuButton.addEventListener("keypress", this);
    50     this._overlayScrollListenerBoundFn = this._overlayScrollListener.bind(this);
    51     window.matchMedia("(-moz-overlay-scrollbars)").addListener(this._overlayScrollListenerBoundFn);
    52     CustomizableUI.addListener(this);
    53     this._initialized = true;
    54   },
    56   _eventListenersAdded: false,
    57   _ensureEventListenersAdded: function() {
    58     if (this._eventListenersAdded)
    59       return;
    60     this._addEventListeners();
    61   },
    63   _addEventListeners: function() {
    64     for (let event of this.kEvents) {
    65       this.panel.addEventListener(event, this);
    66     }
    68     this.helpView.addEventListener("ViewShowing", this._onHelpViewShow, false);
    69     this._eventListenersAdded = true;
    70   },
    72   uninit: function() {
    73     for (let event of this.kEvents) {
    74       this.panel.removeEventListener(event, this);
    75     }
    76     this.helpView.removeEventListener("ViewShowing", this._onHelpViewShow);
    77     this.menuButton.removeEventListener("mousedown", this);
    78     this.menuButton.removeEventListener("keypress", this);
    79     window.matchMedia("(-moz-overlay-scrollbars)").removeListener(this._overlayScrollListenerBoundFn);
    80     CustomizableUI.removeListener(this);
    81     this._overlayScrollListenerBoundFn = null;
    82   },
    84   /**
    85    * Customize mode extracts the mainView and puts it somewhere else while the
    86    * user customizes. Upon completion, this function can be called to put the
    87    * panel back to where it belongs in normal browsing mode.
    88    *
    89    * @param aMainView
    90    *        The mainView node to put back into place.
    91    */
    92   setMainView: function(aMainView) {
    93     this._ensureEventListenersAdded();
    94     this.multiView.setMainView(aMainView);
    95   },
    97   /**
    98    * Opens the menu panel if it's closed, or closes it if it's
    99    * open.
   100    *
   101    * @param aEvent the event that triggers the toggle.
   102    */
   103   toggle: function(aEvent) {
   104     // Don't show the panel if the window is in customization mode,
   105     // since this button doubles as an exit path for the user in this case.
   106     if (document.documentElement.hasAttribute("customizing")) {
   107       return;
   108     }
   109     this._ensureEventListenersAdded();
   110     if (this.panel.state == "open") {
   111       this.hide();
   112     } else if (this.panel.state == "closed") {
   113       this.show(aEvent);
   114     }
   115   },
   117   /**
   118    * Opens the menu panel. If the event target has a child with the
   119    * toolbarbutton-icon attribute, the panel will be anchored on that child.
   120    * Otherwise, the panel is anchored on the event target itself.
   121    *
   122    * @param aEvent the event (if any) that triggers showing the menu.
   123    */
   124   show: function(aEvent) {
   125     let deferred = Promise.defer();
   127     this.ensureReady().then(() => {
   128       if (this.panel.state == "open" ||
   129           document.documentElement.hasAttribute("customizing")) {
   130         deferred.resolve();
   131         return;
   132       }
   134       let editControlPlacement = CustomizableUI.getPlacementOfWidget("edit-controls");
   135       if (editControlPlacement && editControlPlacement.area == CustomizableUI.AREA_PANEL) {
   136         updateEditUIVisibility();
   137       }
   139       let personalBookmarksPlacement = CustomizableUI.getPlacementOfWidget("personal-bookmarks");
   140       if (personalBookmarksPlacement &&
   141           personalBookmarksPlacement.area == CustomizableUI.AREA_PANEL) {
   142         PlacesToolbarHelper.customizeChange();
   143       }
   145       let anchor;
   146       if (!aEvent ||
   147           aEvent.type == "command") {
   148         anchor = this.menuButton;
   149       } else {
   150         anchor = aEvent.target;
   151       }
   153       this.panel.addEventListener("popupshown", function onPopupShown() {
   154         this.removeEventListener("popupshown", onPopupShown);
   155         // As an optimization for the customize mode transition, we preload
   156         // about:customizing in the background once the menu panel is first
   157         // shown.
   158         gCustomizationTabPreloader.ensurePreloading();
   159         deferred.resolve();
   160       });
   162       let iconAnchor =
   163         document.getAnonymousElementByAttribute(anchor, "class",
   164                                                 "toolbarbutton-icon");
   165       this.panel.openPopup(iconAnchor || anchor);
   166     });
   168     return deferred.promise;
   169   },
   171   /**
   172    * If the menu panel is being shown, hide it.
   173    */
   174   hide: function() {
   175     if (document.documentElement.hasAttribute("customizing")) {
   176       return;
   177     }
   179     this.panel.hidePopup();
   180   },
   182   handleEvent: function(aEvent) {
   183     switch (aEvent.type) {
   184       case "popupshowing":
   185         this._adjustLabelsForAutoHyphens();
   186         // Fall through
   187       case "popupshown":
   188         // Fall through
   189       case "popuphiding":
   190         // Fall through
   191       case "popuphidden":
   192         this._updatePanelButton(aEvent.target);
   193         break;
   194       case "mousedown":
   195         if (aEvent.button == 0)
   196           this.toggle(aEvent);
   197         break;
   198       case "keypress":
   199         this.toggle(aEvent);
   200         break;
   201     }
   202   },
   204   isReady: function() {
   205     return !!this._isReady;
   206   },
   208   /**
   209    * Registering the menu panel is done lazily for performance reasons. This
   210    * method is exposed so that CustomizationMode can force panel-readyness in the
   211    * event that customization mode is started before the panel has been opened
   212    * by the user.
   213    *
   214    * @param aCustomizing (optional) set to true if this was called while entering
   215    *        customization mode. If that's the case, we trust that customization
   216    *        mode will handle calling beginBatchUpdate and endBatchUpdate.
   217    *
   218    * @return a Promise that resolves once the panel is ready to roll.
   219    */
   220   ensureReady: function(aCustomizing=false) {
   221     if (this._readyPromise) {
   222       return this._readyPromise;
   223     }
   224     this._readyPromise = Task.spawn(function() {
   225       if (!this._initialized) {
   226         let delayedStartupDeferred = Promise.defer();
   227         let delayedStartupObserver = (aSubject, aTopic, aData) => {
   228           if (aSubject == window) {
   229             Services.obs.removeObserver(delayedStartupObserver, "browser-delayed-startup-finished");
   230             delayedStartupDeferred.resolve();
   231           }
   232         };
   233         Services.obs.addObserver(delayedStartupObserver, "browser-delayed-startup-finished", false);
   234         yield delayedStartupDeferred.promise;
   235       }
   237       this.contents.setAttributeNS("http://www.w3.org/XML/1998/namespace", "lang",
   238                                    getLocale());
   239       if (!this._scrollWidth) {
   240         // In order to properly center the contents of the panel, while ensuring
   241         // that we have enough space on either side to show a scrollbar, we have to
   242         // do a bit of hackery. In particular, we calculate a new width for the
   243         // scroller, based on the system scrollbar width.
   244         this._scrollWidth =
   245           (yield ScrollbarSampler.getSystemScrollbarWidth()) + "px";
   246         let cstyle = window.getComputedStyle(this.scroller);
   247         let widthStr = cstyle.width;
   248         // Get the calculated padding on the left and right sides of
   249         // the scroller too. We'll use that in our final calculation so
   250         // that if a scrollbar appears, we don't have the contents right
   251         // up against the edge of the scroller.
   252         let paddingLeft = cstyle.paddingLeft;
   253         let paddingRight = cstyle.paddingRight;
   254         let calcStr = [widthStr, this._scrollWidth,
   255                        paddingLeft, paddingRight].join(" + ");
   256         this.scroller.style.width = "calc(" + calcStr + ")";
   257       }
   259       if (aCustomizing) {
   260         CustomizableUI.registerMenuPanel(this.contents);
   261       } else {
   262         this.beginBatchUpdate();
   263         try {
   264           CustomizableUI.registerMenuPanel(this.contents);
   265         } finally {
   266           this.endBatchUpdate();
   267         }
   268       }
   269       this._updateQuitTooltip();
   270       this.panel.hidden = false;
   271       this._isReady = true;
   272     }.bind(this)).then(null, Cu.reportError);
   274     return this._readyPromise;
   275   },
   277   /**
   278    * Switch the panel to the main view if it's not already
   279    * in that view.
   280    */
   281   showMainView: function() {
   282     this._ensureEventListenersAdded();
   283     this.multiView.showMainView();
   284   },
   286   /**
   287    * Switch the panel to the help view if it's not already
   288    * in that view.
   289    */
   290   showHelpView: function(aAnchor) {
   291     this._ensureEventListenersAdded();
   292     this.multiView.showSubView("PanelUI-helpView", aAnchor);
   293   },
   295   /**
   296    * Shows a subview in the panel with a given ID.
   297    *
   298    * @param aViewId the ID of the subview to show.
   299    * @param aAnchor the element that spawned the subview.
   300    * @param aPlacementArea the CustomizableUI area that aAnchor is in.
   301    */
   302   showSubView: function(aViewId, aAnchor, aPlacementArea) {
   303     this._ensureEventListenersAdded();
   304     let viewNode = document.getElementById(aViewId);
   305     if (!viewNode) {
   306       Cu.reportError("Could not show panel subview with id: " + aViewId);
   307       return;
   308     }
   310     if (!aAnchor) {
   311       Cu.reportError("Expected an anchor when opening subview with id: " + aViewId);
   312       return;
   313     }
   315     if (aPlacementArea == CustomizableUI.AREA_PANEL) {
   316       this.multiView.showSubView(aViewId, aAnchor);
   317     } else if (!aAnchor.open) {
   318       aAnchor.open = true;
   319       // Emit the ViewShowing event so that the widget definition has a chance
   320       // to lazily populate the subview with things.
   321       let evt = document.createEvent("CustomEvent");
   322       evt.initCustomEvent("ViewShowing", true, true, viewNode);
   323       viewNode.dispatchEvent(evt);
   324       if (evt.defaultPrevented) {
   325         return;
   326       }
   328       let tempPanel = document.createElement("panel");
   329       tempPanel.setAttribute("type", "arrow");
   330       tempPanel.setAttribute("id", "customizationui-widget-panel");
   331       tempPanel.setAttribute("class", "cui-widget-panel");
   332       tempPanel.setAttribute("context", "");
   333       document.getElementById(CustomizableUI.AREA_NAVBAR).appendChild(tempPanel);
   334       // If the view has a footer, set a convenience class on the panel.
   335       tempPanel.classList.toggle("cui-widget-panelWithFooter",
   336                                  viewNode.querySelector(".panel-subview-footer"));
   338       let multiView = document.createElement("panelmultiview");
   339       multiView.setAttribute("nosubviews", "true");
   340       tempPanel.appendChild(multiView);
   341       multiView.setAttribute("mainViewIsSubView", "true");
   342       multiView.setMainView(viewNode);
   343       viewNode.classList.add("cui-widget-panelview");
   344       CustomizableUI.addPanelCloseListeners(tempPanel);
   346       let panelRemover = function() {
   347         tempPanel.removeEventListener("popuphidden", panelRemover);
   348         viewNode.classList.remove("cui-widget-panelview");
   349         CustomizableUI.removePanelCloseListeners(tempPanel);
   350         let evt = new CustomEvent("ViewHiding", {detail: viewNode});
   351         viewNode.dispatchEvent(evt);
   352         aAnchor.open = false;
   354         this.multiView.appendChild(viewNode);
   355         tempPanel.parentElement.removeChild(tempPanel);
   356       }.bind(this);
   357       tempPanel.addEventListener("popuphidden", panelRemover);
   359       let iconAnchor =
   360         document.getAnonymousElementByAttribute(aAnchor, "class",
   361                                                 "toolbarbutton-icon");
   363       tempPanel.openPopup(iconAnchor || aAnchor, "bottomcenter topright");
   364     }
   365   },
   367   /**
   368    * Open a dialog window that allow the user to customize listed character sets.
   369    */
   370   onCharsetCustomizeCommand: function() {
   371     this.hide();
   372     window.openDialog("chrome://global/content/customizeCharset.xul",
   373                       "PrefWindow",
   374                       "chrome,modal=yes,resizable=yes",
   375                       "browser");
   376   },
   378   onWidgetAfterDOMChange: function(aNode, aNextNode, aContainer, aWasRemoval) {
   379     if (aContainer != this.contents) {
   380       return;
   381     }
   382     if (aWasRemoval) {
   383       aNode.removeAttribute("auto-hyphens");
   384     }
   385   },
   387   onWidgetBeforeDOMChange: function(aNode, aNextNode, aContainer, aIsRemoval) {
   388     if (aContainer != this.contents) {
   389       return;
   390     }
   391     if (!aIsRemoval &&
   392         (this.panel.state == "open" ||
   393          document.documentElement.hasAttribute("customizing"))) {
   394       this._adjustLabelsForAutoHyphens(aNode);
   395     }
   396   },
   398   /** 
   399    * Signal that we're about to make a lot of changes to the contents of the
   400    * panels all at once. For performance, we ignore the mutations.
   401    */
   402   beginBatchUpdate: function() {
   403     this._ensureEventListenersAdded();
   404     this.multiView.ignoreMutations = true;
   405   },
   407   /**
   408    * Signal that we're done making bulk changes to the panel. We now pay
   409    * attention to mutations. This automatically synchronizes the multiview
   410    * container with whichever view is displayed if the panel is open.
   411    */
   412   endBatchUpdate: function(aReason) {
   413     this._ensureEventListenersAdded();
   414     this.multiView.ignoreMutations = false;
   415   },
   417   _adjustLabelsForAutoHyphens: function(aNode) {
   418     let toolbarButtons = aNode ? [aNode] :
   419                                  this.contents.querySelectorAll(".toolbarbutton-1");
   420     for (let node of toolbarButtons) {
   421       let label = node.getAttribute("label");
   422       if (!label) {
   423         continue;
   424       }
   425       if (label.contains("\u00ad")) {
   426         node.setAttribute("auto-hyphens", "off");
   427       } else {
   428         node.removeAttribute("auto-hyphens");
   429       }
   430     }
   431   },
   433   /**
   434    * Sets the anchor node into the open or closed state, depending
   435    * on the state of the panel.
   436    */
   437   _updatePanelButton: function() {
   438     this.menuButton.open = this.panel.state == "open" ||
   439                            this.panel.state == "showing";
   440   },
   442   _onHelpViewShow: function(aEvent) {
   443     // Call global menu setup function
   444     buildHelpMenu();
   446     let helpMenu = document.getElementById("menu_HelpPopup");
   447     let items = this.getElementsByTagName("vbox")[0];
   448     let attrs = ["oncommand", "onclick", "label", "key", "disabled"];
   449     let NSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
   451     // Remove all buttons from the view
   452     while (items.firstChild) {
   453       items.removeChild(items.firstChild);
   454     }
   456     // Add the current set of menuitems of the Help menu to this view
   457     let menuItems = Array.prototype.slice.call(helpMenu.getElementsByTagName("menuitem"));
   458     let fragment = document.createDocumentFragment();
   459     for (let node of menuItems) {
   460       if (node.hidden)
   461         continue;
   462       let button = document.createElementNS(NSXUL, "toolbarbutton");
   463       // Copy specific attributes from a menuitem of the Help menu
   464       for (let attrName of attrs) {
   465         if (!node.hasAttribute(attrName))
   466           continue;
   467         button.setAttribute(attrName, node.getAttribute(attrName));
   468       }
   469       button.setAttribute("class", "subviewbutton");
   470       fragment.appendChild(button);
   471     }
   472     items.appendChild(fragment);
   473   },
   475   _updateQuitTooltip: function() {
   476 #ifndef XP_WIN
   477 #ifdef XP_MACOSX
   478     let tooltipId = "quit-button.tooltiptext.mac";
   479 #else
   480     let tooltipId = "quit-button.tooltiptext.linux2";
   481 #endif
   482     let brands = Services.strings.createBundle("chrome://branding/locale/brand.properties");
   483     let stringArgs = [brands.GetStringFromName("brandShortName")];
   485     let key = document.getElementById("key_quitApplication");
   486     stringArgs.push(ShortcutUtils.prettifyShortcut(key));
   487     let tooltipString = CustomizableUI.getLocalizedProperty({x: tooltipId}, "x", stringArgs);
   488     let quitButton = document.getElementById("PanelUI-quit");
   489     quitButton.setAttribute("tooltiptext", tooltipString);
   490 #endif
   491   },
   493   _overlayScrollListenerBoundFn: null,
   494   _overlayScrollListener: function(aMQL) {
   495     ScrollbarSampler.resetSystemScrollbarWidth();
   496     this._scrollWidth = null;
   497   },
   498 };
   500 /**
   501  * Gets the currently selected locale for display.
   502  * @return  the selected locale or "en-US" if none is selected
   503  */
   504 function getLocale() {
   505   const PREF_SELECTED_LOCALE = "general.useragent.locale";
   506   try {
   507     let locale = Services.prefs.getComplexValue(PREF_SELECTED_LOCALE,
   508                                                 Ci.nsIPrefLocalizedString);
   509     if (locale)
   510       return locale;
   511   }
   512   catch (e) { }
   514   try {
   515     return Services.prefs.getCharPref(PREF_SELECTED_LOCALE);
   516   }
   517   catch (e) { }
   519   return "en-US";
   520 }

mercurial