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
5
"use strict";
6
7
var EXPORTED_SYMBOLS = ["CustomizeMode"];
8
9
const kPrefCustomizationDebug = "browser.uiCustomization.debug";
10
const kPaletteId = "customization-palette";
11
const kDragDataTypePrefix = "text/toolbarwrapper-id/";
12
const kSkipSourceNodePref = "browser.uiCustomization.skipSourceNodeCheck";
13
const kDrawInTitlebarPref = "browser.tabs.drawInTitlebar";
14
const kExtraDragSpacePref = "browser.tabs.extraDragSpace";
15
const kKeepBroadcastAttributes = "keepbroadcastattributeswhencustomizing";
16
17
const kPanelItemContextMenu = "customizationPanelItemContextMenu";
18
const kPaletteItemContextMenu = "customizationPaletteItemContextMenu";
19
20
const kDownloadAutohideCheckboxId = "downloads-button-autohide-checkbox";
21
const kDownloadAutohidePanelId = "downloads-button-autohide-panel";
22
const kDownloadAutoHidePref = "browser.download.autohideButton";
23
24
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
25
const { CustomizableUI } = ChromeUtils.import(
27
);
28
const { XPCOMUtils } = ChromeUtils.import(
30
);
31
const { AppConstants } = ChromeUtils.import(
33
);
34
35
XPCOMUtils.defineLazyGlobalGetters(this, ["CSS"]);
36
37
ChromeUtils.defineModuleGetter(
38
this,
39
"AddonManager",
41
);
42
ChromeUtils.defineModuleGetter(
43
this,
44
"AMTelemetry",
46
);
47
ChromeUtils.defineModuleGetter(
48
this,
49
"DragPositionManager",
51
);
52
ChromeUtils.defineModuleGetter(
53
this,
54
"BrowserUtils",
56
);
57
ChromeUtils.defineModuleGetter(
58
this,
59
"SessionStore",
61
);
62
XPCOMUtils.defineLazyGetter(this, "gWidgetsBundle", function() {
63
const kUrl =
65
return Services.strings.createBundle(kUrl);
66
});
67
XPCOMUtils.defineLazyPreferenceGetter(
68
this,
69
"gCosmeticAnimationsEnabled",
70
"toolkit.cosmeticAnimations.enabled"
71
);
72
XPCOMUtils.defineLazyServiceGetter(
73
this,
74
"gTouchBarUpdater",
75
"@mozilla.org/widget/touchbarupdater;1",
76
"nsITouchBarUpdater"
77
);
78
XPCOMUtils.defineLazyPreferenceGetter(
79
this,
80
"gCosmeticAnimationsEnabled",
81
"toolkit.cosmeticAnimations.enabled"
82
);
83
84
let gDebug;
85
XPCOMUtils.defineLazyGetter(this, "log", () => {
86
let scope = {};
87
ChromeUtils.import("resource://gre/modules/Console.jsm", scope);
88
gDebug = Services.prefs.getBoolPref(kPrefCustomizationDebug, false);
89
let consoleOptions = {
90
maxLogLevel: gDebug ? "all" : "log",
91
prefix: "CustomizeMode",
92
};
93
return new scope.ConsoleAPI(consoleOptions);
94
});
95
96
var gDraggingInToolbars;
97
98
var gTab;
99
100
function closeGlobalTab() {
101
let win = gTab.ownerGlobal;
102
if (win.gBrowser.browsers.length == 1) {
103
win.BrowserOpenTab();
104
}
105
win.gBrowser.removeTab(gTab);
106
gTab = null;
107
}
108
109
var gTabsProgressListener = {
110
onLocationChange(aBrowser, aWebProgress, aRequest, aLocation, aFlags) {
111
// Tear down customize mode when the customize mode tab loads some other page.
112
// Customize mode will be re-entered if "about:blank" is loaded again, so
113
// don't tear down in this case.
114
if (
115
!gTab ||
116
gTab.linkedBrowser != aBrowser ||
117
aLocation.spec == "about:blank"
118
) {
119
return;
120
}
121
122
unregisterGlobalTab();
123
},
124
};
125
126
function unregisterGlobalTab() {
127
gTab.removeEventListener("TabClose", unregisterGlobalTab);
128
let win = gTab.ownerGlobal;
129
win.removeEventListener("unload", unregisterGlobalTab);
130
win.gBrowser.removeTabsProgressListener(gTabsProgressListener);
131
132
gTab.removeAttribute("customizemode");
133
134
gTab = null;
135
}
136
137
function CustomizeMode(aWindow) {
138
this.window = aWindow;
139
this.document = aWindow.document;
140
this.browser = aWindow.gBrowser;
141
this.areas = new Set();
142
143
let content = this.$("customization-content-container");
144
if (!content) {
145
this.window.MozXULElement.insertFTLIfNeeded("browser/customizeMode.ftl");
146
let container = this.$("customization-container");
147
container.replaceChild(
148
this.window.MozXULElement.parseXULToFragment(container.firstChild.data),
149
container.lastChild
150
);
151
}
152
// There are two palettes - there's the palette that can be overlayed with
153
// toolbar items in browser.xhtml. This is invisible, and never seen by the
154
// user. Then there's the visible palette, which gets populated and displayed
155
// to the user when in customizing mode.
156
this.visiblePalette = this.$(kPaletteId);
157
this.pongArena = this.$("customization-pong-arena");
158
159
if (this._canDrawInTitlebar()) {
160
this._updateTitlebarCheckbox();
161
this._updateDragSpaceCheckbox();
162
Services.prefs.addObserver(kDrawInTitlebarPref, this);
163
Services.prefs.addObserver(kExtraDragSpacePref, this);
164
} else {
165
this.$("customization-titlebar-visibility-checkbox").hidden = true;
166
this.$("customization-extra-drag-space-checkbox").hidden = true;
167
}
168
169
this.window.addEventListener("unload", this);
170
}
171
172
CustomizeMode.prototype = {
173
_changed: false,
174
_transitioning: false,
175
window: null,
176
document: null,
177
// areas is used to cache the customizable areas when in customization mode.
178
areas: null,
179
// When in customizing mode, we swap out the reference to the invisible
180
// palette in gNavToolbox.palette for our visiblePalette. This way, for the
181
// customizing browser window, when widgets are removed from customizable
182
// areas and added to the palette, they're added to the visible palette.
183
// _stowedPalette is a reference to the old invisible palette so we can
184
// restore gNavToolbox.palette to its original state after exiting
185
// customization mode.
186
_stowedPalette: null,
187
_dragOverItem: null,
188
_customizing: false,
189
_skipSourceNodeCheck: null,
190
_mainViewContext: null,
191
192
get _handler() {
193
return this.window.CustomizationHandler;
194
},
195
196
uninit() {
197
if (this._canDrawInTitlebar()) {
198
Services.prefs.removeObserver(kDrawInTitlebarPref, this);
199
Services.prefs.removeObserver(kExtraDragSpacePref, this);
200
}
201
},
202
203
$(id) {
204
return this.document.getElementById(id);
205
},
206
207
toggle() {
208
if (
209
this._handler.isEnteringCustomizeMode ||
210
this._handler.isExitingCustomizeMode
211
) {
212
this._wantToBeInCustomizeMode = !this._wantToBeInCustomizeMode;
213
return;
214
}
215
if (this._customizing) {
216
this.exit();
217
} else {
218
this.enter();
219
}
220
},
221
222
async _updateThemeButtonIcon() {
223
let lwthemeButton = this.$("customization-lwtheme-button");
224
let lwthemeIcon = lwthemeButton.icon;
225
let theme = (await AddonManager.getAddonsByTypes(["theme"])).find(
226
addon => addon.isActive
227
);
228
lwthemeIcon.style.backgroundImage =
229
theme && theme.iconURL ? "url(" + theme.iconURL + ")" : "";
230
},
231
232
setTab(aTab) {
233
if (gTab == aTab) {
234
return;
235
}
236
237
if (gTab) {
238
closeGlobalTab();
239
}
240
241
gTab = aTab;
242
243
gTab.setAttribute("customizemode", "true");
244
SessionStore.persistTabAttribute("customizemode");
245
246
if (gTab.linkedPanel) {
247
gTab.linkedBrowser.stop();
248
}
249
250
let win = gTab.ownerGlobal;
251
252
win.gBrowser.setTabTitle(gTab);
253
win.gBrowser.setIcon(gTab, "chrome://browser/skin/customize.svg");
254
255
gTab.addEventListener("TabClose", unregisterGlobalTab);
256
257
win.gBrowser.addTabsProgressListener(gTabsProgressListener);
258
259
win.addEventListener("unload", unregisterGlobalTab);
260
261
if (gTab.selected) {
262
win.gCustomizeMode.enter();
263
}
264
},
265
266
enter() {
267
if (!this.window.toolbar.visible) {
268
let w = this.window.getTopWin(true);
269
if (w) {
270
w.gCustomizeMode.enter();
271
return;
272
}
273
let obs = () => {
274
Services.obs.removeObserver(obs, "browser-delayed-startup-finished");
275
w = this.window.getTopWin(true);
276
w.gCustomizeMode.enter();
277
};
278
Services.obs.addObserver(obs, "browser-delayed-startup-finished");
279
this.window.openTrustedLinkIn("about:newtab", "window");
280
return;
281
}
282
this._wantToBeInCustomizeMode = true;
283
284
if (this._customizing || this._handler.isEnteringCustomizeMode) {
285
return;
286
}
287
288
// Exiting; want to re-enter once we've done that.
289
if (this._handler.isExitingCustomizeMode) {
290
log.debug(
291
"Attempted to enter while we're in the middle of exiting. " +
292
"We'll exit after we've entered"
293
);
294
return;
295
}
296
297
if (!gTab) {
298
this.setTab(
299
this.browser.loadOneTab("about:blank", {
300
inBackground: false,
301
forceNotRemote: true,
302
skipAnimation: true,
303
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
304
})
305
);
306
return;
307
}
308
if (!gTab.selected) {
309
// This will force another .enter() to be called via the
310
// onlocationchange handler of the tabbrowser, so we return early.
311
gTab.ownerGlobal.gBrowser.selectedTab = gTab;
312
return;
313
}
314
gTab.ownerGlobal.focus();
315
if (gTab.ownerDocument != this.document) {
316
return;
317
}
318
319
let window = this.window;
320
let document = this.document;
321
322
this._handler.isEnteringCustomizeMode = true;
323
324
// Always disable the reset button at the start of customize mode, it'll be re-enabled
325
// if necessary when we finish entering:
326
let resetButton = this.$("customization-reset-button");
327
resetButton.setAttribute("disabled", "true");
328
329
(async () => {
330
// We shouldn't start customize mode until after browser-delayed-startup has finished:
331
if (!this.window.gBrowserInit.delayedStartupFinished) {
332
await new Promise(resolve => {
333
let delayedStartupObserver = aSubject => {
334
if (aSubject == this.window) {
335
Services.obs.removeObserver(
336
delayedStartupObserver,
337
"browser-delayed-startup-finished"
338
);
339
resolve();
340
}
341
};
342
343
Services.obs.addObserver(
344
delayedStartupObserver,
345
"browser-delayed-startup-finished"
346
);
347
});
348
}
349
350
CustomizableUI.dispatchToolboxEvent("beforecustomization", {}, window);
351
CustomizableUI.notifyStartCustomizing(this.window);
352
353
// Add a keypress listener to the document so that we can quickly exit
354
// customization mode when pressing ESC.
355
document.addEventListener("keypress", this);
356
357
// Same goes for the menu button - if we're customizing, a click on the
358
// menu button means a quick exit from customization mode.
359
window.PanelUI.hide();
360
361
let panelHolder = document.getElementById("customization-panelHolder");
362
let panelContextMenu = document.getElementById(kPanelItemContextMenu);
363
this._previousPanelContextMenuParent = panelContextMenu.parentNode;
364
document.getElementById("mainPopupSet").appendChild(panelContextMenu);
365
panelHolder.appendChild(window.PanelUI.overflowFixedList);
366
367
window.PanelUI.overflowFixedList.setAttribute("customizing", true);
368
window.PanelUI.menuButton.disabled = true;
369
document.getElementById("nav-bar-overflow-button").disabled = true;
370
371
this._transitioning = true;
372
373
let customizer = document.getElementById("customization-container");
374
let browser = document.getElementById("browser");
375
browser.collapsed = true;
376
customizer.hidden = false;
377
378
this._wrapToolbarItemSync(CustomizableUI.AREA_TABSTRIP);
379
380
let customizableToolbars = document.querySelectorAll(
381
"toolbar[customizable=true]:not([autohide=true]):not([collapsed=true])"
382
);
383
for (let toolbar of customizableToolbars) {
384
toolbar.setAttribute("customizing", true);
385
}
386
387
this._updateOverflowPanelArrowOffset();
388
389
await this._doTransition(true);
390
391
// Let everybody in this window know that we're about to customize.
392
CustomizableUI.dispatchToolboxEvent("customizationstarting", {}, window);
393
394
await this._wrapToolbarItems();
395
this.populatePalette();
396
397
this._setupPaletteDragging();
398
399
window.gNavToolbox.addEventListener("toolbarvisibilitychange", this);
400
401
this._updateResetButton();
402
this._updateUndoResetButton();
403
this._updateTouchBarButton();
404
405
this._skipSourceNodeCheck =
406
Services.prefs.getPrefType(kSkipSourceNodePref) ==
407
Ci.nsIPrefBranch.PREF_BOOL &&
408
Services.prefs.getBoolPref(kSkipSourceNodePref);
409
410
CustomizableUI.addListener(this);
411
this._customizing = true;
412
this._transitioning = false;
413
414
// Show the palette now that the transition has finished.
415
this.visiblePalette.hidden = false;
416
window.setTimeout(() => {
417
// Force layout reflow to ensure the animation runs,
418
// and make it async so it doesn't affect the timing.
419
this.visiblePalette.clientTop;
420
this.visiblePalette.setAttribute("showing", "true");
421
}, 0);
422
this._updateEmptyPaletteNotice();
423
424
this._updateThemeButtonIcon();
425
AddonManager.addAddonListener(this);
426
427
this._setupDownloadAutoHideToggle();
428
429
this._handler.isEnteringCustomizeMode = false;
430
431
CustomizableUI.dispatchToolboxEvent("customizationready", {}, window);
432
433
if (!this._wantToBeInCustomizeMode) {
434
this.exit();
435
}
436
})().catch(e => {
437
log.error("Error entering customize mode", e);
438
this._handler.isEnteringCustomizeMode = false;
439
// Exit customize mode to ensure proper clean-up when entering failed.
440
this.exit();
441
});
442
},
443
444
exit() {
445
this._wantToBeInCustomizeMode = false;
446
447
if (!this._customizing || this._handler.isExitingCustomizeMode) {
448
return;
449
}
450
451
// Entering; want to exit once we've done that.
452
if (this._handler.isEnteringCustomizeMode) {
453
log.debug(
454
"Attempted to exit while we're in the middle of entering. " +
455
"We'll exit after we've entered"
456
);
457
return;
458
}
459
460
if (this.resetting) {
461
log.debug(
462
"Attempted to exit while we're resetting. " +
463
"We'll exit after resetting has finished."
464
);
465
return;
466
}
467
468
this._handler.isExitingCustomizeMode = true;
469
470
this._teardownDownloadAutoHideToggle();
471
472
AddonManager.removeAddonListener(this);
473
CustomizableUI.removeListener(this);
474
475
this.document.removeEventListener("keypress", this);
476
477
let window = this.window;
478
let document = this.document;
479
480
this.togglePong(false);
481
482
// Disable the reset and undo reset buttons while transitioning:
483
let resetButton = this.$("customization-reset-button");
484
let undoResetButton = this.$("customization-undo-reset-button");
485
undoResetButton.hidden = resetButton.disabled = true;
486
487
this._transitioning = true;
488
489
(async () => {
490
await this.depopulatePalette();
491
492
await this._doTransition(false);
493
494
if (this.browser.selectedTab == gTab) {
495
if (gTab.linkedBrowser.currentURI.spec == "about:blank") {
496
closeGlobalTab();
497
} else {
498
unregisterGlobalTab();
499
}
500
}
501
let customizer = document.getElementById("customization-container");
502
let browser = document.getElementById("browser");
503
customizer.hidden = true;
504
browser.collapsed = false;
505
506
window.gNavToolbox.removeEventListener("toolbarvisibilitychange", this);
507
508
this._teardownPaletteDragging();
509
510
await this._unwrapToolbarItems();
511
512
// And drop all area references.
513
this.areas.clear();
514
515
// Let everybody in this window know that we're starting to
516
// exit customization mode.
517
CustomizableUI.dispatchToolboxEvent("customizationending", {}, window);
518
519
window.PanelUI.menuButton.disabled = false;
520
let overflowContainer = document.getElementById(
521
"widget-overflow-mainView"
522
).firstElementChild;
523
overflowContainer.appendChild(window.PanelUI.overflowFixedList);
524
document.getElementById("nav-bar-overflow-button").disabled = false;
525
let panelContextMenu = document.getElementById(kPanelItemContextMenu);
526
this._previousPanelContextMenuParent.appendChild(panelContextMenu);
527
528
// We need to set this._customizing to false before removing the tab
529
// or the TabSelect event handler will think that we are exiting
530
// customization mode for a second time.
531
this._customizing = false;
532
533
let customizableToolbars = document.querySelectorAll(
534
"toolbar[customizable=true]:not([autohide=true])"
535
);
536
for (let toolbar of customizableToolbars) {
537
toolbar.removeAttribute("customizing");
538
}
539
540
this._maybeMoveDownloadsButtonToNavBar();
541
542
delete this._lastLightweightTheme;
543
this._changed = false;
544
this._transitioning = false;
545
this._handler.isExitingCustomizeMode = false;
546
CustomizableUI.dispatchToolboxEvent("aftercustomization", {}, window);
547
CustomizableUI.notifyEndCustomizing(window);
548
549
if (this._wantToBeInCustomizeMode) {
550
this.enter();
551
}
552
})().catch(e => {
553
log.error("Error exiting customize mode", e);
554
this._handler.isExitingCustomizeMode = false;
555
});
556
},
557
558
/**
559
* The customize mode transition has 4 phases when entering:
560
* 1) Pre-customization mode
561
* This is the starting phase of the browser.
562
* 2) LWT swapping
563
* This is where we swap some of the lightweight theme styles in order
564
* to make them work in customize mode. We set/unset a customization-
565
* lwtheme attribute iff we're using a lightweight theme.
566
* 3) customize-entering
567
* This phase is a transition, optimized for smoothness.
568
* 4) customize-entered
569
* After the transition completes, this phase draws all of the
570
* expensive detail that isn't necessary during the second phase.
571
*
572
* Exiting customization mode has a similar set of phases, but in reverse
573
* order - customize-entered, customize-exiting, remove LWT swapping,
574
* pre-customization mode.
575
*
576
* When in the customize-entering, customize-entered, or customize-exiting
577
* phases, there is a "customizing" attribute set on the main-window to simplify
578
* excluding certain styles while in any phase of customize mode.
579
*/
580
_doTransition(aEntering) {
581
let docEl = this.document.documentElement;
582
if (aEntering) {
583
docEl.setAttribute("customizing", true);
584
docEl.setAttribute("customize-entered", true);
585
} else {
586
docEl.removeAttribute("customizing");
587
docEl.removeAttribute("customize-entered");
588
}
589
return Promise.resolve();
590
},
591
592
/**
593
* The overflow panel in customize mode should have its arrow pointing
594
* at the overflow button. In order to do this correctly, we pass the
595
* distance between the inside of window and the middle of the button
596
* to the customize mode markup in which the arrow and panel are placed.
597
*/
598
async _updateOverflowPanelArrowOffset() {
599
let currentDensity = this.document.documentElement.getAttribute(
600
"uidensity"
601
);
602
let offset = await this.window.promiseDocumentFlushed(() => {
603
let overflowButton = this.$("nav-bar-overflow-button");
604
let buttonRect = overflowButton.getBoundingClientRect();
605
let endDistance;
606
if (this.window.RTL_UI) {
607
endDistance = buttonRect.left;
608
} else {
609
endDistance = this.window.innerWidth - buttonRect.right;
610
}
611
return endDistance + buttonRect.width / 2;
612
});
613
if (
614
!this.document ||
615
currentDensity != this.document.documentElement.getAttribute("uidensity")
616
) {
617
return;
618
}
619
this.$("customization-panelWrapper").style.setProperty(
620
"--panel-arrow-offset",
621
offset + "px"
622
);
623
},
624
625
_getCustomizableChildForNode(aNode) {
626
// NB: adjusted from _getCustomizableParent to keep that method fast
627
// (it's used during drags), and avoid multiple DOM loops
628
let areas = CustomizableUI.areas;
629
// Caching this length is important because otherwise we'll also iterate
630
// over items we add to the end from within the loop.
631
let numberOfAreas = areas.length;
632
for (let i = 0; i < numberOfAreas; i++) {
633
let area = areas[i];
634
let areaNode = aNode.ownerDocument.getElementById(area);
635
let customizationTarget = CustomizableUI.getCustomizationTarget(areaNode);
636
if (customizationTarget && customizationTarget != areaNode) {
637
areas.push(customizationTarget.id);
638
}
639
let overflowTarget = areaNode && areaNode.getAttribute("overflowtarget");
640
if (overflowTarget) {
641
areas.push(overflowTarget);
642
}
643
}
644
areas.push(kPaletteId);
645
646
while (aNode && aNode.parentNode) {
647
let parent = aNode.parentNode;
648
if (areas.includes(parent.id)) {
649
return aNode;
650
}
651
aNode = parent;
652
}
653
return null;
654
},
655
656
_promiseWidgetAnimationOut(aNode) {
657
if (
658
!gCosmeticAnimationsEnabled ||
659
aNode.getAttribute("cui-anchorid") == "nav-bar-overflow-button" ||
660
(aNode.tagName != "toolbaritem" && aNode.tagName != "toolbarbutton") ||
661
(aNode.id == "downloads-button" && aNode.hidden)
662
) {
663
return null;
664
}
665
666
let animationNode;
667
if (aNode.parentNode && aNode.parentNode.id.startsWith("wrapper-")) {
668
animationNode = aNode.parentNode;
669
} else {
670
animationNode = aNode;
671
}
672
return new Promise(resolve => {
673
function cleanupCustomizationExit() {
674
resolveAnimationPromise();
675
}
676
677
function cleanupWidgetAnimationEnd(e) {
678
if (
679
e.animationName == "widget-animate-out" &&
680
e.target.id == animationNode.id
681
) {
682
resolveAnimationPromise();
683
}
684
}
685
686
function resolveAnimationPromise() {
687
animationNode.removeEventListener(
688
"animationend",
689
cleanupWidgetAnimationEnd
690
);
691
animationNode.removeEventListener(
692
"customizationending",
693
cleanupCustomizationExit
694
);
695
resolve();
696
}
697
698
// Wait until the next frame before setting the class to ensure
699
// we do start the animation.
700
this.window.requestAnimationFrame(() => {
701
this.window.requestAnimationFrame(() => {
702
animationNode.classList.add("animate-out");
703
animationNode.ownerGlobal.gNavToolbox.addEventListener(
704
"customizationending",
705
cleanupCustomizationExit
706
);
707
animationNode.addEventListener(
708
"animationend",
709
cleanupWidgetAnimationEnd
710
);
711
});
712
});
713
});
714
},
715
716
async addToToolbar(aNode) {
717
aNode = this._getCustomizableChildForNode(aNode);
718
if (aNode.localName == "toolbarpaletteitem" && aNode.firstElementChild) {
719
aNode = aNode.firstElementChild;
720
}
721
let widgetAnimationPromise = this._promiseWidgetAnimationOut(aNode);
722
if (widgetAnimationPromise) {
723
await widgetAnimationPromise;
724
}
725
726
let widgetToAdd = aNode.id;
727
if (
728
CustomizableUI.isSpecialWidget(widgetToAdd) &&
729
aNode.closest("#customization-palette")
730
) {
731
widgetToAdd = widgetToAdd.match(
732
/^customizableui-special-(spring|spacer|separator)/
733
)[1];
734
}
735
736
CustomizableUI.addWidgetToArea(widgetToAdd, CustomizableUI.AREA_NAVBAR);
737
if (!this._customizing) {
738
CustomizableUI.dispatchToolboxEvent("customizationchange");
739
}
740
741
// If the user explicitly moves this item, turn off autohide.
742
if (aNode.id == "downloads-button") {
743
Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
744
if (this._customizing) {
745
this._showDownloadsAutoHidePanel();
746
}
747
}
748
749
if (widgetAnimationPromise) {
750
if (aNode.parentNode && aNode.parentNode.id.startsWith("wrapper-")) {
751
aNode.parentNode.classList.remove("animate-out");
752
} else {
753
aNode.classList.remove("animate-out");
754
}
755
}
756
},
757
758
async addToPanel(aNode) {
759
aNode = this._getCustomizableChildForNode(aNode);
760
if (aNode.localName == "toolbarpaletteitem" && aNode.firstElementChild) {
761
aNode = aNode.firstElementChild;
762
}
763
let widgetAnimationPromise = this._promiseWidgetAnimationOut(aNode);
764
if (widgetAnimationPromise) {
765
await widgetAnimationPromise;
766
}
767
768
let panel = CustomizableUI.AREA_FIXED_OVERFLOW_PANEL;
769
CustomizableUI.addWidgetToArea(aNode.id, panel);
770
if (!this._customizing) {
771
CustomizableUI.dispatchToolboxEvent("customizationchange");
772
}
773
774
// If the user explicitly moves this item, turn off autohide.
775
if (aNode.id == "downloads-button") {
776
Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
777
if (this._customizing) {
778
this._showDownloadsAutoHidePanel();
779
}
780
}
781
782
if (widgetAnimationPromise) {
783
if (aNode.parentNode && aNode.parentNode.id.startsWith("wrapper-")) {
784
aNode.parentNode.classList.remove("animate-out");
785
} else {
786
aNode.classList.remove("animate-out");
787
}
788
}
789
if (gCosmeticAnimationsEnabled) {
790
let overflowButton = this.$("nav-bar-overflow-button");
791
BrowserUtils.setToolbarButtonHeightProperty(overflowButton).then(() => {
792
overflowButton.setAttribute("animate", "true");
793
overflowButton.addEventListener("animationend", function onAnimationEnd(
794
event
795
) {
796
if (event.animationName.startsWith("overflow-animation")) {
797
this.setAttribute("fade", "true");
798
} else if (event.animationName == "overflow-fade") {
799
this.removeEventListener("animationend", onAnimationEnd);
800
this.removeAttribute("animate");
801
this.removeAttribute("fade");
802
}
803
});
804
});
805
}
806
},
807
808
async removeFromArea(aNode) {
809
aNode = this._getCustomizableChildForNode(aNode);
810
if (aNode.localName == "toolbarpaletteitem" && aNode.firstElementChild) {
811
aNode = aNode.firstElementChild;
812
}
813
let widgetAnimationPromise = this._promiseWidgetAnimationOut(aNode);
814
if (widgetAnimationPromise) {
815
await widgetAnimationPromise;
816
}
817
818
CustomizableUI.removeWidgetFromArea(aNode.id);
819
if (!this._customizing) {
820
CustomizableUI.dispatchToolboxEvent("customizationchange");
821
}
822
823
// If the user explicitly removes this item, turn off autohide.
824
if (aNode.id == "downloads-button") {
825
Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
826
if (this._customizing) {
827
this._showDownloadsAutoHidePanel();
828
}
829
}
830
if (widgetAnimationPromise) {
831
if (aNode.parentNode && aNode.parentNode.id.startsWith("wrapper-")) {
832
aNode.parentNode.classList.remove("animate-out");
833
} else {
834
aNode.classList.remove("animate-out");
835
}
836
}
837
},
838
839
populatePalette() {
840
let fragment = this.document.createDocumentFragment();
841
let toolboxPalette = this.window.gNavToolbox.palette;
842
843
try {
844
let unusedWidgets = CustomizableUI.getUnusedWidgets(toolboxPalette);
845
for (let widget of unusedWidgets) {
846
let paletteItem = this.makePaletteItem(widget, "palette");
847
if (!paletteItem) {
848
continue;
849
}
850
fragment.appendChild(paletteItem);
851
}
852
853
let flexSpace = CustomizableUI.createSpecialWidget(
854
"spring",
855
this.document
856
);
857
fragment.appendChild(this.wrapToolbarItem(flexSpace, "palette"));
858
859
this.visiblePalette.appendChild(fragment);
860
this._stowedPalette = this.window.gNavToolbox.palette;
861
this.window.gNavToolbox.palette = this.visiblePalette;
862
} catch (ex) {
863
log.error(ex);
864
}
865
},
866
867
// XXXunf Maybe this should use -moz-element instead of wrapping the node?
868
// Would ensure no weird interactions/event handling from original node,
869
// and makes it possible to put this in a lazy-loaded iframe/real tab
870
// while still getting rid of the need for overlays.
871
makePaletteItem(aWidget, aPlace) {
872
let widgetNode = aWidget.forWindow(this.window).node;
873
if (!widgetNode) {
874
log.error(
875
"Widget with id " + aWidget.id + " does not return a valid node"
876
);
877
return null;
878
}
879
// Do not build a palette item for hidden widgets; there's not much to show.
880
if (widgetNode.hidden) {
881
return null;
882
}
883
884
let wrapper = this.createOrUpdateWrapper(widgetNode, aPlace);
885
wrapper.appendChild(widgetNode);
886
return wrapper;
887
},
888
889
depopulatePalette() {
890
return (async () => {
891
this.visiblePalette.hidden = true;
892
let paletteChild = this.visiblePalette.firstElementChild;
893
let nextChild;
894
while (paletteChild) {
895
nextChild = paletteChild.nextElementSibling;
896
let itemId = paletteChild.firstElementChild.id;
897
if (CustomizableUI.isSpecialWidget(itemId)) {
898
this.visiblePalette.removeChild(paletteChild);
899
} else {
900
// XXXunf Currently this doesn't destroy the (now unused) node in the
901
// API provider case. It would be good to do so, but we need to
902
// keep strong refs to it in CustomizableUI (can't iterate of
903
// WeakMaps), and there's the question of what behavior
904
// wrappers should have if consumers keep hold of them.
905
let unwrappedPaletteItem = await this.deferredUnwrapToolbarItem(
906
paletteChild
907
);
908
this._stowedPalette.appendChild(unwrappedPaletteItem);
909
}
910
911
paletteChild = nextChild;
912
}
913
this.visiblePalette.hidden = false;
914
this.window.gNavToolbox.palette = this._stowedPalette;
915
})().catch(log.error);
916
},
917
918
isCustomizableItem(aNode) {
919
return (
920
aNode.localName == "toolbarbutton" ||
921
aNode.localName == "toolbaritem" ||
922
aNode.localName == "toolbarseparator" ||
923
aNode.localName == "toolbarspring" ||
924
aNode.localName == "toolbarspacer"
925
);
926
},
927
928
isWrappedToolbarItem(aNode) {
929
return aNode.localName == "toolbarpaletteitem";
930
},
931
932
deferredWrapToolbarItem(aNode, aPlace) {
933
return new Promise(resolve => {
934
dispatchFunction(() => {
935
let wrapper = this.wrapToolbarItem(aNode, aPlace);
936
resolve(wrapper);
937
});
938
});
939
},
940
941
wrapToolbarItem(aNode, aPlace) {
942
if (!this.isCustomizableItem(aNode)) {
943
return aNode;
944
}
945
let wrapper = this.createOrUpdateWrapper(aNode, aPlace);
946
947
// It's possible that this toolbar node is "mid-flight" and doesn't have
948
// a parent, in which case we skip replacing it. This can happen if a
949
// toolbar item has been dragged into the palette. In that case, we tell
950
// CustomizableUI to remove the widget from its area before putting the
951
// widget in the palette - so the node will have no parent.
952
if (aNode.parentNode) {
953
aNode = aNode.parentNode.replaceChild(wrapper, aNode);
954
}
955
wrapper.appendChild(aNode);
956
return wrapper;
957
},
958
959
createOrUpdateWrapper(aNode, aPlace, aIsUpdate) {
960
let wrapper;
961
if (
962
aIsUpdate &&
963
aNode.parentNode &&
964
aNode.parentNode.localName == "toolbarpaletteitem"
965
) {
966
wrapper = aNode.parentNode;
967
aPlace = wrapper.getAttribute("place");
968
} else {
969
wrapper = this.document.createXULElement("toolbarpaletteitem");
970
// "place" is used to show the label when it's sitting in the palette.
971
wrapper.setAttribute("place", aPlace);
972
}
973
974
// Ensure the wrapped item doesn't look like it's in any special state, and
975
// can't be interactved with when in the customization palette.
976
// Note that some buttons opt out of this with the
977
// keepbroadcastattributeswhencustomizing attribute.
978
if (
979
aNode.hasAttribute("command") &&
980
aNode.getAttribute(kKeepBroadcastAttributes) != "true"
981
) {
982
wrapper.setAttribute("itemcommand", aNode.getAttribute("command"));
983
aNode.removeAttribute("command");
984
}
985
986
if (
987
aNode.hasAttribute("observes") &&
988
aNode.getAttribute(kKeepBroadcastAttributes) != "true"
989
) {
990
wrapper.setAttribute("itemobserves", aNode.getAttribute("observes"));
991
aNode.removeAttribute("observes");
992
}
993
994
if (aNode.getAttribute("checked") == "true") {
995
wrapper.setAttribute("itemchecked", "true");
996
aNode.removeAttribute("checked");
997
}
998
999
if (aNode.hasAttribute("id")) {
1000
wrapper.setAttribute("id", "wrapper-" + aNode.getAttribute("id"));
1001
}
1002
1003
if (aNode.hasAttribute("label")) {
1004
wrapper.setAttribute("title", aNode.getAttribute("label"));
1005
wrapper.setAttribute("tooltiptext", aNode.getAttribute("label"));
1006
} else if (aNode.hasAttribute("title")) {
1007
wrapper.setAttribute("title", aNode.getAttribute("title"));
1008
wrapper.setAttribute("tooltiptext", aNode.getAttribute("title"));
1009
}
1010
1011
if (aNode.hasAttribute("flex")) {
1012
wrapper.setAttribute("flex", aNode.getAttribute("flex"));
1013
}
1014
1015
let removable =
1016
aPlace == "palette" || CustomizableUI.isWidgetRemovable(aNode);
1017
wrapper.setAttribute("removable", removable);
1018
1019
// Allow touch events to initiate dragging in customize mode.
1020
// This is only supported on Windows for now.
1021
wrapper.setAttribute("touchdownstartsdrag", "true");
1022
1023
let contextMenuAttrName = "";
1024
if (aNode.getAttribute("context")) {
1025
contextMenuAttrName = "context";
1026
} else if (aNode.getAttribute("contextmenu")) {
1027
contextMenuAttrName = "contextmenu";
1028
}
1029
let currentContextMenu = aNode.getAttribute(contextMenuAttrName);
1030
let contextMenuForPlace =
1031
aPlace == "menu-panel" ? kPanelItemContextMenu : kPaletteItemContextMenu;
1032
if (aPlace != "toolbar") {
1033
wrapper.setAttribute("context", contextMenuForPlace);
1034
}
1035
// Only keep track of the menu if it is non-default.
1036
if (currentContextMenu && currentContextMenu != contextMenuForPlace) {
1037
aNode.setAttribute("wrapped-context", currentContextMenu);
1038
aNode.setAttribute("wrapped-contextAttrName", contextMenuAttrName);
1039
aNode.removeAttribute(contextMenuAttrName);
1040
} else if (currentContextMenu == contextMenuForPlace) {
1041
aNode.removeAttribute(contextMenuAttrName);
1042
}
1043
1044
// Only add listeners for newly created wrappers:
1045
if (!aIsUpdate) {
1046
wrapper.addEventListener("mousedown", this);
1047
wrapper.addEventListener("mouseup", this);
1048
}
1049
1050
if (CustomizableUI.isSpecialWidget(aNode.id)) {
1051
wrapper.setAttribute(
1052
"title",
1053
gWidgetsBundle.GetStringFromName(aNode.nodeName + ".label")
1054
);
1055
}
1056
1057
return wrapper;
1058
},
1059
1060
deferredUnwrapToolbarItem(aWrapper) {
1061
return new Promise(resolve => {
1062
dispatchFunction(() => {
1063
let item = null;
1064
try {
1065
item = this.unwrapToolbarItem(aWrapper);
1066
} catch (ex) {
1067
Cu.reportError(ex);
1068
}
1069
resolve(item);
1070
});
1071
});
1072
},
1073
1074
unwrapToolbarItem(aWrapper) {
1075
if (aWrapper.nodeName != "toolbarpaletteitem") {
1076
return aWrapper;
1077
}
1078
aWrapper.removeEventListener("mousedown", this);
1079
aWrapper.removeEventListener("mouseup", this);
1080
1081
let place = aWrapper.getAttribute("place");
1082
1083
let toolbarItem = aWrapper.firstElementChild;
1084
if (!toolbarItem) {
1085
log.error(
1086
"no toolbarItem child for " + aWrapper.tagName + "#" + aWrapper.id
1087
);
1088
aWrapper.remove();
1089
return null;
1090
}
1091
1092
if (aWrapper.hasAttribute("itemobserves")) {
1093
toolbarItem.setAttribute(
1094
"observes",
1095
aWrapper.getAttribute("itemobserves")
1096
);
1097
}
1098
1099
if (aWrapper.hasAttribute("itemchecked")) {
1100
toolbarItem.checked = true;
1101
}
1102
1103
if (aWrapper.hasAttribute("itemcommand")) {
1104
let commandID = aWrapper.getAttribute("itemcommand");
1105
toolbarItem.setAttribute("command", commandID);
1106
1107
// XXX Bug 309953 - toolbarbuttons aren't in sync with their commands after customizing
1108
let command = this.$(commandID);
1109
if (command && command.hasAttribute("disabled")) {
1110
toolbarItem.setAttribute("disabled", command.getAttribute("disabled"));
1111
}
1112
}
1113
1114
let wrappedContext = toolbarItem.getAttribute("wrapped-context");
1115
if (wrappedContext) {
1116
let contextAttrName = toolbarItem.getAttribute("wrapped-contextAttrName");
1117
toolbarItem.setAttribute(contextAttrName, wrappedContext);
1118
toolbarItem.removeAttribute("wrapped-contextAttrName");
1119
toolbarItem.removeAttribute("wrapped-context");
1120
} else if (place == "menu-panel") {
1121
toolbarItem.setAttribute("context", kPanelItemContextMenu);
1122
}
1123
1124
if (aWrapper.parentNode) {
1125
aWrapper.parentNode.replaceChild(toolbarItem, aWrapper);
1126
}
1127
return toolbarItem;
1128
},
1129
1130
async _wrapToolbarItem(aArea) {
1131
let target = CustomizableUI.getCustomizeTargetForArea(aArea, this.window);
1132
if (!target || this.areas.has(target)) {
1133
return null;
1134
}
1135
1136
this._addDragHandlers(target);
1137
for (let child of target.children) {
1138
if (this.isCustomizableItem(child) && !this.isWrappedToolbarItem(child)) {
1139
await this.deferredWrapToolbarItem(
1140
child,
1141
CustomizableUI.getPlaceForItem(child)
1142
).catch(log.error);
1143
}
1144
}
1145
this.areas.add(target);
1146
return target;
1147
},
1148
1149
_wrapToolbarItemSync(aArea) {
1150
let target = CustomizableUI.getCustomizeTargetForArea(aArea, this.window);
1151
if (!target || this.areas.has(target)) {
1152
return null;
1153
}
1154
1155
this._addDragHandlers(target);
1156
try {
1157
for (let child of target.children) {
1158
if (
1159
this.isCustomizableItem(child) &&
1160
!this.isWrappedToolbarItem(child)
1161
) {
1162
this.wrapToolbarItem(child, CustomizableUI.getPlaceForItem(child));
1163
}
1164
}
1165
} catch (ex) {
1166
log.error(ex, ex.stack);
1167
}
1168
1169
this.areas.add(target);
1170
return target;
1171
},
1172
1173
async _wrapToolbarItems() {
1174
for (let area of CustomizableUI.areas) {
1175
await this._wrapToolbarItem(area);
1176
}
1177
},
1178
1179
_addDragHandlers(aTarget) {
1180
// Allow dropping on the padding of the arrow panel.
1181
if (aTarget.id == CustomizableUI.AREA_FIXED_OVERFLOW_PANEL) {
1182
aTarget = this.$("customization-panelHolder");
1183
}
1184
aTarget.addEventListener("dragstart", this, true);
1185
aTarget.addEventListener("dragover", this, true);
1186
aTarget.addEventListener("dragexit", this, true);
1187
aTarget.addEventListener("drop", this, true);
1188
aTarget.addEventListener("dragend", this, true);
1189
},
1190
1191
_wrapItemsInArea(target) {
1192
for (let child of target.children) {
1193
if (this.isCustomizableItem(child)) {
1194
this.wrapToolbarItem(child, CustomizableUI.getPlaceForItem(child));
1195
}
1196
}
1197
},
1198
1199
_removeDragHandlers(aTarget) {
1200
// Remove handler from different target if it was added to
1201
// allow dropping on the padding of the arrow panel.
1202
if (aTarget.id == CustomizableUI.AREA_FIXED_OVERFLOW_PANEL) {
1203
aTarget = this.$("customization-panelHolder");
1204
}
1205
aTarget.removeEventListener("dragstart", this, true);
1206
aTarget.removeEventListener("dragover", this, true);
1207
aTarget.removeEventListener("dragexit", this, true);
1208
aTarget.removeEventListener("drop", this, true);
1209
aTarget.removeEventListener("dragend", this, true);
1210
},
1211
1212
_unwrapItemsInArea(target) {
1213
for (let toolbarItem of target.children) {
1214
if (this.isWrappedToolbarItem(toolbarItem)) {
1215
this.unwrapToolbarItem(toolbarItem);
1216
}
1217
}
1218
},
1219
1220
_unwrapToolbarItems() {
1221
return (async () => {
1222
for (let target of this.areas) {
1223
for (let toolbarItem of target.children) {
1224
if (this.isWrappedToolbarItem(toolbarItem)) {
1225
await this.deferredUnwrapToolbarItem(toolbarItem);
1226
}
1227
}
1228
this._removeDragHandlers(target);
1229
}
1230
this.areas.clear();
1231
})().catch(log.error);
1232
},
1233
1234
reset() {
1235
this.resetting = true;
1236
// Disable the reset button temporarily while resetting:
1237
let btn = this.$("customization-reset-button");
1238
btn.disabled = true;
1239
return (async () => {
1240
await this.depopulatePalette();
1241
await this._unwrapToolbarItems();
1242
1243
CustomizableUI.reset();
1244
1245
await this._wrapToolbarItems();
1246
this.populatePalette();
1247
1248
this._updateResetButton();
1249
this._updateUndoResetButton();
1250
this._updateEmptyPaletteNotice();
1251
this._moveDownloadsButtonToNavBar = false;
1252
this.resetting = false;
1253
if (!this._wantToBeInCustomizeMode) {
1254
this.exit();
1255
}
1256
})().catch(log.error);
1257
},
1258
1259
undoReset() {
1260
this.resetting = true;
1261
1262
return (async () => {
1263
await this.depopulatePalette();
1264
await this._unwrapToolbarItems();
1265
1266
CustomizableUI.undoReset();
1267
1268
await this._wrapToolbarItems();
1269
this.populatePalette();
1270
1271
this._updateResetButton();
1272
this._updateUndoResetButton();
1273
this._updateEmptyPaletteNotice();
1274
this._moveDownloadsButtonToNavBar = false;
1275
this.resetting = false;
1276
})().catch(log.error);
1277
},
1278
1279
_onToolbarVisibilityChange(aEvent) {
1280
let toolbar = aEvent.target;
1281
if (
1282
aEvent.detail.visible &&
1283
toolbar.getAttribute("customizable") == "true"
1284
) {
1285
toolbar.setAttribute("customizing", "true");
1286
} else {
1287
toolbar.removeAttribute("customizing");
1288
}
1289
this._onUIChange();
1290
},
1291
1292
onWidgetMoved(aWidgetId, aArea, aOldPosition, aNewPosition) {
1293
this._onUIChange();
1294
},
1295
1296
onWidgetAdded(aWidgetId, aArea, aPosition) {
1297
this._onUIChange();
1298
},
1299
1300
onWidgetRemoved(aWidgetId, aArea) {
1301
this._onUIChange();
1302
},
1303
1304
onWidgetBeforeDOMChange(aNodeToChange, aSecondaryNode, aContainer) {
1305
if (aContainer.ownerGlobal != this.window || this.resetting) {
1306
return;
1307
}
1308
// If we get called for widgets that aren't in the window yet, they might not have
1309
// a parentNode at all.
1310
if (aNodeToChange.parentNode) {
1311
this.unwrapToolbarItem(aNodeToChange.parentNode);
1312
}
1313
if (aSecondaryNode) {
1314
this.unwrapToolbarItem(aSecondaryNode.parentNode);
1315
}
1316
},
1317
1318
onWidgetAfterDOMChange(aNodeToChange, aSecondaryNode, aContainer) {
1319
if (aContainer.ownerGlobal != this.window || this.resetting) {
1320
return;
1321
}
1322
// If the node is still attached to the container, wrap it again:
1323
if (aNodeToChange.parentNode) {
1324
let place = CustomizableUI.getPlaceForItem(aNodeToChange);
1325
this.wrapToolbarItem(aNodeToChange, place);
1326
if (aSecondaryNode) {
1327
this.wrapToolbarItem(aSecondaryNode, place);
1328
}
1329
} else {
1330
// If not, it got removed.
1331
1332
// If an API-based widget is removed while customizing, append it to the palette.
1333
// The _applyDrop code itself will take care of positioning it correctly, if
1334
// applicable. We need the code to be here so removing widgets using CustomizableUI's
1335
// API also does the right thing (and adds it to the palette)
1336
let widgetId = aNodeToChange.id;
1337
let widget = CustomizableUI.getWidget(widgetId);
1338
if (widget.provider == CustomizableUI.PROVIDER_API) {
1339
let paletteItem = this.makePaletteItem(widget, "palette");
1340
this.visiblePalette.appendChild(paletteItem);
1341
}
1342
}
1343
},
1344
1345
onWidgetDestroyed(aWidgetId) {
1346
let wrapper = this.$("wrapper-" + aWidgetId);
1347
if (wrapper) {
1348
wrapper.remove();
1349
}
1350
},
1351
1352
onWidgetAfterCreation(aWidgetId, aArea) {
1353
// If the node was added to an area, we would have gotten an onWidgetAdded notification,
1354
// plus associated DOM change notifications, so only do stuff for the palette:
1355
if (!aArea) {
1356
let widgetNode = this.$(aWidgetId);
1357
if (widgetNode) {
1358
this.wrapToolbarItem(widgetNode, "palette");
1359
} else {
1360
let widget = CustomizableUI.getWidget(aWidgetId);
1361
this.visiblePalette.appendChild(
1362
this.makePaletteItem(widget, "palette")
1363
);
1364
}
1365
}
1366
},
1367
1368
onAreaNodeRegistered(aArea, aContainer) {
1369
if (aContainer.ownerDocument == this.document) {
1370
this._wrapItemsInArea(aContainer);
1371
this._addDragHandlers(aContainer);
1372
this.areas.add(aContainer);
1373
}
1374
},
1375
1376
onAreaNodeUnregistered(aArea, aContainer, aReason) {
1377
if (
1378
aContainer.ownerDocument == this.document &&
1379
aReason == CustomizableUI.REASON_AREA_UNREGISTERED
1380
) {
1381
this._unwrapItemsInArea(aContainer);
1382
this._removeDragHandlers(aContainer);
1383
this.areas.delete(aContainer);
1384
}
1385
},
1386
1387
openAddonsManagerThemes(aEvent) {
1388
aEvent.target.parentNode.parentNode.hidePopup();
1389
AMTelemetry.recordLinkEvent({ object: "customize", value: "manageThemes" });
1390
this.window.BrowserOpenAddonsMgr("addons://list/theme");
1391
},
1392
1393
getMoreThemes(aEvent) {
1394
aEvent.target.parentNode.parentNode.hidePopup();
1395
AMTelemetry.recordLinkEvent({ object: "customize", value: "getThemes" });
1396
let getMoreURL = Services.urlFormatter.formatURLPref(
1397
"lightweightThemes.getMoreURL"
1398
);
1399
this.window.openTrustedLinkIn(getMoreURL, "tab");
1400
},
1401
1402
updateUIDensity(mode) {
1403
this.window.gUIDensity.update(mode);
1404
this._updateOverflowPanelArrowOffset();
1405
},
1406
1407
setUIDensity(mode) {
1408
let win = this.window;
1409
let gUIDensity = win.gUIDensity;
1410
let currentDensity = gUIDensity.getCurrentDensity();
1411
let panel = win.document.getElementById("customization-uidensity-menu");
1412
1413
Services.prefs.setIntPref(gUIDensity.uiDensityPref, mode);
1414
1415
// If the user is choosing a different UI density mode while
1416
// the mode is overriden to Touch, remove the override.
1417
if (currentDensity.overridden) {
1418
Services.prefs.setBoolPref(gUIDensity.autoTouchModePref, false);
1419
}
1420
1421
this._onUIChange();
1422
panel.hidePopup();
1423
this._updateOverflowPanelArrowOffset();
1424
},
1425
1426
resetUIDensity() {
1427
this.window.gUIDensity.update();
1428
this._updateOverflowPanelArrowOffset();
1429
},
1430
1431
onUIDensityMenuShowing() {
1432
let win = this.window;
1433
let doc = win.document;
1434
let gUIDensity = win.gUIDensity;
1435
let currentDensity = gUIDensity.getCurrentDensity();
1436
1437
let normalItem = doc.getElementById(
1438
"customization-uidensity-menuitem-normal"
1439
);
1440
normalItem.mode = gUIDensity.MODE_NORMAL;
1441
1442
let compactItem = doc.getElementById(
1443
"customization-uidensity-menuitem-compact"
1444
);
1445
compactItem.mode = gUIDensity.MODE_COMPACT;
1446
1447
let items = [normalItem, compactItem];
1448
1449
let touchItem = doc.getElementById(
1450
"customization-uidensity-menuitem-touch"
1451
);
1452
// Touch mode can not be enabled in OSX right now.
1453
if (touchItem) {
1454
touchItem.mode = gUIDensity.MODE_TOUCH;
1455
items.push(touchItem);
1456
}
1457
1458
// Mark the active mode menuitem.
1459
for (let item of items) {
1460
if (item.mode == currentDensity.mode) {
1461
item.setAttribute("aria-checked", "true");
1462
item.setAttribute("active", "true");
1463
} else {
1464
item.removeAttribute("aria-checked");
1465
item.removeAttribute("active");
1466
}
1467
}
1468
1469
// Add menu items for automatically switching to Touch mode in Windows Tablet Mode,
1470
// which is only available in Windows 10.
1471
if (AppConstants.isPlatformAndVersionAtLeast("win", "10")) {
1472
let spacer = doc.getElementById("customization-uidensity-touch-spacer");
1473
let checkbox = doc.getElementById(
1474
"customization-uidensity-autotouchmode-checkbox"
1475
);
1476
spacer.removeAttribute("hidden");
1477
checkbox.removeAttribute("hidden");
1478
1479
// Show a hint that the UI density was overridden automatically.
1480
if (currentDensity.overridden) {
1481
let sb = Services.strings.createBundle(
1483
);
1484
touchItem.setAttribute(
1485
"acceltext",
1486
sb.GetStringFromName("uiDensity.menuitem-touch.acceltext")
1487
);
1488
} else {
1489
touchItem.removeAttribute("acceltext");
1490
}
1491
1492
let autoTouchMode = Services.prefs.getBoolPref(
1493
win.gUIDensity.autoTouchModePref
1494
);
1495
if (autoTouchMode) {
1496
checkbox.setAttribute("checked", "true");
1497
} else {
1498
checkbox.removeAttribute("checked");
1499
}
1500
}
1501
},
1502
1503
updateAutoTouchMode(checked) {
1504
Services.prefs.setBoolPref("browser.touchmode.auto", checked);
1505
// Re-render the menu items since the active mode might have
1506
// change because of this.
1507
this.onUIDensityMenuShowing();
1508
this._onUIChange();
1509
},
1510
1511
async onThemesMenuShowing(aEvent) {
1512
const DEFAULT_THEME_ID = "default-theme@mozilla.org";
1513
const LIGHT_THEME_ID = "firefox-compact-light@mozilla.org";
1514
const DARK_THEME_ID = "firefox-compact-dark@mozilla.org";
1515
const MAX_THEME_COUNT = 6;
1516
1517
this._clearThemesMenu(aEvent.target);
1518
1519
let onThemeSelected = panel => {
1520
// This causes us to call _onUIChange when the LWT actually changes,
1521
// so the restore defaults / undo reset button is updated correctly.
1522
this._nextThemeChangeUserTriggered = true;
1523
panel.hidePopup();
1524
};
1525
1526
let doc = this.window.document;
1527
1528
function buildToolbarButton(aTheme) {
1529
let tbb = doc.createXULElement("toolbarbutton");
1530
tbb.theme = aTheme;
1531
tbb.setAttribute("label", aTheme.name);
1532
tbb.setAttribute(
1533
"image",
1535
);
1536
if (aTheme.description) {
1537
tbb.setAttribute("tooltiptext", aTheme.description);
1538
}
1539
tbb.setAttribute("tabindex", "0");
1540
tbb.classList.add("customization-lwtheme-menu-theme");
1541
let isActive = aTheme.isActive;
1542
tbb.setAttribute("aria-checked", isActive);
1543
tbb.setAttribute("role", "menuitemradio");
1544
if (isActive) {
1545
tbb.setAttribute("active", "true");
1546
}
1547
1548
return tbb;
1549
}
1550
1551
let themes = await AddonManager.getAddonsByTypes(["theme"]);
1552
let currentTheme = themes.find(theme => theme.isActive);
1553
1554
// Move the current theme (if any) and the light/dark themes to the start:
1555
let importantThemes = new Set([
1556
DEFAULT_THEME_ID,
1557
LIGHT_THEME_ID,
1558
DARK_THEME_ID,
1559
]);
1560
if (currentTheme) {
1561
importantThemes.add(currentTheme.id);
1562
}
1563
let importantList = [];
1564
for (let importantTheme of importantThemes) {
1565
importantList.push(
1566
...themes.splice(
1567
themes.findIndex(theme => theme.id == importantTheme),
1568
1
1569
)
1570
);
1571
}
1572
1573
// Sort the remainder alphabetically:
1574
themes.sort((a, b) => a.name.localeCompare(b.name));
1575
themes = importantList.concat(themes);
1576
1577
if (themes.length > MAX_THEME_COUNT) {
1578
themes.length = MAX_THEME_COUNT;
1579
}
1580
1581
let footer = doc.getElementById("customization-lwtheme-menu-footer");
1582
let panel = footer.parentNode;
1583
for (let theme of themes) {
1584
let button = buildToolbarButton(theme);
1585
button.addEventListener("command", async () => {
1586
await button.theme.enable();
1587
onThemeSelected(panel);
1588
AMTelemetry.recordActionEvent({
1589
object: "customize",
1590
action: "enable",
1591
extra: { type: "theme", addonId: theme.id },
1592
});
1593
});
1594
panel.insertBefore(button, footer);
1595
}
1596
},
1597
1598
_clearThemesMenu(panel) {
1599
let footer = this.$("customization-lwtheme-menu-footer");
1600
let element = footer;
1601
while (
1602
element.previousElementSibling &&
1603
element.previousElementSibling.localName == "toolbarbutton"
1604
) {
1605
element.previousElementSibling.remove();
1606
}
1607
1608
// Workaround for bug 1059934
1609
panel.removeAttribute("height");
1610
},
1611
1612
_onUIChange() {
1613
this._changed = true;
1614
if (!this.resetting) {
1615
this._updateResetButton();
1616
this._updateUndoResetButton();
1617
this._updateEmptyPaletteNotice();
1618
}
1619
CustomizableUI.dispatchToolboxEvent("customizationchange");
1620
},
1621
1622
_updateEmptyPaletteNotice() {
1623
let paletteItems = this.visiblePalette.getElementsByTagName(
1624
"toolbarpaletteitem"
1625
);
1626
let whimsyButton = this.$("whimsy-button");
1627
1628
if (
1629
paletteItems.length == 1 &&
1630
paletteItems[0].id.includes("wrapper-customizableui-special-spring")
1631
) {
1632
whimsyButton.hidden = false;
1633
} else {
1634
this.togglePong(false);
1635
whimsyButton.hidden = true;
1636
}
1637
},
1638
1639
_updateResetButton() {
1640
let btn = this.$("customization-reset-button");
1641
btn.disabled = CustomizableUI.inDefaultState;
1642
},
1643
1644
_updateUndoResetButton() {
1645
let undoResetButton = this.$("customization-undo-reset-button");
1646
undoResetButton.hidden = !CustomizableUI.canUndoReset;
1647
},
1648
1649
_updateTouchBarButton() {
1650
if (AppConstants.platform != "macosx") {
1651
return;
1652
}
1653
let touchBarButton = this.$("customization-touchbar-button");
1654
let touchBarSpacer = this.$("customization-touchbar-spacer");
1655
1656
let isTouchBarInitialized = gTouchBarUpdater.isTouchBarInitialized();
1657
touchBarButton.hidden = !isTouchBarInitialized;
1658
touchBarSpacer.hidden = !isTouchBarInitialized;
1659
},
1660
1661
handleEvent(aEvent) {
1662
switch (aEvent.type) {
1663
case "toolbarvisibilitychange":
1664
this._onToolbarVisibilityChange(aEvent);
1665
break;
1666
case "dragstart":
1667
this._onDragStart(aEvent);
1668
break;
1669
case "dragover":
1670
this._onDragOver(aEvent);
1671
break;
1672
case "drop":
1673
this._onDragDrop(aEvent);
1674
break;
1675
case "dragexit":
1676
this._onDragExit(aEvent);
1677
break;
1678
case "dragend":
1679
this._onDragEnd(aEvent);
1680
break;
1681
case "mousedown":
1682
this._onMouseDown(aEvent);
1683
break;
1684
case "mouseup":
1685
this._onMouseUp(aEvent);
1686
break;
1687
case "keypress":
1688
if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) {
1689
this.exit();
1690
}
1691
break;
1692
case "unload":
1693
this.uninit();
1694
break;
1695
}
1696
},
1697
1698
/**
1699
* We handle dragover/drop on the outer palette separately
1700
* to avoid overlap with other drag/drop handlers.
1701
*/
1702
_setupPaletteDragging() {
1703
this._addDragHandlers(this.visiblePalette);
1704
1705
this.paletteDragHandler = aEvent => {
1706
let originalTarget = aEvent.originalTarget;
1707
if (
1708
this._isUnwantedDragDrop(aEvent) ||
1709
this.visiblePalette.contains(originalTarget) ||
1710
this.$("customization-panelHolder").contains(originalTarget)
1711
) {
1712
return;
1713
}
1714
// We have a dragover/drop on the palette.
1715
if (aEvent.type == "dragover") {
1716
this._onDragOver(aEvent, this.visiblePalette);
1717
} else {
1718
this._onDragDrop(aEvent, this.visiblePalette);
1719
}
1720
};
1721
let contentContainer = this.$("customization-content-container");
1722
contentContainer.addEventListener(
1723
"dragover",
1724
this.paletteDragHandler,
1725
true
1726
);
1727
contentContainer.addEventListener("drop", this.paletteDragHandler, true);
1728
},
1729
1730
_teardownPaletteDragging() {
1731
DragPositionManager.stop();
1732
this._removeDragHandlers(this.visiblePalette);
1733
1734
let contentContainer = this.$("customization-content-container");
1735
contentContainer.removeEventListener(
1736
"dragover",
1737
this.paletteDragHandler,
1738
true
1739
);
1740
contentContainer.removeEventListener("drop", this.paletteDragHandler, true);
1741
delete this.paletteDragHandler;
1742
},
1743
1744
observe(aSubject, aTopic, aData) {
1745
switch (aTopic) {
1746
case "nsPref:changed":
1747
this._updateResetButton();
1748
this._updateUndoResetButton();
1749
if (this._canDrawInTitlebar()) {
1750
this._updateTitlebarCheckbox();
1751
this._updateDragSpaceCheckbox();
1752
}
1753
break;
1754
}
1755
},
1756
1757
async onInstalled(addon) {
1758
await this.onEnabled(addon);
1759
},
1760
1761
async onEnabled(addon) {
1762
if (addon.type != "theme") {
1763
return;
1764
}
1765
1766
await this._updateThemeButtonIcon();
1767
if (this._nextThemeChangeUserTriggered) {
1768
this._onUIChange();
1769
}
1770
this._nextThemeChangeUserTriggered = false;
1771
},
1772
1773
_canDrawInTitlebar() {
1774
return this.window.TabsInTitlebar.systemSupported;
1775
},
1776
1777
_updateTitlebarCheckbox() {
1778
let drawInTitlebar = Services.prefs.getBoolPref(
1779
kDrawInTitlebarPref,
1780
this.window.matchMedia("(-moz-gtk-csd-hide-titlebar-by-default)").matches
1781
);
1782
let checkbox = this.$("customization-titlebar-visibility-checkbox");
1783
// Drawing in the titlebar means 'hiding' the titlebar.
1784
// We use the attribute rather than a property because if we're not in
1785
// customize mode the button is hidden and properties don't work.
1786
if (drawInTitlebar) {
1787
checkbox.removeAttribute("checked");
1788
} else {
1789
checkbox.setAttribute("checked", "true");
1790
}
1791
},
1792
1793
_updateDragSpaceCheckbox() {
1794
let extraDragSpace = Services.prefs.getBoolPref(kExtraDragSpacePref);
1795
let drawInTitlebar = Services.prefs.getBoolPref(
1796
kDrawInTitlebarPref,
1797
this.window.matchMedia("(-moz-gtk-csd-hide-titlebar-by-default)").matches
1798
);
1799
let menuBar = this.$("toolbar-menubar");
1800
let menuBarEnabled =
1801
menuBar &&
1802
AppConstants.platform != "macosx" &&
1803
menuBar.getAttribute("autohide") != "true";
1804
1805
let checkbox = this.$("customization-extra-drag-space-checkbox");
1806
if (extraDragSpace) {
1807
checkbox.setAttribute("checked", "true");
1808
} else {
1809
checkbox.removeAttribute("checked");
1810
}
1811
1812
if (!drawInTitlebar || menuBarEnabled) {
1813
checkbox.setAttribute("disabled", "true");
1814
} else {
1815
checkbox.removeAttribute("disabled");
1816
}
1817
},
1818
1819
toggleTitlebar(aShouldShowTitlebar) {
1820
// Drawing in the titlebar means not showing the titlebar, hence the negation:
1821
Services.prefs.setBoolPref(kDrawInTitlebarPref, !aShouldShowTitlebar);
1822
this._updateDragSpaceCheckbox();
1823
},
1824
1825
toggleDragSpace(aShouldShowDragSpace) {
1826
Services.prefs.setBoolPref(kExtraDragSpacePref, aShouldShowDragSpace);
1827
},
1828
1829
_getBoundsWithoutFlushing(element) {
1830
return this.window.windowUtils.getBoundsWithoutFlushing(element);
1831
},
1832
1833
_onDragStart(aEvent) {
1834
__dumpDragData(aEvent);
1835
let item = aEvent.target;
1836
while (item && item.localName != "toolbarpaletteitem") {
1837
if (
1838
item.localName == "toolbar" ||
1839
item.id == kPaletteId ||
1840
item.id == "customization-panelHolder"
1841
) {
1842
return;
1843
}
1844
item = item.parentNode;
1845
}
1846
1847
let draggedItem = item.firstElementChild;
1848
let placeForItem = CustomizableUI.getPlaceForItem(item);
1849
1850
let dt = aEvent.dataTransfer;
1851
let documentId = aEvent.target.ownerDocument.documentElement.id;
1852
1853
dt.mozSetDataAt(kDragDataTypePrefix + documentId, draggedItem.id, 0);
1854
dt.effectAllowed = "move";
1855
1856
let itemRect = this._getBoundsWithoutFlushing(draggedItem);
1857
let itemCenter = {
1858
x: itemRect.left + itemRect.width / 2,
1859
y: itemRect.top + itemRect.height / 2,
1860
};
1861
this._dragOffset = {
1862
x: aEvent.clientX - itemCenter.x,
1863
y: aEvent.clientY - itemCenter.y,
1864
};
1865
1866
let toolbarParent = draggedItem.closest("toolbar");
1867
if (toolbarParent) {
1868
let toolbarRect = this._getBoundsWithoutFlushing(toolbarParent);
1869
toolbarParent.style.minHeight = toolbarRect.height + "px";
1870
}
1871
1872
gDraggingInToolbars = new Set();
1873
1874
// Hack needed so that the dragimage will still show the
1875
// item as it appeared before it was hidden.
1876
this._initializeDragAfterMove = () => {
1877
// For automated tests, we sometimes start exiting customization mode
1878
// before this fires, which leaves us with placeholders inserted after
1879
// we've exited. So we need to check that we are indeed customizing.
1880
if (this._customizing && !this._transitioning) {
1881
item.hidden = true;
1882
DragPositionManager.start(this.window);
1883
let canUsePrevSibling =
1884
placeForItem == "toolbar" || placeForItem == "menu-panel";
1885
if (item.nextElementSibling) {
1886
this._setDragActive(
1887
item.nextElementSibling,
1888
"before",
1889
draggedItem.id,
1890
placeForItem
1891
);
1892
this._dragOverItem = item.nextElementSibling;
1893
} else if (canUsePrevSibling && item.previousElementSibling) {
1894
this._setDragActive(
1895
item.previousElementSibling,
1896
"after",
1897
draggedItem.id,
1898
placeForItem
1899
);
1900
this._dragOverItem = item.previousElementSibling;
1901
}
1902
let currentArea = this._getCustomizableParent(item);
1903
currentArea.setAttribute("draggingover", "true");
1904
}
1905
this._initializeDragAfterMove = null;
1906
this.window.clearTimeout(this._dragInitializeTimeout);
1907
};
1908
this._dragInitializeTimeout = this.window.setTimeout(
1909
this._initializeDragAfterMove,
1910
0
1911
);
1912
},
1913
1914
_onDragOver(aEvent, aOverrideTarget) {
1915
if (this._isUnwantedDragDrop(aEvent)) {
1916
return;
1917
}
1918
if (this._initializeDragAfterMove) {
1919
this._initializeDragAfterMove();
1920
}
1921
1922
__dumpDragData(aEvent);
1923
1924
let document = aEvent.target.ownerDocument;
1925
let documentId = document.documentElement.id;
1926
if (!aEvent.dataTransfer.mozTypesAt(0)) {
1927
return;
1928
}
1929
1930
let draggedItemId = aEvent.dataTransfer.mozGetDataAt(
1931
kDragDataTypePrefix + documentId,
1932
0
1933
);
1934
let draggedWrapper = document.getElementById("wrapper-" + draggedItemId);
1935
let targetArea = this._getCustomizableParent(
1936
aOverrideTarget || aEvent.currentTarget
1937
);
1938
let