Bug 966727: Toolbar created by sdk/ui should not be customizable. r=gozala, a=lsblakk
authorZER0 <zer0.kaos@gmail.com>
Fri, 14 Mar 2014 15:30:49 -0700
changeset 217423 4c3b196d2589eafd6888304b586da0d7b741c395
parent 217422 052b1189180097d20f505fe70bcc54d2f8c99577
child 217424 dfd3e578df39ac16d8138286b649ab04e426f021
push id3
push usergszorc@mozilla.com
push dateWed, 29 Oct 2014 02:45:36 +0000
reviewersgozala, lsblakk
bugs966727
milestone29.0a2
Bug 966727: Toolbar created by sdk/ui should not be customizable. r=gozala, a=lsblakk
addon-sdk/source/lib/sdk/input/customizable-ui.js
addon-sdk/source/lib/sdk/input/system.js
addon-sdk/source/lib/sdk/ui/toolbar/view.js
addon-sdk/source/test/test-ui-toolbar.js
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/input/customizable-ui.js
@@ -0,0 +1,40 @@
+/* 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/. */
+"use strict";
+
+const { Cu } = require("chrome");
+
+// Because Firefox Holly, we still need to check if `CustomizableUI` is
+// available. Once Australis will officially land, we can safely remove it.
+// See Bug 959142
+try {
+  Cu.import("resource:///modules/CustomizableUI.jsm", {});
+}
+catch (e) {
+  throw Error("Unsupported Application: The module"  + module.id +
+              " does not support this application.");
+}
+
+const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
+const { receive } = require("../event/utils");
+const { InputPort } = require("./system");
+const { object} = require("../util/sequence");
+const { getOuterId } = require("../window/utils");
+
+const Input = function() {};
+Input.prototype = Object.create(InputPort.prototype);
+
+Input.prototype.onCustomizeStart = function (window) {
+  receive(this, object([getOuterId(window), true]));
+}
+
+Input.prototype.onCustomizeEnd = function (window) {
+  receive(this, object([getOuterId(window), null]));
+}
+
+Input.prototype.addListener = input => CustomizableUI.addListener(input);
+
+Input.prototype.removeListener = input => CustomizableUI.removeListener(input);
+
+exports.CustomizationInput = Input;
--- a/addon-sdk/source/lib/sdk/input/system.js
+++ b/addon-sdk/source/lib/sdk/input/system.js
@@ -44,33 +44,39 @@ const InputPort = function InputPort({id
 
 // InputPort type implements `Input` signal interface.
 InputPort.prototype = new Input();
 InputPort.prototype.constructor = InputPort;
 
 // When port is started (which is when it's subgraph get's
 // first subscriber) actual observer is registered.
 InputPort.start = input => {
-  addObserver(input, input.topic, false);
+  input.addListener(input);
   // Also register add-on unload observer to end this signal
   // when that happens.
   addObserver(input, addonUnloadTopic, false);
 };
 InputPort.prototype[start] = InputPort.start;
 
+InputPort.addListener = input => addObserver(input, input.topic, false);
+InputPort.prototype.addListener = InputPort.addListener;
+
 // When port is stopped (which is when it's subgraph has no
 // no subcribers left) an actual observer unregistered.
 // Note that port stopped once it ends as well (which is when
 // add-on is unloaded).
 InputPort.stop = input => {
-  removeObserver(input, input.topic);
+  input.removeListener(input);
   removeObserver(input, addonUnloadTopic);
 };
 InputPort.prototype[stop] = InputPort.stop;
 
+InputPort.removeListener = input => removeObserver(input, input.topic);
+InputPort.prototype.removeListener = InputPort.removeListener;
+
 // `InputPort` also implements `nsIObserver` interface and
 // `nsISupportsWeakReference` interfaces as it's going to be used as such.
 InputPort.prototype.QueryInterface = function(iid) {
   if (!iid.equals(Ci.nsIObserver) && !iid.equals(Ci.nsISupportsWeakReference))
     throw Cr.NS_ERROR_NO_INTERFACE;
 
   return this;
 };
--- a/addon-sdk/source/lib/sdk/ui/toolbar/view.js
+++ b/addon-sdk/source/lib/sdk/ui/toolbar/view.js
@@ -11,21 +11,23 @@ module.metadata = {
 };
 
 const { Cu } = require("chrome");
 const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
 const { subscribe, send, Reactor, foldp, lift, merges } = require("../../event/utils");
 const { InputPort } = require("../../input/system");
 const { OutputPort } = require("../../output/system");
 const { Interactive } = require("../../input/browser");
+const { CustomizationInput } = require("../../input/customizable-ui");
 const { pairs, map, isEmpty, object,
         each, keys, values } = require("../../util/sequence");
 const { curry, flip } = require("../../lang/functional");
 const { patch, diff } = require("diffpatcher/index");
 const prefs = require("../../preferences/service");
+const { getByOuterId } = require("../../window/utils");
 
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 const PREF_ROOT = "extensions.sdk-toolbar-collapsed.";
 
 
 // There are two output ports one for publishing changes that occured
 // and the other for change requests. Later is synchronous and is only
 // consumed here. Note: it needs to be synchronous to avoid race conditions
@@ -33,18 +35,19 @@ const PREF_ROOT = "extensions.sdk-toolba
 // toolbar is destroyed between the ticks.
 const output = new OutputPort({ id: "toolbar-changed" });
 const syncoutput = new OutputPort({ id: "toolbar-change", sync: true });
 
 // Merge disptached changes and recevied changes from models to keep state up to
 // date.
 const Toolbars = foldp(patch, {}, merges([new InputPort({ id: "toolbar-changed" }),
                                           new InputPort({ id: "toolbar-change" })]));
-const State = lift((toolbars, windows) => ({windows: windows, toolbars: toolbars}),
-                   Toolbars, Interactive);
+const State = lift((toolbars, windows, customizable) =>
+  ({windows: windows, toolbars: toolbars, customizable: customizable}),
+  Toolbars, Interactive, new CustomizationInput());
 
 // Shared event handler that makes `event.target.parent` collapsed.
 // Used as toolbar's close buttons click handler.
 const collapseToolbar = event => {
   const toolbar = event.target.parentNode;
   toolbar.collapsed = true;
 };
 
@@ -83,77 +86,111 @@ const attributesChanged = mutations => {
 // it back. In addition it set's up a listener and observer to communicate
 // state changes.
 const addView = curry((options, {document}) => {
   let view = document.createElementNS(XUL_NS, "toolbar");
   view.setAttribute("id", options.id);
   view.setAttribute("collapsed", options.collapsed);
   view.setAttribute("toolbarname", options.title);
   view.setAttribute("pack", "end");
-  view.setAttribute("defaultset", options.items.join(","));
-  view.setAttribute("customizable", true);
-  view.setAttribute("style", "max-height: 40px;");
+  view.setAttribute("customizable", "false");
+  view.setAttribute("style", "padding: 2px 0; max-height: 40px;");
   view.setAttribute("mode", "icons");
   view.setAttribute("iconsize", "small");
   view.setAttribute("context", "toolbar-context-menu");
+  view.setAttribute("class", "toolbar-primary chromeclass-toolbar");
+
+  let label = document.createElementNS(XUL_NS, "label");
+  label.setAttribute("value", options.title);
+  label.setAttribute("collapsed", "true");
+  view.appendChild(label);
 
   let closeButton = document.createElementNS(XUL_NS, "toolbarbutton");
   closeButton.setAttribute("id", "close-" + options.id);
   closeButton.setAttribute("class", "close-icon");
   closeButton.setAttribute("customizable", false);
   closeButton.addEventListener("command", collapseToolbar);
 
   view.appendChild(closeButton);
 
+  // In order to have a close button not costumizable, aligned on the right,
+  // leaving the customizable capabilities of Australis, we need to create
+  // a toolbar inside a toolbar.
+  // This is should be a temporary hack, we should have a proper XBL for toolbar
+  // instead. See:
+  // https://bugzilla.mozilla.org/show_bug.cgi?id=982005
+  let toolbar = document.createElementNS(XUL_NS, "toolbar");
+  toolbar.setAttribute("id", "inner-" + options.id);
+  toolbar.setAttribute("defaultset", options.items.join(","));
+  toolbar.setAttribute("customizable", "true");
+  toolbar.setAttribute("style", "-moz-appearance: none; overflow: hidden");
+  toolbar.setAttribute("mode", "icons");
+  toolbar.setAttribute("iconsize", "small");
+  toolbar.setAttribute("context", "toolbar-context-menu");
+  toolbar.setAttribute("flex", "1");
+
+  view.insertBefore(toolbar, closeButton);
+
   const observer = new document.defaultView.MutationObserver(attributesChanged);
   observer.observe(view, { attributes: true,
                            attributeFilter: ["collapsed", "toolbarname"] });
 
   const toolbox = document.getElementById("navigator-toolbox");
   toolbox.appendChild(view);
 });
 const viewAdd = curry(flip(addView));
 
 const removeView = curry((id, {document}) => {
   const view = document.getElementById(id);
   if (view) view.remove();
 });
 
-const updateView = curry((id, {title, collapsed}, {document}) => {
+const updateView = curry((id, {title, collapsed, isCustomizing}, {document}) => {
   const view = document.getElementById(id);
-  if (view && title)
+
+  if (!view)
+    return;
+
+  if (title)
     view.setAttribute("toolbarname", title);
-  if (view && collapsed !== void(0))
+
+  if (collapsed !== void(0))
     view.setAttribute("collapsed", Boolean(collapsed));
+
+  if (isCustomizing !== void(0)) {
+    view.querySelector("label").collapsed = !isCustomizing;
+    view.querySelector("toolbar").style.visibility = isCustomizing
+                                                ? "hidden" : "visible";
+  }
 });
 
-
+const viewUpdate = curry(flip(updateView));
 
 // Utility function used to register toolbar into CustomizableUI.
 const registerToolbar = state => {
   // If it's first additon register toolbar as customizableUI component.
-  CustomizableUI.registerArea(state.id, {
+  CustomizableUI.registerArea("inner-" + state.id, {
     type: CustomizableUI.TYPE_TOOLBAR,
     legacy: true,
-    defaultPlacements: [...state.items, "close-" + state.id]
+    defaultPlacements: [...state.items]
   });
 };
 // Utility function used to unregister toolbar from the CustomizableUI.
 const unregisterToolbar = CustomizableUI.unregisterArea;
 
 const reactor = new Reactor({
   onStep: (present, past) => {
     const delta = diff(past, present);
 
     each(([id, update]) => {
       // If update is `null` toolbar is removed, in such case
       // we unregister toolbar and remove it from each window
       // it was added to.
       if (update === null) {
-        unregisterToolbar(id);
+        unregisterToolbar("inner-" + id);
         each(removeView(id), values(past.windows));
 
         send(output, object([id, null]));
       }
       else if (past.toolbars[id]) {
         // If `collapsed` state for toolbar was updated, persist
         // it for a future sessions.
         if (update.collapsed !== void(0))
@@ -185,17 +222,23 @@ const reactor = new Reactor({
       }
     }, pairs(delta.toolbars));
 
     // Add views to every window that was added.
     each(window => {
       if (window)
         each(viewAdd(window), values(past.toolbars));
     }, values(delta.windows));
+
+    each(([id, isCustomizing]) => {
+      each(viewUpdate(getByOuterId(id), {isCustomizing: !!isCustomizing}),
+        keys(present.toolbars));
+
+    }, pairs(delta.customizable))
   },
   onEnd: state => {
     each(id => {
-      unregisterToolbar(id);
+      unregisterToolbar("inner-" + id);
       each(removeView(id), values(state.windows));
     }, keys(state.toolbars));
   }
 });
 reactor.run(State);
--- a/addon-sdk/source/test/test-ui-toolbar.js
+++ b/addon-sdk/source/test/test-ui-toolbar.js
@@ -7,21 +7,22 @@ module.metadata = {
   "engines": {
     "Firefox": "*"
   }
 };
 
 const { Toolbar } = require("sdk/ui/toolbar");
 const { Loader } = require("sdk/test/loader");
 const { identify } = require("sdk/ui/id");
-const { getMostRecentBrowserWindow, open } = require("sdk/window/utils");
+const { getMostRecentBrowserWindow, open, getOuterId } = require("sdk/window/utils");
 const { ready, close } = require("sdk/window/helpers");
 const { defer } = require("sdk/core/promise");
-const { send } = require("sdk/event/utils");
+const { send, stop, Reactor } = require("sdk/event/utils");
 const { object } = require("sdk/util/sequence");
+const { CustomizationInput } = require("sdk/input/customizable-ui");
 const { OutputPort } = require("sdk/output/system");
 const output = new OutputPort({ id: "toolbar-change" });
 
 const wait = (toolbar, event) => {
   let { promise, resolve } = defer();
   toolbar.once(event, resolve);
   return promise;
 };
@@ -352,9 +353,80 @@ exports["test title change"] = function*
   assert.equal(readTitle(t1, w2), "second title",
                "title updated in second window");
 
   t1.destroy();
   yield wait(t1, "detach");
   yield close(w2);
 };
 
+exports["test toolbar is not customizable"] = function*(assert, done) {
+  const { window, document, gCustomizeMode } = getMostRecentBrowserWindow();
+  const outerId = getOuterId(window);
+  const input = new CustomizationInput();
+  const customized = defer();
+  const customizedEnd = defer();
+
+  new Reactor({ onStep: value => {
+    if (value[outerId] === true)
+      customized.resolve();
+    if (value[outerId] === null)
+      customizedEnd.resolve();
+  }}).run(input);
+
+  const toolbar = new Toolbar({ title: "foo" });
+
+  yield wait(toolbar, "attach");
+
+  let view = document.getElementById(toolbar.id);
+  let label = view.querySelector("label");
+  let inner = view.querySelector("toolbar");
+
+  assert.equal(view.getAttribute("customizable"), "false",
+    "The outer toolbar is not customizable.");
+
+  assert.ok(label.collapsed,
+    "The label is not displayed.")
+
+  assert.equal(inner.getAttribute("customizable"), "true",
+    "The inner toolbar is customizable.");
+
+  assert.equal(window.getComputedStyle(inner).visibility, "visible",
+    "The inner toolbar is visible.");
+
+  // Enter in customization mode
+  gCustomizeMode.toggle();
+
+  yield customized.promise;
+
+  assert.equal(view.getAttribute("customizable"), "false",
+    "The outer toolbar is not customizable.");
+
+  assert.equal(label.collapsed, false,
+    "The label is displayed.")
+
+  assert.equal(inner.getAttribute("customizable"), "true",
+    "The inner toolbar is customizable.");
+
+  assert.equal(window.getComputedStyle(inner).visibility, "hidden",
+    "The inner toolbar is hidden.");
+
+  // Exit from customization mode
+  gCustomizeMode.toggle();
+
+  yield customizedEnd.promise;
+
+  assert.equal(view.getAttribute("customizable"), "false",
+    "The outer toolbar is not customizable.");
+
+  assert.ok(label.collapsed,
+    "The label is not displayed.")
+
+  assert.equal(inner.getAttribute("customizable"), "true",
+    "The inner toolbar is customizable.");
+
+  assert.equal(window.getComputedStyle(inner).visibility, "visible",
+    "The inner toolbar is visible.");
+  
+  toolbar.destroy();
+};
+
 require("sdk/test").run(exports);