|
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 |
|
5 "use strict"; |
|
6 |
|
7 this.EXPORTED_SYMBOLS = ["CustomizableUI"]; |
|
8 |
|
9 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; |
|
10 |
|
11 Cu.import("resource://gre/modules/Services.jsm"); |
|
12 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
13 XPCOMUtils.defineLazyModuleGetter(this, "PanelWideWidgetTracker", |
|
14 "resource:///modules/PanelWideWidgetTracker.jsm"); |
|
15 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableWidgets", |
|
16 "resource:///modules/CustomizableWidgets.jsm"); |
|
17 XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask", |
|
18 "resource://gre/modules/DeferredTask.jsm"); |
|
19 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", |
|
20 "resource://gre/modules/PrivateBrowsingUtils.jsm"); |
|
21 XPCOMUtils.defineLazyModuleGetter(this, "Promise", |
|
22 "resource://gre/modules/Promise.jsm"); |
|
23 XPCOMUtils.defineLazyGetter(this, "gWidgetsBundle", function() { |
|
24 const kUrl = "chrome://browser/locale/customizableui/customizableWidgets.properties"; |
|
25 return Services.strings.createBundle(kUrl); |
|
26 }); |
|
27 XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils", |
|
28 "resource://gre/modules/ShortcutUtils.jsm"); |
|
29 XPCOMUtils.defineLazyServiceGetter(this, "gELS", |
|
30 "@mozilla.org/eventlistenerservice;1", "nsIEventListenerService"); |
|
31 |
|
32 const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; |
|
33 |
|
34 const kSpecialWidgetPfx = "customizableui-special-"; |
|
35 |
|
36 const kPrefCustomizationState = "browser.uiCustomization.state"; |
|
37 const kPrefCustomizationAutoAdd = "browser.uiCustomization.autoAdd"; |
|
38 const kPrefCustomizationDebug = "browser.uiCustomization.debug"; |
|
39 const kPrefDrawInTitlebar = "browser.tabs.drawInTitlebar"; |
|
40 |
|
41 /** |
|
42 * The keys are the handlers that are fired when the event type (the value) |
|
43 * is fired on the subview. A widget that provides a subview has the option |
|
44 * of providing onViewShowing and onViewHiding event handlers. |
|
45 */ |
|
46 const kSubviewEvents = [ |
|
47 "ViewShowing", |
|
48 "ViewHiding" |
|
49 ]; |
|
50 |
|
51 /** |
|
52 * gPalette is a map of every widget that CustomizableUI.jsm knows about, keyed |
|
53 * on their IDs. |
|
54 */ |
|
55 let gPalette = new Map(); |
|
56 |
|
57 /** |
|
58 * gAreas maps area IDs to Sets of properties about those areas. An area is a |
|
59 * place where a widget can be put. |
|
60 */ |
|
61 let gAreas = new Map(); |
|
62 |
|
63 /** |
|
64 * gPlacements maps area IDs to Arrays of widget IDs, indicating that the widgets |
|
65 * are placed within that area (either directly in the area node, or in the |
|
66 * customizationTarget of the node). |
|
67 */ |
|
68 let gPlacements = new Map(); |
|
69 |
|
70 /** |
|
71 * gFuturePlacements represent placements that will happen for areas that have |
|
72 * not yet loaded (due to lazy-loading). This can occur when add-ons register |
|
73 * widgets. |
|
74 */ |
|
75 let gFuturePlacements = new Map(); |
|
76 |
|
77 //XXXunf Temporary. Need a nice way to abstract functions to build widgets |
|
78 // of these types. |
|
79 let gSupportedWidgetTypes = new Set(["button", "view", "custom"]); |
|
80 |
|
81 /** |
|
82 * gPanelsForWindow is a list of known panels in a window which we may need to close |
|
83 * should command events fire which target them. |
|
84 */ |
|
85 let gPanelsForWindow = new WeakMap(); |
|
86 |
|
87 /** |
|
88 * gSeenWidgets remembers which widgets the user has seen for the first time |
|
89 * before. This way, if a new widget is created, and the user has not seen it |
|
90 * before, it can be put in its default location. Otherwise, it remains in the |
|
91 * palette. |
|
92 */ |
|
93 let gSeenWidgets = new Set(); |
|
94 |
|
95 /** |
|
96 * gDirtyAreaCache is a set of area IDs for areas where items have been added, |
|
97 * moved or removed at least once. This set is persisted, and is used to |
|
98 * optimize building of toolbars in the default case where no toolbars should |
|
99 * be "dirty". |
|
100 */ |
|
101 let gDirtyAreaCache = new Set(); |
|
102 |
|
103 /** |
|
104 * gPendingBuildAreas is a map from area IDs to map from build nodes to their |
|
105 * existing children at the time of node registration, that are waiting |
|
106 * for the area to be registered |
|
107 */ |
|
108 let gPendingBuildAreas = new Map(); |
|
109 |
|
110 let gSavedState = null; |
|
111 let gRestoring = false; |
|
112 let gDirty = false; |
|
113 let gInBatchStack = 0; |
|
114 let gResetting = false; |
|
115 let gUndoResetting = false; |
|
116 |
|
117 /** |
|
118 * gBuildAreas maps area IDs to actual area nodes within browser windows. |
|
119 */ |
|
120 let gBuildAreas = new Map(); |
|
121 |
|
122 /** |
|
123 * gBuildWindows is a map of windows that have registered build areas, mapped |
|
124 * to a Set of known toolboxes in that window. |
|
125 */ |
|
126 let gBuildWindows = new Map(); |
|
127 |
|
128 let gNewElementCount = 0; |
|
129 let gGroupWrapperCache = new Map(); |
|
130 let gSingleWrapperCache = new WeakMap(); |
|
131 let gListeners = new Set(); |
|
132 |
|
133 let gUIStateBeforeReset = { |
|
134 uiCustomizationState: null, |
|
135 drawInTitlebar: null, |
|
136 }; |
|
137 |
|
138 let gModuleName = "[CustomizableUI]"; |
|
139 #include logging.js |
|
140 |
|
141 let CustomizableUIInternal = { |
|
142 initialize: function() { |
|
143 LOG("Initializing"); |
|
144 |
|
145 this.addListener(this); |
|
146 this._defineBuiltInWidgets(); |
|
147 this.loadSavedState(); |
|
148 |
|
149 let panelPlacements = [ |
|
150 "edit-controls", |
|
151 "zoom-controls", |
|
152 "new-window-button", |
|
153 "privatebrowsing-button", |
|
154 "save-page-button", |
|
155 "print-button", |
|
156 "history-panelmenu", |
|
157 "fullscreen-button", |
|
158 "find-button", |
|
159 "preferences-button", |
|
160 "add-ons-button", |
|
161 "developer-button", |
|
162 ]; |
|
163 |
|
164 if (gPalette.has("switch-to-metro-button")) { |
|
165 panelPlacements.push("switch-to-metro-button"); |
|
166 } |
|
167 |
|
168 #ifdef NIGHTLY_BUILD |
|
169 if (gPalette.has("e10s-button")) { |
|
170 let newWindowIndex = panelPlacements.indexOf("new-window-button"); |
|
171 if (newWindowIndex > -1) { |
|
172 panelPlacements.splice(newWindowIndex + 1, 0, "e10s-button"); |
|
173 } |
|
174 } |
|
175 #endif |
|
176 |
|
177 let showCharacterEncoding = Services.prefs.getComplexValue( |
|
178 "browser.menu.showCharacterEncoding", |
|
179 Ci.nsIPrefLocalizedString |
|
180 ).data; |
|
181 if (showCharacterEncoding == "true") { |
|
182 panelPlacements.push("characterencoding-button"); |
|
183 } |
|
184 |
|
185 this.registerArea(CustomizableUI.AREA_PANEL, { |
|
186 anchor: "PanelUI-menu-button", |
|
187 type: CustomizableUI.TYPE_MENU_PANEL, |
|
188 defaultPlacements: panelPlacements |
|
189 }, true); |
|
190 PanelWideWidgetTracker.init(); |
|
191 |
|
192 this.registerArea(CustomizableUI.AREA_NAVBAR, { |
|
193 legacy: true, |
|
194 type: CustomizableUI.TYPE_TOOLBAR, |
|
195 overflowable: true, |
|
196 defaultPlacements: [ |
|
197 "urlbar-container", |
|
198 "search-container", |
|
199 "webrtc-status-button", |
|
200 "bookmarks-menu-button", |
|
201 "downloads-button", |
|
202 "home-button", |
|
203 "social-share-button", |
|
204 ], |
|
205 defaultCollapsed: false, |
|
206 }, true); |
|
207 #ifndef XP_MACOSX |
|
208 this.registerArea(CustomizableUI.AREA_MENUBAR, { |
|
209 legacy: true, |
|
210 type: CustomizableUI.TYPE_TOOLBAR, |
|
211 defaultPlacements: [ |
|
212 "menubar-items", |
|
213 ], |
|
214 get defaultCollapsed() { |
|
215 #ifdef MENUBAR_CAN_AUTOHIDE |
|
216 #if defined(MOZ_WIDGET_GTK) || defined(MOZ_WIDGET_QT) |
|
217 return true; |
|
218 #else |
|
219 // This is duplicated logic from /browser/base/jar.mn |
|
220 // for win6BrowserOverlay.xul. |
|
221 return Services.appinfo.OS == "WINNT" && |
|
222 Services.sysinfo.getProperty("version") != "5.1"; |
|
223 #endif |
|
224 #endif |
|
225 return false; |
|
226 } |
|
227 }, true); |
|
228 #endif |
|
229 this.registerArea(CustomizableUI.AREA_TABSTRIP, { |
|
230 legacy: true, |
|
231 type: CustomizableUI.TYPE_TOOLBAR, |
|
232 defaultPlacements: [ |
|
233 "tabbrowser-tabs", |
|
234 "new-tab-button", |
|
235 "alltabs-button", |
|
236 ], |
|
237 defaultCollapsed: null, |
|
238 }, true); |
|
239 this.registerArea(CustomizableUI.AREA_BOOKMARKS, { |
|
240 legacy: true, |
|
241 type: CustomizableUI.TYPE_TOOLBAR, |
|
242 defaultPlacements: [ |
|
243 "personal-bookmarks", |
|
244 ], |
|
245 defaultCollapsed: true, |
|
246 }, true); |
|
247 |
|
248 this.registerArea(CustomizableUI.AREA_ADDONBAR, { |
|
249 type: CustomizableUI.TYPE_TOOLBAR, |
|
250 legacy: true, |
|
251 defaultPlacements: ["addonbar-closebutton", "status-bar"], |
|
252 defaultCollapsed: false, |
|
253 }, true); |
|
254 }, |
|
255 |
|
256 get _builtinToolbars() { |
|
257 return new Set([ |
|
258 CustomizableUI.AREA_NAVBAR, |
|
259 CustomizableUI.AREA_BOOKMARKS, |
|
260 CustomizableUI.AREA_TABSTRIP, |
|
261 CustomizableUI.AREA_ADDONBAR, |
|
262 #ifndef XP_MACOSX |
|
263 CustomizableUI.AREA_MENUBAR, |
|
264 #endif |
|
265 ]); |
|
266 }, |
|
267 |
|
268 _defineBuiltInWidgets: function() { |
|
269 //XXXunf Need to figure out how to auto-add new builtin widgets in new |
|
270 // app versions to already customized areas. |
|
271 for (let widgetDefinition of CustomizableWidgets) { |
|
272 this.createBuiltinWidget(widgetDefinition); |
|
273 } |
|
274 }, |
|
275 |
|
276 wrapWidget: function(aWidgetId) { |
|
277 if (gGroupWrapperCache.has(aWidgetId)) { |
|
278 return gGroupWrapperCache.get(aWidgetId); |
|
279 } |
|
280 |
|
281 let provider = this.getWidgetProvider(aWidgetId); |
|
282 if (!provider) { |
|
283 return null; |
|
284 } |
|
285 |
|
286 if (provider == CustomizableUI.PROVIDER_API) { |
|
287 let widget = gPalette.get(aWidgetId); |
|
288 if (!widget.wrapper) { |
|
289 widget.wrapper = new WidgetGroupWrapper(widget); |
|
290 gGroupWrapperCache.set(aWidgetId, widget.wrapper); |
|
291 } |
|
292 return widget.wrapper; |
|
293 } |
|
294 |
|
295 // PROVIDER_SPECIAL gets treated the same as PROVIDER_XUL. |
|
296 let wrapper = new XULWidgetGroupWrapper(aWidgetId); |
|
297 gGroupWrapperCache.set(aWidgetId, wrapper); |
|
298 return wrapper; |
|
299 }, |
|
300 |
|
301 registerArea: function(aName, aProperties, aInternalCaller) { |
|
302 if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) { |
|
303 throw new Error("Invalid area name"); |
|
304 } |
|
305 |
|
306 let areaIsKnown = gAreas.has(aName); |
|
307 let props = areaIsKnown ? gAreas.get(aName) : new Map(); |
|
308 const kImmutableProperties = new Set(["type", "legacy", "overflowable"]); |
|
309 for (let key in aProperties) { |
|
310 if (areaIsKnown && kImmutableProperties.has(key) && |
|
311 props.get(key) != aProperties[key]) { |
|
312 throw new Error("An area cannot change the property for '" + key + "'"); |
|
313 } |
|
314 //XXXgijs for special items, we need to make sure they have an appropriate ID |
|
315 // so we aren't perpetually in a non-default state: |
|
316 if (key == "defaultPlacements" && Array.isArray(aProperties[key])) { |
|
317 props.set(key, aProperties[key].map(x => this.isSpecialWidget(x) ? this.ensureSpecialWidgetId(x) : x )); |
|
318 } else { |
|
319 props.set(key, aProperties[key]); |
|
320 } |
|
321 } |
|
322 // Default to a toolbar: |
|
323 if (!props.has("type")) { |
|
324 props.set("type", CustomizableUI.TYPE_TOOLBAR); |
|
325 } |
|
326 if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) { |
|
327 // Check aProperties instead of props because this check is only interested |
|
328 // in the passed arguments, not the state of a potentially pre-existing area. |
|
329 if (!aInternalCaller && aProperties["defaultCollapsed"]) { |
|
330 throw new Error("defaultCollapsed is only allowed for default toolbars.") |
|
331 } |
|
332 if (!props.has("defaultCollapsed")) { |
|
333 props.set("defaultCollapsed", true); |
|
334 } |
|
335 } else if (props.has("defaultCollapsed")) { |
|
336 throw new Error("defaultCollapsed only applies for TYPE_TOOLBAR areas."); |
|
337 } |
|
338 // Sanity check type: |
|
339 let allTypes = [CustomizableUI.TYPE_TOOLBAR, CustomizableUI.TYPE_MENU_PANEL]; |
|
340 if (allTypes.indexOf(props.get("type")) == -1) { |
|
341 throw new Error("Invalid area type " + props.get("type")); |
|
342 } |
|
343 |
|
344 // And to no placements: |
|
345 if (!props.has("defaultPlacements")) { |
|
346 props.set("defaultPlacements", []); |
|
347 } |
|
348 // Sanity check default placements array: |
|
349 if (!Array.isArray(props.get("defaultPlacements"))) { |
|
350 throw new Error("Should provide an array of default placements"); |
|
351 } |
|
352 |
|
353 if (!areaIsKnown) { |
|
354 gAreas.set(aName, props); |
|
355 |
|
356 if (props.get("legacy") && !gPlacements.has(aName)) { |
|
357 // Guarantee this area exists in gFuturePlacements, to avoid checking it in |
|
358 // various places elsewhere. |
|
359 gFuturePlacements.set(aName, new Set()); |
|
360 } else { |
|
361 this.restoreStateForArea(aName); |
|
362 } |
|
363 |
|
364 // If we have pending build area nodes, register all of them |
|
365 if (gPendingBuildAreas.has(aName)) { |
|
366 let pendingNodes = gPendingBuildAreas.get(aName); |
|
367 for (let [pendingNode, existingChildren] of pendingNodes) { |
|
368 this.registerToolbarNode(pendingNode, existingChildren); |
|
369 } |
|
370 gPendingBuildAreas.delete(aName); |
|
371 } |
|
372 } |
|
373 }, |
|
374 |
|
375 unregisterArea: function(aName, aDestroyPlacements) { |
|
376 if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) { |
|
377 throw new Error("Invalid area name"); |
|
378 } |
|
379 if (!gAreas.has(aName) && !gPlacements.has(aName)) { |
|
380 throw new Error("Area not registered"); |
|
381 } |
|
382 |
|
383 // Move all the widgets out |
|
384 this.beginBatchUpdate(); |
|
385 try { |
|
386 let placements = gPlacements.get(aName); |
|
387 if (placements) { |
|
388 // Need to clone this array so removeWidgetFromArea doesn't modify it |
|
389 placements = [...placements]; |
|
390 placements.forEach(this.removeWidgetFromArea, this); |
|
391 } |
|
392 |
|
393 // Delete all remaining traces. |
|
394 gAreas.delete(aName); |
|
395 // Only destroy placements when necessary: |
|
396 if (aDestroyPlacements) { |
|
397 gPlacements.delete(aName); |
|
398 } else { |
|
399 // Otherwise we need to re-set them, as removeFromArea will have emptied |
|
400 // them out: |
|
401 gPlacements.set(aName, placements); |
|
402 } |
|
403 gFuturePlacements.delete(aName); |
|
404 let existingAreaNodes = gBuildAreas.get(aName); |
|
405 if (existingAreaNodes) { |
|
406 for (let areaNode of existingAreaNodes) { |
|
407 this.notifyListeners("onAreaNodeUnregistered", aName, areaNode.customizationTarget, |
|
408 CustomizableUI.REASON_AREA_UNREGISTERED); |
|
409 } |
|
410 } |
|
411 gBuildAreas.delete(aName); |
|
412 } finally { |
|
413 this.endBatchUpdate(true); |
|
414 } |
|
415 }, |
|
416 |
|
417 registerToolbarNode: function(aToolbar, aExistingChildren) { |
|
418 let area = aToolbar.id; |
|
419 if (gBuildAreas.has(area) && gBuildAreas.get(area).has(aToolbar)) { |
|
420 return; |
|
421 } |
|
422 let document = aToolbar.ownerDocument; |
|
423 let areaProperties = gAreas.get(area); |
|
424 |
|
425 // If this area is not registered, try to do it automatically: |
|
426 if (!areaProperties) { |
|
427 // If there's no defaultset attribute and this isn't a legacy extra toolbar, |
|
428 // we assume that we should wait for registerArea to be called: |
|
429 if (!aToolbar.hasAttribute("defaultset") && |
|
430 !aToolbar.hasAttribute("customindex")) { |
|
431 if (!gPendingBuildAreas.has(area)) { |
|
432 gPendingBuildAreas.set(area, new Map()); |
|
433 } |
|
434 let pendingNodes = gPendingBuildAreas.get(area); |
|
435 pendingNodes.set(aToolbar, aExistingChildren); |
|
436 return; |
|
437 } |
|
438 let props = {type: CustomizableUI.TYPE_TOOLBAR, legacy: true}; |
|
439 let defaultsetAttribute = aToolbar.getAttribute("defaultset") || ""; |
|
440 props.defaultPlacements = defaultsetAttribute.split(',').filter(s => s); |
|
441 this.registerArea(area, props); |
|
442 areaProperties = gAreas.get(area); |
|
443 } |
|
444 |
|
445 this.beginBatchUpdate(); |
|
446 try { |
|
447 let placements = gPlacements.get(area); |
|
448 if (!placements && areaProperties.has("legacy")) { |
|
449 let legacyState = aToolbar.getAttribute("currentset"); |
|
450 if (legacyState) { |
|
451 legacyState = legacyState.split(",").filter(s => s); |
|
452 } |
|
453 |
|
454 // Manually restore the state here, so the legacy state can be converted. |
|
455 this.restoreStateForArea(area, legacyState); |
|
456 placements = gPlacements.get(area); |
|
457 } |
|
458 |
|
459 // Check that the current children and the current placements match. If |
|
460 // not, mark it as dirty: |
|
461 if (aExistingChildren.length != placements.length || |
|
462 aExistingChildren.every((id, i) => id == placements[i])) { |
|
463 gDirtyAreaCache.add(area); |
|
464 } |
|
465 |
|
466 if (areaProperties.has("overflowable")) { |
|
467 aToolbar.overflowable = new OverflowableToolbar(aToolbar); |
|
468 } |
|
469 |
|
470 this.registerBuildArea(area, aToolbar); |
|
471 |
|
472 // We only build the toolbar if it's been marked as "dirty". Dirty means |
|
473 // one of the following things: |
|
474 // 1) Items have been added, moved or removed from this toolbar before. |
|
475 // 2) The number of children of the toolbar does not match the length of |
|
476 // the placements array for that area. |
|
477 // |
|
478 // This notion of being "dirty" is stored in a cache which is persisted |
|
479 // in the saved state. |
|
480 if (gDirtyAreaCache.has(area)) { |
|
481 this.buildArea(area, placements, aToolbar); |
|
482 } |
|
483 this.notifyListeners("onAreaNodeRegistered", area, aToolbar.customizationTarget); |
|
484 aToolbar.setAttribute("currentset", placements.join(",")); |
|
485 } finally { |
|
486 this.endBatchUpdate(); |
|
487 } |
|
488 }, |
|
489 |
|
490 buildArea: function(aArea, aPlacements, aAreaNode) { |
|
491 let document = aAreaNode.ownerDocument; |
|
492 let window = document.defaultView; |
|
493 let inPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(window); |
|
494 let container = aAreaNode.customizationTarget; |
|
495 let areaIsPanel = gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL; |
|
496 |
|
497 if (!container) { |
|
498 throw new Error("Expected area " + aArea |
|
499 + " to have a customizationTarget attribute."); |
|
500 } |
|
501 |
|
502 // Restore nav-bar visibility since it may have been hidden |
|
503 // through a migration path (bug 938980) or an add-on. |
|
504 if (aArea == CustomizableUI.AREA_NAVBAR) { |
|
505 aAreaNode.collapsed = false; |
|
506 } |
|
507 |
|
508 this.beginBatchUpdate(); |
|
509 |
|
510 try { |
|
511 let currentNode = container.firstChild; |
|
512 let placementsToRemove = new Set(); |
|
513 for (let id of aPlacements) { |
|
514 while (currentNode && currentNode.getAttribute("skipintoolbarset") == "true") { |
|
515 currentNode = currentNode.nextSibling; |
|
516 } |
|
517 |
|
518 if (currentNode && currentNode.id == id) { |
|
519 currentNode = currentNode.nextSibling; |
|
520 continue; |
|
521 } |
|
522 |
|
523 if (this.isSpecialWidget(id) && areaIsPanel) { |
|
524 placementsToRemove.add(id); |
|
525 continue; |
|
526 } |
|
527 |
|
528 let [provider, node] = this.getWidgetNode(id, window); |
|
529 if (!node) { |
|
530 LOG("Unknown widget: " + id); |
|
531 continue; |
|
532 } |
|
533 |
|
534 // If the placements have items in them which are (now) no longer removable, |
|
535 // we shouldn't be moving them: |
|
536 if (provider == CustomizableUI.PROVIDER_API) { |
|
537 let widgetInfo = gPalette.get(id); |
|
538 if (!widgetInfo.removable && aArea != widgetInfo.defaultArea) { |
|
539 placementsToRemove.add(id); |
|
540 continue; |
|
541 } |
|
542 } else if (provider == CustomizableUI.PROVIDER_XUL && |
|
543 node.parentNode != container && !this.isWidgetRemovable(node)) { |
|
544 placementsToRemove.add(id); |
|
545 continue; |
|
546 } // Special widgets are always removable, so no need to check them |
|
547 |
|
548 if (inPrivateWindow && provider == CustomizableUI.PROVIDER_API) { |
|
549 let widget = gPalette.get(id); |
|
550 if (!widget.showInPrivateBrowsing && inPrivateWindow) { |
|
551 continue; |
|
552 } |
|
553 } |
|
554 |
|
555 this.ensureButtonContextMenu(node, aAreaNode); |
|
556 if (node.localName == "toolbarbutton") { |
|
557 if (areaIsPanel) { |
|
558 node.setAttribute("wrap", "true"); |
|
559 } else { |
|
560 node.removeAttribute("wrap"); |
|
561 } |
|
562 } |
|
563 |
|
564 this.insertWidgetBefore(node, currentNode, container, aArea); |
|
565 if (gResetting) { |
|
566 this.notifyListeners("onWidgetReset", node, container); |
|
567 } else if (gUndoResetting) { |
|
568 this.notifyListeners("onWidgetUndoMove", node, container); |
|
569 } |
|
570 } |
|
571 |
|
572 if (currentNode) { |
|
573 let palette = aAreaNode.toolbox ? aAreaNode.toolbox.palette : null; |
|
574 let limit = currentNode.previousSibling; |
|
575 let node = container.lastChild; |
|
576 while (node && node != limit) { |
|
577 let previousSibling = node.previousSibling; |
|
578 // Nodes opt-in to removability. If they're removable, and we haven't |
|
579 // seen them in the placements array, then we toss them into the palette |
|
580 // if one exists. If no palette exists, we just remove the node. If the |
|
581 // node is not removable, we leave it where it is. However, we can only |
|
582 // safely touch elements that have an ID - both because we depend on |
|
583 // IDs, and because such elements are not intended to be widgets |
|
584 // (eg, titlebar-placeholder elements). |
|
585 if (node.id && node.getAttribute("skipintoolbarset") != "true") { |
|
586 if (this.isWidgetRemovable(node)) { |
|
587 if (palette && !this.isSpecialWidget(node.id)) { |
|
588 palette.appendChild(node); |
|
589 this.removeLocationAttributes(node); |
|
590 } else { |
|
591 container.removeChild(node); |
|
592 } |
|
593 } else { |
|
594 this.setLocationAttributes(currentNode, aArea); |
|
595 node.setAttribute("removable", false); |
|
596 LOG("Adding non-removable widget to placements of " + aArea + ": " + |
|
597 node.id); |
|
598 gPlacements.get(aArea).push(node.id); |
|
599 gDirty = true; |
|
600 } |
|
601 } |
|
602 node = previousSibling; |
|
603 } |
|
604 } |
|
605 |
|
606 // If there are placements in here which aren't removable from their original area, |
|
607 // we remove them from this area's placement array. They will (have) be(en) added |
|
608 // to their original area's placements array in the block above this one. |
|
609 if (placementsToRemove.size) { |
|
610 let placementAry = gPlacements.get(aArea); |
|
611 for (let id of placementsToRemove) { |
|
612 let index = placementAry.indexOf(id); |
|
613 placementAry.splice(index, 1); |
|
614 } |
|
615 } |
|
616 |
|
617 if (gResetting) { |
|
618 this.notifyListeners("onAreaReset", aArea, container); |
|
619 } |
|
620 } finally { |
|
621 this.endBatchUpdate(); |
|
622 } |
|
623 }, |
|
624 |
|
625 addPanelCloseListeners: function(aPanel) { |
|
626 gELS.addSystemEventListener(aPanel, "click", this, false); |
|
627 gELS.addSystemEventListener(aPanel, "keypress", this, false); |
|
628 let win = aPanel.ownerDocument.defaultView; |
|
629 if (!gPanelsForWindow.has(win)) { |
|
630 gPanelsForWindow.set(win, new Set()); |
|
631 } |
|
632 gPanelsForWindow.get(win).add(this._getPanelForNode(aPanel)); |
|
633 }, |
|
634 |
|
635 removePanelCloseListeners: function(aPanel) { |
|
636 gELS.removeSystemEventListener(aPanel, "click", this, false); |
|
637 gELS.removeSystemEventListener(aPanel, "keypress", this, false); |
|
638 let win = aPanel.ownerDocument.defaultView; |
|
639 let panels = gPanelsForWindow.get(win); |
|
640 if (panels) { |
|
641 panels.delete(this._getPanelForNode(aPanel)); |
|
642 } |
|
643 }, |
|
644 |
|
645 ensureButtonContextMenu: function(aNode, aAreaNode) { |
|
646 const kPanelItemContextMenu = "customizationPanelItemContextMenu"; |
|
647 |
|
648 let currentContextMenu = aNode.getAttribute("context") || |
|
649 aNode.getAttribute("contextmenu"); |
|
650 let place = CustomizableUI.getPlaceForItem(aAreaNode); |
|
651 let contextMenuForPlace = place == "panel" ? |
|
652 kPanelItemContextMenu : |
|
653 null; |
|
654 if (contextMenuForPlace && !currentContextMenu) { |
|
655 aNode.setAttribute("context", contextMenuForPlace); |
|
656 } else if (currentContextMenu == kPanelItemContextMenu && |
|
657 contextMenuForPlace != kPanelItemContextMenu) { |
|
658 aNode.removeAttribute("context"); |
|
659 aNode.removeAttribute("contextmenu"); |
|
660 } |
|
661 }, |
|
662 |
|
663 getWidgetProvider: function(aWidgetId) { |
|
664 if (this.isSpecialWidget(aWidgetId)) { |
|
665 return CustomizableUI.PROVIDER_SPECIAL; |
|
666 } |
|
667 if (gPalette.has(aWidgetId)) { |
|
668 return CustomizableUI.PROVIDER_API; |
|
669 } |
|
670 // If this was an API widget that was destroyed, return null: |
|
671 if (gSeenWidgets.has(aWidgetId)) { |
|
672 return null; |
|
673 } |
|
674 |
|
675 // We fall back to the XUL provider, but we don't know for sure (at this |
|
676 // point) whether it exists there either. So the API is technically lying. |
|
677 // Ideally, it would be able to return an error value (or throw an |
|
678 // exception) if it really didn't exist. Our code calling this function |
|
679 // handles that fine, but this is a public API. |
|
680 return CustomizableUI.PROVIDER_XUL; |
|
681 }, |
|
682 |
|
683 getWidgetNode: function(aWidgetId, aWindow) { |
|
684 let document = aWindow.document; |
|
685 |
|
686 if (this.isSpecialWidget(aWidgetId)) { |
|
687 let widgetNode = document.getElementById(aWidgetId) || |
|
688 this.createSpecialWidget(aWidgetId, document); |
|
689 return [ CustomizableUI.PROVIDER_SPECIAL, widgetNode]; |
|
690 } |
|
691 |
|
692 let widget = gPalette.get(aWidgetId); |
|
693 if (widget) { |
|
694 // If we have an instance of this widget already, just use that. |
|
695 if (widget.instances.has(document)) { |
|
696 LOG("An instance of widget " + aWidgetId + " already exists in this " |
|
697 + "document. Reusing."); |
|
698 return [ CustomizableUI.PROVIDER_API, |
|
699 widget.instances.get(document) ]; |
|
700 } |
|
701 |
|
702 return [ CustomizableUI.PROVIDER_API, |
|
703 this.buildWidget(document, widget) ]; |
|
704 } |
|
705 |
|
706 LOG("Searching for " + aWidgetId + " in toolbox."); |
|
707 let node = this.findWidgetInWindow(aWidgetId, aWindow); |
|
708 if (node) { |
|
709 return [ CustomizableUI.PROVIDER_XUL, node ]; |
|
710 } |
|
711 |
|
712 LOG("No node for " + aWidgetId + " found."); |
|
713 return [null, null]; |
|
714 }, |
|
715 |
|
716 registerMenuPanel: function(aPanelContents) { |
|
717 if (gBuildAreas.has(CustomizableUI.AREA_PANEL) && |
|
718 gBuildAreas.get(CustomizableUI.AREA_PANEL).has(aPanelContents)) { |
|
719 return; |
|
720 } |
|
721 |
|
722 let document = aPanelContents.ownerDocument; |
|
723 |
|
724 aPanelContents.toolbox = document.getElementById("navigator-toolbox"); |
|
725 aPanelContents.customizationTarget = aPanelContents; |
|
726 |
|
727 this.addPanelCloseListeners(this._getPanelForNode(aPanelContents)); |
|
728 |
|
729 let placements = gPlacements.get(CustomizableUI.AREA_PANEL); |
|
730 this.buildArea(CustomizableUI.AREA_PANEL, placements, aPanelContents); |
|
731 this.notifyListeners("onAreaNodeRegistered", CustomizableUI.AREA_PANEL, aPanelContents); |
|
732 |
|
733 for (let child of aPanelContents.children) { |
|
734 if (child.localName != "toolbarbutton") { |
|
735 if (child.localName == "toolbaritem") { |
|
736 this.ensureButtonContextMenu(child, aPanelContents); |
|
737 } |
|
738 continue; |
|
739 } |
|
740 this.ensureButtonContextMenu(child, aPanelContents); |
|
741 child.setAttribute("wrap", "true"); |
|
742 } |
|
743 |
|
744 this.registerBuildArea(CustomizableUI.AREA_PANEL, aPanelContents); |
|
745 }, |
|
746 |
|
747 onWidgetAdded: function(aWidgetId, aArea, aPosition) { |
|
748 this.insertNode(aWidgetId, aArea, aPosition, true); |
|
749 |
|
750 if (!gResetting) { |
|
751 this._clearPreviousUIState(); |
|
752 } |
|
753 }, |
|
754 |
|
755 onWidgetRemoved: function(aWidgetId, aArea) { |
|
756 let areaNodes = gBuildAreas.get(aArea); |
|
757 if (!areaNodes) { |
|
758 return; |
|
759 } |
|
760 |
|
761 let area = gAreas.get(aArea); |
|
762 let isToolbar = area.get("type") == CustomizableUI.TYPE_TOOLBAR; |
|
763 let isOverflowable = isToolbar && area.get("overflowable"); |
|
764 let showInPrivateBrowsing = gPalette.has(aWidgetId) |
|
765 ? gPalette.get(aWidgetId).showInPrivateBrowsing |
|
766 : true; |
|
767 |
|
768 for (let areaNode of areaNodes) { |
|
769 let window = areaNode.ownerDocument.defaultView; |
|
770 if (!showInPrivateBrowsing && |
|
771 PrivateBrowsingUtils.isWindowPrivate(window)) { |
|
772 continue; |
|
773 } |
|
774 |
|
775 let widgetNode = window.document.getElementById(aWidgetId); |
|
776 if (!widgetNode) { |
|
777 INFO("Widget not found, unable to remove"); |
|
778 continue; |
|
779 } |
|
780 let container = areaNode.customizationTarget; |
|
781 if (isOverflowable) { |
|
782 container = areaNode.overflowable.getContainerFor(widgetNode); |
|
783 } |
|
784 |
|
785 this.notifyListeners("onWidgetBeforeDOMChange", widgetNode, null, container, true); |
|
786 |
|
787 // We remove location attributes here to make sure they're gone too when a |
|
788 // widget is removed from a toolbar to the palette. See bug 930950. |
|
789 this.removeLocationAttributes(widgetNode); |
|
790 // We also need to remove the panel context menu if it's there: |
|
791 this.ensureButtonContextMenu(widgetNode); |
|
792 widgetNode.removeAttribute("wrap"); |
|
793 if (gPalette.has(aWidgetId) || this.isSpecialWidget(aWidgetId)) { |
|
794 container.removeChild(widgetNode); |
|
795 } else { |
|
796 areaNode.toolbox.palette.appendChild(widgetNode); |
|
797 } |
|
798 this.notifyListeners("onWidgetAfterDOMChange", widgetNode, null, container, true); |
|
799 |
|
800 if (isToolbar) { |
|
801 areaNode.setAttribute("currentset", gPlacements.get(aArea).join(',')); |
|
802 } |
|
803 |
|
804 let windowCache = gSingleWrapperCache.get(window); |
|
805 if (windowCache) { |
|
806 windowCache.delete(aWidgetId); |
|
807 } |
|
808 } |
|
809 if (!gResetting) { |
|
810 this._clearPreviousUIState(); |
|
811 } |
|
812 }, |
|
813 |
|
814 onWidgetMoved: function(aWidgetId, aArea, aOldPosition, aNewPosition) { |
|
815 this.insertNode(aWidgetId, aArea, aNewPosition); |
|
816 if (!gResetting) { |
|
817 this._clearPreviousUIState(); |
|
818 } |
|
819 }, |
|
820 |
|
821 onCustomizeEnd: function(aWindow) { |
|
822 this._clearPreviousUIState(); |
|
823 }, |
|
824 |
|
825 registerBuildArea: function(aArea, aNode) { |
|
826 // We ensure that the window is registered to have its customization data |
|
827 // cleaned up when unloading. |
|
828 let window = aNode.ownerDocument.defaultView; |
|
829 if (window.closed) { |
|
830 return; |
|
831 } |
|
832 this.registerBuildWindow(window); |
|
833 |
|
834 // Also register this build area's toolbox. |
|
835 if (aNode.toolbox) { |
|
836 gBuildWindows.get(window).add(aNode.toolbox); |
|
837 } |
|
838 |
|
839 if (!gBuildAreas.has(aArea)) { |
|
840 gBuildAreas.set(aArea, new Set()); |
|
841 } |
|
842 |
|
843 gBuildAreas.get(aArea).add(aNode); |
|
844 |
|
845 // Give a class to all customize targets to be used for styling in Customize Mode |
|
846 let customizableNode = this.getCustomizeTargetForArea(aArea, window); |
|
847 customizableNode.classList.add("customization-target"); |
|
848 }, |
|
849 |
|
850 registerBuildWindow: function(aWindow) { |
|
851 if (!gBuildWindows.has(aWindow)) { |
|
852 gBuildWindows.set(aWindow, new Set()); |
|
853 |
|
854 aWindow.addEventListener("unload", this); |
|
855 aWindow.addEventListener("command", this, true); |
|
856 |
|
857 this.notifyListeners("onWindowOpened", aWindow); |
|
858 } |
|
859 }, |
|
860 |
|
861 unregisterBuildWindow: function(aWindow) { |
|
862 aWindow.removeEventListener("unload", this); |
|
863 aWindow.removeEventListener("command", this, true); |
|
864 gPanelsForWindow.delete(aWindow); |
|
865 gBuildWindows.delete(aWindow); |
|
866 gSingleWrapperCache.delete(aWindow); |
|
867 let document = aWindow.document; |
|
868 |
|
869 for (let [areaId, areaNodes] of gBuildAreas) { |
|
870 let areaProperties = gAreas.get(areaId); |
|
871 for (let node of areaNodes) { |
|
872 if (node.ownerDocument == document) { |
|
873 this.notifyListeners("onAreaNodeUnregistered", areaId, node.customizationTarget, |
|
874 CustomizableUI.REASON_WINDOW_CLOSED); |
|
875 if (areaProperties.has("overflowable")) { |
|
876 node.overflowable.uninit(); |
|
877 node.overflowable = null; |
|
878 } |
|
879 areaNodes.delete(node); |
|
880 } |
|
881 } |
|
882 } |
|
883 |
|
884 for (let [,widget] of gPalette) { |
|
885 widget.instances.delete(document); |
|
886 this.notifyListeners("onWidgetInstanceRemoved", widget.id, document); |
|
887 } |
|
888 |
|
889 for (let [area, areaMap] of gPendingBuildAreas) { |
|
890 let toDelete = []; |
|
891 for (let [areaNode, ] of areaMap) { |
|
892 if (areaNode.ownerDocument == document) { |
|
893 toDelete.push(areaNode); |
|
894 } |
|
895 } |
|
896 for (let areaNode of toDelete) { |
|
897 areaMap.delete(toDelete); |
|
898 } |
|
899 } |
|
900 |
|
901 this.notifyListeners("onWindowClosed", aWindow); |
|
902 }, |
|
903 |
|
904 setLocationAttributes: function(aNode, aArea) { |
|
905 let props = gAreas.get(aArea); |
|
906 if (!props) { |
|
907 throw new Error("Expected area " + aArea + " to have a properties Map " + |
|
908 "associated with it."); |
|
909 } |
|
910 |
|
911 aNode.setAttribute("cui-areatype", props.get("type") || ""); |
|
912 let anchor = props.get("anchor"); |
|
913 if (anchor) { |
|
914 aNode.setAttribute("cui-anchorid", anchor); |
|
915 } else { |
|
916 aNode.removeAttribute("cui-anchorid"); |
|
917 } |
|
918 }, |
|
919 |
|
920 removeLocationAttributes: function(aNode) { |
|
921 aNode.removeAttribute("cui-areatype"); |
|
922 aNode.removeAttribute("cui-anchorid"); |
|
923 }, |
|
924 |
|
925 insertNode: function(aWidgetId, aArea, aPosition, isNew) { |
|
926 let areaNodes = gBuildAreas.get(aArea); |
|
927 if (!areaNodes) { |
|
928 return; |
|
929 } |
|
930 |
|
931 let placements = gPlacements.get(aArea); |
|
932 if (!placements) { |
|
933 ERROR("Could not find any placements for " + aArea + |
|
934 " when moving a widget."); |
|
935 return; |
|
936 } |
|
937 |
|
938 // Go through each of the nodes associated with this area and move the |
|
939 // widget to the requested location. |
|
940 for (let areaNode of areaNodes) { |
|
941 this.insertNodeInWindow(aWidgetId, areaNode, isNew); |
|
942 } |
|
943 }, |
|
944 |
|
945 insertNodeInWindow: function(aWidgetId, aAreaNode, isNew) { |
|
946 let window = aAreaNode.ownerDocument.defaultView; |
|
947 let showInPrivateBrowsing = gPalette.has(aWidgetId) |
|
948 ? gPalette.get(aWidgetId).showInPrivateBrowsing |
|
949 : true; |
|
950 |
|
951 if (!showInPrivateBrowsing && PrivateBrowsingUtils.isWindowPrivate(window)) { |
|
952 return; |
|
953 } |
|
954 |
|
955 let [, widgetNode] = this.getWidgetNode(aWidgetId, window); |
|
956 if (!widgetNode) { |
|
957 ERROR("Widget '" + aWidgetId + "' not found, unable to move"); |
|
958 return; |
|
959 } |
|
960 |
|
961 let areaId = aAreaNode.id; |
|
962 if (isNew) { |
|
963 this.ensureButtonContextMenu(widgetNode, aAreaNode); |
|
964 if (widgetNode.localName == "toolbarbutton" && areaId == CustomizableUI.AREA_PANEL) { |
|
965 widgetNode.setAttribute("wrap", "true"); |
|
966 } |
|
967 } |
|
968 |
|
969 let [insertionContainer, nextNode] = this.findInsertionPoints(widgetNode, aAreaNode); |
|
970 this.insertWidgetBefore(widgetNode, nextNode, insertionContainer, areaId); |
|
971 |
|
972 if (gAreas.get(areaId).get("type") == CustomizableUI.TYPE_TOOLBAR) { |
|
973 aAreaNode.setAttribute("currentset", gPlacements.get(areaId).join(',')); |
|
974 } |
|
975 }, |
|
976 |
|
977 findInsertionPoints: function(aNode, aAreaNode) { |
|
978 let areaId = aAreaNode.id; |
|
979 let props = gAreas.get(areaId); |
|
980 |
|
981 // For overflowable toolbars, rely on them (because the work is more complicated): |
|
982 if (props.get("type") == CustomizableUI.TYPE_TOOLBAR && props.get("overflowable")) { |
|
983 return aAreaNode.overflowable.findOverflowedInsertionPoints(aNode); |
|
984 } |
|
985 |
|
986 let container = aAreaNode.customizationTarget; |
|
987 let placements = gPlacements.get(areaId); |
|
988 let nodeIndex = placements.indexOf(aNode.id); |
|
989 |
|
990 while (++nodeIndex < placements.length) { |
|
991 let nextNodeId = placements[nodeIndex]; |
|
992 let nextNode = container.getElementsByAttribute("id", nextNodeId).item(0); |
|
993 |
|
994 if (nextNode) { |
|
995 return [container, nextNode]; |
|
996 } |
|
997 } |
|
998 |
|
999 return [container, null]; |
|
1000 }, |
|
1001 |
|
1002 insertWidgetBefore: function(aNode, aNextNode, aContainer, aArea) { |
|
1003 this.notifyListeners("onWidgetBeforeDOMChange", aNode, aNextNode, aContainer); |
|
1004 this.setLocationAttributes(aNode, aArea); |
|
1005 aContainer.insertBefore(aNode, aNextNode); |
|
1006 this.notifyListeners("onWidgetAfterDOMChange", aNode, aNextNode, aContainer); |
|
1007 }, |
|
1008 |
|
1009 handleEvent: function(aEvent) { |
|
1010 switch (aEvent.type) { |
|
1011 case "command": |
|
1012 if (!this._originalEventInPanel(aEvent)) { |
|
1013 break; |
|
1014 } |
|
1015 aEvent = aEvent.sourceEvent; |
|
1016 // Fall through |
|
1017 case "click": |
|
1018 case "keypress": |
|
1019 this.maybeAutoHidePanel(aEvent); |
|
1020 break; |
|
1021 case "unload": |
|
1022 this.unregisterBuildWindow(aEvent.currentTarget); |
|
1023 break; |
|
1024 } |
|
1025 }, |
|
1026 |
|
1027 _originalEventInPanel: function(aEvent) { |
|
1028 let e = aEvent.sourceEvent; |
|
1029 if (!e) { |
|
1030 return false; |
|
1031 } |
|
1032 let node = this._getPanelForNode(e.target); |
|
1033 if (!node) { |
|
1034 return false; |
|
1035 } |
|
1036 let win = e.view; |
|
1037 let panels = gPanelsForWindow.get(win); |
|
1038 return !!panels && panels.has(node); |
|
1039 }, |
|
1040 |
|
1041 isSpecialWidget: function(aId) { |
|
1042 return (aId.startsWith(kSpecialWidgetPfx) || |
|
1043 aId.startsWith("separator") || |
|
1044 aId.startsWith("spring") || |
|
1045 aId.startsWith("spacer")); |
|
1046 }, |
|
1047 |
|
1048 ensureSpecialWidgetId: function(aId) { |
|
1049 let nodeType = aId.match(/spring|spacer|separator/)[0]; |
|
1050 // If the ID we were passed isn't a generated one, generate one now: |
|
1051 if (nodeType == aId) { |
|
1052 // Ids are differentiated through a unique count suffix. |
|
1053 return kSpecialWidgetPfx + aId + (++gNewElementCount); |
|
1054 } |
|
1055 return aId; |
|
1056 }, |
|
1057 |
|
1058 createSpecialWidget: function(aId, aDocument) { |
|
1059 let nodeName = "toolbar" + aId.match(/spring|spacer|separator/)[0]; |
|
1060 let node = aDocument.createElementNS(kNSXUL, nodeName); |
|
1061 node.id = this.ensureSpecialWidgetId(aId); |
|
1062 if (nodeName == "toolbarspring") { |
|
1063 node.flex = 1; |
|
1064 } |
|
1065 return node; |
|
1066 }, |
|
1067 |
|
1068 /* Find a XUL-provided widget in a window. Don't try to use this |
|
1069 * for an API-provided widget or a special widget. |
|
1070 */ |
|
1071 findWidgetInWindow: function(aId, aWindow) { |
|
1072 if (!gBuildWindows.has(aWindow)) { |
|
1073 throw new Error("Build window not registered"); |
|
1074 } |
|
1075 |
|
1076 if (!aId) { |
|
1077 ERROR("findWidgetInWindow was passed an empty string."); |
|
1078 return null; |
|
1079 } |
|
1080 |
|
1081 let document = aWindow.document; |
|
1082 |
|
1083 // look for a node with the same id, as the node may be |
|
1084 // in a different toolbar. |
|
1085 let node = document.getElementById(aId); |
|
1086 if (node) { |
|
1087 let parent = node.parentNode; |
|
1088 while (parent && !(parent.customizationTarget || |
|
1089 parent == aWindow.gNavToolbox.palette)) { |
|
1090 parent = parent.parentNode; |
|
1091 } |
|
1092 |
|
1093 if (parent) { |
|
1094 let nodeInArea = node.parentNode.localName == "toolbarpaletteitem" ? |
|
1095 node.parentNode : node; |
|
1096 // Check if we're in a customization target, or in the palette: |
|
1097 if ((parent.customizationTarget == nodeInArea.parentNode && |
|
1098 gBuildWindows.get(aWindow).has(parent.toolbox)) || |
|
1099 aWindow.gNavToolbox.palette == nodeInArea.parentNode) { |
|
1100 // Normalize the removable attribute. For backwards compat, if |
|
1101 // the widget is not located in a toolbox palette then absence |
|
1102 // of the "removable" attribute means it is not removable. |
|
1103 if (!node.hasAttribute("removable")) { |
|
1104 // If we first see this in customization mode, it may be in the |
|
1105 // customization palette instead of the toolbox palette. |
|
1106 node.setAttribute("removable", !parent.customizationTarget); |
|
1107 } |
|
1108 return node; |
|
1109 } |
|
1110 } |
|
1111 } |
|
1112 |
|
1113 let toolboxes = gBuildWindows.get(aWindow); |
|
1114 for (let toolbox of toolboxes) { |
|
1115 if (toolbox.palette) { |
|
1116 // Attempt to locate a node with a matching ID within |
|
1117 // the palette. |
|
1118 let node = toolbox.palette.getElementsByAttribute("id", aId)[0]; |
|
1119 if (node) { |
|
1120 // Normalize the removable attribute. For backwards compat, this |
|
1121 // is optional if the widget is located in the toolbox palette, |
|
1122 // and defaults to *true*, unlike if it was located elsewhere. |
|
1123 if (!node.hasAttribute("removable")) { |
|
1124 node.setAttribute("removable", true); |
|
1125 } |
|
1126 return node; |
|
1127 } |
|
1128 } |
|
1129 } |
|
1130 return null; |
|
1131 }, |
|
1132 |
|
1133 buildWidget: function(aDocument, aWidget) { |
|
1134 if (typeof aWidget == "string") { |
|
1135 aWidget = gPalette.get(aWidget); |
|
1136 } |
|
1137 if (!aWidget) { |
|
1138 throw new Error("buildWidget was passed a non-widget to build."); |
|
1139 } |
|
1140 |
|
1141 LOG("Building " + aWidget.id + " of type " + aWidget.type); |
|
1142 |
|
1143 let node; |
|
1144 if (aWidget.type == "custom") { |
|
1145 if (aWidget.onBuild) { |
|
1146 node = aWidget.onBuild(aDocument); |
|
1147 } |
|
1148 if (!node || !(node instanceof aDocument.defaultView.XULElement)) |
|
1149 ERROR("Custom widget with id " + aWidget.id + " does not return a valid node"); |
|
1150 } |
|
1151 else { |
|
1152 if (aWidget.onBeforeCreated) { |
|
1153 aWidget.onBeforeCreated(aDocument); |
|
1154 } |
|
1155 node = aDocument.createElementNS(kNSXUL, "toolbarbutton"); |
|
1156 |
|
1157 node.setAttribute("id", aWidget.id); |
|
1158 node.setAttribute("widget-id", aWidget.id); |
|
1159 node.setAttribute("widget-type", aWidget.type); |
|
1160 if (aWidget.disabled) { |
|
1161 node.setAttribute("disabled", true); |
|
1162 } |
|
1163 node.setAttribute("removable", aWidget.removable); |
|
1164 node.setAttribute("overflows", aWidget.overflows); |
|
1165 node.setAttribute("label", this.getLocalizedProperty(aWidget, "label")); |
|
1166 let additionalTooltipArguments = []; |
|
1167 if (aWidget.shortcutId) { |
|
1168 let keyEl = aDocument.getElementById(aWidget.shortcutId); |
|
1169 if (keyEl) { |
|
1170 additionalTooltipArguments.push(ShortcutUtils.prettifyShortcut(keyEl)); |
|
1171 } else { |
|
1172 ERROR("Key element with id '" + aWidget.shortcutId + "' for widget '" + aWidget.id + |
|
1173 "' not found!"); |
|
1174 } |
|
1175 } |
|
1176 |
|
1177 let tooltip = this.getLocalizedProperty(aWidget, "tooltiptext", additionalTooltipArguments); |
|
1178 node.setAttribute("tooltiptext", tooltip); |
|
1179 node.setAttribute("class", "toolbarbutton-1 chromeclass-toolbar-additional"); |
|
1180 |
|
1181 let commandHandler = this.handleWidgetCommand.bind(this, aWidget, node); |
|
1182 node.addEventListener("command", commandHandler, false); |
|
1183 let clickHandler = this.handleWidgetClick.bind(this, aWidget, node); |
|
1184 node.addEventListener("click", clickHandler, false); |
|
1185 |
|
1186 // If the widget has a view, and has view showing / hiding listeners, |
|
1187 // hook those up to this widget. |
|
1188 if (aWidget.type == "view") { |
|
1189 LOG("Widget " + aWidget.id + " has a view. Auto-registering event handlers."); |
|
1190 let viewNode = aDocument.getElementById(aWidget.viewId); |
|
1191 |
|
1192 if (viewNode) { |
|
1193 // PanelUI relies on the .PanelUI-subView class to be able to show only |
|
1194 // one sub-view at a time. |
|
1195 viewNode.classList.add("PanelUI-subView"); |
|
1196 |
|
1197 for (let eventName of kSubviewEvents) { |
|
1198 let handler = "on" + eventName; |
|
1199 if (typeof aWidget[handler] == "function") { |
|
1200 viewNode.addEventListener(eventName, aWidget[handler], false); |
|
1201 } |
|
1202 } |
|
1203 |
|
1204 LOG("Widget " + aWidget.id + " showing and hiding event handlers set."); |
|
1205 } else { |
|
1206 ERROR("Could not find the view node with id: " + aWidget.viewId + |
|
1207 ", for widget: " + aWidget.id + "."); |
|
1208 } |
|
1209 } |
|
1210 |
|
1211 if (aWidget.onCreated) { |
|
1212 aWidget.onCreated(node); |
|
1213 } |
|
1214 } |
|
1215 |
|
1216 aWidget.instances.set(aDocument, node); |
|
1217 return node; |
|
1218 }, |
|
1219 |
|
1220 getLocalizedProperty: function(aWidget, aProp, aFormatArgs, aDef) { |
|
1221 if (typeof aWidget == "string") { |
|
1222 aWidget = gPalette.get(aWidget); |
|
1223 } |
|
1224 if (!aWidget) { |
|
1225 throw new Error("getLocalizedProperty was passed a non-widget to work with."); |
|
1226 } |
|
1227 let def, name; |
|
1228 // Let widgets pass their own string identifiers or strings, so that |
|
1229 // we can use strings which aren't the default (in case string ids change) |
|
1230 // and so that non-builtin-widgets can also provide labels, tooltips, etc. |
|
1231 if (aWidget[aProp]) { |
|
1232 name = aWidget[aProp]; |
|
1233 // By using this as the default, if a widget provides a full string rather |
|
1234 // than a string ID for localization, we will fall back to that string |
|
1235 // and return that. |
|
1236 def = aDef || name; |
|
1237 } else { |
|
1238 name = aWidget.id + "." + aProp; |
|
1239 def = aDef || ""; |
|
1240 } |
|
1241 try { |
|
1242 if (Array.isArray(aFormatArgs) && aFormatArgs.length) { |
|
1243 return gWidgetsBundle.formatStringFromName(name, aFormatArgs, |
|
1244 aFormatArgs.length) || def; |
|
1245 } |
|
1246 return gWidgetsBundle.GetStringFromName(name) || def; |
|
1247 } catch(ex) { |
|
1248 if (!def) { |
|
1249 ERROR("Could not localize property '" + name + "'."); |
|
1250 } |
|
1251 } |
|
1252 return def; |
|
1253 }, |
|
1254 |
|
1255 handleWidgetCommand: function(aWidget, aNode, aEvent) { |
|
1256 LOG("handleWidgetCommand"); |
|
1257 |
|
1258 if (aWidget.type == "button") { |
|
1259 if (aWidget.onCommand) { |
|
1260 try { |
|
1261 aWidget.onCommand.call(null, aEvent); |
|
1262 } catch (e) { |
|
1263 ERROR(e); |
|
1264 } |
|
1265 } else { |
|
1266 //XXXunf Need to think this through more, and formalize. |
|
1267 Services.obs.notifyObservers(aNode, |
|
1268 "customizedui-widget-command", |
|
1269 aWidget.id); |
|
1270 } |
|
1271 } else if (aWidget.type == "view") { |
|
1272 let ownerWindow = aNode.ownerDocument.defaultView; |
|
1273 let area = this.getPlacementOfWidget(aNode.id).area; |
|
1274 let anchor = aNode; |
|
1275 if (area != CustomizableUI.AREA_PANEL) { |
|
1276 let wrapper = this.wrapWidget(aWidget.id).forWindow(ownerWindow); |
|
1277 if (wrapper && wrapper.anchor) { |
|
1278 this.hidePanelForNode(aNode); |
|
1279 anchor = wrapper.anchor; |
|
1280 } |
|
1281 } |
|
1282 ownerWindow.PanelUI.showSubView(aWidget.viewId, anchor, area); |
|
1283 } |
|
1284 }, |
|
1285 |
|
1286 handleWidgetClick: function(aWidget, aNode, aEvent) { |
|
1287 LOG("handleWidgetClick"); |
|
1288 if (aWidget.onClick) { |
|
1289 try { |
|
1290 aWidget.onClick.call(null, aEvent); |
|
1291 } catch(e) { |
|
1292 Cu.reportError(e); |
|
1293 } |
|
1294 } else { |
|
1295 //XXXunf Need to think this through more, and formalize. |
|
1296 Services.obs.notifyObservers(aNode, "customizedui-widget-click", aWidget.id); |
|
1297 } |
|
1298 }, |
|
1299 |
|
1300 _getPanelForNode: function(aNode) { |
|
1301 let panel = aNode; |
|
1302 while (panel && panel.localName != "panel") |
|
1303 panel = panel.parentNode; |
|
1304 return panel; |
|
1305 }, |
|
1306 |
|
1307 /* |
|
1308 * If people put things in the panel which need more than single-click interaction, |
|
1309 * we don't want to close it. Right now we check for text inputs and menu buttons. |
|
1310 * We also check for being outside of any toolbaritem/toolbarbutton, ie on a blank |
|
1311 * part of the menu. |
|
1312 */ |
|
1313 _isOnInteractiveElement: function(aEvent) { |
|
1314 function getMenuPopupForDescendant(aNode) { |
|
1315 let lastPopup = null; |
|
1316 while (aNode && aNode.parentNode && |
|
1317 aNode.parentNode.localName.startsWith("menu")) { |
|
1318 lastPopup = aNode.localName == "menupopup" ? aNode : lastPopup; |
|
1319 aNode = aNode.parentNode; |
|
1320 } |
|
1321 return lastPopup; |
|
1322 } |
|
1323 |
|
1324 let target = aEvent.originalTarget; |
|
1325 let panel = this._getPanelForNode(aEvent.currentTarget); |
|
1326 // This can happen in e.g. customize mode. If there's no panel, |
|
1327 // there's clearly nothing for us to close; pretend we're interactive. |
|
1328 if (!panel) { |
|
1329 return true; |
|
1330 } |
|
1331 // We keep track of: |
|
1332 // whether we're in an input container (text field) |
|
1333 let inInput = false; |
|
1334 // whether we're in a popup/context menu |
|
1335 let inMenu = false; |
|
1336 // whether we're in a toolbarbutton/toolbaritem |
|
1337 let inItem = false; |
|
1338 // whether the current menuitem has a valid closemenu attribute |
|
1339 let menuitemCloseMenu = "auto"; |
|
1340 // whether the toolbarbutton/item has a valid closemenu attribute. |
|
1341 let closemenu = "auto"; |
|
1342 |
|
1343 // While keeping track of that, we go from the original target back up, |
|
1344 // to the panel if we have to. We bail as soon as we find an input, |
|
1345 // a toolbarbutton/item, or the panel: |
|
1346 while (true && target) { |
|
1347 let tagName = target.localName; |
|
1348 inInput = tagName == "input" || tagName == "textbox"; |
|
1349 inItem = tagName == "toolbaritem" || tagName == "toolbarbutton"; |
|
1350 let isMenuItem = tagName == "menuitem"; |
|
1351 inMenu = inMenu || isMenuItem; |
|
1352 if (inItem && target.hasAttribute("closemenu")) { |
|
1353 let closemenuVal = target.getAttribute("closemenu"); |
|
1354 closemenu = (closemenuVal == "single" || closemenuVal == "none") ? |
|
1355 closemenuVal : "auto"; |
|
1356 } |
|
1357 |
|
1358 if (isMenuItem && target.hasAttribute("closemenu")) { |
|
1359 let closemenuVal = target.getAttribute("closemenu"); |
|
1360 menuitemCloseMenu = (closemenuVal == "single" || closemenuVal == "none") ? |
|
1361 closemenuVal : "auto"; |
|
1362 } |
|
1363 // This isn't in the loop condition because we want to break before |
|
1364 // changing |target| if any of these conditions are true |
|
1365 if (inInput || inItem || target == panel) { |
|
1366 break; |
|
1367 } |
|
1368 // We need specific code for popups: the item on which they were invoked |
|
1369 // isn't necessarily in their parentNode chain: |
|
1370 if (isMenuItem) { |
|
1371 let topmostMenuPopup = getMenuPopupForDescendant(target); |
|
1372 target = (topmostMenuPopup && topmostMenuPopup.triggerNode) || |
|
1373 target.parentNode; |
|
1374 } else { |
|
1375 target = target.parentNode; |
|
1376 } |
|
1377 } |
|
1378 // If the user clicked a menu item... |
|
1379 if (inMenu) { |
|
1380 // We care if we're in an input also, |
|
1381 // or if the user specified closemenu!="auto": |
|
1382 if (inInput || menuitemCloseMenu != "auto") { |
|
1383 return true; |
|
1384 } |
|
1385 // Otherwise, we're probably fine to close the panel |
|
1386 return false; |
|
1387 } |
|
1388 // If we're not in a menu, and we *are* in a type="menu" toolbarbutton, |
|
1389 // we'll now interact with the menu |
|
1390 if (inItem && target.getAttribute("type") == "menu") { |
|
1391 return true; |
|
1392 } |
|
1393 // If we're not in a menu, and we *are* in a type="menu-button" toolbarbutton, |
|
1394 // it depends whether we're in the dropmarker or the 'real' button: |
|
1395 if (inItem && target.getAttribute("type") == "menu-button") { |
|
1396 // 'real' button (which has a single action): |
|
1397 if (target.getAttribute("anonid") == "button") { |
|
1398 return closemenu != "none"; |
|
1399 } |
|
1400 // otherwise, this is the outer button, and the user will now |
|
1401 // interact with the menu: |
|
1402 return true; |
|
1403 } |
|
1404 return inInput || !inItem; |
|
1405 }, |
|
1406 |
|
1407 hidePanelForNode: function(aNode) { |
|
1408 let panel = this._getPanelForNode(aNode); |
|
1409 if (panel) { |
|
1410 panel.hidePopup(); |
|
1411 } |
|
1412 }, |
|
1413 |
|
1414 maybeAutoHidePanel: function(aEvent) { |
|
1415 if (aEvent.type == "keypress") { |
|
1416 if (aEvent.keyCode != aEvent.DOM_VK_RETURN) { |
|
1417 return; |
|
1418 } |
|
1419 // If the user hit enter/return, we don't check preventDefault - it makes sense |
|
1420 // that this was prevented, but we probably still want to close the panel. |
|
1421 // If consumers don't want this to happen, they should specify the closemenu |
|
1422 // attribute. |
|
1423 |
|
1424 } else if (aEvent.type != "command") { // mouse events: |
|
1425 if (aEvent.defaultPrevented || aEvent.button != 0) { |
|
1426 return; |
|
1427 } |
|
1428 let isInteractive = this._isOnInteractiveElement(aEvent); |
|
1429 LOG("maybeAutoHidePanel: interactive ? " + isInteractive); |
|
1430 if (isInteractive) { |
|
1431 return; |
|
1432 } |
|
1433 } |
|
1434 |
|
1435 // We can't use event.target because we might have passed a panelview |
|
1436 // anonymous content boundary as well, and so target points to the |
|
1437 // panelmultiview in that case. Unfortunately, this means we get |
|
1438 // anonymous child nodes instead of the real ones, so looking for the |
|
1439 // 'stoooop, don't close me' attributes is more involved. |
|
1440 let target = aEvent.originalTarget; |
|
1441 let closemenu = "auto"; |
|
1442 let widgetType = "button"; |
|
1443 while (target.parentNode && target.localName != "panel") { |
|
1444 closemenu = target.getAttribute("closemenu"); |
|
1445 widgetType = target.getAttribute("widget-type"); |
|
1446 if (closemenu == "none" || closemenu == "single" || |
|
1447 widgetType == "view") { |
|
1448 break; |
|
1449 } |
|
1450 target = target.parentNode; |
|
1451 } |
|
1452 if (closemenu == "none" || widgetType == "view") { |
|
1453 return; |
|
1454 } |
|
1455 |
|
1456 if (closemenu == "single") { |
|
1457 let panel = this._getPanelForNode(target); |
|
1458 let multiview = panel.querySelector("panelmultiview"); |
|
1459 if (multiview.showingSubView) { |
|
1460 multiview.showMainView(); |
|
1461 return; |
|
1462 } |
|
1463 } |
|
1464 |
|
1465 // If we get here, we can actually hide the popup: |
|
1466 this.hidePanelForNode(aEvent.target); |
|
1467 }, |
|
1468 |
|
1469 getUnusedWidgets: function(aWindowPalette) { |
|
1470 let window = aWindowPalette.ownerDocument.defaultView; |
|
1471 let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(window); |
|
1472 // We use a Set because there can be overlap between the widgets in |
|
1473 // gPalette and the items in the palette, especially after the first |
|
1474 // customization, since programmatically generated widgets will remain |
|
1475 // in the toolbox palette. |
|
1476 let widgets = new Set(); |
|
1477 |
|
1478 // It's possible that some widgets have been defined programmatically and |
|
1479 // have not been overlayed into the palette. We can find those inside |
|
1480 // gPalette. |
|
1481 for (let [id, widget] of gPalette) { |
|
1482 if (!widget.currentArea) { |
|
1483 if (widget.showInPrivateBrowsing || !isWindowPrivate) { |
|
1484 widgets.add(id); |
|
1485 } |
|
1486 } |
|
1487 } |
|
1488 |
|
1489 LOG("Iterating the actual nodes of the window palette"); |
|
1490 for (let node of aWindowPalette.children) { |
|
1491 LOG("In palette children: " + node.id); |
|
1492 if (node.id && !this.getPlacementOfWidget(node.id)) { |
|
1493 widgets.add(node.id); |
|
1494 } |
|
1495 } |
|
1496 |
|
1497 return [...widgets]; |
|
1498 }, |
|
1499 |
|
1500 getPlacementOfWidget: function(aWidgetId, aOnlyRegistered, aDeadAreas) { |
|
1501 if (aOnlyRegistered && !this.widgetExists(aWidgetId)) { |
|
1502 return null; |
|
1503 } |
|
1504 |
|
1505 for (let [area, placements] of gPlacements) { |
|
1506 if (!gAreas.has(area) && !aDeadAreas) { |
|
1507 continue; |
|
1508 } |
|
1509 let index = placements.indexOf(aWidgetId); |
|
1510 if (index != -1) { |
|
1511 return { area: area, position: index }; |
|
1512 } |
|
1513 } |
|
1514 |
|
1515 return null; |
|
1516 }, |
|
1517 |
|
1518 widgetExists: function(aWidgetId) { |
|
1519 if (gPalette.has(aWidgetId) || this.isSpecialWidget(aWidgetId)) { |
|
1520 return true; |
|
1521 } |
|
1522 |
|
1523 // Destroyed API widgets are in gSeenWidgets, but not in gPalette: |
|
1524 if (gSeenWidgets.has(aWidgetId)) { |
|
1525 return false; |
|
1526 } |
|
1527 |
|
1528 // We're assuming XUL widgets always exist, as it's much harder to check, |
|
1529 // and checking would be much more error prone. |
|
1530 return true; |
|
1531 }, |
|
1532 |
|
1533 addWidgetToArea: function(aWidgetId, aArea, aPosition, aInitialAdd) { |
|
1534 if (!gAreas.has(aArea)) { |
|
1535 throw new Error("Unknown customization area: " + aArea); |
|
1536 } |
|
1537 |
|
1538 // Hack: don't want special widgets in the panel (need to check here as well |
|
1539 // as in canWidgetMoveToArea because the menu panel is lazy): |
|
1540 if (gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL && |
|
1541 this.isSpecialWidget(aWidgetId)) { |
|
1542 return; |
|
1543 } |
|
1544 |
|
1545 // If this is a lazy area that hasn't been restored yet, we can't yet modify |
|
1546 // it - would would at least like to add to it. So we keep track of it in |
|
1547 // gFuturePlacements, and use that to add it when restoring the area. We |
|
1548 // throw away aPosition though, as that can only be bogus if the area hasn't |
|
1549 // yet been restorted (caller can't possibly know where its putting the |
|
1550 // widget in relation to other widgets). |
|
1551 if (this.isAreaLazy(aArea)) { |
|
1552 gFuturePlacements.get(aArea).add(aWidgetId); |
|
1553 return; |
|
1554 } |
|
1555 |
|
1556 if (this.isSpecialWidget(aWidgetId)) { |
|
1557 aWidgetId = this.ensureSpecialWidgetId(aWidgetId); |
|
1558 } |
|
1559 |
|
1560 let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true); |
|
1561 if (oldPlacement && oldPlacement.area == aArea) { |
|
1562 this.moveWidgetWithinArea(aWidgetId, aPosition); |
|
1563 return; |
|
1564 } |
|
1565 |
|
1566 // Do nothing if the widget is not allowed to move to the target area. |
|
1567 if (!this.canWidgetMoveToArea(aWidgetId, aArea)) { |
|
1568 return; |
|
1569 } |
|
1570 |
|
1571 if (oldPlacement) { |
|
1572 this.removeWidgetFromArea(aWidgetId); |
|
1573 } |
|
1574 |
|
1575 if (!gPlacements.has(aArea)) { |
|
1576 gPlacements.set(aArea, [aWidgetId]); |
|
1577 aPosition = 0; |
|
1578 } else { |
|
1579 let placements = gPlacements.get(aArea); |
|
1580 if (typeof aPosition != "number") { |
|
1581 aPosition = placements.length; |
|
1582 } |
|
1583 if (aPosition < 0) { |
|
1584 aPosition = 0; |
|
1585 } |
|
1586 placements.splice(aPosition, 0, aWidgetId); |
|
1587 } |
|
1588 |
|
1589 let widget = gPalette.get(aWidgetId); |
|
1590 if (widget) { |
|
1591 widget.currentArea = aArea; |
|
1592 widget.currentPosition = aPosition; |
|
1593 } |
|
1594 |
|
1595 // We initially set placements with addWidgetToArea, so in that case |
|
1596 // we don't consider the area "dirtied". |
|
1597 if (!aInitialAdd) { |
|
1598 gDirtyAreaCache.add(aArea); |
|
1599 } |
|
1600 |
|
1601 gDirty = true; |
|
1602 this.saveState(); |
|
1603 |
|
1604 this.notifyListeners("onWidgetAdded", aWidgetId, aArea, aPosition); |
|
1605 }, |
|
1606 |
|
1607 removeWidgetFromArea: function(aWidgetId) { |
|
1608 let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true); |
|
1609 if (!oldPlacement) { |
|
1610 return; |
|
1611 } |
|
1612 |
|
1613 if (!this.isWidgetRemovable(aWidgetId)) { |
|
1614 return; |
|
1615 } |
|
1616 |
|
1617 let placements = gPlacements.get(oldPlacement.area); |
|
1618 let position = placements.indexOf(aWidgetId); |
|
1619 if (position != -1) { |
|
1620 placements.splice(position, 1); |
|
1621 } |
|
1622 |
|
1623 let widget = gPalette.get(aWidgetId); |
|
1624 if (widget) { |
|
1625 widget.currentArea = null; |
|
1626 widget.currentPosition = null; |
|
1627 } |
|
1628 |
|
1629 gDirty = true; |
|
1630 this.saveState(); |
|
1631 gDirtyAreaCache.add(oldPlacement.area); |
|
1632 |
|
1633 this.notifyListeners("onWidgetRemoved", aWidgetId, oldPlacement.area); |
|
1634 }, |
|
1635 |
|
1636 moveWidgetWithinArea: function(aWidgetId, aPosition) { |
|
1637 let oldPlacement = this.getPlacementOfWidget(aWidgetId); |
|
1638 if (!oldPlacement) { |
|
1639 return; |
|
1640 } |
|
1641 |
|
1642 let placements = gPlacements.get(oldPlacement.area); |
|
1643 if (typeof aPosition != "number") { |
|
1644 aPosition = placements.length; |
|
1645 } else if (aPosition < 0) { |
|
1646 aPosition = 0; |
|
1647 } else if (aPosition > placements.length) { |
|
1648 aPosition = placements.length; |
|
1649 } |
|
1650 |
|
1651 let widget = gPalette.get(aWidgetId); |
|
1652 if (widget) { |
|
1653 widget.currentPosition = aPosition; |
|
1654 widget.currentArea = oldPlacement.area; |
|
1655 } |
|
1656 |
|
1657 if (aPosition == oldPlacement.position) { |
|
1658 return; |
|
1659 } |
|
1660 |
|
1661 placements.splice(oldPlacement.position, 1); |
|
1662 // If we just removed the item from *before* where it is now added, |
|
1663 // we need to compensate the position offset for that: |
|
1664 if (oldPlacement.position < aPosition) { |
|
1665 aPosition--; |
|
1666 } |
|
1667 placements.splice(aPosition, 0, aWidgetId); |
|
1668 |
|
1669 gDirty = true; |
|
1670 gDirtyAreaCache.add(oldPlacement.area); |
|
1671 |
|
1672 this.saveState(); |
|
1673 |
|
1674 this.notifyListeners("onWidgetMoved", aWidgetId, oldPlacement.area, |
|
1675 oldPlacement.position, aPosition); |
|
1676 }, |
|
1677 |
|
1678 // Note that this does not populate gPlacements, which is done lazily so that |
|
1679 // the legacy state can be migrated, which is only available once a browser |
|
1680 // window is openned. |
|
1681 // The panel area is an exception here, since it has no legacy state and is |
|
1682 // built lazily - and therefore wouldn't otherwise result in restoring its |
|
1683 // state immediately when a browser window opens, which is important for |
|
1684 // other consumers of this API. |
|
1685 loadSavedState: function() { |
|
1686 let state = null; |
|
1687 try { |
|
1688 state = Services.prefs.getCharPref(kPrefCustomizationState); |
|
1689 } catch (e) { |
|
1690 LOG("No saved state found"); |
|
1691 // This will fail if nothing has been customized, so silently fall back to |
|
1692 // the defaults. |
|
1693 } |
|
1694 |
|
1695 if (!state) { |
|
1696 return; |
|
1697 } |
|
1698 try { |
|
1699 gSavedState = JSON.parse(state); |
|
1700 if (typeof gSavedState != "object" || gSavedState === null) { |
|
1701 throw "Invalid saved state"; |
|
1702 } |
|
1703 } catch(e) { |
|
1704 Services.prefs.clearUserPref(kPrefCustomizationState); |
|
1705 gSavedState = {}; |
|
1706 LOG("Error loading saved UI customization state, falling back to defaults."); |
|
1707 } |
|
1708 |
|
1709 if (!("placements" in gSavedState)) { |
|
1710 gSavedState.placements = {}; |
|
1711 } |
|
1712 |
|
1713 gSeenWidgets = new Set(gSavedState.seen || []); |
|
1714 gDirtyAreaCache = new Set(gSavedState.dirtyAreaCache || []); |
|
1715 gNewElementCount = gSavedState.newElementCount || 0; |
|
1716 }, |
|
1717 |
|
1718 restoreStateForArea: function(aArea, aLegacyState) { |
|
1719 let placementsPreexisted = gPlacements.has(aArea); |
|
1720 |
|
1721 this.beginBatchUpdate(); |
|
1722 try { |
|
1723 gRestoring = true; |
|
1724 |
|
1725 let restored = false; |
|
1726 if (placementsPreexisted) { |
|
1727 LOG("Restoring " + aArea + " from pre-existing placements"); |
|
1728 for (let [position, id] in Iterator(gPlacements.get(aArea))) { |
|
1729 this.moveWidgetWithinArea(id, position); |
|
1730 } |
|
1731 gDirty = false; |
|
1732 restored = true; |
|
1733 } else { |
|
1734 gPlacements.set(aArea, []); |
|
1735 } |
|
1736 |
|
1737 if (!restored && gSavedState && aArea in gSavedState.placements) { |
|
1738 LOG("Restoring " + aArea + " from saved state"); |
|
1739 let placements = gSavedState.placements[aArea]; |
|
1740 for (let id of placements) |
|
1741 this.addWidgetToArea(id, aArea); |
|
1742 gDirty = false; |
|
1743 restored = true; |
|
1744 } |
|
1745 |
|
1746 if (!restored && aLegacyState) { |
|
1747 LOG("Restoring " + aArea + " from legacy state"); |
|
1748 for (let id of aLegacyState) |
|
1749 this.addWidgetToArea(id, aArea); |
|
1750 // Don't override dirty state, to ensure legacy state is saved here and |
|
1751 // therefore only used once. |
|
1752 restored = true; |
|
1753 } |
|
1754 |
|
1755 if (!restored) { |
|
1756 LOG("Restoring " + aArea + " from default state"); |
|
1757 let defaults = gAreas.get(aArea).get("defaultPlacements"); |
|
1758 if (defaults) { |
|
1759 for (let id of defaults) |
|
1760 this.addWidgetToArea(id, aArea, null, true); |
|
1761 } |
|
1762 gDirty = false; |
|
1763 } |
|
1764 |
|
1765 // Finally, add widgets to the area that were added before the it was able |
|
1766 // to be restored. This can occur when add-ons register widgets for a |
|
1767 // lazily-restored area before it's been restored. |
|
1768 if (gFuturePlacements.has(aArea)) { |
|
1769 for (let id of gFuturePlacements.get(aArea)) |
|
1770 this.addWidgetToArea(id, aArea); |
|
1771 } |
|
1772 |
|
1773 LOG("Placements for " + aArea + ":\n\t" + gPlacements.get(aArea).join("\n\t")); |
|
1774 |
|
1775 gRestoring = false; |
|
1776 } finally { |
|
1777 this.endBatchUpdate(); |
|
1778 } |
|
1779 }, |
|
1780 |
|
1781 saveState: function() { |
|
1782 if (gInBatchStack || !gDirty) { |
|
1783 return; |
|
1784 } |
|
1785 let state = { placements: gPlacements, |
|
1786 seen: gSeenWidgets, |
|
1787 dirtyAreaCache: gDirtyAreaCache, |
|
1788 newElementCount: gNewElementCount }; |
|
1789 |
|
1790 LOG("Saving state."); |
|
1791 let serialized = JSON.stringify(state, this.serializerHelper); |
|
1792 LOG("State saved as: " + serialized); |
|
1793 Services.prefs.setCharPref(kPrefCustomizationState, serialized); |
|
1794 gDirty = false; |
|
1795 }, |
|
1796 |
|
1797 serializerHelper: function(aKey, aValue) { |
|
1798 if (typeof aValue == "object" && aValue.constructor.name == "Map") { |
|
1799 let result = {}; |
|
1800 for (let [mapKey, mapValue] of aValue) |
|
1801 result[mapKey] = mapValue; |
|
1802 return result; |
|
1803 } |
|
1804 |
|
1805 if (typeof aValue == "object" && aValue.constructor.name == "Set") { |
|
1806 return [...aValue]; |
|
1807 } |
|
1808 |
|
1809 return aValue; |
|
1810 }, |
|
1811 |
|
1812 beginBatchUpdate: function() { |
|
1813 gInBatchStack++; |
|
1814 }, |
|
1815 |
|
1816 endBatchUpdate: function(aForceDirty) { |
|
1817 gInBatchStack--; |
|
1818 if (aForceDirty === true) { |
|
1819 gDirty = true; |
|
1820 } |
|
1821 if (gInBatchStack == 0) { |
|
1822 this.saveState(); |
|
1823 } else if (gInBatchStack < 0) { |
|
1824 throw new Error("The batch editing stack should never reach a negative number."); |
|
1825 } |
|
1826 }, |
|
1827 |
|
1828 addListener: function(aListener) { |
|
1829 gListeners.add(aListener); |
|
1830 }, |
|
1831 |
|
1832 removeListener: function(aListener) { |
|
1833 if (aListener == this) { |
|
1834 return; |
|
1835 } |
|
1836 |
|
1837 gListeners.delete(aListener); |
|
1838 }, |
|
1839 |
|
1840 notifyListeners: function(aEvent, ...aArgs) { |
|
1841 if (gRestoring) { |
|
1842 return; |
|
1843 } |
|
1844 |
|
1845 for (let listener of gListeners) { |
|
1846 try { |
|
1847 if (typeof listener[aEvent] == "function") { |
|
1848 listener[aEvent].apply(listener, aArgs); |
|
1849 } |
|
1850 } catch (e) { |
|
1851 ERROR(e + " -- " + e.fileName + ":" + e.lineNumber); |
|
1852 } |
|
1853 } |
|
1854 }, |
|
1855 |
|
1856 _dispatchToolboxEventToWindow: function(aEventType, aDetails, aWindow) { |
|
1857 let evt = new aWindow.CustomEvent(aEventType, { |
|
1858 bubbles: true, |
|
1859 cancelable: true, |
|
1860 detail: aDetails |
|
1861 }); |
|
1862 aWindow.gNavToolbox.dispatchEvent(evt); |
|
1863 }, |
|
1864 |
|
1865 dispatchToolboxEvent: function(aEventType, aDetails={}, aWindow=null) { |
|
1866 if (aWindow) { |
|
1867 return this._dispatchToolboxEventToWindow(aEventType, aDetails, aWindow); |
|
1868 } |
|
1869 for (let [win, ] of gBuildWindows) { |
|
1870 this._dispatchToolboxEventToWindow(aEventType, aDetails, win); |
|
1871 } |
|
1872 }, |
|
1873 |
|
1874 createWidget: function(aProperties) { |
|
1875 let widget = this.normalizeWidget(aProperties, CustomizableUI.SOURCE_EXTERNAL); |
|
1876 //XXXunf This should probably throw. |
|
1877 if (!widget) { |
|
1878 return; |
|
1879 } |
|
1880 |
|
1881 gPalette.set(widget.id, widget); |
|
1882 |
|
1883 // Clear our caches: |
|
1884 gGroupWrapperCache.delete(widget.id); |
|
1885 for (let [win, ] of gBuildWindows) { |
|
1886 let cache = gSingleWrapperCache.get(win); |
|
1887 if (cache) { |
|
1888 cache.delete(widget.id); |
|
1889 } |
|
1890 } |
|
1891 |
|
1892 this.notifyListeners("onWidgetCreated", widget.id); |
|
1893 |
|
1894 if (widget.defaultArea) { |
|
1895 let area = gAreas.get(widget.defaultArea); |
|
1896 //XXXgijs this won't have any effect for legacy items. Sort of OK because |
|
1897 // consumers can modify currentset? Maybe? |
|
1898 if (area.has("defaultPlacements")) { |
|
1899 area.get("defaultPlacements").push(widget.id); |
|
1900 } else { |
|
1901 area.set("defaultPlacements", [widget.id]); |
|
1902 } |
|
1903 } |
|
1904 |
|
1905 // Look through previously saved state to see if we're restoring a widget. |
|
1906 let seenAreas = new Set(); |
|
1907 let widgetMightNeedAutoAdding = true; |
|
1908 for (let [area, placements] of gPlacements) { |
|
1909 seenAreas.add(area); |
|
1910 let areaIsRegistered = gAreas.has(area); |
|
1911 let index = gPlacements.get(area).indexOf(widget.id); |
|
1912 if (index != -1) { |
|
1913 widgetMightNeedAutoAdding = false; |
|
1914 if (areaIsRegistered) { |
|
1915 widget.currentArea = area; |
|
1916 widget.currentPosition = index; |
|
1917 } |
|
1918 break; |
|
1919 } |
|
1920 } |
|
1921 |
|
1922 // Also look at saved state data directly in areas that haven't yet been |
|
1923 // restored. Can't rely on this for restored areas, as they may have |
|
1924 // changed. |
|
1925 if (widgetMightNeedAutoAdding && gSavedState) { |
|
1926 for (let area of Object.keys(gSavedState.placements)) { |
|
1927 if (seenAreas.has(area)) { |
|
1928 continue; |
|
1929 } |
|
1930 |
|
1931 let areaIsRegistered = gAreas.has(area); |
|
1932 let index = gSavedState.placements[area].indexOf(widget.id); |
|
1933 if (index != -1) { |
|
1934 widgetMightNeedAutoAdding = false; |
|
1935 if (areaIsRegistered) { |
|
1936 widget.currentArea = area; |
|
1937 widget.currentPosition = index; |
|
1938 } |
|
1939 break; |
|
1940 } |
|
1941 } |
|
1942 } |
|
1943 |
|
1944 // If we're restoring the widget to it's old placement, fire off the |
|
1945 // onWidgetAdded event - our own handler will take care of adding it to |
|
1946 // any build areas. |
|
1947 if (widget.currentArea) { |
|
1948 this.notifyListeners("onWidgetAdded", widget.id, widget.currentArea, |
|
1949 widget.currentPosition); |
|
1950 } else if (widgetMightNeedAutoAdding) { |
|
1951 let autoAdd = true; |
|
1952 try { |
|
1953 autoAdd = Services.prefs.getBoolPref(kPrefCustomizationAutoAdd); |
|
1954 } catch (e) {} |
|
1955 |
|
1956 // If the widget doesn't have an existing placement, and it hasn't been |
|
1957 // seen before, then add it to its default area so it can be used. |
|
1958 // If the widget is not removable, we *have* to add it to its default |
|
1959 // area here. |
|
1960 let canBeAutoAdded = autoAdd && !gSeenWidgets.has(widget.id); |
|
1961 if (!widget.currentArea && (!widget.removable || canBeAutoAdded)) { |
|
1962 this.beginBatchUpdate(); |
|
1963 try { |
|
1964 gSeenWidgets.add(widget.id); |
|
1965 |
|
1966 if (widget.defaultArea) { |
|
1967 if (this.isAreaLazy(widget.defaultArea)) { |
|
1968 gFuturePlacements.get(widget.defaultArea).add(widget.id); |
|
1969 } else { |
|
1970 this.addWidgetToArea(widget.id, widget.defaultArea); |
|
1971 } |
|
1972 } |
|
1973 } finally { |
|
1974 this.endBatchUpdate(true); |
|
1975 } |
|
1976 } |
|
1977 } |
|
1978 |
|
1979 this.notifyListeners("onWidgetAfterCreation", widget.id, widget.currentArea); |
|
1980 return widget.id; |
|
1981 }, |
|
1982 |
|
1983 createBuiltinWidget: function(aData) { |
|
1984 // This should only ever be called on startup, before any windows are |
|
1985 // opened - so we know there's no build areas to handle. Also, builtin |
|
1986 // widgets are expected to be (mostly) static, so shouldn't affect the |
|
1987 // current placement settings. |
|
1988 let widget = this.normalizeWidget(aData, CustomizableUI.SOURCE_BUILTIN); |
|
1989 if (!widget) { |
|
1990 ERROR("Error creating builtin widget: " + aData.id); |
|
1991 return; |
|
1992 } |
|
1993 |
|
1994 LOG("Creating built-in widget with id: " + widget.id); |
|
1995 gPalette.set(widget.id, widget); |
|
1996 }, |
|
1997 |
|
1998 // Returns true if the area will eventually lazily restore (but hasn't yet). |
|
1999 isAreaLazy: function(aArea) { |
|
2000 if (gPlacements.has(aArea)) { |
|
2001 return false; |
|
2002 } |
|
2003 return gAreas.get(aArea).has("legacy"); |
|
2004 }, |
|
2005 |
|
2006 //XXXunf Log some warnings here, when the data provided isn't up to scratch. |
|
2007 normalizeWidget: function(aData, aSource) { |
|
2008 let widget = { |
|
2009 implementation: aData, |
|
2010 source: aSource || "addon", |
|
2011 instances: new Map(), |
|
2012 currentArea: null, |
|
2013 removable: true, |
|
2014 overflows: true, |
|
2015 defaultArea: null, |
|
2016 shortcutId: null, |
|
2017 tooltiptext: null, |
|
2018 showInPrivateBrowsing: true, |
|
2019 }; |
|
2020 |
|
2021 if (typeof aData.id != "string" || !/^[a-z0-9-_]{1,}$/i.test(aData.id)) { |
|
2022 ERROR("Given an illegal id in normalizeWidget: " + aData.id); |
|
2023 return null; |
|
2024 } |
|
2025 |
|
2026 delete widget.implementation.currentArea; |
|
2027 widget.implementation.__defineGetter__("currentArea", function() widget.currentArea); |
|
2028 |
|
2029 const kReqStringProps = ["id"]; |
|
2030 for (let prop of kReqStringProps) { |
|
2031 if (typeof aData[prop] != "string") { |
|
2032 ERROR("Missing required property '" + prop + "' in normalizeWidget: " |
|
2033 + aData.id); |
|
2034 return null; |
|
2035 } |
|
2036 widget[prop] = aData[prop]; |
|
2037 } |
|
2038 |
|
2039 const kOptStringProps = ["label", "tooltiptext", "shortcutId"]; |
|
2040 for (let prop of kOptStringProps) { |
|
2041 if (typeof aData[prop] == "string") { |
|
2042 widget[prop] = aData[prop]; |
|
2043 } |
|
2044 } |
|
2045 |
|
2046 const kOptBoolProps = ["removable", "showInPrivateBrowsing", "overflows"]; |
|
2047 for (let prop of kOptBoolProps) { |
|
2048 if (typeof aData[prop] == "boolean") { |
|
2049 widget[prop] = aData[prop]; |
|
2050 } |
|
2051 } |
|
2052 |
|
2053 if (aData.defaultArea && gAreas.has(aData.defaultArea)) { |
|
2054 widget.defaultArea = aData.defaultArea; |
|
2055 } else if (!widget.removable) { |
|
2056 ERROR("Widget '" + widget.id + "' is not removable but does not specify " + |
|
2057 "a valid defaultArea. That's not possible; it must specify a " + |
|
2058 "valid defaultArea as well."); |
|
2059 return null; |
|
2060 } |
|
2061 |
|
2062 if ("type" in aData && gSupportedWidgetTypes.has(aData.type)) { |
|
2063 widget.type = aData.type; |
|
2064 } else { |
|
2065 widget.type = "button"; |
|
2066 } |
|
2067 |
|
2068 widget.disabled = aData.disabled === true; |
|
2069 |
|
2070 this.wrapWidgetEventHandler("onBeforeCreated", widget); |
|
2071 this.wrapWidgetEventHandler("onClick", widget); |
|
2072 this.wrapWidgetEventHandler("onCreated", widget); |
|
2073 |
|
2074 if (widget.type == "button") { |
|
2075 widget.onCommand = typeof aData.onCommand == "function" ? |
|
2076 aData.onCommand : |
|
2077 null; |
|
2078 } else if (widget.type == "view") { |
|
2079 if (typeof aData.viewId != "string") { |
|
2080 ERROR("Expected a string for widget " + widget.id + " viewId, but got " |
|
2081 + aData.viewId); |
|
2082 return null; |
|
2083 } |
|
2084 widget.viewId = aData.viewId; |
|
2085 |
|
2086 this.wrapWidgetEventHandler("onViewShowing", widget); |
|
2087 this.wrapWidgetEventHandler("onViewHiding", widget); |
|
2088 } else if (widget.type == "custom") { |
|
2089 this.wrapWidgetEventHandler("onBuild", widget); |
|
2090 } |
|
2091 |
|
2092 if (gPalette.has(widget.id)) { |
|
2093 return null; |
|
2094 } |
|
2095 |
|
2096 return widget; |
|
2097 }, |
|
2098 |
|
2099 wrapWidgetEventHandler: function(aEventName, aWidget) { |
|
2100 if (typeof aWidget.implementation[aEventName] != "function") { |
|
2101 aWidget[aEventName] = null; |
|
2102 return; |
|
2103 } |
|
2104 aWidget[aEventName] = function(...aArgs) { |
|
2105 // Wrap inside a try...catch to properly log errors, until bug 862627 is |
|
2106 // fixed, which in turn might help bug 503244. |
|
2107 try { |
|
2108 // Don't copy the function to the normalized widget object, instead |
|
2109 // keep it on the original object provided to the API so that |
|
2110 // additional methods can be implemented and used by the event |
|
2111 // handlers. |
|
2112 return aWidget.implementation[aEventName].apply(aWidget.implementation, |
|
2113 aArgs); |
|
2114 } catch (e) { |
|
2115 Cu.reportError(e); |
|
2116 } |
|
2117 }; |
|
2118 }, |
|
2119 |
|
2120 destroyWidget: function(aWidgetId) { |
|
2121 let widget = gPalette.get(aWidgetId); |
|
2122 if (!widget) { |
|
2123 gGroupWrapperCache.delete(aWidgetId); |
|
2124 for (let [window, ] of gBuildWindows) { |
|
2125 let windowCache = gSingleWrapperCache.get(window); |
|
2126 if (windowCache) { |
|
2127 windowCache.delete(aWidgetId); |
|
2128 } |
|
2129 } |
|
2130 return; |
|
2131 } |
|
2132 |
|
2133 // Remove it from the default placements of an area if it was added there: |
|
2134 if (widget.defaultArea) { |
|
2135 let area = gAreas.get(widget.defaultArea); |
|
2136 if (area) { |
|
2137 let defaultPlacements = area.get("defaultPlacements"); |
|
2138 // We can assume this is present because if a widget has a defaultArea, |
|
2139 // we automatically create a defaultPlacements array for that area. |
|
2140 let widgetIndex = defaultPlacements.indexOf(aWidgetId); |
|
2141 if (widgetIndex != -1) { |
|
2142 defaultPlacements.splice(widgetIndex, 1); |
|
2143 } |
|
2144 } |
|
2145 } |
|
2146 |
|
2147 // This will not remove the widget from gPlacements - we want to keep the |
|
2148 // setting so the widget gets put back in it's old position if/when it |
|
2149 // returns. |
|
2150 for (let [window, ] of gBuildWindows) { |
|
2151 let windowCache = gSingleWrapperCache.get(window); |
|
2152 if (windowCache) { |
|
2153 windowCache.delete(aWidgetId); |
|
2154 } |
|
2155 let widgetNode = window.document.getElementById(aWidgetId) || |
|
2156 window.gNavToolbox.palette.getElementsByAttribute("id", aWidgetId)[0]; |
|
2157 if (widgetNode) { |
|
2158 let container = widgetNode.parentNode |
|
2159 this.notifyListeners("onWidgetBeforeDOMChange", widgetNode, null, |
|
2160 container, true); |
|
2161 widgetNode.remove(); |
|
2162 this.notifyListeners("onWidgetAfterDOMChange", widgetNode, null, |
|
2163 container, true); |
|
2164 } |
|
2165 if (widget.type == "view") { |
|
2166 let viewNode = window.document.getElementById(widget.viewId); |
|
2167 if (viewNode) { |
|
2168 for (let eventName of kSubviewEvents) { |
|
2169 let handler = "on" + eventName; |
|
2170 if (typeof widget[handler] == "function") { |
|
2171 viewNode.removeEventListener(eventName, widget[handler], false); |
|
2172 } |
|
2173 } |
|
2174 } |
|
2175 } |
|
2176 } |
|
2177 |
|
2178 gPalette.delete(aWidgetId); |
|
2179 gGroupWrapperCache.delete(aWidgetId); |
|
2180 |
|
2181 this.notifyListeners("onWidgetDestroyed", aWidgetId); |
|
2182 }, |
|
2183 |
|
2184 getCustomizeTargetForArea: function(aArea, aWindow) { |
|
2185 let buildAreaNodes = gBuildAreas.get(aArea); |
|
2186 if (!buildAreaNodes) { |
|
2187 return null; |
|
2188 } |
|
2189 |
|
2190 for (let node of buildAreaNodes) { |
|
2191 if (node.ownerDocument.defaultView === aWindow) { |
|
2192 return node.customizationTarget ? node.customizationTarget : node; |
|
2193 } |
|
2194 } |
|
2195 |
|
2196 return null; |
|
2197 }, |
|
2198 |
|
2199 reset: function() { |
|
2200 gResetting = true; |
|
2201 this._resetUIState(); |
|
2202 |
|
2203 // Rebuild each registered area (across windows) to reflect the state that |
|
2204 // was reset above. |
|
2205 this._rebuildRegisteredAreas(); |
|
2206 |
|
2207 gResetting = false; |
|
2208 }, |
|
2209 |
|
2210 _resetUIState: function() { |
|
2211 try { |
|
2212 gUIStateBeforeReset.drawInTitlebar = Services.prefs.getBoolPref(kPrefDrawInTitlebar); |
|
2213 gUIStateBeforeReset.uiCustomizationState = Services.prefs.getCharPref(kPrefCustomizationState); |
|
2214 } catch(e) { } |
|
2215 |
|
2216 this._resetExtraToolbars(); |
|
2217 |
|
2218 Services.prefs.clearUserPref(kPrefCustomizationState); |
|
2219 Services.prefs.clearUserPref(kPrefDrawInTitlebar); |
|
2220 LOG("State reset"); |
|
2221 |
|
2222 // Reset placements to make restoring default placements possible. |
|
2223 gPlacements = new Map(); |
|
2224 gDirtyAreaCache = new Set(); |
|
2225 gSeenWidgets = new Set(); |
|
2226 // Clear the saved state to ensure that defaults will be used. |
|
2227 gSavedState = null; |
|
2228 // Restore the state for each area to its defaults |
|
2229 for (let [areaId,] of gAreas) { |
|
2230 this.restoreStateForArea(areaId); |
|
2231 } |
|
2232 }, |
|
2233 |
|
2234 _resetExtraToolbars: function(aFilter = null) { |
|
2235 let firstWindow = true; // Only need to unregister and persist once |
|
2236 for (let [win, ] of gBuildWindows) { |
|
2237 let toolbox = win.gNavToolbox; |
|
2238 for (let child of toolbox.children) { |
|
2239 let matchesFilter = !aFilter || aFilter == child.id; |
|
2240 if (child.hasAttribute("customindex") && matchesFilter) { |
|
2241 let toolbarId = "toolbar" + child.getAttribute("customindex"); |
|
2242 toolbox.toolbarset.removeAttribute(toolbarId); |
|
2243 if (firstWindow) { |
|
2244 win.document.persist(toolbox.toolbarset.id, toolbarId); |
|
2245 // We have to unregister it properly to ensure we don't kill |
|
2246 // XUL widgets which might be in here |
|
2247 this.unregisterArea(child.id, true); |
|
2248 } |
|
2249 child.remove(); |
|
2250 } |
|
2251 } |
|
2252 firstWindow = false; |
|
2253 } |
|
2254 }, |
|
2255 |
|
2256 _rebuildRegisteredAreas: function() { |
|
2257 for (let [areaId, areaNodes] of gBuildAreas) { |
|
2258 let placements = gPlacements.get(areaId); |
|
2259 let isFirstChangedToolbar = true; |
|
2260 for (let areaNode of areaNodes) { |
|
2261 this.buildArea(areaId, placements, areaNode); |
|
2262 |
|
2263 let area = gAreas.get(areaId); |
|
2264 if (area.get("type") == CustomizableUI.TYPE_TOOLBAR) { |
|
2265 let defaultCollapsed = area.get("defaultCollapsed"); |
|
2266 let win = areaNode.ownerDocument.defaultView; |
|
2267 if (defaultCollapsed !== null) { |
|
2268 win.setToolbarVisibility(areaNode, !defaultCollapsed, isFirstChangedToolbar); |
|
2269 } |
|
2270 } |
|
2271 isFirstChangedToolbar = false; |
|
2272 } |
|
2273 } |
|
2274 }, |
|
2275 |
|
2276 /** |
|
2277 * Undoes a previous reset, restoring the state of the UI to the state prior to the reset. |
|
2278 */ |
|
2279 undoReset: function() { |
|
2280 if (gUIStateBeforeReset.uiCustomizationState == null || |
|
2281 gUIStateBeforeReset.drawInTitlebar == null) { |
|
2282 return; |
|
2283 } |
|
2284 gUndoResetting = true; |
|
2285 |
|
2286 let uiCustomizationState = gUIStateBeforeReset.uiCustomizationState; |
|
2287 let drawInTitlebar = gUIStateBeforeReset.drawInTitlebar; |
|
2288 |
|
2289 // Need to clear the previous state before setting the prefs |
|
2290 // because pref observers may check if there is a previous UI state. |
|
2291 this._clearPreviousUIState(); |
|
2292 |
|
2293 Services.prefs.setCharPref(kPrefCustomizationState, uiCustomizationState); |
|
2294 Services.prefs.setBoolPref(kPrefDrawInTitlebar, drawInTitlebar); |
|
2295 this.loadSavedState(); |
|
2296 // If the user just customizes toolbar/titlebar visibility, gSavedState will be null |
|
2297 // and we don't need to do anything else here: |
|
2298 if (gSavedState) { |
|
2299 for (let areaId of Object.keys(gSavedState.placements)) { |
|
2300 let placements = gSavedState.placements[areaId]; |
|
2301 gPlacements.set(areaId, placements); |
|
2302 } |
|
2303 this._rebuildRegisteredAreas(); |
|
2304 } |
|
2305 |
|
2306 gUndoResetting = false; |
|
2307 }, |
|
2308 |
|
2309 _clearPreviousUIState: function() { |
|
2310 Object.getOwnPropertyNames(gUIStateBeforeReset).forEach((prop) => { |
|
2311 gUIStateBeforeReset[prop] = null; |
|
2312 }); |
|
2313 }, |
|
2314 |
|
2315 removeExtraToolbar: function(aToolbarId) { |
|
2316 this._resetExtraToolbars(aToolbarId); |
|
2317 }, |
|
2318 |
|
2319 /** |
|
2320 * @param {String|Node} aWidget - widget ID or a widget node (preferred for performance). |
|
2321 * @return {Boolean} whether the widget is removable |
|
2322 */ |
|
2323 isWidgetRemovable: function(aWidget) { |
|
2324 let widgetId; |
|
2325 let widgetNode; |
|
2326 if (typeof aWidget == "string") { |
|
2327 widgetId = aWidget; |
|
2328 } else { |
|
2329 widgetId = aWidget.id; |
|
2330 widgetNode = aWidget; |
|
2331 } |
|
2332 let provider = this.getWidgetProvider(widgetId); |
|
2333 |
|
2334 if (provider == CustomizableUI.PROVIDER_API) { |
|
2335 return gPalette.get(widgetId).removable; |
|
2336 } |
|
2337 |
|
2338 if (provider == CustomizableUI.PROVIDER_XUL) { |
|
2339 if (gBuildWindows.size == 0) { |
|
2340 // We don't have any build windows to look at, so just assume for now |
|
2341 // that its removable. |
|
2342 return true; |
|
2343 } |
|
2344 |
|
2345 if (!widgetNode) { |
|
2346 // Pick any of the build windows to look at. |
|
2347 let [window,] = [...gBuildWindows][0]; |
|
2348 [, widgetNode] = this.getWidgetNode(widgetId, window); |
|
2349 } |
|
2350 // If we don't have a node, we assume it's removable. This can happen because |
|
2351 // getWidgetProvider returns PROVIDER_XUL by default, but this will also happen |
|
2352 // for API-provided widgets which have been destroyed. |
|
2353 if (!widgetNode) { |
|
2354 return true; |
|
2355 } |
|
2356 return widgetNode.getAttribute("removable") == "true"; |
|
2357 } |
|
2358 |
|
2359 // Otherwise this is either a special widget, which is always removable, or |
|
2360 // an API widget which has already been removed from gPalette. Returning true |
|
2361 // here allows us to then remove its ID from any placements where it might |
|
2362 // still occur. |
|
2363 return true; |
|
2364 }, |
|
2365 |
|
2366 canWidgetMoveToArea: function(aWidgetId, aArea) { |
|
2367 let placement = this.getPlacementOfWidget(aWidgetId); |
|
2368 if (placement && placement.area != aArea) { |
|
2369 // Special widgets can't move to the menu panel. |
|
2370 if (this.isSpecialWidget(aWidgetId) && gAreas.has(aArea) && |
|
2371 gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL) { |
|
2372 return false; |
|
2373 } |
|
2374 // For everything else, just return whether the widget is removable. |
|
2375 return this.isWidgetRemovable(aWidgetId); |
|
2376 } |
|
2377 |
|
2378 return true; |
|
2379 }, |
|
2380 |
|
2381 ensureWidgetPlacedInWindow: function(aWidgetId, aWindow) { |
|
2382 let placement = this.getPlacementOfWidget(aWidgetId); |
|
2383 if (!placement) { |
|
2384 return false; |
|
2385 } |
|
2386 let areaNodes = gBuildAreas.get(placement.area); |
|
2387 if (!areaNodes) { |
|
2388 return false; |
|
2389 } |
|
2390 let container = [...areaNodes].filter((n) => n.ownerDocument.defaultView == aWindow); |
|
2391 if (!container.length) { |
|
2392 return false; |
|
2393 } |
|
2394 let existingNode = container[0].getElementsByAttribute("id", aWidgetId)[0]; |
|
2395 if (existingNode) { |
|
2396 return true; |
|
2397 } |
|
2398 |
|
2399 this.insertNodeInWindow(aWidgetId, container[0], true); |
|
2400 return true; |
|
2401 }, |
|
2402 |
|
2403 get inDefaultState() { |
|
2404 for (let [areaId, props] of gAreas) { |
|
2405 let defaultPlacements = props.get("defaultPlacements"); |
|
2406 // Areas without default placements (like legacy ones?) get skipped |
|
2407 if (!defaultPlacements) { |
|
2408 continue; |
|
2409 } |
|
2410 |
|
2411 let currentPlacements = gPlacements.get(areaId); |
|
2412 // We're excluding all of the placement IDs for items that do not exist, |
|
2413 // and items that have removable="false", |
|
2414 // because we don't want to consider them when determining if we're |
|
2415 // in the default state. This way, if an add-on introduces a widget |
|
2416 // and is then uninstalled, the leftover placement doesn't cause us to |
|
2417 // automatically assume that the buttons are not in the default state. |
|
2418 let buildAreaNodes = gBuildAreas.get(areaId); |
|
2419 if (buildAreaNodes && buildAreaNodes.size) { |
|
2420 let container = [...buildAreaNodes][0]; |
|
2421 let removableOrDefault = (itemNodeOrItem) => { |
|
2422 let item = (itemNodeOrItem && itemNodeOrItem.id) || itemNodeOrItem; |
|
2423 let isRemovable = this.isWidgetRemovable(itemNodeOrItem); |
|
2424 let isInDefault = defaultPlacements.indexOf(item) != -1; |
|
2425 return isRemovable || isInDefault; |
|
2426 }; |
|
2427 // Toolbars have a currentSet property which also deals correctly with overflown |
|
2428 // widgets (if any) - use that instead: |
|
2429 if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) { |
|
2430 let currentSet = container.currentSet; |
|
2431 currentPlacements = currentSet ? currentSet.split(',') : []; |
|
2432 currentPlacements = currentPlacements.filter(removableOrDefault); |
|
2433 } else { |
|
2434 // Clone the array so we don't modify the actual placements... |
|
2435 currentPlacements = [...currentPlacements]; |
|
2436 currentPlacements = currentPlacements.filter((item) => { |
|
2437 let itemNode = container.getElementsByAttribute("id", item)[0]; |
|
2438 return itemNode && removableOrDefault(itemNode || item); |
|
2439 }); |
|
2440 } |
|
2441 |
|
2442 if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) { |
|
2443 let attribute = container.getAttribute("type") == "menubar" ? "autohide" : "collapsed"; |
|
2444 let collapsed = container.getAttribute(attribute) == "true"; |
|
2445 let defaultCollapsed = props.get("defaultCollapsed"); |
|
2446 if (defaultCollapsed !== null && collapsed != defaultCollapsed) { |
|
2447 LOG("Found " + areaId + " had non-default toolbar visibility (expected " + defaultCollapsed + ", was " + collapsed + ")"); |
|
2448 return false; |
|
2449 } |
|
2450 } |
|
2451 } |
|
2452 LOG("Checking default state for " + areaId + ":\n" + currentPlacements.join(",") + |
|
2453 "\nvs.\n" + defaultPlacements.join(",")); |
|
2454 |
|
2455 if (currentPlacements.length != defaultPlacements.length) { |
|
2456 return false; |
|
2457 } |
|
2458 |
|
2459 for (let i = 0; i < currentPlacements.length; ++i) { |
|
2460 if (currentPlacements[i] != defaultPlacements[i]) { |
|
2461 LOG("Found " + currentPlacements[i] + " in " + areaId + " where " + |
|
2462 defaultPlacements[i] + " was expected!"); |
|
2463 return false; |
|
2464 } |
|
2465 } |
|
2466 } |
|
2467 |
|
2468 if (Services.prefs.prefHasUserValue(kPrefDrawInTitlebar)) { |
|
2469 LOG(kPrefDrawInTitlebar + " pref is non-default"); |
|
2470 return false; |
|
2471 } |
|
2472 |
|
2473 return true; |
|
2474 }, |
|
2475 |
|
2476 setToolbarVisibility: function(aToolbarId, aIsVisible) { |
|
2477 // We only persist the attribute the first time. |
|
2478 let isFirstChangedToolbar = true; |
|
2479 for (let window of CustomizableUI.windows) { |
|
2480 let toolbar = window.document.getElementById(aToolbarId); |
|
2481 if (toolbar) { |
|
2482 window.setToolbarVisibility(toolbar, aIsVisible, isFirstChangedToolbar); |
|
2483 isFirstChangedToolbar = false; |
|
2484 } |
|
2485 } |
|
2486 }, |
|
2487 }; |
|
2488 Object.freeze(CustomizableUIInternal); |
|
2489 |
|
2490 this.CustomizableUI = { |
|
2491 /** |
|
2492 * Constant reference to the ID of the menu panel. |
|
2493 */ |
|
2494 get AREA_PANEL() "PanelUI-contents", |
|
2495 /** |
|
2496 * Constant reference to the ID of the navigation toolbar. |
|
2497 */ |
|
2498 get AREA_NAVBAR() "nav-bar", |
|
2499 /** |
|
2500 * Constant reference to the ID of the menubar's toolbar. |
|
2501 */ |
|
2502 get AREA_MENUBAR() "toolbar-menubar", |
|
2503 /** |
|
2504 * Constant reference to the ID of the tabstrip toolbar. |
|
2505 */ |
|
2506 get AREA_TABSTRIP() "TabsToolbar", |
|
2507 /** |
|
2508 * Constant reference to the ID of the bookmarks toolbar. |
|
2509 */ |
|
2510 get AREA_BOOKMARKS() "PersonalToolbar", |
|
2511 /** |
|
2512 * Constant reference to the ID of the addon-bar toolbar shim. |
|
2513 * Do not use, this will be removed as soon as reasonably possible. |
|
2514 * @deprecated |
|
2515 */ |
|
2516 get AREA_ADDONBAR() "addon-bar", |
|
2517 /** |
|
2518 * Constant indicating the area is a menu panel. |
|
2519 */ |
|
2520 get TYPE_MENU_PANEL() "menu-panel", |
|
2521 /** |
|
2522 * Constant indicating the area is a toolbar. |
|
2523 */ |
|
2524 get TYPE_TOOLBAR() "toolbar", |
|
2525 |
|
2526 /** |
|
2527 * Constant indicating a XUL-type provider. |
|
2528 */ |
|
2529 get PROVIDER_XUL() "xul", |
|
2530 /** |
|
2531 * Constant indicating an API-type provider. |
|
2532 */ |
|
2533 get PROVIDER_API() "api", |
|
2534 /** |
|
2535 * Constant indicating dynamic (special) widgets: spring, spacer, and separator. |
|
2536 */ |
|
2537 get PROVIDER_SPECIAL() "special", |
|
2538 |
|
2539 /** |
|
2540 * Constant indicating the widget is built-in |
|
2541 */ |
|
2542 get SOURCE_BUILTIN() "builtin", |
|
2543 /** |
|
2544 * Constant indicating the widget is externally provided |
|
2545 * (e.g. by add-ons or other items not part of the builtin widget set). |
|
2546 */ |
|
2547 get SOURCE_EXTERNAL() "external", |
|
2548 |
|
2549 /** |
|
2550 * The class used to distinguish items that span the entire menu panel. |
|
2551 */ |
|
2552 get WIDE_PANEL_CLASS() "panel-wide-item", |
|
2553 /** |
|
2554 * The (constant) number of columns in the menu panel. |
|
2555 */ |
|
2556 get PANEL_COLUMN_COUNT() 3, |
|
2557 |
|
2558 /** |
|
2559 * Constant indicating the reason the event was fired was a window closing |
|
2560 */ |
|
2561 get REASON_WINDOW_CLOSED() "window-closed", |
|
2562 /** |
|
2563 * Constant indicating the reason the event was fired was an area being |
|
2564 * unregistered separately from window closing mechanics. |
|
2565 */ |
|
2566 get REASON_AREA_UNREGISTERED() "area-unregistered", |
|
2567 |
|
2568 |
|
2569 /** |
|
2570 * An iteratable property of windows managed by CustomizableUI. |
|
2571 * Note that this can *only* be used as an iterator. ie: |
|
2572 * for (let window of CustomizableUI.windows) { ... } |
|
2573 */ |
|
2574 windows: { |
|
2575 "@@iterator": function*() { |
|
2576 for (let [window,] of gBuildWindows) |
|
2577 yield window; |
|
2578 } |
|
2579 }, |
|
2580 |
|
2581 /** |
|
2582 * Add a listener object that will get fired for various events regarding |
|
2583 * customization. |
|
2584 * |
|
2585 * @param aListener the listener object to add |
|
2586 * |
|
2587 * Not all event handler methods need to be defined. |
|
2588 * CustomizableUI will catch exceptions. Events are dispatched |
|
2589 * synchronously on the UI thread, so if you can delay any/some of your |
|
2590 * processing, that is advisable. The following event handlers are supported: |
|
2591 * - onWidgetAdded(aWidgetId, aArea, aPosition) |
|
2592 * Fired when a widget is added to an area. aWidgetId is the widget that |
|
2593 * was added, aArea the area it was added to, and aPosition the position |
|
2594 * in which it was added. |
|
2595 * - onWidgetMoved(aWidgetId, aArea, aOldPosition, aNewPosition) |
|
2596 * Fired when a widget is moved within its area. aWidgetId is the widget |
|
2597 * that was moved, aArea the area it was moved in, aOldPosition its old |
|
2598 * position, and aNewPosition its new position. |
|
2599 * - onWidgetRemoved(aWidgetId, aArea) |
|
2600 * Fired when a widget is removed from its area. aWidgetId is the widget |
|
2601 * that was removed, aArea the area it was removed from. |
|
2602 * |
|
2603 * - onWidgetBeforeDOMChange(aNode, aNextNode, aContainer, aIsRemoval) |
|
2604 * Fired *before* a widget's DOM node is acted upon by CustomizableUI |
|
2605 * (to add, move or remove it). aNode is the DOM node changed, aNextNode |
|
2606 * the DOM node (if any) before which a widget will be inserted, |
|
2607 * aContainer the *actual* DOM container (could be an overflow panel in |
|
2608 * case of an overflowable toolbar), and aWasRemoval is true iff the |
|
2609 * action about to happen is the removal of the DOM node. |
|
2610 * - onWidgetAfterDOMChange(aNode, aNextNode, aContainer, aWasRemoval) |
|
2611 * Like onWidgetBeforeDOMChange, but fired after the change to the DOM |
|
2612 * node of the widget. |
|
2613 * |
|
2614 * - onWidgetReset(aNode, aContainer) |
|
2615 * Fired after a reset to default placements moves a widget's node to a |
|
2616 * different location. aNode is the widget's node, aContainer is the |
|
2617 * area it was moved into (NB: it might already have been there and been |
|
2618 * moved to a different position!) |
|
2619 * - onWidgetUndoMove(aNode, aContainer) |
|
2620 * Fired after undoing a reset to default placements moves a widget's |
|
2621 * node to a different location. aNode is the widget's node, aContainer |
|
2622 * is the area it was moved into (NB: it might already have been there |
|
2623 * and been moved to a different position!) |
|
2624 * - onAreaReset(aArea, aContainer) |
|
2625 * Fired after a reset to default placements is complete on an area's |
|
2626 * DOM node. Note that this is fired for each DOM node. aArea is the area |
|
2627 * that was reset, aContainer the DOM node that was reset. |
|
2628 * |
|
2629 * - onWidgetCreated(aWidgetId) |
|
2630 * Fired when a widget with id aWidgetId has been created, but before it |
|
2631 * is added to any placements or any DOM nodes have been constructed. |
|
2632 * Only fired for API-based widgets. |
|
2633 * - onWidgetAfterCreation(aWidgetId, aArea) |
|
2634 * Fired after a widget with id aWidgetId has been created, and has been |
|
2635 * added to either its default area or the area in which it was placed |
|
2636 * previously. If the widget has no default area and/or it has never |
|
2637 * been placed anywhere, aArea may be null. Only fired for API-based |
|
2638 * widgets. |
|
2639 * - onWidgetDestroyed(aWidgetId) |
|
2640 * Fired when widgets are destroyed. aWidgetId is the widget that is |
|
2641 * being destroyed. Only fired for API-based widgets. |
|
2642 * - onWidgetInstanceRemoved(aWidgetId, aDocument) |
|
2643 * Fired when a window is unloaded and a widget's instance is destroyed |
|
2644 * because of this. Only fired for API-based widgets. |
|
2645 * |
|
2646 * - onWidgetDrag(aWidgetId, aArea) |
|
2647 * Fired both when and after customize mode drag handling system tries |
|
2648 * to determine the width and height of widget aWidgetId when dragged to a |
|
2649 * different area. aArea will be the area the item is dragged to, or |
|
2650 * undefined after the measurements have been done and the node has been |
|
2651 * moved back to its 'regular' area. |
|
2652 * |
|
2653 * - onCustomizeStart(aWindow) |
|
2654 * Fired when opening customize mode in aWindow. |
|
2655 * - onCustomizeEnd(aWindow) |
|
2656 * Fired when exiting customize mode in aWindow. |
|
2657 * |
|
2658 * - onWidgetOverflow(aNode, aContainer) |
|
2659 * Fired when a widget's DOM node is overflowing its container, a toolbar, |
|
2660 * and will be displayed in the overflow panel. |
|
2661 * - onWidgetUnderflow(aNode, aContainer) |
|
2662 * Fired when a widget's DOM node is *not* overflowing its container, a |
|
2663 * toolbar, anymore. |
|
2664 * - onWindowOpened(aWindow) |
|
2665 * Fired when a window has been opened that is managed by CustomizableUI, |
|
2666 * once all of the prerequisite setup has been done. |
|
2667 * - onWindowClosed(aWindow) |
|
2668 * Fired when a window that has been managed by CustomizableUI has been |
|
2669 * closed. |
|
2670 * - onAreaNodeRegistered(aArea, aContainer) |
|
2671 * Fired after an area node is first built when it is registered. This |
|
2672 * is often when the window has opened, but in the case of add-ons, |
|
2673 * could fire when the node has just been registered with CustomizableUI |
|
2674 * after an add-on update or disable/enable sequence. |
|
2675 * - onAreaNodeUnregistered(aArea, aContainer, aReason) |
|
2676 * Fired when an area node is explicitly unregistered by an API caller, |
|
2677 * or by a window closing. The aReason parameter indicates which of |
|
2678 * these is the case. |
|
2679 */ |
|
2680 addListener: function(aListener) { |
|
2681 CustomizableUIInternal.addListener(aListener); |
|
2682 }, |
|
2683 /** |
|
2684 * Remove a listener added with addListener |
|
2685 * @param aListener the listener object to remove |
|
2686 */ |
|
2687 removeListener: function(aListener) { |
|
2688 CustomizableUIInternal.removeListener(aListener); |
|
2689 }, |
|
2690 |
|
2691 /** |
|
2692 * Register a customizable area with CustomizableUI. |
|
2693 * @param aName the name of the area to register. Can only contain |
|
2694 * alphanumeric characters, dashes (-) and underscores (_). |
|
2695 * @param aProps the properties of the area. The following properties are |
|
2696 * recognized: |
|
2697 * - type: the type of area. Either TYPE_TOOLBAR (default) or |
|
2698 * TYPE_MENU_PANEL; |
|
2699 * - anchor: for a menu panel or overflowable toolbar, the |
|
2700 * anchoring node for the panel. |
|
2701 * - legacy: set to true if you want customizableui to |
|
2702 * automatically migrate the currentset attribute |
|
2703 * - overflowable: set to true if your toolbar is overflowable. |
|
2704 * This requires an anchor, and only has an |
|
2705 * effect for toolbars. |
|
2706 * - defaultPlacements: an array of widget IDs making up the |
|
2707 * default contents of the area |
|
2708 * - defaultCollapsed: (INTERNAL ONLY) applies if the type is TYPE_TOOLBAR, specifies |
|
2709 * if toolbar is collapsed by default (default to true). |
|
2710 * Specify null to ensure that reset/inDefaultArea don't care |
|
2711 * about a toolbar's collapsed state |
|
2712 */ |
|
2713 registerArea: function(aName, aProperties) { |
|
2714 CustomizableUIInternal.registerArea(aName, aProperties); |
|
2715 }, |
|
2716 /** |
|
2717 * Register a concrete node for a registered area. This method is automatically |
|
2718 * called from any toolbar in the main browser window that has its |
|
2719 * "customizable" attribute set to true. There should normally be no need to |
|
2720 * call it yourself. |
|
2721 * |
|
2722 * Note that ideally, you should register your toolbar using registerArea |
|
2723 * before any of the toolbars have their XBL bindings constructed (which |
|
2724 * will happen when they're added to the DOM and are not hidden). If you |
|
2725 * don't, and your toolbar has a defaultset attribute, CustomizableUI will |
|
2726 * register it automatically. If your toolbar does not have a defaultset |
|
2727 * attribute, the node will be saved for processing when you call |
|
2728 * registerArea. Note that CustomizableUI won't restore state in the area, |
|
2729 * allow the user to customize it in customize mode, or otherwise deal |
|
2730 * with it, until the area has been registered. |
|
2731 */ |
|
2732 registerToolbarNode: function(aToolbar, aExistingChildren) { |
|
2733 CustomizableUIInternal.registerToolbarNode(aToolbar, aExistingChildren); |
|
2734 }, |
|
2735 /** |
|
2736 * Register the menu panel node. This method should not be called by anyone |
|
2737 * apart from the built-in PanelUI. |
|
2738 * @param aPanel the panel DOM node being registered. |
|
2739 */ |
|
2740 registerMenuPanel: function(aPanel) { |
|
2741 CustomizableUIInternal.registerMenuPanel(aPanel); |
|
2742 }, |
|
2743 /** |
|
2744 * Unregister a customizable area. The inverse of registerArea. |
|
2745 * |
|
2746 * Unregistering an area will remove all the (removable) widgets in the |
|
2747 * area, which will return to the panel, and destroy all other traces |
|
2748 * of the area within CustomizableUI. Note that this means the *contents* |
|
2749 * of the area's DOM nodes will be moved to the panel or removed, but |
|
2750 * the area's DOM nodes *themselves* will stay. |
|
2751 * |
|
2752 * Furthermore, by default the placements of the area will be kept in the |
|
2753 * saved state (!) and restored if you re-register the area at a later |
|
2754 * point. This is useful for e.g. add-ons that get disabled and then |
|
2755 * re-enabled (e.g. when they update). |
|
2756 * |
|
2757 * You can override this last behaviour (and destroy the placements |
|
2758 * information in the saved state) by passing true for aDestroyPlacements. |
|
2759 * |
|
2760 * @param aName the name of the area to unregister |
|
2761 * @param aDestroyPlacements whether to destroy the placements information |
|
2762 * for the area, too. |
|
2763 */ |
|
2764 unregisterArea: function(aName, aDestroyPlacements) { |
|
2765 CustomizableUIInternal.unregisterArea(aName, aDestroyPlacements); |
|
2766 }, |
|
2767 /** |
|
2768 * Add a widget to an area. |
|
2769 * If the area to which you try to add is not known to CustomizableUI, |
|
2770 * this will throw. |
|
2771 * If the area to which you try to add has not yet been restored from its |
|
2772 * legacy state, this will postpone the addition. |
|
2773 * If the area to which you try to add is the same as the area in which |
|
2774 * the widget is currently placed, this will do the same as |
|
2775 * moveWidgetWithinArea. |
|
2776 * If the widget cannot be removed from its original location, this will |
|
2777 * no-op. |
|
2778 * |
|
2779 * This will fire an onWidgetAdded notification, |
|
2780 * and an onWidgetBeforeDOMChange and onWidgetAfterDOMChange notification |
|
2781 * for each window CustomizableUI knows about. |
|
2782 * |
|
2783 * @param aWidgetId the ID of the widget to add |
|
2784 * @param aArea the ID of the area to add the widget to |
|
2785 * @param aPosition the position at which to add the widget. If you do not |
|
2786 * pass a position, the widget will be added to the end |
|
2787 * of the area. |
|
2788 */ |
|
2789 addWidgetToArea: function(aWidgetId, aArea, aPosition) { |
|
2790 CustomizableUIInternal.addWidgetToArea(aWidgetId, aArea, aPosition); |
|
2791 }, |
|
2792 /** |
|
2793 * Remove a widget from its area. If the widget cannot be removed from its |
|
2794 * area, or is not in any area, this will no-op. Otherwise, this will fire an |
|
2795 * onWidgetRemoved notification, and an onWidgetBeforeDOMChange and |
|
2796 * onWidgetAfterDOMChange notification for each window CustomizableUI knows |
|
2797 * about. |
|
2798 * |
|
2799 * @param aWidgetId the ID of the widget to remove |
|
2800 */ |
|
2801 removeWidgetFromArea: function(aWidgetId) { |
|
2802 CustomizableUIInternal.removeWidgetFromArea(aWidgetId); |
|
2803 }, |
|
2804 /** |
|
2805 * Move a widget within an area. |
|
2806 * If the widget is not in any area, this will no-op. |
|
2807 * If the widget is already at the indicated position, this will no-op. |
|
2808 * |
|
2809 * Otherwise, this will move the widget and fire an onWidgetMoved notification, |
|
2810 * and an onWidgetBeforeDOMChange and onWidgetAfterDOMChange notification for |
|
2811 * each window CustomizableUI knows about. |
|
2812 * |
|
2813 * @param aWidgetId the ID of the widget to move |
|
2814 * @param aPosition the position to move the widget to. |
|
2815 * Negative values or values greater than the number of |
|
2816 * widgets will be interpreted to mean moving the widget to |
|
2817 * respectively the first or last position. |
|
2818 */ |
|
2819 moveWidgetWithinArea: function(aWidgetId, aPosition) { |
|
2820 CustomizableUIInternal.moveWidgetWithinArea(aWidgetId, aPosition); |
|
2821 }, |
|
2822 /** |
|
2823 * Ensure a XUL-based widget created in a window after areas were |
|
2824 * initialized moves to its correct position. |
|
2825 * This is roughly equivalent to manually looking up the position and using |
|
2826 * insertItem in the old API, but a lot less work for consumers. |
|
2827 * Always prefer this over using toolbar.insertItem (which might no-op |
|
2828 * because it delegates to addWidgetToArea) or, worse, moving items in the |
|
2829 * DOM yourself. |
|
2830 * |
|
2831 * @param aWidgetId the ID of the widget that was just created |
|
2832 * @param aWindow the window in which you want to ensure it was added. |
|
2833 * |
|
2834 * NB: why is this API per-window, you wonder? Because if you need this, |
|
2835 * presumably you yourself need to create the widget in all the windows |
|
2836 * and need to loop through them anyway. |
|
2837 */ |
|
2838 ensureWidgetPlacedInWindow: function(aWidgetId, aWindow) { |
|
2839 return CustomizableUIInternal.ensureWidgetPlacedInWindow(aWidgetId, aWindow); |
|
2840 }, |
|
2841 /** |
|
2842 * Start a batch update of items. |
|
2843 * During a batch update, the customization state is not saved to the user's |
|
2844 * preferences file, in order to reduce (possibly sync) IO. |
|
2845 * Calls to begin/endBatchUpdate may be nested. |
|
2846 * |
|
2847 * Callers should ensure that NO MATTER WHAT they call endBatchUpdate once |
|
2848 * for each call to beginBatchUpdate, even if there are exceptions in the |
|
2849 * code in the batch update. Otherwise, for the duration of the |
|
2850 * Firefox session, customization state is never saved. Typically, you |
|
2851 * would do this using a try...finally block. |
|
2852 */ |
|
2853 beginBatchUpdate: function() { |
|
2854 CustomizableUIInternal.beginBatchUpdate(); |
|
2855 }, |
|
2856 /** |
|
2857 * End a batch update. See the documentation for beginBatchUpdate above. |
|
2858 * |
|
2859 * State is not saved if we believe it is identical to the last known |
|
2860 * saved state. State is only ever saved when all batch updates have |
|
2861 * finished (ie there has been 1 endBatchUpdate call for each |
|
2862 * beginBatchUpdate call). If any of the endBatchUpdate calls pass |
|
2863 * aForceDirty=true, we will flush to the prefs file. |
|
2864 * |
|
2865 * @param aForceDirty force CustomizableUI to flush to the prefs file when |
|
2866 * all batch updates have finished. |
|
2867 */ |
|
2868 endBatchUpdate: function(aForceDirty) { |
|
2869 CustomizableUIInternal.endBatchUpdate(aForceDirty); |
|
2870 }, |
|
2871 /** |
|
2872 * Create a widget. |
|
2873 * |
|
2874 * To create a widget, you should pass an object with its desired |
|
2875 * properties. The following properties are supported: |
|
2876 * |
|
2877 * - id: the ID of the widget (required). |
|
2878 * - type: a string indicating the type of widget. Possible types |
|
2879 * are: |
|
2880 * 'button' - for simple button widgets (the default) |
|
2881 * 'view' - for buttons that open a panel or subview, |
|
2882 * depending on where they are placed. |
|
2883 * 'custom' - for fine-grained control over the creation |
|
2884 * of the widget. |
|
2885 * - viewId: Only useful for views (and required there): the id of the |
|
2886 * <panelview> that should be shown when clicking the widget. |
|
2887 * - onBuild(aDoc): Only useful for custom widgets (and required there); a |
|
2888 * function that will be invoked with the document in which |
|
2889 * to build a widget. Should return the DOM node that has |
|
2890 * been constructed. |
|
2891 * - onBeforeCreated(aDoc): Attached to all non-custom widgets; a function |
|
2892 * that will be invoked before the widget gets a DOM node |
|
2893 * constructed, passing the document in which that will happen. |
|
2894 * This is useful especially for 'view' type widgets that need |
|
2895 * to construct their views on the fly (e.g. from bootstrapped |
|
2896 * add-ons) |
|
2897 * - onCreated(aNode): Attached to all widgets; a function that will be invoked |
|
2898 * whenever the widget has a DOM node constructed, passing the |
|
2899 * constructed node as an argument. |
|
2900 * - onCommand(aEvt): Only useful for button widgets; a function that will be |
|
2901 * invoked when the user activates the button. |
|
2902 * - onClick(aEvt): Attached to all widgets; a function that will be invoked |
|
2903 * when the user clicks the widget. |
|
2904 * - onViewShowing(aEvt): Only useful for views; a function that will be |
|
2905 * invoked when a user shows your view. |
|
2906 * - onViewHiding(aEvt): Only useful for views; a function that will be |
|
2907 * invoked when a user hides your view. |
|
2908 * - tooltiptext: string to use for the tooltip of the widget |
|
2909 * - label: string to use for the label of the widget |
|
2910 * - removable: whether the widget is removable (optional, default: true) |
|
2911 * NB: if you specify false here, you must provide a |
|
2912 * defaultArea, too. |
|
2913 * - overflows: whether widget can overflow when in an overflowable |
|
2914 * toolbar (optional, default: true) |
|
2915 * - defaultArea: default area to add the widget to |
|
2916 * (optional, default: none; required if non-removable) |
|
2917 * - shortcutId: id of an element that has a shortcut for this widget |
|
2918 * (optional, default: null). This is only used to display |
|
2919 * the shortcut as part of the tooltip for builtin widgets |
|
2920 * (which have strings inside |
|
2921 * customizableWidgets.properties). If you're in an add-on, |
|
2922 * you should not set this property. |
|
2923 * - showInPrivateBrowsing: whether to show the widget in private browsing |
|
2924 * mode (optional, default: true) |
|
2925 * |
|
2926 * @param aProperties the specifications for the widget. |
|
2927 * @return a wrapper around the created widget (see getWidget) |
|
2928 */ |
|
2929 createWidget: function(aProperties) { |
|
2930 return CustomizableUIInternal.wrapWidget( |
|
2931 CustomizableUIInternal.createWidget(aProperties) |
|
2932 ); |
|
2933 }, |
|
2934 /** |
|
2935 * Destroy a widget |
|
2936 * |
|
2937 * If the widget is part of the default placements in an area, this will |
|
2938 * remove it from there. It will also remove any DOM instances. However, |
|
2939 * it will keep the widget in the placements for whatever area it was |
|
2940 * in at the time. You can remove it from there yourself by calling |
|
2941 * CustomizableUI.removeWidgetFromArea(aWidgetId). |
|
2942 * |
|
2943 * @param aWidgetId the ID of the widget to destroy |
|
2944 */ |
|
2945 destroyWidget: function(aWidgetId) { |
|
2946 CustomizableUIInternal.destroyWidget(aWidgetId); |
|
2947 }, |
|
2948 /** |
|
2949 * Get a wrapper object with information about the widget. |
|
2950 * The object provides the following properties |
|
2951 * (all read-only unless otherwise indicated): |
|
2952 * |
|
2953 * - id: the widget's ID; |
|
2954 * - type: the type of widget (button, view, custom). For |
|
2955 * XUL-provided widgets, this is always 'custom'; |
|
2956 * - provider: the provider type of the widget, id est one of |
|
2957 * PROVIDER_API or PROVIDER_XUL; |
|
2958 * - forWindow(w): a method to obtain a single window wrapper for a widget, |
|
2959 * in the window w passed as the only argument; |
|
2960 * - instances: an array of all instances (single window wrappers) |
|
2961 * of the widget. This array is NOT live; |
|
2962 * - areaType: the type of the widget's current area |
|
2963 * - isGroup: true; will be false for wrappers around single widget nodes; |
|
2964 * - source: for API-provided widgets, whether they are built-in to |
|
2965 * Firefox or add-on-provided; |
|
2966 * - disabled: for API-provided widgets, whether the widget is currently |
|
2967 * disabled. NB: this property is writable, and will toggle |
|
2968 * all the widgets' nodes' disabled states; |
|
2969 * - label: for API-provied widgets, the label of the widget; |
|
2970 * - tooltiptext: for API-provided widgets, the tooltip of the widget; |
|
2971 * - showInPrivateBrowsing: for API-provided widgets, whether the widget is |
|
2972 * visible in private browsing; |
|
2973 * |
|
2974 * Single window wrappers obtained through forWindow(someWindow) or from the |
|
2975 * instances array have the following properties |
|
2976 * (all read-only unless otherwise indicated): |
|
2977 * |
|
2978 * - id: the widget's ID; |
|
2979 * - type: the type of widget (button, view, custom). For |
|
2980 * XUL-provided widgets, this is always 'custom'; |
|
2981 * - provider: the provider type of the widget, id est one of |
|
2982 * PROVIDER_API or PROVIDER_XUL; |
|
2983 * - node: reference to the corresponding DOM node; |
|
2984 * - anchor: the anchor on which to anchor panels opened from this |
|
2985 * node. This will point to the overflow chevron on |
|
2986 * overflowable toolbars if and only if your widget node |
|
2987 * is overflowed, to the anchor for the panel menu |
|
2988 * if your widget is inside the panel menu, and to the |
|
2989 * node itself in all other cases; |
|
2990 * - overflowed: boolean indicating whether the node is currently in the |
|
2991 * overflow panel of the toolbar; |
|
2992 * - isGroup: false; will be true for the group widget; |
|
2993 * - label: for API-provided widgets, convenience getter for the |
|
2994 * label attribute of the DOM node; |
|
2995 * - tooltiptext: for API-provided widgets, convenience getter for the |
|
2996 * tooltiptext attribute of the DOM node; |
|
2997 * - disabled: for API-provided widgets, convenience getter *and setter* |
|
2998 * for the disabled state of this single widget. Note that |
|
2999 * you may prefer to use the group wrapper's getter/setter |
|
3000 * instead. |
|
3001 * |
|
3002 * @param aWidgetId the ID of the widget whose information you need |
|
3003 * @return a wrapper around the widget as described above, or null if the |
|
3004 * widget is known not to exist (anymore). NB: non-null return |
|
3005 * is no guarantee the widget exists because we cannot know in |
|
3006 * advance if a XUL widget exists or not. |
|
3007 */ |
|
3008 getWidget: function(aWidgetId) { |
|
3009 return CustomizableUIInternal.wrapWidget(aWidgetId); |
|
3010 }, |
|
3011 /** |
|
3012 * Get an array of widget wrappers (see getWidget) for all the widgets |
|
3013 * which are currently not in any area (so which are in the palette). |
|
3014 * |
|
3015 * @param aWindowPalette the palette (and by extension, the window) in which |
|
3016 * CustomizableUI should look. This matters because of |
|
3017 * course XUL-provided widgets could be available in |
|
3018 * some windows but not others, and likewise |
|
3019 * API-provided widgets might not exist in a private |
|
3020 * window (because of the showInPrivateBrowsing |
|
3021 * property). |
|
3022 * |
|
3023 * @return an array of widget wrappers (see getWidget) |
|
3024 */ |
|
3025 getUnusedWidgets: function(aWindowPalette) { |
|
3026 return CustomizableUIInternal.getUnusedWidgets(aWindowPalette).map( |
|
3027 CustomizableUIInternal.wrapWidget, |
|
3028 CustomizableUIInternal |
|
3029 ); |
|
3030 }, |
|
3031 /** |
|
3032 * Get an array of all the widget IDs placed in an area. This is roughly |
|
3033 * equivalent to fetching the currentset attribute and splitting by commas |
|
3034 * in the legacy APIs. Modifying the array will not affect CustomizableUI. |
|
3035 * |
|
3036 * @param aArea the ID of the area whose placements you want to obtain. |
|
3037 * @return an array containing the widget IDs that are in the area. |
|
3038 * |
|
3039 * NB: will throw if called too early (before placements have been fetched) |
|
3040 * or if the area is not currently known to CustomizableUI. |
|
3041 */ |
|
3042 getWidgetIdsInArea: function(aArea) { |
|
3043 if (!gAreas.has(aArea)) { |
|
3044 throw new Error("Unknown customization area: " + aArea); |
|
3045 } |
|
3046 if (!gPlacements.has(aArea)) { |
|
3047 throw new Error("Area not yet restored"); |
|
3048 } |
|
3049 |
|
3050 // We need to clone this, as we don't want to let consumers muck with placements |
|
3051 return [...gPlacements.get(aArea)]; |
|
3052 }, |
|
3053 /** |
|
3054 * Get an array of widget wrappers for all the widgets in an area. This is |
|
3055 * the same as calling getWidgetIdsInArea and .map() ing the result through |
|
3056 * CustomizableUI.getWidget. Careful: this means that if there are IDs in there |
|
3057 * which don't have corresponding DOM nodes (like in the old-style currentset |
|
3058 * attribute), there might be nulls in this array, or items for which |
|
3059 * wrapper.forWindow(win) will return null. |
|
3060 * |
|
3061 * @param aArea the ID of the area whose widgets you want to obtain. |
|
3062 * @return an array of widget wrappers and/or null values for the widget IDs |
|
3063 * placed in an area. |
|
3064 * |
|
3065 * NB: will throw if called too early (before placements have been fetched) |
|
3066 * or if the area is not currently known to CustomizableUI. |
|
3067 */ |
|
3068 getWidgetsInArea: function(aArea) { |
|
3069 return this.getWidgetIdsInArea(aArea).map( |
|
3070 CustomizableUIInternal.wrapWidget, |
|
3071 CustomizableUIInternal |
|
3072 ); |
|
3073 }, |
|
3074 /** |
|
3075 * Obtain an array of all the area IDs known to CustomizableUI. |
|
3076 * This array is created for you, so is modifiable without CustomizableUI |
|
3077 * being affected. |
|
3078 */ |
|
3079 get areas() { |
|
3080 return [area for ([area, props] of gAreas)]; |
|
3081 }, |
|
3082 /** |
|
3083 * Check what kind of area (toolbar or menu panel) an area is. This is |
|
3084 * useful if you have a widget that needs to behave differently depending |
|
3085 * on its location. Note that widget wrappers have a convenience getter |
|
3086 * property (areaType) for this purpose. |
|
3087 * |
|
3088 * @param aArea the ID of the area whose type you want to know |
|
3089 * @return TYPE_TOOLBAR or TYPE_MENU_PANEL depending on the area, null if |
|
3090 * the area is unknown. |
|
3091 */ |
|
3092 getAreaType: function(aArea) { |
|
3093 let area = gAreas.get(aArea); |
|
3094 return area ? area.get("type") : null; |
|
3095 }, |
|
3096 /** |
|
3097 * Check if a toolbar is collapsed by default. |
|
3098 * |
|
3099 * @param aArea the ID of the area whose default-collapsed state you want to know. |
|
3100 * @return `true` or `false` depending on the area, null if the area is unknown, |
|
3101 * or its collapsed state cannot normally be controlled by the user |
|
3102 */ |
|
3103 isToolbarDefaultCollapsed: function(aArea) { |
|
3104 let area = gAreas.get(aArea); |
|
3105 return area ? area.get("defaultCollapsed") : null; |
|
3106 }, |
|
3107 /** |
|
3108 * Obtain the DOM node that is the customize target for an area in a |
|
3109 * specific window. |
|
3110 * |
|
3111 * Areas can have a customization target that does not correspond to the |
|
3112 * node itself. In particular, toolbars that have a customizationtarget |
|
3113 * attribute set will have their customization target set to that node. |
|
3114 * This means widgets will end up in the customization target, not in the |
|
3115 * DOM node with the ID that corresponds to the area ID. This is useful |
|
3116 * because it lets you have fixed content in a toolbar (e.g. the panel |
|
3117 * menu item in the navbar) and have all the customizable widgets use |
|
3118 * the customization target. |
|
3119 * |
|
3120 * Using this API yourself is discouraged; you should generally not need |
|
3121 * to be asking for the DOM container node used for a particular area. |
|
3122 * In particular, if you're wanting to check it in relation to a widget's |
|
3123 * node, your DOM node might not be a direct child of the customize target |
|
3124 * in a window if, for instance, the window is in customization mode, or if |
|
3125 * this is an overflowable toolbar and the widget has been overflowed. |
|
3126 * |
|
3127 * @param aArea the ID of the area whose customize target you want to have |
|
3128 * @param aWindow the window where you want to fetch the DOM node. |
|
3129 * @return the customize target DOM node for aArea in aWindow |
|
3130 */ |
|
3131 getCustomizeTargetForArea: function(aArea, aWindow) { |
|
3132 return CustomizableUIInternal.getCustomizeTargetForArea(aArea, aWindow); |
|
3133 }, |
|
3134 /** |
|
3135 * Reset the customization state back to its default. |
|
3136 * |
|
3137 * This is the nuclear option. You should never call this except if the user |
|
3138 * explicitly requests it. Firefox does this when the user clicks the |
|
3139 * "Restore Defaults" button in customize mode. |
|
3140 */ |
|
3141 reset: function() { |
|
3142 CustomizableUIInternal.reset(); |
|
3143 }, |
|
3144 |
|
3145 /** |
|
3146 * Undo the previous reset, can only be called immediately after a reset. |
|
3147 * @return a promise that will be resolved when the operation is complete. |
|
3148 */ |
|
3149 undoReset: function() { |
|
3150 CustomizableUIInternal.undoReset(); |
|
3151 }, |
|
3152 |
|
3153 /** |
|
3154 * Remove a custom toolbar added in a previous version of Firefox or using |
|
3155 * an add-on. NB: only works on the customizable toolbars generated by |
|
3156 * the toolbox itself. Intended for use from CustomizeMode, not by |
|
3157 * other consumers. |
|
3158 * @param aToolbarId the ID of the toolbar to remove |
|
3159 */ |
|
3160 removeExtraToolbar: function(aToolbarId) { |
|
3161 CustomizableUIInternal.removeExtraToolbar(aToolbarId); |
|
3162 }, |
|
3163 |
|
3164 /** |
|
3165 * Can the last Restore Defaults operation be undone. |
|
3166 * |
|
3167 * @return A boolean stating whether an undo of the |
|
3168 * Restore Defaults can be performed. |
|
3169 */ |
|
3170 get canUndoReset() { |
|
3171 return gUIStateBeforeReset.uiCustomizationState != null || |
|
3172 gUIStateBeforeReset.drawInTitlebar != null; |
|
3173 }, |
|
3174 |
|
3175 /** |
|
3176 * Get the placement of a widget. This is by far the best way to obtain |
|
3177 * information about what the state of your widget is. The internals of |
|
3178 * this call are cheap (no DOM necessary) and you will know where the user |
|
3179 * has put your widget. |
|
3180 * |
|
3181 * @param aWidgetId the ID of the widget whose placement you want to know |
|
3182 * @return |
|
3183 * { |
|
3184 * area: "somearea", // The ID of the area where the widget is placed |
|
3185 * position: 42 // the index in the placements array corresponding to |
|
3186 * // your widget. |
|
3187 * } |
|
3188 * |
|
3189 * OR |
|
3190 * |
|
3191 * null // if the widget is not placed anywhere (ie in the palette) |
|
3192 */ |
|
3193 getPlacementOfWidget: function(aWidgetId) { |
|
3194 return CustomizableUIInternal.getPlacementOfWidget(aWidgetId, true); |
|
3195 }, |
|
3196 /** |
|
3197 * Check if a widget can be removed from the area it's in. |
|
3198 * |
|
3199 * Note that if you're wanting to move the widget somewhere, you should |
|
3200 * generally be checking canWidgetMoveToArea, because that will return |
|
3201 * true if the widget is already in the area where you want to move it (!). |
|
3202 * |
|
3203 * NB: oh, also, this method might lie if the widget in question is a |
|
3204 * XUL-provided widget and there are no windows open, because it |
|
3205 * can obviously not check anything in this case. It will return |
|
3206 * true. You will be able to move the widget elsewhere. However, |
|
3207 * once the user reopens a window, the widget will move back to its |
|
3208 * 'proper' area automagically. |
|
3209 * |
|
3210 * @param aWidgetId a widget ID or DOM node to check |
|
3211 * @return true if the widget can be removed from its area, |
|
3212 * false otherwise. |
|
3213 */ |
|
3214 isWidgetRemovable: function(aWidgetId) { |
|
3215 return CustomizableUIInternal.isWidgetRemovable(aWidgetId); |
|
3216 }, |
|
3217 /** |
|
3218 * Check if a widget can be moved to a particular area. Like |
|
3219 * isWidgetRemovable but better, because it'll return true if the widget |
|
3220 * is already in the right area. |
|
3221 * |
|
3222 * @param aWidgetId the widget ID or DOM node you want to move somewhere |
|
3223 * @param aArea the area ID you want to move it to. |
|
3224 * @return true if this is possible, false if it is not. The same caveats as |
|
3225 * for isWidgetRemovable apply, however, if no windows are open. |
|
3226 */ |
|
3227 canWidgetMoveToArea: function(aWidgetId, aArea) { |
|
3228 return CustomizableUIInternal.canWidgetMoveToArea(aWidgetId, aArea); |
|
3229 }, |
|
3230 /** |
|
3231 * Whether we're in a default state. Note that non-removable non-default |
|
3232 * widgets and non-existing widgets are not taken into account in determining |
|
3233 * whether we're in the default state. |
|
3234 * |
|
3235 * NB: this is a property with a getter. The getter is NOT cheap, because |
|
3236 * it does smart things with non-removable non-default items, non-existent |
|
3237 * items, and so forth. Please don't call unless necessary. |
|
3238 */ |
|
3239 get inDefaultState() { |
|
3240 return CustomizableUIInternal.inDefaultState; |
|
3241 }, |
|
3242 |
|
3243 /** |
|
3244 * Set a toolbar's visibility state in all windows. |
|
3245 * @param aToolbarId the toolbar whose visibility should be adjusted |
|
3246 * @param aIsVisible whether the toolbar should be visible |
|
3247 */ |
|
3248 setToolbarVisibility: function(aToolbarId, aIsVisible) { |
|
3249 CustomizableUIInternal.setToolbarVisibility(aToolbarId, aIsVisible); |
|
3250 }, |
|
3251 |
|
3252 /** |
|
3253 * Get a localized property off a (widget?) object. |
|
3254 * |
|
3255 * NB: this is unlikely to be useful unless you're in Firefox code, because |
|
3256 * this code uses the builtin widget stringbundle, and can't be told |
|
3257 * to use add-on-provided strings. It's mainly here as convenience for |
|
3258 * custom builtin widgets that build their own DOM but use the same |
|
3259 * stringbundle as the other builtin widgets. |
|
3260 * |
|
3261 * @param aWidget the object whose property we should use to fetch a |
|
3262 * localizable string; |
|
3263 * @param aProp the property on the object to use for the fetching; |
|
3264 * @param aFormatArgs (optional) any extra arguments to use for a formatted |
|
3265 * string; |
|
3266 * @param aDef (optional) the default to return if we don't find the |
|
3267 * string in the stringbundle; |
|
3268 * |
|
3269 * @return the localized string, or aDef if the string isn't in the bundle. |
|
3270 * If no default is provided, |
|
3271 * if aProp exists on aWidget, we'll return that, |
|
3272 * otherwise we'll return the empty string |
|
3273 * |
|
3274 */ |
|
3275 getLocalizedProperty: function(aWidget, aProp, aFormatArgs, aDef) { |
|
3276 return CustomizableUIInternal.getLocalizedProperty(aWidget, aProp, |
|
3277 aFormatArgs, aDef); |
|
3278 }, |
|
3279 /** |
|
3280 * Given a node, walk up to the first panel in its ancestor chain, and |
|
3281 * close it. |
|
3282 * |
|
3283 * @param aNode a node whose panel should be closed; |
|
3284 */ |
|
3285 hidePanelForNode: function(aNode) { |
|
3286 CustomizableUIInternal.hidePanelForNode(aNode); |
|
3287 }, |
|
3288 /** |
|
3289 * Check if a widget is a "special" widget: a spring, spacer or separator. |
|
3290 * |
|
3291 * @param aWidgetId the widget ID to check. |
|
3292 * @return true if the widget is 'special', false otherwise. |
|
3293 */ |
|
3294 isSpecialWidget: function(aWidgetId) { |
|
3295 return CustomizableUIInternal.isSpecialWidget(aWidgetId); |
|
3296 }, |
|
3297 /** |
|
3298 * Add listeners to a panel that will close it. For use from the menu panel |
|
3299 * and overflowable toolbar implementations, unlikely to be useful for |
|
3300 * consumers. |
|
3301 * |
|
3302 * @param aPanel the panel to which listeners should be attached. |
|
3303 */ |
|
3304 addPanelCloseListeners: function(aPanel) { |
|
3305 CustomizableUIInternal.addPanelCloseListeners(aPanel); |
|
3306 }, |
|
3307 /** |
|
3308 * Remove close listeners that have been added to a panel with |
|
3309 * addPanelCloseListeners. For use from the menu panel and overflowable |
|
3310 * toolbar implementations, unlikely to be useful for consumers. |
|
3311 * |
|
3312 * @param aPanel the panel from which listeners should be removed. |
|
3313 */ |
|
3314 removePanelCloseListeners: function(aPanel) { |
|
3315 CustomizableUIInternal.removePanelCloseListeners(aPanel); |
|
3316 }, |
|
3317 /** |
|
3318 * Notify listeners a widget is about to be dragged to an area. For use from |
|
3319 * Customize Mode only, do not use otherwise. |
|
3320 * |
|
3321 * @param aWidgetId the ID of the widget that is being dragged to an area. |
|
3322 * @param aArea the ID of the area to which the widget is being dragged. |
|
3323 */ |
|
3324 onWidgetDrag: function(aWidgetId, aArea) { |
|
3325 CustomizableUIInternal.notifyListeners("onWidgetDrag", aWidgetId, aArea); |
|
3326 }, |
|
3327 /** |
|
3328 * Notify listeners that a window is entering customize mode. For use from |
|
3329 * Customize Mode only, do not use otherwise. |
|
3330 * @param aWindow the window entering customize mode |
|
3331 */ |
|
3332 notifyStartCustomizing: function(aWindow) { |
|
3333 CustomizableUIInternal.notifyListeners("onCustomizeStart", aWindow); |
|
3334 }, |
|
3335 /** |
|
3336 * Notify listeners that a window is exiting customize mode. For use from |
|
3337 * Customize Mode only, do not use otherwise. |
|
3338 * @param aWindow the window exiting customize mode |
|
3339 */ |
|
3340 notifyEndCustomizing: function(aWindow) { |
|
3341 CustomizableUIInternal.notifyListeners("onCustomizeEnd", aWindow); |
|
3342 }, |
|
3343 |
|
3344 /** |
|
3345 * Notify toolbox(es) of a particular event. If you don't pass aWindow, |
|
3346 * all toolboxes will be notified. For use from Customize Mode only, |
|
3347 * do not use otherwise. |
|
3348 * @param aEvent the name of the event to send. |
|
3349 * @param aDetails optional, the details of the event. |
|
3350 * @param aWindow optional, the window in which to send the event. |
|
3351 */ |
|
3352 dispatchToolboxEvent: function(aEvent, aDetails={}, aWindow=null) { |
|
3353 CustomizableUIInternal.dispatchToolboxEvent(aEvent, aDetails, aWindow); |
|
3354 }, |
|
3355 |
|
3356 /** |
|
3357 * Check whether an area is overflowable. |
|
3358 * |
|
3359 * @param aAreaId the ID of an area to check for overflowable-ness |
|
3360 * @return true if the area is overflowable, false otherwise. |
|
3361 */ |
|
3362 isAreaOverflowable: function(aAreaId) { |
|
3363 let area = gAreas.get(aAreaId); |
|
3364 return area ? area.get("type") == this.TYPE_TOOLBAR && area.get("overflowable") |
|
3365 : false; |
|
3366 }, |
|
3367 /** |
|
3368 * Obtain a string indicating the place of an element. This is intended |
|
3369 * for use from customize mode; You should generally use getPlacementOfWidget |
|
3370 * instead, which is cheaper because it does not use the DOM. |
|
3371 * |
|
3372 * @param aElement the DOM node whose place we need to check |
|
3373 * @return "toolbar" if the node is in a toolbar, "panel" if it is in the |
|
3374 * menu panel, "palette" if it is in the (visible!) customization |
|
3375 * palette, undefined otherwise. |
|
3376 */ |
|
3377 getPlaceForItem: function(aElement) { |
|
3378 let place; |
|
3379 let node = aElement; |
|
3380 while (node && !place) { |
|
3381 if (node.localName == "toolbar") |
|
3382 place = "toolbar"; |
|
3383 else if (node.id == CustomizableUI.AREA_PANEL) |
|
3384 place = "panel"; |
|
3385 else if (node.id == "customization-palette") |
|
3386 place = "palette"; |
|
3387 |
|
3388 node = node.parentNode; |
|
3389 } |
|
3390 return place; |
|
3391 }, |
|
3392 |
|
3393 /** |
|
3394 * Check if a toolbar is builtin or not. |
|
3395 * @param aToolbarId the ID of the toolbar you want to check |
|
3396 */ |
|
3397 isBuiltinToolbar: function(aToolbarId) { |
|
3398 return CustomizableUIInternal._builtinToolbars.has(aToolbarId); |
|
3399 }, |
|
3400 }; |
|
3401 Object.freeze(this.CustomizableUI); |
|
3402 Object.freeze(this.CustomizableUI.windows); |
|
3403 |
|
3404 /** |
|
3405 * All external consumers of widgets are really interacting with these wrappers |
|
3406 * which provide a common interface. |
|
3407 */ |
|
3408 |
|
3409 /** |
|
3410 * WidgetGroupWrapper is the common interface for interacting with an entire |
|
3411 * widget group - AKA, all instances of a widget across a series of windows. |
|
3412 * This particular wrapper is only used for widgets created via the provider |
|
3413 * API. |
|
3414 */ |
|
3415 function WidgetGroupWrapper(aWidget) { |
|
3416 this.isGroup = true; |
|
3417 |
|
3418 const kBareProps = ["id", "source", "type", "disabled", "label", "tooltiptext", |
|
3419 "showInPrivateBrowsing"]; |
|
3420 for (let prop of kBareProps) { |
|
3421 let propertyName = prop; |
|
3422 this.__defineGetter__(propertyName, function() aWidget[propertyName]); |
|
3423 } |
|
3424 |
|
3425 this.__defineGetter__("provider", function() CustomizableUI.PROVIDER_API); |
|
3426 |
|
3427 this.__defineSetter__("disabled", function(aValue) { |
|
3428 aValue = !!aValue; |
|
3429 aWidget.disabled = aValue; |
|
3430 for (let [,instance] of aWidget.instances) { |
|
3431 instance.disabled = aValue; |
|
3432 } |
|
3433 }); |
|
3434 |
|
3435 this.forWindow = function WidgetGroupWrapper_forWindow(aWindow) { |
|
3436 let wrapperMap; |
|
3437 if (!gSingleWrapperCache.has(aWindow)) { |
|
3438 wrapperMap = new Map(); |
|
3439 gSingleWrapperCache.set(aWindow, wrapperMap); |
|
3440 } else { |
|
3441 wrapperMap = gSingleWrapperCache.get(aWindow); |
|
3442 } |
|
3443 if (wrapperMap.has(aWidget.id)) { |
|
3444 return wrapperMap.get(aWidget.id); |
|
3445 } |
|
3446 |
|
3447 let instance = aWidget.instances.get(aWindow.document); |
|
3448 if (!instance && |
|
3449 (aWidget.showInPrivateBrowsing || !PrivateBrowsingUtils.isWindowPrivate(aWindow))) { |
|
3450 instance = CustomizableUIInternal.buildWidget(aWindow.document, |
|
3451 aWidget); |
|
3452 } |
|
3453 |
|
3454 let wrapper = new WidgetSingleWrapper(aWidget, instance); |
|
3455 wrapperMap.set(aWidget.id, wrapper); |
|
3456 return wrapper; |
|
3457 }; |
|
3458 |
|
3459 this.__defineGetter__("instances", function() { |
|
3460 // Can't use gBuildWindows here because some areas load lazily: |
|
3461 let placement = CustomizableUIInternal.getPlacementOfWidget(aWidget.id); |
|
3462 if (!placement) { |
|
3463 return []; |
|
3464 } |
|
3465 let area = placement.area; |
|
3466 let buildAreas = gBuildAreas.get(area); |
|
3467 if (!buildAreas) { |
|
3468 return []; |
|
3469 } |
|
3470 return [this.forWindow(node.ownerDocument.defaultView) for (node of buildAreas)]; |
|
3471 }); |
|
3472 |
|
3473 this.__defineGetter__("areaType", function() { |
|
3474 let areaProps = gAreas.get(aWidget.currentArea); |
|
3475 return areaProps && areaProps.get("type"); |
|
3476 }); |
|
3477 |
|
3478 Object.freeze(this); |
|
3479 } |
|
3480 |
|
3481 /** |
|
3482 * A WidgetSingleWrapper is a wrapper around a single instance of a widget in |
|
3483 * a particular window. |
|
3484 */ |
|
3485 function WidgetSingleWrapper(aWidget, aNode) { |
|
3486 this.isGroup = false; |
|
3487 |
|
3488 this.node = aNode; |
|
3489 this.provider = CustomizableUI.PROVIDER_API; |
|
3490 |
|
3491 const kGlobalProps = ["id", "type"]; |
|
3492 for (let prop of kGlobalProps) { |
|
3493 this[prop] = aWidget[prop]; |
|
3494 } |
|
3495 |
|
3496 const kNodeProps = ["label", "tooltiptext"]; |
|
3497 for (let prop of kNodeProps) { |
|
3498 let propertyName = prop; |
|
3499 // Look at the node for these, instead of the widget data, to ensure the |
|
3500 // wrapper always reflects this live instance. |
|
3501 this.__defineGetter__(propertyName, |
|
3502 function() aNode.getAttribute(propertyName)); |
|
3503 } |
|
3504 |
|
3505 this.__defineGetter__("disabled", function() aNode.disabled); |
|
3506 this.__defineSetter__("disabled", function(aValue) { |
|
3507 aNode.disabled = !!aValue; |
|
3508 }); |
|
3509 |
|
3510 this.__defineGetter__("anchor", function() { |
|
3511 let anchorId; |
|
3512 // First check for an anchor for the area: |
|
3513 let placement = CustomizableUIInternal.getPlacementOfWidget(aWidget.id); |
|
3514 if (placement) { |
|
3515 anchorId = gAreas.get(placement.area).get("anchor"); |
|
3516 } |
|
3517 if (!anchorId) { |
|
3518 anchorId = aNode.getAttribute("cui-anchorid"); |
|
3519 } |
|
3520 |
|
3521 return anchorId ? aNode.ownerDocument.getElementById(anchorId) |
|
3522 : aNode; |
|
3523 }); |
|
3524 |
|
3525 this.__defineGetter__("overflowed", function() { |
|
3526 return aNode.getAttribute("overflowedItem") == "true"; |
|
3527 }); |
|
3528 |
|
3529 Object.freeze(this); |
|
3530 } |
|
3531 |
|
3532 /** |
|
3533 * XULWidgetGroupWrapper is the common interface for interacting with an entire |
|
3534 * widget group - AKA, all instances of a widget across a series of windows. |
|
3535 * This particular wrapper is only used for widgets created via the old-school |
|
3536 * XUL method (overlays, or programmatically injecting toolbaritems, or other |
|
3537 * such things). |
|
3538 */ |
|
3539 //XXXunf Going to need to hook this up to some events to keep it all live. |
|
3540 function XULWidgetGroupWrapper(aWidgetId) { |
|
3541 this.isGroup = true; |
|
3542 this.id = aWidgetId; |
|
3543 this.type = "custom"; |
|
3544 this.provider = CustomizableUI.PROVIDER_XUL; |
|
3545 |
|
3546 this.forWindow = function XULWidgetGroupWrapper_forWindow(aWindow) { |
|
3547 let wrapperMap; |
|
3548 if (!gSingleWrapperCache.has(aWindow)) { |
|
3549 wrapperMap = new Map(); |
|
3550 gSingleWrapperCache.set(aWindow, wrapperMap); |
|
3551 } else { |
|
3552 wrapperMap = gSingleWrapperCache.get(aWindow); |
|
3553 } |
|
3554 if (wrapperMap.has(aWidgetId)) { |
|
3555 return wrapperMap.get(aWidgetId); |
|
3556 } |
|
3557 |
|
3558 let instance = aWindow.document.getElementById(aWidgetId); |
|
3559 if (!instance) { |
|
3560 // Toolbar palettes aren't part of the document, so elements in there |
|
3561 // won't be found via document.getElementById(). |
|
3562 instance = aWindow.gNavToolbox.palette.getElementsByAttribute("id", aWidgetId)[0]; |
|
3563 } |
|
3564 |
|
3565 let wrapper = new XULWidgetSingleWrapper(aWidgetId, instance, aWindow.document); |
|
3566 wrapperMap.set(aWidgetId, wrapper); |
|
3567 return wrapper; |
|
3568 }; |
|
3569 |
|
3570 this.__defineGetter__("areaType", function() { |
|
3571 let placement = CustomizableUIInternal.getPlacementOfWidget(aWidgetId); |
|
3572 if (!placement) { |
|
3573 return null; |
|
3574 } |
|
3575 |
|
3576 let areaProps = gAreas.get(placement.area); |
|
3577 return areaProps && areaProps.get("type"); |
|
3578 }); |
|
3579 |
|
3580 this.__defineGetter__("instances", function() { |
|
3581 return [this.forWindow(win) for ([win,] of gBuildWindows)]; |
|
3582 }); |
|
3583 |
|
3584 Object.freeze(this); |
|
3585 } |
|
3586 |
|
3587 /** |
|
3588 * A XULWidgetSingleWrapper is a wrapper around a single instance of a XUL |
|
3589 * widget in a particular window. |
|
3590 */ |
|
3591 function XULWidgetSingleWrapper(aWidgetId, aNode, aDocument) { |
|
3592 this.isGroup = false; |
|
3593 |
|
3594 this.id = aWidgetId; |
|
3595 this.type = "custom"; |
|
3596 this.provider = CustomizableUI.PROVIDER_XUL; |
|
3597 |
|
3598 let weakDoc = Cu.getWeakReference(aDocument); |
|
3599 // If we keep a strong ref, the weak ref will never die, so null it out: |
|
3600 aDocument = null; |
|
3601 |
|
3602 this.__defineGetter__("node", function() { |
|
3603 // If we've set this to null (further down), we're sure there's nothing to |
|
3604 // be gotten here, so bail out early: |
|
3605 if (!weakDoc) { |
|
3606 return null; |
|
3607 } |
|
3608 if (aNode) { |
|
3609 // Return the last known node if it's still in the DOM... |
|
3610 if (aNode.ownerDocument.contains(aNode)) { |
|
3611 return aNode; |
|
3612 } |
|
3613 // ... or the toolbox |
|
3614 let toolbox = aNode.ownerDocument.defaultView.gNavToolbox; |
|
3615 if (toolbox && toolbox.palette && aNode.parentNode == toolbox.palette) { |
|
3616 return aNode; |
|
3617 } |
|
3618 // If it isn't, clear the cached value and fall through to the "slow" case: |
|
3619 aNode = null; |
|
3620 } |
|
3621 |
|
3622 let doc = weakDoc.get(); |
|
3623 if (doc) { |
|
3624 // Store locally so we can cache the result: |
|
3625 aNode = CustomizableUIInternal.findWidgetInWindow(aWidgetId, doc.defaultView); |
|
3626 return aNode; |
|
3627 } |
|
3628 // The weakref to the document is dead, we're done here forever more: |
|
3629 weakDoc = null; |
|
3630 return null; |
|
3631 }); |
|
3632 |
|
3633 this.__defineGetter__("anchor", function() { |
|
3634 let anchorId; |
|
3635 // First check for an anchor for the area: |
|
3636 let placement = CustomizableUIInternal.getPlacementOfWidget(aWidgetId); |
|
3637 if (placement) { |
|
3638 anchorId = gAreas.get(placement.area).get("anchor"); |
|
3639 } |
|
3640 |
|
3641 let node = this.node; |
|
3642 if (!anchorId && node) { |
|
3643 anchorId = node.getAttribute("cui-anchorid"); |
|
3644 } |
|
3645 |
|
3646 return (anchorId && node) ? node.ownerDocument.getElementById(anchorId) : node; |
|
3647 }); |
|
3648 |
|
3649 this.__defineGetter__("overflowed", function() { |
|
3650 let node = this.node; |
|
3651 if (!node) { |
|
3652 return false; |
|
3653 } |
|
3654 return node.getAttribute("overflowedItem") == "true"; |
|
3655 }); |
|
3656 |
|
3657 Object.freeze(this); |
|
3658 } |
|
3659 |
|
3660 const LAZY_RESIZE_INTERVAL_MS = 200; |
|
3661 |
|
3662 function OverflowableToolbar(aToolbarNode) { |
|
3663 this._toolbar = aToolbarNode; |
|
3664 this._collapsed = new Map(); |
|
3665 this._enabled = true; |
|
3666 |
|
3667 this._toolbar.setAttribute("overflowable", "true"); |
|
3668 let doc = this._toolbar.ownerDocument; |
|
3669 this._target = this._toolbar.customizationTarget; |
|
3670 this._list = doc.getElementById(this._toolbar.getAttribute("overflowtarget")); |
|
3671 this._list.toolbox = this._toolbar.toolbox; |
|
3672 this._list.customizationTarget = this._list; |
|
3673 |
|
3674 let window = this._toolbar.ownerDocument.defaultView; |
|
3675 if (window.gBrowserInit.delayedStartupFinished) { |
|
3676 this.init(); |
|
3677 } else { |
|
3678 Services.obs.addObserver(this, "browser-delayed-startup-finished", false); |
|
3679 } |
|
3680 } |
|
3681 |
|
3682 OverflowableToolbar.prototype = { |
|
3683 initialized: false, |
|
3684 _forceOnOverflow: false, |
|
3685 |
|
3686 observe: function(aSubject, aTopic, aData) { |
|
3687 if (aTopic == "browser-delayed-startup-finished" && |
|
3688 aSubject == this._toolbar.ownerDocument.defaultView) { |
|
3689 Services.obs.removeObserver(this, "browser-delayed-startup-finished"); |
|
3690 this.init(); |
|
3691 } |
|
3692 }, |
|
3693 |
|
3694 init: function() { |
|
3695 let doc = this._toolbar.ownerDocument; |
|
3696 let window = doc.defaultView; |
|
3697 window.addEventListener("resize", this); |
|
3698 window.gNavToolbox.addEventListener("customizationstarting", this); |
|
3699 window.gNavToolbox.addEventListener("aftercustomization", this); |
|
3700 |
|
3701 let chevronId = this._toolbar.getAttribute("overflowbutton"); |
|
3702 this._chevron = doc.getElementById(chevronId); |
|
3703 this._chevron.addEventListener("command", this); |
|
3704 |
|
3705 let panelId = this._toolbar.getAttribute("overflowpanel"); |
|
3706 this._panel = doc.getElementById(panelId); |
|
3707 this._panel.addEventListener("popuphiding", this); |
|
3708 CustomizableUIInternal.addPanelCloseListeners(this._panel); |
|
3709 |
|
3710 CustomizableUI.addListener(this); |
|
3711 |
|
3712 // The 'overflow' event may have been fired before init was called. |
|
3713 if (this._toolbar.overflowedDuringConstruction) { |
|
3714 this.onOverflow(this._toolbar.overflowedDuringConstruction); |
|
3715 this._toolbar.overflowedDuringConstruction = null; |
|
3716 } |
|
3717 |
|
3718 this.initialized = true; |
|
3719 }, |
|
3720 |
|
3721 uninit: function() { |
|
3722 this._toolbar.removeEventListener("overflow", this._toolbar); |
|
3723 this._toolbar.removeEventListener("underflow", this._toolbar); |
|
3724 this._toolbar.removeAttribute("overflowable"); |
|
3725 |
|
3726 if (!this.initialized) { |
|
3727 Services.obs.removeObserver(this, "browser-delayed-startup-finished"); |
|
3728 return; |
|
3729 } |
|
3730 |
|
3731 this._disable(); |
|
3732 |
|
3733 let window = this._toolbar.ownerDocument.defaultView; |
|
3734 window.removeEventListener("resize", this); |
|
3735 window.gNavToolbox.removeEventListener("customizationstarting", this); |
|
3736 window.gNavToolbox.removeEventListener("aftercustomization", this); |
|
3737 this._chevron.removeEventListener("command", this); |
|
3738 this._panel.removeEventListener("popuphiding", this); |
|
3739 CustomizableUI.removeListener(this); |
|
3740 CustomizableUIInternal.removePanelCloseListeners(this._panel); |
|
3741 }, |
|
3742 |
|
3743 handleEvent: function(aEvent) { |
|
3744 switch(aEvent.type) { |
|
3745 case "resize": |
|
3746 this._onResize(aEvent); |
|
3747 break; |
|
3748 case "command": |
|
3749 if (aEvent.target == this._chevron) { |
|
3750 this._onClickChevron(aEvent); |
|
3751 } else { |
|
3752 this._panel.hidePopup(); |
|
3753 } |
|
3754 break; |
|
3755 case "popuphiding": |
|
3756 this._onPanelHiding(aEvent); |
|
3757 break; |
|
3758 case "customizationstarting": |
|
3759 this._disable(); |
|
3760 break; |
|
3761 case "aftercustomization": |
|
3762 this._enable(); |
|
3763 break; |
|
3764 } |
|
3765 }, |
|
3766 |
|
3767 show: function() { |
|
3768 let deferred = Promise.defer(); |
|
3769 if (this._panel.state == "open") { |
|
3770 deferred.resolve(); |
|
3771 return deferred.promise; |
|
3772 } |
|
3773 let doc = this._panel.ownerDocument; |
|
3774 this._panel.hidden = false; |
|
3775 let contextMenu = doc.getElementById(this._panel.getAttribute("context")); |
|
3776 gELS.addSystemEventListener(contextMenu, 'command', this, true); |
|
3777 let anchor = doc.getAnonymousElementByAttribute(this._chevron, "class", "toolbarbutton-icon"); |
|
3778 this._panel.openPopup(anchor || this._chevron); |
|
3779 this._chevron.open = true; |
|
3780 |
|
3781 this._panel.addEventListener("popupshown", function onPopupShown() { |
|
3782 this.removeEventListener("popupshown", onPopupShown); |
|
3783 deferred.resolve(); |
|
3784 }); |
|
3785 |
|
3786 return deferred.promise; |
|
3787 }, |
|
3788 |
|
3789 _onClickChevron: function(aEvent) { |
|
3790 if (this._chevron.open) { |
|
3791 this._panel.hidePopup(); |
|
3792 this._chevron.open = false; |
|
3793 } else { |
|
3794 this.show(); |
|
3795 } |
|
3796 }, |
|
3797 |
|
3798 _onPanelHiding: function(aEvent) { |
|
3799 this._chevron.open = false; |
|
3800 let doc = aEvent.target.ownerDocument; |
|
3801 let contextMenu = doc.getElementById(this._panel.getAttribute("context")); |
|
3802 gELS.removeSystemEventListener(contextMenu, 'command', this, true); |
|
3803 }, |
|
3804 |
|
3805 onOverflow: function(aEvent) { |
|
3806 if (!this._enabled || |
|
3807 (aEvent && aEvent.target != this._toolbar.customizationTarget)) |
|
3808 return; |
|
3809 |
|
3810 let child = this._target.lastChild; |
|
3811 |
|
3812 while (child && this._target.scrollLeftMax > 0) { |
|
3813 let prevChild = child.previousSibling; |
|
3814 |
|
3815 if (child.getAttribute("overflows") != "false") { |
|
3816 this._collapsed.set(child.id, this._target.clientWidth); |
|
3817 child.setAttribute("overflowedItem", true); |
|
3818 child.setAttribute("cui-anchorid", this._chevron.id); |
|
3819 CustomizableUIInternal.notifyListeners("onWidgetOverflow", child, this._target); |
|
3820 |
|
3821 this._list.insertBefore(child, this._list.firstChild); |
|
3822 if (!this._toolbar.hasAttribute("overflowing")) { |
|
3823 CustomizableUI.addListener(this); |
|
3824 } |
|
3825 this._toolbar.setAttribute("overflowing", "true"); |
|
3826 } |
|
3827 child = prevChild; |
|
3828 }; |
|
3829 |
|
3830 let win = this._target.ownerDocument.defaultView; |
|
3831 win.UpdateUrlbarSearchSplitterState(); |
|
3832 }, |
|
3833 |
|
3834 _onResize: function(aEvent) { |
|
3835 if (!this._lazyResizeHandler) { |
|
3836 this._lazyResizeHandler = new DeferredTask(this._onLazyResize.bind(this), |
|
3837 LAZY_RESIZE_INTERVAL_MS); |
|
3838 } |
|
3839 this._lazyResizeHandler.arm(); |
|
3840 }, |
|
3841 |
|
3842 _moveItemsBackToTheirOrigin: function(shouldMoveAllItems) { |
|
3843 let placements = gPlacements.get(this._toolbar.id); |
|
3844 while (this._list.firstChild) { |
|
3845 let child = this._list.firstChild; |
|
3846 let minSize = this._collapsed.get(child.id); |
|
3847 |
|
3848 if (!shouldMoveAllItems && |
|
3849 minSize && |
|
3850 this._target.clientWidth <= minSize) { |
|
3851 return; |
|
3852 } |
|
3853 |
|
3854 this._collapsed.delete(child.id); |
|
3855 let beforeNodeIndex = placements.indexOf(child.id) + 1; |
|
3856 // If this is a skipintoolbarset item, meaning it doesn't occur in the placements list, |
|
3857 // we're inserting it at the end. This will mean first-in, first-out (more or less) |
|
3858 // leading to as little change in order as possible. |
|
3859 if (beforeNodeIndex == 0) { |
|
3860 beforeNodeIndex = placements.length; |
|
3861 } |
|
3862 let inserted = false; |
|
3863 for (; beforeNodeIndex < placements.length; beforeNodeIndex++) { |
|
3864 let beforeNode = this._target.getElementsByAttribute("id", placements[beforeNodeIndex])[0]; |
|
3865 if (beforeNode) { |
|
3866 this._target.insertBefore(child, beforeNode); |
|
3867 inserted = true; |
|
3868 break; |
|
3869 } |
|
3870 } |
|
3871 if (!inserted) { |
|
3872 this._target.appendChild(child); |
|
3873 } |
|
3874 child.removeAttribute("cui-anchorid"); |
|
3875 child.removeAttribute("overflowedItem"); |
|
3876 CustomizableUIInternal.notifyListeners("onWidgetUnderflow", child, this._target); |
|
3877 } |
|
3878 |
|
3879 let win = this._target.ownerDocument.defaultView; |
|
3880 win.UpdateUrlbarSearchSplitterState(); |
|
3881 |
|
3882 if (!this._collapsed.size) { |
|
3883 this._toolbar.removeAttribute("overflowing"); |
|
3884 CustomizableUI.removeListener(this); |
|
3885 } |
|
3886 }, |
|
3887 |
|
3888 _onLazyResize: function() { |
|
3889 if (!this._enabled) |
|
3890 return; |
|
3891 |
|
3892 if (this._target.scrollLeftMax > 0) { |
|
3893 this.onOverflow(); |
|
3894 } else { |
|
3895 this._moveItemsBackToTheirOrigin(); |
|
3896 } |
|
3897 }, |
|
3898 |
|
3899 _disable: function() { |
|
3900 this._enabled = false; |
|
3901 this._moveItemsBackToTheirOrigin(true); |
|
3902 if (this._lazyResizeHandler) { |
|
3903 this._lazyResizeHandler.disarm(); |
|
3904 } |
|
3905 }, |
|
3906 |
|
3907 _enable: function() { |
|
3908 this._enabled = true; |
|
3909 this.onOverflow(); |
|
3910 }, |
|
3911 |
|
3912 onWidgetBeforeDOMChange: function(aNode, aNextNode, aContainer) { |
|
3913 if (aContainer != this._target && aContainer != this._list) { |
|
3914 return; |
|
3915 } |
|
3916 // When we (re)move an item, update all the items that come after it in the list |
|
3917 // with the minsize *of the item before the to-be-removed node*. This way, we |
|
3918 // ensure that we try to move items back as soon as that's possible. |
|
3919 if (aNode.parentNode == this._list) { |
|
3920 let updatedMinSize; |
|
3921 if (aNode.previousSibling) { |
|
3922 updatedMinSize = this._collapsed.get(aNode.previousSibling.id); |
|
3923 } else { |
|
3924 // Force (these) items to try to flow back into the bar: |
|
3925 updatedMinSize = 1; |
|
3926 } |
|
3927 let nextItem = aNode.nextSibling; |
|
3928 while (nextItem) { |
|
3929 this._collapsed.set(nextItem.id, updatedMinSize); |
|
3930 nextItem = nextItem.nextSibling; |
|
3931 } |
|
3932 } |
|
3933 }, |
|
3934 |
|
3935 onWidgetAfterDOMChange: function(aNode, aNextNode, aContainer) { |
|
3936 if (aContainer != this._target && aContainer != this._list) { |
|
3937 return; |
|
3938 } |
|
3939 |
|
3940 let nowInBar = aNode.parentNode == aContainer; |
|
3941 let nowOverflowed = aNode.parentNode == this._list; |
|
3942 let wasOverflowed = this._collapsed.has(aNode.id); |
|
3943 |
|
3944 // If this wasn't overflowed before... |
|
3945 if (!wasOverflowed) { |
|
3946 // ... but it is now, then we added to the overflow panel. Exciting stuff: |
|
3947 if (nowOverflowed) { |
|
3948 // NB: we're guaranteed that it has a previousSibling, because if it didn't, |
|
3949 // we would have added it to the toolbar instead. See getOverflowedNextNode. |
|
3950 let prevId = aNode.previousSibling.id; |
|
3951 let minSize = this._collapsed.get(prevId); |
|
3952 this._collapsed.set(aNode.id, minSize); |
|
3953 aNode.setAttribute("cui-anchorid", this._chevron.id); |
|
3954 aNode.setAttribute("overflowedItem", true); |
|
3955 CustomizableUIInternal.notifyListeners("onWidgetOverflow", aNode, this._target); |
|
3956 } |
|
3957 // If it is not overflowed and not in the toolbar, and was not overflowed |
|
3958 // either, it moved out of the toolbar. That means there's now space in there! |
|
3959 // Let's try to move stuff back: |
|
3960 else if (!nowInBar) { |
|
3961 this._moveItemsBackToTheirOrigin(true); |
|
3962 } |
|
3963 // If it's in the toolbar now, then we don't care. An overflow event may |
|
3964 // fire afterwards; that's ok! |
|
3965 } |
|
3966 // If it used to be overflowed... |
|
3967 else { |
|
3968 // ... and isn't anymore, let's remove our bookkeeping: |
|
3969 if (!nowOverflowed) { |
|
3970 this._collapsed.delete(aNode.id); |
|
3971 aNode.removeAttribute("cui-anchorid"); |
|
3972 aNode.removeAttribute("overflowedItem"); |
|
3973 CustomizableUIInternal.notifyListeners("onWidgetUnderflow", aNode, this._target); |
|
3974 |
|
3975 if (!this._collapsed.size) { |
|
3976 this._toolbar.removeAttribute("overflowing"); |
|
3977 CustomizableUI.removeListener(this); |
|
3978 } |
|
3979 } |
|
3980 // but if it still is, it must have changed places. Bookkeep: |
|
3981 else { |
|
3982 if (aNode.previousSibling) { |
|
3983 let prevId = aNode.previousSibling.id; |
|
3984 let minSize = this._collapsed.get(prevId); |
|
3985 this._collapsed.set(aNode.id, minSize); |
|
3986 } else { |
|
3987 // If it's now the first item in the overflow list, |
|
3988 // maybe we can return it: |
|
3989 this._moveItemsBackToTheirOrigin(); |
|
3990 } |
|
3991 } |
|
3992 } |
|
3993 }, |
|
3994 |
|
3995 findOverflowedInsertionPoints: function(aNode) { |
|
3996 let newNodeCanOverflow = aNode.getAttribute("overflows") != "false"; |
|
3997 let areaId = this._toolbar.id; |
|
3998 let placements = gPlacements.get(areaId); |
|
3999 let nodeIndex = placements.indexOf(aNode.id); |
|
4000 let nodeBeforeNewNodeIsOverflown = false; |
|
4001 |
|
4002 let loopIndex = -1; |
|
4003 while (++loopIndex < placements.length) { |
|
4004 let nextNodeId = placements[loopIndex]; |
|
4005 if (loopIndex > nodeIndex) { |
|
4006 if (newNodeCanOverflow && this._collapsed.has(nextNodeId)) { |
|
4007 let nextNode = this._list.getElementsByAttribute("id", nextNodeId).item(0); |
|
4008 if (nextNode) { |
|
4009 return [this._list, nextNode]; |
|
4010 } |
|
4011 } |
|
4012 if (!nodeBeforeNewNodeIsOverflown || !newNodeCanOverflow) { |
|
4013 let nextNode = this._target.getElementsByAttribute("id", nextNodeId).item(0); |
|
4014 if (nextNode) { |
|
4015 return [this._target, nextNode]; |
|
4016 } |
|
4017 } |
|
4018 } else if (loopIndex < nodeIndex && this._collapsed.has(nextNodeId)) { |
|
4019 nodeBeforeNewNodeIsOverflown = true; |
|
4020 } |
|
4021 } |
|
4022 |
|
4023 let containerForAppending = (this._collapsed.size && newNodeCanOverflow) ? |
|
4024 this._list : this._target; |
|
4025 return [containerForAppending, null]; |
|
4026 }, |
|
4027 |
|
4028 getContainerFor: function(aNode) { |
|
4029 if (aNode.getAttribute("overflowedItem") == "true") { |
|
4030 return this._list; |
|
4031 } |
|
4032 return this._target; |
|
4033 }, |
|
4034 }; |
|
4035 |
|
4036 CustomizableUIInternal.initialize(); |