addon-sdk/source/lib/sdk/ui/toolbar/view.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/addon-sdk/source/lib/sdk/ui/toolbar/view.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,244 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 +* License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 +* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +"use strict";
     1.8 +
     1.9 +module.metadata = {
    1.10 +  "stability": "experimental",
    1.11 +  "engines": {
    1.12 +    "Firefox": "> 28"
    1.13 +  }
    1.14 +};
    1.15 +
    1.16 +const { Cu } = require("chrome");
    1.17 +const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
    1.18 +const { subscribe, send, Reactor, foldp, lift, merges } = require("../../event/utils");
    1.19 +const { InputPort } = require("../../input/system");
    1.20 +const { OutputPort } = require("../../output/system");
    1.21 +const { Interactive } = require("../../input/browser");
    1.22 +const { CustomizationInput } = require("../../input/customizable-ui");
    1.23 +const { pairs, map, isEmpty, object,
    1.24 +        each, keys, values } = require("../../util/sequence");
    1.25 +const { curry, flip } = require("../../lang/functional");
    1.26 +const { patch, diff } = require("diffpatcher/index");
    1.27 +const prefs = require("../../preferences/service");
    1.28 +const { getByOuterId } = require("../../window/utils");
    1.29 +
    1.30 +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
    1.31 +const PREF_ROOT = "extensions.sdk-toolbar-collapsed.";
    1.32 +
    1.33 +
    1.34 +// There are two output ports one for publishing changes that occured
    1.35 +// and the other for change requests. Later is synchronous and is only
    1.36 +// consumed here. Note: it needs to be synchronous to avoid race conditions
    1.37 +// when `collapsed` attribute changes are caused by user interaction and
    1.38 +// toolbar is destroyed between the ticks.
    1.39 +const output = new OutputPort({ id: "toolbar-changed" });
    1.40 +const syncoutput = new OutputPort({ id: "toolbar-change", sync: true });
    1.41 +
    1.42 +// Merge disptached changes and recevied changes from models to keep state up to
    1.43 +// date.
    1.44 +const Toolbars = foldp(patch, {}, merges([new InputPort({ id: "toolbar-changed" }),
    1.45 +                                          new InputPort({ id: "toolbar-change" })]));
    1.46 +const State = lift((toolbars, windows, customizable) =>
    1.47 +  ({windows: windows, toolbars: toolbars, customizable: customizable}),
    1.48 +  Toolbars, Interactive, new CustomizationInput());
    1.49 +
    1.50 +// Shared event handler that makes `event.target.parent` collapsed.
    1.51 +// Used as toolbar's close buttons click handler.
    1.52 +const collapseToolbar = event => {
    1.53 +  const toolbar = event.target.parentNode;
    1.54 +  toolbar.collapsed = true;
    1.55 +};
    1.56 +
    1.57 +const parseAttribute = x =>
    1.58 +  x === "true" ? true :
    1.59 +  x === "false" ? false :
    1.60 +  x === "" ? null :
    1.61 +  x;
    1.62 +
    1.63 +// Shared mutation observer that is used to observe `toolbar` node's
    1.64 +// attribute mutations. Mutations are aggregated in the `delta` hash
    1.65 +// and send to `ToolbarStateChanged` channel to let model know state
    1.66 +// has changed.
    1.67 +const attributesChanged = mutations => {
    1.68 +  const delta = mutations.reduce((changes, {attributeName, target}) => {
    1.69 +    const id = target.id;
    1.70 +    const field = attributeName === "toolbarname" ? "title" : attributeName;
    1.71 +    let change = changes[id] || (changes[id] = {});
    1.72 +    change[field] = parseAttribute(target.getAttribute(attributeName));
    1.73 +    return changes;
    1.74 +  }, {});
    1.75 +
    1.76 +  // Calculate what are the updates from the current state and if there are
    1.77 +  // any send them.
    1.78 +  const updates = diff(reactor.value, patch(reactor.value, delta));
    1.79 +
    1.80 +  if (!isEmpty(pairs(updates))) {
    1.81 +    // TODO: Consider sending sync to make sure that there won't be a new
    1.82 +    // update doing a delete in the meantime.
    1.83 +    send(syncoutput, updates);
    1.84 +  }
    1.85 +};
    1.86 +
    1.87 +
    1.88 +// Utility function creates `toolbar` with a "close" button and returns
    1.89 +// it back. In addition it set's up a listener and observer to communicate
    1.90 +// state changes.
    1.91 +const addView = curry((options, {document}) => {
    1.92 +  let view = document.createElementNS(XUL_NS, "toolbar");
    1.93 +  view.setAttribute("id", options.id);
    1.94 +  view.setAttribute("collapsed", options.collapsed);
    1.95 +  view.setAttribute("toolbarname", options.title);
    1.96 +  view.setAttribute("pack", "end");
    1.97 +  view.setAttribute("customizable", "false");
    1.98 +  view.setAttribute("style", "padding: 2px 0; max-height: 40px;");
    1.99 +  view.setAttribute("mode", "icons");
   1.100 +  view.setAttribute("iconsize", "small");
   1.101 +  view.setAttribute("context", "toolbar-context-menu");
   1.102 +  view.setAttribute("class", "toolbar-primary chromeclass-toolbar");
   1.103 +
   1.104 +  let label = document.createElementNS(XUL_NS, "label");
   1.105 +  label.setAttribute("value", options.title);
   1.106 +  label.setAttribute("collapsed", "true");
   1.107 +  view.appendChild(label);
   1.108 +
   1.109 +  let closeButton = document.createElementNS(XUL_NS, "toolbarbutton");
   1.110 +  closeButton.setAttribute("id", "close-" + options.id);
   1.111 +  closeButton.setAttribute("class", "close-icon");
   1.112 +  closeButton.setAttribute("customizable", false);
   1.113 +  closeButton.addEventListener("command", collapseToolbar);
   1.114 +
   1.115 +  view.appendChild(closeButton);
   1.116 +
   1.117 +  // In order to have a close button not costumizable, aligned on the right,
   1.118 +  // leaving the customizable capabilities of Australis, we need to create
   1.119 +  // a toolbar inside a toolbar.
   1.120 +  // This is should be a temporary hack, we should have a proper XBL for toolbar
   1.121 +  // instead. See:
   1.122 +  // https://bugzilla.mozilla.org/show_bug.cgi?id=982005
   1.123 +  let toolbar = document.createElementNS(XUL_NS, "toolbar");
   1.124 +  toolbar.setAttribute("id", "inner-" + options.id);
   1.125 +  toolbar.setAttribute("defaultset", options.items.join(","));
   1.126 +  toolbar.setAttribute("customizable", "true");
   1.127 +  toolbar.setAttribute("style", "-moz-appearance: none; overflow: hidden");
   1.128 +  toolbar.setAttribute("mode", "icons");
   1.129 +  toolbar.setAttribute("iconsize", "small");
   1.130 +  toolbar.setAttribute("context", "toolbar-context-menu");
   1.131 +  toolbar.setAttribute("flex", "1");
   1.132 +
   1.133 +  view.insertBefore(toolbar, closeButton);
   1.134 +
   1.135 +  const observer = new document.defaultView.MutationObserver(attributesChanged);
   1.136 +  observer.observe(view, { attributes: true,
   1.137 +                           attributeFilter: ["collapsed", "toolbarname"] });
   1.138 +
   1.139 +  const toolbox = document.getElementById("navigator-toolbox");
   1.140 +  toolbox.appendChild(view);
   1.141 +});
   1.142 +const viewAdd = curry(flip(addView));
   1.143 +
   1.144 +const removeView = curry((id, {document}) => {
   1.145 +  const view = document.getElementById(id);
   1.146 +  if (view) view.remove();
   1.147 +});
   1.148 +
   1.149 +const updateView = curry((id, {title, collapsed, isCustomizing}, {document}) => {
   1.150 +  const view = document.getElementById(id);
   1.151 +
   1.152 +  if (!view)
   1.153 +    return;
   1.154 +
   1.155 +  if (title)
   1.156 +    view.setAttribute("toolbarname", title);
   1.157 +
   1.158 +  if (collapsed !== void(0))
   1.159 +    view.setAttribute("collapsed", Boolean(collapsed));
   1.160 +
   1.161 +  if (isCustomizing !== void(0)) {
   1.162 +    view.querySelector("label").collapsed = !isCustomizing;
   1.163 +    view.querySelector("toolbar").style.visibility = isCustomizing
   1.164 +                                                ? "hidden" : "visible";
   1.165 +  }
   1.166 +});
   1.167 +
   1.168 +const viewUpdate = curry(flip(updateView));
   1.169 +
   1.170 +// Utility function used to register toolbar into CustomizableUI.
   1.171 +const registerToolbar = state => {
   1.172 +  // If it's first additon register toolbar as customizableUI component.
   1.173 +  CustomizableUI.registerArea("inner-" + state.id, {
   1.174 +    type: CustomizableUI.TYPE_TOOLBAR,
   1.175 +    legacy: true,
   1.176 +    defaultPlacements: [...state.items]
   1.177 +  });
   1.178 +};
   1.179 +// Utility function used to unregister toolbar from the CustomizableUI.
   1.180 +const unregisterToolbar = CustomizableUI.unregisterArea;
   1.181 +
   1.182 +const reactor = new Reactor({
   1.183 +  onStep: (present, past) => {
   1.184 +    const delta = diff(past, present);
   1.185 +
   1.186 +    each(([id, update]) => {
   1.187 +      // If update is `null` toolbar is removed, in such case
   1.188 +      // we unregister toolbar and remove it from each window
   1.189 +      // it was added to.
   1.190 +      if (update === null) {
   1.191 +        unregisterToolbar("inner-" + id);
   1.192 +        each(removeView(id), values(past.windows));
   1.193 +
   1.194 +        send(output, object([id, null]));
   1.195 +      }
   1.196 +      else if (past.toolbars[id]) {
   1.197 +        // If `collapsed` state for toolbar was updated, persist
   1.198 +        // it for a future sessions.
   1.199 +        if (update.collapsed !== void(0))
   1.200 +          prefs.set(PREF_ROOT + id, update.collapsed);
   1.201 +
   1.202 +        // Reflect update in each window it was added to.
   1.203 +        each(updateView(id, update), values(past.windows));
   1.204 +
   1.205 +        send(output, object([id, update]));
   1.206 +      }
   1.207 +      // Hack: Mutation observers are invoked async, which means that if
   1.208 +      // client does `hide(toolbar)` & then `toolbar.destroy()` by the
   1.209 +      // time we'll get update for `collapsed` toolbar will be removed.
   1.210 +      // For now we check if `update.id` is present which will be undefined
   1.211 +      // in such cases.
   1.212 +      else if (update.id) {
   1.213 +        // If it is a new toolbar we create initial state by overriding
   1.214 +        // `collapsed` filed with value persisted in previous sessions.
   1.215 +        const state = patch(update, {
   1.216 +          collapsed: prefs.get(PREF_ROOT + id, update.collapsed),
   1.217 +        });
   1.218 +
   1.219 +        // Register toolbar and add it each window known in the past
   1.220 +        // (note that new windows if any will be handled in loop below).
   1.221 +        registerToolbar(state);
   1.222 +        each(addView(state), values(past.windows));
   1.223 +
   1.224 +        send(output, object([state.id, state]));
   1.225 +      }
   1.226 +    }, pairs(delta.toolbars));
   1.227 +
   1.228 +    // Add views to every window that was added.
   1.229 +    each(window => {
   1.230 +      if (window)
   1.231 +        each(viewAdd(window), values(past.toolbars));
   1.232 +    }, values(delta.windows));
   1.233 +
   1.234 +    each(([id, isCustomizing]) => {
   1.235 +      each(viewUpdate(getByOuterId(id), {isCustomizing: !!isCustomizing}),
   1.236 +        keys(present.toolbars));
   1.237 +
   1.238 +    }, pairs(delta.customizable))
   1.239 +  },
   1.240 +  onEnd: state => {
   1.241 +    each(id => {
   1.242 +      unregisterToolbar("inner-" + id);
   1.243 +      each(removeView(id), values(state.windows));
   1.244 +    }, keys(state.toolbars));
   1.245 +  }
   1.246 +});
   1.247 +reactor.run(State);

mercurial