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);