Bug 966727: Toolbar created by sdk/ui should not be customizable. r=gozala, a=lsblakk
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);