Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
michael@0 | 1 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 4 | "use strict"; |
michael@0 | 5 | |
michael@0 | 6 | module.metadata = { |
michael@0 | 7 | "stability": "experimental", |
michael@0 | 8 | "engines": { |
michael@0 | 9 | "Firefox": "> 28" |
michael@0 | 10 | } |
michael@0 | 11 | }; |
michael@0 | 12 | |
michael@0 | 13 | const { Cu } = require("chrome"); |
michael@0 | 14 | const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {}); |
michael@0 | 15 | const { subscribe, send, Reactor, foldp, lift, merges } = require("../../event/utils"); |
michael@0 | 16 | const { InputPort } = require("../../input/system"); |
michael@0 | 17 | const { OutputPort } = require("../../output/system"); |
michael@0 | 18 | const { Interactive } = require("../../input/browser"); |
michael@0 | 19 | const { CustomizationInput } = require("../../input/customizable-ui"); |
michael@0 | 20 | const { pairs, map, isEmpty, object, |
michael@0 | 21 | each, keys, values } = require("../../util/sequence"); |
michael@0 | 22 | const { curry, flip } = require("../../lang/functional"); |
michael@0 | 23 | const { patch, diff } = require("diffpatcher/index"); |
michael@0 | 24 | const prefs = require("../../preferences/service"); |
michael@0 | 25 | const { getByOuterId } = require("../../window/utils"); |
michael@0 | 26 | |
michael@0 | 27 | const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; |
michael@0 | 28 | const PREF_ROOT = "extensions.sdk-toolbar-collapsed."; |
michael@0 | 29 | |
michael@0 | 30 | |
michael@0 | 31 | // There are two output ports one for publishing changes that occured |
michael@0 | 32 | // and the other for change requests. Later is synchronous and is only |
michael@0 | 33 | // consumed here. Note: it needs to be synchronous to avoid race conditions |
michael@0 | 34 | // when `collapsed` attribute changes are caused by user interaction and |
michael@0 | 35 | // toolbar is destroyed between the ticks. |
michael@0 | 36 | const output = new OutputPort({ id: "toolbar-changed" }); |
michael@0 | 37 | const syncoutput = new OutputPort({ id: "toolbar-change", sync: true }); |
michael@0 | 38 | |
michael@0 | 39 | // Merge disptached changes and recevied changes from models to keep state up to |
michael@0 | 40 | // date. |
michael@0 | 41 | const Toolbars = foldp(patch, {}, merges([new InputPort({ id: "toolbar-changed" }), |
michael@0 | 42 | new InputPort({ id: "toolbar-change" })])); |
michael@0 | 43 | const State = lift((toolbars, windows, customizable) => |
michael@0 | 44 | ({windows: windows, toolbars: toolbars, customizable: customizable}), |
michael@0 | 45 | Toolbars, Interactive, new CustomizationInput()); |
michael@0 | 46 | |
michael@0 | 47 | // Shared event handler that makes `event.target.parent` collapsed. |
michael@0 | 48 | // Used as toolbar's close buttons click handler. |
michael@0 | 49 | const collapseToolbar = event => { |
michael@0 | 50 | const toolbar = event.target.parentNode; |
michael@0 | 51 | toolbar.collapsed = true; |
michael@0 | 52 | }; |
michael@0 | 53 | |
michael@0 | 54 | const parseAttribute = x => |
michael@0 | 55 | x === "true" ? true : |
michael@0 | 56 | x === "false" ? false : |
michael@0 | 57 | x === "" ? null : |
michael@0 | 58 | x; |
michael@0 | 59 | |
michael@0 | 60 | // Shared mutation observer that is used to observe `toolbar` node's |
michael@0 | 61 | // attribute mutations. Mutations are aggregated in the `delta` hash |
michael@0 | 62 | // and send to `ToolbarStateChanged` channel to let model know state |
michael@0 | 63 | // has changed. |
michael@0 | 64 | const attributesChanged = mutations => { |
michael@0 | 65 | const delta = mutations.reduce((changes, {attributeName, target}) => { |
michael@0 | 66 | const id = target.id; |
michael@0 | 67 | const field = attributeName === "toolbarname" ? "title" : attributeName; |
michael@0 | 68 | let change = changes[id] || (changes[id] = {}); |
michael@0 | 69 | change[field] = parseAttribute(target.getAttribute(attributeName)); |
michael@0 | 70 | return changes; |
michael@0 | 71 | }, {}); |
michael@0 | 72 | |
michael@0 | 73 | // Calculate what are the updates from the current state and if there are |
michael@0 | 74 | // any send them. |
michael@0 | 75 | const updates = diff(reactor.value, patch(reactor.value, delta)); |
michael@0 | 76 | |
michael@0 | 77 | if (!isEmpty(pairs(updates))) { |
michael@0 | 78 | // TODO: Consider sending sync to make sure that there won't be a new |
michael@0 | 79 | // update doing a delete in the meantime. |
michael@0 | 80 | send(syncoutput, updates); |
michael@0 | 81 | } |
michael@0 | 82 | }; |
michael@0 | 83 | |
michael@0 | 84 | |
michael@0 | 85 | // Utility function creates `toolbar` with a "close" button and returns |
michael@0 | 86 | // it back. In addition it set's up a listener and observer to communicate |
michael@0 | 87 | // state changes. |
michael@0 | 88 | const addView = curry((options, {document}) => { |
michael@0 | 89 | let view = document.createElementNS(XUL_NS, "toolbar"); |
michael@0 | 90 | view.setAttribute("id", options.id); |
michael@0 | 91 | view.setAttribute("collapsed", options.collapsed); |
michael@0 | 92 | view.setAttribute("toolbarname", options.title); |
michael@0 | 93 | view.setAttribute("pack", "end"); |
michael@0 | 94 | view.setAttribute("customizable", "false"); |
michael@0 | 95 | view.setAttribute("style", "padding: 2px 0; max-height: 40px;"); |
michael@0 | 96 | view.setAttribute("mode", "icons"); |
michael@0 | 97 | view.setAttribute("iconsize", "small"); |
michael@0 | 98 | view.setAttribute("context", "toolbar-context-menu"); |
michael@0 | 99 | view.setAttribute("class", "toolbar-primary chromeclass-toolbar"); |
michael@0 | 100 | |
michael@0 | 101 | let label = document.createElementNS(XUL_NS, "label"); |
michael@0 | 102 | label.setAttribute("value", options.title); |
michael@0 | 103 | label.setAttribute("collapsed", "true"); |
michael@0 | 104 | view.appendChild(label); |
michael@0 | 105 | |
michael@0 | 106 | let closeButton = document.createElementNS(XUL_NS, "toolbarbutton"); |
michael@0 | 107 | closeButton.setAttribute("id", "close-" + options.id); |
michael@0 | 108 | closeButton.setAttribute("class", "close-icon"); |
michael@0 | 109 | closeButton.setAttribute("customizable", false); |
michael@0 | 110 | closeButton.addEventListener("command", collapseToolbar); |
michael@0 | 111 | |
michael@0 | 112 | view.appendChild(closeButton); |
michael@0 | 113 | |
michael@0 | 114 | // In order to have a close button not costumizable, aligned on the right, |
michael@0 | 115 | // leaving the customizable capabilities of Australis, we need to create |
michael@0 | 116 | // a toolbar inside a toolbar. |
michael@0 | 117 | // This is should be a temporary hack, we should have a proper XBL for toolbar |
michael@0 | 118 | // instead. See: |
michael@0 | 119 | // https://bugzilla.mozilla.org/show_bug.cgi?id=982005 |
michael@0 | 120 | let toolbar = document.createElementNS(XUL_NS, "toolbar"); |
michael@0 | 121 | toolbar.setAttribute("id", "inner-" + options.id); |
michael@0 | 122 | toolbar.setAttribute("defaultset", options.items.join(",")); |
michael@0 | 123 | toolbar.setAttribute("customizable", "true"); |
michael@0 | 124 | toolbar.setAttribute("style", "-moz-appearance: none; overflow: hidden"); |
michael@0 | 125 | toolbar.setAttribute("mode", "icons"); |
michael@0 | 126 | toolbar.setAttribute("iconsize", "small"); |
michael@0 | 127 | toolbar.setAttribute("context", "toolbar-context-menu"); |
michael@0 | 128 | toolbar.setAttribute("flex", "1"); |
michael@0 | 129 | |
michael@0 | 130 | view.insertBefore(toolbar, closeButton); |
michael@0 | 131 | |
michael@0 | 132 | const observer = new document.defaultView.MutationObserver(attributesChanged); |
michael@0 | 133 | observer.observe(view, { attributes: true, |
michael@0 | 134 | attributeFilter: ["collapsed", "toolbarname"] }); |
michael@0 | 135 | |
michael@0 | 136 | const toolbox = document.getElementById("navigator-toolbox"); |
michael@0 | 137 | toolbox.appendChild(view); |
michael@0 | 138 | }); |
michael@0 | 139 | const viewAdd = curry(flip(addView)); |
michael@0 | 140 | |
michael@0 | 141 | const removeView = curry((id, {document}) => { |
michael@0 | 142 | const view = document.getElementById(id); |
michael@0 | 143 | if (view) view.remove(); |
michael@0 | 144 | }); |
michael@0 | 145 | |
michael@0 | 146 | const updateView = curry((id, {title, collapsed, isCustomizing}, {document}) => { |
michael@0 | 147 | const view = document.getElementById(id); |
michael@0 | 148 | |
michael@0 | 149 | if (!view) |
michael@0 | 150 | return; |
michael@0 | 151 | |
michael@0 | 152 | if (title) |
michael@0 | 153 | view.setAttribute("toolbarname", title); |
michael@0 | 154 | |
michael@0 | 155 | if (collapsed !== void(0)) |
michael@0 | 156 | view.setAttribute("collapsed", Boolean(collapsed)); |
michael@0 | 157 | |
michael@0 | 158 | if (isCustomizing !== void(0)) { |
michael@0 | 159 | view.querySelector("label").collapsed = !isCustomizing; |
michael@0 | 160 | view.querySelector("toolbar").style.visibility = isCustomizing |
michael@0 | 161 | ? "hidden" : "visible"; |
michael@0 | 162 | } |
michael@0 | 163 | }); |
michael@0 | 164 | |
michael@0 | 165 | const viewUpdate = curry(flip(updateView)); |
michael@0 | 166 | |
michael@0 | 167 | // Utility function used to register toolbar into CustomizableUI. |
michael@0 | 168 | const registerToolbar = state => { |
michael@0 | 169 | // If it's first additon register toolbar as customizableUI component. |
michael@0 | 170 | CustomizableUI.registerArea("inner-" + state.id, { |
michael@0 | 171 | type: CustomizableUI.TYPE_TOOLBAR, |
michael@0 | 172 | legacy: true, |
michael@0 | 173 | defaultPlacements: [...state.items] |
michael@0 | 174 | }); |
michael@0 | 175 | }; |
michael@0 | 176 | // Utility function used to unregister toolbar from the CustomizableUI. |
michael@0 | 177 | const unregisterToolbar = CustomizableUI.unregisterArea; |
michael@0 | 178 | |
michael@0 | 179 | const reactor = new Reactor({ |
michael@0 | 180 | onStep: (present, past) => { |
michael@0 | 181 | const delta = diff(past, present); |
michael@0 | 182 | |
michael@0 | 183 | each(([id, update]) => { |
michael@0 | 184 | // If update is `null` toolbar is removed, in such case |
michael@0 | 185 | // we unregister toolbar and remove it from each window |
michael@0 | 186 | // it was added to. |
michael@0 | 187 | if (update === null) { |
michael@0 | 188 | unregisterToolbar("inner-" + id); |
michael@0 | 189 | each(removeView(id), values(past.windows)); |
michael@0 | 190 | |
michael@0 | 191 | send(output, object([id, null])); |
michael@0 | 192 | } |
michael@0 | 193 | else if (past.toolbars[id]) { |
michael@0 | 194 | // If `collapsed` state for toolbar was updated, persist |
michael@0 | 195 | // it for a future sessions. |
michael@0 | 196 | if (update.collapsed !== void(0)) |
michael@0 | 197 | prefs.set(PREF_ROOT + id, update.collapsed); |
michael@0 | 198 | |
michael@0 | 199 | // Reflect update in each window it was added to. |
michael@0 | 200 | each(updateView(id, update), values(past.windows)); |
michael@0 | 201 | |
michael@0 | 202 | send(output, object([id, update])); |
michael@0 | 203 | } |
michael@0 | 204 | // Hack: Mutation observers are invoked async, which means that if |
michael@0 | 205 | // client does `hide(toolbar)` & then `toolbar.destroy()` by the |
michael@0 | 206 | // time we'll get update for `collapsed` toolbar will be removed. |
michael@0 | 207 | // For now we check if `update.id` is present which will be undefined |
michael@0 | 208 | // in such cases. |
michael@0 | 209 | else if (update.id) { |
michael@0 | 210 | // If it is a new toolbar we create initial state by overriding |
michael@0 | 211 | // `collapsed` filed with value persisted in previous sessions. |
michael@0 | 212 | const state = patch(update, { |
michael@0 | 213 | collapsed: prefs.get(PREF_ROOT + id, update.collapsed), |
michael@0 | 214 | }); |
michael@0 | 215 | |
michael@0 | 216 | // Register toolbar and add it each window known in the past |
michael@0 | 217 | // (note that new windows if any will be handled in loop below). |
michael@0 | 218 | registerToolbar(state); |
michael@0 | 219 | each(addView(state), values(past.windows)); |
michael@0 | 220 | |
michael@0 | 221 | send(output, object([state.id, state])); |
michael@0 | 222 | } |
michael@0 | 223 | }, pairs(delta.toolbars)); |
michael@0 | 224 | |
michael@0 | 225 | // Add views to every window that was added. |
michael@0 | 226 | each(window => { |
michael@0 | 227 | if (window) |
michael@0 | 228 | each(viewAdd(window), values(past.toolbars)); |
michael@0 | 229 | }, values(delta.windows)); |
michael@0 | 230 | |
michael@0 | 231 | each(([id, isCustomizing]) => { |
michael@0 | 232 | each(viewUpdate(getByOuterId(id), {isCustomizing: !!isCustomizing}), |
michael@0 | 233 | keys(present.toolbars)); |
michael@0 | 234 | |
michael@0 | 235 | }, pairs(delta.customizable)) |
michael@0 | 236 | }, |
michael@0 | 237 | onEnd: state => { |
michael@0 | 238 | each(id => { |
michael@0 | 239 | unregisterToolbar("inner-" + id); |
michael@0 | 240 | each(removeView(id), values(state.windows)); |
michael@0 | 241 | }, keys(state.toolbars)); |
michael@0 | 242 | } |
michael@0 | 243 | }); |
michael@0 | 244 | reactor.run(State); |