|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 'use strict'; |
|
5 |
|
6 module.metadata = { |
|
7 'stability': 'experimental', |
|
8 'engines': { |
|
9 'Firefox': '> 28' |
|
10 } |
|
11 }; |
|
12 |
|
13 const { Cu } = require('chrome'); |
|
14 const { on, off, emit } = require('../../event/core'); |
|
15 |
|
16 const { data } = require('sdk/self'); |
|
17 |
|
18 const { isObject } = require('../../lang/type'); |
|
19 |
|
20 const { getMostRecentBrowserWindow } = require('../../window/utils'); |
|
21 const { ignoreWindow } = require('../../private-browsing/utils'); |
|
22 const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {}); |
|
23 const { AREA_PANEL, AREA_NAVBAR } = CustomizableUI; |
|
24 |
|
25 const { events: viewEvents } = require('./view/events'); |
|
26 |
|
27 const XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'; |
|
28 |
|
29 const views = new Map(); |
|
30 const customizedWindows = new WeakMap(); |
|
31 |
|
32 const buttonListener = { |
|
33 onCustomizeStart: window => { |
|
34 for (let [id, view] of views) { |
|
35 setIcon(id, window, view.icon); |
|
36 setLabel(id, window, view.label); |
|
37 } |
|
38 |
|
39 customizedWindows.set(window, true); |
|
40 }, |
|
41 onCustomizeEnd: window => { |
|
42 customizedWindows.delete(window); |
|
43 |
|
44 for (let [id, ] of views) { |
|
45 let placement = CustomizableUI.getPlacementOfWidget(id); |
|
46 |
|
47 if (placement) |
|
48 emit(viewEvents, 'data', { type: 'update', target: id, window: window }); |
|
49 } |
|
50 }, |
|
51 onWidgetAfterDOMChange: (node, nextNode, container) => { |
|
52 let { id } = node; |
|
53 let view = views.get(id); |
|
54 let window = node.ownerDocument.defaultView; |
|
55 |
|
56 if (view) { |
|
57 emit(viewEvents, 'data', { type: 'update', target: id, window: window }); |
|
58 } |
|
59 } |
|
60 }; |
|
61 |
|
62 CustomizableUI.addListener(buttonListener); |
|
63 |
|
64 require('../../system/unload').when( _ => |
|
65 CustomizableUI.removeListener(buttonListener) |
|
66 ); |
|
67 |
|
68 function getNode(id, window) { |
|
69 return !views.has(id) || ignoreWindow(window) |
|
70 ? null |
|
71 : CustomizableUI.getWidget(id).forWindow(window).node |
|
72 }; |
|
73 |
|
74 function isInToolbar(id) { |
|
75 let placement = CustomizableUI.getPlacementOfWidget(id); |
|
76 |
|
77 return placement && CustomizableUI.getAreaType(placement.area) === 'toolbar'; |
|
78 } |
|
79 |
|
80 |
|
81 function getImage(icon, isInToolbar, pixelRatio) { |
|
82 let targetSize = (isInToolbar ? 18 : 32) * pixelRatio; |
|
83 let bestSize = 0; |
|
84 let image = icon; |
|
85 |
|
86 if (isObject(icon)) { |
|
87 for (let size of Object.keys(icon)) { |
|
88 size = +size; |
|
89 let offset = targetSize - size; |
|
90 |
|
91 if (offset === 0) { |
|
92 bestSize = size; |
|
93 break; |
|
94 } |
|
95 |
|
96 let delta = Math.abs(offset) - Math.abs(targetSize - bestSize); |
|
97 |
|
98 if (delta < 0) |
|
99 bestSize = size; |
|
100 } |
|
101 |
|
102 image = icon[bestSize]; |
|
103 } |
|
104 |
|
105 if (image.indexOf('./') === 0) |
|
106 return data.url(image.substr(2)); |
|
107 |
|
108 return image; |
|
109 } |
|
110 |
|
111 function nodeFor(id, window=getMostRecentBrowserWindow()) { |
|
112 return customizedWindows.has(window) ? null : getNode(id, window); |
|
113 }; |
|
114 exports.nodeFor = nodeFor; |
|
115 |
|
116 function create(options) { |
|
117 let { id, label, icon, type } = options; |
|
118 |
|
119 if (views.has(id)) |
|
120 throw new Error('The ID "' + id + '" seems already used.'); |
|
121 |
|
122 CustomizableUI.createWidget({ |
|
123 id: id, |
|
124 type: 'custom', |
|
125 removable: true, |
|
126 defaultArea: AREA_NAVBAR, |
|
127 allowedAreas: [ AREA_PANEL, AREA_NAVBAR ], |
|
128 |
|
129 onBuild: function(document) { |
|
130 let window = document.defaultView; |
|
131 |
|
132 let node = document.createElementNS(XUL_NS, 'toolbarbutton'); |
|
133 |
|
134 let image = getImage(icon, true, window.devicePixelRatio); |
|
135 |
|
136 if (ignoreWindow(window)) |
|
137 node.style.display = 'none'; |
|
138 |
|
139 node.setAttribute('id', this.id); |
|
140 node.setAttribute('class', 'toolbarbutton-1 chromeclass-toolbar-additional'); |
|
141 node.setAttribute('type', type); |
|
142 node.setAttribute('label', label); |
|
143 node.setAttribute('tooltiptext', label); |
|
144 node.setAttribute('image', image); |
|
145 node.setAttribute('sdk-button', 'true'); |
|
146 |
|
147 views.set(id, { |
|
148 area: this.currentArea, |
|
149 icon: icon, |
|
150 label: label |
|
151 }); |
|
152 |
|
153 node.addEventListener('command', function(event) { |
|
154 if (views.has(id)) { |
|
155 emit(viewEvents, 'data', { |
|
156 type: 'click', |
|
157 target: id, |
|
158 window: event.view, |
|
159 checked: node.checked |
|
160 }); |
|
161 } |
|
162 }); |
|
163 |
|
164 return node; |
|
165 } |
|
166 }); |
|
167 }; |
|
168 exports.create = create; |
|
169 |
|
170 function dispose(id) { |
|
171 if (!views.has(id)) return; |
|
172 |
|
173 views.delete(id); |
|
174 CustomizableUI.destroyWidget(id); |
|
175 } |
|
176 exports.dispose = dispose; |
|
177 |
|
178 function setIcon(id, window, icon) { |
|
179 let node = getNode(id, window); |
|
180 |
|
181 if (node) { |
|
182 icon = customizedWindows.has(window) ? views.get(id).icon : icon; |
|
183 let image = getImage(icon, isInToolbar(id), window.devicePixelRatio); |
|
184 |
|
185 node.setAttribute('image', image); |
|
186 } |
|
187 } |
|
188 exports.setIcon = setIcon; |
|
189 |
|
190 function setLabel(id, window, label) { |
|
191 let node = nodeFor(id, window); |
|
192 |
|
193 if (node) { |
|
194 node.setAttribute('label', label); |
|
195 node.setAttribute('tooltiptext', label); |
|
196 } |
|
197 } |
|
198 exports.setLabel = setLabel; |
|
199 |
|
200 function setDisabled(id, window, disabled) { |
|
201 let node = nodeFor(id, window); |
|
202 |
|
203 if (node) |
|
204 node.disabled = disabled; |
|
205 } |
|
206 exports.setDisabled = setDisabled; |
|
207 |
|
208 function setChecked(id, window, checked) { |
|
209 let node = nodeFor(id, window); |
|
210 |
|
211 if (node) |
|
212 node.checked = checked; |
|
213 } |
|
214 exports.setChecked = setChecked; |
|
215 |
|
216 function click(id) { |
|
217 let node = nodeFor(id); |
|
218 |
|
219 if (node) |
|
220 node.click(); |
|
221 } |
|
222 exports.click = click; |