Source code

Revision control

Other Tools

1
/* This Source Code Form is subject to the terms of the Mozilla Public
2
* License, v. 2.0. If a copy of the MPL was not distributed with this
3
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
"use strict";
5
6
var EXPORTED_SYMBOLS = ["CustomizableUI"];
7
8
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
9
const { XPCOMUtils } = ChromeUtils.import(
11
);
12
const { AppConstants } = ChromeUtils.import(
14
);
15
16
XPCOMUtils.defineLazyModuleGetters(this, {
18
AddonManagerPrivate: "resource://gre/modules/AddonManager.jsm",
19
SearchWidgetTracker: "resource:///modules/SearchWidgetTracker.jsm",
20
CustomizableWidgets: "resource:///modules/CustomizableWidgets.jsm",
24
});
25
26
XPCOMUtils.defineLazyGetter(this, "gWidgetsBundle", function() {
27
const kUrl =
29
return Services.strings.createBundle(kUrl);
30
});
31
32
XPCOMUtils.defineLazyServiceGetter(
33
this,
34
"gELS",
35
"@mozilla.org/eventlistenerservice;1",
36
"nsIEventListenerService"
37
);
38
39
const kDefaultThemeID = "default-theme@mozilla.org";
40
41
const kSpecialWidgetPfx = "customizableui-special-";
42
43
const kPrefCustomizationState = "browser.uiCustomization.state";
44
const kPrefCustomizationAutoAdd = "browser.uiCustomization.autoAdd";
45
const kPrefCustomizationDebug = "browser.uiCustomization.debug";
46
const kPrefDrawInTitlebar = "browser.tabs.drawInTitlebar";
47
const kPrefExtraDragSpace = "browser.tabs.extraDragSpace";
48
const kPrefUIDensity = "browser.uidensity";
49
const kPrefAutoTouchMode = "browser.touchmode.auto";
50
const kPrefAutoHideDownloadsButton = "browser.download.autohideButton";
51
52
const kExpectedWindowURL = AppConstants.BROWSER_CHROME_URL;
53
54
var gDefaultTheme;
55
var gSelectedTheme;
56
57
/**
58
* The keys are the handlers that are fired when the event type (the value)
59
* is fired on the subview. A widget that provides a subview has the option
60
* of providing onViewShowing and onViewHiding event handlers.
61
*/
62
const kSubviewEvents = ["ViewShowing", "ViewHiding"];
63
64
/**
65
* The current version. We can use this to auto-add new default widgets as necessary.
66
* (would be const but isn't because of testing purposes)
67
*/
68
var kVersion = 16;
69
70
/**
71
* Buttons removed from built-ins by version they were removed. kVersion must be
72
* bumped any time a new id is added to this. Use the button id as key, and
73
* version the button is removed in as the value. e.g. "pocket-button": 5
74
*/
75
var ObsoleteBuiltinButtons = {
76
"feed-button": 15,
77
};
78
79
/**
80
* gPalette is a map of every widget that CustomizableUI.jsm knows about, keyed
81
* on their IDs.
82
*/
83
var gPalette = new Map();
84
85
/**
86
* gAreas maps area IDs to Sets of properties about those areas. An area is a
87
* place where a widget can be put.
88
*/
89
var gAreas = new Map();
90
91
/**
92
* gPlacements maps area IDs to Arrays of widget IDs, indicating that the widgets
93
* are placed within that area (either directly in the area node, or in the
94
* customizationTarget of the node).
95
*/
96
var gPlacements = new Map();
97
98
/**
99
* gFuturePlacements represent placements that will happen for areas that have
100
* not yet loaded (due to lazy-loading). This can occur when add-ons register
101
* widgets.
102
*/
103
var gFuturePlacements = new Map();
104
105
// XXXunf Temporary. Need a nice way to abstract functions to build widgets
106
// of these types.
107
var gSupportedWidgetTypes = new Set(["button", "view", "custom"]);
108
109
/**
110
* gPanelsForWindow is a list of known panels in a window which we may need to close
111
* should command events fire which target them.
112
*/
113
var gPanelsForWindow = new WeakMap();
114
115
/**
116
* gSeenWidgets remembers which widgets the user has seen for the first time
117
* before. This way, if a new widget is created, and the user has not seen it
118
* before, it can be put in its default location. Otherwise, it remains in the
119
* palette.
120
*/
121
var gSeenWidgets = new Set();
122
123
/**
124
* gDirtyAreaCache is a set of area IDs for areas where items have been added,
125
* moved or removed at least once. This set is persisted, and is used to
126
* optimize building of toolbars in the default case where no toolbars should
127
* be "dirty".
128
*/
129
var gDirtyAreaCache = new Set();
130
131
/**
132
* gPendingBuildAreas is a map from area IDs to map from build nodes to their
133
* existing children at the time of node registration, that are waiting
134
* for the area to be registered
135
*/
136
var gPendingBuildAreas = new Map();
137
138
var gSavedState = null;
139
var gRestoring = false;
140
var gDirty = false;
141
var gInBatchStack = 0;
142
var gResetting = false;
143
var gUndoResetting = false;
144
145
/**
146
* gBuildAreas maps area IDs to actual area nodes within browser windows.
147
*/
148
var gBuildAreas = new Map();
149
150
/**
151
* gBuildWindows is a map of windows that have registered build areas, mapped
152
* to a Set of known toolboxes in that window.
153
*/
154
var gBuildWindows = new Map();
155
156
var gNewElementCount = 0;
157
var gGroupWrapperCache = new Map();
158
var gSingleWrapperCache = new WeakMap();
159
var gListeners = new Set();
160
161
var gUIStateBeforeReset = {
162
uiCustomizationState: null,
163
drawInTitlebar: null,
164
extraDragSpace: null,
165
currentTheme: null,
166
uiDensity: null,
167
autoTouchMode: null,
168
};
169
170
XPCOMUtils.defineLazyPreferenceGetter(
171
this,
172
"gDebuggingEnabled",
173
kPrefCustomizationDebug,
174
false,
175
(pref, oldVal, newVal) => {
176
if (typeof log != "undefined") {
177
log.maxLogLevel = newVal ? "all" : "log";
178
}
179
}
180
);
181
182
XPCOMUtils.defineLazyGetter(this, "log", () => {
183
let scope = {};
184
ChromeUtils.import("resource://gre/modules/Console.jsm", scope);
185
let consoleOptions = {
186
maxLogLevel: gDebuggingEnabled ? "all" : "log",
187
prefix: "CustomizableUI",
188
};
189
return new scope.ConsoleAPI(consoleOptions);
190
});
191
192
var CustomizableUIInternal = {
193
initialize() {
194
log.debug("Initializing");
195
196
AddonManagerPrivate.databaseReady.then(async () => {
197
AddonManager.addAddonListener(this);
198
199
let addons = await AddonManager.getAddonsByTypes(["theme"]);
200
gDefaultTheme = addons.find(addon => addon.id == kDefaultThemeID);
201
gSelectedTheme = addons.find(addon => addon.isActive) || gDefaultTheme;
202
});
203
204
this.addListener(this);
205
this._defineBuiltInWidgets();
206
this.loadSavedState();
207
this._updateForNewVersion();
208
this._markObsoleteBuiltinButtonsSeen();
209
210
this.registerArea(
211
CustomizableUI.AREA_FIXED_OVERFLOW_PANEL,
212
{
213
type: CustomizableUI.TYPE_MENU_PANEL,
214
defaultPlacements: [],
215
anchor: "nav-bar-overflow-button",
216
},
217
true
218
);
219
220
let navbarPlacements = [
221
"back-button",
222
"forward-button",
223
"stop-reload-button",
224
"home-button",
225
"spring",
226
"urlbar-container",
227
"spring",
228
"downloads-button",
229
"library-button",
230
"sidebar-button",
231
"fxa-toolbar-menu-button",
232
];
233
234
if (AppConstants.MOZ_DEV_EDITION) {
235
navbarPlacements.splice(7, 0, "developer-button");
236
}
237
238
this.registerArea(
239
CustomizableUI.AREA_NAVBAR,
240
{
241
type: CustomizableUI.TYPE_TOOLBAR,
242
overflowable: true,
243
defaultPlacements: navbarPlacements,
244
defaultCollapsed: false,
245
},
246
true
247
);
248
249
if (AppConstants.MENUBAR_CAN_AUTOHIDE) {
250
this.registerArea(
251
CustomizableUI.AREA_MENUBAR,
252
{
253
type: CustomizableUI.TYPE_TOOLBAR,
254
defaultPlacements: ["menubar-items"],
255
defaultCollapsed: true,
256
},
257
true
258
);
259
}
260
261
this.registerArea(
262
CustomizableUI.AREA_TABSTRIP,
263
{
264
type: CustomizableUI.TYPE_TOOLBAR,
265
defaultPlacements: [
266
"tabbrowser-tabs",
267
"new-tab-button",
268
"alltabs-button",
269
],
270
defaultCollapsed: null,
271
},
272
true
273
);
274
this.registerArea(
275
CustomizableUI.AREA_BOOKMARKS,
276
{
277
type: CustomizableUI.TYPE_TOOLBAR,
278
defaultPlacements: ["personal-bookmarks"],
279
defaultCollapsed: true,
280
},
281
true
282
);
283
284
SearchWidgetTracker.init();
285
},
286
287
onEnabled(addon) {
288
if (addon.type == "theme") {
289
gSelectedTheme = addon;
290
}
291
},
292
293
get _builtinAreas() {
294
return new Set([
295
...this._builtinToolbars,
296
CustomizableUI.AREA_FIXED_OVERFLOW_PANEL,
297
]);
298
},
299
300
get _builtinToolbars() {
301
let toolbars = new Set([
302
CustomizableUI.AREA_NAVBAR,
303
CustomizableUI.AREA_BOOKMARKS,
304
CustomizableUI.AREA_TABSTRIP,
305
]);
306
if (AppConstants.platform != "macosx") {
307
toolbars.add(CustomizableUI.AREA_MENUBAR);
308
}
309
return toolbars;
310
},
311
312
_defineBuiltInWidgets() {
313
for (let widgetDefinition of CustomizableWidgets) {
314
this.createBuiltinWidget(widgetDefinition);
315
}
316
},
317
318
// eslint-disable-next-line complexity
319
_updateForNewVersion() {
320
// We should still enter even if gSavedState.currentVersion >= kVersion
321
// because the per-widget pref facility is independent of versioning.
322
if (!gSavedState) {
323
// Flip all the prefs so we don't try to re-introduce later:
324
for (let [, widget] of gPalette) {
325
if (widget.defaultArea && widget._introducedInVersion === "pref") {
326
let prefId = "browser.toolbarbuttons.introduced." + widget.id;
327
Services.prefs.setBoolPref(prefId, true);
328
}
329
}
330
return;
331
}
332
333
let currentVersion = gSavedState.currentVersion;
334
for (let [id, widget] of gPalette) {
335
if (widget.defaultArea) {
336
let shouldAdd = false;
337
let shouldSetPref = false;
338
let prefId = "browser.toolbarbuttons.introduced." + widget.id;
339
if (widget._introducedInVersion === "pref") {
340
try {
341
shouldAdd = !Services.prefs.getBoolPref(prefId);
342
} catch (ex) {
343
// Pref doesn't exist:
344
shouldAdd = true;
345
}
346
shouldSetPref = shouldAdd;
347
} else if (widget._introducedInVersion > currentVersion) {
348
shouldAdd = true;
349
}
350
351
if (shouldAdd) {
352
let futurePlacements = gFuturePlacements.get(widget.defaultArea);
353
if (futurePlacements) {
354
futurePlacements.add(id);
355
} else {
356
gFuturePlacements.set(widget.defaultArea, new Set([id]));
357
}
358
if (shouldSetPref) {
359
Services.prefs.setBoolPref(prefId, true);
360
}
361
}
362
}
363
}
364
365
if (
366
currentVersion < 7 &&
367
gSavedState.placements &&
368
gSavedState.placements[CustomizableUI.AREA_NAVBAR]
369
) {
370
let placements = gSavedState.placements[CustomizableUI.AREA_NAVBAR];
371
let newPlacements = [
372
"back-button",
373
"forward-button",
374
"stop-reload-button",
375
"home-button",
376
];
377
for (let button of placements) {
378
if (!newPlacements.includes(button)) {
379
newPlacements.push(button);
380
}
381
}
382
383
if (!newPlacements.includes("sidebar-button")) {
384
newPlacements.push("sidebar-button");
385
}
386
387
gSavedState.placements[CustomizableUI.AREA_NAVBAR] = newPlacements;
388
}
389
390
if (
391
currentVersion < 8 &&
392
gSavedState.placements &&
393
gSavedState.placements["PanelUI-contents"]
394
) {
395
let savedPanelPlacements = gSavedState.placements["PanelUI-contents"];
396
delete gSavedState.placements["PanelUI-contents"];
397
let defaultPlacements = [
398
"edit-controls",
399
"zoom-controls",
400
"new-window-button",
401
"privatebrowsing-button",
402
"save-page-button",
403
"print-button",
404
"history-panelmenu",
405
"fullscreen-button",
406
"find-button",
407
"preferences-button",
408
"add-ons-button",
409
"sync-button",
410
];
411
412
if (!AppConstants.MOZ_DEV_EDITION) {
413
defaultPlacements.splice(-1, 0, "developer-button");
414
}
415
416
let showCharacterEncoding = Services.prefs.getComplexValue(
417
"browser.menu.showCharacterEncoding",
418
Ci.nsIPrefLocalizedString
419
).data;
420
if (showCharacterEncoding == "true") {
421
defaultPlacements.push("characterencoding-button");
422
}
423
424
savedPanelPlacements = savedPanelPlacements.filter(
425
id => !defaultPlacements.includes(id)
426
);
427
428
if (savedPanelPlacements.length) {
429
gSavedState.placements[
430
CustomizableUI.AREA_FIXED_OVERFLOW_PANEL
431
] = savedPanelPlacements;
432
}
433
}
434
435
if (
436
currentVersion < 9 &&
437
gSavedState.placements &&
438
gSavedState.placements["nav-bar"]
439
) {
440
let placements = gSavedState.placements["nav-bar"];
441
if (placements.includes("urlbar-container")) {
442
let urlbarIndex = placements.indexOf("urlbar-container");
443
let secondSpringIndex = urlbarIndex + 1;
444
// Insert if there isn't already a spring before the urlbar
445
if (
446
urlbarIndex == 0 ||
447
!placements[urlbarIndex - 1].startsWith(kSpecialWidgetPfx + "spring")
448
) {
449
placements.splice(urlbarIndex, 0, "spring");
450
// The url bar is now 1 index later, so increment the insertion point for
451
// the second spring.
452
secondSpringIndex++;
453
}
454
// If the search container is present, insert after the search container
455
// instead of after the url bar
456
let searchContainerIndex = placements.indexOf("search-container");
457
if (searchContainerIndex != -1) {
458
secondSpringIndex = searchContainerIndex + 1;
459
}
460
if (
461
secondSpringIndex == placements.length ||
462
!placements[secondSpringIndex].startsWith(
463
kSpecialWidgetPfx + "spring"
464
)
465
) {
466
placements.splice(secondSpringIndex, 0, "spring");
467
}
468
}
469
470
// Finally, replace the bookmarks menu button with the library one if present
471
if (placements.includes("bookmarks-menu-button")) {
472
let bmbIndex = placements.indexOf("bookmarks-menu-button");
473
placements.splice(bmbIndex, 1);
474
let downloadButtonIndex = placements.indexOf("downloads-button");
475
let libraryIndex =
476
downloadButtonIndex == -1 ? bmbIndex : downloadButtonIndex + 1;
477
placements.splice(libraryIndex, 0, "library-button");
478
}
479
}
480
481
if (currentVersion < 10 && gSavedState.placements) {
482
for (let placements of Object.values(gSavedState.placements)) {
483
if (placements.includes("webcompat-reporter-button")) {
484
placements.splice(placements.indexOf("webcompat-reporter-button"), 1);
485
break;
486
}
487
}
488
}
489
490
// Move the downloads button to the default position in the navbar if it's
491
// not there already.
492
if (currentVersion < 11 && gSavedState.placements) {
493
let navbarPlacements = gSavedState.placements[CustomizableUI.AREA_NAVBAR];
494
// First remove from wherever it currently lives, if anywhere:
495
for (let placements of Object.values(gSavedState.placements)) {
496
let existingIndex = placements.indexOf("downloads-button");
497
if (existingIndex != -1) {
498
placements.splice(existingIndex, 1);
499
break; // It can only be in 1 place, so no point looking elsewhere.
500
}
501
}
502
503
// Now put the button in the navbar in the correct spot:
504
if (navbarPlacements) {
505
let insertionPoint = navbarPlacements.indexOf("urlbar-container");
506
// Deliberately iterate to 1 past the end of the array to insert at the
507
// end if need be.
508
while (++insertionPoint < navbarPlacements.length) {
509
let widget = navbarPlacements[insertionPoint];
510
// If we find a non-searchbar, non-spacer node, break out of the loop:
511
if (
512
widget != "search-container" &&
513
!this.matchingSpecials(widget, "spring")
514
) {
515
break;
516
}
517
}
518
// We either found the right spot, or reached the end of the
519
// placements, so insert here:
520
navbarPlacements.splice(insertionPoint, 0, "downloads-button");
521
}
522
}
523
524
if (currentVersion < 12 && gSavedState.placements) {
525
const removedButtons = [
526
"loop-call-button",
527
"loop-button-throttled",
528
"pocket-button",
529
];
530
for (let placements of Object.values(gSavedState.placements)) {
531
for (let button of removedButtons) {
532
let buttonIndex = placements.indexOf(button);
533
if (buttonIndex != -1) {
534
placements.splice(buttonIndex, 1);
535
}
536
}
537
}
538
}
539
540
// Remove the old placements from the now-gone Nightly-only
541
// "New non-e10s window" button.
542
if (currentVersion < 13 && gSavedState.placements) {
543
for (let placements of Object.values(gSavedState.placements)) {
544
let buttonIndex = placements.indexOf("e10s-button");
545
if (buttonIndex != -1) {
546
placements.splice(buttonIndex, 1);
547
}
548
}
549
}
550
551
// Remove unsupported custom toolbar saved placements
552
if (currentVersion < 14 && gSavedState.placements) {
553
for (let area in gSavedState.placements) {
554
if (!this._builtinAreas.has(area)) {
555
delete gSavedState.placements[area];
556
}
557
}
558
}
559
560
// Add the FxA toolbar menu as the right most button item
561
if (currentVersion < 16 && gSavedState.placements) {
562
let navbarPlacements = gSavedState.placements[CustomizableUI.AREA_NAVBAR];
563
// Place the menu item as the first item to the left of the hamburger menu
564
if (navbarPlacements) {
565
navbarPlacements.push("fxa-toolbar-menu-button");
566
}
567
}
568
},
569
570
/**
571
* _markObsoleteBuiltinButtonsSeen
572
* when upgrading, ensure obsoleted buttons are in seen state.
573
*/
574
_markObsoleteBuiltinButtonsSeen() {
575
if (!gSavedState) {
576
return;
577
}
578
let currentVersion = gSavedState.currentVersion;
579
if (currentVersion >= kVersion) {
580
return;
581
}
582
// we're upgrading, update state if necessary
583
for (let id in ObsoleteBuiltinButtons) {
584
let version = ObsoleteBuiltinButtons[id];
585
if (version == kVersion) {
586
gSeenWidgets.add(id);
587
gDirty = true;
588
}
589
}
590
},
591
592
_placeNewDefaultWidgetsInArea(aArea) {
593
let futurePlacedWidgets = gFuturePlacements.get(aArea);
594
let savedPlacements =
595
gSavedState && gSavedState.placements && gSavedState.placements[aArea];
596
let defaultPlacements = gAreas.get(aArea).get("defaultPlacements");
597
if (
598
!savedPlacements ||
599
!savedPlacements.length ||
600
!futurePlacedWidgets ||
601
!defaultPlacements ||
602
!defaultPlacements.length
603
) {
604
return;
605
}
606
let defaultWidgetIndex = -1;
607
608
for (let widgetId of futurePlacedWidgets) {
609
let widget = gPalette.get(widgetId);
610
if (
611
!widget ||
612
widget.source !== CustomizableUI.SOURCE_BUILTIN ||
613
!widget.defaultArea ||
614
!widget._introducedInVersion ||
615
savedPlacements.includes(widget.id)
616
) {
617
continue;
618
}
619
defaultWidgetIndex = defaultPlacements.indexOf(widget.id);
620
if (defaultWidgetIndex === -1) {
621
continue;
622
}
623
// Now we know that this widget should be here by default, was newly introduced,
624
// and we have a saved state to insert into, and a default state to work off of.
625
// Try introducing after widgets that come before it in the default placements:
626
for (let i = defaultWidgetIndex; i >= 0; i--) {
627
// Special case: if the defaults list this widget as coming first, insert at the beginning:
628
if (i === 0 && i === defaultWidgetIndex) {
629
savedPlacements.splice(0, 0, widget.id);
630
// Before you ask, yes, deleting things inside a let x of y loop where y is a Set is
631
// safe, and we won't skip any items.
632
futurePlacedWidgets.delete(widget.id);
633
gDirty = true;
634
break;
635
}
636
// Otherwise, if we're somewhere other than the beginning, check if the previous
637
// widget is in the saved placements.
638
if (i) {
639
let previousWidget = defaultPlacements[i - 1];
640
let previousWidgetIndex = savedPlacements.indexOf(previousWidget);
641
if (previousWidgetIndex != -1) {
642
savedPlacements.splice(previousWidgetIndex + 1, 0, widget.id);
643
futurePlacedWidgets.delete(widget.id);
644
gDirty = true;
645
break;
646
}
647
}
648
}
649
// The loop above either inserts the item or doesn't - either way, we can get away
650
// with doing nothing else now; if the item remains in gFuturePlacements, we'll
651
// add it at the end in restoreStateForArea.
652
}
653
this.saveState();
654
},
655
656
getCustomizationTarget(aElement) {
657
if (!aElement) {
658
return null;
659
}
660
661
if (
662
!aElement._customizationTarget &&
663
aElement.hasAttribute("customizable")
664
) {
665
let id = aElement.getAttribute("customizationtarget");
666
if (id) {
667
aElement._customizationTarget = aElement.ownerDocument.getElementById(
668
id
669
);
670
}
671
672
if (!aElement._customizationTarget) {
673
aElement._customizationTarget = aElement;
674
}
675
}
676
677
return aElement._customizationTarget;
678
},
679
680
wrapWidget(aWidgetId) {
681
if (gGroupWrapperCache.has(aWidgetId)) {
682
return gGroupWrapperCache.get(aWidgetId);
683
}
684
685
let provider = this.getWidgetProvider(aWidgetId);
686
if (!provider) {
687
return null;
688
}
689
690
if (provider == CustomizableUI.PROVIDER_API) {
691
let widget = gPalette.get(aWidgetId);
692
if (!widget.wrapper) {
693
widget.wrapper = new WidgetGroupWrapper(widget);
694
gGroupWrapperCache.set(aWidgetId, widget.wrapper);
695
}
696
return widget.wrapper;
697
}
698
699
// PROVIDER_SPECIAL gets treated the same as PROVIDER_XUL.
700
// XXXgijs: this causes bugs in code that depends on widgetWrapper.provider
701
// giving an accurate answer... filed as bug 1379821
702
let wrapper = new XULWidgetGroupWrapper(aWidgetId);
703
gGroupWrapperCache.set(aWidgetId, wrapper);
704
return wrapper;
705
},
706
707
registerArea(aName, aProperties, aInternalCaller) {
708
if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) {
709
throw new Error("Invalid area name");
710
}
711
712
let areaIsKnown = gAreas.has(aName);
713
let props = areaIsKnown ? gAreas.get(aName) : new Map();
714
const kImmutableProperties = new Set(["type", "overflowable"]);
715
for (let key in aProperties) {
716
if (
717
areaIsKnown &&
718
kImmutableProperties.has(key) &&
719
props.get(key) != aProperties[key]
720
) {
721
throw new Error("An area cannot change the property for '" + key + "'");
722
}
723
props.set(key, aProperties[key]);
724
}
725
// Default to a toolbar:
726
if (!props.has("type")) {
727
props.set("type", CustomizableUI.TYPE_TOOLBAR);
728
}
729
if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) {
730
// Check aProperties instead of props because this check is only interested
731
// in the passed arguments, not the state of a potentially pre-existing area.
732
if (!aInternalCaller && aProperties.defaultCollapsed) {
733
throw new Error(
734
"defaultCollapsed is only allowed for default toolbars."
735
);
736
}
737
if (!props.has("defaultCollapsed")) {
738
props.set("defaultCollapsed", true);
739
}
740
} else if (props.has("defaultCollapsed")) {
741
throw new Error("defaultCollapsed only applies for TYPE_TOOLBAR areas.");
742
}
743
// Sanity check type:
744
let allTypes = [
745
CustomizableUI.TYPE_TOOLBAR,
746
CustomizableUI.TYPE_MENU_PANEL,
747
];
748
if (!allTypes.includes(props.get("type"))) {
749
throw new Error("Invalid area type " + props.get("type"));
750
}
751
752
// And to no placements:
753
if (!props.has("defaultPlacements")) {
754
props.set("defaultPlacements", []);
755
}
756
// Sanity check default placements array:
757
if (!Array.isArray(props.get("defaultPlacements"))) {
758
throw new Error("Should provide an array of default placements");
759
}
760
761
if (!areaIsKnown) {
762
gAreas.set(aName, props);
763
764
// Reconcile new default widgets. Have to do this before we start restoring things.
765
this._placeNewDefaultWidgetsInArea(aName);
766
767
if (
768
props.get("type") == CustomizableUI.TYPE_TOOLBAR &&
769
!gPlacements.has(aName)
770
) {
771
// Guarantee this area exists in gFuturePlacements, to avoid checking it in
772
// various places elsewhere.
773
if (!gFuturePlacements.has(aName)) {
774
gFuturePlacements.set(aName, new Set());
775
}
776
} else {
777
this.restoreStateForArea(aName);
778
}
779
780
// If we have pending build area nodes, register all of them
781
if (gPendingBuildAreas.has(aName)) {
782
let pendingNodes = gPendingBuildAreas.get(aName);
783
for (let pendingNode of pendingNodes) {
784
this.registerToolbarNode(pendingNode);
785
}
786
gPendingBuildAreas.delete(aName);
787
}
788
}
789
},
790
791
unregisterArea(aName, aDestroyPlacements) {
792
if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) {
793
throw new Error("Invalid area name");
794
}
795
if (!gAreas.has(aName) && !gPlacements.has(aName)) {
796
throw new Error("Area not registered");
797
}
798
799
// Move all the widgets out
800
this.beginBatchUpdate();
801
try {
802
let placements = gPlacements.get(aName);
803
if (placements) {
804
// Need to clone this array so removeWidgetFromArea doesn't modify it
805
placements = [...placements];
806
placements.forEach(this.removeWidgetFromArea, this);
807
}
808
809
// Delete all remaining traces.
810
gAreas.delete(aName);
811
// Only destroy placements when necessary:
812
if (aDestroyPlacements) {
813
gPlacements.delete(aName);
814
} else {
815
// Otherwise we need to re-set them, as removeFromArea will have emptied
816
// them out:
817
gPlacements.set(aName, placements);
818
}
819
gFuturePlacements.delete(aName);
820
let existingAreaNodes = gBuildAreas.get(aName);
821
if (existingAreaNodes) {
822
for (let areaNode of existingAreaNodes) {
823
this.notifyListeners(
824
"onAreaNodeUnregistered",
825
aName,
826
this.getCustomizationTarget(areaNode),
827
CustomizableUI.REASON_AREA_UNREGISTERED
828
);
829
}
830
}
831
gBuildAreas.delete(aName);
832
} finally {
833
this.endBatchUpdate(true);
834
}
835
},
836
837
registerToolbarNode(aToolbar) {
838
let area = aToolbar.id;
839
if (gBuildAreas.has(area) && gBuildAreas.get(area).has(aToolbar)) {
840
return;
841
}
842
let areaProperties = gAreas.get(area);
843
844
// If this area is not registered, try to do it automatically:
845
if (!areaProperties) {
846
if (!gPendingBuildAreas.has(area)) {
847
gPendingBuildAreas.set(area, []);
848
}
849
gPendingBuildAreas.get(area).push(aToolbar);
850
return;
851
}
852
853
this.beginBatchUpdate();
854
try {
855
let placements = gPlacements.get(area);
856
if (
857
!placements &&
858
areaProperties.get("type") == CustomizableUI.TYPE_TOOLBAR
859
) {
860
this.restoreStateForArea(area);
861
placements = gPlacements.get(area);
862
}
863
864
// For toolbars that need it, mark as dirty.
865
let defaultPlacements = areaProperties.get("defaultPlacements");
866
if (
867
!this._builtinToolbars.has(area) ||
868
placements.length != defaultPlacements.length ||
869
!placements.every((id, i) => id == defaultPlacements[i])
870
) {
871
gDirtyAreaCache.add(area);
872
}
873
874
if (areaProperties.has("overflowable")) {
875
aToolbar.overflowable = new OverflowableToolbar(aToolbar);
876
}
877
878
this.registerBuildArea(area, aToolbar);
879
880
// We only build the toolbar if it's been marked as "dirty". Dirty means
881
// one of the following things:
882
// 1) Items have been added, moved or removed from this toolbar before.
883
// 2) The number of children of the toolbar does not match the length of
884
// the placements array for that area.
885
//
886
// This notion of being "dirty" is stored in a cache which is persisted
887
// in the saved state.
888
if (gDirtyAreaCache.has(area)) {
889
this.buildArea(area, placements, aToolbar);
890
} else {
891
// We must have a builtin toolbar that's in the default state. We need
892
// to only make sure that all the special nodes are correct.
893
let specials = placements.filter(p => this.isSpecialWidget(p));
894
if (specials.length) {
895
this.updateSpecialsForBuiltinToolbar(aToolbar, specials);
896
}
897
}
898
this.notifyListeners(
899
"onAreaNodeRegistered",
900
area,
901
this.getCustomizationTarget(aToolbar)
902
);
903
} finally {
904
this.endBatchUpdate();
905
}
906
},
907
908
updateSpecialsForBuiltinToolbar(aToolbar, aSpecialIDs) {
909
// Nodes are going to be in the correct order, so we can do this straightforwardly:
910
let { children } = this.getCustomizationTarget(aToolbar);
911
for (let kid of children) {
912
if (
913
this.matchingSpecials(aSpecialIDs[0], kid) &&
914
kid.getAttribute("skipintoolbarset") != "true"
915
) {
916
kid.id = aSpecialIDs.shift();
917
}
918
if (!aSpecialIDs.length) {
919
return;
920
}
921
}
922
},
923
924
buildArea(aArea, aPlacements, aAreaNode) {
925
let document = aAreaNode.ownerDocument;
926
let window = document.defaultView;
927
let inPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(window);
928
let container = this.getCustomizationTarget(aAreaNode);
929
let areaIsPanel =
930
gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL;
931
932
if (!container) {
933
throw new Error(
934
"Expected area " + aArea + " to have a customizationTarget attribute."
935
);
936
}
937
938
// Restore nav-bar visibility since it may have been hidden
939
// through a migration path (bug 938980) or an add-on.
940
if (aArea == CustomizableUI.AREA_NAVBAR) {
941
aAreaNode.collapsed = false;
942
}
943
944
this.beginBatchUpdate();
945
946
try {
947
let currentNode = container.firstElementChild;
948
let placementsToRemove = new Set();
949
for (let id of aPlacements) {
950
while (
951
currentNode &&
952
currentNode.getAttribute("skipintoolbarset") == "true"
953
) {
954
currentNode = currentNode.nextElementSibling;
955
}
956
957
// Fix ids for specials and continue, for correctly placed specials.
958
if (
959
currentNode &&
960
(!currentNode.id || CustomizableUI.isSpecialWidget(currentNode)) &&
961
this.matchingSpecials(id, currentNode)
962
) {
963
currentNode.id = id;
964
}
965
if (currentNode && currentNode.id == id) {
966
currentNode = currentNode.nextElementSibling;
967
continue;
968
}
969
970
if (this.isSpecialWidget(id) && areaIsPanel) {
971
placementsToRemove.add(id);
972
continue;
973
}
974
975
let [provider, node] = this.getWidgetNode(id, window);
976
if (!node) {
977
log.debug("Unknown widget: " + id);
978
continue;
979
}
980
981
let widget = null;
982
// If the placements have items in them which are (now) no longer removable,
983
// we shouldn't be moving them:
984
if (provider == CustomizableUI.PROVIDER_API) {
985
widget = gPalette.get(id);
986
if (!widget.removable && aArea != widget.defaultArea) {
987
placementsToRemove.add(id);
988
continue;
989
}
990
} else if (
991
provider == CustomizableUI.PROVIDER_XUL &&
992
node.parentNode != container &&
993
!this.isWidgetRemovable(node)
994
) {
995
placementsToRemove.add(id);
996
continue;
997
} // Special widgets are always removable, so no need to check them
998
999
if (inPrivateWindow && widget && !widget.showInPrivateBrowsing) {
1000
continue;
1001
}
1002
1003
this.ensureButtonContextMenu(node, aAreaNode);
1004
1005
// This needs updating in case we're resetting / undoing a reset.
1006
if (widget) {
1007
widget.currentArea = aArea;
1008
}
1009
this.insertWidgetBefore(node, currentNode, container, aArea);
1010
if (gResetting) {
1011
this.notifyListeners("onWidgetReset", node, container);
1012
} else if (gUndoResetting) {
1013
this.notifyListeners("onWidgetUndoMove", node, container);
1014
}
1015
}
1016
1017
if (currentNode) {
1018
let palette = window.gNavToolbox ? window.gNavToolbox.palette : null;
1019
let limit = currentNode.previousElementSibling;
1020
let node = container.lastElementChild;
1021
while (node && node != limit) {
1022
let previousSibling = node.previousElementSibling;
1023
// Nodes opt-in to removability. If they're removable, and we haven't
1024
// seen them in the placements array, then we toss them into the palette
1025
// if one exists. If no palette exists, we just remove the node. If the
1026
// node is not removable, we leave it where it is. However, we can only
1027
// safely touch elements that have an ID - both because we depend on
1028
// IDs (or are specials), and because such elements are not intended to
1029
// be widgets (eg, titlebar-spacer elements).
1030
if (
1031
(node.id || this.isSpecialWidget(node)) &&
1032
node.getAttribute("skipintoolbarset") != "true"
1033
) {
1034
if (this.isWidgetRemovable(node)) {
1035
if (node.id && (gResetting || gUndoResetting)) {
1036
let widget = gPalette.get(node.id);
1037
if (widget) {
1038
widget.currentArea = null;
1039
}
1040
}
1041
if (palette && !this.isSpecialWidget(node.id)) {
1042
palette.appendChild(node);
1043
this.removeLocationAttributes(node);
1044
} else {
1045
container.removeChild(node);
1046
}
1047
} else {
1048
node.setAttribute("removable", false);
1049
log.debug(
1050
"Adding non-removable widget to placements of " +
1051
aArea +
1052
": " +
1053
node.id
1054
);
1055
gPlacements.get(aArea).push(node.id);
1056
gDirty = true;
1057
}
1058
}
1059
node = previousSibling;
1060
}
1061
}
1062
1063
// If there are placements in here which aren't removable from their original area,
1064
// we remove them from this area's placement array. They will (have) be(en) added
1065
// to their original area's placements array in the block above this one.
1066
if (placementsToRemove.size) {
1067
let placementAry = gPlacements.get(aArea);
1068
for (let id of placementsToRemove) {
1069
let index = placementAry.indexOf(id);
1070
placementAry.splice(index, 1);
1071
}
1072
}
1073
1074
if (gResetting) {
1075
this.notifyListeners("onAreaReset", aArea, container);
1076
}
1077
} finally {
1078
this.endBatchUpdate();
1079
}
1080
},
1081
1082
addPanelCloseListeners(aPanel) {
1083
gELS.addSystemEventListener(aPanel, "click", this, false);
1084
gELS.addSystemEventListener(aPanel, "keypress", this, false);
1085
let win = aPanel.ownerGlobal;
1086
if (!gPanelsForWindow.has(win)) {
1087
gPanelsForWindow.set(win, new Set());
1088
}
1089
gPanelsForWindow.get(win).add(this._getPanelForNode(aPanel));
1090
},
1091
1092
removePanelCloseListeners(aPanel) {
1093
gELS.removeSystemEventListener(aPanel, "click", this, false);
1094
gELS.removeSystemEventListener(aPanel, "keypress", this, false);
1095
let win = aPanel.ownerGlobal;
1096
let panels = gPanelsForWindow.get(win);
1097
if (panels) {
1098
panels.delete(this._getPanelForNode(aPanel));
1099
}
1100
},
1101
1102
ensureButtonContextMenu(aNode, aAreaNode, forcePanel) {
1103
const kPanelItemContextMenu = "customizationPanelItemContextMenu";
1104
1105
let currentContextMenu =
1106
aNode.getAttribute("context") || aNode.getAttribute("contextmenu");
1107
let contextMenuForPlace =
1108
forcePanel || "menu-panel" == CustomizableUI.getPlaceForItem(aAreaNode)
1109
? kPanelItemContextMenu
1110
: null;
1111
if (contextMenuForPlace && !currentContextMenu) {
1112
aNode.setAttribute("context", contextMenuForPlace);
1113
} else if (
1114
currentContextMenu == kPanelItemContextMenu &&
1115
contextMenuForPlace != kPanelItemContextMenu
1116
) {
1117
aNode.removeAttribute("context");
1118
aNode.removeAttribute("contextmenu");
1119
}
1120
},
1121
1122
getWidgetProvider(aWidgetId) {
1123
if (this.isSpecialWidget(aWidgetId)) {
1124
return CustomizableUI.PROVIDER_SPECIAL;
1125
}
1126
if (gPalette.has(aWidgetId)) {
1127
return CustomizableUI.PROVIDER_API;
1128
}
1129
// If this was an API widget that was destroyed, return null:
1130
if (gSeenWidgets.has(aWidgetId)) {
1131
return null;
1132
}
1133
1134
// We fall back to the XUL provider, but we don't know for sure (at this
1135
// point) whether it exists there either. So the API is technically lying.
1136
// Ideally, it would be able to return an error value (or throw an
1137
// exception) if it really didn't exist. Our code calling this function
1138
// handles that fine, but this is a public API.
1139
return CustomizableUI.PROVIDER_XUL;
1140
},
1141
1142
getWidgetNode(aWidgetId, aWindow) {
1143
let document = aWindow.document;
1144
1145
if (this.isSpecialWidget(aWidgetId)) {
1146
let widgetNode =
1147
document.getElementById(aWidgetId) ||
1148
this.createSpecialWidget(aWidgetId, document);
1149
return [CustomizableUI.PROVIDER_SPECIAL, widgetNode];
1150
}
1151
1152
let widget = gPalette.get(aWidgetId);
1153
if (widget) {
1154
// If we have an instance of this widget already, just use that.
1155
if (widget.instances.has(document)) {
1156
log.debug(
1157
"An instance of widget " +
1158
aWidgetId +
1159
" already exists in this " +
1160
"document. Reusing."
1161
);
1162
return [CustomizableUI.PROVIDER_API, widget.instances.get(document)];
1163
}
1164
1165
return [CustomizableUI.PROVIDER_API, this.buildWidget(document, widget)];
1166
}
1167
1168
log.debug("Searching for " + aWidgetId + " in toolbox.");
1169
let node = this.findWidgetInWindow(aWidgetId, aWindow);
1170
if (node) {
1171
return [CustomizableUI.PROVIDER_XUL, node];
1172
}
1173
1174
log.debug("No node for " + aWidgetId + " found.");
1175
return [null, null];
1176
},
1177
1178
registerMenuPanel(aPanelContents, aArea) {
1179
if (gBuildAreas.has(aArea) && gBuildAreas.get(aArea).has(aPanelContents)) {
1180
return;
1181
}
1182
1183
aPanelContents._customizationTarget = aPanelContents;
1184
this.addPanelCloseListeners(this._getPanelForNode(aPanelContents));
1185
1186
let placements = gPlacements.get(aArea);
1187
this.buildArea(aArea, placements, aPanelContents);
1188
this.notifyListeners("onAreaNodeRegistered", aArea, aPanelContents);
1189
1190
for (let child of aPanelContents.children) {
1191
if (child.localName != "toolbarbutton") {
1192
if (child.localName == "toolbaritem") {
1193
this.ensureButtonContextMenu(child, aPanelContents, true);
1194
}
1195
continue;
1196
}
1197
this.ensureButtonContextMenu(child, aPanelContents, true);
1198
}
1199
1200
this.registerBuildArea(aArea, aPanelContents);
1201
},
1202
1203
onWidgetAdded(aWidgetId, aArea, aPosition) {
1204
this.insertNode(aWidgetId, aArea, aPosition, true);
1205
1206
if (!gResetting) {
1207
this._clearPreviousUIState();
1208
}
1209
},
1210
1211
onWidgetRemoved(aWidgetId, aArea) {
1212
let areaNodes = gBuildAreas.get(aArea);
1213
if (!areaNodes) {
1214
return;
1215
}
1216
1217
let area = gAreas.get(aArea);
1218
let isToolbar = area.get("type") == CustomizableUI.TYPE_TOOLBAR;
1219
let isOverflowable = isToolbar && area.get("overflowable");
1220
let showInPrivateBrowsing = gPalette.has(aWidgetId)
1221
? gPalette.get(aWidgetId).showInPrivateBrowsing
1222
: true;
1223
1224
for (let areaNode of areaNodes) {
1225
let window = areaNode.ownerGlobal;
1226
if (
1227
!showInPrivateBrowsing &&
1228
PrivateBrowsingUtils.isWindowPrivate(window)
1229
) {
1230
continue;
1231
}
1232
1233
let container = this.getCustomizationTarget(areaNode);
1234
let widgetNode = window.document.getElementById(aWidgetId);
1235
if (widgetNode && isOverflowable) {
1236
container = areaNode.overflowable.getContainerFor(widgetNode);
1237
}
1238
1239
if (!widgetNode || !container.contains(widgetNode)) {
1240
log.info(
1241
"Widget " + aWidgetId + " not found, unable to remove from " + aArea
1242
);
1243
continue;
1244
}
1245
1246
this.notifyListeners(
1247
"onWidgetBeforeDOMChange",
1248
widgetNode,
1249
null,
1250
container,
1251
true
1252
);
1253
1254
// We remove location attributes here to make sure they're gone too when a
1255
// widget is removed from a toolbar to the palette. See bug 930950.
1256
this.removeLocationAttributes(widgetNode);
1257
// We also need to remove the panel context menu if it's there:
1258
this.ensureButtonContextMenu(widgetNode);
1259
if (gPalette.has(aWidgetId) || this.isSpecialWidget(aWidgetId)) {
1260
container.removeChild(widgetNode);
1261
} else {
1262
window.gNavToolbox.palette.appendChild(widgetNode);
1263
}
1264
this.notifyListeners(
1265
"onWidgetAfterDOMChange",
1266
widgetNode,
1267
null,
1268
container,
1269
true
1270
);
1271
1272
let windowCache = gSingleWrapperCache.get(window);
1273
if (windowCache) {
1274
windowCache.delete(aWidgetId);
1275
}
1276
}
1277
if (!gResetting) {
1278
this._clearPreviousUIState();
1279
}
1280
},
1281
1282
onWidgetMoved(aWidgetId, aArea, aOldPosition, aNewPosition) {
1283
this.insertNode(aWidgetId, aArea, aNewPosition);
1284
if (!gResetting) {
1285
this._clearPreviousUIState();
1286
}
1287
},
1288
1289
onCustomizeEnd(aWindow) {
1290
this._clearPreviousUIState();
1291
},
1292
1293
registerBuildArea(aArea, aNode) {
1294
// We ensure that the window is registered to have its customization data
1295
// cleaned up when unloading.
1296
let window = aNode.ownerGlobal;
1297
if (window.closed) {
1298
return;
1299
}
1300
this.registerBuildWindow(window);
1301
1302
// Also register this build area's toolbox.
1303
if (window.gNavToolbox) {
1304
gBuildWindows.get(window).add(window.gNavToolbox);
1305
}
1306
1307
if (!gBuildAreas.has(aArea)) {
1308
gBuildAreas.set(aArea, new Set());
1309
}
1310
1311
gBuildAreas.get(aArea).add(aNode);
1312
1313
// Give a class to all customize targets to be used for styling in Customize Mode
1314
let customizableNode = this.getCustomizeTargetForArea(aArea, window);
1315
customizableNode.classList.add("customization-target");
1316
},
1317
1318
registerBuildWindow(aWindow) {
1319
if (!gBuildWindows.has(aWindow)) {
1320
gBuildWindows.set(aWindow, new Set());
1321
1322
aWindow.addEventListener("unload", this);
1323
aWindow.addEventListener("command", this, true);
1324
1325
this.notifyListeners("onWindowOpened", aWindow);
1326
}
1327
},
1328
1329
unregisterBuildWindow(aWindow) {
1330
aWindow.removeEventListener("unload", this);
1331
aWindow.removeEventListener("command", this, true);
1332
gPanelsForWindow.delete(aWindow);
1333
gBuildWindows.delete(aWindow);
1334
gSingleWrapperCache.delete(aWindow);
1335
let document = aWindow.document;
1336
1337
for (let [areaId, areaNodes] of gBuildAreas) {
1338
let areaProperties = gAreas.get(areaId);
1339
for (let node of areaNodes) {
1340
if (node.ownerDocument == document) {
1341
this.notifyListeners(
1342
"onAreaNodeUnregistered",
1343
areaId,
1344
this.getCustomizationTarget(node),
1345
CustomizableUI.REASON_WINDOW_CLOSED
1346
);
1347
if (areaProperties.has("overflowable")) {
1348
node.overflowable.uninit();
1349
node.overflowable = null;
1350
}
1351
areaNodes.delete(node);
1352
}
1353
}
1354
}
1355
1356
for (let [, widget] of gPalette) {
1357
widget.instances.delete(document);
1358
this.notifyListeners("onWidgetInstanceRemoved", widget.id, document);
1359
}
1360
1361
for (let [, pendingNodes] of gPendingBuildAreas) {
1362
for (let i = pendingNodes.length - 1; i >= 0; i--) {
1363
if (pendingNodes[i].ownerDocument == document) {
1364
pendingNodes.splice(i, 1);
1365
}
1366
}
1367
}
1368
1369
this.notifyListeners("onWindowClosed", aWindow);
1370
},
1371
1372
setLocationAttributes(aNode, aArea) {
1373
let props = gAreas.get(aArea);
1374
if (!props) {
1375
throw new Error(
1376
"Expected area " +
1377
aArea +
1378
" to have a properties Map " +
1379
"associated with it."
1380
);
1381
}
1382
1383
aNode.setAttribute("cui-areatype", props.get("type") || "");
1384
let anchor = props.get("anchor");
1385
if (anchor) {
1386
aNode.setAttribute("cui-anchorid", anchor);
1387
} else {
1388
aNode.removeAttribute("cui-anchorid");
1389
}
1390
},
1391
1392
removeLocationAttributes(aNode) {
1393
aNode.removeAttribute("cui-areatype");
1394
aNode.removeAttribute("cui-anchorid");
1395
},
1396
1397
insertNode(aWidgetId, aArea, aPosition, isNew) {
1398
let areaNodes = gBuildAreas.get(aArea);
1399
if (!areaNodes) {
1400
return;
1401
}
1402
1403
let placements = gPlacements.get(aArea);
1404
if (!placements) {
1405
log.error(
1406
"Could not find any placements for " + aArea + " when moving a widget."
1407
);
1408
return;
1409
}
1410
1411
// Go through each of the nodes associated with this area and move the
1412
// widget to the requested location.
1413
for (let areaNode of areaNodes) {
1414
this.insertNodeInWindow(aWidgetId, areaNode, isNew);
1415
}
1416
},
1417
1418
insertNodeInWindow(aWidgetId, aAreaNode, isNew) {
1419
let window = aAreaNode.ownerGlobal;
1420
let showInPrivateBrowsing = gPalette.has(aWidgetId)
1421
? gPalette.get(aWidgetId).showInPrivateBrowsing
1422
: true;
1423
1424
if (
1425
!showInPrivateBrowsing &&
1426
PrivateBrowsingUtils.isWindowPrivate(window)
1427
) {
1428
return;
1429
}
1430
1431
let [, widgetNode] = this.getWidgetNode(aWidgetId, window);
1432
if (!widgetNode) {
1433
log.error("Widget '" + aWidgetId + "' not found, unable to move");
1434
return;
1435
}
1436
1437
let areaId = aAreaNode.id;
1438
if (isNew) {
1439
this.ensureButtonContextMenu(widgetNode, aAreaNode);
1440
}
1441
1442
let [insertionContainer, nextNode] = this.findInsertionPoints(
1443
widgetNode,
1444
aAreaNode
1445
);
1446
this.insertWidgetBefore(widgetNode, nextNode, insertionContainer, areaId);
1447
},
1448
1449
findInsertionPoints(aNode, aAreaNode) {
1450
let areaId = aAreaNode.id;
1451
let props = gAreas.get(areaId);
1452
1453
// For overflowable toolbars, rely on them (because the work is more complicated):
1454
if (
1455
props.get("type") == CustomizableUI.TYPE_TOOLBAR &&
1456
props.get("overflowable")
1457
) {
1458
return aAreaNode.overflowable.findOverflowedInsertionPoints(aNode);
1459
}
1460
1461
let container = this.getCustomizationTarget(aAreaNode);
1462
let placements = gPlacements.get(areaId);
1463
let nodeIndex = placements.indexOf(aNode.id);
1464
1465
while (++nodeIndex < placements.length) {
1466
let nextNodeId = placements[nodeIndex];
1467
let nextNode = aNode.ownerDocument.getElementById(nextNodeId);
1468
// If the next placed widget exists, and is a direct child of the
1469
// container, or wrapped in a customize mode wrapper (toolbarpaletteitem)
1470
// inside the container, insert beside it.
1471
// We have to check the parent to avoid errors when the placement ids
1472
// are for nodes that are no longer customizable.
1473
if (
1474
nextNode &&
1475
(nextNode.parentNode == container ||
1476
(nextNode.parentNode.localName == "toolbarpaletteitem" &&
1477
nextNode.parentNode.parentNode == container))
1478
) {
1479
return [container, nextNode];
1480
}
1481
}
1482
1483
return [container, null];
1484
},
1485
1486
insertWidgetBefore(aNode, aNextNode, aContainer, aArea) {
1487
this.notifyListeners(
1488
"onWidgetBeforeDOMChange",
1489
aNode,
1490
aNextNode,
1491
aContainer
1492
);
1493
this.setLocationAttributes(aNode, aArea);
1494
aContainer.insertBefore(aNode, aNextNode);
1495
this.notifyListeners(
1496
"onWidgetAfterDOMChange",
1497
aNode,
1498
aNextNode,
1499
aContainer
1500
);
1501
},
1502
1503
handleEvent(aEvent) {
1504
switch (aEvent.type) {
1505
case "command":
1506
if (!this._originalEventInPanel(aEvent)) {
1507
break;
1508
}
1509
aEvent = aEvent.sourceEvent;
1510
// Fall through
1511
case "click":
1512
case "keypress":
1513
this.maybeAutoHidePanel(aEvent);
1514
break;
1515
case "unload":
1516
this.unregisterBuildWindow(aEvent.currentTarget);
1517
break;
1518
}
1519
},
1520
1521
_originalEventInPanel(aEvent) {
1522
let e = aEvent.sourceEvent;
1523
if (!e) {
1524
return false;
1525
}
1526
let node = this._getPanelForNode(e.target);
1527
if (!node) {
1528
return false;
1529
}
1530
let win = e.view;
1531
let panels = gPanelsForWindow.get(win);
1532
return !!panels && panels.has(node);
1533
},
1534
1535
_getSpecialIdForNode(aNode) {
1536
if (typeof aNode == "object" && aNode.localName) {
1537
if (aNode.id) {
1538
return aNode.id;
1539
}
1540
if (aNode.localName.startsWith("toolbar")) {
1541
return aNode.localName.substring(7);
1542
}
1543
return "";
1544
}
1545
return aNode;
1546
},
1547
1548
isSpecialWidget(aId) {
1549
aId = this._getSpecialIdForNode(aId);
1550
return (
1551
aId.startsWith(kSpecialWidgetPfx) ||
1552
aId.startsWith("separator") ||
1553
aId.startsWith("spring") ||
1554
aId.startsWith("spacer")
1555
);
1556
},
1557
1558
matchingSpecials(aId1, aId2) {
1559
aId1 = this._getSpecialIdForNode(aId1);
1560
aId2 = this._getSpecialIdForNode(aId2);
1561
1562
return (
1563
this.isSpecialWidget(aId1) &&
1564
this.isSpecialWidget(aId2) &&
1565
aId1.match(/spring|spacer|separator/)[0] ==
1566
aId2.match(/spring|spacer|separator/)[0]
1567
);
1568
},
1569
1570
ensureSpecialWidgetId(aId) {
1571
let nodeType = aId.match(/spring|spacer|separator/)[0];
1572
// If the ID we were passed isn't a generated one, generate one now:
1573
if (nodeType == aId) {
1574
// Ids are differentiated through a unique count suffix.
1575
return kSpecialWidgetPfx + aId + ++gNewElementCount;
1576
}
1577
return aId;
1578
},
1579
1580
createSpecialWidget(aId, aDocument) {
1581
let nodeName = "toolbar" + aId.match(/spring|spacer|separator/)[0];
1582
let node = aDocument.createXULElement(nodeName);
1583
node.className = "chromeclass-toolbar-additional";
1584
node.id = this.ensureSpecialWidgetId(aId);
1585
return node;
1586
},
1587
1588
/* Find a XUL-provided widget in a window. Don't try to use this
1589
* for an API-provided widget or a special widget.
1590
*/
1591
findWidgetInWindow(aId, aWindow) {
1592
if (!gBuildWindows.has(aWindow)) {
1593
throw new Error("Build window not registered");
1594
}
1595
1596
if (!aId) {
1597
log.error("findWidgetInWindow was passed an empty string.");
1598
return null;
1599
}
1600
1601
let document = aWindow.document;
1602
1603
// look for a node with the same id, as the node may be
1604
// in a different toolbar.
1605
let node = document.getElementById(aId);
1606
if (node) {
1607
let parent = node.parentNode;
1608
while (
1609
parent &&
1610
!(
1611
this.getCustomizationTarget(parent) ||
1612
parent == aWindow.gNavToolbox.palette
1613
)
1614
) {
1615
parent = parent.parentNode;
1616
}
1617
1618
if (parent) {
1619
let nodeInArea =
1620
node.parentNode.localName == "toolbarpaletteitem"
1621
? node.parentNode
1622
: node;
1623
// Check if we're in a customization target, or in the palette:
1624
if (
1625
(this.getCustomizationTarget(parent) == nodeInArea.parentNode &&
1626
gBuildWindows.get(aWindow).has(aWindow.gNavToolbox)) ||
1627
aWindow.gNavToolbox.palette == nodeInArea.parentNode
1628
) {
1629
// Normalize the removable attribute. For backwards compat, if
1630
// the widget is not located in a toolbox palette then absence
1631
// of the "removable" attribute means it is not removable.
1632
if (!node.hasAttribute("removable")) {
1633
// If we first see this in customization mode, it may be in the
1634
// customization palette instead of the toolbox palette.
1635
node.setAttribute(
1636
"removable",
1637
!this.getCustomizationTarget(parent)
1638
);
1639
}
1640
return node;
1641
}
1642
}
1643
}
1644
1645
let toolboxes = gBuildWindows.get(aWindow);
1646
for (let toolbox of toolboxes) {
1647
if (toolbox.palette) {
1648
// Attempt to locate an element with a matching ID within
1649
// the palette.
1650
let element = toolbox.palette.getElementsByAttribute("id", aId)[0];
1651
if (element) {
1652
// Normalize the removable attribute. For backwards compat, this
1653
// is optional if the widget is located in the toolbox palette,
1654
// and defaults to *true*, unlike if it was located elsewhere.
1655
if (!element.hasAttribute("removable")) {
1656
element.setAttribute("removable", true);
1657
}
1658
return element;
1659
}
1660
}
1661
}
1662
return null;
1663
},
1664
1665
buildWidget(aDocument, aWidget) {
1666
if (aDocument.documentURI != kExpectedWindowURL) {
1667
throw new Error("buildWidget was called for a non-browser window!");
1668
}
1669
if (typeof aWidget == "string") {
1670
aWidget = gPalette.get(aWidget);
1671
}
1672
if (!aWidget) {
1673
throw new Error("buildWidget was passed a non-widget to build.");
1674
}
1675
if (
1676
!aWidget.showInPrivateBrowsing &&
1677
PrivateBrowsingUtils.isWindowPrivate(aDocument.defaultView)
1678
) {
1679
return null;
1680
}
1681
1682
log.debug("Building " + aWidget.id + " of type " + aWidget.type);
1683
1684
let node;
1685
if (aWidget.type == "custom") {
1686
if (aWidget.onBuild) {
1687
node = aWidget.onBuild(aDocument);
1688
}
1689
if (!node || !(node instanceof aDocument.defaultView.XULElement)) {
1690
log.error(
1691
"Custom widget with id " +
1692
aWidget.id +
1693
" does not return a valid node"
1694
);
1695
}
1696
} else {
1697
if (aWidget.onBeforeCreated) {
1698
aWidget.onBeforeCreated(aDocument);
1699
}
1700
node = aDocument.createXULElement("toolbarbutton");
1701
1702
node.setAttribute("id", aWidget.id);
1703
node.setAttribute("widget-id", aWidget.id);
1704
node.setAttribute("widget-type", aWidget.type);
1705
if (aWidget.disabled) {
1706
node.setAttribute("disabled", true);
1707
}
1708
node.setAttribute("removable", aWidget.removable);
1709
node.setAttribute("overflows", aWidget.overflows);
1710
if (aWidget.tabSpecific) {
1711
node.setAttribute("tabspecific", aWidget.tabSpecific);
1712
}
1713
node.setAttribute("label", this.getLocalizedProperty(aWidget, "label"));
1714
let additionalTooltipArguments = [];
1715
if (aWidget.shortcutId) {
1716
let keyEl = aDocument.getElementById(aWidget.shortcutId);
1717
if (keyEl) {
1718
additionalTooltipArguments.push(
1719
ShortcutUtils.prettifyShortcut(keyEl)
1720
);
1721
} else {
1722
log.error(
1723
"Key element with id '" +
1724
aWidget.shortcutId +
1725
"' for widget '" +
1726
aWidget.id +
1727
"' not found!"
1728
);
1729
}
1730
}
1731
1732
let tooltip = this.getLocalizedProperty(
1733
aWidget,
1734
"tooltiptext",
1735
additionalTooltipArguments
1736
);
1737
if (tooltip) {
1738
node.setAttribute("tooltiptext", tooltip);
1739
}
1740
1741
let commandHandler = this.handleWidgetCommand.bind(this, aWidget, node);
1742
node.addEventListener("command", commandHandler);
1743
let clickHandler = this.handleWidgetClick.bind(this, aWidget, node);
1744
node.addEventListener("click", clickHandler);
1745
1746
let nodeClasses = ["toolbarbutton-1", "chromeclass-toolbar-additional"];
1747
1748
// If the widget has a view, and has view showing / hiding listeners,
1749
// hook those up to this widget.
1750
if (aWidget.type == "view") {
1751
log.debug(
1752
"Widget " +
1753
aWidget.id +
1754
" has a view. Auto-registering event handlers."
1755
);
1756
let viewNode = aDocument.getElementById(aWidget.viewId);
1757
1758
if (viewNode) {
1759
// PanelUI relies on the .PanelUI-subView class to be able to show only
1760
// one sub-view at a time.
1761
viewNode.classList.add("PanelUI-subView");
1762
if (aWidget.source == CustomizableUI.SOURCE_BUILTIN) {
1763
nodeClasses.push("subviewbutton-nav");
1764
}
1765
this.ensureSubviewListeners(viewNode);
1766
} else {
1767
log.error(
1768
"Could not find the view node with id: " +
1769
aWidget.viewId +
1770
", for widget: " +
1771
aWidget.id +
1772
"."
1773
);
1774
}
1775
1776
let keyPressHandler = this.handleWidgetKeyPress.bind(
1777
this,
1778
aWidget,
1779
node
1780
);
1781
node.addEventListener("keypress", keyPressHandler);
1782
}
1783
node.setAttribute("class", nodeClasses.join(" "));
1784
1785
if (aWidget.onCreated) {
1786
aWidget.onCreated(node);
1787
}
1788
}
1789
1790
aWidget.instances.set(aDocument, node);
1791
return node;
1792
},
1793
1794
ensureSubviewListeners(viewNode) {
1795
if (viewNode._addedEventListeners) {
1796
return;
1797
}
1798
let viewId = viewNode.id;
1799
let widget = [...gPalette.values()].find(w => w.viewId == viewId);
1800
if (!widget) {
1801
return;
1802
}
1803
for (let eventName of kSubviewEvents) {
1804
let handler = "on" + eventName;
1805
if (typeof widget[handler] == "function") {
1806
viewNode.addEventListener(eventName, widget[handler]);
1807
}
1808
}
1809
viewNode._addedEventListeners = true;
1810
log.debug(
1811
"Widget " + widget.id + " showing and hiding event handlers set."
1812
);
1813
},
1814
1815
getLocalizedProperty(aWidget, aProp, aFormatArgs, aDef) {
1816
const kReqStringProps = ["label"];
1817
1818
if (typeof aWidget == "string") {
1819
aWidget = gPalette.get(aWidget);
1820
}
1821
if (!aWidget) {
1822
throw new Error(
1823
"getLocalizedProperty was passed a non-widget to work with."
1824
);
1825
}
1826
let def, name;
1827
// Let widgets pass their own string identifiers or strings, so that
1828
// we can use strings which aren't the default (in case string ids change)
1829
// and so that non-builtin-widgets can also provide labels, tooltips, etc.
1830
if (aWidget[aProp] != null) {
1831
name = aWidget[aProp];
1832
// By using this as the default, if a widget provides a full string rather
1833
// than a string ID for localization, we will fall back to that string
1834
// and return that.
1835
def = aDef || name;
1836
} else {
1837
name = aWidget.id + "." + aProp;
1838
def = aDef || "";
1839
}
1840
if (aWidget.localized === false) {
1841
return def;
1842
}
1843
try {
1844
if (Array.isArray(aFormatArgs) && aFormatArgs.length) {
1845
return gWidgetsBundle.formatStringFromName(name, aFormatArgs) || def;
1846
}
1847
return gWidgetsBundle.GetStringFromName(name) || def;
1848
} catch (ex) {
1849
// If an empty string was explicitly passed, treat it as an actual
1850
// value rather than a missing property.
1851
if (!def && (name != "" || kReqStringProps.includes(aProp))) {
1852
log.error("Could not localize property '" + name + "'.");
1853
}
1854
}
1855
return def;
1856
},
1857
1858
addShortcut(aShortcutNode, aTargetNode = aShortcutNode) {
1859
// Detect if we've already been here before.
1860
if (aTargetNode.hasAttribute("shortcut")) {
1861
return;
1862
}
1863
1864
let document = aShortcutNode.ownerDocument;
1865
let shortcutId = aShortcutNode.getAttribute("key");
1866
let shortcut;
1867
if (shortcutId) {
1868
shortcut = document.getElementById(shortcutId);
1869
} else {
1870
let commandId = aShortcutNode.getAttribute("command");
1871
if (commandId) {
1872
shortcut = ShortcutUtils.findShortcut(
1873
document.getElementById(commandId)
1874
);
1875
}
1876
}
1877
if (!shortcut) {
1878
return;
1879
}
1880
1881
aTargetNode.setAttribute(
1882
"shortcut",
1883
ShortcutUtils.prettifyShortcut(shortcut)
1884
);
1885
},
1886
1887
handleWidgetCommand(aWidget, aNode, aEvent) {
1888
// Note that aEvent can be a keypress event for widgets of type "view".
1889
log.debug("handleWidgetCommand");
1890
1891
if (aWidget.onBeforeCommand) {
1892
try {
1893
aWidget.onBeforeCommand.call(null, aEvent);
1894
} catch (e) {
1895
log.error(e);
1896
}
1897
}
1898
1899
if (aWidget.type == "button") {
1900
if (aWidget.onCommand) {
1901
try {
1902
aWidget.onCommand.call(null, aEvent);
1903
} catch (e) {
1904
log.error(e);
1905
}
1906
} else {
1907
// XXXunf Need to think this through more, and formalize.
1908
Services.obs.notifyObservers(
1909
aNode,
1910
"customizedui-widget-command",
1911
aWidget.id
1912
);
1913
}
1914
} else if (aWidget.type == "view") {
1915
let ownerWindow = aNode.ownerGlobal;
1916
let area = this.getPlacementOfWidget(aNode.id).area;
1917
let areaType = CustomizableUI.getAreaType(area);
1918
let anchor = aNode;
1919
if (areaType != CustomizableUI.TYPE_MENU_PANEL) {
1920
let wrapper = this.wrapWidget(aWidget.id).forWindow(ownerWindow);
1921
1922
let hasMultiView = !!aNode.closest("panelmultiview");
1923
if (wrapper && !hasMultiView && wrapper.anchor) {
1924
this.hidePanelForNode(aNode);
1925
anchor = wrapper.anchor;
1926
}
1927
}
1928
1929
ownerWindow.PanelUI.showSubView(aWidget.viewId, anchor, aEvent);
1930
}
1931
},
1932
1933
handleWidgetClick(aWidget, aNode, aEvent) {
1934
log.debug("handleWidgetClick");
1935
if (aWidget.onClick) {
1936
try {
1937
aWidget.onClick.call(null, aEvent);