|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 this.EXPORTED_SYMBOLS = ["CustomizeMode"]; |
|
8 |
|
9 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; |
|
10 |
|
11 const kPrefCustomizationDebug = "browser.uiCustomization.debug"; |
|
12 const kPrefCustomizationAnimation = "browser.uiCustomization.disableAnimation"; |
|
13 const kPaletteId = "customization-palette"; |
|
14 const kAboutURI = "about:customizing"; |
|
15 const kDragDataTypePrefix = "text/toolbarwrapper-id/"; |
|
16 const kPlaceholderClass = "panel-customization-placeholder"; |
|
17 const kSkipSourceNodePref = "browser.uiCustomization.skipSourceNodeCheck"; |
|
18 const kToolbarVisibilityBtn = "customization-toolbar-visibility-button"; |
|
19 const kDrawInTitlebarPref = "browser.tabs.drawInTitlebar"; |
|
20 const kMaxTransitionDurationMs = 2000; |
|
21 |
|
22 const kPanelItemContextMenu = "customizationPanelItemContextMenu"; |
|
23 const kPaletteItemContextMenu = "customizationPaletteItemContextMenu"; |
|
24 |
|
25 Cu.import("resource://gre/modules/Services.jsm"); |
|
26 Cu.import("resource:///modules/CustomizableUI.jsm"); |
|
27 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
28 Cu.import("resource://gre/modules/Task.jsm"); |
|
29 Cu.import("resource://gre/modules/Promise.jsm"); |
|
30 |
|
31 XPCOMUtils.defineLazyModuleGetter(this, "DragPositionManager", |
|
32 "resource:///modules/DragPositionManager.jsm"); |
|
33 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry", |
|
34 "resource:///modules/BrowserUITelemetry.jsm"); |
|
35 |
|
36 let gModuleName = "[CustomizeMode]"; |
|
37 #include logging.js |
|
38 |
|
39 let gDisableAnimation = null; |
|
40 |
|
41 let gDraggingInToolbars; |
|
42 |
|
43 function CustomizeMode(aWindow) { |
|
44 if (gDisableAnimation === null) { |
|
45 gDisableAnimation = Services.prefs.getPrefType(kPrefCustomizationAnimation) == Ci.nsIPrefBranch.PREF_BOOL && |
|
46 Services.prefs.getBoolPref(kPrefCustomizationAnimation); |
|
47 } |
|
48 this.window = aWindow; |
|
49 this.document = aWindow.document; |
|
50 this.browser = aWindow.gBrowser; |
|
51 |
|
52 // There are two palettes - there's the palette that can be overlayed with |
|
53 // toolbar items in browser.xul. This is invisible, and never seen by the |
|
54 // user. Then there's the visible palette, which gets populated and displayed |
|
55 // to the user when in customizing mode. |
|
56 this.visiblePalette = this.document.getElementById(kPaletteId); |
|
57 this.paletteEmptyNotice = this.document.getElementById("customization-empty"); |
|
58 this.paletteSpacer = this.document.getElementById("customization-spacer"); |
|
59 this.tipPanel = this.document.getElementById("customization-tipPanel"); |
|
60 #ifdef CAN_DRAW_IN_TITLEBAR |
|
61 this._updateTitlebarButton(); |
|
62 Services.prefs.addObserver(kDrawInTitlebarPref, this, false); |
|
63 this.window.addEventListener("unload", this); |
|
64 #endif |
|
65 }; |
|
66 |
|
67 CustomizeMode.prototype = { |
|
68 _changed: false, |
|
69 _transitioning: false, |
|
70 window: null, |
|
71 document: null, |
|
72 // areas is used to cache the customizable areas when in customization mode. |
|
73 areas: null, |
|
74 // When in customizing mode, we swap out the reference to the invisible |
|
75 // palette in gNavToolbox.palette for our visiblePalette. This way, for the |
|
76 // customizing browser window, when widgets are removed from customizable |
|
77 // areas and added to the palette, they're added to the visible palette. |
|
78 // _stowedPalette is a reference to the old invisible palette so we can |
|
79 // restore gNavToolbox.palette to its original state after exiting |
|
80 // customization mode. |
|
81 _stowedPalette: null, |
|
82 _dragOverItem: null, |
|
83 _customizing: false, |
|
84 _skipSourceNodeCheck: null, |
|
85 _mainViewContext: null, |
|
86 |
|
87 get panelUIContents() { |
|
88 return this.document.getElementById("PanelUI-contents"); |
|
89 }, |
|
90 |
|
91 get _handler() { |
|
92 return this.window.CustomizationHandler; |
|
93 }, |
|
94 |
|
95 uninit: function() { |
|
96 #ifdef CAN_DRAW_IN_TITLEBAR |
|
97 Services.prefs.removeObserver(kDrawInTitlebarPref, this); |
|
98 #endif |
|
99 }, |
|
100 |
|
101 toggle: function() { |
|
102 if (this._handler.isEnteringCustomizeMode || this._handler.isExitingCustomizeMode) { |
|
103 this._wantToBeInCustomizeMode = !this._wantToBeInCustomizeMode; |
|
104 return; |
|
105 } |
|
106 if (this._customizing) { |
|
107 this.exit(); |
|
108 } else { |
|
109 this.enter(); |
|
110 } |
|
111 }, |
|
112 |
|
113 enter: function() { |
|
114 this._wantToBeInCustomizeMode = true; |
|
115 |
|
116 if (this._customizing || this._handler.isEnteringCustomizeMode) { |
|
117 return; |
|
118 } |
|
119 |
|
120 // Exiting; want to re-enter once we've done that. |
|
121 if (this._handler.isExitingCustomizeMode) { |
|
122 LOG("Attempted to enter while we're in the middle of exiting. " + |
|
123 "We'll exit after we've entered"); |
|
124 return; |
|
125 } |
|
126 |
|
127 |
|
128 // We don't need to switch to kAboutURI, or open a new tab at |
|
129 // kAboutURI if we're already on it. |
|
130 if (this.browser.selectedBrowser.currentURI.spec != kAboutURI) { |
|
131 this.window.switchToTabHavingURI(kAboutURI, true, { |
|
132 skipTabAnimation: true, |
|
133 }); |
|
134 return; |
|
135 } |
|
136 |
|
137 let window = this.window; |
|
138 let document = this.document; |
|
139 |
|
140 this._handler.isEnteringCustomizeMode = true; |
|
141 |
|
142 // Always disable the reset button at the start of customize mode, it'll be re-enabled |
|
143 // if necessary when we finish entering: |
|
144 let resetButton = this.document.getElementById("customization-reset-button"); |
|
145 resetButton.setAttribute("disabled", "true"); |
|
146 |
|
147 Task.spawn(function() { |
|
148 // We shouldn't start customize mode until after browser-delayed-startup has finished: |
|
149 if (!this.window.gBrowserInit.delayedStartupFinished) { |
|
150 let delayedStartupDeferred = Promise.defer(); |
|
151 let delayedStartupObserver = function(aSubject) { |
|
152 if (aSubject == this.window) { |
|
153 Services.obs.removeObserver(delayedStartupObserver, "browser-delayed-startup-finished"); |
|
154 delayedStartupDeferred.resolve(); |
|
155 } |
|
156 }.bind(this); |
|
157 Services.obs.addObserver(delayedStartupObserver, "browser-delayed-startup-finished", false); |
|
158 yield delayedStartupDeferred.promise; |
|
159 } |
|
160 |
|
161 let toolbarVisibilityBtn = document.getElementById(kToolbarVisibilityBtn); |
|
162 let togglableToolbars = window.getTogglableToolbars(); |
|
163 let bookmarksToolbar = document.getElementById("PersonalToolbar"); |
|
164 if (togglableToolbars.length == 0) { |
|
165 toolbarVisibilityBtn.setAttribute("hidden", "true"); |
|
166 } else { |
|
167 toolbarVisibilityBtn.removeAttribute("hidden"); |
|
168 } |
|
169 |
|
170 // Disable lightweight themes while in customization mode since |
|
171 // they don't have large enough images to pad the full browser window. |
|
172 if (this.document.documentElement._lightweightTheme) |
|
173 this.document.documentElement._lightweightTheme.disable(); |
|
174 |
|
175 CustomizableUI.dispatchToolboxEvent("beforecustomization", {}, window); |
|
176 CustomizableUI.notifyStartCustomizing(this.window); |
|
177 |
|
178 // Add a keypress listener to the document so that we can quickly exit |
|
179 // customization mode when pressing ESC. |
|
180 document.addEventListener("keypress", this); |
|
181 |
|
182 // Same goes for the menu button - if we're customizing, a click on the |
|
183 // menu button means a quick exit from customization mode. |
|
184 window.PanelUI.hide(); |
|
185 window.PanelUI.menuButton.addEventListener("command", this); |
|
186 window.PanelUI.menuButton.open = true; |
|
187 window.PanelUI.beginBatchUpdate(); |
|
188 |
|
189 // The menu panel is lazy, and registers itself when the popup shows. We |
|
190 // need to force the menu panel to register itself, or else customization |
|
191 // is really not going to work. We pass "true" to ensureReady to |
|
192 // indicate that we're handling calling startBatchUpdate and |
|
193 // endBatchUpdate. |
|
194 if (!window.PanelUI.isReady()) { |
|
195 yield window.PanelUI.ensureReady(true); |
|
196 } |
|
197 |
|
198 // Hide the palette before starting the transition for increased perf. |
|
199 this.visiblePalette.hidden = true; |
|
200 this.visiblePalette.removeAttribute("showing"); |
|
201 |
|
202 // Disable the button-text fade-out mask |
|
203 // during the transition for increased perf. |
|
204 let panelContents = window.PanelUI.contents; |
|
205 panelContents.setAttribute("customize-transitioning", "true"); |
|
206 |
|
207 // Move the mainView in the panel to the holder so that we can see it |
|
208 // while customizing. |
|
209 let mainView = window.PanelUI.mainView; |
|
210 let panelHolder = document.getElementById("customization-panelHolder"); |
|
211 panelHolder.appendChild(mainView); |
|
212 |
|
213 let customizeButton = document.getElementById("PanelUI-customize"); |
|
214 customizeButton.setAttribute("enterLabel", customizeButton.getAttribute("label")); |
|
215 customizeButton.setAttribute("label", customizeButton.getAttribute("exitLabel")); |
|
216 customizeButton.setAttribute("enterTooltiptext", customizeButton.getAttribute("tooltiptext")); |
|
217 customizeButton.setAttribute("tooltiptext", customizeButton.getAttribute("exitTooltiptext")); |
|
218 |
|
219 this._transitioning = true; |
|
220 |
|
221 let customizer = document.getElementById("customization-container"); |
|
222 customizer.parentNode.selectedPanel = customizer; |
|
223 customizer.hidden = false; |
|
224 |
|
225 yield this._doTransition(true); |
|
226 |
|
227 // Let everybody in this window know that we're about to customize. |
|
228 CustomizableUI.dispatchToolboxEvent("customizationstarting", {}, window); |
|
229 |
|
230 this._mainViewContext = mainView.getAttribute("context"); |
|
231 if (this._mainViewContext) { |
|
232 mainView.removeAttribute("context"); |
|
233 } |
|
234 |
|
235 this._showPanelCustomizationPlaceholders(); |
|
236 |
|
237 yield this._wrapToolbarItems(); |
|
238 this.populatePalette(); |
|
239 |
|
240 this._addDragHandlers(this.visiblePalette); |
|
241 |
|
242 window.gNavToolbox.addEventListener("toolbarvisibilitychange", this); |
|
243 |
|
244 document.getElementById("PanelUI-help").setAttribute("disabled", true); |
|
245 document.getElementById("PanelUI-quit").setAttribute("disabled", true); |
|
246 |
|
247 this._updateResetButton(); |
|
248 this._updateUndoResetButton(); |
|
249 |
|
250 this._skipSourceNodeCheck = Services.prefs.getPrefType(kSkipSourceNodePref) == Ci.nsIPrefBranch.PREF_BOOL && |
|
251 Services.prefs.getBoolPref(kSkipSourceNodePref); |
|
252 |
|
253 let customizableToolbars = document.querySelectorAll("toolbar[customizable=true]:not([autohide=true]):not([collapsed=true])"); |
|
254 for (let toolbar of customizableToolbars) |
|
255 toolbar.setAttribute("customizing", true); |
|
256 |
|
257 CustomizableUI.addListener(this); |
|
258 window.PanelUI.endBatchUpdate(); |
|
259 this._customizing = true; |
|
260 this._transitioning = false; |
|
261 |
|
262 // Show the palette now that the transition has finished. |
|
263 this.visiblePalette.hidden = false; |
|
264 window.setTimeout(() => { |
|
265 // Force layout reflow to ensure the animation runs, |
|
266 // and make it async so it doesn't affect the timing. |
|
267 this.visiblePalette.clientTop; |
|
268 this.visiblePalette.setAttribute("showing", "true"); |
|
269 }, 0); |
|
270 this.paletteSpacer.hidden = true; |
|
271 this._updateEmptyPaletteNotice(); |
|
272 |
|
273 this.maybeShowTip(panelHolder); |
|
274 |
|
275 this._handler.isEnteringCustomizeMode = false; |
|
276 panelContents.removeAttribute("customize-transitioning"); |
|
277 |
|
278 CustomizableUI.dispatchToolboxEvent("customizationready", {}, window); |
|
279 this._enableOutlinesTimeout = window.setTimeout(() => { |
|
280 this.document.getElementById("nav-bar").setAttribute("showoutline", "true"); |
|
281 this.panelUIContents.setAttribute("showoutline", "true"); |
|
282 delete this._enableOutlinesTimeout; |
|
283 }, 0); |
|
284 |
|
285 // It's possible that we didn't enter customize mode via the menu panel, |
|
286 // meaning we didn't kick off about:customizing preloading. If that's |
|
287 // the case, let's kick it off for the next time we load this mode. |
|
288 window.gCustomizationTabPreloader.ensurePreloading(); |
|
289 if (!this._wantToBeInCustomizeMode) { |
|
290 this.exit(); |
|
291 } |
|
292 }.bind(this)).then(null, function(e) { |
|
293 ERROR(e); |
|
294 // We should ensure this has been called, and calling it again doesn't hurt: |
|
295 window.PanelUI.endBatchUpdate(); |
|
296 this._handler.isEnteringCustomizeMode = false; |
|
297 }.bind(this)); |
|
298 }, |
|
299 |
|
300 exit: function() { |
|
301 this._wantToBeInCustomizeMode = false; |
|
302 |
|
303 if (!this._customizing || this._handler.isExitingCustomizeMode) { |
|
304 return; |
|
305 } |
|
306 |
|
307 // Entering; want to exit once we've done that. |
|
308 if (this._handler.isEnteringCustomizeMode) { |
|
309 LOG("Attempted to exit while we're in the middle of entering. " + |
|
310 "We'll exit after we've entered"); |
|
311 return; |
|
312 } |
|
313 |
|
314 if (this.resetting) { |
|
315 LOG("Attempted to exit while we're resetting. " + |
|
316 "We'll exit after resetting has finished."); |
|
317 return; |
|
318 } |
|
319 |
|
320 this.hideTip(); |
|
321 |
|
322 this._handler.isExitingCustomizeMode = true; |
|
323 |
|
324 if (this._enableOutlinesTimeout) { |
|
325 this.window.clearTimeout(this._enableOutlinesTimeout); |
|
326 } else { |
|
327 this.document.getElementById("nav-bar").removeAttribute("showoutline"); |
|
328 this.panelUIContents.removeAttribute("showoutline"); |
|
329 } |
|
330 |
|
331 this._removeExtraToolbarsIfEmpty(); |
|
332 |
|
333 CustomizableUI.removeListener(this); |
|
334 |
|
335 this.document.removeEventListener("keypress", this); |
|
336 this.window.PanelUI.menuButton.removeEventListener("command", this); |
|
337 this.window.PanelUI.menuButton.open = false; |
|
338 |
|
339 this.window.PanelUI.beginBatchUpdate(); |
|
340 |
|
341 this._removePanelCustomizationPlaceholders(); |
|
342 |
|
343 let window = this.window; |
|
344 let document = this.document; |
|
345 let documentElement = document.documentElement; |
|
346 |
|
347 // Hide the palette before starting the transition for increased perf. |
|
348 this.paletteSpacer.hidden = false; |
|
349 this.visiblePalette.hidden = true; |
|
350 this.visiblePalette.removeAttribute("showing"); |
|
351 this.paletteEmptyNotice.hidden = true; |
|
352 |
|
353 // Disable the button-text fade-out mask |
|
354 // during the transition for increased perf. |
|
355 let panelContents = window.PanelUI.contents; |
|
356 panelContents.setAttribute("customize-transitioning", "true"); |
|
357 |
|
358 // Disable the reset and undo reset buttons while transitioning: |
|
359 let resetButton = this.document.getElementById("customization-reset-button"); |
|
360 let undoResetButton = this.document.getElementById("customization-undo-reset-button"); |
|
361 undoResetButton.hidden = resetButton.disabled = true; |
|
362 |
|
363 this._transitioning = true; |
|
364 |
|
365 Task.spawn(function() { |
|
366 yield this.depopulatePalette(); |
|
367 |
|
368 yield this._doTransition(false); |
|
369 |
|
370 let browser = document.getElementById("browser"); |
|
371 if (this.browser.selectedBrowser.currentURI.spec == kAboutURI) { |
|
372 let custBrowser = this.browser.selectedBrowser; |
|
373 if (custBrowser.canGoBack) { |
|
374 // If there's history to this tab, just go back. |
|
375 // Note that this throws an exception if the previous document has a |
|
376 // problematic URL (e.g. about:idontexist) |
|
377 try { |
|
378 custBrowser.goBack(); |
|
379 } catch (ex) { |
|
380 ERROR(ex); |
|
381 } |
|
382 } else { |
|
383 // If we can't go back, we're removing the about:customization tab. |
|
384 // We only do this if we're the top window for this window (so not |
|
385 // a dialog window, for example). |
|
386 if (window.getTopWin(true) == window) { |
|
387 let customizationTab = this.browser.selectedTab; |
|
388 if (this.browser.browsers.length == 1) { |
|
389 window.BrowserOpenTab(); |
|
390 } |
|
391 this.browser.removeTab(customizationTab); |
|
392 } |
|
393 } |
|
394 } |
|
395 browser.parentNode.selectedPanel = browser; |
|
396 let customizer = document.getElementById("customization-container"); |
|
397 customizer.hidden = true; |
|
398 |
|
399 window.gNavToolbox.removeEventListener("toolbarvisibilitychange", this); |
|
400 |
|
401 DragPositionManager.stop(); |
|
402 this._removeDragHandlers(this.visiblePalette); |
|
403 |
|
404 yield this._unwrapToolbarItems(); |
|
405 |
|
406 if (this._changed) { |
|
407 // XXXmconley: At first, it seems strange to also persist the old way with |
|
408 // currentset - but this might actually be useful for switching |
|
409 // to old builds. We might want to keep this around for a little |
|
410 // bit. |
|
411 this.persistCurrentSets(); |
|
412 } |
|
413 |
|
414 // And drop all area references. |
|
415 this.areas = []; |
|
416 |
|
417 // Let everybody in this window know that we're starting to |
|
418 // exit customization mode. |
|
419 CustomizableUI.dispatchToolboxEvent("customizationending", {}, window); |
|
420 |
|
421 window.PanelUI.setMainView(window.PanelUI.mainView); |
|
422 window.PanelUI.menuButton.disabled = false; |
|
423 |
|
424 let customizeButton = document.getElementById("PanelUI-customize"); |
|
425 customizeButton.setAttribute("exitLabel", customizeButton.getAttribute("label")); |
|
426 customizeButton.setAttribute("label", customizeButton.getAttribute("enterLabel")); |
|
427 customizeButton.setAttribute("exitTooltiptext", customizeButton.getAttribute("tooltiptext")); |
|
428 customizeButton.setAttribute("tooltiptext", customizeButton.getAttribute("enterTooltiptext")); |
|
429 |
|
430 // We have to use setAttribute/removeAttribute here instead of the |
|
431 // property because the XBL property will be set later, and right |
|
432 // now we'd be setting an expando, which breaks the XBL property. |
|
433 document.getElementById("PanelUI-help").removeAttribute("disabled"); |
|
434 document.getElementById("PanelUI-quit").removeAttribute("disabled"); |
|
435 |
|
436 panelContents.removeAttribute("customize-transitioning"); |
|
437 |
|
438 // We need to set this._customizing to false before removing the tab |
|
439 // or the TabSelect event handler will think that we are exiting |
|
440 // customization mode for a second time. |
|
441 this._customizing = false; |
|
442 |
|
443 let mainView = window.PanelUI.mainView; |
|
444 if (this._mainViewContext) { |
|
445 mainView.setAttribute("context", this._mainViewContext); |
|
446 } |
|
447 |
|
448 if (this.document.documentElement._lightweightTheme) |
|
449 this.document.documentElement._lightweightTheme.enable(); |
|
450 |
|
451 let customizableToolbars = document.querySelectorAll("toolbar[customizable=true]:not([autohide=true])"); |
|
452 for (let toolbar of customizableToolbars) |
|
453 toolbar.removeAttribute("customizing"); |
|
454 |
|
455 this.window.PanelUI.endBatchUpdate(); |
|
456 this._changed = false; |
|
457 this._transitioning = false; |
|
458 this._handler.isExitingCustomizeMode = false; |
|
459 CustomizableUI.dispatchToolboxEvent("aftercustomization", {}, window); |
|
460 CustomizableUI.notifyEndCustomizing(window); |
|
461 |
|
462 if (this._wantToBeInCustomizeMode) { |
|
463 this.enter(); |
|
464 } |
|
465 }.bind(this)).then(null, function(e) { |
|
466 ERROR(e); |
|
467 // We should ensure this has been called, and calling it again doesn't hurt: |
|
468 window.PanelUI.endBatchUpdate(); |
|
469 this._handler.isExitingCustomizeMode = false; |
|
470 }.bind(this)); |
|
471 }, |
|
472 |
|
473 /** |
|
474 * The customize mode transition has 3 phases when entering: |
|
475 * 1) Pre-customization mode |
|
476 * This is the starting phase of the browser. |
|
477 * 2) customize-entering |
|
478 * This phase is a transition, optimized for smoothness. |
|
479 * 3) customize-entered |
|
480 * After the transition completes, this phase draws all of the |
|
481 * expensive detail that isn't necessary during the second phase. |
|
482 * |
|
483 * Exiting customization mode has a similar set of phases, but in reverse |
|
484 * order - customize-entered, customize-exiting, pre-customization mode. |
|
485 * |
|
486 * When in the customize-entering, customize-entered, or customize-exiting |
|
487 * phases, there is a "customizing" attribute set on the main-window to simplify |
|
488 * excluding certain styles while in any phase of customize mode. |
|
489 */ |
|
490 _doTransition: function(aEntering) { |
|
491 let deferred = Promise.defer(); |
|
492 let deck = this.document.getElementById("content-deck"); |
|
493 |
|
494 let customizeTransitionEnd = function(aEvent) { |
|
495 if (aEvent != "timedout" && |
|
496 (aEvent.originalTarget != deck || aEvent.propertyName != "margin-left")) { |
|
497 return; |
|
498 } |
|
499 this.window.clearTimeout(catchAllTimeout); |
|
500 // Bug 962677: We let the event loop breathe for before we do the final |
|
501 // stage of the transition to improve perceived performance. |
|
502 this.window.setTimeout(function () { |
|
503 deck.removeEventListener("transitionend", customizeTransitionEnd); |
|
504 |
|
505 if (!aEntering) { |
|
506 this.document.documentElement.removeAttribute("customize-exiting"); |
|
507 this.document.documentElement.removeAttribute("customizing"); |
|
508 } else { |
|
509 this.document.documentElement.setAttribute("customize-entered", true); |
|
510 this.document.documentElement.removeAttribute("customize-entering"); |
|
511 } |
|
512 CustomizableUI.dispatchToolboxEvent("customization-transitionend", aEntering, this.window); |
|
513 |
|
514 deferred.resolve(); |
|
515 }.bind(this), 0); |
|
516 }.bind(this); |
|
517 deck.addEventListener("transitionend", customizeTransitionEnd); |
|
518 |
|
519 if (gDisableAnimation) { |
|
520 this.document.getElementById("tab-view-deck").setAttribute("fastcustomizeanimation", true); |
|
521 } |
|
522 if (aEntering) { |
|
523 this.document.documentElement.setAttribute("customizing", true); |
|
524 this.document.documentElement.setAttribute("customize-entering", true); |
|
525 } else { |
|
526 this.document.documentElement.setAttribute("customize-exiting", true); |
|
527 this.document.documentElement.removeAttribute("customize-entered"); |
|
528 } |
|
529 |
|
530 let catchAll = () => customizeTransitionEnd("timedout"); |
|
531 let catchAllTimeout = this.window.setTimeout(catchAll, kMaxTransitionDurationMs); |
|
532 return deferred.promise; |
|
533 }, |
|
534 |
|
535 maybeShowTip: function(aAnchor) { |
|
536 let shown = false; |
|
537 const kShownPref = "browser.customizemode.tip0.shown"; |
|
538 try { |
|
539 shown = Services.prefs.getBoolPref(kShownPref); |
|
540 } catch (ex) {} |
|
541 if (shown) |
|
542 return; |
|
543 |
|
544 let anchorNode = aAnchor || this.document.getElementById("customization-panelHolder"); |
|
545 let messageNode = this.tipPanel.querySelector(".customization-tipPanel-contentMessage"); |
|
546 if (!messageNode.childElementCount) { |
|
547 // Put the tip contents in the popup. |
|
548 let bundle = this.document.getElementById("bundle_browser"); |
|
549 const kLabelClass = "customization-tipPanel-link"; |
|
550 messageNode.innerHTML = bundle.getFormattedString("customizeTips.tip0", [ |
|
551 "<label class=\"customization-tipPanel-em\" value=\"" + |
|
552 bundle.getString("customizeTips.tip0.hint") + "\"/>", |
|
553 this.document.getElementById("bundle_brand").getString("brandShortName"), |
|
554 "<label class=\"" + kLabelClass + " text-link\" value=\"" + |
|
555 bundle.getString("customizeTips.tip0.learnMore") + "\"/>" |
|
556 ]); |
|
557 |
|
558 messageNode.querySelector("." + kLabelClass).addEventListener("click", () => { |
|
559 let url = Services.urlFormatter.formatURLPref("browser.customizemode.tip0.learnMoreUrl"); |
|
560 let browser = this.browser; |
|
561 browser.selectedTab = browser.addTab(url); |
|
562 this.hideTip(); |
|
563 }); |
|
564 } |
|
565 |
|
566 this.tipPanel.hidden = false; |
|
567 this.tipPanel.openPopup(anchorNode); |
|
568 Services.prefs.setBoolPref(kShownPref, true); |
|
569 }, |
|
570 |
|
571 hideTip: function() { |
|
572 this.tipPanel.hidePopup(); |
|
573 }, |
|
574 |
|
575 _getCustomizableChildForNode: function(aNode) { |
|
576 // NB: adjusted from _getCustomizableParent to keep that method fast |
|
577 // (it's used during drags), and avoid multiple DOM loops |
|
578 let areas = CustomizableUI.areas; |
|
579 // Caching this length is important because otherwise we'll also iterate |
|
580 // over items we add to the end from within the loop. |
|
581 let numberOfAreas = areas.length; |
|
582 for (let i = 0; i < numberOfAreas; i++) { |
|
583 let area = areas[i]; |
|
584 let areaNode = aNode.ownerDocument.getElementById(area); |
|
585 let customizationTarget = areaNode && areaNode.customizationTarget; |
|
586 if (customizationTarget && customizationTarget != areaNode) { |
|
587 areas.push(customizationTarget.id); |
|
588 } |
|
589 let overflowTarget = areaNode.getAttribute("overflowtarget"); |
|
590 if (overflowTarget) { |
|
591 areas.push(overflowTarget); |
|
592 } |
|
593 } |
|
594 areas.push(kPaletteId); |
|
595 |
|
596 while (aNode && aNode.parentNode) { |
|
597 let parent = aNode.parentNode; |
|
598 if (areas.indexOf(parent.id) != -1) { |
|
599 return aNode; |
|
600 } |
|
601 aNode = parent; |
|
602 } |
|
603 return null; |
|
604 }, |
|
605 |
|
606 addToToolbar: function(aNode) { |
|
607 aNode = this._getCustomizableChildForNode(aNode); |
|
608 if (aNode.localName == "toolbarpaletteitem" && aNode.firstChild) { |
|
609 aNode = aNode.firstChild; |
|
610 } |
|
611 CustomizableUI.addWidgetToArea(aNode.id, CustomizableUI.AREA_NAVBAR); |
|
612 if (!this._customizing) { |
|
613 CustomizableUI.dispatchToolboxEvent("customizationchange"); |
|
614 } |
|
615 }, |
|
616 |
|
617 addToPanel: function(aNode) { |
|
618 aNode = this._getCustomizableChildForNode(aNode); |
|
619 if (aNode.localName == "toolbarpaletteitem" && aNode.firstChild) { |
|
620 aNode = aNode.firstChild; |
|
621 } |
|
622 CustomizableUI.addWidgetToArea(aNode.id, CustomizableUI.AREA_PANEL); |
|
623 if (!this._customizing) { |
|
624 CustomizableUI.dispatchToolboxEvent("customizationchange"); |
|
625 } |
|
626 }, |
|
627 |
|
628 removeFromArea: function(aNode) { |
|
629 aNode = this._getCustomizableChildForNode(aNode); |
|
630 if (aNode.localName == "toolbarpaletteitem" && aNode.firstChild) { |
|
631 aNode = aNode.firstChild; |
|
632 } |
|
633 CustomizableUI.removeWidgetFromArea(aNode.id); |
|
634 if (!this._customizing) { |
|
635 CustomizableUI.dispatchToolboxEvent("customizationchange"); |
|
636 } |
|
637 }, |
|
638 |
|
639 populatePalette: function() { |
|
640 let fragment = this.document.createDocumentFragment(); |
|
641 let toolboxPalette = this.window.gNavToolbox.palette; |
|
642 |
|
643 try { |
|
644 let unusedWidgets = CustomizableUI.getUnusedWidgets(toolboxPalette); |
|
645 for (let widget of unusedWidgets) { |
|
646 let paletteItem = this.makePaletteItem(widget, "palette"); |
|
647 fragment.appendChild(paletteItem); |
|
648 } |
|
649 |
|
650 this.visiblePalette.appendChild(fragment); |
|
651 this._stowedPalette = this.window.gNavToolbox.palette; |
|
652 this.window.gNavToolbox.palette = this.visiblePalette; |
|
653 } catch (ex) { |
|
654 ERROR(ex); |
|
655 } |
|
656 }, |
|
657 |
|
658 //XXXunf Maybe this should use -moz-element instead of wrapping the node? |
|
659 // Would ensure no weird interactions/event handling from original node, |
|
660 // and makes it possible to put this in a lazy-loaded iframe/real tab |
|
661 // while still getting rid of the need for overlays. |
|
662 makePaletteItem: function(aWidget, aPlace) { |
|
663 let widgetNode = aWidget.forWindow(this.window).node; |
|
664 let wrapper = this.createOrUpdateWrapper(widgetNode, aPlace); |
|
665 wrapper.appendChild(widgetNode); |
|
666 return wrapper; |
|
667 }, |
|
668 |
|
669 depopulatePalette: function() { |
|
670 return Task.spawn(function() { |
|
671 this.visiblePalette.hidden = true; |
|
672 let paletteChild = this.visiblePalette.firstChild; |
|
673 let nextChild; |
|
674 while (paletteChild) { |
|
675 nextChild = paletteChild.nextElementSibling; |
|
676 let provider = CustomizableUI.getWidget(paletteChild.id).provider; |
|
677 if (provider == CustomizableUI.PROVIDER_XUL) { |
|
678 let unwrappedPaletteItem = |
|
679 yield this.deferredUnwrapToolbarItem(paletteChild); |
|
680 this._stowedPalette.appendChild(unwrappedPaletteItem); |
|
681 } else if (provider == CustomizableUI.PROVIDER_API) { |
|
682 //XXXunf Currently this doesn't destroy the (now unused) node. It would |
|
683 // be good to do so, but we need to keep strong refs to it in |
|
684 // CustomizableUI (can't iterate of WeakMaps), and there's the |
|
685 // question of what behavior wrappers should have if consumers |
|
686 // keep hold of them. |
|
687 //widget.destroyInstance(widgetNode); |
|
688 } else if (provider == CustomizableUI.PROVIDER_SPECIAL) { |
|
689 this.visiblePalette.removeChild(paletteChild); |
|
690 } |
|
691 |
|
692 paletteChild = nextChild; |
|
693 } |
|
694 this.visiblePalette.hidden = false; |
|
695 this.window.gNavToolbox.palette = this._stowedPalette; |
|
696 }.bind(this)).then(null, ERROR); |
|
697 }, |
|
698 |
|
699 isCustomizableItem: function(aNode) { |
|
700 return aNode.localName == "toolbarbutton" || |
|
701 aNode.localName == "toolbaritem" || |
|
702 aNode.localName == "toolbarseparator" || |
|
703 aNode.localName == "toolbarspring" || |
|
704 aNode.localName == "toolbarspacer"; |
|
705 }, |
|
706 |
|
707 isWrappedToolbarItem: function(aNode) { |
|
708 return aNode.localName == "toolbarpaletteitem"; |
|
709 }, |
|
710 |
|
711 deferredWrapToolbarItem: function(aNode, aPlace) { |
|
712 let deferred = Promise.defer(); |
|
713 |
|
714 dispatchFunction(function() { |
|
715 let wrapper = this.wrapToolbarItem(aNode, aPlace); |
|
716 deferred.resolve(wrapper); |
|
717 }.bind(this)) |
|
718 |
|
719 return deferred.promise; |
|
720 }, |
|
721 |
|
722 wrapToolbarItem: function(aNode, aPlace) { |
|
723 if (!this.isCustomizableItem(aNode)) { |
|
724 return aNode; |
|
725 } |
|
726 let wrapper = this.createOrUpdateWrapper(aNode, aPlace); |
|
727 |
|
728 // It's possible that this toolbar node is "mid-flight" and doesn't have |
|
729 // a parent, in which case we skip replacing it. This can happen if a |
|
730 // toolbar item has been dragged into the palette. In that case, we tell |
|
731 // CustomizableUI to remove the widget from its area before putting the |
|
732 // widget in the palette - so the node will have no parent. |
|
733 if (aNode.parentNode) { |
|
734 aNode = aNode.parentNode.replaceChild(wrapper, aNode); |
|
735 } |
|
736 wrapper.appendChild(aNode); |
|
737 return wrapper; |
|
738 }, |
|
739 |
|
740 createOrUpdateWrapper: function(aNode, aPlace, aIsUpdate) { |
|
741 let wrapper; |
|
742 if (aIsUpdate && aNode.parentNode && aNode.parentNode.localName == "toolbarpaletteitem") { |
|
743 wrapper = aNode.parentNode; |
|
744 aPlace = wrapper.getAttribute("place"); |
|
745 } else { |
|
746 wrapper = this.document.createElement("toolbarpaletteitem"); |
|
747 // "place" is used by toolkit to add the toolbarpaletteitem-palette |
|
748 // binding to a toolbarpaletteitem, which gives it a label node for when |
|
749 // it's sitting in the palette. |
|
750 wrapper.setAttribute("place", aPlace); |
|
751 } |
|
752 |
|
753 |
|
754 // Ensure the wrapped item doesn't look like it's in any special state, and |
|
755 // can't be interactved with when in the customization palette. |
|
756 if (aNode.hasAttribute("command")) { |
|
757 wrapper.setAttribute("itemcommand", aNode.getAttribute("command")); |
|
758 aNode.removeAttribute("command"); |
|
759 } |
|
760 |
|
761 if (aNode.hasAttribute("observes")) { |
|
762 wrapper.setAttribute("itemobserves", aNode.getAttribute("observes")); |
|
763 aNode.removeAttribute("observes"); |
|
764 } |
|
765 |
|
766 if (aNode.getAttribute("checked") == "true") { |
|
767 wrapper.setAttribute("itemchecked", "true"); |
|
768 aNode.removeAttribute("checked"); |
|
769 } |
|
770 |
|
771 if (aNode.hasAttribute("id")) { |
|
772 wrapper.setAttribute("id", "wrapper-" + aNode.getAttribute("id")); |
|
773 } |
|
774 |
|
775 if (aNode.hasAttribute("label")) { |
|
776 wrapper.setAttribute("title", aNode.getAttribute("label")); |
|
777 } else if (aNode.hasAttribute("title")) { |
|
778 wrapper.setAttribute("title", aNode.getAttribute("title")); |
|
779 } |
|
780 |
|
781 if (aNode.hasAttribute("flex")) { |
|
782 wrapper.setAttribute("flex", aNode.getAttribute("flex")); |
|
783 } |
|
784 |
|
785 if (aPlace == "panel") { |
|
786 if (aNode.classList.contains(CustomizableUI.WIDE_PANEL_CLASS)) { |
|
787 wrapper.setAttribute("haswideitem", "true"); |
|
788 } else if (wrapper.hasAttribute("haswideitem")) { |
|
789 wrapper.removeAttribute("haswideitem"); |
|
790 } |
|
791 } |
|
792 |
|
793 let removable = aPlace == "palette" || CustomizableUI.isWidgetRemovable(aNode); |
|
794 wrapper.setAttribute("removable", removable); |
|
795 |
|
796 let contextMenuAttrName = aNode.getAttribute("context") ? "context" : |
|
797 aNode.getAttribute("contextmenu") ? "contextmenu" : ""; |
|
798 let currentContextMenu = aNode.getAttribute(contextMenuAttrName); |
|
799 let contextMenuForPlace = aPlace == "panel" ? |
|
800 kPanelItemContextMenu : |
|
801 kPaletteItemContextMenu; |
|
802 if (aPlace != "toolbar") { |
|
803 wrapper.setAttribute("context", contextMenuForPlace); |
|
804 } |
|
805 // Only keep track of the menu if it is non-default. |
|
806 if (currentContextMenu && |
|
807 currentContextMenu != contextMenuForPlace) { |
|
808 aNode.setAttribute("wrapped-context", currentContextMenu); |
|
809 aNode.setAttribute("wrapped-contextAttrName", contextMenuAttrName) |
|
810 aNode.removeAttribute(contextMenuAttrName); |
|
811 } else if (currentContextMenu == contextMenuForPlace) { |
|
812 aNode.removeAttribute(contextMenuAttrName); |
|
813 } |
|
814 |
|
815 // Only add listeners for newly created wrappers: |
|
816 if (!aIsUpdate) { |
|
817 wrapper.addEventListener("mousedown", this); |
|
818 wrapper.addEventListener("mouseup", this); |
|
819 } |
|
820 |
|
821 return wrapper; |
|
822 }, |
|
823 |
|
824 deferredUnwrapToolbarItem: function(aWrapper) { |
|
825 let deferred = Promise.defer(); |
|
826 dispatchFunction(function() { |
|
827 let item = null; |
|
828 try { |
|
829 item = this.unwrapToolbarItem(aWrapper); |
|
830 } catch (ex) { |
|
831 Cu.reportError(ex); |
|
832 } |
|
833 deferred.resolve(item); |
|
834 }.bind(this)); |
|
835 return deferred.promise; |
|
836 }, |
|
837 |
|
838 unwrapToolbarItem: function(aWrapper) { |
|
839 if (aWrapper.nodeName != "toolbarpaletteitem") { |
|
840 return aWrapper; |
|
841 } |
|
842 aWrapper.removeEventListener("mousedown", this); |
|
843 aWrapper.removeEventListener("mouseup", this); |
|
844 |
|
845 let place = aWrapper.getAttribute("place"); |
|
846 |
|
847 let toolbarItem = aWrapper.firstChild; |
|
848 if (!toolbarItem) { |
|
849 ERROR("no toolbarItem child for " + aWrapper.tagName + "#" + aWrapper.id); |
|
850 aWrapper.remove(); |
|
851 return null; |
|
852 } |
|
853 |
|
854 if (aWrapper.hasAttribute("itemobserves")) { |
|
855 toolbarItem.setAttribute("observes", aWrapper.getAttribute("itemobserves")); |
|
856 } |
|
857 |
|
858 if (aWrapper.hasAttribute("itemchecked")) { |
|
859 toolbarItem.checked = true; |
|
860 } |
|
861 |
|
862 if (aWrapper.hasAttribute("itemcommand")) { |
|
863 let commandID = aWrapper.getAttribute("itemcommand"); |
|
864 toolbarItem.setAttribute("command", commandID); |
|
865 |
|
866 //XXX Bug 309953 - toolbarbuttons aren't in sync with their commands after customizing |
|
867 let command = this.document.getElementById(commandID); |
|
868 if (command && command.hasAttribute("disabled")) { |
|
869 toolbarItem.setAttribute("disabled", command.getAttribute("disabled")); |
|
870 } |
|
871 } |
|
872 |
|
873 let wrappedContext = toolbarItem.getAttribute("wrapped-context"); |
|
874 if (wrappedContext) { |
|
875 let contextAttrName = toolbarItem.getAttribute("wrapped-contextAttrName"); |
|
876 toolbarItem.setAttribute(contextAttrName, wrappedContext); |
|
877 toolbarItem.removeAttribute("wrapped-contextAttrName"); |
|
878 toolbarItem.removeAttribute("wrapped-context"); |
|
879 } else if (place == "panel") { |
|
880 toolbarItem.setAttribute("context", kPanelItemContextMenu); |
|
881 } |
|
882 |
|
883 if (aWrapper.parentNode) { |
|
884 aWrapper.parentNode.replaceChild(toolbarItem, aWrapper); |
|
885 } |
|
886 return toolbarItem; |
|
887 }, |
|
888 |
|
889 _wrapToolbarItems: function() { |
|
890 let window = this.window; |
|
891 // Add drag-and-drop event handlers to all of the customizable areas. |
|
892 return Task.spawn(function() { |
|
893 this.areas = []; |
|
894 for (let area of CustomizableUI.areas) { |
|
895 let target = CustomizableUI.getCustomizeTargetForArea(area, window); |
|
896 this._addDragHandlers(target); |
|
897 for (let child of target.children) { |
|
898 if (this.isCustomizableItem(child)) { |
|
899 yield this.deferredWrapToolbarItem(child, CustomizableUI.getPlaceForItem(child)); |
|
900 } |
|
901 } |
|
902 this.areas.push(target); |
|
903 } |
|
904 }.bind(this)).then(null, ERROR); |
|
905 }, |
|
906 |
|
907 _addDragHandlers: function(aTarget) { |
|
908 aTarget.addEventListener("dragstart", this, true); |
|
909 aTarget.addEventListener("dragover", this, true); |
|
910 aTarget.addEventListener("dragexit", this, true); |
|
911 aTarget.addEventListener("drop", this, true); |
|
912 aTarget.addEventListener("dragend", this, true); |
|
913 }, |
|
914 |
|
915 _wrapItemsInArea: function(target) { |
|
916 for (let child of target.children) { |
|
917 if (this.isCustomizableItem(child)) { |
|
918 this.wrapToolbarItem(child, CustomizableUI.getPlaceForItem(child)); |
|
919 } |
|
920 } |
|
921 }, |
|
922 |
|
923 _removeDragHandlers: function(aTarget) { |
|
924 aTarget.removeEventListener("dragstart", this, true); |
|
925 aTarget.removeEventListener("dragover", this, true); |
|
926 aTarget.removeEventListener("dragexit", this, true); |
|
927 aTarget.removeEventListener("drop", this, true); |
|
928 aTarget.removeEventListener("dragend", this, true); |
|
929 }, |
|
930 |
|
931 _unwrapItemsInArea: function(target) { |
|
932 for (let toolbarItem of target.children) { |
|
933 if (this.isWrappedToolbarItem(toolbarItem)) { |
|
934 this.unwrapToolbarItem(toolbarItem); |
|
935 } |
|
936 } |
|
937 }, |
|
938 |
|
939 _unwrapToolbarItems: function() { |
|
940 return Task.spawn(function() { |
|
941 for (let target of this.areas) { |
|
942 for (let toolbarItem of target.children) { |
|
943 if (this.isWrappedToolbarItem(toolbarItem)) { |
|
944 yield this.deferredUnwrapToolbarItem(toolbarItem); |
|
945 } |
|
946 } |
|
947 this._removeDragHandlers(target); |
|
948 } |
|
949 }.bind(this)).then(null, ERROR); |
|
950 }, |
|
951 |
|
952 _removeExtraToolbarsIfEmpty: function() { |
|
953 let toolbox = this.window.gNavToolbox; |
|
954 for (let child of toolbox.children) { |
|
955 if (child.hasAttribute("customindex")) { |
|
956 let placements = CustomizableUI.getWidgetIdsInArea(child.id); |
|
957 if (!placements.length) { |
|
958 CustomizableUI.removeExtraToolbar(child.id); |
|
959 } |
|
960 } |
|
961 } |
|
962 }, |
|
963 |
|
964 persistCurrentSets: function(aSetBeforePersisting) { |
|
965 let document = this.document; |
|
966 let toolbars = document.querySelectorAll("toolbar[customizable='true'][currentset]"); |
|
967 for (let toolbar of toolbars) { |
|
968 if (aSetBeforePersisting) { |
|
969 let set = toolbar.currentSet; |
|
970 toolbar.setAttribute("currentset", set); |
|
971 } |
|
972 // Persist the currentset attribute directly on hardcoded toolbars. |
|
973 document.persist(toolbar.id, "currentset"); |
|
974 } |
|
975 }, |
|
976 |
|
977 reset: function() { |
|
978 this.resetting = true; |
|
979 // Disable the reset button temporarily while resetting: |
|
980 let btn = this.document.getElementById("customization-reset-button"); |
|
981 BrowserUITelemetry.countCustomizationEvent("reset"); |
|
982 btn.disabled = true; |
|
983 return Task.spawn(function() { |
|
984 this._removePanelCustomizationPlaceholders(); |
|
985 yield this.depopulatePalette(); |
|
986 yield this._unwrapToolbarItems(); |
|
987 |
|
988 CustomizableUI.reset(); |
|
989 |
|
990 yield this._wrapToolbarItems(); |
|
991 this.populatePalette(); |
|
992 |
|
993 this.persistCurrentSets(true); |
|
994 |
|
995 this._updateResetButton(); |
|
996 this._updateUndoResetButton(); |
|
997 this._updateEmptyPaletteNotice(); |
|
998 this._showPanelCustomizationPlaceholders(); |
|
999 this.resetting = false; |
|
1000 if (!this._wantToBeInCustomizeMode) { |
|
1001 this.exit(); |
|
1002 } |
|
1003 }.bind(this)).then(null, ERROR); |
|
1004 }, |
|
1005 |
|
1006 undoReset: function() { |
|
1007 this.resetting = true; |
|
1008 |
|
1009 return Task.spawn(function() { |
|
1010 this._removePanelCustomizationPlaceholders(); |
|
1011 yield this.depopulatePalette(); |
|
1012 yield this._unwrapToolbarItems(); |
|
1013 |
|
1014 CustomizableUI.undoReset(); |
|
1015 |
|
1016 yield this._wrapToolbarItems(); |
|
1017 this.populatePalette(); |
|
1018 |
|
1019 this.persistCurrentSets(true); |
|
1020 |
|
1021 this._updateResetButton(); |
|
1022 this._updateUndoResetButton(); |
|
1023 this._updateEmptyPaletteNotice(); |
|
1024 this.resetting = false; |
|
1025 }.bind(this)).then(null, ERROR); |
|
1026 }, |
|
1027 |
|
1028 _onToolbarVisibilityChange: function(aEvent) { |
|
1029 let toolbar = aEvent.target; |
|
1030 if (aEvent.detail.visible && toolbar.getAttribute("customizable") == "true") { |
|
1031 toolbar.setAttribute("customizing", "true"); |
|
1032 } else { |
|
1033 toolbar.removeAttribute("customizing"); |
|
1034 } |
|
1035 this._onUIChange(); |
|
1036 }, |
|
1037 |
|
1038 onWidgetMoved: function(aWidgetId, aArea, aOldPosition, aNewPosition) { |
|
1039 this._onUIChange(); |
|
1040 }, |
|
1041 |
|
1042 onWidgetAdded: function(aWidgetId, aArea, aPosition) { |
|
1043 this._onUIChange(); |
|
1044 }, |
|
1045 |
|
1046 onWidgetRemoved: function(aWidgetId, aArea) { |
|
1047 this._onUIChange(); |
|
1048 }, |
|
1049 |
|
1050 onWidgetBeforeDOMChange: function(aNodeToChange, aSecondaryNode, aContainer) { |
|
1051 if (aContainer.ownerDocument.defaultView != this.window || this.resetting) { |
|
1052 return; |
|
1053 } |
|
1054 if (aContainer.id == CustomizableUI.AREA_PANEL) { |
|
1055 this._removePanelCustomizationPlaceholders(); |
|
1056 } |
|
1057 // If we get called for widgets that aren't in the window yet, they might not have |
|
1058 // a parentNode at all. |
|
1059 if (aNodeToChange.parentNode) { |
|
1060 this.unwrapToolbarItem(aNodeToChange.parentNode); |
|
1061 } |
|
1062 if (aSecondaryNode) { |
|
1063 this.unwrapToolbarItem(aSecondaryNode.parentNode); |
|
1064 } |
|
1065 }, |
|
1066 |
|
1067 onWidgetAfterDOMChange: function(aNodeToChange, aSecondaryNode, aContainer) { |
|
1068 if (aContainer.ownerDocument.defaultView != this.window || this.resetting) { |
|
1069 return; |
|
1070 } |
|
1071 // If the node is still attached to the container, wrap it again: |
|
1072 if (aNodeToChange.parentNode) { |
|
1073 let place = CustomizableUI.getPlaceForItem(aNodeToChange); |
|
1074 this.wrapToolbarItem(aNodeToChange, place); |
|
1075 if (aSecondaryNode) { |
|
1076 this.wrapToolbarItem(aSecondaryNode, place); |
|
1077 } |
|
1078 } else { |
|
1079 // If not, it got removed. |
|
1080 |
|
1081 // If an API-based widget is removed while customizing, append it to the palette. |
|
1082 // The _applyDrop code itself will take care of positioning it correctly, if |
|
1083 // applicable. We need the code to be here so removing widgets using CustomizableUI's |
|
1084 // API also does the right thing (and adds it to the palette) |
|
1085 let widgetId = aNodeToChange.id; |
|
1086 let widget = CustomizableUI.getWidget(widgetId); |
|
1087 if (widget.provider == CustomizableUI.PROVIDER_API) { |
|
1088 let paletteItem = this.makePaletteItem(widget, "palette"); |
|
1089 this.visiblePalette.appendChild(paletteItem); |
|
1090 } |
|
1091 } |
|
1092 if (aContainer.id == CustomizableUI.AREA_PANEL) { |
|
1093 this._showPanelCustomizationPlaceholders(); |
|
1094 } |
|
1095 }, |
|
1096 |
|
1097 onWidgetDestroyed: function(aWidgetId) { |
|
1098 let wrapper = this.document.getElementById("wrapper-" + aWidgetId); |
|
1099 if (wrapper) { |
|
1100 let wasInPanel = wrapper.parentNode == this.panelUIContents; |
|
1101 wrapper.remove(); |
|
1102 if (wasInPanel) { |
|
1103 this._showPanelCustomizationPlaceholders(); |
|
1104 } |
|
1105 } |
|
1106 }, |
|
1107 |
|
1108 onWidgetAfterCreation: function(aWidgetId, aArea) { |
|
1109 // If the node was added to an area, we would have gotten an onWidgetAdded notification, |
|
1110 // plus associated DOM change notifications, so only do stuff for the palette: |
|
1111 if (!aArea) { |
|
1112 let widgetNode = this.document.getElementById(aWidgetId); |
|
1113 if (widgetNode) { |
|
1114 this.wrapToolbarItem(widgetNode, "palette"); |
|
1115 } else { |
|
1116 let widget = CustomizableUI.getWidget(aWidgetId); |
|
1117 this.visiblePalette.appendChild(this.makePaletteItem(widget, "palette")); |
|
1118 } |
|
1119 } |
|
1120 }, |
|
1121 |
|
1122 onAreaNodeRegistered: function(aArea, aContainer) { |
|
1123 if (aContainer.ownerDocument == this.document) { |
|
1124 this._wrapItemsInArea(aContainer); |
|
1125 this._addDragHandlers(aContainer); |
|
1126 DragPositionManager.add(this.window, aArea, aContainer); |
|
1127 this.areas.push(aContainer); |
|
1128 } |
|
1129 }, |
|
1130 |
|
1131 onAreaNodeUnregistered: function(aArea, aContainer, aReason) { |
|
1132 if (aContainer.ownerDocument == this.document && aReason == CustomizableUI.REASON_AREA_UNREGISTERED) { |
|
1133 this._unwrapItemsInArea(aContainer); |
|
1134 this._removeDragHandlers(aContainer); |
|
1135 DragPositionManager.remove(this.window, aArea, aContainer); |
|
1136 let index = this.areas.indexOf(aContainer); |
|
1137 this.areas.splice(index, 1); |
|
1138 } |
|
1139 }, |
|
1140 |
|
1141 _onUIChange: function() { |
|
1142 this._changed = true; |
|
1143 if (!this.resetting) { |
|
1144 this._updateResetButton(); |
|
1145 this._updateUndoResetButton(); |
|
1146 this._updateEmptyPaletteNotice(); |
|
1147 } |
|
1148 CustomizableUI.dispatchToolboxEvent("customizationchange"); |
|
1149 }, |
|
1150 |
|
1151 _updateEmptyPaletteNotice: function() { |
|
1152 let paletteItems = this.visiblePalette.getElementsByTagName("toolbarpaletteitem"); |
|
1153 this.paletteEmptyNotice.hidden = !!paletteItems.length; |
|
1154 }, |
|
1155 |
|
1156 _updateResetButton: function() { |
|
1157 let btn = this.document.getElementById("customization-reset-button"); |
|
1158 btn.disabled = CustomizableUI.inDefaultState; |
|
1159 }, |
|
1160 |
|
1161 _updateUndoResetButton: function() { |
|
1162 let undoResetButton = this.document.getElementById("customization-undo-reset-button"); |
|
1163 undoResetButton.hidden = !CustomizableUI.canUndoReset; |
|
1164 }, |
|
1165 |
|
1166 handleEvent: function(aEvent) { |
|
1167 switch(aEvent.type) { |
|
1168 case "toolbarvisibilitychange": |
|
1169 this._onToolbarVisibilityChange(aEvent); |
|
1170 break; |
|
1171 case "dragstart": |
|
1172 this._onDragStart(aEvent); |
|
1173 break; |
|
1174 case "dragover": |
|
1175 this._onDragOver(aEvent); |
|
1176 break; |
|
1177 case "drop": |
|
1178 this._onDragDrop(aEvent); |
|
1179 break; |
|
1180 case "dragexit": |
|
1181 this._onDragExit(aEvent); |
|
1182 break; |
|
1183 case "dragend": |
|
1184 this._onDragEnd(aEvent); |
|
1185 break; |
|
1186 case "command": |
|
1187 if (aEvent.originalTarget == this.window.PanelUI.menuButton) { |
|
1188 this.exit(); |
|
1189 aEvent.preventDefault(); |
|
1190 } |
|
1191 break; |
|
1192 case "mousedown": |
|
1193 this._onMouseDown(aEvent); |
|
1194 break; |
|
1195 case "mouseup": |
|
1196 this._onMouseUp(aEvent); |
|
1197 break; |
|
1198 case "keypress": |
|
1199 if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) { |
|
1200 this.exit(); |
|
1201 } |
|
1202 break; |
|
1203 #ifdef CAN_DRAW_IN_TITLEBAR |
|
1204 case "unload": |
|
1205 this.uninit(); |
|
1206 break; |
|
1207 #endif |
|
1208 } |
|
1209 }, |
|
1210 |
|
1211 #ifdef CAN_DRAW_IN_TITLEBAR |
|
1212 observe: function(aSubject, aTopic, aData) { |
|
1213 switch (aTopic) { |
|
1214 case "nsPref:changed": |
|
1215 this._updateResetButton(); |
|
1216 this._updateTitlebarButton(); |
|
1217 this._updateUndoResetButton(); |
|
1218 break; |
|
1219 } |
|
1220 }, |
|
1221 |
|
1222 _updateTitlebarButton: function() { |
|
1223 let drawInTitlebar = true; |
|
1224 try { |
|
1225 drawInTitlebar = Services.prefs.getBoolPref(kDrawInTitlebarPref); |
|
1226 } catch (ex) { } |
|
1227 let button = this.document.getElementById("customization-titlebar-visibility-button"); |
|
1228 // Drawing in the titlebar means 'hiding' the titlebar: |
|
1229 if (drawInTitlebar) { |
|
1230 button.removeAttribute("checked"); |
|
1231 } else { |
|
1232 button.setAttribute("checked", "true"); |
|
1233 } |
|
1234 }, |
|
1235 |
|
1236 toggleTitlebar: function(aShouldShowTitlebar) { |
|
1237 // Drawing in the titlebar means not showing the titlebar, hence the negation: |
|
1238 Services.prefs.setBoolPref(kDrawInTitlebarPref, !aShouldShowTitlebar); |
|
1239 }, |
|
1240 #endif |
|
1241 |
|
1242 _onDragStart: function(aEvent) { |
|
1243 __dumpDragData(aEvent); |
|
1244 let item = aEvent.target; |
|
1245 while (item && item.localName != "toolbarpaletteitem") { |
|
1246 if (item.localName == "toolbar") { |
|
1247 return; |
|
1248 } |
|
1249 item = item.parentNode; |
|
1250 } |
|
1251 |
|
1252 let draggedItem = item.firstChild; |
|
1253 let placeForItem = CustomizableUI.getPlaceForItem(item); |
|
1254 let isRemovable = placeForItem == "palette" || |
|
1255 CustomizableUI.isWidgetRemovable(draggedItem); |
|
1256 if (item.classList.contains(kPlaceholderClass) || !isRemovable) { |
|
1257 return; |
|
1258 } |
|
1259 |
|
1260 let dt = aEvent.dataTransfer; |
|
1261 let documentId = aEvent.target.ownerDocument.documentElement.id; |
|
1262 let isInToolbar = placeForItem == "toolbar"; |
|
1263 |
|
1264 dt.mozSetDataAt(kDragDataTypePrefix + documentId, draggedItem.id, 0); |
|
1265 dt.effectAllowed = "move"; |
|
1266 |
|
1267 let itemRect = draggedItem.getBoundingClientRect(); |
|
1268 let itemCenter = {x: itemRect.left + itemRect.width / 2, |
|
1269 y: itemRect.top + itemRect.height / 2}; |
|
1270 this._dragOffset = {x: aEvent.clientX - itemCenter.x, |
|
1271 y: aEvent.clientY - itemCenter.y}; |
|
1272 |
|
1273 gDraggingInToolbars = new Set(); |
|
1274 |
|
1275 // Hack needed so that the dragimage will still show the |
|
1276 // item as it appeared before it was hidden. |
|
1277 this._initializeDragAfterMove = function() { |
|
1278 // For automated tests, we sometimes start exiting customization mode |
|
1279 // before this fires, which leaves us with placeholders inserted after |
|
1280 // we've exited. So we need to check that we are indeed customizing. |
|
1281 if (this._customizing && !this._transitioning) { |
|
1282 item.hidden = true; |
|
1283 this._showPanelCustomizationPlaceholders(); |
|
1284 DragPositionManager.start(this.window); |
|
1285 if (item.nextSibling) { |
|
1286 this._setDragActive(item.nextSibling, "before", draggedItem.id, isInToolbar); |
|
1287 this._dragOverItem = item.nextSibling; |
|
1288 } else if (isInToolbar && item.previousSibling) { |
|
1289 this._setDragActive(item.previousSibling, "after", draggedItem.id, isInToolbar); |
|
1290 this._dragOverItem = item.previousSibling; |
|
1291 } |
|
1292 } |
|
1293 this._initializeDragAfterMove = null; |
|
1294 this.window.clearTimeout(this._dragInitializeTimeout); |
|
1295 }.bind(this); |
|
1296 this._dragInitializeTimeout = this.window.setTimeout(this._initializeDragAfterMove, 0); |
|
1297 }, |
|
1298 |
|
1299 _onDragOver: function(aEvent) { |
|
1300 if (this._isUnwantedDragDrop(aEvent)) { |
|
1301 return; |
|
1302 } |
|
1303 if (this._initializeDragAfterMove) { |
|
1304 this._initializeDragAfterMove(); |
|
1305 } |
|
1306 |
|
1307 __dumpDragData(aEvent); |
|
1308 |
|
1309 let document = aEvent.target.ownerDocument; |
|
1310 let documentId = document.documentElement.id; |
|
1311 if (!aEvent.dataTransfer.mozTypesAt(0)) { |
|
1312 return; |
|
1313 } |
|
1314 |
|
1315 let draggedItemId = |
|
1316 aEvent.dataTransfer.mozGetDataAt(kDragDataTypePrefix + documentId, 0); |
|
1317 let draggedWrapper = document.getElementById("wrapper-" + draggedItemId); |
|
1318 let targetArea = this._getCustomizableParent(aEvent.currentTarget); |
|
1319 let originArea = this._getCustomizableParent(draggedWrapper); |
|
1320 |
|
1321 // Do nothing if the target or origin are not customizable. |
|
1322 if (!targetArea || !originArea) { |
|
1323 return; |
|
1324 } |
|
1325 |
|
1326 // Do nothing if the widget is not allowed to be removed. |
|
1327 if (targetArea.id == kPaletteId && |
|
1328 !CustomizableUI.isWidgetRemovable(draggedItemId)) { |
|
1329 return; |
|
1330 } |
|
1331 |
|
1332 // Do nothing if the widget is not allowed to move to the target area. |
|
1333 if (targetArea.id != kPaletteId && |
|
1334 !CustomizableUI.canWidgetMoveToArea(draggedItemId, targetArea.id)) { |
|
1335 return; |
|
1336 } |
|
1337 |
|
1338 let targetIsToolbar = CustomizableUI.getAreaType(targetArea.id) == "toolbar"; |
|
1339 let targetNode = this._getDragOverNode(aEvent, targetArea, targetIsToolbar, draggedItemId); |
|
1340 |
|
1341 // We need to determine the place that the widget is being dropped in |
|
1342 // the target. |
|
1343 let dragOverItem, dragValue; |
|
1344 if (targetNode == targetArea.customizationTarget) { |
|
1345 // We'll assume if the user is dragging directly over the target, that |
|
1346 // they're attempting to append a child to that target. |
|
1347 dragOverItem = (targetIsToolbar ? this._findVisiblePreviousSiblingNode(targetNode.lastChild) : |
|
1348 targetNode.lastChild) || targetNode; |
|
1349 dragValue = "after"; |
|
1350 } else { |
|
1351 let targetParent = targetNode.parentNode; |
|
1352 let position = Array.indexOf(targetParent.children, targetNode); |
|
1353 if (position == -1) { |
|
1354 dragOverItem = targetIsToolbar ? this._findVisiblePreviousSiblingNode(targetNode.lastChild) : |
|
1355 targetParent.lastChild; |
|
1356 dragValue = "after"; |
|
1357 } else { |
|
1358 dragOverItem = targetParent.children[position]; |
|
1359 if (!targetIsToolbar) { |
|
1360 dragValue = "before"; |
|
1361 } else { |
|
1362 dragOverItem = this._findVisiblePreviousSiblingNode(targetParent.children[position]); |
|
1363 // Check if the aDraggedItem is hovered past the first half of dragOverItem |
|
1364 let window = dragOverItem.ownerDocument.defaultView; |
|
1365 let direction = window.getComputedStyle(dragOverItem, null).direction; |
|
1366 let itemRect = dragOverItem.getBoundingClientRect(); |
|
1367 let dropTargetCenter = itemRect.left + (itemRect.width / 2); |
|
1368 let existingDir = dragOverItem.getAttribute("dragover"); |
|
1369 if ((existingDir == "before") == (direction == "ltr")) { |
|
1370 dropTargetCenter += (parseInt(dragOverItem.style.borderLeftWidth) || 0) / 2; |
|
1371 } else { |
|
1372 dropTargetCenter -= (parseInt(dragOverItem.style.borderRightWidth) || 0) / 2; |
|
1373 } |
|
1374 let before = direction == "ltr" ? aEvent.clientX < dropTargetCenter : aEvent.clientX > dropTargetCenter; |
|
1375 dragValue = before ? "before" : "after"; |
|
1376 } |
|
1377 } |
|
1378 } |
|
1379 |
|
1380 if (this._dragOverItem && dragOverItem != this._dragOverItem) { |
|
1381 this._cancelDragActive(this._dragOverItem, dragOverItem); |
|
1382 } |
|
1383 |
|
1384 if (dragOverItem != this._dragOverItem || dragValue != dragOverItem.getAttribute("dragover")) { |
|
1385 if (dragOverItem != targetArea.customizationTarget) { |
|
1386 this._setDragActive(dragOverItem, dragValue, draggedItemId, targetIsToolbar); |
|
1387 } else if (targetIsToolbar) { |
|
1388 this._updateToolbarCustomizationOutline(this.window, targetArea); |
|
1389 } |
|
1390 this._dragOverItem = dragOverItem; |
|
1391 } |
|
1392 |
|
1393 aEvent.preventDefault(); |
|
1394 aEvent.stopPropagation(); |
|
1395 }, |
|
1396 |
|
1397 _onDragDrop: function(aEvent) { |
|
1398 if (this._isUnwantedDragDrop(aEvent)) { |
|
1399 return; |
|
1400 } |
|
1401 |
|
1402 __dumpDragData(aEvent); |
|
1403 this._initializeDragAfterMove = null; |
|
1404 this.window.clearTimeout(this._dragInitializeTimeout); |
|
1405 |
|
1406 let targetArea = this._getCustomizableParent(aEvent.currentTarget); |
|
1407 let document = aEvent.target.ownerDocument; |
|
1408 let documentId = document.documentElement.id; |
|
1409 let draggedItemId = |
|
1410 aEvent.dataTransfer.mozGetDataAt(kDragDataTypePrefix + documentId, 0); |
|
1411 let draggedWrapper = document.getElementById("wrapper-" + draggedItemId); |
|
1412 let originArea = this._getCustomizableParent(draggedWrapper); |
|
1413 if (this._dragSizeMap) { |
|
1414 this._dragSizeMap.clear(); |
|
1415 } |
|
1416 // Do nothing if the target area or origin area are not customizable. |
|
1417 if (!targetArea || !originArea) { |
|
1418 return; |
|
1419 } |
|
1420 let targetNode = this._dragOverItem; |
|
1421 let dropDir = targetNode.getAttribute("dragover"); |
|
1422 // Need to insert *after* this node if we promised the user that: |
|
1423 if (targetNode != targetArea && dropDir == "after") { |
|
1424 if (targetNode.nextSibling) { |
|
1425 targetNode = targetNode.nextSibling; |
|
1426 } else { |
|
1427 targetNode = targetArea; |
|
1428 } |
|
1429 } |
|
1430 // If the target node is a placeholder, get its sibling as the real target. |
|
1431 while (targetNode.classList.contains(kPlaceholderClass) && targetNode.nextSibling) { |
|
1432 targetNode = targetNode.nextSibling; |
|
1433 } |
|
1434 if (targetNode.tagName == "toolbarpaletteitem") { |
|
1435 targetNode = targetNode.firstChild; |
|
1436 } |
|
1437 |
|
1438 this._cancelDragActive(this._dragOverItem, null, true); |
|
1439 this._removePanelCustomizationPlaceholders(); |
|
1440 |
|
1441 try { |
|
1442 this._applyDrop(aEvent, targetArea, originArea, draggedItemId, targetNode); |
|
1443 } catch (ex) { |
|
1444 ERROR(ex, ex.stack); |
|
1445 } |
|
1446 |
|
1447 this._showPanelCustomizationPlaceholders(); |
|
1448 }, |
|
1449 |
|
1450 _applyDrop: function(aEvent, aTargetArea, aOriginArea, aDraggedItemId, aTargetNode) { |
|
1451 let document = aEvent.target.ownerDocument; |
|
1452 let draggedItem = document.getElementById(aDraggedItemId); |
|
1453 draggedItem.hidden = false; |
|
1454 draggedItem.removeAttribute("mousedown"); |
|
1455 |
|
1456 // Do nothing if the target was dropped onto itself (ie, no change in area |
|
1457 // or position). |
|
1458 if (draggedItem == aTargetNode) { |
|
1459 return; |
|
1460 } |
|
1461 |
|
1462 // Is the target area the customization palette? |
|
1463 if (aTargetArea.id == kPaletteId) { |
|
1464 // Did we drag from outside the palette? |
|
1465 if (aOriginArea.id !== kPaletteId) { |
|
1466 if (!CustomizableUI.isWidgetRemovable(aDraggedItemId)) { |
|
1467 return; |
|
1468 } |
|
1469 |
|
1470 CustomizableUI.removeWidgetFromArea(aDraggedItemId); |
|
1471 BrowserUITelemetry.countCustomizationEvent("remove"); |
|
1472 // Special widgets are removed outright, we can return here: |
|
1473 if (CustomizableUI.isSpecialWidget(aDraggedItemId)) { |
|
1474 return; |
|
1475 } |
|
1476 } |
|
1477 draggedItem = draggedItem.parentNode; |
|
1478 |
|
1479 // If the target node is the palette itself, just append |
|
1480 if (aTargetNode == this.visiblePalette) { |
|
1481 this.visiblePalette.appendChild(draggedItem); |
|
1482 } else { |
|
1483 // The items in the palette are wrapped, so we need the target node's parent here: |
|
1484 this.visiblePalette.insertBefore(draggedItem, aTargetNode.parentNode); |
|
1485 } |
|
1486 if (aOriginArea.id !== kPaletteId) { |
|
1487 // The dragend event already fires when the item moves within the palette. |
|
1488 this._onDragEnd(aEvent); |
|
1489 } |
|
1490 return; |
|
1491 } |
|
1492 |
|
1493 if (!CustomizableUI.canWidgetMoveToArea(aDraggedItemId, aTargetArea.id)) { |
|
1494 return; |
|
1495 } |
|
1496 |
|
1497 // Skipintoolbarset items won't really be moved: |
|
1498 if (draggedItem.getAttribute("skipintoolbarset") == "true") { |
|
1499 // These items should never leave their area: |
|
1500 if (aTargetArea != aOriginArea) { |
|
1501 return; |
|
1502 } |
|
1503 let place = draggedItem.parentNode.getAttribute("place"); |
|
1504 this.unwrapToolbarItem(draggedItem.parentNode); |
|
1505 if (aTargetNode == aTargetArea.customizationTarget) { |
|
1506 aTargetArea.customizationTarget.appendChild(draggedItem); |
|
1507 } else { |
|
1508 this.unwrapToolbarItem(aTargetNode.parentNode); |
|
1509 aTargetArea.customizationTarget.insertBefore(draggedItem, aTargetNode); |
|
1510 this.wrapToolbarItem(aTargetNode, place); |
|
1511 } |
|
1512 this.wrapToolbarItem(draggedItem, place); |
|
1513 BrowserUITelemetry.countCustomizationEvent("move"); |
|
1514 return; |
|
1515 } |
|
1516 |
|
1517 // Is the target the customization area itself? If so, we just add the |
|
1518 // widget to the end of the area. |
|
1519 if (aTargetNode == aTargetArea.customizationTarget) { |
|
1520 CustomizableUI.addWidgetToArea(aDraggedItemId, aTargetArea.id); |
|
1521 // For the purposes of BrowserUITelemetry, we consider both moving a widget |
|
1522 // within the same area, and adding a widget from one area to another area |
|
1523 // as a "move". An "add" is only when we move an item from the palette into |
|
1524 // an area. |
|
1525 let custEventType = aOriginArea.id == kPaletteId ? "add" : "move"; |
|
1526 BrowserUITelemetry.countCustomizationEvent(custEventType); |
|
1527 this._onDragEnd(aEvent); |
|
1528 return; |
|
1529 } |
|
1530 |
|
1531 // We need to determine the place that the widget is being dropped in |
|
1532 // the target. |
|
1533 let placement; |
|
1534 let itemForPlacement = aTargetNode; |
|
1535 // Skip the skipintoolbarset items when determining the place of the item: |
|
1536 while (itemForPlacement && itemForPlacement.getAttribute("skipintoolbarset") == "true" && |
|
1537 itemForPlacement.parentNode && |
|
1538 itemForPlacement.parentNode.nodeName == "toolbarpaletteitem") { |
|
1539 itemForPlacement = itemForPlacement.parentNode.nextSibling; |
|
1540 if (itemForPlacement && itemForPlacement.nodeName == "toolbarpaletteitem") { |
|
1541 itemForPlacement = itemForPlacement.firstChild; |
|
1542 } |
|
1543 } |
|
1544 if (itemForPlacement && !itemForPlacement.classList.contains(kPlaceholderClass)) { |
|
1545 let targetNodeId = (itemForPlacement.nodeName == "toolbarpaletteitem") ? |
|
1546 itemForPlacement.firstChild && itemForPlacement.firstChild.id : |
|
1547 itemForPlacement.id; |
|
1548 placement = CustomizableUI.getPlacementOfWidget(targetNodeId); |
|
1549 } |
|
1550 if (!placement) { |
|
1551 LOG("Could not get a position for " + aTargetNode.nodeName + "#" + aTargetNode.id + "." + aTargetNode.className); |
|
1552 } |
|
1553 let position = placement ? placement.position : null; |
|
1554 |
|
1555 // Is the target area the same as the origin? Since we've already handled |
|
1556 // the possibility that the target is the customization palette, we know |
|
1557 // that the widget is moving within a customizable area. |
|
1558 if (aTargetArea == aOriginArea) { |
|
1559 CustomizableUI.moveWidgetWithinArea(aDraggedItemId, position); |
|
1560 } else { |
|
1561 CustomizableUI.addWidgetToArea(aDraggedItemId, aTargetArea.id, position); |
|
1562 } |
|
1563 |
|
1564 this._onDragEnd(aEvent); |
|
1565 |
|
1566 // For BrowserUITelemetry, an "add" is only when we move an item from the palette |
|
1567 // into an area. Otherwise, it's a move. |
|
1568 let custEventType = aOriginArea.id == kPaletteId ? "add" : "move"; |
|
1569 BrowserUITelemetry.countCustomizationEvent(custEventType); |
|
1570 |
|
1571 // If we dropped onto a skipintoolbarset item, manually correct the drop location: |
|
1572 if (aTargetNode != itemForPlacement) { |
|
1573 let draggedWrapper = draggedItem.parentNode; |
|
1574 let container = draggedWrapper.parentNode; |
|
1575 container.insertBefore(draggedWrapper, aTargetNode.parentNode); |
|
1576 } |
|
1577 }, |
|
1578 |
|
1579 _onDragExit: function(aEvent) { |
|
1580 if (this._isUnwantedDragDrop(aEvent)) { |
|
1581 return; |
|
1582 } |
|
1583 |
|
1584 __dumpDragData(aEvent); |
|
1585 |
|
1586 // When leaving customization areas, cancel the drag on the last dragover item |
|
1587 // We've attached the listener to areas, so aEvent.currentTarget will be the area. |
|
1588 // We don't care about dragexit events fired on descendants of the area, |
|
1589 // so we check that the event's target is the same as the area to which the listener |
|
1590 // was attached. |
|
1591 if (this._dragOverItem && aEvent.target == aEvent.currentTarget) { |
|
1592 this._cancelDragActive(this._dragOverItem); |
|
1593 this._dragOverItem = null; |
|
1594 } |
|
1595 }, |
|
1596 |
|
1597 /** |
|
1598 * To workaround bug 460801 we manually forward the drop event here when dragend wouldn't be fired. |
|
1599 */ |
|
1600 _onDragEnd: function(aEvent) { |
|
1601 if (this._isUnwantedDragDrop(aEvent)) { |
|
1602 return; |
|
1603 } |
|
1604 this._initializeDragAfterMove = null; |
|
1605 this.window.clearTimeout(this._dragInitializeTimeout); |
|
1606 __dumpDragData(aEvent, "_onDragEnd"); |
|
1607 |
|
1608 let document = aEvent.target.ownerDocument; |
|
1609 document.documentElement.removeAttribute("customizing-movingItem"); |
|
1610 |
|
1611 let documentId = document.documentElement.id; |
|
1612 if (!aEvent.dataTransfer.mozTypesAt(0)) { |
|
1613 return; |
|
1614 } |
|
1615 |
|
1616 let draggedItemId = |
|
1617 aEvent.dataTransfer.mozGetDataAt(kDragDataTypePrefix + documentId, 0); |
|
1618 |
|
1619 let draggedWrapper = document.getElementById("wrapper-" + draggedItemId); |
|
1620 draggedWrapper.hidden = false; |
|
1621 draggedWrapper.removeAttribute("mousedown"); |
|
1622 if (this._dragOverItem) { |
|
1623 this._cancelDragActive(this._dragOverItem); |
|
1624 this._dragOverItem = null; |
|
1625 } |
|
1626 this._updateToolbarCustomizationOutline(this.window); |
|
1627 this._showPanelCustomizationPlaceholders(); |
|
1628 DragPositionManager.stop(); |
|
1629 }, |
|
1630 |
|
1631 _isUnwantedDragDrop: function(aEvent) { |
|
1632 // The simulated events generated by synthesizeDragStart/synthesizeDrop in |
|
1633 // mochitests are used only for testing whether the right data is being put |
|
1634 // into the dataTransfer. Neither cause a real drop to occur, so they don't |
|
1635 // set the source node. There isn't a means of testing real drag and drops, |
|
1636 // so this pref skips the check but it should only be set by test code. |
|
1637 if (this._skipSourceNodeCheck) { |
|
1638 return false; |
|
1639 } |
|
1640 |
|
1641 /* Discard drag events that originated from a separate window to |
|
1642 prevent content->chrome privilege escalations. */ |
|
1643 let mozSourceNode = aEvent.dataTransfer.mozSourceNode; |
|
1644 // mozSourceNode is null in the dragStart event handler or if |
|
1645 // the drag event originated in an external application. |
|
1646 return !mozSourceNode || |
|
1647 mozSourceNode.ownerDocument.defaultView != this.window; |
|
1648 }, |
|
1649 |
|
1650 _setDragActive: function(aItem, aValue, aDraggedItemId, aInToolbar) { |
|
1651 if (!aItem) { |
|
1652 return; |
|
1653 } |
|
1654 |
|
1655 if (aItem.getAttribute("dragover") != aValue) { |
|
1656 aItem.setAttribute("dragover", aValue); |
|
1657 |
|
1658 let window = aItem.ownerDocument.defaultView; |
|
1659 let draggedItem = window.document.getElementById(aDraggedItemId); |
|
1660 if (!aInToolbar) { |
|
1661 this._setGridDragActive(aItem, draggedItem, aValue); |
|
1662 } else { |
|
1663 let targetArea = this._getCustomizableParent(aItem); |
|
1664 this._updateToolbarCustomizationOutline(window, targetArea); |
|
1665 let makeSpaceImmediately = false; |
|
1666 if (!gDraggingInToolbars.has(targetArea.id)) { |
|
1667 gDraggingInToolbars.add(targetArea.id); |
|
1668 let draggedWrapper = this.document.getElementById("wrapper-" + aDraggedItemId); |
|
1669 let originArea = this._getCustomizableParent(draggedWrapper); |
|
1670 makeSpaceImmediately = originArea == targetArea; |
|
1671 } |
|
1672 // Calculate width of the item when it'd be dropped in this position |
|
1673 let width = this._getDragItemSize(aItem, draggedItem).width; |
|
1674 let direction = window.getComputedStyle(aItem).direction; |
|
1675 let prop, otherProp; |
|
1676 // If we're inserting before in ltr, or after in rtl: |
|
1677 if ((aValue == "before") == (direction == "ltr")) { |
|
1678 prop = "borderLeftWidth"; |
|
1679 otherProp = "border-right-width"; |
|
1680 } else { |
|
1681 // otherwise: |
|
1682 prop = "borderRightWidth"; |
|
1683 otherProp = "border-left-width"; |
|
1684 } |
|
1685 if (makeSpaceImmediately) { |
|
1686 aItem.setAttribute("notransition", "true"); |
|
1687 } |
|
1688 aItem.style[prop] = width + 'px'; |
|
1689 aItem.style.removeProperty(otherProp); |
|
1690 if (makeSpaceImmediately) { |
|
1691 // Force a layout flush: |
|
1692 aItem.getBoundingClientRect(); |
|
1693 aItem.removeAttribute("notransition"); |
|
1694 } |
|
1695 } |
|
1696 } |
|
1697 }, |
|
1698 _cancelDragActive: function(aItem, aNextItem, aNoTransition) { |
|
1699 this._updateToolbarCustomizationOutline(aItem.ownerDocument.defaultView); |
|
1700 let currentArea = this._getCustomizableParent(aItem); |
|
1701 if (!currentArea) { |
|
1702 return; |
|
1703 } |
|
1704 let isToolbar = CustomizableUI.getAreaType(currentArea.id) == "toolbar"; |
|
1705 if (isToolbar) { |
|
1706 if (aNoTransition) { |
|
1707 aItem.setAttribute("notransition", "true"); |
|
1708 } |
|
1709 aItem.removeAttribute("dragover"); |
|
1710 // Remove both property values in the case that the end padding |
|
1711 // had been set. |
|
1712 aItem.style.removeProperty("border-left-width"); |
|
1713 aItem.style.removeProperty("border-right-width"); |
|
1714 if (aNoTransition) { |
|
1715 // Force a layout flush: |
|
1716 aItem.getBoundingClientRect(); |
|
1717 aItem.removeAttribute("notransition"); |
|
1718 } |
|
1719 } else { |
|
1720 aItem.removeAttribute("dragover"); |
|
1721 if (aNextItem) { |
|
1722 let nextArea = this._getCustomizableParent(aNextItem); |
|
1723 if (nextArea == currentArea) { |
|
1724 // No need to do anything if we're still dragging in this area: |
|
1725 return; |
|
1726 } |
|
1727 } |
|
1728 // Otherwise, clear everything out: |
|
1729 let positionManager = DragPositionManager.getManagerForArea(currentArea); |
|
1730 positionManager.clearPlaceholders(currentArea, aNoTransition); |
|
1731 } |
|
1732 }, |
|
1733 |
|
1734 _setGridDragActive: function(aDragOverNode, aDraggedItem, aValue) { |
|
1735 let targetArea = this._getCustomizableParent(aDragOverNode); |
|
1736 let draggedWrapper = this.document.getElementById("wrapper-" + aDraggedItem.id); |
|
1737 let originArea = this._getCustomizableParent(draggedWrapper); |
|
1738 let positionManager = DragPositionManager.getManagerForArea(targetArea); |
|
1739 let draggedSize = this._getDragItemSize(aDragOverNode, aDraggedItem); |
|
1740 let isWide = aDraggedItem.classList.contains(CustomizableUI.WIDE_PANEL_CLASS); |
|
1741 positionManager.insertPlaceholder(targetArea, aDragOverNode, isWide, draggedSize, |
|
1742 originArea == targetArea); |
|
1743 }, |
|
1744 |
|
1745 _getDragItemSize: function(aDragOverNode, aDraggedItem) { |
|
1746 // Cache it good, cache it real good. |
|
1747 if (!this._dragSizeMap) |
|
1748 this._dragSizeMap = new WeakMap(); |
|
1749 if (!this._dragSizeMap.has(aDraggedItem)) |
|
1750 this._dragSizeMap.set(aDraggedItem, new WeakMap()); |
|
1751 let itemMap = this._dragSizeMap.get(aDraggedItem); |
|
1752 let targetArea = this._getCustomizableParent(aDragOverNode); |
|
1753 let currentArea = this._getCustomizableParent(aDraggedItem); |
|
1754 // Return the size for this target from cache, if it exists. |
|
1755 let size = itemMap.get(targetArea); |
|
1756 if (size) |
|
1757 return size; |
|
1758 |
|
1759 // Calculate size of the item when it'd be dropped in this position. |
|
1760 let currentParent = aDraggedItem.parentNode; |
|
1761 let currentSibling = aDraggedItem.nextSibling; |
|
1762 const kAreaType = "cui-areatype"; |
|
1763 let areaType, currentType; |
|
1764 |
|
1765 if (targetArea != currentArea) { |
|
1766 // Move the widget temporarily next to the placeholder. |
|
1767 aDragOverNode.parentNode.insertBefore(aDraggedItem, aDragOverNode); |
|
1768 // Update the node's areaType. |
|
1769 areaType = CustomizableUI.getAreaType(targetArea.id); |
|
1770 currentType = aDraggedItem.hasAttribute(kAreaType) && |
|
1771 aDraggedItem.getAttribute(kAreaType); |
|
1772 if (areaType) |
|
1773 aDraggedItem.setAttribute(kAreaType, areaType); |
|
1774 this.wrapToolbarItem(aDraggedItem, areaType || "palette"); |
|
1775 CustomizableUI.onWidgetDrag(aDraggedItem.id, targetArea.id); |
|
1776 } else { |
|
1777 aDraggedItem.parentNode.hidden = false; |
|
1778 } |
|
1779 |
|
1780 // Fetch the new size. |
|
1781 let rect = aDraggedItem.parentNode.getBoundingClientRect(); |
|
1782 size = {width: rect.width, height: rect.height}; |
|
1783 // Cache the found value of size for this target. |
|
1784 itemMap.set(targetArea, size); |
|
1785 |
|
1786 if (targetArea != currentArea) { |
|
1787 this.unwrapToolbarItem(aDraggedItem.parentNode); |
|
1788 // Put the item back into its previous position. |
|
1789 currentParent.insertBefore(aDraggedItem, currentSibling); |
|
1790 // restore the areaType |
|
1791 if (areaType) { |
|
1792 if (currentType === false) |
|
1793 aDraggedItem.removeAttribute(kAreaType); |
|
1794 else |
|
1795 aDraggedItem.setAttribute(kAreaType, currentType); |
|
1796 } |
|
1797 this.createOrUpdateWrapper(aDraggedItem, null, true); |
|
1798 CustomizableUI.onWidgetDrag(aDraggedItem.id); |
|
1799 } else { |
|
1800 aDraggedItem.parentNode.hidden = true; |
|
1801 } |
|
1802 return size; |
|
1803 }, |
|
1804 |
|
1805 _getCustomizableParent: function(aElement) { |
|
1806 let areas = CustomizableUI.areas; |
|
1807 areas.push(kPaletteId); |
|
1808 while (aElement) { |
|
1809 if (areas.indexOf(aElement.id) != -1) { |
|
1810 return aElement; |
|
1811 } |
|
1812 aElement = aElement.parentNode; |
|
1813 } |
|
1814 return null; |
|
1815 }, |
|
1816 |
|
1817 _getDragOverNode: function(aEvent, aAreaElement, aInToolbar, aDraggedItemId) { |
|
1818 let expectedParent = aAreaElement.customizationTarget || aAreaElement; |
|
1819 // Our tests are stupid. Cope: |
|
1820 if (!aEvent.clientX && !aEvent.clientY) { |
|
1821 return aEvent.target; |
|
1822 } |
|
1823 // Offset the drag event's position with the offset to the center of |
|
1824 // the thing we're dragging |
|
1825 let dragX = aEvent.clientX - this._dragOffset.x; |
|
1826 let dragY = aEvent.clientY - this._dragOffset.y; |
|
1827 |
|
1828 // Ensure this is within the container |
|
1829 let boundsContainer = expectedParent; |
|
1830 // NB: because the panel UI itself is inside a scrolling container, we need |
|
1831 // to use the parent bounds (otherwise, if the panel UI is scrolled down, |
|
1832 // the numbers we get are in window coordinates which leads to various kinds |
|
1833 // of weirdness) |
|
1834 if (boundsContainer == this.panelUIContents) { |
|
1835 boundsContainer = boundsContainer.parentNode; |
|
1836 } |
|
1837 let bounds = boundsContainer.getBoundingClientRect(); |
|
1838 dragX = Math.min(bounds.right, Math.max(dragX, bounds.left)); |
|
1839 dragY = Math.min(bounds.bottom, Math.max(dragY, bounds.top)); |
|
1840 |
|
1841 let targetNode; |
|
1842 if (aInToolbar) { |
|
1843 targetNode = aAreaElement.ownerDocument.elementFromPoint(dragX, dragY); |
|
1844 while (targetNode && targetNode.parentNode != expectedParent) { |
|
1845 targetNode = targetNode.parentNode; |
|
1846 } |
|
1847 } else { |
|
1848 let positionManager = DragPositionManager.getManagerForArea(aAreaElement); |
|
1849 // Make it relative to the container: |
|
1850 dragX -= bounds.left; |
|
1851 // NB: but if we're in the panel UI, we need to use the actual panel |
|
1852 // contents instead of the scrolling container to determine our origin |
|
1853 // offset against: |
|
1854 if (expectedParent == this.panelUIContents) { |
|
1855 dragY -= this.panelUIContents.getBoundingClientRect().top; |
|
1856 } else { |
|
1857 dragY -= bounds.top; |
|
1858 } |
|
1859 // Find the closest node: |
|
1860 targetNode = positionManager.find(aAreaElement, dragX, dragY, aDraggedItemId); |
|
1861 } |
|
1862 return targetNode || aEvent.target; |
|
1863 }, |
|
1864 |
|
1865 _onMouseDown: function(aEvent) { |
|
1866 LOG("_onMouseDown"); |
|
1867 if (aEvent.button != 0) { |
|
1868 return; |
|
1869 } |
|
1870 let doc = aEvent.target.ownerDocument; |
|
1871 doc.documentElement.setAttribute("customizing-movingItem", true); |
|
1872 let item = this._getWrapper(aEvent.target); |
|
1873 if (item && !item.classList.contains(kPlaceholderClass) && |
|
1874 item.getAttribute("removable") == "true") { |
|
1875 item.setAttribute("mousedown", "true"); |
|
1876 } |
|
1877 }, |
|
1878 |
|
1879 _onMouseUp: function(aEvent) { |
|
1880 LOG("_onMouseUp"); |
|
1881 if (aEvent.button != 0) { |
|
1882 return; |
|
1883 } |
|
1884 let doc = aEvent.target.ownerDocument; |
|
1885 doc.documentElement.removeAttribute("customizing-movingItem"); |
|
1886 let item = this._getWrapper(aEvent.target); |
|
1887 if (item) { |
|
1888 item.removeAttribute("mousedown"); |
|
1889 } |
|
1890 }, |
|
1891 |
|
1892 _getWrapper: function(aElement) { |
|
1893 while (aElement && aElement.localName != "toolbarpaletteitem") { |
|
1894 if (aElement.localName == "toolbar") |
|
1895 return null; |
|
1896 aElement = aElement.parentNode; |
|
1897 } |
|
1898 return aElement; |
|
1899 }, |
|
1900 |
|
1901 _showPanelCustomizationPlaceholders: function() { |
|
1902 let doc = this.document; |
|
1903 let contents = this.panelUIContents; |
|
1904 let narrowItemsAfterWideItem = 0; |
|
1905 let node = contents.lastChild; |
|
1906 while (node && !node.classList.contains(CustomizableUI.WIDE_PANEL_CLASS) && |
|
1907 (!node.firstChild || !node.firstChild.classList.contains(CustomizableUI.WIDE_PANEL_CLASS))) { |
|
1908 if (!node.hidden && !node.classList.contains(kPlaceholderClass)) { |
|
1909 narrowItemsAfterWideItem++; |
|
1910 } |
|
1911 node = node.previousSibling; |
|
1912 } |
|
1913 |
|
1914 let orphanedItems = narrowItemsAfterWideItem % CustomizableUI.PANEL_COLUMN_COUNT; |
|
1915 let placeholders = CustomizableUI.PANEL_COLUMN_COUNT - orphanedItems; |
|
1916 |
|
1917 let currentPlaceholderCount = contents.querySelectorAll("." + kPlaceholderClass).length; |
|
1918 if (placeholders > currentPlaceholderCount) { |
|
1919 while (placeholders-- > currentPlaceholderCount) { |
|
1920 let placeholder = doc.createElement("toolbarpaletteitem"); |
|
1921 placeholder.classList.add(kPlaceholderClass); |
|
1922 //XXXjaws The toolbarbutton child here is only necessary to get |
|
1923 // the styling right here. |
|
1924 let placeholderChild = doc.createElement("toolbarbutton"); |
|
1925 placeholderChild.classList.add(kPlaceholderClass + "-child"); |
|
1926 placeholder.appendChild(placeholderChild); |
|
1927 contents.appendChild(placeholder); |
|
1928 } |
|
1929 } else if (placeholders < currentPlaceholderCount) { |
|
1930 while (placeholders++ < currentPlaceholderCount) { |
|
1931 contents.querySelectorAll("." + kPlaceholderClass)[0].remove(); |
|
1932 } |
|
1933 } |
|
1934 }, |
|
1935 |
|
1936 _removePanelCustomizationPlaceholders: function() { |
|
1937 let contents = this.panelUIContents; |
|
1938 let oldPlaceholders = contents.getElementsByClassName(kPlaceholderClass); |
|
1939 while (oldPlaceholders.length) { |
|
1940 contents.removeChild(oldPlaceholders[0]); |
|
1941 } |
|
1942 }, |
|
1943 |
|
1944 /** |
|
1945 * Update toolbar customization targets during drag events to add or remove |
|
1946 * outlines to indicate that an area is customizable. |
|
1947 * |
|
1948 * @param aWindow The XUL window in which outlines should be updated. |
|
1949 * @param {Element} [aToolbarArea=null] The element of the customizable toolbar area to add the |
|
1950 * outline to. If aToolbarArea is falsy, the outline will be |
|
1951 * removed from all toolbar areas. |
|
1952 */ |
|
1953 _updateToolbarCustomizationOutline: function(aWindow, aToolbarArea = null) { |
|
1954 // Remove the attribute from existing customization targets |
|
1955 for (let area of CustomizableUI.areas) { |
|
1956 if (CustomizableUI.getAreaType(area) != CustomizableUI.TYPE_TOOLBAR) { |
|
1957 continue; |
|
1958 } |
|
1959 let target = CustomizableUI.getCustomizeTargetForArea(area, aWindow); |
|
1960 target.removeAttribute("customizing-dragovertarget"); |
|
1961 } |
|
1962 |
|
1963 // Now set the attribute on the desired target |
|
1964 if (aToolbarArea) { |
|
1965 if (CustomizableUI.getAreaType(aToolbarArea.id) != CustomizableUI.TYPE_TOOLBAR) |
|
1966 return; |
|
1967 let target = CustomizableUI.getCustomizeTargetForArea(aToolbarArea.id, aWindow); |
|
1968 target.setAttribute("customizing-dragovertarget", true); |
|
1969 } |
|
1970 }, |
|
1971 |
|
1972 _findVisiblePreviousSiblingNode: function(aReferenceNode) { |
|
1973 while (aReferenceNode && |
|
1974 aReferenceNode.localName == "toolbarpaletteitem" && |
|
1975 aReferenceNode.firstChild.hidden) { |
|
1976 aReferenceNode = aReferenceNode.previousSibling; |
|
1977 } |
|
1978 return aReferenceNode; |
|
1979 }, |
|
1980 }; |
|
1981 |
|
1982 function __dumpDragData(aEvent, caller) { |
|
1983 if (!gDebug) { |
|
1984 return; |
|
1985 } |
|
1986 let str = "Dumping drag data (" + (caller ? caller + " in " : "") + "CustomizeMode.jsm) {\n"; |
|
1987 str += " type: " + aEvent["type"] + "\n"; |
|
1988 for (let el of ["target", "currentTarget", "relatedTarget"]) { |
|
1989 if (aEvent[el]) { |
|
1990 str += " " + el + ": " + aEvent[el] + "(localName=" + aEvent[el].localName + "; id=" + aEvent[el].id + ")\n"; |
|
1991 } |
|
1992 } |
|
1993 for (let prop in aEvent.dataTransfer) { |
|
1994 if (typeof aEvent.dataTransfer[prop] != "function") { |
|
1995 str += " dataTransfer[" + prop + "]: " + aEvent.dataTransfer[prop] + "\n"; |
|
1996 } |
|
1997 } |
|
1998 str += "}"; |
|
1999 LOG(str); |
|
2000 } |
|
2001 |
|
2002 function dispatchFunction(aFunc) { |
|
2003 Services.tm.currentThread.dispatch(aFunc, Ci.nsIThread.DISPATCH_NORMAL); |
|
2004 } |