browser/components/customizableui/src/CustomizableUI.jsm

changeset 1
ca08bd8f51b2
equal deleted inserted replaced
-1:000000000000 0:71f011c7fdf4
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();

mercurial