1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/components/customizableui/src/CustomizableUI.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,4036 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +this.EXPORTED_SYMBOLS = ["CustomizableUI"]; 1.11 + 1.12 +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; 1.13 + 1.14 +Cu.import("resource://gre/modules/Services.jsm"); 1.15 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.16 +XPCOMUtils.defineLazyModuleGetter(this, "PanelWideWidgetTracker", 1.17 + "resource:///modules/PanelWideWidgetTracker.jsm"); 1.18 +XPCOMUtils.defineLazyModuleGetter(this, "CustomizableWidgets", 1.19 + "resource:///modules/CustomizableWidgets.jsm"); 1.20 +XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask", 1.21 + "resource://gre/modules/DeferredTask.jsm"); 1.22 +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", 1.23 + "resource://gre/modules/PrivateBrowsingUtils.jsm"); 1.24 +XPCOMUtils.defineLazyModuleGetter(this, "Promise", 1.25 + "resource://gre/modules/Promise.jsm"); 1.26 +XPCOMUtils.defineLazyGetter(this, "gWidgetsBundle", function() { 1.27 + const kUrl = "chrome://browser/locale/customizableui/customizableWidgets.properties"; 1.28 + return Services.strings.createBundle(kUrl); 1.29 +}); 1.30 +XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils", 1.31 + "resource://gre/modules/ShortcutUtils.jsm"); 1.32 +XPCOMUtils.defineLazyServiceGetter(this, "gELS", 1.33 + "@mozilla.org/eventlistenerservice;1", "nsIEventListenerService"); 1.34 + 1.35 +const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; 1.36 + 1.37 +const kSpecialWidgetPfx = "customizableui-special-"; 1.38 + 1.39 +const kPrefCustomizationState = "browser.uiCustomization.state"; 1.40 +const kPrefCustomizationAutoAdd = "browser.uiCustomization.autoAdd"; 1.41 +const kPrefCustomizationDebug = "browser.uiCustomization.debug"; 1.42 +const kPrefDrawInTitlebar = "browser.tabs.drawInTitlebar"; 1.43 + 1.44 +/** 1.45 + * The keys are the handlers that are fired when the event type (the value) 1.46 + * is fired on the subview. A widget that provides a subview has the option 1.47 + * of providing onViewShowing and onViewHiding event handlers. 1.48 + */ 1.49 +const kSubviewEvents = [ 1.50 + "ViewShowing", 1.51 + "ViewHiding" 1.52 +]; 1.53 + 1.54 +/** 1.55 + * gPalette is a map of every widget that CustomizableUI.jsm knows about, keyed 1.56 + * on their IDs. 1.57 + */ 1.58 +let gPalette = new Map(); 1.59 + 1.60 +/** 1.61 + * gAreas maps area IDs to Sets of properties about those areas. An area is a 1.62 + * place where a widget can be put. 1.63 + */ 1.64 +let gAreas = new Map(); 1.65 + 1.66 +/** 1.67 + * gPlacements maps area IDs to Arrays of widget IDs, indicating that the widgets 1.68 + * are placed within that area (either directly in the area node, or in the 1.69 + * customizationTarget of the node). 1.70 + */ 1.71 +let gPlacements = new Map(); 1.72 + 1.73 +/** 1.74 + * gFuturePlacements represent placements that will happen for areas that have 1.75 + * not yet loaded (due to lazy-loading). This can occur when add-ons register 1.76 + * widgets. 1.77 + */ 1.78 +let gFuturePlacements = new Map(); 1.79 + 1.80 +//XXXunf Temporary. Need a nice way to abstract functions to build widgets 1.81 +// of these types. 1.82 +let gSupportedWidgetTypes = new Set(["button", "view", "custom"]); 1.83 + 1.84 +/** 1.85 + * gPanelsForWindow is a list of known panels in a window which we may need to close 1.86 + * should command events fire which target them. 1.87 + */ 1.88 +let gPanelsForWindow = new WeakMap(); 1.89 + 1.90 +/** 1.91 + * gSeenWidgets remembers which widgets the user has seen for the first time 1.92 + * before. This way, if a new widget is created, and the user has not seen it 1.93 + * before, it can be put in its default location. Otherwise, it remains in the 1.94 + * palette. 1.95 + */ 1.96 +let gSeenWidgets = new Set(); 1.97 + 1.98 +/** 1.99 + * gDirtyAreaCache is a set of area IDs for areas where items have been added, 1.100 + * moved or removed at least once. This set is persisted, and is used to 1.101 + * optimize building of toolbars in the default case where no toolbars should 1.102 + * be "dirty". 1.103 + */ 1.104 +let gDirtyAreaCache = new Set(); 1.105 + 1.106 +/** 1.107 + * gPendingBuildAreas is a map from area IDs to map from build nodes to their 1.108 + * existing children at the time of node registration, that are waiting 1.109 + * for the area to be registered 1.110 + */ 1.111 +let gPendingBuildAreas = new Map(); 1.112 + 1.113 +let gSavedState = null; 1.114 +let gRestoring = false; 1.115 +let gDirty = false; 1.116 +let gInBatchStack = 0; 1.117 +let gResetting = false; 1.118 +let gUndoResetting = false; 1.119 + 1.120 +/** 1.121 + * gBuildAreas maps area IDs to actual area nodes within browser windows. 1.122 + */ 1.123 +let gBuildAreas = new Map(); 1.124 + 1.125 +/** 1.126 + * gBuildWindows is a map of windows that have registered build areas, mapped 1.127 + * to a Set of known toolboxes in that window. 1.128 + */ 1.129 +let gBuildWindows = new Map(); 1.130 + 1.131 +let gNewElementCount = 0; 1.132 +let gGroupWrapperCache = new Map(); 1.133 +let gSingleWrapperCache = new WeakMap(); 1.134 +let gListeners = new Set(); 1.135 + 1.136 +let gUIStateBeforeReset = { 1.137 + uiCustomizationState: null, 1.138 + drawInTitlebar: null, 1.139 +}; 1.140 + 1.141 +let gModuleName = "[CustomizableUI]"; 1.142 +#include logging.js 1.143 + 1.144 +let CustomizableUIInternal = { 1.145 + initialize: function() { 1.146 + LOG("Initializing"); 1.147 + 1.148 + this.addListener(this); 1.149 + this._defineBuiltInWidgets(); 1.150 + this.loadSavedState(); 1.151 + 1.152 + let panelPlacements = [ 1.153 + "edit-controls", 1.154 + "zoom-controls", 1.155 + "new-window-button", 1.156 + "privatebrowsing-button", 1.157 + "save-page-button", 1.158 + "print-button", 1.159 + "history-panelmenu", 1.160 + "fullscreen-button", 1.161 + "find-button", 1.162 + "preferences-button", 1.163 + "add-ons-button", 1.164 + "developer-button", 1.165 + ]; 1.166 + 1.167 + if (gPalette.has("switch-to-metro-button")) { 1.168 + panelPlacements.push("switch-to-metro-button"); 1.169 + } 1.170 + 1.171 +#ifdef NIGHTLY_BUILD 1.172 + if (gPalette.has("e10s-button")) { 1.173 + let newWindowIndex = panelPlacements.indexOf("new-window-button"); 1.174 + if (newWindowIndex > -1) { 1.175 + panelPlacements.splice(newWindowIndex + 1, 0, "e10s-button"); 1.176 + } 1.177 + } 1.178 +#endif 1.179 + 1.180 + let showCharacterEncoding = Services.prefs.getComplexValue( 1.181 + "browser.menu.showCharacterEncoding", 1.182 + Ci.nsIPrefLocalizedString 1.183 + ).data; 1.184 + if (showCharacterEncoding == "true") { 1.185 + panelPlacements.push("characterencoding-button"); 1.186 + } 1.187 + 1.188 + this.registerArea(CustomizableUI.AREA_PANEL, { 1.189 + anchor: "PanelUI-menu-button", 1.190 + type: CustomizableUI.TYPE_MENU_PANEL, 1.191 + defaultPlacements: panelPlacements 1.192 + }, true); 1.193 + PanelWideWidgetTracker.init(); 1.194 + 1.195 + this.registerArea(CustomizableUI.AREA_NAVBAR, { 1.196 + legacy: true, 1.197 + type: CustomizableUI.TYPE_TOOLBAR, 1.198 + overflowable: true, 1.199 + defaultPlacements: [ 1.200 + "urlbar-container", 1.201 + "search-container", 1.202 + "webrtc-status-button", 1.203 + "bookmarks-menu-button", 1.204 + "downloads-button", 1.205 + "home-button", 1.206 + "social-share-button", 1.207 + ], 1.208 + defaultCollapsed: false, 1.209 + }, true); 1.210 +#ifndef XP_MACOSX 1.211 + this.registerArea(CustomizableUI.AREA_MENUBAR, { 1.212 + legacy: true, 1.213 + type: CustomizableUI.TYPE_TOOLBAR, 1.214 + defaultPlacements: [ 1.215 + "menubar-items", 1.216 + ], 1.217 + get defaultCollapsed() { 1.218 +#ifdef MENUBAR_CAN_AUTOHIDE 1.219 +#if defined(MOZ_WIDGET_GTK) || defined(MOZ_WIDGET_QT) 1.220 + return true; 1.221 +#else 1.222 + // This is duplicated logic from /browser/base/jar.mn 1.223 + // for win6BrowserOverlay.xul. 1.224 + return Services.appinfo.OS == "WINNT" && 1.225 + Services.sysinfo.getProperty("version") != "5.1"; 1.226 +#endif 1.227 +#endif 1.228 + return false; 1.229 + } 1.230 + }, true); 1.231 +#endif 1.232 + this.registerArea(CustomizableUI.AREA_TABSTRIP, { 1.233 + legacy: true, 1.234 + type: CustomizableUI.TYPE_TOOLBAR, 1.235 + defaultPlacements: [ 1.236 + "tabbrowser-tabs", 1.237 + "new-tab-button", 1.238 + "alltabs-button", 1.239 + ], 1.240 + defaultCollapsed: null, 1.241 + }, true); 1.242 + this.registerArea(CustomizableUI.AREA_BOOKMARKS, { 1.243 + legacy: true, 1.244 + type: CustomizableUI.TYPE_TOOLBAR, 1.245 + defaultPlacements: [ 1.246 + "personal-bookmarks", 1.247 + ], 1.248 + defaultCollapsed: true, 1.249 + }, true); 1.250 + 1.251 + this.registerArea(CustomizableUI.AREA_ADDONBAR, { 1.252 + type: CustomizableUI.TYPE_TOOLBAR, 1.253 + legacy: true, 1.254 + defaultPlacements: ["addonbar-closebutton", "status-bar"], 1.255 + defaultCollapsed: false, 1.256 + }, true); 1.257 + }, 1.258 + 1.259 + get _builtinToolbars() { 1.260 + return new Set([ 1.261 + CustomizableUI.AREA_NAVBAR, 1.262 + CustomizableUI.AREA_BOOKMARKS, 1.263 + CustomizableUI.AREA_TABSTRIP, 1.264 + CustomizableUI.AREA_ADDONBAR, 1.265 +#ifndef XP_MACOSX 1.266 + CustomizableUI.AREA_MENUBAR, 1.267 +#endif 1.268 + ]); 1.269 + }, 1.270 + 1.271 + _defineBuiltInWidgets: function() { 1.272 + //XXXunf Need to figure out how to auto-add new builtin widgets in new 1.273 + // app versions to already customized areas. 1.274 + for (let widgetDefinition of CustomizableWidgets) { 1.275 + this.createBuiltinWidget(widgetDefinition); 1.276 + } 1.277 + }, 1.278 + 1.279 + wrapWidget: function(aWidgetId) { 1.280 + if (gGroupWrapperCache.has(aWidgetId)) { 1.281 + return gGroupWrapperCache.get(aWidgetId); 1.282 + } 1.283 + 1.284 + let provider = this.getWidgetProvider(aWidgetId); 1.285 + if (!provider) { 1.286 + return null; 1.287 + } 1.288 + 1.289 + if (provider == CustomizableUI.PROVIDER_API) { 1.290 + let widget = gPalette.get(aWidgetId); 1.291 + if (!widget.wrapper) { 1.292 + widget.wrapper = new WidgetGroupWrapper(widget); 1.293 + gGroupWrapperCache.set(aWidgetId, widget.wrapper); 1.294 + } 1.295 + return widget.wrapper; 1.296 + } 1.297 + 1.298 + // PROVIDER_SPECIAL gets treated the same as PROVIDER_XUL. 1.299 + let wrapper = new XULWidgetGroupWrapper(aWidgetId); 1.300 + gGroupWrapperCache.set(aWidgetId, wrapper); 1.301 + return wrapper; 1.302 + }, 1.303 + 1.304 + registerArea: function(aName, aProperties, aInternalCaller) { 1.305 + if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) { 1.306 + throw new Error("Invalid area name"); 1.307 + } 1.308 + 1.309 + let areaIsKnown = gAreas.has(aName); 1.310 + let props = areaIsKnown ? gAreas.get(aName) : new Map(); 1.311 + const kImmutableProperties = new Set(["type", "legacy", "overflowable"]); 1.312 + for (let key in aProperties) { 1.313 + if (areaIsKnown && kImmutableProperties.has(key) && 1.314 + props.get(key) != aProperties[key]) { 1.315 + throw new Error("An area cannot change the property for '" + key + "'"); 1.316 + } 1.317 + //XXXgijs for special items, we need to make sure they have an appropriate ID 1.318 + // so we aren't perpetually in a non-default state: 1.319 + if (key == "defaultPlacements" && Array.isArray(aProperties[key])) { 1.320 + props.set(key, aProperties[key].map(x => this.isSpecialWidget(x) ? this.ensureSpecialWidgetId(x) : x )); 1.321 + } else { 1.322 + props.set(key, aProperties[key]); 1.323 + } 1.324 + } 1.325 + // Default to a toolbar: 1.326 + if (!props.has("type")) { 1.327 + props.set("type", CustomizableUI.TYPE_TOOLBAR); 1.328 + } 1.329 + if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) { 1.330 + // Check aProperties instead of props because this check is only interested 1.331 + // in the passed arguments, not the state of a potentially pre-existing area. 1.332 + if (!aInternalCaller && aProperties["defaultCollapsed"]) { 1.333 + throw new Error("defaultCollapsed is only allowed for default toolbars.") 1.334 + } 1.335 + if (!props.has("defaultCollapsed")) { 1.336 + props.set("defaultCollapsed", true); 1.337 + } 1.338 + } else if (props.has("defaultCollapsed")) { 1.339 + throw new Error("defaultCollapsed only applies for TYPE_TOOLBAR areas."); 1.340 + } 1.341 + // Sanity check type: 1.342 + let allTypes = [CustomizableUI.TYPE_TOOLBAR, CustomizableUI.TYPE_MENU_PANEL]; 1.343 + if (allTypes.indexOf(props.get("type")) == -1) { 1.344 + throw new Error("Invalid area type " + props.get("type")); 1.345 + } 1.346 + 1.347 + // And to no placements: 1.348 + if (!props.has("defaultPlacements")) { 1.349 + props.set("defaultPlacements", []); 1.350 + } 1.351 + // Sanity check default placements array: 1.352 + if (!Array.isArray(props.get("defaultPlacements"))) { 1.353 + throw new Error("Should provide an array of default placements"); 1.354 + } 1.355 + 1.356 + if (!areaIsKnown) { 1.357 + gAreas.set(aName, props); 1.358 + 1.359 + if (props.get("legacy") && !gPlacements.has(aName)) { 1.360 + // Guarantee this area exists in gFuturePlacements, to avoid checking it in 1.361 + // various places elsewhere. 1.362 + gFuturePlacements.set(aName, new Set()); 1.363 + } else { 1.364 + this.restoreStateForArea(aName); 1.365 + } 1.366 + 1.367 + // If we have pending build area nodes, register all of them 1.368 + if (gPendingBuildAreas.has(aName)) { 1.369 + let pendingNodes = gPendingBuildAreas.get(aName); 1.370 + for (let [pendingNode, existingChildren] of pendingNodes) { 1.371 + this.registerToolbarNode(pendingNode, existingChildren); 1.372 + } 1.373 + gPendingBuildAreas.delete(aName); 1.374 + } 1.375 + } 1.376 + }, 1.377 + 1.378 + unregisterArea: function(aName, aDestroyPlacements) { 1.379 + if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) { 1.380 + throw new Error("Invalid area name"); 1.381 + } 1.382 + if (!gAreas.has(aName) && !gPlacements.has(aName)) { 1.383 + throw new Error("Area not registered"); 1.384 + } 1.385 + 1.386 + // Move all the widgets out 1.387 + this.beginBatchUpdate(); 1.388 + try { 1.389 + let placements = gPlacements.get(aName); 1.390 + if (placements) { 1.391 + // Need to clone this array so removeWidgetFromArea doesn't modify it 1.392 + placements = [...placements]; 1.393 + placements.forEach(this.removeWidgetFromArea, this); 1.394 + } 1.395 + 1.396 + // Delete all remaining traces. 1.397 + gAreas.delete(aName); 1.398 + // Only destroy placements when necessary: 1.399 + if (aDestroyPlacements) { 1.400 + gPlacements.delete(aName); 1.401 + } else { 1.402 + // Otherwise we need to re-set them, as removeFromArea will have emptied 1.403 + // them out: 1.404 + gPlacements.set(aName, placements); 1.405 + } 1.406 + gFuturePlacements.delete(aName); 1.407 + let existingAreaNodes = gBuildAreas.get(aName); 1.408 + if (existingAreaNodes) { 1.409 + for (let areaNode of existingAreaNodes) { 1.410 + this.notifyListeners("onAreaNodeUnregistered", aName, areaNode.customizationTarget, 1.411 + CustomizableUI.REASON_AREA_UNREGISTERED); 1.412 + } 1.413 + } 1.414 + gBuildAreas.delete(aName); 1.415 + } finally { 1.416 + this.endBatchUpdate(true); 1.417 + } 1.418 + }, 1.419 + 1.420 + registerToolbarNode: function(aToolbar, aExistingChildren) { 1.421 + let area = aToolbar.id; 1.422 + if (gBuildAreas.has(area) && gBuildAreas.get(area).has(aToolbar)) { 1.423 + return; 1.424 + } 1.425 + let document = aToolbar.ownerDocument; 1.426 + let areaProperties = gAreas.get(area); 1.427 + 1.428 + // If this area is not registered, try to do it automatically: 1.429 + if (!areaProperties) { 1.430 + // If there's no defaultset attribute and this isn't a legacy extra toolbar, 1.431 + // we assume that we should wait for registerArea to be called: 1.432 + if (!aToolbar.hasAttribute("defaultset") && 1.433 + !aToolbar.hasAttribute("customindex")) { 1.434 + if (!gPendingBuildAreas.has(area)) { 1.435 + gPendingBuildAreas.set(area, new Map()); 1.436 + } 1.437 + let pendingNodes = gPendingBuildAreas.get(area); 1.438 + pendingNodes.set(aToolbar, aExistingChildren); 1.439 + return; 1.440 + } 1.441 + let props = {type: CustomizableUI.TYPE_TOOLBAR, legacy: true}; 1.442 + let defaultsetAttribute = aToolbar.getAttribute("defaultset") || ""; 1.443 + props.defaultPlacements = defaultsetAttribute.split(',').filter(s => s); 1.444 + this.registerArea(area, props); 1.445 + areaProperties = gAreas.get(area); 1.446 + } 1.447 + 1.448 + this.beginBatchUpdate(); 1.449 + try { 1.450 + let placements = gPlacements.get(area); 1.451 + if (!placements && areaProperties.has("legacy")) { 1.452 + let legacyState = aToolbar.getAttribute("currentset"); 1.453 + if (legacyState) { 1.454 + legacyState = legacyState.split(",").filter(s => s); 1.455 + } 1.456 + 1.457 + // Manually restore the state here, so the legacy state can be converted. 1.458 + this.restoreStateForArea(area, legacyState); 1.459 + placements = gPlacements.get(area); 1.460 + } 1.461 + 1.462 + // Check that the current children and the current placements match. If 1.463 + // not, mark it as dirty: 1.464 + if (aExistingChildren.length != placements.length || 1.465 + aExistingChildren.every((id, i) => id == placements[i])) { 1.466 + gDirtyAreaCache.add(area); 1.467 + } 1.468 + 1.469 + if (areaProperties.has("overflowable")) { 1.470 + aToolbar.overflowable = new OverflowableToolbar(aToolbar); 1.471 + } 1.472 + 1.473 + this.registerBuildArea(area, aToolbar); 1.474 + 1.475 + // We only build the toolbar if it's been marked as "dirty". Dirty means 1.476 + // one of the following things: 1.477 + // 1) Items have been added, moved or removed from this toolbar before. 1.478 + // 2) The number of children of the toolbar does not match the length of 1.479 + // the placements array for that area. 1.480 + // 1.481 + // This notion of being "dirty" is stored in a cache which is persisted 1.482 + // in the saved state. 1.483 + if (gDirtyAreaCache.has(area)) { 1.484 + this.buildArea(area, placements, aToolbar); 1.485 + } 1.486 + this.notifyListeners("onAreaNodeRegistered", area, aToolbar.customizationTarget); 1.487 + aToolbar.setAttribute("currentset", placements.join(",")); 1.488 + } finally { 1.489 + this.endBatchUpdate(); 1.490 + } 1.491 + }, 1.492 + 1.493 + buildArea: function(aArea, aPlacements, aAreaNode) { 1.494 + let document = aAreaNode.ownerDocument; 1.495 + let window = document.defaultView; 1.496 + let inPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(window); 1.497 + let container = aAreaNode.customizationTarget; 1.498 + let areaIsPanel = gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL; 1.499 + 1.500 + if (!container) { 1.501 + throw new Error("Expected area " + aArea 1.502 + + " to have a customizationTarget attribute."); 1.503 + } 1.504 + 1.505 + // Restore nav-bar visibility since it may have been hidden 1.506 + // through a migration path (bug 938980) or an add-on. 1.507 + if (aArea == CustomizableUI.AREA_NAVBAR) { 1.508 + aAreaNode.collapsed = false; 1.509 + } 1.510 + 1.511 + this.beginBatchUpdate(); 1.512 + 1.513 + try { 1.514 + let currentNode = container.firstChild; 1.515 + let placementsToRemove = new Set(); 1.516 + for (let id of aPlacements) { 1.517 + while (currentNode && currentNode.getAttribute("skipintoolbarset") == "true") { 1.518 + currentNode = currentNode.nextSibling; 1.519 + } 1.520 + 1.521 + if (currentNode && currentNode.id == id) { 1.522 + currentNode = currentNode.nextSibling; 1.523 + continue; 1.524 + } 1.525 + 1.526 + if (this.isSpecialWidget(id) && areaIsPanel) { 1.527 + placementsToRemove.add(id); 1.528 + continue; 1.529 + } 1.530 + 1.531 + let [provider, node] = this.getWidgetNode(id, window); 1.532 + if (!node) { 1.533 + LOG("Unknown widget: " + id); 1.534 + continue; 1.535 + } 1.536 + 1.537 + // If the placements have items in them which are (now) no longer removable, 1.538 + // we shouldn't be moving them: 1.539 + if (provider == CustomizableUI.PROVIDER_API) { 1.540 + let widgetInfo = gPalette.get(id); 1.541 + if (!widgetInfo.removable && aArea != widgetInfo.defaultArea) { 1.542 + placementsToRemove.add(id); 1.543 + continue; 1.544 + } 1.545 + } else if (provider == CustomizableUI.PROVIDER_XUL && 1.546 + node.parentNode != container && !this.isWidgetRemovable(node)) { 1.547 + placementsToRemove.add(id); 1.548 + continue; 1.549 + } // Special widgets are always removable, so no need to check them 1.550 + 1.551 + if (inPrivateWindow && provider == CustomizableUI.PROVIDER_API) { 1.552 + let widget = gPalette.get(id); 1.553 + if (!widget.showInPrivateBrowsing && inPrivateWindow) { 1.554 + continue; 1.555 + } 1.556 + } 1.557 + 1.558 + this.ensureButtonContextMenu(node, aAreaNode); 1.559 + if (node.localName == "toolbarbutton") { 1.560 + if (areaIsPanel) { 1.561 + node.setAttribute("wrap", "true"); 1.562 + } else { 1.563 + node.removeAttribute("wrap"); 1.564 + } 1.565 + } 1.566 + 1.567 + this.insertWidgetBefore(node, currentNode, container, aArea); 1.568 + if (gResetting) { 1.569 + this.notifyListeners("onWidgetReset", node, container); 1.570 + } else if (gUndoResetting) { 1.571 + this.notifyListeners("onWidgetUndoMove", node, container); 1.572 + } 1.573 + } 1.574 + 1.575 + if (currentNode) { 1.576 + let palette = aAreaNode.toolbox ? aAreaNode.toolbox.palette : null; 1.577 + let limit = currentNode.previousSibling; 1.578 + let node = container.lastChild; 1.579 + while (node && node != limit) { 1.580 + let previousSibling = node.previousSibling; 1.581 + // Nodes opt-in to removability. If they're removable, and we haven't 1.582 + // seen them in the placements array, then we toss them into the palette 1.583 + // if one exists. If no palette exists, we just remove the node. If the 1.584 + // node is not removable, we leave it where it is. However, we can only 1.585 + // safely touch elements that have an ID - both because we depend on 1.586 + // IDs, and because such elements are not intended to be widgets 1.587 + // (eg, titlebar-placeholder elements). 1.588 + if (node.id && node.getAttribute("skipintoolbarset") != "true") { 1.589 + if (this.isWidgetRemovable(node)) { 1.590 + if (palette && !this.isSpecialWidget(node.id)) { 1.591 + palette.appendChild(node); 1.592 + this.removeLocationAttributes(node); 1.593 + } else { 1.594 + container.removeChild(node); 1.595 + } 1.596 + } else { 1.597 + this.setLocationAttributes(currentNode, aArea); 1.598 + node.setAttribute("removable", false); 1.599 + LOG("Adding non-removable widget to placements of " + aArea + ": " + 1.600 + node.id); 1.601 + gPlacements.get(aArea).push(node.id); 1.602 + gDirty = true; 1.603 + } 1.604 + } 1.605 + node = previousSibling; 1.606 + } 1.607 + } 1.608 + 1.609 + // If there are placements in here which aren't removable from their original area, 1.610 + // we remove them from this area's placement array. They will (have) be(en) added 1.611 + // to their original area's placements array in the block above this one. 1.612 + if (placementsToRemove.size) { 1.613 + let placementAry = gPlacements.get(aArea); 1.614 + for (let id of placementsToRemove) { 1.615 + let index = placementAry.indexOf(id); 1.616 + placementAry.splice(index, 1); 1.617 + } 1.618 + } 1.619 + 1.620 + if (gResetting) { 1.621 + this.notifyListeners("onAreaReset", aArea, container); 1.622 + } 1.623 + } finally { 1.624 + this.endBatchUpdate(); 1.625 + } 1.626 + }, 1.627 + 1.628 + addPanelCloseListeners: function(aPanel) { 1.629 + gELS.addSystemEventListener(aPanel, "click", this, false); 1.630 + gELS.addSystemEventListener(aPanel, "keypress", this, false); 1.631 + let win = aPanel.ownerDocument.defaultView; 1.632 + if (!gPanelsForWindow.has(win)) { 1.633 + gPanelsForWindow.set(win, new Set()); 1.634 + } 1.635 + gPanelsForWindow.get(win).add(this._getPanelForNode(aPanel)); 1.636 + }, 1.637 + 1.638 + removePanelCloseListeners: function(aPanel) { 1.639 + gELS.removeSystemEventListener(aPanel, "click", this, false); 1.640 + gELS.removeSystemEventListener(aPanel, "keypress", this, false); 1.641 + let win = aPanel.ownerDocument.defaultView; 1.642 + let panels = gPanelsForWindow.get(win); 1.643 + if (panels) { 1.644 + panels.delete(this._getPanelForNode(aPanel)); 1.645 + } 1.646 + }, 1.647 + 1.648 + ensureButtonContextMenu: function(aNode, aAreaNode) { 1.649 + const kPanelItemContextMenu = "customizationPanelItemContextMenu"; 1.650 + 1.651 + let currentContextMenu = aNode.getAttribute("context") || 1.652 + aNode.getAttribute("contextmenu"); 1.653 + let place = CustomizableUI.getPlaceForItem(aAreaNode); 1.654 + let contextMenuForPlace = place == "panel" ? 1.655 + kPanelItemContextMenu : 1.656 + null; 1.657 + if (contextMenuForPlace && !currentContextMenu) { 1.658 + aNode.setAttribute("context", contextMenuForPlace); 1.659 + } else if (currentContextMenu == kPanelItemContextMenu && 1.660 + contextMenuForPlace != kPanelItemContextMenu) { 1.661 + aNode.removeAttribute("context"); 1.662 + aNode.removeAttribute("contextmenu"); 1.663 + } 1.664 + }, 1.665 + 1.666 + getWidgetProvider: function(aWidgetId) { 1.667 + if (this.isSpecialWidget(aWidgetId)) { 1.668 + return CustomizableUI.PROVIDER_SPECIAL; 1.669 + } 1.670 + if (gPalette.has(aWidgetId)) { 1.671 + return CustomizableUI.PROVIDER_API; 1.672 + } 1.673 + // If this was an API widget that was destroyed, return null: 1.674 + if (gSeenWidgets.has(aWidgetId)) { 1.675 + return null; 1.676 + } 1.677 + 1.678 + // We fall back to the XUL provider, but we don't know for sure (at this 1.679 + // point) whether it exists there either. So the API is technically lying. 1.680 + // Ideally, it would be able to return an error value (or throw an 1.681 + // exception) if it really didn't exist. Our code calling this function 1.682 + // handles that fine, but this is a public API. 1.683 + return CustomizableUI.PROVIDER_XUL; 1.684 + }, 1.685 + 1.686 + getWidgetNode: function(aWidgetId, aWindow) { 1.687 + let document = aWindow.document; 1.688 + 1.689 + if (this.isSpecialWidget(aWidgetId)) { 1.690 + let widgetNode = document.getElementById(aWidgetId) || 1.691 + this.createSpecialWidget(aWidgetId, document); 1.692 + return [ CustomizableUI.PROVIDER_SPECIAL, widgetNode]; 1.693 + } 1.694 + 1.695 + let widget = gPalette.get(aWidgetId); 1.696 + if (widget) { 1.697 + // If we have an instance of this widget already, just use that. 1.698 + if (widget.instances.has(document)) { 1.699 + LOG("An instance of widget " + aWidgetId + " already exists in this " 1.700 + + "document. Reusing."); 1.701 + return [ CustomizableUI.PROVIDER_API, 1.702 + widget.instances.get(document) ]; 1.703 + } 1.704 + 1.705 + return [ CustomizableUI.PROVIDER_API, 1.706 + this.buildWidget(document, widget) ]; 1.707 + } 1.708 + 1.709 + LOG("Searching for " + aWidgetId + " in toolbox."); 1.710 + let node = this.findWidgetInWindow(aWidgetId, aWindow); 1.711 + if (node) { 1.712 + return [ CustomizableUI.PROVIDER_XUL, node ]; 1.713 + } 1.714 + 1.715 + LOG("No node for " + aWidgetId + " found."); 1.716 + return [null, null]; 1.717 + }, 1.718 + 1.719 + registerMenuPanel: function(aPanelContents) { 1.720 + if (gBuildAreas.has(CustomizableUI.AREA_PANEL) && 1.721 + gBuildAreas.get(CustomizableUI.AREA_PANEL).has(aPanelContents)) { 1.722 + return; 1.723 + } 1.724 + 1.725 + let document = aPanelContents.ownerDocument; 1.726 + 1.727 + aPanelContents.toolbox = document.getElementById("navigator-toolbox"); 1.728 + aPanelContents.customizationTarget = aPanelContents; 1.729 + 1.730 + this.addPanelCloseListeners(this._getPanelForNode(aPanelContents)); 1.731 + 1.732 + let placements = gPlacements.get(CustomizableUI.AREA_PANEL); 1.733 + this.buildArea(CustomizableUI.AREA_PANEL, placements, aPanelContents); 1.734 + this.notifyListeners("onAreaNodeRegistered", CustomizableUI.AREA_PANEL, aPanelContents); 1.735 + 1.736 + for (let child of aPanelContents.children) { 1.737 + if (child.localName != "toolbarbutton") { 1.738 + if (child.localName == "toolbaritem") { 1.739 + this.ensureButtonContextMenu(child, aPanelContents); 1.740 + } 1.741 + continue; 1.742 + } 1.743 + this.ensureButtonContextMenu(child, aPanelContents); 1.744 + child.setAttribute("wrap", "true"); 1.745 + } 1.746 + 1.747 + this.registerBuildArea(CustomizableUI.AREA_PANEL, aPanelContents); 1.748 + }, 1.749 + 1.750 + onWidgetAdded: function(aWidgetId, aArea, aPosition) { 1.751 + this.insertNode(aWidgetId, aArea, aPosition, true); 1.752 + 1.753 + if (!gResetting) { 1.754 + this._clearPreviousUIState(); 1.755 + } 1.756 + }, 1.757 + 1.758 + onWidgetRemoved: function(aWidgetId, aArea) { 1.759 + let areaNodes = gBuildAreas.get(aArea); 1.760 + if (!areaNodes) { 1.761 + return; 1.762 + } 1.763 + 1.764 + let area = gAreas.get(aArea); 1.765 + let isToolbar = area.get("type") == CustomizableUI.TYPE_TOOLBAR; 1.766 + let isOverflowable = isToolbar && area.get("overflowable"); 1.767 + let showInPrivateBrowsing = gPalette.has(aWidgetId) 1.768 + ? gPalette.get(aWidgetId).showInPrivateBrowsing 1.769 + : true; 1.770 + 1.771 + for (let areaNode of areaNodes) { 1.772 + let window = areaNode.ownerDocument.defaultView; 1.773 + if (!showInPrivateBrowsing && 1.774 + PrivateBrowsingUtils.isWindowPrivate(window)) { 1.775 + continue; 1.776 + } 1.777 + 1.778 + let widgetNode = window.document.getElementById(aWidgetId); 1.779 + if (!widgetNode) { 1.780 + INFO("Widget not found, unable to remove"); 1.781 + continue; 1.782 + } 1.783 + let container = areaNode.customizationTarget; 1.784 + if (isOverflowable) { 1.785 + container = areaNode.overflowable.getContainerFor(widgetNode); 1.786 + } 1.787 + 1.788 + this.notifyListeners("onWidgetBeforeDOMChange", widgetNode, null, container, true); 1.789 + 1.790 + // We remove location attributes here to make sure they're gone too when a 1.791 + // widget is removed from a toolbar to the palette. See bug 930950. 1.792 + this.removeLocationAttributes(widgetNode); 1.793 + // We also need to remove the panel context menu if it's there: 1.794 + this.ensureButtonContextMenu(widgetNode); 1.795 + widgetNode.removeAttribute("wrap"); 1.796 + if (gPalette.has(aWidgetId) || this.isSpecialWidget(aWidgetId)) { 1.797 + container.removeChild(widgetNode); 1.798 + } else { 1.799 + areaNode.toolbox.palette.appendChild(widgetNode); 1.800 + } 1.801 + this.notifyListeners("onWidgetAfterDOMChange", widgetNode, null, container, true); 1.802 + 1.803 + if (isToolbar) { 1.804 + areaNode.setAttribute("currentset", gPlacements.get(aArea).join(',')); 1.805 + } 1.806 + 1.807 + let windowCache = gSingleWrapperCache.get(window); 1.808 + if (windowCache) { 1.809 + windowCache.delete(aWidgetId); 1.810 + } 1.811 + } 1.812 + if (!gResetting) { 1.813 + this._clearPreviousUIState(); 1.814 + } 1.815 + }, 1.816 + 1.817 + onWidgetMoved: function(aWidgetId, aArea, aOldPosition, aNewPosition) { 1.818 + this.insertNode(aWidgetId, aArea, aNewPosition); 1.819 + if (!gResetting) { 1.820 + this._clearPreviousUIState(); 1.821 + } 1.822 + }, 1.823 + 1.824 + onCustomizeEnd: function(aWindow) { 1.825 + this._clearPreviousUIState(); 1.826 + }, 1.827 + 1.828 + registerBuildArea: function(aArea, aNode) { 1.829 + // We ensure that the window is registered to have its customization data 1.830 + // cleaned up when unloading. 1.831 + let window = aNode.ownerDocument.defaultView; 1.832 + if (window.closed) { 1.833 + return; 1.834 + } 1.835 + this.registerBuildWindow(window); 1.836 + 1.837 + // Also register this build area's toolbox. 1.838 + if (aNode.toolbox) { 1.839 + gBuildWindows.get(window).add(aNode.toolbox); 1.840 + } 1.841 + 1.842 + if (!gBuildAreas.has(aArea)) { 1.843 + gBuildAreas.set(aArea, new Set()); 1.844 + } 1.845 + 1.846 + gBuildAreas.get(aArea).add(aNode); 1.847 + 1.848 + // Give a class to all customize targets to be used for styling in Customize Mode 1.849 + let customizableNode = this.getCustomizeTargetForArea(aArea, window); 1.850 + customizableNode.classList.add("customization-target"); 1.851 + }, 1.852 + 1.853 + registerBuildWindow: function(aWindow) { 1.854 + if (!gBuildWindows.has(aWindow)) { 1.855 + gBuildWindows.set(aWindow, new Set()); 1.856 + 1.857 + aWindow.addEventListener("unload", this); 1.858 + aWindow.addEventListener("command", this, true); 1.859 + 1.860 + this.notifyListeners("onWindowOpened", aWindow); 1.861 + } 1.862 + }, 1.863 + 1.864 + unregisterBuildWindow: function(aWindow) { 1.865 + aWindow.removeEventListener("unload", this); 1.866 + aWindow.removeEventListener("command", this, true); 1.867 + gPanelsForWindow.delete(aWindow); 1.868 + gBuildWindows.delete(aWindow); 1.869 + gSingleWrapperCache.delete(aWindow); 1.870 + let document = aWindow.document; 1.871 + 1.872 + for (let [areaId, areaNodes] of gBuildAreas) { 1.873 + let areaProperties = gAreas.get(areaId); 1.874 + for (let node of areaNodes) { 1.875 + if (node.ownerDocument == document) { 1.876 + this.notifyListeners("onAreaNodeUnregistered", areaId, node.customizationTarget, 1.877 + CustomizableUI.REASON_WINDOW_CLOSED); 1.878 + if (areaProperties.has("overflowable")) { 1.879 + node.overflowable.uninit(); 1.880 + node.overflowable = null; 1.881 + } 1.882 + areaNodes.delete(node); 1.883 + } 1.884 + } 1.885 + } 1.886 + 1.887 + for (let [,widget] of gPalette) { 1.888 + widget.instances.delete(document); 1.889 + this.notifyListeners("onWidgetInstanceRemoved", widget.id, document); 1.890 + } 1.891 + 1.892 + for (let [area, areaMap] of gPendingBuildAreas) { 1.893 + let toDelete = []; 1.894 + for (let [areaNode, ] of areaMap) { 1.895 + if (areaNode.ownerDocument == document) { 1.896 + toDelete.push(areaNode); 1.897 + } 1.898 + } 1.899 + for (let areaNode of toDelete) { 1.900 + areaMap.delete(toDelete); 1.901 + } 1.902 + } 1.903 + 1.904 + this.notifyListeners("onWindowClosed", aWindow); 1.905 + }, 1.906 + 1.907 + setLocationAttributes: function(aNode, aArea) { 1.908 + let props = gAreas.get(aArea); 1.909 + if (!props) { 1.910 + throw new Error("Expected area " + aArea + " to have a properties Map " + 1.911 + "associated with it."); 1.912 + } 1.913 + 1.914 + aNode.setAttribute("cui-areatype", props.get("type") || ""); 1.915 + let anchor = props.get("anchor"); 1.916 + if (anchor) { 1.917 + aNode.setAttribute("cui-anchorid", anchor); 1.918 + } else { 1.919 + aNode.removeAttribute("cui-anchorid"); 1.920 + } 1.921 + }, 1.922 + 1.923 + removeLocationAttributes: function(aNode) { 1.924 + aNode.removeAttribute("cui-areatype"); 1.925 + aNode.removeAttribute("cui-anchorid"); 1.926 + }, 1.927 + 1.928 + insertNode: function(aWidgetId, aArea, aPosition, isNew) { 1.929 + let areaNodes = gBuildAreas.get(aArea); 1.930 + if (!areaNodes) { 1.931 + return; 1.932 + } 1.933 + 1.934 + let placements = gPlacements.get(aArea); 1.935 + if (!placements) { 1.936 + ERROR("Could not find any placements for " + aArea + 1.937 + " when moving a widget."); 1.938 + return; 1.939 + } 1.940 + 1.941 + // Go through each of the nodes associated with this area and move the 1.942 + // widget to the requested location. 1.943 + for (let areaNode of areaNodes) { 1.944 + this.insertNodeInWindow(aWidgetId, areaNode, isNew); 1.945 + } 1.946 + }, 1.947 + 1.948 + insertNodeInWindow: function(aWidgetId, aAreaNode, isNew) { 1.949 + let window = aAreaNode.ownerDocument.defaultView; 1.950 + let showInPrivateBrowsing = gPalette.has(aWidgetId) 1.951 + ? gPalette.get(aWidgetId).showInPrivateBrowsing 1.952 + : true; 1.953 + 1.954 + if (!showInPrivateBrowsing && PrivateBrowsingUtils.isWindowPrivate(window)) { 1.955 + return; 1.956 + } 1.957 + 1.958 + let [, widgetNode] = this.getWidgetNode(aWidgetId, window); 1.959 + if (!widgetNode) { 1.960 + ERROR("Widget '" + aWidgetId + "' not found, unable to move"); 1.961 + return; 1.962 + } 1.963 + 1.964 + let areaId = aAreaNode.id; 1.965 + if (isNew) { 1.966 + this.ensureButtonContextMenu(widgetNode, aAreaNode); 1.967 + if (widgetNode.localName == "toolbarbutton" && areaId == CustomizableUI.AREA_PANEL) { 1.968 + widgetNode.setAttribute("wrap", "true"); 1.969 + } 1.970 + } 1.971 + 1.972 + let [insertionContainer, nextNode] = this.findInsertionPoints(widgetNode, aAreaNode); 1.973 + this.insertWidgetBefore(widgetNode, nextNode, insertionContainer, areaId); 1.974 + 1.975 + if (gAreas.get(areaId).get("type") == CustomizableUI.TYPE_TOOLBAR) { 1.976 + aAreaNode.setAttribute("currentset", gPlacements.get(areaId).join(',')); 1.977 + } 1.978 + }, 1.979 + 1.980 + findInsertionPoints: function(aNode, aAreaNode) { 1.981 + let areaId = aAreaNode.id; 1.982 + let props = gAreas.get(areaId); 1.983 + 1.984 + // For overflowable toolbars, rely on them (because the work is more complicated): 1.985 + if (props.get("type") == CustomizableUI.TYPE_TOOLBAR && props.get("overflowable")) { 1.986 + return aAreaNode.overflowable.findOverflowedInsertionPoints(aNode); 1.987 + } 1.988 + 1.989 + let container = aAreaNode.customizationTarget; 1.990 + let placements = gPlacements.get(areaId); 1.991 + let nodeIndex = placements.indexOf(aNode.id); 1.992 + 1.993 + while (++nodeIndex < placements.length) { 1.994 + let nextNodeId = placements[nodeIndex]; 1.995 + let nextNode = container.getElementsByAttribute("id", nextNodeId).item(0); 1.996 + 1.997 + if (nextNode) { 1.998 + return [container, nextNode]; 1.999 + } 1.1000 + } 1.1001 + 1.1002 + return [container, null]; 1.1003 + }, 1.1004 + 1.1005 + insertWidgetBefore: function(aNode, aNextNode, aContainer, aArea) { 1.1006 + this.notifyListeners("onWidgetBeforeDOMChange", aNode, aNextNode, aContainer); 1.1007 + this.setLocationAttributes(aNode, aArea); 1.1008 + aContainer.insertBefore(aNode, aNextNode); 1.1009 + this.notifyListeners("onWidgetAfterDOMChange", aNode, aNextNode, aContainer); 1.1010 + }, 1.1011 + 1.1012 + handleEvent: function(aEvent) { 1.1013 + switch (aEvent.type) { 1.1014 + case "command": 1.1015 + if (!this._originalEventInPanel(aEvent)) { 1.1016 + break; 1.1017 + } 1.1018 + aEvent = aEvent.sourceEvent; 1.1019 + // Fall through 1.1020 + case "click": 1.1021 + case "keypress": 1.1022 + this.maybeAutoHidePanel(aEvent); 1.1023 + break; 1.1024 + case "unload": 1.1025 + this.unregisterBuildWindow(aEvent.currentTarget); 1.1026 + break; 1.1027 + } 1.1028 + }, 1.1029 + 1.1030 + _originalEventInPanel: function(aEvent) { 1.1031 + let e = aEvent.sourceEvent; 1.1032 + if (!e) { 1.1033 + return false; 1.1034 + } 1.1035 + let node = this._getPanelForNode(e.target); 1.1036 + if (!node) { 1.1037 + return false; 1.1038 + } 1.1039 + let win = e.view; 1.1040 + let panels = gPanelsForWindow.get(win); 1.1041 + return !!panels && panels.has(node); 1.1042 + }, 1.1043 + 1.1044 + isSpecialWidget: function(aId) { 1.1045 + return (aId.startsWith(kSpecialWidgetPfx) || 1.1046 + aId.startsWith("separator") || 1.1047 + aId.startsWith("spring") || 1.1048 + aId.startsWith("spacer")); 1.1049 + }, 1.1050 + 1.1051 + ensureSpecialWidgetId: function(aId) { 1.1052 + let nodeType = aId.match(/spring|spacer|separator/)[0]; 1.1053 + // If the ID we were passed isn't a generated one, generate one now: 1.1054 + if (nodeType == aId) { 1.1055 + // Ids are differentiated through a unique count suffix. 1.1056 + return kSpecialWidgetPfx + aId + (++gNewElementCount); 1.1057 + } 1.1058 + return aId; 1.1059 + }, 1.1060 + 1.1061 + createSpecialWidget: function(aId, aDocument) { 1.1062 + let nodeName = "toolbar" + aId.match(/spring|spacer|separator/)[0]; 1.1063 + let node = aDocument.createElementNS(kNSXUL, nodeName); 1.1064 + node.id = this.ensureSpecialWidgetId(aId); 1.1065 + if (nodeName == "toolbarspring") { 1.1066 + node.flex = 1; 1.1067 + } 1.1068 + return node; 1.1069 + }, 1.1070 + 1.1071 + /* Find a XUL-provided widget in a window. Don't try to use this 1.1072 + * for an API-provided widget or a special widget. 1.1073 + */ 1.1074 + findWidgetInWindow: function(aId, aWindow) { 1.1075 + if (!gBuildWindows.has(aWindow)) { 1.1076 + throw new Error("Build window not registered"); 1.1077 + } 1.1078 + 1.1079 + if (!aId) { 1.1080 + ERROR("findWidgetInWindow was passed an empty string."); 1.1081 + return null; 1.1082 + } 1.1083 + 1.1084 + let document = aWindow.document; 1.1085 + 1.1086 + // look for a node with the same id, as the node may be 1.1087 + // in a different toolbar. 1.1088 + let node = document.getElementById(aId); 1.1089 + if (node) { 1.1090 + let parent = node.parentNode; 1.1091 + while (parent && !(parent.customizationTarget || 1.1092 + parent == aWindow.gNavToolbox.palette)) { 1.1093 + parent = parent.parentNode; 1.1094 + } 1.1095 + 1.1096 + if (parent) { 1.1097 + let nodeInArea = node.parentNode.localName == "toolbarpaletteitem" ? 1.1098 + node.parentNode : node; 1.1099 + // Check if we're in a customization target, or in the palette: 1.1100 + if ((parent.customizationTarget == nodeInArea.parentNode && 1.1101 + gBuildWindows.get(aWindow).has(parent.toolbox)) || 1.1102 + aWindow.gNavToolbox.palette == nodeInArea.parentNode) { 1.1103 + // Normalize the removable attribute. For backwards compat, if 1.1104 + // the widget is not located in a toolbox palette then absence 1.1105 + // of the "removable" attribute means it is not removable. 1.1106 + if (!node.hasAttribute("removable")) { 1.1107 + // If we first see this in customization mode, it may be in the 1.1108 + // customization palette instead of the toolbox palette. 1.1109 + node.setAttribute("removable", !parent.customizationTarget); 1.1110 + } 1.1111 + return node; 1.1112 + } 1.1113 + } 1.1114 + } 1.1115 + 1.1116 + let toolboxes = gBuildWindows.get(aWindow); 1.1117 + for (let toolbox of toolboxes) { 1.1118 + if (toolbox.palette) { 1.1119 + // Attempt to locate a node with a matching ID within 1.1120 + // the palette. 1.1121 + let node = toolbox.palette.getElementsByAttribute("id", aId)[0]; 1.1122 + if (node) { 1.1123 + // Normalize the removable attribute. For backwards compat, this 1.1124 + // is optional if the widget is located in the toolbox palette, 1.1125 + // and defaults to *true*, unlike if it was located elsewhere. 1.1126 + if (!node.hasAttribute("removable")) { 1.1127 + node.setAttribute("removable", true); 1.1128 + } 1.1129 + return node; 1.1130 + } 1.1131 + } 1.1132 + } 1.1133 + return null; 1.1134 + }, 1.1135 + 1.1136 + buildWidget: function(aDocument, aWidget) { 1.1137 + if (typeof aWidget == "string") { 1.1138 + aWidget = gPalette.get(aWidget); 1.1139 + } 1.1140 + if (!aWidget) { 1.1141 + throw new Error("buildWidget was passed a non-widget to build."); 1.1142 + } 1.1143 + 1.1144 + LOG("Building " + aWidget.id + " of type " + aWidget.type); 1.1145 + 1.1146 + let node; 1.1147 + if (aWidget.type == "custom") { 1.1148 + if (aWidget.onBuild) { 1.1149 + node = aWidget.onBuild(aDocument); 1.1150 + } 1.1151 + if (!node || !(node instanceof aDocument.defaultView.XULElement)) 1.1152 + ERROR("Custom widget with id " + aWidget.id + " does not return a valid node"); 1.1153 + } 1.1154 + else { 1.1155 + if (aWidget.onBeforeCreated) { 1.1156 + aWidget.onBeforeCreated(aDocument); 1.1157 + } 1.1158 + node = aDocument.createElementNS(kNSXUL, "toolbarbutton"); 1.1159 + 1.1160 + node.setAttribute("id", aWidget.id); 1.1161 + node.setAttribute("widget-id", aWidget.id); 1.1162 + node.setAttribute("widget-type", aWidget.type); 1.1163 + if (aWidget.disabled) { 1.1164 + node.setAttribute("disabled", true); 1.1165 + } 1.1166 + node.setAttribute("removable", aWidget.removable); 1.1167 + node.setAttribute("overflows", aWidget.overflows); 1.1168 + node.setAttribute("label", this.getLocalizedProperty(aWidget, "label")); 1.1169 + let additionalTooltipArguments = []; 1.1170 + if (aWidget.shortcutId) { 1.1171 + let keyEl = aDocument.getElementById(aWidget.shortcutId); 1.1172 + if (keyEl) { 1.1173 + additionalTooltipArguments.push(ShortcutUtils.prettifyShortcut(keyEl)); 1.1174 + } else { 1.1175 + ERROR("Key element with id '" + aWidget.shortcutId + "' for widget '" + aWidget.id + 1.1176 + "' not found!"); 1.1177 + } 1.1178 + } 1.1179 + 1.1180 + let tooltip = this.getLocalizedProperty(aWidget, "tooltiptext", additionalTooltipArguments); 1.1181 + node.setAttribute("tooltiptext", tooltip); 1.1182 + node.setAttribute("class", "toolbarbutton-1 chromeclass-toolbar-additional"); 1.1183 + 1.1184 + let commandHandler = this.handleWidgetCommand.bind(this, aWidget, node); 1.1185 + node.addEventListener("command", commandHandler, false); 1.1186 + let clickHandler = this.handleWidgetClick.bind(this, aWidget, node); 1.1187 + node.addEventListener("click", clickHandler, false); 1.1188 + 1.1189 + // If the widget has a view, and has view showing / hiding listeners, 1.1190 + // hook those up to this widget. 1.1191 + if (aWidget.type == "view") { 1.1192 + LOG("Widget " + aWidget.id + " has a view. Auto-registering event handlers."); 1.1193 + let viewNode = aDocument.getElementById(aWidget.viewId); 1.1194 + 1.1195 + if (viewNode) { 1.1196 + // PanelUI relies on the .PanelUI-subView class to be able to show only 1.1197 + // one sub-view at a time. 1.1198 + viewNode.classList.add("PanelUI-subView"); 1.1199 + 1.1200 + for (let eventName of kSubviewEvents) { 1.1201 + let handler = "on" + eventName; 1.1202 + if (typeof aWidget[handler] == "function") { 1.1203 + viewNode.addEventListener(eventName, aWidget[handler], false); 1.1204 + } 1.1205 + } 1.1206 + 1.1207 + LOG("Widget " + aWidget.id + " showing and hiding event handlers set."); 1.1208 + } else { 1.1209 + ERROR("Could not find the view node with id: " + aWidget.viewId + 1.1210 + ", for widget: " + aWidget.id + "."); 1.1211 + } 1.1212 + } 1.1213 + 1.1214 + if (aWidget.onCreated) { 1.1215 + aWidget.onCreated(node); 1.1216 + } 1.1217 + } 1.1218 + 1.1219 + aWidget.instances.set(aDocument, node); 1.1220 + return node; 1.1221 + }, 1.1222 + 1.1223 + getLocalizedProperty: function(aWidget, aProp, aFormatArgs, aDef) { 1.1224 + if (typeof aWidget == "string") { 1.1225 + aWidget = gPalette.get(aWidget); 1.1226 + } 1.1227 + if (!aWidget) { 1.1228 + throw new Error("getLocalizedProperty was passed a non-widget to work with."); 1.1229 + } 1.1230 + let def, name; 1.1231 + // Let widgets pass their own string identifiers or strings, so that 1.1232 + // we can use strings which aren't the default (in case string ids change) 1.1233 + // and so that non-builtin-widgets can also provide labels, tooltips, etc. 1.1234 + if (aWidget[aProp]) { 1.1235 + name = aWidget[aProp]; 1.1236 + // By using this as the default, if a widget provides a full string rather 1.1237 + // than a string ID for localization, we will fall back to that string 1.1238 + // and return that. 1.1239 + def = aDef || name; 1.1240 + } else { 1.1241 + name = aWidget.id + "." + aProp; 1.1242 + def = aDef || ""; 1.1243 + } 1.1244 + try { 1.1245 + if (Array.isArray(aFormatArgs) && aFormatArgs.length) { 1.1246 + return gWidgetsBundle.formatStringFromName(name, aFormatArgs, 1.1247 + aFormatArgs.length) || def; 1.1248 + } 1.1249 + return gWidgetsBundle.GetStringFromName(name) || def; 1.1250 + } catch(ex) { 1.1251 + if (!def) { 1.1252 + ERROR("Could not localize property '" + name + "'."); 1.1253 + } 1.1254 + } 1.1255 + return def; 1.1256 + }, 1.1257 + 1.1258 + handleWidgetCommand: function(aWidget, aNode, aEvent) { 1.1259 + LOG("handleWidgetCommand"); 1.1260 + 1.1261 + if (aWidget.type == "button") { 1.1262 + if (aWidget.onCommand) { 1.1263 + try { 1.1264 + aWidget.onCommand.call(null, aEvent); 1.1265 + } catch (e) { 1.1266 + ERROR(e); 1.1267 + } 1.1268 + } else { 1.1269 + //XXXunf Need to think this through more, and formalize. 1.1270 + Services.obs.notifyObservers(aNode, 1.1271 + "customizedui-widget-command", 1.1272 + aWidget.id); 1.1273 + } 1.1274 + } else if (aWidget.type == "view") { 1.1275 + let ownerWindow = aNode.ownerDocument.defaultView; 1.1276 + let area = this.getPlacementOfWidget(aNode.id).area; 1.1277 + let anchor = aNode; 1.1278 + if (area != CustomizableUI.AREA_PANEL) { 1.1279 + let wrapper = this.wrapWidget(aWidget.id).forWindow(ownerWindow); 1.1280 + if (wrapper && wrapper.anchor) { 1.1281 + this.hidePanelForNode(aNode); 1.1282 + anchor = wrapper.anchor; 1.1283 + } 1.1284 + } 1.1285 + ownerWindow.PanelUI.showSubView(aWidget.viewId, anchor, area); 1.1286 + } 1.1287 + }, 1.1288 + 1.1289 + handleWidgetClick: function(aWidget, aNode, aEvent) { 1.1290 + LOG("handleWidgetClick"); 1.1291 + if (aWidget.onClick) { 1.1292 + try { 1.1293 + aWidget.onClick.call(null, aEvent); 1.1294 + } catch(e) { 1.1295 + Cu.reportError(e); 1.1296 + } 1.1297 + } else { 1.1298 + //XXXunf Need to think this through more, and formalize. 1.1299 + Services.obs.notifyObservers(aNode, "customizedui-widget-click", aWidget.id); 1.1300 + } 1.1301 + }, 1.1302 + 1.1303 + _getPanelForNode: function(aNode) { 1.1304 + let panel = aNode; 1.1305 + while (panel && panel.localName != "panel") 1.1306 + panel = panel.parentNode; 1.1307 + return panel; 1.1308 + }, 1.1309 + 1.1310 + /* 1.1311 + * If people put things in the panel which need more than single-click interaction, 1.1312 + * we don't want to close it. Right now we check for text inputs and menu buttons. 1.1313 + * We also check for being outside of any toolbaritem/toolbarbutton, ie on a blank 1.1314 + * part of the menu. 1.1315 + */ 1.1316 + _isOnInteractiveElement: function(aEvent) { 1.1317 + function getMenuPopupForDescendant(aNode) { 1.1318 + let lastPopup = null; 1.1319 + while (aNode && aNode.parentNode && 1.1320 + aNode.parentNode.localName.startsWith("menu")) { 1.1321 + lastPopup = aNode.localName == "menupopup" ? aNode : lastPopup; 1.1322 + aNode = aNode.parentNode; 1.1323 + } 1.1324 + return lastPopup; 1.1325 + } 1.1326 + 1.1327 + let target = aEvent.originalTarget; 1.1328 + let panel = this._getPanelForNode(aEvent.currentTarget); 1.1329 + // This can happen in e.g. customize mode. If there's no panel, 1.1330 + // there's clearly nothing for us to close; pretend we're interactive. 1.1331 + if (!panel) { 1.1332 + return true; 1.1333 + } 1.1334 + // We keep track of: 1.1335 + // whether we're in an input container (text field) 1.1336 + let inInput = false; 1.1337 + // whether we're in a popup/context menu 1.1338 + let inMenu = false; 1.1339 + // whether we're in a toolbarbutton/toolbaritem 1.1340 + let inItem = false; 1.1341 + // whether the current menuitem has a valid closemenu attribute 1.1342 + let menuitemCloseMenu = "auto"; 1.1343 + // whether the toolbarbutton/item has a valid closemenu attribute. 1.1344 + let closemenu = "auto"; 1.1345 + 1.1346 + // While keeping track of that, we go from the original target back up, 1.1347 + // to the panel if we have to. We bail as soon as we find an input, 1.1348 + // a toolbarbutton/item, or the panel: 1.1349 + while (true && target) { 1.1350 + let tagName = target.localName; 1.1351 + inInput = tagName == "input" || tagName == "textbox"; 1.1352 + inItem = tagName == "toolbaritem" || tagName == "toolbarbutton"; 1.1353 + let isMenuItem = tagName == "menuitem"; 1.1354 + inMenu = inMenu || isMenuItem; 1.1355 + if (inItem && target.hasAttribute("closemenu")) { 1.1356 + let closemenuVal = target.getAttribute("closemenu"); 1.1357 + closemenu = (closemenuVal == "single" || closemenuVal == "none") ? 1.1358 + closemenuVal : "auto"; 1.1359 + } 1.1360 + 1.1361 + if (isMenuItem && target.hasAttribute("closemenu")) { 1.1362 + let closemenuVal = target.getAttribute("closemenu"); 1.1363 + menuitemCloseMenu = (closemenuVal == "single" || closemenuVal == "none") ? 1.1364 + closemenuVal : "auto"; 1.1365 + } 1.1366 + // This isn't in the loop condition because we want to break before 1.1367 + // changing |target| if any of these conditions are true 1.1368 + if (inInput || inItem || target == panel) { 1.1369 + break; 1.1370 + } 1.1371 + // We need specific code for popups: the item on which they were invoked 1.1372 + // isn't necessarily in their parentNode chain: 1.1373 + if (isMenuItem) { 1.1374 + let topmostMenuPopup = getMenuPopupForDescendant(target); 1.1375 + target = (topmostMenuPopup && topmostMenuPopup.triggerNode) || 1.1376 + target.parentNode; 1.1377 + } else { 1.1378 + target = target.parentNode; 1.1379 + } 1.1380 + } 1.1381 + // If the user clicked a menu item... 1.1382 + if (inMenu) { 1.1383 + // We care if we're in an input also, 1.1384 + // or if the user specified closemenu!="auto": 1.1385 + if (inInput || menuitemCloseMenu != "auto") { 1.1386 + return true; 1.1387 + } 1.1388 + // Otherwise, we're probably fine to close the panel 1.1389 + return false; 1.1390 + } 1.1391 + // If we're not in a menu, and we *are* in a type="menu" toolbarbutton, 1.1392 + // we'll now interact with the menu 1.1393 + if (inItem && target.getAttribute("type") == "menu") { 1.1394 + return true; 1.1395 + } 1.1396 + // If we're not in a menu, and we *are* in a type="menu-button" toolbarbutton, 1.1397 + // it depends whether we're in the dropmarker or the 'real' button: 1.1398 + if (inItem && target.getAttribute("type") == "menu-button") { 1.1399 + // 'real' button (which has a single action): 1.1400 + if (target.getAttribute("anonid") == "button") { 1.1401 + return closemenu != "none"; 1.1402 + } 1.1403 + // otherwise, this is the outer button, and the user will now 1.1404 + // interact with the menu: 1.1405 + return true; 1.1406 + } 1.1407 + return inInput || !inItem; 1.1408 + }, 1.1409 + 1.1410 + hidePanelForNode: function(aNode) { 1.1411 + let panel = this._getPanelForNode(aNode); 1.1412 + if (panel) { 1.1413 + panel.hidePopup(); 1.1414 + } 1.1415 + }, 1.1416 + 1.1417 + maybeAutoHidePanel: function(aEvent) { 1.1418 + if (aEvent.type == "keypress") { 1.1419 + if (aEvent.keyCode != aEvent.DOM_VK_RETURN) { 1.1420 + return; 1.1421 + } 1.1422 + // If the user hit enter/return, we don't check preventDefault - it makes sense 1.1423 + // that this was prevented, but we probably still want to close the panel. 1.1424 + // If consumers don't want this to happen, they should specify the closemenu 1.1425 + // attribute. 1.1426 + 1.1427 + } else if (aEvent.type != "command") { // mouse events: 1.1428 + if (aEvent.defaultPrevented || aEvent.button != 0) { 1.1429 + return; 1.1430 + } 1.1431 + let isInteractive = this._isOnInteractiveElement(aEvent); 1.1432 + LOG("maybeAutoHidePanel: interactive ? " + isInteractive); 1.1433 + if (isInteractive) { 1.1434 + return; 1.1435 + } 1.1436 + } 1.1437 + 1.1438 + // We can't use event.target because we might have passed a panelview 1.1439 + // anonymous content boundary as well, and so target points to the 1.1440 + // panelmultiview in that case. Unfortunately, this means we get 1.1441 + // anonymous child nodes instead of the real ones, so looking for the 1.1442 + // 'stoooop, don't close me' attributes is more involved. 1.1443 + let target = aEvent.originalTarget; 1.1444 + let closemenu = "auto"; 1.1445 + let widgetType = "button"; 1.1446 + while (target.parentNode && target.localName != "panel") { 1.1447 + closemenu = target.getAttribute("closemenu"); 1.1448 + widgetType = target.getAttribute("widget-type"); 1.1449 + if (closemenu == "none" || closemenu == "single" || 1.1450 + widgetType == "view") { 1.1451 + break; 1.1452 + } 1.1453 + target = target.parentNode; 1.1454 + } 1.1455 + if (closemenu == "none" || widgetType == "view") { 1.1456 + return; 1.1457 + } 1.1458 + 1.1459 + if (closemenu == "single") { 1.1460 + let panel = this._getPanelForNode(target); 1.1461 + let multiview = panel.querySelector("panelmultiview"); 1.1462 + if (multiview.showingSubView) { 1.1463 + multiview.showMainView(); 1.1464 + return; 1.1465 + } 1.1466 + } 1.1467 + 1.1468 + // If we get here, we can actually hide the popup: 1.1469 + this.hidePanelForNode(aEvent.target); 1.1470 + }, 1.1471 + 1.1472 + getUnusedWidgets: function(aWindowPalette) { 1.1473 + let window = aWindowPalette.ownerDocument.defaultView; 1.1474 + let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(window); 1.1475 + // We use a Set because there can be overlap between the widgets in 1.1476 + // gPalette and the items in the palette, especially after the first 1.1477 + // customization, since programmatically generated widgets will remain 1.1478 + // in the toolbox palette. 1.1479 + let widgets = new Set(); 1.1480 + 1.1481 + // It's possible that some widgets have been defined programmatically and 1.1482 + // have not been overlayed into the palette. We can find those inside 1.1483 + // gPalette. 1.1484 + for (let [id, widget] of gPalette) { 1.1485 + if (!widget.currentArea) { 1.1486 + if (widget.showInPrivateBrowsing || !isWindowPrivate) { 1.1487 + widgets.add(id); 1.1488 + } 1.1489 + } 1.1490 + } 1.1491 + 1.1492 + LOG("Iterating the actual nodes of the window palette"); 1.1493 + for (let node of aWindowPalette.children) { 1.1494 + LOG("In palette children: " + node.id); 1.1495 + if (node.id && !this.getPlacementOfWidget(node.id)) { 1.1496 + widgets.add(node.id); 1.1497 + } 1.1498 + } 1.1499 + 1.1500 + return [...widgets]; 1.1501 + }, 1.1502 + 1.1503 + getPlacementOfWidget: function(aWidgetId, aOnlyRegistered, aDeadAreas) { 1.1504 + if (aOnlyRegistered && !this.widgetExists(aWidgetId)) { 1.1505 + return null; 1.1506 + } 1.1507 + 1.1508 + for (let [area, placements] of gPlacements) { 1.1509 + if (!gAreas.has(area) && !aDeadAreas) { 1.1510 + continue; 1.1511 + } 1.1512 + let index = placements.indexOf(aWidgetId); 1.1513 + if (index != -1) { 1.1514 + return { area: area, position: index }; 1.1515 + } 1.1516 + } 1.1517 + 1.1518 + return null; 1.1519 + }, 1.1520 + 1.1521 + widgetExists: function(aWidgetId) { 1.1522 + if (gPalette.has(aWidgetId) || this.isSpecialWidget(aWidgetId)) { 1.1523 + return true; 1.1524 + } 1.1525 + 1.1526 + // Destroyed API widgets are in gSeenWidgets, but not in gPalette: 1.1527 + if (gSeenWidgets.has(aWidgetId)) { 1.1528 + return false; 1.1529 + } 1.1530 + 1.1531 + // We're assuming XUL widgets always exist, as it's much harder to check, 1.1532 + // and checking would be much more error prone. 1.1533 + return true; 1.1534 + }, 1.1535 + 1.1536 + addWidgetToArea: function(aWidgetId, aArea, aPosition, aInitialAdd) { 1.1537 + if (!gAreas.has(aArea)) { 1.1538 + throw new Error("Unknown customization area: " + aArea); 1.1539 + } 1.1540 + 1.1541 + // Hack: don't want special widgets in the panel (need to check here as well 1.1542 + // as in canWidgetMoveToArea because the menu panel is lazy): 1.1543 + if (gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL && 1.1544 + this.isSpecialWidget(aWidgetId)) { 1.1545 + return; 1.1546 + } 1.1547 + 1.1548 + // If this is a lazy area that hasn't been restored yet, we can't yet modify 1.1549 + // it - would would at least like to add to it. So we keep track of it in 1.1550 + // gFuturePlacements, and use that to add it when restoring the area. We 1.1551 + // throw away aPosition though, as that can only be bogus if the area hasn't 1.1552 + // yet been restorted (caller can't possibly know where its putting the 1.1553 + // widget in relation to other widgets). 1.1554 + if (this.isAreaLazy(aArea)) { 1.1555 + gFuturePlacements.get(aArea).add(aWidgetId); 1.1556 + return; 1.1557 + } 1.1558 + 1.1559 + if (this.isSpecialWidget(aWidgetId)) { 1.1560 + aWidgetId = this.ensureSpecialWidgetId(aWidgetId); 1.1561 + } 1.1562 + 1.1563 + let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true); 1.1564 + if (oldPlacement && oldPlacement.area == aArea) { 1.1565 + this.moveWidgetWithinArea(aWidgetId, aPosition); 1.1566 + return; 1.1567 + } 1.1568 + 1.1569 + // Do nothing if the widget is not allowed to move to the target area. 1.1570 + if (!this.canWidgetMoveToArea(aWidgetId, aArea)) { 1.1571 + return; 1.1572 + } 1.1573 + 1.1574 + if (oldPlacement) { 1.1575 + this.removeWidgetFromArea(aWidgetId); 1.1576 + } 1.1577 + 1.1578 + if (!gPlacements.has(aArea)) { 1.1579 + gPlacements.set(aArea, [aWidgetId]); 1.1580 + aPosition = 0; 1.1581 + } else { 1.1582 + let placements = gPlacements.get(aArea); 1.1583 + if (typeof aPosition != "number") { 1.1584 + aPosition = placements.length; 1.1585 + } 1.1586 + if (aPosition < 0) { 1.1587 + aPosition = 0; 1.1588 + } 1.1589 + placements.splice(aPosition, 0, aWidgetId); 1.1590 + } 1.1591 + 1.1592 + let widget = gPalette.get(aWidgetId); 1.1593 + if (widget) { 1.1594 + widget.currentArea = aArea; 1.1595 + widget.currentPosition = aPosition; 1.1596 + } 1.1597 + 1.1598 + // We initially set placements with addWidgetToArea, so in that case 1.1599 + // we don't consider the area "dirtied". 1.1600 + if (!aInitialAdd) { 1.1601 + gDirtyAreaCache.add(aArea); 1.1602 + } 1.1603 + 1.1604 + gDirty = true; 1.1605 + this.saveState(); 1.1606 + 1.1607 + this.notifyListeners("onWidgetAdded", aWidgetId, aArea, aPosition); 1.1608 + }, 1.1609 + 1.1610 + removeWidgetFromArea: function(aWidgetId) { 1.1611 + let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true); 1.1612 + if (!oldPlacement) { 1.1613 + return; 1.1614 + } 1.1615 + 1.1616 + if (!this.isWidgetRemovable(aWidgetId)) { 1.1617 + return; 1.1618 + } 1.1619 + 1.1620 + let placements = gPlacements.get(oldPlacement.area); 1.1621 + let position = placements.indexOf(aWidgetId); 1.1622 + if (position != -1) { 1.1623 + placements.splice(position, 1); 1.1624 + } 1.1625 + 1.1626 + let widget = gPalette.get(aWidgetId); 1.1627 + if (widget) { 1.1628 + widget.currentArea = null; 1.1629 + widget.currentPosition = null; 1.1630 + } 1.1631 + 1.1632 + gDirty = true; 1.1633 + this.saveState(); 1.1634 + gDirtyAreaCache.add(oldPlacement.area); 1.1635 + 1.1636 + this.notifyListeners("onWidgetRemoved", aWidgetId, oldPlacement.area); 1.1637 + }, 1.1638 + 1.1639 + moveWidgetWithinArea: function(aWidgetId, aPosition) { 1.1640 + let oldPlacement = this.getPlacementOfWidget(aWidgetId); 1.1641 + if (!oldPlacement) { 1.1642 + return; 1.1643 + } 1.1644 + 1.1645 + let placements = gPlacements.get(oldPlacement.area); 1.1646 + if (typeof aPosition != "number") { 1.1647 + aPosition = placements.length; 1.1648 + } else if (aPosition < 0) { 1.1649 + aPosition = 0; 1.1650 + } else if (aPosition > placements.length) { 1.1651 + aPosition = placements.length; 1.1652 + } 1.1653 + 1.1654 + let widget = gPalette.get(aWidgetId); 1.1655 + if (widget) { 1.1656 + widget.currentPosition = aPosition; 1.1657 + widget.currentArea = oldPlacement.area; 1.1658 + } 1.1659 + 1.1660 + if (aPosition == oldPlacement.position) { 1.1661 + return; 1.1662 + } 1.1663 + 1.1664 + placements.splice(oldPlacement.position, 1); 1.1665 + // If we just removed the item from *before* where it is now added, 1.1666 + // we need to compensate the position offset for that: 1.1667 + if (oldPlacement.position < aPosition) { 1.1668 + aPosition--; 1.1669 + } 1.1670 + placements.splice(aPosition, 0, aWidgetId); 1.1671 + 1.1672 + gDirty = true; 1.1673 + gDirtyAreaCache.add(oldPlacement.area); 1.1674 + 1.1675 + this.saveState(); 1.1676 + 1.1677 + this.notifyListeners("onWidgetMoved", aWidgetId, oldPlacement.area, 1.1678 + oldPlacement.position, aPosition); 1.1679 + }, 1.1680 + 1.1681 + // Note that this does not populate gPlacements, which is done lazily so that 1.1682 + // the legacy state can be migrated, which is only available once a browser 1.1683 + // window is openned. 1.1684 + // The panel area is an exception here, since it has no legacy state and is 1.1685 + // built lazily - and therefore wouldn't otherwise result in restoring its 1.1686 + // state immediately when a browser window opens, which is important for 1.1687 + // other consumers of this API. 1.1688 + loadSavedState: function() { 1.1689 + let state = null; 1.1690 + try { 1.1691 + state = Services.prefs.getCharPref(kPrefCustomizationState); 1.1692 + } catch (e) { 1.1693 + LOG("No saved state found"); 1.1694 + // This will fail if nothing has been customized, so silently fall back to 1.1695 + // the defaults. 1.1696 + } 1.1697 + 1.1698 + if (!state) { 1.1699 + return; 1.1700 + } 1.1701 + try { 1.1702 + gSavedState = JSON.parse(state); 1.1703 + if (typeof gSavedState != "object" || gSavedState === null) { 1.1704 + throw "Invalid saved state"; 1.1705 + } 1.1706 + } catch(e) { 1.1707 + Services.prefs.clearUserPref(kPrefCustomizationState); 1.1708 + gSavedState = {}; 1.1709 + LOG("Error loading saved UI customization state, falling back to defaults."); 1.1710 + } 1.1711 + 1.1712 + if (!("placements" in gSavedState)) { 1.1713 + gSavedState.placements = {}; 1.1714 + } 1.1715 + 1.1716 + gSeenWidgets = new Set(gSavedState.seen || []); 1.1717 + gDirtyAreaCache = new Set(gSavedState.dirtyAreaCache || []); 1.1718 + gNewElementCount = gSavedState.newElementCount || 0; 1.1719 + }, 1.1720 + 1.1721 + restoreStateForArea: function(aArea, aLegacyState) { 1.1722 + let placementsPreexisted = gPlacements.has(aArea); 1.1723 + 1.1724 + this.beginBatchUpdate(); 1.1725 + try { 1.1726 + gRestoring = true; 1.1727 + 1.1728 + let restored = false; 1.1729 + if (placementsPreexisted) { 1.1730 + LOG("Restoring " + aArea + " from pre-existing placements"); 1.1731 + for (let [position, id] in Iterator(gPlacements.get(aArea))) { 1.1732 + this.moveWidgetWithinArea(id, position); 1.1733 + } 1.1734 + gDirty = false; 1.1735 + restored = true; 1.1736 + } else { 1.1737 + gPlacements.set(aArea, []); 1.1738 + } 1.1739 + 1.1740 + if (!restored && gSavedState && aArea in gSavedState.placements) { 1.1741 + LOG("Restoring " + aArea + " from saved state"); 1.1742 + let placements = gSavedState.placements[aArea]; 1.1743 + for (let id of placements) 1.1744 + this.addWidgetToArea(id, aArea); 1.1745 + gDirty = false; 1.1746 + restored = true; 1.1747 + } 1.1748 + 1.1749 + if (!restored && aLegacyState) { 1.1750 + LOG("Restoring " + aArea + " from legacy state"); 1.1751 + for (let id of aLegacyState) 1.1752 + this.addWidgetToArea(id, aArea); 1.1753 + // Don't override dirty state, to ensure legacy state is saved here and 1.1754 + // therefore only used once. 1.1755 + restored = true; 1.1756 + } 1.1757 + 1.1758 + if (!restored) { 1.1759 + LOG("Restoring " + aArea + " from default state"); 1.1760 + let defaults = gAreas.get(aArea).get("defaultPlacements"); 1.1761 + if (defaults) { 1.1762 + for (let id of defaults) 1.1763 + this.addWidgetToArea(id, aArea, null, true); 1.1764 + } 1.1765 + gDirty = false; 1.1766 + } 1.1767 + 1.1768 + // Finally, add widgets to the area that were added before the it was able 1.1769 + // to be restored. This can occur when add-ons register widgets for a 1.1770 + // lazily-restored area before it's been restored. 1.1771 + if (gFuturePlacements.has(aArea)) { 1.1772 + for (let id of gFuturePlacements.get(aArea)) 1.1773 + this.addWidgetToArea(id, aArea); 1.1774 + } 1.1775 + 1.1776 + LOG("Placements for " + aArea + ":\n\t" + gPlacements.get(aArea).join("\n\t")); 1.1777 + 1.1778 + gRestoring = false; 1.1779 + } finally { 1.1780 + this.endBatchUpdate(); 1.1781 + } 1.1782 + }, 1.1783 + 1.1784 + saveState: function() { 1.1785 + if (gInBatchStack || !gDirty) { 1.1786 + return; 1.1787 + } 1.1788 + let state = { placements: gPlacements, 1.1789 + seen: gSeenWidgets, 1.1790 + dirtyAreaCache: gDirtyAreaCache, 1.1791 + newElementCount: gNewElementCount }; 1.1792 + 1.1793 + LOG("Saving state."); 1.1794 + let serialized = JSON.stringify(state, this.serializerHelper); 1.1795 + LOG("State saved as: " + serialized); 1.1796 + Services.prefs.setCharPref(kPrefCustomizationState, serialized); 1.1797 + gDirty = false; 1.1798 + }, 1.1799 + 1.1800 + serializerHelper: function(aKey, aValue) { 1.1801 + if (typeof aValue == "object" && aValue.constructor.name == "Map") { 1.1802 + let result = {}; 1.1803 + for (let [mapKey, mapValue] of aValue) 1.1804 + result[mapKey] = mapValue; 1.1805 + return result; 1.1806 + } 1.1807 + 1.1808 + if (typeof aValue == "object" && aValue.constructor.name == "Set") { 1.1809 + return [...aValue]; 1.1810 + } 1.1811 + 1.1812 + return aValue; 1.1813 + }, 1.1814 + 1.1815 + beginBatchUpdate: function() { 1.1816 + gInBatchStack++; 1.1817 + }, 1.1818 + 1.1819 + endBatchUpdate: function(aForceDirty) { 1.1820 + gInBatchStack--; 1.1821 + if (aForceDirty === true) { 1.1822 + gDirty = true; 1.1823 + } 1.1824 + if (gInBatchStack == 0) { 1.1825 + this.saveState(); 1.1826 + } else if (gInBatchStack < 0) { 1.1827 + throw new Error("The batch editing stack should never reach a negative number."); 1.1828 + } 1.1829 + }, 1.1830 + 1.1831 + addListener: function(aListener) { 1.1832 + gListeners.add(aListener); 1.1833 + }, 1.1834 + 1.1835 + removeListener: function(aListener) { 1.1836 + if (aListener == this) { 1.1837 + return; 1.1838 + } 1.1839 + 1.1840 + gListeners.delete(aListener); 1.1841 + }, 1.1842 + 1.1843 + notifyListeners: function(aEvent, ...aArgs) { 1.1844 + if (gRestoring) { 1.1845 + return; 1.1846 + } 1.1847 + 1.1848 + for (let listener of gListeners) { 1.1849 + try { 1.1850 + if (typeof listener[aEvent] == "function") { 1.1851 + listener[aEvent].apply(listener, aArgs); 1.1852 + } 1.1853 + } catch (e) { 1.1854 + ERROR(e + " -- " + e.fileName + ":" + e.lineNumber); 1.1855 + } 1.1856 + } 1.1857 + }, 1.1858 + 1.1859 + _dispatchToolboxEventToWindow: function(aEventType, aDetails, aWindow) { 1.1860 + let evt = new aWindow.CustomEvent(aEventType, { 1.1861 + bubbles: true, 1.1862 + cancelable: true, 1.1863 + detail: aDetails 1.1864 + }); 1.1865 + aWindow.gNavToolbox.dispatchEvent(evt); 1.1866 + }, 1.1867 + 1.1868 + dispatchToolboxEvent: function(aEventType, aDetails={}, aWindow=null) { 1.1869 + if (aWindow) { 1.1870 + return this._dispatchToolboxEventToWindow(aEventType, aDetails, aWindow); 1.1871 + } 1.1872 + for (let [win, ] of gBuildWindows) { 1.1873 + this._dispatchToolboxEventToWindow(aEventType, aDetails, win); 1.1874 + } 1.1875 + }, 1.1876 + 1.1877 + createWidget: function(aProperties) { 1.1878 + let widget = this.normalizeWidget(aProperties, CustomizableUI.SOURCE_EXTERNAL); 1.1879 + //XXXunf This should probably throw. 1.1880 + if (!widget) { 1.1881 + return; 1.1882 + } 1.1883 + 1.1884 + gPalette.set(widget.id, widget); 1.1885 + 1.1886 + // Clear our caches: 1.1887 + gGroupWrapperCache.delete(widget.id); 1.1888 + for (let [win, ] of gBuildWindows) { 1.1889 + let cache = gSingleWrapperCache.get(win); 1.1890 + if (cache) { 1.1891 + cache.delete(widget.id); 1.1892 + } 1.1893 + } 1.1894 + 1.1895 + this.notifyListeners("onWidgetCreated", widget.id); 1.1896 + 1.1897 + if (widget.defaultArea) { 1.1898 + let area = gAreas.get(widget.defaultArea); 1.1899 + //XXXgijs this won't have any effect for legacy items. Sort of OK because 1.1900 + // consumers can modify currentset? Maybe? 1.1901 + if (area.has("defaultPlacements")) { 1.1902 + area.get("defaultPlacements").push(widget.id); 1.1903 + } else { 1.1904 + area.set("defaultPlacements", [widget.id]); 1.1905 + } 1.1906 + } 1.1907 + 1.1908 + // Look through previously saved state to see if we're restoring a widget. 1.1909 + let seenAreas = new Set(); 1.1910 + let widgetMightNeedAutoAdding = true; 1.1911 + for (let [area, placements] of gPlacements) { 1.1912 + seenAreas.add(area); 1.1913 + let areaIsRegistered = gAreas.has(area); 1.1914 + let index = gPlacements.get(area).indexOf(widget.id); 1.1915 + if (index != -1) { 1.1916 + widgetMightNeedAutoAdding = false; 1.1917 + if (areaIsRegistered) { 1.1918 + widget.currentArea = area; 1.1919 + widget.currentPosition = index; 1.1920 + } 1.1921 + break; 1.1922 + } 1.1923 + } 1.1924 + 1.1925 + // Also look at saved state data directly in areas that haven't yet been 1.1926 + // restored. Can't rely on this for restored areas, as they may have 1.1927 + // changed. 1.1928 + if (widgetMightNeedAutoAdding && gSavedState) { 1.1929 + for (let area of Object.keys(gSavedState.placements)) { 1.1930 + if (seenAreas.has(area)) { 1.1931 + continue; 1.1932 + } 1.1933 + 1.1934 + let areaIsRegistered = gAreas.has(area); 1.1935 + let index = gSavedState.placements[area].indexOf(widget.id); 1.1936 + if (index != -1) { 1.1937 + widgetMightNeedAutoAdding = false; 1.1938 + if (areaIsRegistered) { 1.1939 + widget.currentArea = area; 1.1940 + widget.currentPosition = index; 1.1941 + } 1.1942 + break; 1.1943 + } 1.1944 + } 1.1945 + } 1.1946 + 1.1947 + // If we're restoring the widget to it's old placement, fire off the 1.1948 + // onWidgetAdded event - our own handler will take care of adding it to 1.1949 + // any build areas. 1.1950 + if (widget.currentArea) { 1.1951 + this.notifyListeners("onWidgetAdded", widget.id, widget.currentArea, 1.1952 + widget.currentPosition); 1.1953 + } else if (widgetMightNeedAutoAdding) { 1.1954 + let autoAdd = true; 1.1955 + try { 1.1956 + autoAdd = Services.prefs.getBoolPref(kPrefCustomizationAutoAdd); 1.1957 + } catch (e) {} 1.1958 + 1.1959 + // If the widget doesn't have an existing placement, and it hasn't been 1.1960 + // seen before, then add it to its default area so it can be used. 1.1961 + // If the widget is not removable, we *have* to add it to its default 1.1962 + // area here. 1.1963 + let canBeAutoAdded = autoAdd && !gSeenWidgets.has(widget.id); 1.1964 + if (!widget.currentArea && (!widget.removable || canBeAutoAdded)) { 1.1965 + this.beginBatchUpdate(); 1.1966 + try { 1.1967 + gSeenWidgets.add(widget.id); 1.1968 + 1.1969 + if (widget.defaultArea) { 1.1970 + if (this.isAreaLazy(widget.defaultArea)) { 1.1971 + gFuturePlacements.get(widget.defaultArea).add(widget.id); 1.1972 + } else { 1.1973 + this.addWidgetToArea(widget.id, widget.defaultArea); 1.1974 + } 1.1975 + } 1.1976 + } finally { 1.1977 + this.endBatchUpdate(true); 1.1978 + } 1.1979 + } 1.1980 + } 1.1981 + 1.1982 + this.notifyListeners("onWidgetAfterCreation", widget.id, widget.currentArea); 1.1983 + return widget.id; 1.1984 + }, 1.1985 + 1.1986 + createBuiltinWidget: function(aData) { 1.1987 + // This should only ever be called on startup, before any windows are 1.1988 + // opened - so we know there's no build areas to handle. Also, builtin 1.1989 + // widgets are expected to be (mostly) static, so shouldn't affect the 1.1990 + // current placement settings. 1.1991 + let widget = this.normalizeWidget(aData, CustomizableUI.SOURCE_BUILTIN); 1.1992 + if (!widget) { 1.1993 + ERROR("Error creating builtin widget: " + aData.id); 1.1994 + return; 1.1995 + } 1.1996 + 1.1997 + LOG("Creating built-in widget with id: " + widget.id); 1.1998 + gPalette.set(widget.id, widget); 1.1999 + }, 1.2000 + 1.2001 + // Returns true if the area will eventually lazily restore (but hasn't yet). 1.2002 + isAreaLazy: function(aArea) { 1.2003 + if (gPlacements.has(aArea)) { 1.2004 + return false; 1.2005 + } 1.2006 + return gAreas.get(aArea).has("legacy"); 1.2007 + }, 1.2008 + 1.2009 + //XXXunf Log some warnings here, when the data provided isn't up to scratch. 1.2010 + normalizeWidget: function(aData, aSource) { 1.2011 + let widget = { 1.2012 + implementation: aData, 1.2013 + source: aSource || "addon", 1.2014 + instances: new Map(), 1.2015 + currentArea: null, 1.2016 + removable: true, 1.2017 + overflows: true, 1.2018 + defaultArea: null, 1.2019 + shortcutId: null, 1.2020 + tooltiptext: null, 1.2021 + showInPrivateBrowsing: true, 1.2022 + }; 1.2023 + 1.2024 + if (typeof aData.id != "string" || !/^[a-z0-9-_]{1,}$/i.test(aData.id)) { 1.2025 + ERROR("Given an illegal id in normalizeWidget: " + aData.id); 1.2026 + return null; 1.2027 + } 1.2028 + 1.2029 + delete widget.implementation.currentArea; 1.2030 + widget.implementation.__defineGetter__("currentArea", function() widget.currentArea); 1.2031 + 1.2032 + const kReqStringProps = ["id"]; 1.2033 + for (let prop of kReqStringProps) { 1.2034 + if (typeof aData[prop] != "string") { 1.2035 + ERROR("Missing required property '" + prop + "' in normalizeWidget: " 1.2036 + + aData.id); 1.2037 + return null; 1.2038 + } 1.2039 + widget[prop] = aData[prop]; 1.2040 + } 1.2041 + 1.2042 + const kOptStringProps = ["label", "tooltiptext", "shortcutId"]; 1.2043 + for (let prop of kOptStringProps) { 1.2044 + if (typeof aData[prop] == "string") { 1.2045 + widget[prop] = aData[prop]; 1.2046 + } 1.2047 + } 1.2048 + 1.2049 + const kOptBoolProps = ["removable", "showInPrivateBrowsing", "overflows"]; 1.2050 + for (let prop of kOptBoolProps) { 1.2051 + if (typeof aData[prop] == "boolean") { 1.2052 + widget[prop] = aData[prop]; 1.2053 + } 1.2054 + } 1.2055 + 1.2056 + if (aData.defaultArea && gAreas.has(aData.defaultArea)) { 1.2057 + widget.defaultArea = aData.defaultArea; 1.2058 + } else if (!widget.removable) { 1.2059 + ERROR("Widget '" + widget.id + "' is not removable but does not specify " + 1.2060 + "a valid defaultArea. That's not possible; it must specify a " + 1.2061 + "valid defaultArea as well."); 1.2062 + return null; 1.2063 + } 1.2064 + 1.2065 + if ("type" in aData && gSupportedWidgetTypes.has(aData.type)) { 1.2066 + widget.type = aData.type; 1.2067 + } else { 1.2068 + widget.type = "button"; 1.2069 + } 1.2070 + 1.2071 + widget.disabled = aData.disabled === true; 1.2072 + 1.2073 + this.wrapWidgetEventHandler("onBeforeCreated", widget); 1.2074 + this.wrapWidgetEventHandler("onClick", widget); 1.2075 + this.wrapWidgetEventHandler("onCreated", widget); 1.2076 + 1.2077 + if (widget.type == "button") { 1.2078 + widget.onCommand = typeof aData.onCommand == "function" ? 1.2079 + aData.onCommand : 1.2080 + null; 1.2081 + } else if (widget.type == "view") { 1.2082 + if (typeof aData.viewId != "string") { 1.2083 + ERROR("Expected a string for widget " + widget.id + " viewId, but got " 1.2084 + + aData.viewId); 1.2085 + return null; 1.2086 + } 1.2087 + widget.viewId = aData.viewId; 1.2088 + 1.2089 + this.wrapWidgetEventHandler("onViewShowing", widget); 1.2090 + this.wrapWidgetEventHandler("onViewHiding", widget); 1.2091 + } else if (widget.type == "custom") { 1.2092 + this.wrapWidgetEventHandler("onBuild", widget); 1.2093 + } 1.2094 + 1.2095 + if (gPalette.has(widget.id)) { 1.2096 + return null; 1.2097 + } 1.2098 + 1.2099 + return widget; 1.2100 + }, 1.2101 + 1.2102 + wrapWidgetEventHandler: function(aEventName, aWidget) { 1.2103 + if (typeof aWidget.implementation[aEventName] != "function") { 1.2104 + aWidget[aEventName] = null; 1.2105 + return; 1.2106 + } 1.2107 + aWidget[aEventName] = function(...aArgs) { 1.2108 + // Wrap inside a try...catch to properly log errors, until bug 862627 is 1.2109 + // fixed, which in turn might help bug 503244. 1.2110 + try { 1.2111 + // Don't copy the function to the normalized widget object, instead 1.2112 + // keep it on the original object provided to the API so that 1.2113 + // additional methods can be implemented and used by the event 1.2114 + // handlers. 1.2115 + return aWidget.implementation[aEventName].apply(aWidget.implementation, 1.2116 + aArgs); 1.2117 + } catch (e) { 1.2118 + Cu.reportError(e); 1.2119 + } 1.2120 + }; 1.2121 + }, 1.2122 + 1.2123 + destroyWidget: function(aWidgetId) { 1.2124 + let widget = gPalette.get(aWidgetId); 1.2125 + if (!widget) { 1.2126 + gGroupWrapperCache.delete(aWidgetId); 1.2127 + for (let [window, ] of gBuildWindows) { 1.2128 + let windowCache = gSingleWrapperCache.get(window); 1.2129 + if (windowCache) { 1.2130 + windowCache.delete(aWidgetId); 1.2131 + } 1.2132 + } 1.2133 + return; 1.2134 + } 1.2135 + 1.2136 + // Remove it from the default placements of an area if it was added there: 1.2137 + if (widget.defaultArea) { 1.2138 + let area = gAreas.get(widget.defaultArea); 1.2139 + if (area) { 1.2140 + let defaultPlacements = area.get("defaultPlacements"); 1.2141 + // We can assume this is present because if a widget has a defaultArea, 1.2142 + // we automatically create a defaultPlacements array for that area. 1.2143 + let widgetIndex = defaultPlacements.indexOf(aWidgetId); 1.2144 + if (widgetIndex != -1) { 1.2145 + defaultPlacements.splice(widgetIndex, 1); 1.2146 + } 1.2147 + } 1.2148 + } 1.2149 + 1.2150 + // This will not remove the widget from gPlacements - we want to keep the 1.2151 + // setting so the widget gets put back in it's old position if/when it 1.2152 + // returns. 1.2153 + for (let [window, ] of gBuildWindows) { 1.2154 + let windowCache = gSingleWrapperCache.get(window); 1.2155 + if (windowCache) { 1.2156 + windowCache.delete(aWidgetId); 1.2157 + } 1.2158 + let widgetNode = window.document.getElementById(aWidgetId) || 1.2159 + window.gNavToolbox.palette.getElementsByAttribute("id", aWidgetId)[0]; 1.2160 + if (widgetNode) { 1.2161 + let container = widgetNode.parentNode 1.2162 + this.notifyListeners("onWidgetBeforeDOMChange", widgetNode, null, 1.2163 + container, true); 1.2164 + widgetNode.remove(); 1.2165 + this.notifyListeners("onWidgetAfterDOMChange", widgetNode, null, 1.2166 + container, true); 1.2167 + } 1.2168 + if (widget.type == "view") { 1.2169 + let viewNode = window.document.getElementById(widget.viewId); 1.2170 + if (viewNode) { 1.2171 + for (let eventName of kSubviewEvents) { 1.2172 + let handler = "on" + eventName; 1.2173 + if (typeof widget[handler] == "function") { 1.2174 + viewNode.removeEventListener(eventName, widget[handler], false); 1.2175 + } 1.2176 + } 1.2177 + } 1.2178 + } 1.2179 + } 1.2180 + 1.2181 + gPalette.delete(aWidgetId); 1.2182 + gGroupWrapperCache.delete(aWidgetId); 1.2183 + 1.2184 + this.notifyListeners("onWidgetDestroyed", aWidgetId); 1.2185 + }, 1.2186 + 1.2187 + getCustomizeTargetForArea: function(aArea, aWindow) { 1.2188 + let buildAreaNodes = gBuildAreas.get(aArea); 1.2189 + if (!buildAreaNodes) { 1.2190 + return null; 1.2191 + } 1.2192 + 1.2193 + for (let node of buildAreaNodes) { 1.2194 + if (node.ownerDocument.defaultView === aWindow) { 1.2195 + return node.customizationTarget ? node.customizationTarget : node; 1.2196 + } 1.2197 + } 1.2198 + 1.2199 + return null; 1.2200 + }, 1.2201 + 1.2202 + reset: function() { 1.2203 + gResetting = true; 1.2204 + this._resetUIState(); 1.2205 + 1.2206 + // Rebuild each registered area (across windows) to reflect the state that 1.2207 + // was reset above. 1.2208 + this._rebuildRegisteredAreas(); 1.2209 + 1.2210 + gResetting = false; 1.2211 + }, 1.2212 + 1.2213 + _resetUIState: function() { 1.2214 + try { 1.2215 + gUIStateBeforeReset.drawInTitlebar = Services.prefs.getBoolPref(kPrefDrawInTitlebar); 1.2216 + gUIStateBeforeReset.uiCustomizationState = Services.prefs.getCharPref(kPrefCustomizationState); 1.2217 + } catch(e) { } 1.2218 + 1.2219 + this._resetExtraToolbars(); 1.2220 + 1.2221 + Services.prefs.clearUserPref(kPrefCustomizationState); 1.2222 + Services.prefs.clearUserPref(kPrefDrawInTitlebar); 1.2223 + LOG("State reset"); 1.2224 + 1.2225 + // Reset placements to make restoring default placements possible. 1.2226 + gPlacements = new Map(); 1.2227 + gDirtyAreaCache = new Set(); 1.2228 + gSeenWidgets = new Set(); 1.2229 + // Clear the saved state to ensure that defaults will be used. 1.2230 + gSavedState = null; 1.2231 + // Restore the state for each area to its defaults 1.2232 + for (let [areaId,] of gAreas) { 1.2233 + this.restoreStateForArea(areaId); 1.2234 + } 1.2235 + }, 1.2236 + 1.2237 + _resetExtraToolbars: function(aFilter = null) { 1.2238 + let firstWindow = true; // Only need to unregister and persist once 1.2239 + for (let [win, ] of gBuildWindows) { 1.2240 + let toolbox = win.gNavToolbox; 1.2241 + for (let child of toolbox.children) { 1.2242 + let matchesFilter = !aFilter || aFilter == child.id; 1.2243 + if (child.hasAttribute("customindex") && matchesFilter) { 1.2244 + let toolbarId = "toolbar" + child.getAttribute("customindex"); 1.2245 + toolbox.toolbarset.removeAttribute(toolbarId); 1.2246 + if (firstWindow) { 1.2247 + win.document.persist(toolbox.toolbarset.id, toolbarId); 1.2248 + // We have to unregister it properly to ensure we don't kill 1.2249 + // XUL widgets which might be in here 1.2250 + this.unregisterArea(child.id, true); 1.2251 + } 1.2252 + child.remove(); 1.2253 + } 1.2254 + } 1.2255 + firstWindow = false; 1.2256 + } 1.2257 + }, 1.2258 + 1.2259 + _rebuildRegisteredAreas: function() { 1.2260 + for (let [areaId, areaNodes] of gBuildAreas) { 1.2261 + let placements = gPlacements.get(areaId); 1.2262 + let isFirstChangedToolbar = true; 1.2263 + for (let areaNode of areaNodes) { 1.2264 + this.buildArea(areaId, placements, areaNode); 1.2265 + 1.2266 + let area = gAreas.get(areaId); 1.2267 + if (area.get("type") == CustomizableUI.TYPE_TOOLBAR) { 1.2268 + let defaultCollapsed = area.get("defaultCollapsed"); 1.2269 + let win = areaNode.ownerDocument.defaultView; 1.2270 + if (defaultCollapsed !== null) { 1.2271 + win.setToolbarVisibility(areaNode, !defaultCollapsed, isFirstChangedToolbar); 1.2272 + } 1.2273 + } 1.2274 + isFirstChangedToolbar = false; 1.2275 + } 1.2276 + } 1.2277 + }, 1.2278 + 1.2279 + /** 1.2280 + * Undoes a previous reset, restoring the state of the UI to the state prior to the reset. 1.2281 + */ 1.2282 + undoReset: function() { 1.2283 + if (gUIStateBeforeReset.uiCustomizationState == null || 1.2284 + gUIStateBeforeReset.drawInTitlebar == null) { 1.2285 + return; 1.2286 + } 1.2287 + gUndoResetting = true; 1.2288 + 1.2289 + let uiCustomizationState = gUIStateBeforeReset.uiCustomizationState; 1.2290 + let drawInTitlebar = gUIStateBeforeReset.drawInTitlebar; 1.2291 + 1.2292 + // Need to clear the previous state before setting the prefs 1.2293 + // because pref observers may check if there is a previous UI state. 1.2294 + this._clearPreviousUIState(); 1.2295 + 1.2296 + Services.prefs.setCharPref(kPrefCustomizationState, uiCustomizationState); 1.2297 + Services.prefs.setBoolPref(kPrefDrawInTitlebar, drawInTitlebar); 1.2298 + this.loadSavedState(); 1.2299 + // If the user just customizes toolbar/titlebar visibility, gSavedState will be null 1.2300 + // and we don't need to do anything else here: 1.2301 + if (gSavedState) { 1.2302 + for (let areaId of Object.keys(gSavedState.placements)) { 1.2303 + let placements = gSavedState.placements[areaId]; 1.2304 + gPlacements.set(areaId, placements); 1.2305 + } 1.2306 + this._rebuildRegisteredAreas(); 1.2307 + } 1.2308 + 1.2309 + gUndoResetting = false; 1.2310 + }, 1.2311 + 1.2312 + _clearPreviousUIState: function() { 1.2313 + Object.getOwnPropertyNames(gUIStateBeforeReset).forEach((prop) => { 1.2314 + gUIStateBeforeReset[prop] = null; 1.2315 + }); 1.2316 + }, 1.2317 + 1.2318 + removeExtraToolbar: function(aToolbarId) { 1.2319 + this._resetExtraToolbars(aToolbarId); 1.2320 + }, 1.2321 + 1.2322 + /** 1.2323 + * @param {String|Node} aWidget - widget ID or a widget node (preferred for performance). 1.2324 + * @return {Boolean} whether the widget is removable 1.2325 + */ 1.2326 + isWidgetRemovable: function(aWidget) { 1.2327 + let widgetId; 1.2328 + let widgetNode; 1.2329 + if (typeof aWidget == "string") { 1.2330 + widgetId = aWidget; 1.2331 + } else { 1.2332 + widgetId = aWidget.id; 1.2333 + widgetNode = aWidget; 1.2334 + } 1.2335 + let provider = this.getWidgetProvider(widgetId); 1.2336 + 1.2337 + if (provider == CustomizableUI.PROVIDER_API) { 1.2338 + return gPalette.get(widgetId).removable; 1.2339 + } 1.2340 + 1.2341 + if (provider == CustomizableUI.PROVIDER_XUL) { 1.2342 + if (gBuildWindows.size == 0) { 1.2343 + // We don't have any build windows to look at, so just assume for now 1.2344 + // that its removable. 1.2345 + return true; 1.2346 + } 1.2347 + 1.2348 + if (!widgetNode) { 1.2349 + // Pick any of the build windows to look at. 1.2350 + let [window,] = [...gBuildWindows][0]; 1.2351 + [, widgetNode] = this.getWidgetNode(widgetId, window); 1.2352 + } 1.2353 + // If we don't have a node, we assume it's removable. This can happen because 1.2354 + // getWidgetProvider returns PROVIDER_XUL by default, but this will also happen 1.2355 + // for API-provided widgets which have been destroyed. 1.2356 + if (!widgetNode) { 1.2357 + return true; 1.2358 + } 1.2359 + return widgetNode.getAttribute("removable") == "true"; 1.2360 + } 1.2361 + 1.2362 + // Otherwise this is either a special widget, which is always removable, or 1.2363 + // an API widget which has already been removed from gPalette. Returning true 1.2364 + // here allows us to then remove its ID from any placements where it might 1.2365 + // still occur. 1.2366 + return true; 1.2367 + }, 1.2368 + 1.2369 + canWidgetMoveToArea: function(aWidgetId, aArea) { 1.2370 + let placement = this.getPlacementOfWidget(aWidgetId); 1.2371 + if (placement && placement.area != aArea) { 1.2372 + // Special widgets can't move to the menu panel. 1.2373 + if (this.isSpecialWidget(aWidgetId) && gAreas.has(aArea) && 1.2374 + gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL) { 1.2375 + return false; 1.2376 + } 1.2377 + // For everything else, just return whether the widget is removable. 1.2378 + return this.isWidgetRemovable(aWidgetId); 1.2379 + } 1.2380 + 1.2381 + return true; 1.2382 + }, 1.2383 + 1.2384 + ensureWidgetPlacedInWindow: function(aWidgetId, aWindow) { 1.2385 + let placement = this.getPlacementOfWidget(aWidgetId); 1.2386 + if (!placement) { 1.2387 + return false; 1.2388 + } 1.2389 + let areaNodes = gBuildAreas.get(placement.area); 1.2390 + if (!areaNodes) { 1.2391 + return false; 1.2392 + } 1.2393 + let container = [...areaNodes].filter((n) => n.ownerDocument.defaultView == aWindow); 1.2394 + if (!container.length) { 1.2395 + return false; 1.2396 + } 1.2397 + let existingNode = container[0].getElementsByAttribute("id", aWidgetId)[0]; 1.2398 + if (existingNode) { 1.2399 + return true; 1.2400 + } 1.2401 + 1.2402 + this.insertNodeInWindow(aWidgetId, container[0], true); 1.2403 + return true; 1.2404 + }, 1.2405 + 1.2406 + get inDefaultState() { 1.2407 + for (let [areaId, props] of gAreas) { 1.2408 + let defaultPlacements = props.get("defaultPlacements"); 1.2409 + // Areas without default placements (like legacy ones?) get skipped 1.2410 + if (!defaultPlacements) { 1.2411 + continue; 1.2412 + } 1.2413 + 1.2414 + let currentPlacements = gPlacements.get(areaId); 1.2415 + // We're excluding all of the placement IDs for items that do not exist, 1.2416 + // and items that have removable="false", 1.2417 + // because we don't want to consider them when determining if we're 1.2418 + // in the default state. This way, if an add-on introduces a widget 1.2419 + // and is then uninstalled, the leftover placement doesn't cause us to 1.2420 + // automatically assume that the buttons are not in the default state. 1.2421 + let buildAreaNodes = gBuildAreas.get(areaId); 1.2422 + if (buildAreaNodes && buildAreaNodes.size) { 1.2423 + let container = [...buildAreaNodes][0]; 1.2424 + let removableOrDefault = (itemNodeOrItem) => { 1.2425 + let item = (itemNodeOrItem && itemNodeOrItem.id) || itemNodeOrItem; 1.2426 + let isRemovable = this.isWidgetRemovable(itemNodeOrItem); 1.2427 + let isInDefault = defaultPlacements.indexOf(item) != -1; 1.2428 + return isRemovable || isInDefault; 1.2429 + }; 1.2430 + // Toolbars have a currentSet property which also deals correctly with overflown 1.2431 + // widgets (if any) - use that instead: 1.2432 + if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) { 1.2433 + let currentSet = container.currentSet; 1.2434 + currentPlacements = currentSet ? currentSet.split(',') : []; 1.2435 + currentPlacements = currentPlacements.filter(removableOrDefault); 1.2436 + } else { 1.2437 + // Clone the array so we don't modify the actual placements... 1.2438 + currentPlacements = [...currentPlacements]; 1.2439 + currentPlacements = currentPlacements.filter((item) => { 1.2440 + let itemNode = container.getElementsByAttribute("id", item)[0]; 1.2441 + return itemNode && removableOrDefault(itemNode || item); 1.2442 + }); 1.2443 + } 1.2444 + 1.2445 + if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) { 1.2446 + let attribute = container.getAttribute("type") == "menubar" ? "autohide" : "collapsed"; 1.2447 + let collapsed = container.getAttribute(attribute) == "true"; 1.2448 + let defaultCollapsed = props.get("defaultCollapsed"); 1.2449 + if (defaultCollapsed !== null && collapsed != defaultCollapsed) { 1.2450 + LOG("Found " + areaId + " had non-default toolbar visibility (expected " + defaultCollapsed + ", was " + collapsed + ")"); 1.2451 + return false; 1.2452 + } 1.2453 + } 1.2454 + } 1.2455 + LOG("Checking default state for " + areaId + ":\n" + currentPlacements.join(",") + 1.2456 + "\nvs.\n" + defaultPlacements.join(",")); 1.2457 + 1.2458 + if (currentPlacements.length != defaultPlacements.length) { 1.2459 + return false; 1.2460 + } 1.2461 + 1.2462 + for (let i = 0; i < currentPlacements.length; ++i) { 1.2463 + if (currentPlacements[i] != defaultPlacements[i]) { 1.2464 + LOG("Found " + currentPlacements[i] + " in " + areaId + " where " + 1.2465 + defaultPlacements[i] + " was expected!"); 1.2466 + return false; 1.2467 + } 1.2468 + } 1.2469 + } 1.2470 + 1.2471 + if (Services.prefs.prefHasUserValue(kPrefDrawInTitlebar)) { 1.2472 + LOG(kPrefDrawInTitlebar + " pref is non-default"); 1.2473 + return false; 1.2474 + } 1.2475 + 1.2476 + return true; 1.2477 + }, 1.2478 + 1.2479 + setToolbarVisibility: function(aToolbarId, aIsVisible) { 1.2480 + // We only persist the attribute the first time. 1.2481 + let isFirstChangedToolbar = true; 1.2482 + for (let window of CustomizableUI.windows) { 1.2483 + let toolbar = window.document.getElementById(aToolbarId); 1.2484 + if (toolbar) { 1.2485 + window.setToolbarVisibility(toolbar, aIsVisible, isFirstChangedToolbar); 1.2486 + isFirstChangedToolbar = false; 1.2487 + } 1.2488 + } 1.2489 + }, 1.2490 +}; 1.2491 +Object.freeze(CustomizableUIInternal); 1.2492 + 1.2493 +this.CustomizableUI = { 1.2494 + /** 1.2495 + * Constant reference to the ID of the menu panel. 1.2496 + */ 1.2497 + get AREA_PANEL() "PanelUI-contents", 1.2498 + /** 1.2499 + * Constant reference to the ID of the navigation toolbar. 1.2500 + */ 1.2501 + get AREA_NAVBAR() "nav-bar", 1.2502 + /** 1.2503 + * Constant reference to the ID of the menubar's toolbar. 1.2504 + */ 1.2505 + get AREA_MENUBAR() "toolbar-menubar", 1.2506 + /** 1.2507 + * Constant reference to the ID of the tabstrip toolbar. 1.2508 + */ 1.2509 + get AREA_TABSTRIP() "TabsToolbar", 1.2510 + /** 1.2511 + * Constant reference to the ID of the bookmarks toolbar. 1.2512 + */ 1.2513 + get AREA_BOOKMARKS() "PersonalToolbar", 1.2514 + /** 1.2515 + * Constant reference to the ID of the addon-bar toolbar shim. 1.2516 + * Do not use, this will be removed as soon as reasonably possible. 1.2517 + * @deprecated 1.2518 + */ 1.2519 + get AREA_ADDONBAR() "addon-bar", 1.2520 + /** 1.2521 + * Constant indicating the area is a menu panel. 1.2522 + */ 1.2523 + get TYPE_MENU_PANEL() "menu-panel", 1.2524 + /** 1.2525 + * Constant indicating the area is a toolbar. 1.2526 + */ 1.2527 + get TYPE_TOOLBAR() "toolbar", 1.2528 + 1.2529 + /** 1.2530 + * Constant indicating a XUL-type provider. 1.2531 + */ 1.2532 + get PROVIDER_XUL() "xul", 1.2533 + /** 1.2534 + * Constant indicating an API-type provider. 1.2535 + */ 1.2536 + get PROVIDER_API() "api", 1.2537 + /** 1.2538 + * Constant indicating dynamic (special) widgets: spring, spacer, and separator. 1.2539 + */ 1.2540 + get PROVIDER_SPECIAL() "special", 1.2541 + 1.2542 + /** 1.2543 + * Constant indicating the widget is built-in 1.2544 + */ 1.2545 + get SOURCE_BUILTIN() "builtin", 1.2546 + /** 1.2547 + * Constant indicating the widget is externally provided 1.2548 + * (e.g. by add-ons or other items not part of the builtin widget set). 1.2549 + */ 1.2550 + get SOURCE_EXTERNAL() "external", 1.2551 + 1.2552 + /** 1.2553 + * The class used to distinguish items that span the entire menu panel. 1.2554 + */ 1.2555 + get WIDE_PANEL_CLASS() "panel-wide-item", 1.2556 + /** 1.2557 + * The (constant) number of columns in the menu panel. 1.2558 + */ 1.2559 + get PANEL_COLUMN_COUNT() 3, 1.2560 + 1.2561 + /** 1.2562 + * Constant indicating the reason the event was fired was a window closing 1.2563 + */ 1.2564 + get REASON_WINDOW_CLOSED() "window-closed", 1.2565 + /** 1.2566 + * Constant indicating the reason the event was fired was an area being 1.2567 + * unregistered separately from window closing mechanics. 1.2568 + */ 1.2569 + get REASON_AREA_UNREGISTERED() "area-unregistered", 1.2570 + 1.2571 + 1.2572 + /** 1.2573 + * An iteratable property of windows managed by CustomizableUI. 1.2574 + * Note that this can *only* be used as an iterator. ie: 1.2575 + * for (let window of CustomizableUI.windows) { ... } 1.2576 + */ 1.2577 + windows: { 1.2578 + "@@iterator": function*() { 1.2579 + for (let [window,] of gBuildWindows) 1.2580 + yield window; 1.2581 + } 1.2582 + }, 1.2583 + 1.2584 + /** 1.2585 + * Add a listener object that will get fired for various events regarding 1.2586 + * customization. 1.2587 + * 1.2588 + * @param aListener the listener object to add 1.2589 + * 1.2590 + * Not all event handler methods need to be defined. 1.2591 + * CustomizableUI will catch exceptions. Events are dispatched 1.2592 + * synchronously on the UI thread, so if you can delay any/some of your 1.2593 + * processing, that is advisable. The following event handlers are supported: 1.2594 + * - onWidgetAdded(aWidgetId, aArea, aPosition) 1.2595 + * Fired when a widget is added to an area. aWidgetId is the widget that 1.2596 + * was added, aArea the area it was added to, and aPosition the position 1.2597 + * in which it was added. 1.2598 + * - onWidgetMoved(aWidgetId, aArea, aOldPosition, aNewPosition) 1.2599 + * Fired when a widget is moved within its area. aWidgetId is the widget 1.2600 + * that was moved, aArea the area it was moved in, aOldPosition its old 1.2601 + * position, and aNewPosition its new position. 1.2602 + * - onWidgetRemoved(aWidgetId, aArea) 1.2603 + * Fired when a widget is removed from its area. aWidgetId is the widget 1.2604 + * that was removed, aArea the area it was removed from. 1.2605 + * 1.2606 + * - onWidgetBeforeDOMChange(aNode, aNextNode, aContainer, aIsRemoval) 1.2607 + * Fired *before* a widget's DOM node is acted upon by CustomizableUI 1.2608 + * (to add, move or remove it). aNode is the DOM node changed, aNextNode 1.2609 + * the DOM node (if any) before which a widget will be inserted, 1.2610 + * aContainer the *actual* DOM container (could be an overflow panel in 1.2611 + * case of an overflowable toolbar), and aWasRemoval is true iff the 1.2612 + * action about to happen is the removal of the DOM node. 1.2613 + * - onWidgetAfterDOMChange(aNode, aNextNode, aContainer, aWasRemoval) 1.2614 + * Like onWidgetBeforeDOMChange, but fired after the change to the DOM 1.2615 + * node of the widget. 1.2616 + * 1.2617 + * - onWidgetReset(aNode, aContainer) 1.2618 + * Fired after a reset to default placements moves a widget's node to a 1.2619 + * different location. aNode is the widget's node, aContainer is the 1.2620 + * area it was moved into (NB: it might already have been there and been 1.2621 + * moved to a different position!) 1.2622 + * - onWidgetUndoMove(aNode, aContainer) 1.2623 + * Fired after undoing a reset to default placements moves a widget's 1.2624 + * node to a different location. aNode is the widget's node, aContainer 1.2625 + * is the area it was moved into (NB: it might already have been there 1.2626 + * and been moved to a different position!) 1.2627 + * - onAreaReset(aArea, aContainer) 1.2628 + * Fired after a reset to default placements is complete on an area's 1.2629 + * DOM node. Note that this is fired for each DOM node. aArea is the area 1.2630 + * that was reset, aContainer the DOM node that was reset. 1.2631 + * 1.2632 + * - onWidgetCreated(aWidgetId) 1.2633 + * Fired when a widget with id aWidgetId has been created, but before it 1.2634 + * is added to any placements or any DOM nodes have been constructed. 1.2635 + * Only fired for API-based widgets. 1.2636 + * - onWidgetAfterCreation(aWidgetId, aArea) 1.2637 + * Fired after a widget with id aWidgetId has been created, and has been 1.2638 + * added to either its default area or the area in which it was placed 1.2639 + * previously. If the widget has no default area and/or it has never 1.2640 + * been placed anywhere, aArea may be null. Only fired for API-based 1.2641 + * widgets. 1.2642 + * - onWidgetDestroyed(aWidgetId) 1.2643 + * Fired when widgets are destroyed. aWidgetId is the widget that is 1.2644 + * being destroyed. Only fired for API-based widgets. 1.2645 + * - onWidgetInstanceRemoved(aWidgetId, aDocument) 1.2646 + * Fired when a window is unloaded and a widget's instance is destroyed 1.2647 + * because of this. Only fired for API-based widgets. 1.2648 + * 1.2649 + * - onWidgetDrag(aWidgetId, aArea) 1.2650 + * Fired both when and after customize mode drag handling system tries 1.2651 + * to determine the width and height of widget aWidgetId when dragged to a 1.2652 + * different area. aArea will be the area the item is dragged to, or 1.2653 + * undefined after the measurements have been done and the node has been 1.2654 + * moved back to its 'regular' area. 1.2655 + * 1.2656 + * - onCustomizeStart(aWindow) 1.2657 + * Fired when opening customize mode in aWindow. 1.2658 + * - onCustomizeEnd(aWindow) 1.2659 + * Fired when exiting customize mode in aWindow. 1.2660 + * 1.2661 + * - onWidgetOverflow(aNode, aContainer) 1.2662 + * Fired when a widget's DOM node is overflowing its container, a toolbar, 1.2663 + * and will be displayed in the overflow panel. 1.2664 + * - onWidgetUnderflow(aNode, aContainer) 1.2665 + * Fired when a widget's DOM node is *not* overflowing its container, a 1.2666 + * toolbar, anymore. 1.2667 + * - onWindowOpened(aWindow) 1.2668 + * Fired when a window has been opened that is managed by CustomizableUI, 1.2669 + * once all of the prerequisite setup has been done. 1.2670 + * - onWindowClosed(aWindow) 1.2671 + * Fired when a window that has been managed by CustomizableUI has been 1.2672 + * closed. 1.2673 + * - onAreaNodeRegistered(aArea, aContainer) 1.2674 + * Fired after an area node is first built when it is registered. This 1.2675 + * is often when the window has opened, but in the case of add-ons, 1.2676 + * could fire when the node has just been registered with CustomizableUI 1.2677 + * after an add-on update or disable/enable sequence. 1.2678 + * - onAreaNodeUnregistered(aArea, aContainer, aReason) 1.2679 + * Fired when an area node is explicitly unregistered by an API caller, 1.2680 + * or by a window closing. The aReason parameter indicates which of 1.2681 + * these is the case. 1.2682 + */ 1.2683 + addListener: function(aListener) { 1.2684 + CustomizableUIInternal.addListener(aListener); 1.2685 + }, 1.2686 + /** 1.2687 + * Remove a listener added with addListener 1.2688 + * @param aListener the listener object to remove 1.2689 + */ 1.2690 + removeListener: function(aListener) { 1.2691 + CustomizableUIInternal.removeListener(aListener); 1.2692 + }, 1.2693 + 1.2694 + /** 1.2695 + * Register a customizable area with CustomizableUI. 1.2696 + * @param aName the name of the area to register. Can only contain 1.2697 + * alphanumeric characters, dashes (-) and underscores (_). 1.2698 + * @param aProps the properties of the area. The following properties are 1.2699 + * recognized: 1.2700 + * - type: the type of area. Either TYPE_TOOLBAR (default) or 1.2701 + * TYPE_MENU_PANEL; 1.2702 + * - anchor: for a menu panel or overflowable toolbar, the 1.2703 + * anchoring node for the panel. 1.2704 + * - legacy: set to true if you want customizableui to 1.2705 + * automatically migrate the currentset attribute 1.2706 + * - overflowable: set to true if your toolbar is overflowable. 1.2707 + * This requires an anchor, and only has an 1.2708 + * effect for toolbars. 1.2709 + * - defaultPlacements: an array of widget IDs making up the 1.2710 + * default contents of the area 1.2711 + * - defaultCollapsed: (INTERNAL ONLY) applies if the type is TYPE_TOOLBAR, specifies 1.2712 + * if toolbar is collapsed by default (default to true). 1.2713 + * Specify null to ensure that reset/inDefaultArea don't care 1.2714 + * about a toolbar's collapsed state 1.2715 + */ 1.2716 + registerArea: function(aName, aProperties) { 1.2717 + CustomizableUIInternal.registerArea(aName, aProperties); 1.2718 + }, 1.2719 + /** 1.2720 + * Register a concrete node for a registered area. This method is automatically 1.2721 + * called from any toolbar in the main browser window that has its 1.2722 + * "customizable" attribute set to true. There should normally be no need to 1.2723 + * call it yourself. 1.2724 + * 1.2725 + * Note that ideally, you should register your toolbar using registerArea 1.2726 + * before any of the toolbars have their XBL bindings constructed (which 1.2727 + * will happen when they're added to the DOM and are not hidden). If you 1.2728 + * don't, and your toolbar has a defaultset attribute, CustomizableUI will 1.2729 + * register it automatically. If your toolbar does not have a defaultset 1.2730 + * attribute, the node will be saved for processing when you call 1.2731 + * registerArea. Note that CustomizableUI won't restore state in the area, 1.2732 + * allow the user to customize it in customize mode, or otherwise deal 1.2733 + * with it, until the area has been registered. 1.2734 + */ 1.2735 + registerToolbarNode: function(aToolbar, aExistingChildren) { 1.2736 + CustomizableUIInternal.registerToolbarNode(aToolbar, aExistingChildren); 1.2737 + }, 1.2738 + /** 1.2739 + * Register the menu panel node. This method should not be called by anyone 1.2740 + * apart from the built-in PanelUI. 1.2741 + * @param aPanel the panel DOM node being registered. 1.2742 + */ 1.2743 + registerMenuPanel: function(aPanel) { 1.2744 + CustomizableUIInternal.registerMenuPanel(aPanel); 1.2745 + }, 1.2746 + /** 1.2747 + * Unregister a customizable area. The inverse of registerArea. 1.2748 + * 1.2749 + * Unregistering an area will remove all the (removable) widgets in the 1.2750 + * area, which will return to the panel, and destroy all other traces 1.2751 + * of the area within CustomizableUI. Note that this means the *contents* 1.2752 + * of the area's DOM nodes will be moved to the panel or removed, but 1.2753 + * the area's DOM nodes *themselves* will stay. 1.2754 + * 1.2755 + * Furthermore, by default the placements of the area will be kept in the 1.2756 + * saved state (!) and restored if you re-register the area at a later 1.2757 + * point. This is useful for e.g. add-ons that get disabled and then 1.2758 + * re-enabled (e.g. when they update). 1.2759 + * 1.2760 + * You can override this last behaviour (and destroy the placements 1.2761 + * information in the saved state) by passing true for aDestroyPlacements. 1.2762 + * 1.2763 + * @param aName the name of the area to unregister 1.2764 + * @param aDestroyPlacements whether to destroy the placements information 1.2765 + * for the area, too. 1.2766 + */ 1.2767 + unregisterArea: function(aName, aDestroyPlacements) { 1.2768 + CustomizableUIInternal.unregisterArea(aName, aDestroyPlacements); 1.2769 + }, 1.2770 + /** 1.2771 + * Add a widget to an area. 1.2772 + * If the area to which you try to add is not known to CustomizableUI, 1.2773 + * this will throw. 1.2774 + * If the area to which you try to add has not yet been restored from its 1.2775 + * legacy state, this will postpone the addition. 1.2776 + * If the area to which you try to add is the same as the area in which 1.2777 + * the widget is currently placed, this will do the same as 1.2778 + * moveWidgetWithinArea. 1.2779 + * If the widget cannot be removed from its original location, this will 1.2780 + * no-op. 1.2781 + * 1.2782 + * This will fire an onWidgetAdded notification, 1.2783 + * and an onWidgetBeforeDOMChange and onWidgetAfterDOMChange notification 1.2784 + * for each window CustomizableUI knows about. 1.2785 + * 1.2786 + * @param aWidgetId the ID of the widget to add 1.2787 + * @param aArea the ID of the area to add the widget to 1.2788 + * @param aPosition the position at which to add the widget. If you do not 1.2789 + * pass a position, the widget will be added to the end 1.2790 + * of the area. 1.2791 + */ 1.2792 + addWidgetToArea: function(aWidgetId, aArea, aPosition) { 1.2793 + CustomizableUIInternal.addWidgetToArea(aWidgetId, aArea, aPosition); 1.2794 + }, 1.2795 + /** 1.2796 + * Remove a widget from its area. If the widget cannot be removed from its 1.2797 + * area, or is not in any area, this will no-op. Otherwise, this will fire an 1.2798 + * onWidgetRemoved notification, and an onWidgetBeforeDOMChange and 1.2799 + * onWidgetAfterDOMChange notification for each window CustomizableUI knows 1.2800 + * about. 1.2801 + * 1.2802 + * @param aWidgetId the ID of the widget to remove 1.2803 + */ 1.2804 + removeWidgetFromArea: function(aWidgetId) { 1.2805 + CustomizableUIInternal.removeWidgetFromArea(aWidgetId); 1.2806 + }, 1.2807 + /** 1.2808 + * Move a widget within an area. 1.2809 + * If the widget is not in any area, this will no-op. 1.2810 + * If the widget is already at the indicated position, this will no-op. 1.2811 + * 1.2812 + * Otherwise, this will move the widget and fire an onWidgetMoved notification, 1.2813 + * and an onWidgetBeforeDOMChange and onWidgetAfterDOMChange notification for 1.2814 + * each window CustomizableUI knows about. 1.2815 + * 1.2816 + * @param aWidgetId the ID of the widget to move 1.2817 + * @param aPosition the position to move the widget to. 1.2818 + * Negative values or values greater than the number of 1.2819 + * widgets will be interpreted to mean moving the widget to 1.2820 + * respectively the first or last position. 1.2821 + */ 1.2822 + moveWidgetWithinArea: function(aWidgetId, aPosition) { 1.2823 + CustomizableUIInternal.moveWidgetWithinArea(aWidgetId, aPosition); 1.2824 + }, 1.2825 + /** 1.2826 + * Ensure a XUL-based widget created in a window after areas were 1.2827 + * initialized moves to its correct position. 1.2828 + * This is roughly equivalent to manually looking up the position and using 1.2829 + * insertItem in the old API, but a lot less work for consumers. 1.2830 + * Always prefer this over using toolbar.insertItem (which might no-op 1.2831 + * because it delegates to addWidgetToArea) or, worse, moving items in the 1.2832 + * DOM yourself. 1.2833 + * 1.2834 + * @param aWidgetId the ID of the widget that was just created 1.2835 + * @param aWindow the window in which you want to ensure it was added. 1.2836 + * 1.2837 + * NB: why is this API per-window, you wonder? Because if you need this, 1.2838 + * presumably you yourself need to create the widget in all the windows 1.2839 + * and need to loop through them anyway. 1.2840 + */ 1.2841 + ensureWidgetPlacedInWindow: function(aWidgetId, aWindow) { 1.2842 + return CustomizableUIInternal.ensureWidgetPlacedInWindow(aWidgetId, aWindow); 1.2843 + }, 1.2844 + /** 1.2845 + * Start a batch update of items. 1.2846 + * During a batch update, the customization state is not saved to the user's 1.2847 + * preferences file, in order to reduce (possibly sync) IO. 1.2848 + * Calls to begin/endBatchUpdate may be nested. 1.2849 + * 1.2850 + * Callers should ensure that NO MATTER WHAT they call endBatchUpdate once 1.2851 + * for each call to beginBatchUpdate, even if there are exceptions in the 1.2852 + * code in the batch update. Otherwise, for the duration of the 1.2853 + * Firefox session, customization state is never saved. Typically, you 1.2854 + * would do this using a try...finally block. 1.2855 + */ 1.2856 + beginBatchUpdate: function() { 1.2857 + CustomizableUIInternal.beginBatchUpdate(); 1.2858 + }, 1.2859 + /** 1.2860 + * End a batch update. See the documentation for beginBatchUpdate above. 1.2861 + * 1.2862 + * State is not saved if we believe it is identical to the last known 1.2863 + * saved state. State is only ever saved when all batch updates have 1.2864 + * finished (ie there has been 1 endBatchUpdate call for each 1.2865 + * beginBatchUpdate call). If any of the endBatchUpdate calls pass 1.2866 + * aForceDirty=true, we will flush to the prefs file. 1.2867 + * 1.2868 + * @param aForceDirty force CustomizableUI to flush to the prefs file when 1.2869 + * all batch updates have finished. 1.2870 + */ 1.2871 + endBatchUpdate: function(aForceDirty) { 1.2872 + CustomizableUIInternal.endBatchUpdate(aForceDirty); 1.2873 + }, 1.2874 + /** 1.2875 + * Create a widget. 1.2876 + * 1.2877 + * To create a widget, you should pass an object with its desired 1.2878 + * properties. The following properties are supported: 1.2879 + * 1.2880 + * - id: the ID of the widget (required). 1.2881 + * - type: a string indicating the type of widget. Possible types 1.2882 + * are: 1.2883 + * 'button' - for simple button widgets (the default) 1.2884 + * 'view' - for buttons that open a panel or subview, 1.2885 + * depending on where they are placed. 1.2886 + * 'custom' - for fine-grained control over the creation 1.2887 + * of the widget. 1.2888 + * - viewId: Only useful for views (and required there): the id of the 1.2889 + * <panelview> that should be shown when clicking the widget. 1.2890 + * - onBuild(aDoc): Only useful for custom widgets (and required there); a 1.2891 + * function that will be invoked with the document in which 1.2892 + * to build a widget. Should return the DOM node that has 1.2893 + * been constructed. 1.2894 + * - onBeforeCreated(aDoc): Attached to all non-custom widgets; a function 1.2895 + * that will be invoked before the widget gets a DOM node 1.2896 + * constructed, passing the document in which that will happen. 1.2897 + * This is useful especially for 'view' type widgets that need 1.2898 + * to construct their views on the fly (e.g. from bootstrapped 1.2899 + * add-ons) 1.2900 + * - onCreated(aNode): Attached to all widgets; a function that will be invoked 1.2901 + * whenever the widget has a DOM node constructed, passing the 1.2902 + * constructed node as an argument. 1.2903 + * - onCommand(aEvt): Only useful for button widgets; a function that will be 1.2904 + * invoked when the user activates the button. 1.2905 + * - onClick(aEvt): Attached to all widgets; a function that will be invoked 1.2906 + * when the user clicks the widget. 1.2907 + * - onViewShowing(aEvt): Only useful for views; a function that will be 1.2908 + * invoked when a user shows your view. 1.2909 + * - onViewHiding(aEvt): Only useful for views; a function that will be 1.2910 + * invoked when a user hides your view. 1.2911 + * - tooltiptext: string to use for the tooltip of the widget 1.2912 + * - label: string to use for the label of the widget 1.2913 + * - removable: whether the widget is removable (optional, default: true) 1.2914 + * NB: if you specify false here, you must provide a 1.2915 + * defaultArea, too. 1.2916 + * - overflows: whether widget can overflow when in an overflowable 1.2917 + * toolbar (optional, default: true) 1.2918 + * - defaultArea: default area to add the widget to 1.2919 + * (optional, default: none; required if non-removable) 1.2920 + * - shortcutId: id of an element that has a shortcut for this widget 1.2921 + * (optional, default: null). This is only used to display 1.2922 + * the shortcut as part of the tooltip for builtin widgets 1.2923 + * (which have strings inside 1.2924 + * customizableWidgets.properties). If you're in an add-on, 1.2925 + * you should not set this property. 1.2926 + * - showInPrivateBrowsing: whether to show the widget in private browsing 1.2927 + * mode (optional, default: true) 1.2928 + * 1.2929 + * @param aProperties the specifications for the widget. 1.2930 + * @return a wrapper around the created widget (see getWidget) 1.2931 + */ 1.2932 + createWidget: function(aProperties) { 1.2933 + return CustomizableUIInternal.wrapWidget( 1.2934 + CustomizableUIInternal.createWidget(aProperties) 1.2935 + ); 1.2936 + }, 1.2937 + /** 1.2938 + * Destroy a widget 1.2939 + * 1.2940 + * If the widget is part of the default placements in an area, this will 1.2941 + * remove it from there. It will also remove any DOM instances. However, 1.2942 + * it will keep the widget in the placements for whatever area it was 1.2943 + * in at the time. You can remove it from there yourself by calling 1.2944 + * CustomizableUI.removeWidgetFromArea(aWidgetId). 1.2945 + * 1.2946 + * @param aWidgetId the ID of the widget to destroy 1.2947 + */ 1.2948 + destroyWidget: function(aWidgetId) { 1.2949 + CustomizableUIInternal.destroyWidget(aWidgetId); 1.2950 + }, 1.2951 + /** 1.2952 + * Get a wrapper object with information about the widget. 1.2953 + * The object provides the following properties 1.2954 + * (all read-only unless otherwise indicated): 1.2955 + * 1.2956 + * - id: the widget's ID; 1.2957 + * - type: the type of widget (button, view, custom). For 1.2958 + * XUL-provided widgets, this is always 'custom'; 1.2959 + * - provider: the provider type of the widget, id est one of 1.2960 + * PROVIDER_API or PROVIDER_XUL; 1.2961 + * - forWindow(w): a method to obtain a single window wrapper for a widget, 1.2962 + * in the window w passed as the only argument; 1.2963 + * - instances: an array of all instances (single window wrappers) 1.2964 + * of the widget. This array is NOT live; 1.2965 + * - areaType: the type of the widget's current area 1.2966 + * - isGroup: true; will be false for wrappers around single widget nodes; 1.2967 + * - source: for API-provided widgets, whether they are built-in to 1.2968 + * Firefox or add-on-provided; 1.2969 + * - disabled: for API-provided widgets, whether the widget is currently 1.2970 + * disabled. NB: this property is writable, and will toggle 1.2971 + * all the widgets' nodes' disabled states; 1.2972 + * - label: for API-provied widgets, the label of the widget; 1.2973 + * - tooltiptext: for API-provided widgets, the tooltip of the widget; 1.2974 + * - showInPrivateBrowsing: for API-provided widgets, whether the widget is 1.2975 + * visible in private browsing; 1.2976 + * 1.2977 + * Single window wrappers obtained through forWindow(someWindow) or from the 1.2978 + * instances array have the following properties 1.2979 + * (all read-only unless otherwise indicated): 1.2980 + * 1.2981 + * - id: the widget's ID; 1.2982 + * - type: the type of widget (button, view, custom). For 1.2983 + * XUL-provided widgets, this is always 'custom'; 1.2984 + * - provider: the provider type of the widget, id est one of 1.2985 + * PROVIDER_API or PROVIDER_XUL; 1.2986 + * - node: reference to the corresponding DOM node; 1.2987 + * - anchor: the anchor on which to anchor panels opened from this 1.2988 + * node. This will point to the overflow chevron on 1.2989 + * overflowable toolbars if and only if your widget node 1.2990 + * is overflowed, to the anchor for the panel menu 1.2991 + * if your widget is inside the panel menu, and to the 1.2992 + * node itself in all other cases; 1.2993 + * - overflowed: boolean indicating whether the node is currently in the 1.2994 + * overflow panel of the toolbar; 1.2995 + * - isGroup: false; will be true for the group widget; 1.2996 + * - label: for API-provided widgets, convenience getter for the 1.2997 + * label attribute of the DOM node; 1.2998 + * - tooltiptext: for API-provided widgets, convenience getter for the 1.2999 + * tooltiptext attribute of the DOM node; 1.3000 + * - disabled: for API-provided widgets, convenience getter *and setter* 1.3001 + * for the disabled state of this single widget. Note that 1.3002 + * you may prefer to use the group wrapper's getter/setter 1.3003 + * instead. 1.3004 + * 1.3005 + * @param aWidgetId the ID of the widget whose information you need 1.3006 + * @return a wrapper around the widget as described above, or null if the 1.3007 + * widget is known not to exist (anymore). NB: non-null return 1.3008 + * is no guarantee the widget exists because we cannot know in 1.3009 + * advance if a XUL widget exists or not. 1.3010 + */ 1.3011 + getWidget: function(aWidgetId) { 1.3012 + return CustomizableUIInternal.wrapWidget(aWidgetId); 1.3013 + }, 1.3014 + /** 1.3015 + * Get an array of widget wrappers (see getWidget) for all the widgets 1.3016 + * which are currently not in any area (so which are in the palette). 1.3017 + * 1.3018 + * @param aWindowPalette the palette (and by extension, the window) in which 1.3019 + * CustomizableUI should look. This matters because of 1.3020 + * course XUL-provided widgets could be available in 1.3021 + * some windows but not others, and likewise 1.3022 + * API-provided widgets might not exist in a private 1.3023 + * window (because of the showInPrivateBrowsing 1.3024 + * property). 1.3025 + * 1.3026 + * @return an array of widget wrappers (see getWidget) 1.3027 + */ 1.3028 + getUnusedWidgets: function(aWindowPalette) { 1.3029 + return CustomizableUIInternal.getUnusedWidgets(aWindowPalette).map( 1.3030 + CustomizableUIInternal.wrapWidget, 1.3031 + CustomizableUIInternal 1.3032 + ); 1.3033 + }, 1.3034 + /** 1.3035 + * Get an array of all the widget IDs placed in an area. This is roughly 1.3036 + * equivalent to fetching the currentset attribute and splitting by commas 1.3037 + * in the legacy APIs. Modifying the array will not affect CustomizableUI. 1.3038 + * 1.3039 + * @param aArea the ID of the area whose placements you want to obtain. 1.3040 + * @return an array containing the widget IDs that are in the area. 1.3041 + * 1.3042 + * NB: will throw if called too early (before placements have been fetched) 1.3043 + * or if the area is not currently known to CustomizableUI. 1.3044 + */ 1.3045 + getWidgetIdsInArea: function(aArea) { 1.3046 + if (!gAreas.has(aArea)) { 1.3047 + throw new Error("Unknown customization area: " + aArea); 1.3048 + } 1.3049 + if (!gPlacements.has(aArea)) { 1.3050 + throw new Error("Area not yet restored"); 1.3051 + } 1.3052 + 1.3053 + // We need to clone this, as we don't want to let consumers muck with placements 1.3054 + return [...gPlacements.get(aArea)]; 1.3055 + }, 1.3056 + /** 1.3057 + * Get an array of widget wrappers for all the widgets in an area. This is 1.3058 + * the same as calling getWidgetIdsInArea and .map() ing the result through 1.3059 + * CustomizableUI.getWidget. Careful: this means that if there are IDs in there 1.3060 + * which don't have corresponding DOM nodes (like in the old-style currentset 1.3061 + * attribute), there might be nulls in this array, or items for which 1.3062 + * wrapper.forWindow(win) will return null. 1.3063 + * 1.3064 + * @param aArea the ID of the area whose widgets you want to obtain. 1.3065 + * @return an array of widget wrappers and/or null values for the widget IDs 1.3066 + * placed in an area. 1.3067 + * 1.3068 + * NB: will throw if called too early (before placements have been fetched) 1.3069 + * or if the area is not currently known to CustomizableUI. 1.3070 + */ 1.3071 + getWidgetsInArea: function(aArea) { 1.3072 + return this.getWidgetIdsInArea(aArea).map( 1.3073 + CustomizableUIInternal.wrapWidget, 1.3074 + CustomizableUIInternal 1.3075 + ); 1.3076 + }, 1.3077 + /** 1.3078 + * Obtain an array of all the area IDs known to CustomizableUI. 1.3079 + * This array is created for you, so is modifiable without CustomizableUI 1.3080 + * being affected. 1.3081 + */ 1.3082 + get areas() { 1.3083 + return [area for ([area, props] of gAreas)]; 1.3084 + }, 1.3085 + /** 1.3086 + * Check what kind of area (toolbar or menu panel) an area is. This is 1.3087 + * useful if you have a widget that needs to behave differently depending 1.3088 + * on its location. Note that widget wrappers have a convenience getter 1.3089 + * property (areaType) for this purpose. 1.3090 + * 1.3091 + * @param aArea the ID of the area whose type you want to know 1.3092 + * @return TYPE_TOOLBAR or TYPE_MENU_PANEL depending on the area, null if 1.3093 + * the area is unknown. 1.3094 + */ 1.3095 + getAreaType: function(aArea) { 1.3096 + let area = gAreas.get(aArea); 1.3097 + return area ? area.get("type") : null; 1.3098 + }, 1.3099 + /** 1.3100 + * Check if a toolbar is collapsed by default. 1.3101 + * 1.3102 + * @param aArea the ID of the area whose default-collapsed state you want to know. 1.3103 + * @return `true` or `false` depending on the area, null if the area is unknown, 1.3104 + * or its collapsed state cannot normally be controlled by the user 1.3105 + */ 1.3106 + isToolbarDefaultCollapsed: function(aArea) { 1.3107 + let area = gAreas.get(aArea); 1.3108 + return area ? area.get("defaultCollapsed") : null; 1.3109 + }, 1.3110 + /** 1.3111 + * Obtain the DOM node that is the customize target for an area in a 1.3112 + * specific window. 1.3113 + * 1.3114 + * Areas can have a customization target that does not correspond to the 1.3115 + * node itself. In particular, toolbars that have a customizationtarget 1.3116 + * attribute set will have their customization target set to that node. 1.3117 + * This means widgets will end up in the customization target, not in the 1.3118 + * DOM node with the ID that corresponds to the area ID. This is useful 1.3119 + * because it lets you have fixed content in a toolbar (e.g. the panel 1.3120 + * menu item in the navbar) and have all the customizable widgets use 1.3121 + * the customization target. 1.3122 + * 1.3123 + * Using this API yourself is discouraged; you should generally not need 1.3124 + * to be asking for the DOM container node used for a particular area. 1.3125 + * In particular, if you're wanting to check it in relation to a widget's 1.3126 + * node, your DOM node might not be a direct child of the customize target 1.3127 + * in a window if, for instance, the window is in customization mode, or if 1.3128 + * this is an overflowable toolbar and the widget has been overflowed. 1.3129 + * 1.3130 + * @param aArea the ID of the area whose customize target you want to have 1.3131 + * @param aWindow the window where you want to fetch the DOM node. 1.3132 + * @return the customize target DOM node for aArea in aWindow 1.3133 + */ 1.3134 + getCustomizeTargetForArea: function(aArea, aWindow) { 1.3135 + return CustomizableUIInternal.getCustomizeTargetForArea(aArea, aWindow); 1.3136 + }, 1.3137 + /** 1.3138 + * Reset the customization state back to its default. 1.3139 + * 1.3140 + * This is the nuclear option. You should never call this except if the user 1.3141 + * explicitly requests it. Firefox does this when the user clicks the 1.3142 + * "Restore Defaults" button in customize mode. 1.3143 + */ 1.3144 + reset: function() { 1.3145 + CustomizableUIInternal.reset(); 1.3146 + }, 1.3147 + 1.3148 + /** 1.3149 + * Undo the previous reset, can only be called immediately after a reset. 1.3150 + * @return a promise that will be resolved when the operation is complete. 1.3151 + */ 1.3152 + undoReset: function() { 1.3153 + CustomizableUIInternal.undoReset(); 1.3154 + }, 1.3155 + 1.3156 + /** 1.3157 + * Remove a custom toolbar added in a previous version of Firefox or using 1.3158 + * an add-on. NB: only works on the customizable toolbars generated by 1.3159 + * the toolbox itself. Intended for use from CustomizeMode, not by 1.3160 + * other consumers. 1.3161 + * @param aToolbarId the ID of the toolbar to remove 1.3162 + */ 1.3163 + removeExtraToolbar: function(aToolbarId) { 1.3164 + CustomizableUIInternal.removeExtraToolbar(aToolbarId); 1.3165 + }, 1.3166 + 1.3167 + /** 1.3168 + * Can the last Restore Defaults operation be undone. 1.3169 + * 1.3170 + * @return A boolean stating whether an undo of the 1.3171 + * Restore Defaults can be performed. 1.3172 + */ 1.3173 + get canUndoReset() { 1.3174 + return gUIStateBeforeReset.uiCustomizationState != null || 1.3175 + gUIStateBeforeReset.drawInTitlebar != null; 1.3176 + }, 1.3177 + 1.3178 + /** 1.3179 + * Get the placement of a widget. This is by far the best way to obtain 1.3180 + * information about what the state of your widget is. The internals of 1.3181 + * this call are cheap (no DOM necessary) and you will know where the user 1.3182 + * has put your widget. 1.3183 + * 1.3184 + * @param aWidgetId the ID of the widget whose placement you want to know 1.3185 + * @return 1.3186 + * { 1.3187 + * area: "somearea", // The ID of the area where the widget is placed 1.3188 + * position: 42 // the index in the placements array corresponding to 1.3189 + * // your widget. 1.3190 + * } 1.3191 + * 1.3192 + * OR 1.3193 + * 1.3194 + * null // if the widget is not placed anywhere (ie in the palette) 1.3195 + */ 1.3196 + getPlacementOfWidget: function(aWidgetId) { 1.3197 + return CustomizableUIInternal.getPlacementOfWidget(aWidgetId, true); 1.3198 + }, 1.3199 + /** 1.3200 + * Check if a widget can be removed from the area it's in. 1.3201 + * 1.3202 + * Note that if you're wanting to move the widget somewhere, you should 1.3203 + * generally be checking canWidgetMoveToArea, because that will return 1.3204 + * true if the widget is already in the area where you want to move it (!). 1.3205 + * 1.3206 + * NB: oh, also, this method might lie if the widget in question is a 1.3207 + * XUL-provided widget and there are no windows open, because it 1.3208 + * can obviously not check anything in this case. It will return 1.3209 + * true. You will be able to move the widget elsewhere. However, 1.3210 + * once the user reopens a window, the widget will move back to its 1.3211 + * 'proper' area automagically. 1.3212 + * 1.3213 + * @param aWidgetId a widget ID or DOM node to check 1.3214 + * @return true if the widget can be removed from its area, 1.3215 + * false otherwise. 1.3216 + */ 1.3217 + isWidgetRemovable: function(aWidgetId) { 1.3218 + return CustomizableUIInternal.isWidgetRemovable(aWidgetId); 1.3219 + }, 1.3220 + /** 1.3221 + * Check if a widget can be moved to a particular area. Like 1.3222 + * isWidgetRemovable but better, because it'll return true if the widget 1.3223 + * is already in the right area. 1.3224 + * 1.3225 + * @param aWidgetId the widget ID or DOM node you want to move somewhere 1.3226 + * @param aArea the area ID you want to move it to. 1.3227 + * @return true if this is possible, false if it is not. The same caveats as 1.3228 + * for isWidgetRemovable apply, however, if no windows are open. 1.3229 + */ 1.3230 + canWidgetMoveToArea: function(aWidgetId, aArea) { 1.3231 + return CustomizableUIInternal.canWidgetMoveToArea(aWidgetId, aArea); 1.3232 + }, 1.3233 + /** 1.3234 + * Whether we're in a default state. Note that non-removable non-default 1.3235 + * widgets and non-existing widgets are not taken into account in determining 1.3236 + * whether we're in the default state. 1.3237 + * 1.3238 + * NB: this is a property with a getter. The getter is NOT cheap, because 1.3239 + * it does smart things with non-removable non-default items, non-existent 1.3240 + * items, and so forth. Please don't call unless necessary. 1.3241 + */ 1.3242 + get inDefaultState() { 1.3243 + return CustomizableUIInternal.inDefaultState; 1.3244 + }, 1.3245 + 1.3246 + /** 1.3247 + * Set a toolbar's visibility state in all windows. 1.3248 + * @param aToolbarId the toolbar whose visibility should be adjusted 1.3249 + * @param aIsVisible whether the toolbar should be visible 1.3250 + */ 1.3251 + setToolbarVisibility: function(aToolbarId, aIsVisible) { 1.3252 + CustomizableUIInternal.setToolbarVisibility(aToolbarId, aIsVisible); 1.3253 + }, 1.3254 + 1.3255 + /** 1.3256 + * Get a localized property off a (widget?) object. 1.3257 + * 1.3258 + * NB: this is unlikely to be useful unless you're in Firefox code, because 1.3259 + * this code uses the builtin widget stringbundle, and can't be told 1.3260 + * to use add-on-provided strings. It's mainly here as convenience for 1.3261 + * custom builtin widgets that build their own DOM but use the same 1.3262 + * stringbundle as the other builtin widgets. 1.3263 + * 1.3264 + * @param aWidget the object whose property we should use to fetch a 1.3265 + * localizable string; 1.3266 + * @param aProp the property on the object to use for the fetching; 1.3267 + * @param aFormatArgs (optional) any extra arguments to use for a formatted 1.3268 + * string; 1.3269 + * @param aDef (optional) the default to return if we don't find the 1.3270 + * string in the stringbundle; 1.3271 + * 1.3272 + * @return the localized string, or aDef if the string isn't in the bundle. 1.3273 + * If no default is provided, 1.3274 + * if aProp exists on aWidget, we'll return that, 1.3275 + * otherwise we'll return the empty string 1.3276 + * 1.3277 + */ 1.3278 + getLocalizedProperty: function(aWidget, aProp, aFormatArgs, aDef) { 1.3279 + return CustomizableUIInternal.getLocalizedProperty(aWidget, aProp, 1.3280 + aFormatArgs, aDef); 1.3281 + }, 1.3282 + /** 1.3283 + * Given a node, walk up to the first panel in its ancestor chain, and 1.3284 + * close it. 1.3285 + * 1.3286 + * @param aNode a node whose panel should be closed; 1.3287 + */ 1.3288 + hidePanelForNode: function(aNode) { 1.3289 + CustomizableUIInternal.hidePanelForNode(aNode); 1.3290 + }, 1.3291 + /** 1.3292 + * Check if a widget is a "special" widget: a spring, spacer or separator. 1.3293 + * 1.3294 + * @param aWidgetId the widget ID to check. 1.3295 + * @return true if the widget is 'special', false otherwise. 1.3296 + */ 1.3297 + isSpecialWidget: function(aWidgetId) { 1.3298 + return CustomizableUIInternal.isSpecialWidget(aWidgetId); 1.3299 + }, 1.3300 + /** 1.3301 + * Add listeners to a panel that will close it. For use from the menu panel 1.3302 + * and overflowable toolbar implementations, unlikely to be useful for 1.3303 + * consumers. 1.3304 + * 1.3305 + * @param aPanel the panel to which listeners should be attached. 1.3306 + */ 1.3307 + addPanelCloseListeners: function(aPanel) { 1.3308 + CustomizableUIInternal.addPanelCloseListeners(aPanel); 1.3309 + }, 1.3310 + /** 1.3311 + * Remove close listeners that have been added to a panel with 1.3312 + * addPanelCloseListeners. For use from the menu panel and overflowable 1.3313 + * toolbar implementations, unlikely to be useful for consumers. 1.3314 + * 1.3315 + * @param aPanel the panel from which listeners should be removed. 1.3316 + */ 1.3317 + removePanelCloseListeners: function(aPanel) { 1.3318 + CustomizableUIInternal.removePanelCloseListeners(aPanel); 1.3319 + }, 1.3320 + /** 1.3321 + * Notify listeners a widget is about to be dragged to an area. For use from 1.3322 + * Customize Mode only, do not use otherwise. 1.3323 + * 1.3324 + * @param aWidgetId the ID of the widget that is being dragged to an area. 1.3325 + * @param aArea the ID of the area to which the widget is being dragged. 1.3326 + */ 1.3327 + onWidgetDrag: function(aWidgetId, aArea) { 1.3328 + CustomizableUIInternal.notifyListeners("onWidgetDrag", aWidgetId, aArea); 1.3329 + }, 1.3330 + /** 1.3331 + * Notify listeners that a window is entering customize mode. For use from 1.3332 + * Customize Mode only, do not use otherwise. 1.3333 + * @param aWindow the window entering customize mode 1.3334 + */ 1.3335 + notifyStartCustomizing: function(aWindow) { 1.3336 + CustomizableUIInternal.notifyListeners("onCustomizeStart", aWindow); 1.3337 + }, 1.3338 + /** 1.3339 + * Notify listeners that a window is exiting customize mode. For use from 1.3340 + * Customize Mode only, do not use otherwise. 1.3341 + * @param aWindow the window exiting customize mode 1.3342 + */ 1.3343 + notifyEndCustomizing: function(aWindow) { 1.3344 + CustomizableUIInternal.notifyListeners("onCustomizeEnd", aWindow); 1.3345 + }, 1.3346 + 1.3347 + /** 1.3348 + * Notify toolbox(es) of a particular event. If you don't pass aWindow, 1.3349 + * all toolboxes will be notified. For use from Customize Mode only, 1.3350 + * do not use otherwise. 1.3351 + * @param aEvent the name of the event to send. 1.3352 + * @param aDetails optional, the details of the event. 1.3353 + * @param aWindow optional, the window in which to send the event. 1.3354 + */ 1.3355 + dispatchToolboxEvent: function(aEvent, aDetails={}, aWindow=null) { 1.3356 + CustomizableUIInternal.dispatchToolboxEvent(aEvent, aDetails, aWindow); 1.3357 + }, 1.3358 + 1.3359 + /** 1.3360 + * Check whether an area is overflowable. 1.3361 + * 1.3362 + * @param aAreaId the ID of an area to check for overflowable-ness 1.3363 + * @return true if the area is overflowable, false otherwise. 1.3364 + */ 1.3365 + isAreaOverflowable: function(aAreaId) { 1.3366 + let area = gAreas.get(aAreaId); 1.3367 + return area ? area.get("type") == this.TYPE_TOOLBAR && area.get("overflowable") 1.3368 + : false; 1.3369 + }, 1.3370 + /** 1.3371 + * Obtain a string indicating the place of an element. This is intended 1.3372 + * for use from customize mode; You should generally use getPlacementOfWidget 1.3373 + * instead, which is cheaper because it does not use the DOM. 1.3374 + * 1.3375 + * @param aElement the DOM node whose place we need to check 1.3376 + * @return "toolbar" if the node is in a toolbar, "panel" if it is in the 1.3377 + * menu panel, "palette" if it is in the (visible!) customization 1.3378 + * palette, undefined otherwise. 1.3379 + */ 1.3380 + getPlaceForItem: function(aElement) { 1.3381 + let place; 1.3382 + let node = aElement; 1.3383 + while (node && !place) { 1.3384 + if (node.localName == "toolbar") 1.3385 + place = "toolbar"; 1.3386 + else if (node.id == CustomizableUI.AREA_PANEL) 1.3387 + place = "panel"; 1.3388 + else if (node.id == "customization-palette") 1.3389 + place = "palette"; 1.3390 + 1.3391 + node = node.parentNode; 1.3392 + } 1.3393 + return place; 1.3394 + }, 1.3395 + 1.3396 + /** 1.3397 + * Check if a toolbar is builtin or not. 1.3398 + * @param aToolbarId the ID of the toolbar you want to check 1.3399 + */ 1.3400 + isBuiltinToolbar: function(aToolbarId) { 1.3401 + return CustomizableUIInternal._builtinToolbars.has(aToolbarId); 1.3402 + }, 1.3403 +}; 1.3404 +Object.freeze(this.CustomizableUI); 1.3405 +Object.freeze(this.CustomizableUI.windows); 1.3406 + 1.3407 +/** 1.3408 + * All external consumers of widgets are really interacting with these wrappers 1.3409 + * which provide a common interface. 1.3410 + */ 1.3411 + 1.3412 +/** 1.3413 + * WidgetGroupWrapper is the common interface for interacting with an entire 1.3414 + * widget group - AKA, all instances of a widget across a series of windows. 1.3415 + * This particular wrapper is only used for widgets created via the provider 1.3416 + * API. 1.3417 + */ 1.3418 +function WidgetGroupWrapper(aWidget) { 1.3419 + this.isGroup = true; 1.3420 + 1.3421 + const kBareProps = ["id", "source", "type", "disabled", "label", "tooltiptext", 1.3422 + "showInPrivateBrowsing"]; 1.3423 + for (let prop of kBareProps) { 1.3424 + let propertyName = prop; 1.3425 + this.__defineGetter__(propertyName, function() aWidget[propertyName]); 1.3426 + } 1.3427 + 1.3428 + this.__defineGetter__("provider", function() CustomizableUI.PROVIDER_API); 1.3429 + 1.3430 + this.__defineSetter__("disabled", function(aValue) { 1.3431 + aValue = !!aValue; 1.3432 + aWidget.disabled = aValue; 1.3433 + for (let [,instance] of aWidget.instances) { 1.3434 + instance.disabled = aValue; 1.3435 + } 1.3436 + }); 1.3437 + 1.3438 + this.forWindow = function WidgetGroupWrapper_forWindow(aWindow) { 1.3439 + let wrapperMap; 1.3440 + if (!gSingleWrapperCache.has(aWindow)) { 1.3441 + wrapperMap = new Map(); 1.3442 + gSingleWrapperCache.set(aWindow, wrapperMap); 1.3443 + } else { 1.3444 + wrapperMap = gSingleWrapperCache.get(aWindow); 1.3445 + } 1.3446 + if (wrapperMap.has(aWidget.id)) { 1.3447 + return wrapperMap.get(aWidget.id); 1.3448 + } 1.3449 + 1.3450 + let instance = aWidget.instances.get(aWindow.document); 1.3451 + if (!instance && 1.3452 + (aWidget.showInPrivateBrowsing || !PrivateBrowsingUtils.isWindowPrivate(aWindow))) { 1.3453 + instance = CustomizableUIInternal.buildWidget(aWindow.document, 1.3454 + aWidget); 1.3455 + } 1.3456 + 1.3457 + let wrapper = new WidgetSingleWrapper(aWidget, instance); 1.3458 + wrapperMap.set(aWidget.id, wrapper); 1.3459 + return wrapper; 1.3460 + }; 1.3461 + 1.3462 + this.__defineGetter__("instances", function() { 1.3463 + // Can't use gBuildWindows here because some areas load lazily: 1.3464 + let placement = CustomizableUIInternal.getPlacementOfWidget(aWidget.id); 1.3465 + if (!placement) { 1.3466 + return []; 1.3467 + } 1.3468 + let area = placement.area; 1.3469 + let buildAreas = gBuildAreas.get(area); 1.3470 + if (!buildAreas) { 1.3471 + return []; 1.3472 + } 1.3473 + return [this.forWindow(node.ownerDocument.defaultView) for (node of buildAreas)]; 1.3474 + }); 1.3475 + 1.3476 + this.__defineGetter__("areaType", function() { 1.3477 + let areaProps = gAreas.get(aWidget.currentArea); 1.3478 + return areaProps && areaProps.get("type"); 1.3479 + }); 1.3480 + 1.3481 + Object.freeze(this); 1.3482 +} 1.3483 + 1.3484 +/** 1.3485 + * A WidgetSingleWrapper is a wrapper around a single instance of a widget in 1.3486 + * a particular window. 1.3487 + */ 1.3488 +function WidgetSingleWrapper(aWidget, aNode) { 1.3489 + this.isGroup = false; 1.3490 + 1.3491 + this.node = aNode; 1.3492 + this.provider = CustomizableUI.PROVIDER_API; 1.3493 + 1.3494 + const kGlobalProps = ["id", "type"]; 1.3495 + for (let prop of kGlobalProps) { 1.3496 + this[prop] = aWidget[prop]; 1.3497 + } 1.3498 + 1.3499 + const kNodeProps = ["label", "tooltiptext"]; 1.3500 + for (let prop of kNodeProps) { 1.3501 + let propertyName = prop; 1.3502 + // Look at the node for these, instead of the widget data, to ensure the 1.3503 + // wrapper always reflects this live instance. 1.3504 + this.__defineGetter__(propertyName, 1.3505 + function() aNode.getAttribute(propertyName)); 1.3506 + } 1.3507 + 1.3508 + this.__defineGetter__("disabled", function() aNode.disabled); 1.3509 + this.__defineSetter__("disabled", function(aValue) { 1.3510 + aNode.disabled = !!aValue; 1.3511 + }); 1.3512 + 1.3513 + this.__defineGetter__("anchor", function() { 1.3514 + let anchorId; 1.3515 + // First check for an anchor for the area: 1.3516 + let placement = CustomizableUIInternal.getPlacementOfWidget(aWidget.id); 1.3517 + if (placement) { 1.3518 + anchorId = gAreas.get(placement.area).get("anchor"); 1.3519 + } 1.3520 + if (!anchorId) { 1.3521 + anchorId = aNode.getAttribute("cui-anchorid"); 1.3522 + } 1.3523 + 1.3524 + return anchorId ? aNode.ownerDocument.getElementById(anchorId) 1.3525 + : aNode; 1.3526 + }); 1.3527 + 1.3528 + this.__defineGetter__("overflowed", function() { 1.3529 + return aNode.getAttribute("overflowedItem") == "true"; 1.3530 + }); 1.3531 + 1.3532 + Object.freeze(this); 1.3533 +} 1.3534 + 1.3535 +/** 1.3536 + * XULWidgetGroupWrapper is the common interface for interacting with an entire 1.3537 + * widget group - AKA, all instances of a widget across a series of windows. 1.3538 + * This particular wrapper is only used for widgets created via the old-school 1.3539 + * XUL method (overlays, or programmatically injecting toolbaritems, or other 1.3540 + * such things). 1.3541 + */ 1.3542 +//XXXunf Going to need to hook this up to some events to keep it all live. 1.3543 +function XULWidgetGroupWrapper(aWidgetId) { 1.3544 + this.isGroup = true; 1.3545 + this.id = aWidgetId; 1.3546 + this.type = "custom"; 1.3547 + this.provider = CustomizableUI.PROVIDER_XUL; 1.3548 + 1.3549 + this.forWindow = function XULWidgetGroupWrapper_forWindow(aWindow) { 1.3550 + let wrapperMap; 1.3551 + if (!gSingleWrapperCache.has(aWindow)) { 1.3552 + wrapperMap = new Map(); 1.3553 + gSingleWrapperCache.set(aWindow, wrapperMap); 1.3554 + } else { 1.3555 + wrapperMap = gSingleWrapperCache.get(aWindow); 1.3556 + } 1.3557 + if (wrapperMap.has(aWidgetId)) { 1.3558 + return wrapperMap.get(aWidgetId); 1.3559 + } 1.3560 + 1.3561 + let instance = aWindow.document.getElementById(aWidgetId); 1.3562 + if (!instance) { 1.3563 + // Toolbar palettes aren't part of the document, so elements in there 1.3564 + // won't be found via document.getElementById(). 1.3565 + instance = aWindow.gNavToolbox.palette.getElementsByAttribute("id", aWidgetId)[0]; 1.3566 + } 1.3567 + 1.3568 + let wrapper = new XULWidgetSingleWrapper(aWidgetId, instance, aWindow.document); 1.3569 + wrapperMap.set(aWidgetId, wrapper); 1.3570 + return wrapper; 1.3571 + }; 1.3572 + 1.3573 + this.__defineGetter__("areaType", function() { 1.3574 + let placement = CustomizableUIInternal.getPlacementOfWidget(aWidgetId); 1.3575 + if (!placement) { 1.3576 + return null; 1.3577 + } 1.3578 + 1.3579 + let areaProps = gAreas.get(placement.area); 1.3580 + return areaProps && areaProps.get("type"); 1.3581 + }); 1.3582 + 1.3583 + this.__defineGetter__("instances", function() { 1.3584 + return [this.forWindow(win) for ([win,] of gBuildWindows)]; 1.3585 + }); 1.3586 + 1.3587 + Object.freeze(this); 1.3588 +} 1.3589 + 1.3590 +/** 1.3591 + * A XULWidgetSingleWrapper is a wrapper around a single instance of a XUL 1.3592 + * widget in a particular window. 1.3593 + */ 1.3594 +function XULWidgetSingleWrapper(aWidgetId, aNode, aDocument) { 1.3595 + this.isGroup = false; 1.3596 + 1.3597 + this.id = aWidgetId; 1.3598 + this.type = "custom"; 1.3599 + this.provider = CustomizableUI.PROVIDER_XUL; 1.3600 + 1.3601 + let weakDoc = Cu.getWeakReference(aDocument); 1.3602 + // If we keep a strong ref, the weak ref will never die, so null it out: 1.3603 + aDocument = null; 1.3604 + 1.3605 + this.__defineGetter__("node", function() { 1.3606 + // If we've set this to null (further down), we're sure there's nothing to 1.3607 + // be gotten here, so bail out early: 1.3608 + if (!weakDoc) { 1.3609 + return null; 1.3610 + } 1.3611 + if (aNode) { 1.3612 + // Return the last known node if it's still in the DOM... 1.3613 + if (aNode.ownerDocument.contains(aNode)) { 1.3614 + return aNode; 1.3615 + } 1.3616 + // ... or the toolbox 1.3617 + let toolbox = aNode.ownerDocument.defaultView.gNavToolbox; 1.3618 + if (toolbox && toolbox.palette && aNode.parentNode == toolbox.palette) { 1.3619 + return aNode; 1.3620 + } 1.3621 + // If it isn't, clear the cached value and fall through to the "slow" case: 1.3622 + aNode = null; 1.3623 + } 1.3624 + 1.3625 + let doc = weakDoc.get(); 1.3626 + if (doc) { 1.3627 + // Store locally so we can cache the result: 1.3628 + aNode = CustomizableUIInternal.findWidgetInWindow(aWidgetId, doc.defaultView); 1.3629 + return aNode; 1.3630 + } 1.3631 + // The weakref to the document is dead, we're done here forever more: 1.3632 + weakDoc = null; 1.3633 + return null; 1.3634 + }); 1.3635 + 1.3636 + this.__defineGetter__("anchor", function() { 1.3637 + let anchorId; 1.3638 + // First check for an anchor for the area: 1.3639 + let placement = CustomizableUIInternal.getPlacementOfWidget(aWidgetId); 1.3640 + if (placement) { 1.3641 + anchorId = gAreas.get(placement.area).get("anchor"); 1.3642 + } 1.3643 + 1.3644 + let node = this.node; 1.3645 + if (!anchorId && node) { 1.3646 + anchorId = node.getAttribute("cui-anchorid"); 1.3647 + } 1.3648 + 1.3649 + return (anchorId && node) ? node.ownerDocument.getElementById(anchorId) : node; 1.3650 + }); 1.3651 + 1.3652 + this.__defineGetter__("overflowed", function() { 1.3653 + let node = this.node; 1.3654 + if (!node) { 1.3655 + return false; 1.3656 + } 1.3657 + return node.getAttribute("overflowedItem") == "true"; 1.3658 + }); 1.3659 + 1.3660 + Object.freeze(this); 1.3661 +} 1.3662 + 1.3663 +const LAZY_RESIZE_INTERVAL_MS = 200; 1.3664 + 1.3665 +function OverflowableToolbar(aToolbarNode) { 1.3666 + this._toolbar = aToolbarNode; 1.3667 + this._collapsed = new Map(); 1.3668 + this._enabled = true; 1.3669 + 1.3670 + this._toolbar.setAttribute("overflowable", "true"); 1.3671 + let doc = this._toolbar.ownerDocument; 1.3672 + this._target = this._toolbar.customizationTarget; 1.3673 + this._list = doc.getElementById(this._toolbar.getAttribute("overflowtarget")); 1.3674 + this._list.toolbox = this._toolbar.toolbox; 1.3675 + this._list.customizationTarget = this._list; 1.3676 + 1.3677 + let window = this._toolbar.ownerDocument.defaultView; 1.3678 + if (window.gBrowserInit.delayedStartupFinished) { 1.3679 + this.init(); 1.3680 + } else { 1.3681 + Services.obs.addObserver(this, "browser-delayed-startup-finished", false); 1.3682 + } 1.3683 +} 1.3684 + 1.3685 +OverflowableToolbar.prototype = { 1.3686 + initialized: false, 1.3687 + _forceOnOverflow: false, 1.3688 + 1.3689 + observe: function(aSubject, aTopic, aData) { 1.3690 + if (aTopic == "browser-delayed-startup-finished" && 1.3691 + aSubject == this._toolbar.ownerDocument.defaultView) { 1.3692 + Services.obs.removeObserver(this, "browser-delayed-startup-finished"); 1.3693 + this.init(); 1.3694 + } 1.3695 + }, 1.3696 + 1.3697 + init: function() { 1.3698 + let doc = this._toolbar.ownerDocument; 1.3699 + let window = doc.defaultView; 1.3700 + window.addEventListener("resize", this); 1.3701 + window.gNavToolbox.addEventListener("customizationstarting", this); 1.3702 + window.gNavToolbox.addEventListener("aftercustomization", this); 1.3703 + 1.3704 + let chevronId = this._toolbar.getAttribute("overflowbutton"); 1.3705 + this._chevron = doc.getElementById(chevronId); 1.3706 + this._chevron.addEventListener("command", this); 1.3707 + 1.3708 + let panelId = this._toolbar.getAttribute("overflowpanel"); 1.3709 + this._panel = doc.getElementById(panelId); 1.3710 + this._panel.addEventListener("popuphiding", this); 1.3711 + CustomizableUIInternal.addPanelCloseListeners(this._panel); 1.3712 + 1.3713 + CustomizableUI.addListener(this); 1.3714 + 1.3715 + // The 'overflow' event may have been fired before init was called. 1.3716 + if (this._toolbar.overflowedDuringConstruction) { 1.3717 + this.onOverflow(this._toolbar.overflowedDuringConstruction); 1.3718 + this._toolbar.overflowedDuringConstruction = null; 1.3719 + } 1.3720 + 1.3721 + this.initialized = true; 1.3722 + }, 1.3723 + 1.3724 + uninit: function() { 1.3725 + this._toolbar.removeEventListener("overflow", this._toolbar); 1.3726 + this._toolbar.removeEventListener("underflow", this._toolbar); 1.3727 + this._toolbar.removeAttribute("overflowable"); 1.3728 + 1.3729 + if (!this.initialized) { 1.3730 + Services.obs.removeObserver(this, "browser-delayed-startup-finished"); 1.3731 + return; 1.3732 + } 1.3733 + 1.3734 + this._disable(); 1.3735 + 1.3736 + let window = this._toolbar.ownerDocument.defaultView; 1.3737 + window.removeEventListener("resize", this); 1.3738 + window.gNavToolbox.removeEventListener("customizationstarting", this); 1.3739 + window.gNavToolbox.removeEventListener("aftercustomization", this); 1.3740 + this._chevron.removeEventListener("command", this); 1.3741 + this._panel.removeEventListener("popuphiding", this); 1.3742 + CustomizableUI.removeListener(this); 1.3743 + CustomizableUIInternal.removePanelCloseListeners(this._panel); 1.3744 + }, 1.3745 + 1.3746 + handleEvent: function(aEvent) { 1.3747 + switch(aEvent.type) { 1.3748 + case "resize": 1.3749 + this._onResize(aEvent); 1.3750 + break; 1.3751 + case "command": 1.3752 + if (aEvent.target == this._chevron) { 1.3753 + this._onClickChevron(aEvent); 1.3754 + } else { 1.3755 + this._panel.hidePopup(); 1.3756 + } 1.3757 + break; 1.3758 + case "popuphiding": 1.3759 + this._onPanelHiding(aEvent); 1.3760 + break; 1.3761 + case "customizationstarting": 1.3762 + this._disable(); 1.3763 + break; 1.3764 + case "aftercustomization": 1.3765 + this._enable(); 1.3766 + break; 1.3767 + } 1.3768 + }, 1.3769 + 1.3770 + show: function() { 1.3771 + let deferred = Promise.defer(); 1.3772 + if (this._panel.state == "open") { 1.3773 + deferred.resolve(); 1.3774 + return deferred.promise; 1.3775 + } 1.3776 + let doc = this._panel.ownerDocument; 1.3777 + this._panel.hidden = false; 1.3778 + let contextMenu = doc.getElementById(this._panel.getAttribute("context")); 1.3779 + gELS.addSystemEventListener(contextMenu, 'command', this, true); 1.3780 + let anchor = doc.getAnonymousElementByAttribute(this._chevron, "class", "toolbarbutton-icon"); 1.3781 + this._panel.openPopup(anchor || this._chevron); 1.3782 + this._chevron.open = true; 1.3783 + 1.3784 + this._panel.addEventListener("popupshown", function onPopupShown() { 1.3785 + this.removeEventListener("popupshown", onPopupShown); 1.3786 + deferred.resolve(); 1.3787 + }); 1.3788 + 1.3789 + return deferred.promise; 1.3790 + }, 1.3791 + 1.3792 + _onClickChevron: function(aEvent) { 1.3793 + if (this._chevron.open) { 1.3794 + this._panel.hidePopup(); 1.3795 + this._chevron.open = false; 1.3796 + } else { 1.3797 + this.show(); 1.3798 + } 1.3799 + }, 1.3800 + 1.3801 + _onPanelHiding: function(aEvent) { 1.3802 + this._chevron.open = false; 1.3803 + let doc = aEvent.target.ownerDocument; 1.3804 + let contextMenu = doc.getElementById(this._panel.getAttribute("context")); 1.3805 + gELS.removeSystemEventListener(contextMenu, 'command', this, true); 1.3806 + }, 1.3807 + 1.3808 + onOverflow: function(aEvent) { 1.3809 + if (!this._enabled || 1.3810 + (aEvent && aEvent.target != this._toolbar.customizationTarget)) 1.3811 + return; 1.3812 + 1.3813 + let child = this._target.lastChild; 1.3814 + 1.3815 + while (child && this._target.scrollLeftMax > 0) { 1.3816 + let prevChild = child.previousSibling; 1.3817 + 1.3818 + if (child.getAttribute("overflows") != "false") { 1.3819 + this._collapsed.set(child.id, this._target.clientWidth); 1.3820 + child.setAttribute("overflowedItem", true); 1.3821 + child.setAttribute("cui-anchorid", this._chevron.id); 1.3822 + CustomizableUIInternal.notifyListeners("onWidgetOverflow", child, this._target); 1.3823 + 1.3824 + this._list.insertBefore(child, this._list.firstChild); 1.3825 + if (!this._toolbar.hasAttribute("overflowing")) { 1.3826 + CustomizableUI.addListener(this); 1.3827 + } 1.3828 + this._toolbar.setAttribute("overflowing", "true"); 1.3829 + } 1.3830 + child = prevChild; 1.3831 + }; 1.3832 + 1.3833 + let win = this._target.ownerDocument.defaultView; 1.3834 + win.UpdateUrlbarSearchSplitterState(); 1.3835 + }, 1.3836 + 1.3837 + _onResize: function(aEvent) { 1.3838 + if (!this._lazyResizeHandler) { 1.3839 + this._lazyResizeHandler = new DeferredTask(this._onLazyResize.bind(this), 1.3840 + LAZY_RESIZE_INTERVAL_MS); 1.3841 + } 1.3842 + this._lazyResizeHandler.arm(); 1.3843 + }, 1.3844 + 1.3845 + _moveItemsBackToTheirOrigin: function(shouldMoveAllItems) { 1.3846 + let placements = gPlacements.get(this._toolbar.id); 1.3847 + while (this._list.firstChild) { 1.3848 + let child = this._list.firstChild; 1.3849 + let minSize = this._collapsed.get(child.id); 1.3850 + 1.3851 + if (!shouldMoveAllItems && 1.3852 + minSize && 1.3853 + this._target.clientWidth <= minSize) { 1.3854 + return; 1.3855 + } 1.3856 + 1.3857 + this._collapsed.delete(child.id); 1.3858 + let beforeNodeIndex = placements.indexOf(child.id) + 1; 1.3859 + // If this is a skipintoolbarset item, meaning it doesn't occur in the placements list, 1.3860 + // we're inserting it at the end. This will mean first-in, first-out (more or less) 1.3861 + // leading to as little change in order as possible. 1.3862 + if (beforeNodeIndex == 0) { 1.3863 + beforeNodeIndex = placements.length; 1.3864 + } 1.3865 + let inserted = false; 1.3866 + for (; beforeNodeIndex < placements.length; beforeNodeIndex++) { 1.3867 + let beforeNode = this._target.getElementsByAttribute("id", placements[beforeNodeIndex])[0]; 1.3868 + if (beforeNode) { 1.3869 + this._target.insertBefore(child, beforeNode); 1.3870 + inserted = true; 1.3871 + break; 1.3872 + } 1.3873 + } 1.3874 + if (!inserted) { 1.3875 + this._target.appendChild(child); 1.3876 + } 1.3877 + child.removeAttribute("cui-anchorid"); 1.3878 + child.removeAttribute("overflowedItem"); 1.3879 + CustomizableUIInternal.notifyListeners("onWidgetUnderflow", child, this._target); 1.3880 + } 1.3881 + 1.3882 + let win = this._target.ownerDocument.defaultView; 1.3883 + win.UpdateUrlbarSearchSplitterState(); 1.3884 + 1.3885 + if (!this._collapsed.size) { 1.3886 + this._toolbar.removeAttribute("overflowing"); 1.3887 + CustomizableUI.removeListener(this); 1.3888 + } 1.3889 + }, 1.3890 + 1.3891 + _onLazyResize: function() { 1.3892 + if (!this._enabled) 1.3893 + return; 1.3894 + 1.3895 + if (this._target.scrollLeftMax > 0) { 1.3896 + this.onOverflow(); 1.3897 + } else { 1.3898 + this._moveItemsBackToTheirOrigin(); 1.3899 + } 1.3900 + }, 1.3901 + 1.3902 + _disable: function() { 1.3903 + this._enabled = false; 1.3904 + this._moveItemsBackToTheirOrigin(true); 1.3905 + if (this._lazyResizeHandler) { 1.3906 + this._lazyResizeHandler.disarm(); 1.3907 + } 1.3908 + }, 1.3909 + 1.3910 + _enable: function() { 1.3911 + this._enabled = true; 1.3912 + this.onOverflow(); 1.3913 + }, 1.3914 + 1.3915 + onWidgetBeforeDOMChange: function(aNode, aNextNode, aContainer) { 1.3916 + if (aContainer != this._target && aContainer != this._list) { 1.3917 + return; 1.3918 + } 1.3919 + // When we (re)move an item, update all the items that come after it in the list 1.3920 + // with the minsize *of the item before the to-be-removed node*. This way, we 1.3921 + // ensure that we try to move items back as soon as that's possible. 1.3922 + if (aNode.parentNode == this._list) { 1.3923 + let updatedMinSize; 1.3924 + if (aNode.previousSibling) { 1.3925 + updatedMinSize = this._collapsed.get(aNode.previousSibling.id); 1.3926 + } else { 1.3927 + // Force (these) items to try to flow back into the bar: 1.3928 + updatedMinSize = 1; 1.3929 + } 1.3930 + let nextItem = aNode.nextSibling; 1.3931 + while (nextItem) { 1.3932 + this._collapsed.set(nextItem.id, updatedMinSize); 1.3933 + nextItem = nextItem.nextSibling; 1.3934 + } 1.3935 + } 1.3936 + }, 1.3937 + 1.3938 + onWidgetAfterDOMChange: function(aNode, aNextNode, aContainer) { 1.3939 + if (aContainer != this._target && aContainer != this._list) { 1.3940 + return; 1.3941 + } 1.3942 + 1.3943 + let nowInBar = aNode.parentNode == aContainer; 1.3944 + let nowOverflowed = aNode.parentNode == this._list; 1.3945 + let wasOverflowed = this._collapsed.has(aNode.id); 1.3946 + 1.3947 + // If this wasn't overflowed before... 1.3948 + if (!wasOverflowed) { 1.3949 + // ... but it is now, then we added to the overflow panel. Exciting stuff: 1.3950 + if (nowOverflowed) { 1.3951 + // NB: we're guaranteed that it has a previousSibling, because if it didn't, 1.3952 + // we would have added it to the toolbar instead. See getOverflowedNextNode. 1.3953 + let prevId = aNode.previousSibling.id; 1.3954 + let minSize = this._collapsed.get(prevId); 1.3955 + this._collapsed.set(aNode.id, minSize); 1.3956 + aNode.setAttribute("cui-anchorid", this._chevron.id); 1.3957 + aNode.setAttribute("overflowedItem", true); 1.3958 + CustomizableUIInternal.notifyListeners("onWidgetOverflow", aNode, this._target); 1.3959 + } 1.3960 + // If it is not overflowed and not in the toolbar, and was not overflowed 1.3961 + // either, it moved out of the toolbar. That means there's now space in there! 1.3962 + // Let's try to move stuff back: 1.3963 + else if (!nowInBar) { 1.3964 + this._moveItemsBackToTheirOrigin(true); 1.3965 + } 1.3966 + // If it's in the toolbar now, then we don't care. An overflow event may 1.3967 + // fire afterwards; that's ok! 1.3968 + } 1.3969 + // If it used to be overflowed... 1.3970 + else { 1.3971 + // ... and isn't anymore, let's remove our bookkeeping: 1.3972 + if (!nowOverflowed) { 1.3973 + this._collapsed.delete(aNode.id); 1.3974 + aNode.removeAttribute("cui-anchorid"); 1.3975 + aNode.removeAttribute("overflowedItem"); 1.3976 + CustomizableUIInternal.notifyListeners("onWidgetUnderflow", aNode, this._target); 1.3977 + 1.3978 + if (!this._collapsed.size) { 1.3979 + this._toolbar.removeAttribute("overflowing"); 1.3980 + CustomizableUI.removeListener(this); 1.3981 + } 1.3982 + } 1.3983 + // but if it still is, it must have changed places. Bookkeep: 1.3984 + else { 1.3985 + if (aNode.previousSibling) { 1.3986 + let prevId = aNode.previousSibling.id; 1.3987 + let minSize = this._collapsed.get(prevId); 1.3988 + this._collapsed.set(aNode.id, minSize); 1.3989 + } else { 1.3990 + // If it's now the first item in the overflow list, 1.3991 + // maybe we can return it: 1.3992 + this._moveItemsBackToTheirOrigin(); 1.3993 + } 1.3994 + } 1.3995 + } 1.3996 + }, 1.3997 + 1.3998 + findOverflowedInsertionPoints: function(aNode) { 1.3999 + let newNodeCanOverflow = aNode.getAttribute("overflows") != "false"; 1.4000 + let areaId = this._toolbar.id; 1.4001 + let placements = gPlacements.get(areaId); 1.4002 + let nodeIndex = placements.indexOf(aNode.id); 1.4003 + let nodeBeforeNewNodeIsOverflown = false; 1.4004 + 1.4005 + let loopIndex = -1; 1.4006 + while (++loopIndex < placements.length) { 1.4007 + let nextNodeId = placements[loopIndex]; 1.4008 + if (loopIndex > nodeIndex) { 1.4009 + if (newNodeCanOverflow && this._collapsed.has(nextNodeId)) { 1.4010 + let nextNode = this._list.getElementsByAttribute("id", nextNodeId).item(0); 1.4011 + if (nextNode) { 1.4012 + return [this._list, nextNode]; 1.4013 + } 1.4014 + } 1.4015 + if (!nodeBeforeNewNodeIsOverflown || !newNodeCanOverflow) { 1.4016 + let nextNode = this._target.getElementsByAttribute("id", nextNodeId).item(0); 1.4017 + if (nextNode) { 1.4018 + return [this._target, nextNode]; 1.4019 + } 1.4020 + } 1.4021 + } else if (loopIndex < nodeIndex && this._collapsed.has(nextNodeId)) { 1.4022 + nodeBeforeNewNodeIsOverflown = true; 1.4023 + } 1.4024 + } 1.4025 + 1.4026 + let containerForAppending = (this._collapsed.size && newNodeCanOverflow) ? 1.4027 + this._list : this._target; 1.4028 + return [containerForAppending, null]; 1.4029 + }, 1.4030 + 1.4031 + getContainerFor: function(aNode) { 1.4032 + if (aNode.getAttribute("overflowedItem") == "true") { 1.4033 + return this._list; 1.4034 + } 1.4035 + return this._target; 1.4036 + }, 1.4037 +}; 1.4038 + 1.4039 +CustomizableUIInternal.initialize();