browser/components/customizableui/src/CustomizableUI.jsm

Wed, 31 Dec 2014 13:27:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 13:27:57 +0100
branch
TOR_BUG_3246
changeset 6
8bccb770b82d
permissions
-rw-r--r--

Ignore runtime configuration files generated during quality assurance.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 "use strict";
michael@0 6
michael@0 7 this.EXPORTED_SYMBOLS = ["CustomizableUI"];
michael@0 8
michael@0 9 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
michael@0 10
michael@0 11 Cu.import("resource://gre/modules/Services.jsm");
michael@0 12 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 13 XPCOMUtils.defineLazyModuleGetter(this, "PanelWideWidgetTracker",
michael@0 14 "resource:///modules/PanelWideWidgetTracker.jsm");
michael@0 15 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableWidgets",
michael@0 16 "resource:///modules/CustomizableWidgets.jsm");
michael@0 17 XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
michael@0 18 "resource://gre/modules/DeferredTask.jsm");
michael@0 19 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
michael@0 20 "resource://gre/modules/PrivateBrowsingUtils.jsm");
michael@0 21 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
michael@0 22 "resource://gre/modules/Promise.jsm");
michael@0 23 XPCOMUtils.defineLazyGetter(this, "gWidgetsBundle", function() {
michael@0 24 const kUrl = "chrome://browser/locale/customizableui/customizableWidgets.properties";
michael@0 25 return Services.strings.createBundle(kUrl);
michael@0 26 });
michael@0 27 XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils",
michael@0 28 "resource://gre/modules/ShortcutUtils.jsm");
michael@0 29 XPCOMUtils.defineLazyServiceGetter(this, "gELS",
michael@0 30 "@mozilla.org/eventlistenerservice;1", "nsIEventListenerService");
michael@0 31
michael@0 32 const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
michael@0 33
michael@0 34 const kSpecialWidgetPfx = "customizableui-special-";
michael@0 35
michael@0 36 const kPrefCustomizationState = "browser.uiCustomization.state";
michael@0 37 const kPrefCustomizationAutoAdd = "browser.uiCustomization.autoAdd";
michael@0 38 const kPrefCustomizationDebug = "browser.uiCustomization.debug";
michael@0 39 const kPrefDrawInTitlebar = "browser.tabs.drawInTitlebar";
michael@0 40
michael@0 41 /**
michael@0 42 * The keys are the handlers that are fired when the event type (the value)
michael@0 43 * is fired on the subview. A widget that provides a subview has the option
michael@0 44 * of providing onViewShowing and onViewHiding event handlers.
michael@0 45 */
michael@0 46 const kSubviewEvents = [
michael@0 47 "ViewShowing",
michael@0 48 "ViewHiding"
michael@0 49 ];
michael@0 50
michael@0 51 /**
michael@0 52 * gPalette is a map of every widget that CustomizableUI.jsm knows about, keyed
michael@0 53 * on their IDs.
michael@0 54 */
michael@0 55 let gPalette = new Map();
michael@0 56
michael@0 57 /**
michael@0 58 * gAreas maps area IDs to Sets of properties about those areas. An area is a
michael@0 59 * place where a widget can be put.
michael@0 60 */
michael@0 61 let gAreas = new Map();
michael@0 62
michael@0 63 /**
michael@0 64 * gPlacements maps area IDs to Arrays of widget IDs, indicating that the widgets
michael@0 65 * are placed within that area (either directly in the area node, or in the
michael@0 66 * customizationTarget of the node).
michael@0 67 */
michael@0 68 let gPlacements = new Map();
michael@0 69
michael@0 70 /**
michael@0 71 * gFuturePlacements represent placements that will happen for areas that have
michael@0 72 * not yet loaded (due to lazy-loading). This can occur when add-ons register
michael@0 73 * widgets.
michael@0 74 */
michael@0 75 let gFuturePlacements = new Map();
michael@0 76
michael@0 77 //XXXunf Temporary. Need a nice way to abstract functions to build widgets
michael@0 78 // of these types.
michael@0 79 let gSupportedWidgetTypes = new Set(["button", "view", "custom"]);
michael@0 80
michael@0 81 /**
michael@0 82 * gPanelsForWindow is a list of known panels in a window which we may need to close
michael@0 83 * should command events fire which target them.
michael@0 84 */
michael@0 85 let gPanelsForWindow = new WeakMap();
michael@0 86
michael@0 87 /**
michael@0 88 * gSeenWidgets remembers which widgets the user has seen for the first time
michael@0 89 * before. This way, if a new widget is created, and the user has not seen it
michael@0 90 * before, it can be put in its default location. Otherwise, it remains in the
michael@0 91 * palette.
michael@0 92 */
michael@0 93 let gSeenWidgets = new Set();
michael@0 94
michael@0 95 /**
michael@0 96 * gDirtyAreaCache is a set of area IDs for areas where items have been added,
michael@0 97 * moved or removed at least once. This set is persisted, and is used to
michael@0 98 * optimize building of toolbars in the default case where no toolbars should
michael@0 99 * be "dirty".
michael@0 100 */
michael@0 101 let gDirtyAreaCache = new Set();
michael@0 102
michael@0 103 /**
michael@0 104 * gPendingBuildAreas is a map from area IDs to map from build nodes to their
michael@0 105 * existing children at the time of node registration, that are waiting
michael@0 106 * for the area to be registered
michael@0 107 */
michael@0 108 let gPendingBuildAreas = new Map();
michael@0 109
michael@0 110 let gSavedState = null;
michael@0 111 let gRestoring = false;
michael@0 112 let gDirty = false;
michael@0 113 let gInBatchStack = 0;
michael@0 114 let gResetting = false;
michael@0 115 let gUndoResetting = false;
michael@0 116
michael@0 117 /**
michael@0 118 * gBuildAreas maps area IDs to actual area nodes within browser windows.
michael@0 119 */
michael@0 120 let gBuildAreas = new Map();
michael@0 121
michael@0 122 /**
michael@0 123 * gBuildWindows is a map of windows that have registered build areas, mapped
michael@0 124 * to a Set of known toolboxes in that window.
michael@0 125 */
michael@0 126 let gBuildWindows = new Map();
michael@0 127
michael@0 128 let gNewElementCount = 0;
michael@0 129 let gGroupWrapperCache = new Map();
michael@0 130 let gSingleWrapperCache = new WeakMap();
michael@0 131 let gListeners = new Set();
michael@0 132
michael@0 133 let gUIStateBeforeReset = {
michael@0 134 uiCustomizationState: null,
michael@0 135 drawInTitlebar: null,
michael@0 136 };
michael@0 137
michael@0 138 let gModuleName = "[CustomizableUI]";
michael@0 139 #include logging.js
michael@0 140
michael@0 141 let CustomizableUIInternal = {
michael@0 142 initialize: function() {
michael@0 143 LOG("Initializing");
michael@0 144
michael@0 145 this.addListener(this);
michael@0 146 this._defineBuiltInWidgets();
michael@0 147 this.loadSavedState();
michael@0 148
michael@0 149 let panelPlacements = [
michael@0 150 "edit-controls",
michael@0 151 "zoom-controls",
michael@0 152 "new-window-button",
michael@0 153 "privatebrowsing-button",
michael@0 154 "save-page-button",
michael@0 155 "print-button",
michael@0 156 "history-panelmenu",
michael@0 157 "fullscreen-button",
michael@0 158 "find-button",
michael@0 159 "preferences-button",
michael@0 160 "add-ons-button",
michael@0 161 "developer-button",
michael@0 162 ];
michael@0 163
michael@0 164 if (gPalette.has("switch-to-metro-button")) {
michael@0 165 panelPlacements.push("switch-to-metro-button");
michael@0 166 }
michael@0 167
michael@0 168 #ifdef NIGHTLY_BUILD
michael@0 169 if (gPalette.has("e10s-button")) {
michael@0 170 let newWindowIndex = panelPlacements.indexOf("new-window-button");
michael@0 171 if (newWindowIndex > -1) {
michael@0 172 panelPlacements.splice(newWindowIndex + 1, 0, "e10s-button");
michael@0 173 }
michael@0 174 }
michael@0 175 #endif
michael@0 176
michael@0 177 let showCharacterEncoding = Services.prefs.getComplexValue(
michael@0 178 "browser.menu.showCharacterEncoding",
michael@0 179 Ci.nsIPrefLocalizedString
michael@0 180 ).data;
michael@0 181 if (showCharacterEncoding == "true") {
michael@0 182 panelPlacements.push("characterencoding-button");
michael@0 183 }
michael@0 184
michael@0 185 this.registerArea(CustomizableUI.AREA_PANEL, {
michael@0 186 anchor: "PanelUI-menu-button",
michael@0 187 type: CustomizableUI.TYPE_MENU_PANEL,
michael@0 188 defaultPlacements: panelPlacements
michael@0 189 }, true);
michael@0 190 PanelWideWidgetTracker.init();
michael@0 191
michael@0 192 this.registerArea(CustomizableUI.AREA_NAVBAR, {
michael@0 193 legacy: true,
michael@0 194 type: CustomizableUI.TYPE_TOOLBAR,
michael@0 195 overflowable: true,
michael@0 196 defaultPlacements: [
michael@0 197 "urlbar-container",
michael@0 198 "search-container",
michael@0 199 "webrtc-status-button",
michael@0 200 "bookmarks-menu-button",
michael@0 201 "downloads-button",
michael@0 202 "home-button",
michael@0 203 "social-share-button",
michael@0 204 ],
michael@0 205 defaultCollapsed: false,
michael@0 206 }, true);
michael@0 207 #ifndef XP_MACOSX
michael@0 208 this.registerArea(CustomizableUI.AREA_MENUBAR, {
michael@0 209 legacy: true,
michael@0 210 type: CustomizableUI.TYPE_TOOLBAR,
michael@0 211 defaultPlacements: [
michael@0 212 "menubar-items",
michael@0 213 ],
michael@0 214 get defaultCollapsed() {
michael@0 215 #ifdef MENUBAR_CAN_AUTOHIDE
michael@0 216 #if defined(MOZ_WIDGET_GTK) || defined(MOZ_WIDGET_QT)
michael@0 217 return true;
michael@0 218 #else
michael@0 219 // This is duplicated logic from /browser/base/jar.mn
michael@0 220 // for win6BrowserOverlay.xul.
michael@0 221 return Services.appinfo.OS == "WINNT" &&
michael@0 222 Services.sysinfo.getProperty("version") != "5.1";
michael@0 223 #endif
michael@0 224 #endif
michael@0 225 return false;
michael@0 226 }
michael@0 227 }, true);
michael@0 228 #endif
michael@0 229 this.registerArea(CustomizableUI.AREA_TABSTRIP, {
michael@0 230 legacy: true,
michael@0 231 type: CustomizableUI.TYPE_TOOLBAR,
michael@0 232 defaultPlacements: [
michael@0 233 "tabbrowser-tabs",
michael@0 234 "new-tab-button",
michael@0 235 "alltabs-button",
michael@0 236 ],
michael@0 237 defaultCollapsed: null,
michael@0 238 }, true);
michael@0 239 this.registerArea(CustomizableUI.AREA_BOOKMARKS, {
michael@0 240 legacy: true,
michael@0 241 type: CustomizableUI.TYPE_TOOLBAR,
michael@0 242 defaultPlacements: [
michael@0 243 "personal-bookmarks",
michael@0 244 ],
michael@0 245 defaultCollapsed: true,
michael@0 246 }, true);
michael@0 247
michael@0 248 this.registerArea(CustomizableUI.AREA_ADDONBAR, {
michael@0 249 type: CustomizableUI.TYPE_TOOLBAR,
michael@0 250 legacy: true,
michael@0 251 defaultPlacements: ["addonbar-closebutton", "status-bar"],
michael@0 252 defaultCollapsed: false,
michael@0 253 }, true);
michael@0 254 },
michael@0 255
michael@0 256 get _builtinToolbars() {
michael@0 257 return new Set([
michael@0 258 CustomizableUI.AREA_NAVBAR,
michael@0 259 CustomizableUI.AREA_BOOKMARKS,
michael@0 260 CustomizableUI.AREA_TABSTRIP,
michael@0 261 CustomizableUI.AREA_ADDONBAR,
michael@0 262 #ifndef XP_MACOSX
michael@0 263 CustomizableUI.AREA_MENUBAR,
michael@0 264 #endif
michael@0 265 ]);
michael@0 266 },
michael@0 267
michael@0 268 _defineBuiltInWidgets: function() {
michael@0 269 //XXXunf Need to figure out how to auto-add new builtin widgets in new
michael@0 270 // app versions to already customized areas.
michael@0 271 for (let widgetDefinition of CustomizableWidgets) {
michael@0 272 this.createBuiltinWidget(widgetDefinition);
michael@0 273 }
michael@0 274 },
michael@0 275
michael@0 276 wrapWidget: function(aWidgetId) {
michael@0 277 if (gGroupWrapperCache.has(aWidgetId)) {
michael@0 278 return gGroupWrapperCache.get(aWidgetId);
michael@0 279 }
michael@0 280
michael@0 281 let provider = this.getWidgetProvider(aWidgetId);
michael@0 282 if (!provider) {
michael@0 283 return null;
michael@0 284 }
michael@0 285
michael@0 286 if (provider == CustomizableUI.PROVIDER_API) {
michael@0 287 let widget = gPalette.get(aWidgetId);
michael@0 288 if (!widget.wrapper) {
michael@0 289 widget.wrapper = new WidgetGroupWrapper(widget);
michael@0 290 gGroupWrapperCache.set(aWidgetId, widget.wrapper);
michael@0 291 }
michael@0 292 return widget.wrapper;
michael@0 293 }
michael@0 294
michael@0 295 // PROVIDER_SPECIAL gets treated the same as PROVIDER_XUL.
michael@0 296 let wrapper = new XULWidgetGroupWrapper(aWidgetId);
michael@0 297 gGroupWrapperCache.set(aWidgetId, wrapper);
michael@0 298 return wrapper;
michael@0 299 },
michael@0 300
michael@0 301 registerArea: function(aName, aProperties, aInternalCaller) {
michael@0 302 if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) {
michael@0 303 throw new Error("Invalid area name");
michael@0 304 }
michael@0 305
michael@0 306 let areaIsKnown = gAreas.has(aName);
michael@0 307 let props = areaIsKnown ? gAreas.get(aName) : new Map();
michael@0 308 const kImmutableProperties = new Set(["type", "legacy", "overflowable"]);
michael@0 309 for (let key in aProperties) {
michael@0 310 if (areaIsKnown && kImmutableProperties.has(key) &&
michael@0 311 props.get(key) != aProperties[key]) {
michael@0 312 throw new Error("An area cannot change the property for '" + key + "'");
michael@0 313 }
michael@0 314 //XXXgijs for special items, we need to make sure they have an appropriate ID
michael@0 315 // so we aren't perpetually in a non-default state:
michael@0 316 if (key == "defaultPlacements" && Array.isArray(aProperties[key])) {
michael@0 317 props.set(key, aProperties[key].map(x => this.isSpecialWidget(x) ? this.ensureSpecialWidgetId(x) : x ));
michael@0 318 } else {
michael@0 319 props.set(key, aProperties[key]);
michael@0 320 }
michael@0 321 }
michael@0 322 // Default to a toolbar:
michael@0 323 if (!props.has("type")) {
michael@0 324 props.set("type", CustomizableUI.TYPE_TOOLBAR);
michael@0 325 }
michael@0 326 if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) {
michael@0 327 // Check aProperties instead of props because this check is only interested
michael@0 328 // in the passed arguments, not the state of a potentially pre-existing area.
michael@0 329 if (!aInternalCaller && aProperties["defaultCollapsed"]) {
michael@0 330 throw new Error("defaultCollapsed is only allowed for default toolbars.")
michael@0 331 }
michael@0 332 if (!props.has("defaultCollapsed")) {
michael@0 333 props.set("defaultCollapsed", true);
michael@0 334 }
michael@0 335 } else if (props.has("defaultCollapsed")) {
michael@0 336 throw new Error("defaultCollapsed only applies for TYPE_TOOLBAR areas.");
michael@0 337 }
michael@0 338 // Sanity check type:
michael@0 339 let allTypes = [CustomizableUI.TYPE_TOOLBAR, CustomizableUI.TYPE_MENU_PANEL];
michael@0 340 if (allTypes.indexOf(props.get("type")) == -1) {
michael@0 341 throw new Error("Invalid area type " + props.get("type"));
michael@0 342 }
michael@0 343
michael@0 344 // And to no placements:
michael@0 345 if (!props.has("defaultPlacements")) {
michael@0 346 props.set("defaultPlacements", []);
michael@0 347 }
michael@0 348 // Sanity check default placements array:
michael@0 349 if (!Array.isArray(props.get("defaultPlacements"))) {
michael@0 350 throw new Error("Should provide an array of default placements");
michael@0 351 }
michael@0 352
michael@0 353 if (!areaIsKnown) {
michael@0 354 gAreas.set(aName, props);
michael@0 355
michael@0 356 if (props.get("legacy") && !gPlacements.has(aName)) {
michael@0 357 // Guarantee this area exists in gFuturePlacements, to avoid checking it in
michael@0 358 // various places elsewhere.
michael@0 359 gFuturePlacements.set(aName, new Set());
michael@0 360 } else {
michael@0 361 this.restoreStateForArea(aName);
michael@0 362 }
michael@0 363
michael@0 364 // If we have pending build area nodes, register all of them
michael@0 365 if (gPendingBuildAreas.has(aName)) {
michael@0 366 let pendingNodes = gPendingBuildAreas.get(aName);
michael@0 367 for (let [pendingNode, existingChildren] of pendingNodes) {
michael@0 368 this.registerToolbarNode(pendingNode, existingChildren);
michael@0 369 }
michael@0 370 gPendingBuildAreas.delete(aName);
michael@0 371 }
michael@0 372 }
michael@0 373 },
michael@0 374
michael@0 375 unregisterArea: function(aName, aDestroyPlacements) {
michael@0 376 if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) {
michael@0 377 throw new Error("Invalid area name");
michael@0 378 }
michael@0 379 if (!gAreas.has(aName) && !gPlacements.has(aName)) {
michael@0 380 throw new Error("Area not registered");
michael@0 381 }
michael@0 382
michael@0 383 // Move all the widgets out
michael@0 384 this.beginBatchUpdate();
michael@0 385 try {
michael@0 386 let placements = gPlacements.get(aName);
michael@0 387 if (placements) {
michael@0 388 // Need to clone this array so removeWidgetFromArea doesn't modify it
michael@0 389 placements = [...placements];
michael@0 390 placements.forEach(this.removeWidgetFromArea, this);
michael@0 391 }
michael@0 392
michael@0 393 // Delete all remaining traces.
michael@0 394 gAreas.delete(aName);
michael@0 395 // Only destroy placements when necessary:
michael@0 396 if (aDestroyPlacements) {
michael@0 397 gPlacements.delete(aName);
michael@0 398 } else {
michael@0 399 // Otherwise we need to re-set them, as removeFromArea will have emptied
michael@0 400 // them out:
michael@0 401 gPlacements.set(aName, placements);
michael@0 402 }
michael@0 403 gFuturePlacements.delete(aName);
michael@0 404 let existingAreaNodes = gBuildAreas.get(aName);
michael@0 405 if (existingAreaNodes) {
michael@0 406 for (let areaNode of existingAreaNodes) {
michael@0 407 this.notifyListeners("onAreaNodeUnregistered", aName, areaNode.customizationTarget,
michael@0 408 CustomizableUI.REASON_AREA_UNREGISTERED);
michael@0 409 }
michael@0 410 }
michael@0 411 gBuildAreas.delete(aName);
michael@0 412 } finally {
michael@0 413 this.endBatchUpdate(true);
michael@0 414 }
michael@0 415 },
michael@0 416
michael@0 417 registerToolbarNode: function(aToolbar, aExistingChildren) {
michael@0 418 let area = aToolbar.id;
michael@0 419 if (gBuildAreas.has(area) && gBuildAreas.get(area).has(aToolbar)) {
michael@0 420 return;
michael@0 421 }
michael@0 422 let document = aToolbar.ownerDocument;
michael@0 423 let areaProperties = gAreas.get(area);
michael@0 424
michael@0 425 // If this area is not registered, try to do it automatically:
michael@0 426 if (!areaProperties) {
michael@0 427 // If there's no defaultset attribute and this isn't a legacy extra toolbar,
michael@0 428 // we assume that we should wait for registerArea to be called:
michael@0 429 if (!aToolbar.hasAttribute("defaultset") &&
michael@0 430 !aToolbar.hasAttribute("customindex")) {
michael@0 431 if (!gPendingBuildAreas.has(area)) {
michael@0 432 gPendingBuildAreas.set(area, new Map());
michael@0 433 }
michael@0 434 let pendingNodes = gPendingBuildAreas.get(area);
michael@0 435 pendingNodes.set(aToolbar, aExistingChildren);
michael@0 436 return;
michael@0 437 }
michael@0 438 let props = {type: CustomizableUI.TYPE_TOOLBAR, legacy: true};
michael@0 439 let defaultsetAttribute = aToolbar.getAttribute("defaultset") || "";
michael@0 440 props.defaultPlacements = defaultsetAttribute.split(',').filter(s => s);
michael@0 441 this.registerArea(area, props);
michael@0 442 areaProperties = gAreas.get(area);
michael@0 443 }
michael@0 444
michael@0 445 this.beginBatchUpdate();
michael@0 446 try {
michael@0 447 let placements = gPlacements.get(area);
michael@0 448 if (!placements && areaProperties.has("legacy")) {
michael@0 449 let legacyState = aToolbar.getAttribute("currentset");
michael@0 450 if (legacyState) {
michael@0 451 legacyState = legacyState.split(",").filter(s => s);
michael@0 452 }
michael@0 453
michael@0 454 // Manually restore the state here, so the legacy state can be converted.
michael@0 455 this.restoreStateForArea(area, legacyState);
michael@0 456 placements = gPlacements.get(area);
michael@0 457 }
michael@0 458
michael@0 459 // Check that the current children and the current placements match. If
michael@0 460 // not, mark it as dirty:
michael@0 461 if (aExistingChildren.length != placements.length ||
michael@0 462 aExistingChildren.every((id, i) => id == placements[i])) {
michael@0 463 gDirtyAreaCache.add(area);
michael@0 464 }
michael@0 465
michael@0 466 if (areaProperties.has("overflowable")) {
michael@0 467 aToolbar.overflowable = new OverflowableToolbar(aToolbar);
michael@0 468 }
michael@0 469
michael@0 470 this.registerBuildArea(area, aToolbar);
michael@0 471
michael@0 472 // We only build the toolbar if it's been marked as "dirty". Dirty means
michael@0 473 // one of the following things:
michael@0 474 // 1) Items have been added, moved or removed from this toolbar before.
michael@0 475 // 2) The number of children of the toolbar does not match the length of
michael@0 476 // the placements array for that area.
michael@0 477 //
michael@0 478 // This notion of being "dirty" is stored in a cache which is persisted
michael@0 479 // in the saved state.
michael@0 480 if (gDirtyAreaCache.has(area)) {
michael@0 481 this.buildArea(area, placements, aToolbar);
michael@0 482 }
michael@0 483 this.notifyListeners("onAreaNodeRegistered", area, aToolbar.customizationTarget);
michael@0 484 aToolbar.setAttribute("currentset", placements.join(","));
michael@0 485 } finally {
michael@0 486 this.endBatchUpdate();
michael@0 487 }
michael@0 488 },
michael@0 489
michael@0 490 buildArea: function(aArea, aPlacements, aAreaNode) {
michael@0 491 let document = aAreaNode.ownerDocument;
michael@0 492 let window = document.defaultView;
michael@0 493 let inPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(window);
michael@0 494 let container = aAreaNode.customizationTarget;
michael@0 495 let areaIsPanel = gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL;
michael@0 496
michael@0 497 if (!container) {
michael@0 498 throw new Error("Expected area " + aArea
michael@0 499 + " to have a customizationTarget attribute.");
michael@0 500 }
michael@0 501
michael@0 502 // Restore nav-bar visibility since it may have been hidden
michael@0 503 // through a migration path (bug 938980) or an add-on.
michael@0 504 if (aArea == CustomizableUI.AREA_NAVBAR) {
michael@0 505 aAreaNode.collapsed = false;
michael@0 506 }
michael@0 507
michael@0 508 this.beginBatchUpdate();
michael@0 509
michael@0 510 try {
michael@0 511 let currentNode = container.firstChild;
michael@0 512 let placementsToRemove = new Set();
michael@0 513 for (let id of aPlacements) {
michael@0 514 while (currentNode && currentNode.getAttribute("skipintoolbarset") == "true") {
michael@0 515 currentNode = currentNode.nextSibling;
michael@0 516 }
michael@0 517
michael@0 518 if (currentNode && currentNode.id == id) {
michael@0 519 currentNode = currentNode.nextSibling;
michael@0 520 continue;
michael@0 521 }
michael@0 522
michael@0 523 if (this.isSpecialWidget(id) && areaIsPanel) {
michael@0 524 placementsToRemove.add(id);
michael@0 525 continue;
michael@0 526 }
michael@0 527
michael@0 528 let [provider, node] = this.getWidgetNode(id, window);
michael@0 529 if (!node) {
michael@0 530 LOG("Unknown widget: " + id);
michael@0 531 continue;
michael@0 532 }
michael@0 533
michael@0 534 // If the placements have items in them which are (now) no longer removable,
michael@0 535 // we shouldn't be moving them:
michael@0 536 if (provider == CustomizableUI.PROVIDER_API) {
michael@0 537 let widgetInfo = gPalette.get(id);
michael@0 538 if (!widgetInfo.removable && aArea != widgetInfo.defaultArea) {
michael@0 539 placementsToRemove.add(id);
michael@0 540 continue;
michael@0 541 }
michael@0 542 } else if (provider == CustomizableUI.PROVIDER_XUL &&
michael@0 543 node.parentNode != container && !this.isWidgetRemovable(node)) {
michael@0 544 placementsToRemove.add(id);
michael@0 545 continue;
michael@0 546 } // Special widgets are always removable, so no need to check them
michael@0 547
michael@0 548 if (inPrivateWindow && provider == CustomizableUI.PROVIDER_API) {
michael@0 549 let widget = gPalette.get(id);
michael@0 550 if (!widget.showInPrivateBrowsing && inPrivateWindow) {
michael@0 551 continue;
michael@0 552 }
michael@0 553 }
michael@0 554
michael@0 555 this.ensureButtonContextMenu(node, aAreaNode);
michael@0 556 if (node.localName == "toolbarbutton") {
michael@0 557 if (areaIsPanel) {
michael@0 558 node.setAttribute("wrap", "true");
michael@0 559 } else {
michael@0 560 node.removeAttribute("wrap");
michael@0 561 }
michael@0 562 }
michael@0 563
michael@0 564 this.insertWidgetBefore(node, currentNode, container, aArea);
michael@0 565 if (gResetting) {
michael@0 566 this.notifyListeners("onWidgetReset", node, container);
michael@0 567 } else if (gUndoResetting) {
michael@0 568 this.notifyListeners("onWidgetUndoMove", node, container);
michael@0 569 }
michael@0 570 }
michael@0 571
michael@0 572 if (currentNode) {
michael@0 573 let palette = aAreaNode.toolbox ? aAreaNode.toolbox.palette : null;
michael@0 574 let limit = currentNode.previousSibling;
michael@0 575 let node = container.lastChild;
michael@0 576 while (node && node != limit) {
michael@0 577 let previousSibling = node.previousSibling;
michael@0 578 // Nodes opt-in to removability. If they're removable, and we haven't
michael@0 579 // seen them in the placements array, then we toss them into the palette
michael@0 580 // if one exists. If no palette exists, we just remove the node. If the
michael@0 581 // node is not removable, we leave it where it is. However, we can only
michael@0 582 // safely touch elements that have an ID - both because we depend on
michael@0 583 // IDs, and because such elements are not intended to be widgets
michael@0 584 // (eg, titlebar-placeholder elements).
michael@0 585 if (node.id && node.getAttribute("skipintoolbarset") != "true") {
michael@0 586 if (this.isWidgetRemovable(node)) {
michael@0 587 if (palette && !this.isSpecialWidget(node.id)) {
michael@0 588 palette.appendChild(node);
michael@0 589 this.removeLocationAttributes(node);
michael@0 590 } else {
michael@0 591 container.removeChild(node);
michael@0 592 }
michael@0 593 } else {
michael@0 594 this.setLocationAttributes(currentNode, aArea);
michael@0 595 node.setAttribute("removable", false);
michael@0 596 LOG("Adding non-removable widget to placements of " + aArea + ": " +
michael@0 597 node.id);
michael@0 598 gPlacements.get(aArea).push(node.id);
michael@0 599 gDirty = true;
michael@0 600 }
michael@0 601 }
michael@0 602 node = previousSibling;
michael@0 603 }
michael@0 604 }
michael@0 605
michael@0 606 // If there are placements in here which aren't removable from their original area,
michael@0 607 // we remove them from this area's placement array. They will (have) be(en) added
michael@0 608 // to their original area's placements array in the block above this one.
michael@0 609 if (placementsToRemove.size) {
michael@0 610 let placementAry = gPlacements.get(aArea);
michael@0 611 for (let id of placementsToRemove) {
michael@0 612 let index = placementAry.indexOf(id);
michael@0 613 placementAry.splice(index, 1);
michael@0 614 }
michael@0 615 }
michael@0 616
michael@0 617 if (gResetting) {
michael@0 618 this.notifyListeners("onAreaReset", aArea, container);
michael@0 619 }
michael@0 620 } finally {
michael@0 621 this.endBatchUpdate();
michael@0 622 }
michael@0 623 },
michael@0 624
michael@0 625 addPanelCloseListeners: function(aPanel) {
michael@0 626 gELS.addSystemEventListener(aPanel, "click", this, false);
michael@0 627 gELS.addSystemEventListener(aPanel, "keypress", this, false);
michael@0 628 let win = aPanel.ownerDocument.defaultView;
michael@0 629 if (!gPanelsForWindow.has(win)) {
michael@0 630 gPanelsForWindow.set(win, new Set());
michael@0 631 }
michael@0 632 gPanelsForWindow.get(win).add(this._getPanelForNode(aPanel));
michael@0 633 },
michael@0 634
michael@0 635 removePanelCloseListeners: function(aPanel) {
michael@0 636 gELS.removeSystemEventListener(aPanel, "click", this, false);
michael@0 637 gELS.removeSystemEventListener(aPanel, "keypress", this, false);
michael@0 638 let win = aPanel.ownerDocument.defaultView;
michael@0 639 let panels = gPanelsForWindow.get(win);
michael@0 640 if (panels) {
michael@0 641 panels.delete(this._getPanelForNode(aPanel));
michael@0 642 }
michael@0 643 },
michael@0 644
michael@0 645 ensureButtonContextMenu: function(aNode, aAreaNode) {
michael@0 646 const kPanelItemContextMenu = "customizationPanelItemContextMenu";
michael@0 647
michael@0 648 let currentContextMenu = aNode.getAttribute("context") ||
michael@0 649 aNode.getAttribute("contextmenu");
michael@0 650 let place = CustomizableUI.getPlaceForItem(aAreaNode);
michael@0 651 let contextMenuForPlace = place == "panel" ?
michael@0 652 kPanelItemContextMenu :
michael@0 653 null;
michael@0 654 if (contextMenuForPlace && !currentContextMenu) {
michael@0 655 aNode.setAttribute("context", contextMenuForPlace);
michael@0 656 } else if (currentContextMenu == kPanelItemContextMenu &&
michael@0 657 contextMenuForPlace != kPanelItemContextMenu) {
michael@0 658 aNode.removeAttribute("context");
michael@0 659 aNode.removeAttribute("contextmenu");
michael@0 660 }
michael@0 661 },
michael@0 662
michael@0 663 getWidgetProvider: function(aWidgetId) {
michael@0 664 if (this.isSpecialWidget(aWidgetId)) {
michael@0 665 return CustomizableUI.PROVIDER_SPECIAL;
michael@0 666 }
michael@0 667 if (gPalette.has(aWidgetId)) {
michael@0 668 return CustomizableUI.PROVIDER_API;
michael@0 669 }
michael@0 670 // If this was an API widget that was destroyed, return null:
michael@0 671 if (gSeenWidgets.has(aWidgetId)) {
michael@0 672 return null;
michael@0 673 }
michael@0 674
michael@0 675 // We fall back to the XUL provider, but we don't know for sure (at this
michael@0 676 // point) whether it exists there either. So the API is technically lying.
michael@0 677 // Ideally, it would be able to return an error value (or throw an
michael@0 678 // exception) if it really didn't exist. Our code calling this function
michael@0 679 // handles that fine, but this is a public API.
michael@0 680 return CustomizableUI.PROVIDER_XUL;
michael@0 681 },
michael@0 682
michael@0 683 getWidgetNode: function(aWidgetId, aWindow) {
michael@0 684 let document = aWindow.document;
michael@0 685
michael@0 686 if (this.isSpecialWidget(aWidgetId)) {
michael@0 687 let widgetNode = document.getElementById(aWidgetId) ||
michael@0 688 this.createSpecialWidget(aWidgetId, document);
michael@0 689 return [ CustomizableUI.PROVIDER_SPECIAL, widgetNode];
michael@0 690 }
michael@0 691
michael@0 692 let widget = gPalette.get(aWidgetId);
michael@0 693 if (widget) {
michael@0 694 // If we have an instance of this widget already, just use that.
michael@0 695 if (widget.instances.has(document)) {
michael@0 696 LOG("An instance of widget " + aWidgetId + " already exists in this "
michael@0 697 + "document. Reusing.");
michael@0 698 return [ CustomizableUI.PROVIDER_API,
michael@0 699 widget.instances.get(document) ];
michael@0 700 }
michael@0 701
michael@0 702 return [ CustomizableUI.PROVIDER_API,
michael@0 703 this.buildWidget(document, widget) ];
michael@0 704 }
michael@0 705
michael@0 706 LOG("Searching for " + aWidgetId + " in toolbox.");
michael@0 707 let node = this.findWidgetInWindow(aWidgetId, aWindow);
michael@0 708 if (node) {
michael@0 709 return [ CustomizableUI.PROVIDER_XUL, node ];
michael@0 710 }
michael@0 711
michael@0 712 LOG("No node for " + aWidgetId + " found.");
michael@0 713 return [null, null];
michael@0 714 },
michael@0 715
michael@0 716 registerMenuPanel: function(aPanelContents) {
michael@0 717 if (gBuildAreas.has(CustomizableUI.AREA_PANEL) &&
michael@0 718 gBuildAreas.get(CustomizableUI.AREA_PANEL).has(aPanelContents)) {
michael@0 719 return;
michael@0 720 }
michael@0 721
michael@0 722 let document = aPanelContents.ownerDocument;
michael@0 723
michael@0 724 aPanelContents.toolbox = document.getElementById("navigator-toolbox");
michael@0 725 aPanelContents.customizationTarget = aPanelContents;
michael@0 726
michael@0 727 this.addPanelCloseListeners(this._getPanelForNode(aPanelContents));
michael@0 728
michael@0 729 let placements = gPlacements.get(CustomizableUI.AREA_PANEL);
michael@0 730 this.buildArea(CustomizableUI.AREA_PANEL, placements, aPanelContents);
michael@0 731 this.notifyListeners("onAreaNodeRegistered", CustomizableUI.AREA_PANEL, aPanelContents);
michael@0 732
michael@0 733 for (let child of aPanelContents.children) {
michael@0 734 if (child.localName != "toolbarbutton") {
michael@0 735 if (child.localName == "toolbaritem") {
michael@0 736 this.ensureButtonContextMenu(child, aPanelContents);
michael@0 737 }
michael@0 738 continue;
michael@0 739 }
michael@0 740 this.ensureButtonContextMenu(child, aPanelContents);
michael@0 741 child.setAttribute("wrap", "true");
michael@0 742 }
michael@0 743
michael@0 744 this.registerBuildArea(CustomizableUI.AREA_PANEL, aPanelContents);
michael@0 745 },
michael@0 746
michael@0 747 onWidgetAdded: function(aWidgetId, aArea, aPosition) {
michael@0 748 this.insertNode(aWidgetId, aArea, aPosition, true);
michael@0 749
michael@0 750 if (!gResetting) {
michael@0 751 this._clearPreviousUIState();
michael@0 752 }
michael@0 753 },
michael@0 754
michael@0 755 onWidgetRemoved: function(aWidgetId, aArea) {
michael@0 756 let areaNodes = gBuildAreas.get(aArea);
michael@0 757 if (!areaNodes) {
michael@0 758 return;
michael@0 759 }
michael@0 760
michael@0 761 let area = gAreas.get(aArea);
michael@0 762 let isToolbar = area.get("type") == CustomizableUI.TYPE_TOOLBAR;
michael@0 763 let isOverflowable = isToolbar && area.get("overflowable");
michael@0 764 let showInPrivateBrowsing = gPalette.has(aWidgetId)
michael@0 765 ? gPalette.get(aWidgetId).showInPrivateBrowsing
michael@0 766 : true;
michael@0 767
michael@0 768 for (let areaNode of areaNodes) {
michael@0 769 let window = areaNode.ownerDocument.defaultView;
michael@0 770 if (!showInPrivateBrowsing &&
michael@0 771 PrivateBrowsingUtils.isWindowPrivate(window)) {
michael@0 772 continue;
michael@0 773 }
michael@0 774
michael@0 775 let widgetNode = window.document.getElementById(aWidgetId);
michael@0 776 if (!widgetNode) {
michael@0 777 INFO("Widget not found, unable to remove");
michael@0 778 continue;
michael@0 779 }
michael@0 780 let container = areaNode.customizationTarget;
michael@0 781 if (isOverflowable) {
michael@0 782 container = areaNode.overflowable.getContainerFor(widgetNode);
michael@0 783 }
michael@0 784
michael@0 785 this.notifyListeners("onWidgetBeforeDOMChange", widgetNode, null, container, true);
michael@0 786
michael@0 787 // We remove location attributes here to make sure they're gone too when a
michael@0 788 // widget is removed from a toolbar to the palette. See bug 930950.
michael@0 789 this.removeLocationAttributes(widgetNode);
michael@0 790 // We also need to remove the panel context menu if it's there:
michael@0 791 this.ensureButtonContextMenu(widgetNode);
michael@0 792 widgetNode.removeAttribute("wrap");
michael@0 793 if (gPalette.has(aWidgetId) || this.isSpecialWidget(aWidgetId)) {
michael@0 794 container.removeChild(widgetNode);
michael@0 795 } else {
michael@0 796 areaNode.toolbox.palette.appendChild(widgetNode);
michael@0 797 }
michael@0 798 this.notifyListeners("onWidgetAfterDOMChange", widgetNode, null, container, true);
michael@0 799
michael@0 800 if (isToolbar) {
michael@0 801 areaNode.setAttribute("currentset", gPlacements.get(aArea).join(','));
michael@0 802 }
michael@0 803
michael@0 804 let windowCache = gSingleWrapperCache.get(window);
michael@0 805 if (windowCache) {
michael@0 806 windowCache.delete(aWidgetId);
michael@0 807 }
michael@0 808 }
michael@0 809 if (!gResetting) {
michael@0 810 this._clearPreviousUIState();
michael@0 811 }
michael@0 812 },
michael@0 813
michael@0 814 onWidgetMoved: function(aWidgetId, aArea, aOldPosition, aNewPosition) {
michael@0 815 this.insertNode(aWidgetId, aArea, aNewPosition);
michael@0 816 if (!gResetting) {
michael@0 817 this._clearPreviousUIState();
michael@0 818 }
michael@0 819 },
michael@0 820
michael@0 821 onCustomizeEnd: function(aWindow) {
michael@0 822 this._clearPreviousUIState();
michael@0 823 },
michael@0 824
michael@0 825 registerBuildArea: function(aArea, aNode) {
michael@0 826 // We ensure that the window is registered to have its customization data
michael@0 827 // cleaned up when unloading.
michael@0 828 let window = aNode.ownerDocument.defaultView;
michael@0 829 if (window.closed) {
michael@0 830 return;
michael@0 831 }
michael@0 832 this.registerBuildWindow(window);
michael@0 833
michael@0 834 // Also register this build area's toolbox.
michael@0 835 if (aNode.toolbox) {
michael@0 836 gBuildWindows.get(window).add(aNode.toolbox);
michael@0 837 }
michael@0 838
michael@0 839 if (!gBuildAreas.has(aArea)) {
michael@0 840 gBuildAreas.set(aArea, new Set());
michael@0 841 }
michael@0 842
michael@0 843 gBuildAreas.get(aArea).add(aNode);
michael@0 844
michael@0 845 // Give a class to all customize targets to be used for styling in Customize Mode
michael@0 846 let customizableNode = this.getCustomizeTargetForArea(aArea, window);
michael@0 847 customizableNode.classList.add("customization-target");
michael@0 848 },
michael@0 849
michael@0 850 registerBuildWindow: function(aWindow) {
michael@0 851 if (!gBuildWindows.has(aWindow)) {
michael@0 852 gBuildWindows.set(aWindow, new Set());
michael@0 853
michael@0 854 aWindow.addEventListener("unload", this);
michael@0 855 aWindow.addEventListener("command", this, true);
michael@0 856
michael@0 857 this.notifyListeners("onWindowOpened", aWindow);
michael@0 858 }
michael@0 859 },
michael@0 860
michael@0 861 unregisterBuildWindow: function(aWindow) {
michael@0 862 aWindow.removeEventListener("unload", this);
michael@0 863 aWindow.removeEventListener("command", this, true);
michael@0 864 gPanelsForWindow.delete(aWindow);
michael@0 865 gBuildWindows.delete(aWindow);
michael@0 866 gSingleWrapperCache.delete(aWindow);
michael@0 867 let document = aWindow.document;
michael@0 868
michael@0 869 for (let [areaId, areaNodes] of gBuildAreas) {
michael@0 870 let areaProperties = gAreas.get(areaId);
michael@0 871 for (let node of areaNodes) {
michael@0 872 if (node.ownerDocument == document) {
michael@0 873 this.notifyListeners("onAreaNodeUnregistered", areaId, node.customizationTarget,
michael@0 874 CustomizableUI.REASON_WINDOW_CLOSED);
michael@0 875 if (areaProperties.has("overflowable")) {
michael@0 876 node.overflowable.uninit();
michael@0 877 node.overflowable = null;
michael@0 878 }
michael@0 879 areaNodes.delete(node);
michael@0 880 }
michael@0 881 }
michael@0 882 }
michael@0 883
michael@0 884 for (let [,widget] of gPalette) {
michael@0 885 widget.instances.delete(document);
michael@0 886 this.notifyListeners("onWidgetInstanceRemoved", widget.id, document);
michael@0 887 }
michael@0 888
michael@0 889 for (let [area, areaMap] of gPendingBuildAreas) {
michael@0 890 let toDelete = [];
michael@0 891 for (let [areaNode, ] of areaMap) {
michael@0 892 if (areaNode.ownerDocument == document) {
michael@0 893 toDelete.push(areaNode);
michael@0 894 }
michael@0 895 }
michael@0 896 for (let areaNode of toDelete) {
michael@0 897 areaMap.delete(toDelete);
michael@0 898 }
michael@0 899 }
michael@0 900
michael@0 901 this.notifyListeners("onWindowClosed", aWindow);
michael@0 902 },
michael@0 903
michael@0 904 setLocationAttributes: function(aNode, aArea) {
michael@0 905 let props = gAreas.get(aArea);
michael@0 906 if (!props) {
michael@0 907 throw new Error("Expected area " + aArea + " to have a properties Map " +
michael@0 908 "associated with it.");
michael@0 909 }
michael@0 910
michael@0 911 aNode.setAttribute("cui-areatype", props.get("type") || "");
michael@0 912 let anchor = props.get("anchor");
michael@0 913 if (anchor) {
michael@0 914 aNode.setAttribute("cui-anchorid", anchor);
michael@0 915 } else {
michael@0 916 aNode.removeAttribute("cui-anchorid");
michael@0 917 }
michael@0 918 },
michael@0 919
michael@0 920 removeLocationAttributes: function(aNode) {
michael@0 921 aNode.removeAttribute("cui-areatype");
michael@0 922 aNode.removeAttribute("cui-anchorid");
michael@0 923 },
michael@0 924
michael@0 925 insertNode: function(aWidgetId, aArea, aPosition, isNew) {
michael@0 926 let areaNodes = gBuildAreas.get(aArea);
michael@0 927 if (!areaNodes) {
michael@0 928 return;
michael@0 929 }
michael@0 930
michael@0 931 let placements = gPlacements.get(aArea);
michael@0 932 if (!placements) {
michael@0 933 ERROR("Could not find any placements for " + aArea +
michael@0 934 " when moving a widget.");
michael@0 935 return;
michael@0 936 }
michael@0 937
michael@0 938 // Go through each of the nodes associated with this area and move the
michael@0 939 // widget to the requested location.
michael@0 940 for (let areaNode of areaNodes) {
michael@0 941 this.insertNodeInWindow(aWidgetId, areaNode, isNew);
michael@0 942 }
michael@0 943 },
michael@0 944
michael@0 945 insertNodeInWindow: function(aWidgetId, aAreaNode, isNew) {
michael@0 946 let window = aAreaNode.ownerDocument.defaultView;
michael@0 947 let showInPrivateBrowsing = gPalette.has(aWidgetId)
michael@0 948 ? gPalette.get(aWidgetId).showInPrivateBrowsing
michael@0 949 : true;
michael@0 950
michael@0 951 if (!showInPrivateBrowsing && PrivateBrowsingUtils.isWindowPrivate(window)) {
michael@0 952 return;
michael@0 953 }
michael@0 954
michael@0 955 let [, widgetNode] = this.getWidgetNode(aWidgetId, window);
michael@0 956 if (!widgetNode) {
michael@0 957 ERROR("Widget '" + aWidgetId + "' not found, unable to move");
michael@0 958 return;
michael@0 959 }
michael@0 960
michael@0 961 let areaId = aAreaNode.id;
michael@0 962 if (isNew) {
michael@0 963 this.ensureButtonContextMenu(widgetNode, aAreaNode);
michael@0 964 if (widgetNode.localName == "toolbarbutton" && areaId == CustomizableUI.AREA_PANEL) {
michael@0 965 widgetNode.setAttribute("wrap", "true");
michael@0 966 }
michael@0 967 }
michael@0 968
michael@0 969 let [insertionContainer, nextNode] = this.findInsertionPoints(widgetNode, aAreaNode);
michael@0 970 this.insertWidgetBefore(widgetNode, nextNode, insertionContainer, areaId);
michael@0 971
michael@0 972 if (gAreas.get(areaId).get("type") == CustomizableUI.TYPE_TOOLBAR) {
michael@0 973 aAreaNode.setAttribute("currentset", gPlacements.get(areaId).join(','));
michael@0 974 }
michael@0 975 },
michael@0 976
michael@0 977 findInsertionPoints: function(aNode, aAreaNode) {
michael@0 978 let areaId = aAreaNode.id;
michael@0 979 let props = gAreas.get(areaId);
michael@0 980
michael@0 981 // For overflowable toolbars, rely on them (because the work is more complicated):
michael@0 982 if (props.get("type") == CustomizableUI.TYPE_TOOLBAR && props.get("overflowable")) {
michael@0 983 return aAreaNode.overflowable.findOverflowedInsertionPoints(aNode);
michael@0 984 }
michael@0 985
michael@0 986 let container = aAreaNode.customizationTarget;
michael@0 987 let placements = gPlacements.get(areaId);
michael@0 988 let nodeIndex = placements.indexOf(aNode.id);
michael@0 989
michael@0 990 while (++nodeIndex < placements.length) {
michael@0 991 let nextNodeId = placements[nodeIndex];
michael@0 992 let nextNode = container.getElementsByAttribute("id", nextNodeId).item(0);
michael@0 993
michael@0 994 if (nextNode) {
michael@0 995 return [container, nextNode];
michael@0 996 }
michael@0 997 }
michael@0 998
michael@0 999 return [container, null];
michael@0 1000 },
michael@0 1001
michael@0 1002 insertWidgetBefore: function(aNode, aNextNode, aContainer, aArea) {
michael@0 1003 this.notifyListeners("onWidgetBeforeDOMChange", aNode, aNextNode, aContainer);
michael@0 1004 this.setLocationAttributes(aNode, aArea);
michael@0 1005 aContainer.insertBefore(aNode, aNextNode);
michael@0 1006 this.notifyListeners("onWidgetAfterDOMChange", aNode, aNextNode, aContainer);
michael@0 1007 },
michael@0 1008
michael@0 1009 handleEvent: function(aEvent) {
michael@0 1010 switch (aEvent.type) {
michael@0 1011 case "command":
michael@0 1012 if (!this._originalEventInPanel(aEvent)) {
michael@0 1013 break;
michael@0 1014 }
michael@0 1015 aEvent = aEvent.sourceEvent;
michael@0 1016 // Fall through
michael@0 1017 case "click":
michael@0 1018 case "keypress":
michael@0 1019 this.maybeAutoHidePanel(aEvent);
michael@0 1020 break;
michael@0 1021 case "unload":
michael@0 1022 this.unregisterBuildWindow(aEvent.currentTarget);
michael@0 1023 break;
michael@0 1024 }
michael@0 1025 },
michael@0 1026
michael@0 1027 _originalEventInPanel: function(aEvent) {
michael@0 1028 let e = aEvent.sourceEvent;
michael@0 1029 if (!e) {
michael@0 1030 return false;
michael@0 1031 }
michael@0 1032 let node = this._getPanelForNode(e.target);
michael@0 1033 if (!node) {
michael@0 1034 return false;
michael@0 1035 }
michael@0 1036 let win = e.view;
michael@0 1037 let panels = gPanelsForWindow.get(win);
michael@0 1038 return !!panels && panels.has(node);
michael@0 1039 },
michael@0 1040
michael@0 1041 isSpecialWidget: function(aId) {
michael@0 1042 return (aId.startsWith(kSpecialWidgetPfx) ||
michael@0 1043 aId.startsWith("separator") ||
michael@0 1044 aId.startsWith("spring") ||
michael@0 1045 aId.startsWith("spacer"));
michael@0 1046 },
michael@0 1047
michael@0 1048 ensureSpecialWidgetId: function(aId) {
michael@0 1049 let nodeType = aId.match(/spring|spacer|separator/)[0];
michael@0 1050 // If the ID we were passed isn't a generated one, generate one now:
michael@0 1051 if (nodeType == aId) {
michael@0 1052 // Ids are differentiated through a unique count suffix.
michael@0 1053 return kSpecialWidgetPfx + aId + (++gNewElementCount);
michael@0 1054 }
michael@0 1055 return aId;
michael@0 1056 },
michael@0 1057
michael@0 1058 createSpecialWidget: function(aId, aDocument) {
michael@0 1059 let nodeName = "toolbar" + aId.match(/spring|spacer|separator/)[0];
michael@0 1060 let node = aDocument.createElementNS(kNSXUL, nodeName);
michael@0 1061 node.id = this.ensureSpecialWidgetId(aId);
michael@0 1062 if (nodeName == "toolbarspring") {
michael@0 1063 node.flex = 1;
michael@0 1064 }
michael@0 1065 return node;
michael@0 1066 },
michael@0 1067
michael@0 1068 /* Find a XUL-provided widget in a window. Don't try to use this
michael@0 1069 * for an API-provided widget or a special widget.
michael@0 1070 */
michael@0 1071 findWidgetInWindow: function(aId, aWindow) {
michael@0 1072 if (!gBuildWindows.has(aWindow)) {
michael@0 1073 throw new Error("Build window not registered");
michael@0 1074 }
michael@0 1075
michael@0 1076 if (!aId) {
michael@0 1077 ERROR("findWidgetInWindow was passed an empty string.");
michael@0 1078 return null;
michael@0 1079 }
michael@0 1080
michael@0 1081 let document = aWindow.document;
michael@0 1082
michael@0 1083 // look for a node with the same id, as the node may be
michael@0 1084 // in a different toolbar.
michael@0 1085 let node = document.getElementById(aId);
michael@0 1086 if (node) {
michael@0 1087 let parent = node.parentNode;
michael@0 1088 while (parent && !(parent.customizationTarget ||
michael@0 1089 parent == aWindow.gNavToolbox.palette)) {
michael@0 1090 parent = parent.parentNode;
michael@0 1091 }
michael@0 1092
michael@0 1093 if (parent) {
michael@0 1094 let nodeInArea = node.parentNode.localName == "toolbarpaletteitem" ?
michael@0 1095 node.parentNode : node;
michael@0 1096 // Check if we're in a customization target, or in the palette:
michael@0 1097 if ((parent.customizationTarget == nodeInArea.parentNode &&
michael@0 1098 gBuildWindows.get(aWindow).has(parent.toolbox)) ||
michael@0 1099 aWindow.gNavToolbox.palette == nodeInArea.parentNode) {
michael@0 1100 // Normalize the removable attribute. For backwards compat, if
michael@0 1101 // the widget is not located in a toolbox palette then absence
michael@0 1102 // of the "removable" attribute means it is not removable.
michael@0 1103 if (!node.hasAttribute("removable")) {
michael@0 1104 // If we first see this in customization mode, it may be in the
michael@0 1105 // customization palette instead of the toolbox palette.
michael@0 1106 node.setAttribute("removable", !parent.customizationTarget);
michael@0 1107 }
michael@0 1108 return node;
michael@0 1109 }
michael@0 1110 }
michael@0 1111 }
michael@0 1112
michael@0 1113 let toolboxes = gBuildWindows.get(aWindow);
michael@0 1114 for (let toolbox of toolboxes) {
michael@0 1115 if (toolbox.palette) {
michael@0 1116 // Attempt to locate a node with a matching ID within
michael@0 1117 // the palette.
michael@0 1118 let node = toolbox.palette.getElementsByAttribute("id", aId)[0];
michael@0 1119 if (node) {
michael@0 1120 // Normalize the removable attribute. For backwards compat, this
michael@0 1121 // is optional if the widget is located in the toolbox palette,
michael@0 1122 // and defaults to *true*, unlike if it was located elsewhere.
michael@0 1123 if (!node.hasAttribute("removable")) {
michael@0 1124 node.setAttribute("removable", true);
michael@0 1125 }
michael@0 1126 return node;
michael@0 1127 }
michael@0 1128 }
michael@0 1129 }
michael@0 1130 return null;
michael@0 1131 },
michael@0 1132
michael@0 1133 buildWidget: function(aDocument, aWidget) {
michael@0 1134 if (typeof aWidget == "string") {
michael@0 1135 aWidget = gPalette.get(aWidget);
michael@0 1136 }
michael@0 1137 if (!aWidget) {
michael@0 1138 throw new Error("buildWidget was passed a non-widget to build.");
michael@0 1139 }
michael@0 1140
michael@0 1141 LOG("Building " + aWidget.id + " of type " + aWidget.type);
michael@0 1142
michael@0 1143 let node;
michael@0 1144 if (aWidget.type == "custom") {
michael@0 1145 if (aWidget.onBuild) {
michael@0 1146 node = aWidget.onBuild(aDocument);
michael@0 1147 }
michael@0 1148 if (!node || !(node instanceof aDocument.defaultView.XULElement))
michael@0 1149 ERROR("Custom widget with id " + aWidget.id + " does not return a valid node");
michael@0 1150 }
michael@0 1151 else {
michael@0 1152 if (aWidget.onBeforeCreated) {
michael@0 1153 aWidget.onBeforeCreated(aDocument);
michael@0 1154 }
michael@0 1155 node = aDocument.createElementNS(kNSXUL, "toolbarbutton");
michael@0 1156
michael@0 1157 node.setAttribute("id", aWidget.id);
michael@0 1158 node.setAttribute("widget-id", aWidget.id);
michael@0 1159 node.setAttribute("widget-type", aWidget.type);
michael@0 1160 if (aWidget.disabled) {
michael@0 1161 node.setAttribute("disabled", true);
michael@0 1162 }
michael@0 1163 node.setAttribute("removable", aWidget.removable);
michael@0 1164 node.setAttribute("overflows", aWidget.overflows);
michael@0 1165 node.setAttribute("label", this.getLocalizedProperty(aWidget, "label"));
michael@0 1166 let additionalTooltipArguments = [];
michael@0 1167 if (aWidget.shortcutId) {
michael@0 1168 let keyEl = aDocument.getElementById(aWidget.shortcutId);
michael@0 1169 if (keyEl) {
michael@0 1170 additionalTooltipArguments.push(ShortcutUtils.prettifyShortcut(keyEl));
michael@0 1171 } else {
michael@0 1172 ERROR("Key element with id '" + aWidget.shortcutId + "' for widget '" + aWidget.id +
michael@0 1173 "' not found!");
michael@0 1174 }
michael@0 1175 }
michael@0 1176
michael@0 1177 let tooltip = this.getLocalizedProperty(aWidget, "tooltiptext", additionalTooltipArguments);
michael@0 1178 node.setAttribute("tooltiptext", tooltip);
michael@0 1179 node.setAttribute("class", "toolbarbutton-1 chromeclass-toolbar-additional");
michael@0 1180
michael@0 1181 let commandHandler = this.handleWidgetCommand.bind(this, aWidget, node);
michael@0 1182 node.addEventListener("command", commandHandler, false);
michael@0 1183 let clickHandler = this.handleWidgetClick.bind(this, aWidget, node);
michael@0 1184 node.addEventListener("click", clickHandler, false);
michael@0 1185
michael@0 1186 // If the widget has a view, and has view showing / hiding listeners,
michael@0 1187 // hook those up to this widget.
michael@0 1188 if (aWidget.type == "view") {
michael@0 1189 LOG("Widget " + aWidget.id + " has a view. Auto-registering event handlers.");
michael@0 1190 let viewNode = aDocument.getElementById(aWidget.viewId);
michael@0 1191
michael@0 1192 if (viewNode) {
michael@0 1193 // PanelUI relies on the .PanelUI-subView class to be able to show only
michael@0 1194 // one sub-view at a time.
michael@0 1195 viewNode.classList.add("PanelUI-subView");
michael@0 1196
michael@0 1197 for (let eventName of kSubviewEvents) {
michael@0 1198 let handler = "on" + eventName;
michael@0 1199 if (typeof aWidget[handler] == "function") {
michael@0 1200 viewNode.addEventListener(eventName, aWidget[handler], false);
michael@0 1201 }
michael@0 1202 }
michael@0 1203
michael@0 1204 LOG("Widget " + aWidget.id + " showing and hiding event handlers set.");
michael@0 1205 } else {
michael@0 1206 ERROR("Could not find the view node with id: " + aWidget.viewId +
michael@0 1207 ", for widget: " + aWidget.id + ".");
michael@0 1208 }
michael@0 1209 }
michael@0 1210
michael@0 1211 if (aWidget.onCreated) {
michael@0 1212 aWidget.onCreated(node);
michael@0 1213 }
michael@0 1214 }
michael@0 1215
michael@0 1216 aWidget.instances.set(aDocument, node);
michael@0 1217 return node;
michael@0 1218 },
michael@0 1219
michael@0 1220 getLocalizedProperty: function(aWidget, aProp, aFormatArgs, aDef) {
michael@0 1221 if (typeof aWidget == "string") {
michael@0 1222 aWidget = gPalette.get(aWidget);
michael@0 1223 }
michael@0 1224 if (!aWidget) {
michael@0 1225 throw new Error("getLocalizedProperty was passed a non-widget to work with.");
michael@0 1226 }
michael@0 1227 let def, name;
michael@0 1228 // Let widgets pass their own string identifiers or strings, so that
michael@0 1229 // we can use strings which aren't the default (in case string ids change)
michael@0 1230 // and so that non-builtin-widgets can also provide labels, tooltips, etc.
michael@0 1231 if (aWidget[aProp]) {
michael@0 1232 name = aWidget[aProp];
michael@0 1233 // By using this as the default, if a widget provides a full string rather
michael@0 1234 // than a string ID for localization, we will fall back to that string
michael@0 1235 // and return that.
michael@0 1236 def = aDef || name;
michael@0 1237 } else {
michael@0 1238 name = aWidget.id + "." + aProp;
michael@0 1239 def = aDef || "";
michael@0 1240 }
michael@0 1241 try {
michael@0 1242 if (Array.isArray(aFormatArgs) && aFormatArgs.length) {
michael@0 1243 return gWidgetsBundle.formatStringFromName(name, aFormatArgs,
michael@0 1244 aFormatArgs.length) || def;
michael@0 1245 }
michael@0 1246 return gWidgetsBundle.GetStringFromName(name) || def;
michael@0 1247 } catch(ex) {
michael@0 1248 if (!def) {
michael@0 1249 ERROR("Could not localize property '" + name + "'.");
michael@0 1250 }
michael@0 1251 }
michael@0 1252 return def;
michael@0 1253 },
michael@0 1254
michael@0 1255 handleWidgetCommand: function(aWidget, aNode, aEvent) {
michael@0 1256 LOG("handleWidgetCommand");
michael@0 1257
michael@0 1258 if (aWidget.type == "button") {
michael@0 1259 if (aWidget.onCommand) {
michael@0 1260 try {
michael@0 1261 aWidget.onCommand.call(null, aEvent);
michael@0 1262 } catch (e) {
michael@0 1263 ERROR(e);
michael@0 1264 }
michael@0 1265 } else {
michael@0 1266 //XXXunf Need to think this through more, and formalize.
michael@0 1267 Services.obs.notifyObservers(aNode,
michael@0 1268 "customizedui-widget-command",
michael@0 1269 aWidget.id);
michael@0 1270 }
michael@0 1271 } else if (aWidget.type == "view") {
michael@0 1272 let ownerWindow = aNode.ownerDocument.defaultView;
michael@0 1273 let area = this.getPlacementOfWidget(aNode.id).area;
michael@0 1274 let anchor = aNode;
michael@0 1275 if (area != CustomizableUI.AREA_PANEL) {
michael@0 1276 let wrapper = this.wrapWidget(aWidget.id).forWindow(ownerWindow);
michael@0 1277 if (wrapper && wrapper.anchor) {
michael@0 1278 this.hidePanelForNode(aNode);
michael@0 1279 anchor = wrapper.anchor;
michael@0 1280 }
michael@0 1281 }
michael@0 1282 ownerWindow.PanelUI.showSubView(aWidget.viewId, anchor, area);
michael@0 1283 }
michael@0 1284 },
michael@0 1285
michael@0 1286 handleWidgetClick: function(aWidget, aNode, aEvent) {
michael@0 1287 LOG("handleWidgetClick");
michael@0 1288 if (aWidget.onClick) {
michael@0 1289 try {
michael@0 1290 aWidget.onClick.call(null, aEvent);
michael@0 1291 } catch(e) {
michael@0 1292 Cu.reportError(e);
michael@0 1293 }
michael@0 1294 } else {
michael@0 1295 //XXXunf Need to think this through more, and formalize.
michael@0 1296 Services.obs.notifyObservers(aNode, "customizedui-widget-click", aWidget.id);
michael@0 1297 }
michael@0 1298 },
michael@0 1299
michael@0 1300 _getPanelForNode: function(aNode) {
michael@0 1301 let panel = aNode;
michael@0 1302 while (panel && panel.localName != "panel")
michael@0 1303 panel = panel.parentNode;
michael@0 1304 return panel;
michael@0 1305 },
michael@0 1306
michael@0 1307 /*
michael@0 1308 * If people put things in the panel which need more than single-click interaction,
michael@0 1309 * we don't want to close it. Right now we check for text inputs and menu buttons.
michael@0 1310 * We also check for being outside of any toolbaritem/toolbarbutton, ie on a blank
michael@0 1311 * part of the menu.
michael@0 1312 */
michael@0 1313 _isOnInteractiveElement: function(aEvent) {
michael@0 1314 function getMenuPopupForDescendant(aNode) {
michael@0 1315 let lastPopup = null;
michael@0 1316 while (aNode && aNode.parentNode &&
michael@0 1317 aNode.parentNode.localName.startsWith("menu")) {
michael@0 1318 lastPopup = aNode.localName == "menupopup" ? aNode : lastPopup;
michael@0 1319 aNode = aNode.parentNode;
michael@0 1320 }
michael@0 1321 return lastPopup;
michael@0 1322 }
michael@0 1323
michael@0 1324 let target = aEvent.originalTarget;
michael@0 1325 let panel = this._getPanelForNode(aEvent.currentTarget);
michael@0 1326 // This can happen in e.g. customize mode. If there's no panel,
michael@0 1327 // there's clearly nothing for us to close; pretend we're interactive.
michael@0 1328 if (!panel) {
michael@0 1329 return true;
michael@0 1330 }
michael@0 1331 // We keep track of:
michael@0 1332 // whether we're in an input container (text field)
michael@0 1333 let inInput = false;
michael@0 1334 // whether we're in a popup/context menu
michael@0 1335 let inMenu = false;
michael@0 1336 // whether we're in a toolbarbutton/toolbaritem
michael@0 1337 let inItem = false;
michael@0 1338 // whether the current menuitem has a valid closemenu attribute
michael@0 1339 let menuitemCloseMenu = "auto";
michael@0 1340 // whether the toolbarbutton/item has a valid closemenu attribute.
michael@0 1341 let closemenu = "auto";
michael@0 1342
michael@0 1343 // While keeping track of that, we go from the original target back up,
michael@0 1344 // to the panel if we have to. We bail as soon as we find an input,
michael@0 1345 // a toolbarbutton/item, or the panel:
michael@0 1346 while (true && target) {
michael@0 1347 let tagName = target.localName;
michael@0 1348 inInput = tagName == "input" || tagName == "textbox";
michael@0 1349 inItem = tagName == "toolbaritem" || tagName == "toolbarbutton";
michael@0 1350 let isMenuItem = tagName == "menuitem";
michael@0 1351 inMenu = inMenu || isMenuItem;
michael@0 1352 if (inItem && target.hasAttribute("closemenu")) {
michael@0 1353 let closemenuVal = target.getAttribute("closemenu");
michael@0 1354 closemenu = (closemenuVal == "single" || closemenuVal == "none") ?
michael@0 1355 closemenuVal : "auto";
michael@0 1356 }
michael@0 1357
michael@0 1358 if (isMenuItem && target.hasAttribute("closemenu")) {
michael@0 1359 let closemenuVal = target.getAttribute("closemenu");
michael@0 1360 menuitemCloseMenu = (closemenuVal == "single" || closemenuVal == "none") ?
michael@0 1361 closemenuVal : "auto";
michael@0 1362 }
michael@0 1363 // This isn't in the loop condition because we want to break before
michael@0 1364 // changing |target| if any of these conditions are true
michael@0 1365 if (inInput || inItem || target == panel) {
michael@0 1366 break;
michael@0 1367 }
michael@0 1368 // We need specific code for popups: the item on which they were invoked
michael@0 1369 // isn't necessarily in their parentNode chain:
michael@0 1370 if (isMenuItem) {
michael@0 1371 let topmostMenuPopup = getMenuPopupForDescendant(target);
michael@0 1372 target = (topmostMenuPopup && topmostMenuPopup.triggerNode) ||
michael@0 1373 target.parentNode;
michael@0 1374 } else {
michael@0 1375 target = target.parentNode;
michael@0 1376 }
michael@0 1377 }
michael@0 1378 // If the user clicked a menu item...
michael@0 1379 if (inMenu) {
michael@0 1380 // We care if we're in an input also,
michael@0 1381 // or if the user specified closemenu!="auto":
michael@0 1382 if (inInput || menuitemCloseMenu != "auto") {
michael@0 1383 return true;
michael@0 1384 }
michael@0 1385 // Otherwise, we're probably fine to close the panel
michael@0 1386 return false;
michael@0 1387 }
michael@0 1388 // If we're not in a menu, and we *are* in a type="menu" toolbarbutton,
michael@0 1389 // we'll now interact with the menu
michael@0 1390 if (inItem && target.getAttribute("type") == "menu") {
michael@0 1391 return true;
michael@0 1392 }
michael@0 1393 // If we're not in a menu, and we *are* in a type="menu-button" toolbarbutton,
michael@0 1394 // it depends whether we're in the dropmarker or the 'real' button:
michael@0 1395 if (inItem && target.getAttribute("type") == "menu-button") {
michael@0 1396 // 'real' button (which has a single action):
michael@0 1397 if (target.getAttribute("anonid") == "button") {
michael@0 1398 return closemenu != "none";
michael@0 1399 }
michael@0 1400 // otherwise, this is the outer button, and the user will now
michael@0 1401 // interact with the menu:
michael@0 1402 return true;
michael@0 1403 }
michael@0 1404 return inInput || !inItem;
michael@0 1405 },
michael@0 1406
michael@0 1407 hidePanelForNode: function(aNode) {
michael@0 1408 let panel = this._getPanelForNode(aNode);
michael@0 1409 if (panel) {
michael@0 1410 panel.hidePopup();
michael@0 1411 }
michael@0 1412 },
michael@0 1413
michael@0 1414 maybeAutoHidePanel: function(aEvent) {
michael@0 1415 if (aEvent.type == "keypress") {
michael@0 1416 if (aEvent.keyCode != aEvent.DOM_VK_RETURN) {
michael@0 1417 return;
michael@0 1418 }
michael@0 1419 // If the user hit enter/return, we don't check preventDefault - it makes sense
michael@0 1420 // that this was prevented, but we probably still want to close the panel.
michael@0 1421 // If consumers don't want this to happen, they should specify the closemenu
michael@0 1422 // attribute.
michael@0 1423
michael@0 1424 } else if (aEvent.type != "command") { // mouse events:
michael@0 1425 if (aEvent.defaultPrevented || aEvent.button != 0) {
michael@0 1426 return;
michael@0 1427 }
michael@0 1428 let isInteractive = this._isOnInteractiveElement(aEvent);
michael@0 1429 LOG("maybeAutoHidePanel: interactive ? " + isInteractive);
michael@0 1430 if (isInteractive) {
michael@0 1431 return;
michael@0 1432 }
michael@0 1433 }
michael@0 1434
michael@0 1435 // We can't use event.target because we might have passed a panelview
michael@0 1436 // anonymous content boundary as well, and so target points to the
michael@0 1437 // panelmultiview in that case. Unfortunately, this means we get
michael@0 1438 // anonymous child nodes instead of the real ones, so looking for the
michael@0 1439 // 'stoooop, don't close me' attributes is more involved.
michael@0 1440 let target = aEvent.originalTarget;
michael@0 1441 let closemenu = "auto";
michael@0 1442 let widgetType = "button";
michael@0 1443 while (target.parentNode && target.localName != "panel") {
michael@0 1444 closemenu = target.getAttribute("closemenu");
michael@0 1445 widgetType = target.getAttribute("widget-type");
michael@0 1446 if (closemenu == "none" || closemenu == "single" ||
michael@0 1447 widgetType == "view") {
michael@0 1448 break;
michael@0 1449 }
michael@0 1450 target = target.parentNode;
michael@0 1451 }
michael@0 1452 if (closemenu == "none" || widgetType == "view") {
michael@0 1453 return;
michael@0 1454 }
michael@0 1455
michael@0 1456 if (closemenu == "single") {
michael@0 1457 let panel = this._getPanelForNode(target);
michael@0 1458 let multiview = panel.querySelector("panelmultiview");
michael@0 1459 if (multiview.showingSubView) {
michael@0 1460 multiview.showMainView();
michael@0 1461 return;
michael@0 1462 }
michael@0 1463 }
michael@0 1464
michael@0 1465 // If we get here, we can actually hide the popup:
michael@0 1466 this.hidePanelForNode(aEvent.target);
michael@0 1467 },
michael@0 1468
michael@0 1469 getUnusedWidgets: function(aWindowPalette) {
michael@0 1470 let window = aWindowPalette.ownerDocument.defaultView;
michael@0 1471 let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(window);
michael@0 1472 // We use a Set because there can be overlap between the widgets in
michael@0 1473 // gPalette and the items in the palette, especially after the first
michael@0 1474 // customization, since programmatically generated widgets will remain
michael@0 1475 // in the toolbox palette.
michael@0 1476 let widgets = new Set();
michael@0 1477
michael@0 1478 // It's possible that some widgets have been defined programmatically and
michael@0 1479 // have not been overlayed into the palette. We can find those inside
michael@0 1480 // gPalette.
michael@0 1481 for (let [id, widget] of gPalette) {
michael@0 1482 if (!widget.currentArea) {
michael@0 1483 if (widget.showInPrivateBrowsing || !isWindowPrivate) {
michael@0 1484 widgets.add(id);
michael@0 1485 }
michael@0 1486 }
michael@0 1487 }
michael@0 1488
michael@0 1489 LOG("Iterating the actual nodes of the window palette");
michael@0 1490 for (let node of aWindowPalette.children) {
michael@0 1491 LOG("In palette children: " + node.id);
michael@0 1492 if (node.id && !this.getPlacementOfWidget(node.id)) {
michael@0 1493 widgets.add(node.id);
michael@0 1494 }
michael@0 1495 }
michael@0 1496
michael@0 1497 return [...widgets];
michael@0 1498 },
michael@0 1499
michael@0 1500 getPlacementOfWidget: function(aWidgetId, aOnlyRegistered, aDeadAreas) {
michael@0 1501 if (aOnlyRegistered && !this.widgetExists(aWidgetId)) {
michael@0 1502 return null;
michael@0 1503 }
michael@0 1504
michael@0 1505 for (let [area, placements] of gPlacements) {
michael@0 1506 if (!gAreas.has(area) && !aDeadAreas) {
michael@0 1507 continue;
michael@0 1508 }
michael@0 1509 let index = placements.indexOf(aWidgetId);
michael@0 1510 if (index != -1) {
michael@0 1511 return { area: area, position: index };
michael@0 1512 }
michael@0 1513 }
michael@0 1514
michael@0 1515 return null;
michael@0 1516 },
michael@0 1517
michael@0 1518 widgetExists: function(aWidgetId) {
michael@0 1519 if (gPalette.has(aWidgetId) || this.isSpecialWidget(aWidgetId)) {
michael@0 1520 return true;
michael@0 1521 }
michael@0 1522
michael@0 1523 // Destroyed API widgets are in gSeenWidgets, but not in gPalette:
michael@0 1524 if (gSeenWidgets.has(aWidgetId)) {
michael@0 1525 return false;
michael@0 1526 }
michael@0 1527
michael@0 1528 // We're assuming XUL widgets always exist, as it's much harder to check,
michael@0 1529 // and checking would be much more error prone.
michael@0 1530 return true;
michael@0 1531 },
michael@0 1532
michael@0 1533 addWidgetToArea: function(aWidgetId, aArea, aPosition, aInitialAdd) {
michael@0 1534 if (!gAreas.has(aArea)) {
michael@0 1535 throw new Error("Unknown customization area: " + aArea);
michael@0 1536 }
michael@0 1537
michael@0 1538 // Hack: don't want special widgets in the panel (need to check here as well
michael@0 1539 // as in canWidgetMoveToArea because the menu panel is lazy):
michael@0 1540 if (gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL &&
michael@0 1541 this.isSpecialWidget(aWidgetId)) {
michael@0 1542 return;
michael@0 1543 }
michael@0 1544
michael@0 1545 // If this is a lazy area that hasn't been restored yet, we can't yet modify
michael@0 1546 // it - would would at least like to add to it. So we keep track of it in
michael@0 1547 // gFuturePlacements, and use that to add it when restoring the area. We
michael@0 1548 // throw away aPosition though, as that can only be bogus if the area hasn't
michael@0 1549 // yet been restorted (caller can't possibly know where its putting the
michael@0 1550 // widget in relation to other widgets).
michael@0 1551 if (this.isAreaLazy(aArea)) {
michael@0 1552 gFuturePlacements.get(aArea).add(aWidgetId);
michael@0 1553 return;
michael@0 1554 }
michael@0 1555
michael@0 1556 if (this.isSpecialWidget(aWidgetId)) {
michael@0 1557 aWidgetId = this.ensureSpecialWidgetId(aWidgetId);
michael@0 1558 }
michael@0 1559
michael@0 1560 let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true);
michael@0 1561 if (oldPlacement && oldPlacement.area == aArea) {
michael@0 1562 this.moveWidgetWithinArea(aWidgetId, aPosition);
michael@0 1563 return;
michael@0 1564 }
michael@0 1565
michael@0 1566 // Do nothing if the widget is not allowed to move to the target area.
michael@0 1567 if (!this.canWidgetMoveToArea(aWidgetId, aArea)) {
michael@0 1568 return;
michael@0 1569 }
michael@0 1570
michael@0 1571 if (oldPlacement) {
michael@0 1572 this.removeWidgetFromArea(aWidgetId);
michael@0 1573 }
michael@0 1574
michael@0 1575 if (!gPlacements.has(aArea)) {
michael@0 1576 gPlacements.set(aArea, [aWidgetId]);
michael@0 1577 aPosition = 0;
michael@0 1578 } else {
michael@0 1579 let placements = gPlacements.get(aArea);
michael@0 1580 if (typeof aPosition != "number") {
michael@0 1581 aPosition = placements.length;
michael@0 1582 }
michael@0 1583 if (aPosition < 0) {
michael@0 1584 aPosition = 0;
michael@0 1585 }
michael@0 1586 placements.splice(aPosition, 0, aWidgetId);
michael@0 1587 }
michael@0 1588
michael@0 1589 let widget = gPalette.get(aWidgetId);
michael@0 1590 if (widget) {
michael@0 1591 widget.currentArea = aArea;
michael@0 1592 widget.currentPosition = aPosition;
michael@0 1593 }
michael@0 1594
michael@0 1595 // We initially set placements with addWidgetToArea, so in that case
michael@0 1596 // we don't consider the area "dirtied".
michael@0 1597 if (!aInitialAdd) {
michael@0 1598 gDirtyAreaCache.add(aArea);
michael@0 1599 }
michael@0 1600
michael@0 1601 gDirty = true;
michael@0 1602 this.saveState();
michael@0 1603
michael@0 1604 this.notifyListeners("onWidgetAdded", aWidgetId, aArea, aPosition);
michael@0 1605 },
michael@0 1606
michael@0 1607 removeWidgetFromArea: function(aWidgetId) {
michael@0 1608 let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true);
michael@0 1609 if (!oldPlacement) {
michael@0 1610 return;
michael@0 1611 }
michael@0 1612
michael@0 1613 if (!this.isWidgetRemovable(aWidgetId)) {
michael@0 1614 return;
michael@0 1615 }
michael@0 1616
michael@0 1617 let placements = gPlacements.get(oldPlacement.area);
michael@0 1618 let position = placements.indexOf(aWidgetId);
michael@0 1619 if (position != -1) {
michael@0 1620 placements.splice(position, 1);
michael@0 1621 }
michael@0 1622
michael@0 1623 let widget = gPalette.get(aWidgetId);
michael@0 1624 if (widget) {
michael@0 1625 widget.currentArea = null;
michael@0 1626 widget.currentPosition = null;
michael@0 1627 }
michael@0 1628
michael@0 1629 gDirty = true;
michael@0 1630 this.saveState();
michael@0 1631 gDirtyAreaCache.add(oldPlacement.area);
michael@0 1632
michael@0 1633 this.notifyListeners("onWidgetRemoved", aWidgetId, oldPlacement.area);
michael@0 1634 },
michael@0 1635
michael@0 1636 moveWidgetWithinArea: function(aWidgetId, aPosition) {
michael@0 1637 let oldPlacement = this.getPlacementOfWidget(aWidgetId);
michael@0 1638 if (!oldPlacement) {
michael@0 1639 return;
michael@0 1640 }
michael@0 1641
michael@0 1642 let placements = gPlacements.get(oldPlacement.area);
michael@0 1643 if (typeof aPosition != "number") {
michael@0 1644 aPosition = placements.length;
michael@0 1645 } else if (aPosition < 0) {
michael@0 1646 aPosition = 0;
michael@0 1647 } else if (aPosition > placements.length) {
michael@0 1648 aPosition = placements.length;
michael@0 1649 }
michael@0 1650
michael@0 1651 let widget = gPalette.get(aWidgetId);
michael@0 1652 if (widget) {
michael@0 1653 widget.currentPosition = aPosition;
michael@0 1654 widget.currentArea = oldPlacement.area;
michael@0 1655 }
michael@0 1656
michael@0 1657 if (aPosition == oldPlacement.position) {
michael@0 1658 return;
michael@0 1659 }
michael@0 1660
michael@0 1661 placements.splice(oldPlacement.position, 1);
michael@0 1662 // If we just removed the item from *before* where it is now added,
michael@0 1663 // we need to compensate the position offset for that:
michael@0 1664 if (oldPlacement.position < aPosition) {
michael@0 1665 aPosition--;
michael@0 1666 }
michael@0 1667 placements.splice(aPosition, 0, aWidgetId);
michael@0 1668
michael@0 1669 gDirty = true;
michael@0 1670 gDirtyAreaCache.add(oldPlacement.area);
michael@0 1671
michael@0 1672 this.saveState();
michael@0 1673
michael@0 1674 this.notifyListeners("onWidgetMoved", aWidgetId, oldPlacement.area,
michael@0 1675 oldPlacement.position, aPosition);
michael@0 1676 },
michael@0 1677
michael@0 1678 // Note that this does not populate gPlacements, which is done lazily so that
michael@0 1679 // the legacy state can be migrated, which is only available once a browser
michael@0 1680 // window is openned.
michael@0 1681 // The panel area is an exception here, since it has no legacy state and is
michael@0 1682 // built lazily - and therefore wouldn't otherwise result in restoring its
michael@0 1683 // state immediately when a browser window opens, which is important for
michael@0 1684 // other consumers of this API.
michael@0 1685 loadSavedState: function() {
michael@0 1686 let state = null;
michael@0 1687 try {
michael@0 1688 state = Services.prefs.getCharPref(kPrefCustomizationState);
michael@0 1689 } catch (e) {
michael@0 1690 LOG("No saved state found");
michael@0 1691 // This will fail if nothing has been customized, so silently fall back to
michael@0 1692 // the defaults.
michael@0 1693 }
michael@0 1694
michael@0 1695 if (!state) {
michael@0 1696 return;
michael@0 1697 }
michael@0 1698 try {
michael@0 1699 gSavedState = JSON.parse(state);
michael@0 1700 if (typeof gSavedState != "object" || gSavedState === null) {
michael@0 1701 throw "Invalid saved state";
michael@0 1702 }
michael@0 1703 } catch(e) {
michael@0 1704 Services.prefs.clearUserPref(kPrefCustomizationState);
michael@0 1705 gSavedState = {};
michael@0 1706 LOG("Error loading saved UI customization state, falling back to defaults.");
michael@0 1707 }
michael@0 1708
michael@0 1709 if (!("placements" in gSavedState)) {
michael@0 1710 gSavedState.placements = {};
michael@0 1711 }
michael@0 1712
michael@0 1713 gSeenWidgets = new Set(gSavedState.seen || []);
michael@0 1714 gDirtyAreaCache = new Set(gSavedState.dirtyAreaCache || []);
michael@0 1715 gNewElementCount = gSavedState.newElementCount || 0;
michael@0 1716 },
michael@0 1717
michael@0 1718 restoreStateForArea: function(aArea, aLegacyState) {
michael@0 1719 let placementsPreexisted = gPlacements.has(aArea);
michael@0 1720
michael@0 1721 this.beginBatchUpdate();
michael@0 1722 try {
michael@0 1723 gRestoring = true;
michael@0 1724
michael@0 1725 let restored = false;
michael@0 1726 if (placementsPreexisted) {
michael@0 1727 LOG("Restoring " + aArea + " from pre-existing placements");
michael@0 1728 for (let [position, id] in Iterator(gPlacements.get(aArea))) {
michael@0 1729 this.moveWidgetWithinArea(id, position);
michael@0 1730 }
michael@0 1731 gDirty = false;
michael@0 1732 restored = true;
michael@0 1733 } else {
michael@0 1734 gPlacements.set(aArea, []);
michael@0 1735 }
michael@0 1736
michael@0 1737 if (!restored && gSavedState && aArea in gSavedState.placements) {
michael@0 1738 LOG("Restoring " + aArea + " from saved state");
michael@0 1739 let placements = gSavedState.placements[aArea];
michael@0 1740 for (let id of placements)
michael@0 1741 this.addWidgetToArea(id, aArea);
michael@0 1742 gDirty = false;
michael@0 1743 restored = true;
michael@0 1744 }
michael@0 1745
michael@0 1746 if (!restored && aLegacyState) {
michael@0 1747 LOG("Restoring " + aArea + " from legacy state");
michael@0 1748 for (let id of aLegacyState)
michael@0 1749 this.addWidgetToArea(id, aArea);
michael@0 1750 // Don't override dirty state, to ensure legacy state is saved here and
michael@0 1751 // therefore only used once.
michael@0 1752 restored = true;
michael@0 1753 }
michael@0 1754
michael@0 1755 if (!restored) {
michael@0 1756 LOG("Restoring " + aArea + " from default state");
michael@0 1757 let defaults = gAreas.get(aArea).get("defaultPlacements");
michael@0 1758 if (defaults) {
michael@0 1759 for (let id of defaults)
michael@0 1760 this.addWidgetToArea(id, aArea, null, true);
michael@0 1761 }
michael@0 1762 gDirty = false;
michael@0 1763 }
michael@0 1764
michael@0 1765 // Finally, add widgets to the area that were added before the it was able
michael@0 1766 // to be restored. This can occur when add-ons register widgets for a
michael@0 1767 // lazily-restored area before it's been restored.
michael@0 1768 if (gFuturePlacements.has(aArea)) {
michael@0 1769 for (let id of gFuturePlacements.get(aArea))
michael@0 1770 this.addWidgetToArea(id, aArea);
michael@0 1771 }
michael@0 1772
michael@0 1773 LOG("Placements for " + aArea + ":\n\t" + gPlacements.get(aArea).join("\n\t"));
michael@0 1774
michael@0 1775 gRestoring = false;
michael@0 1776 } finally {
michael@0 1777 this.endBatchUpdate();
michael@0 1778 }
michael@0 1779 },
michael@0 1780
michael@0 1781 saveState: function() {
michael@0 1782 if (gInBatchStack || !gDirty) {
michael@0 1783 return;
michael@0 1784 }
michael@0 1785 let state = { placements: gPlacements,
michael@0 1786 seen: gSeenWidgets,
michael@0 1787 dirtyAreaCache: gDirtyAreaCache,
michael@0 1788 newElementCount: gNewElementCount };
michael@0 1789
michael@0 1790 LOG("Saving state.");
michael@0 1791 let serialized = JSON.stringify(state, this.serializerHelper);
michael@0 1792 LOG("State saved as: " + serialized);
michael@0 1793 Services.prefs.setCharPref(kPrefCustomizationState, serialized);
michael@0 1794 gDirty = false;
michael@0 1795 },
michael@0 1796
michael@0 1797 serializerHelper: function(aKey, aValue) {
michael@0 1798 if (typeof aValue == "object" && aValue.constructor.name == "Map") {
michael@0 1799 let result = {};
michael@0 1800 for (let [mapKey, mapValue] of aValue)
michael@0 1801 result[mapKey] = mapValue;
michael@0 1802 return result;
michael@0 1803 }
michael@0 1804
michael@0 1805 if (typeof aValue == "object" && aValue.constructor.name == "Set") {
michael@0 1806 return [...aValue];
michael@0 1807 }
michael@0 1808
michael@0 1809 return aValue;
michael@0 1810 },
michael@0 1811
michael@0 1812 beginBatchUpdate: function() {
michael@0 1813 gInBatchStack++;
michael@0 1814 },
michael@0 1815
michael@0 1816 endBatchUpdate: function(aForceDirty) {
michael@0 1817 gInBatchStack--;
michael@0 1818 if (aForceDirty === true) {
michael@0 1819 gDirty = true;
michael@0 1820 }
michael@0 1821 if (gInBatchStack == 0) {
michael@0 1822 this.saveState();
michael@0 1823 } else if (gInBatchStack < 0) {
michael@0 1824 throw new Error("The batch editing stack should never reach a negative number.");
michael@0 1825 }
michael@0 1826 },
michael@0 1827
michael@0 1828 addListener: function(aListener) {
michael@0 1829 gListeners.add(aListener);
michael@0 1830 },
michael@0 1831
michael@0 1832 removeListener: function(aListener) {
michael@0 1833 if (aListener == this) {
michael@0 1834 return;
michael@0 1835 }
michael@0 1836
michael@0 1837 gListeners.delete(aListener);
michael@0 1838 },
michael@0 1839
michael@0 1840 notifyListeners: function(aEvent, ...aArgs) {
michael@0 1841 if (gRestoring) {
michael@0 1842 return;
michael@0 1843 }
michael@0 1844
michael@0 1845 for (let listener of gListeners) {
michael@0 1846 try {
michael@0 1847 if (typeof listener[aEvent] == "function") {
michael@0 1848 listener[aEvent].apply(listener, aArgs);
michael@0 1849 }
michael@0 1850 } catch (e) {
michael@0 1851 ERROR(e + " -- " + e.fileName + ":" + e.lineNumber);
michael@0 1852 }
michael@0 1853 }
michael@0 1854 },
michael@0 1855
michael@0 1856 _dispatchToolboxEventToWindow: function(aEventType, aDetails, aWindow) {
michael@0 1857 let evt = new aWindow.CustomEvent(aEventType, {
michael@0 1858 bubbles: true,
michael@0 1859 cancelable: true,
michael@0 1860 detail: aDetails
michael@0 1861 });
michael@0 1862 aWindow.gNavToolbox.dispatchEvent(evt);
michael@0 1863 },
michael@0 1864
michael@0 1865 dispatchToolboxEvent: function(aEventType, aDetails={}, aWindow=null) {
michael@0 1866 if (aWindow) {
michael@0 1867 return this._dispatchToolboxEventToWindow(aEventType, aDetails, aWindow);
michael@0 1868 }
michael@0 1869 for (let [win, ] of gBuildWindows) {
michael@0 1870 this._dispatchToolboxEventToWindow(aEventType, aDetails, win);
michael@0 1871 }
michael@0 1872 },
michael@0 1873
michael@0 1874 createWidget: function(aProperties) {
michael@0 1875 let widget = this.normalizeWidget(aProperties, CustomizableUI.SOURCE_EXTERNAL);
michael@0 1876 //XXXunf This should probably throw.
michael@0 1877 if (!widget) {
michael@0 1878 return;
michael@0 1879 }
michael@0 1880
michael@0 1881 gPalette.set(widget.id, widget);
michael@0 1882
michael@0 1883 // Clear our caches:
michael@0 1884 gGroupWrapperCache.delete(widget.id);
michael@0 1885 for (let [win, ] of gBuildWindows) {
michael@0 1886 let cache = gSingleWrapperCache.get(win);
michael@0 1887 if (cache) {
michael@0 1888 cache.delete(widget.id);
michael@0 1889 }
michael@0 1890 }
michael@0 1891
michael@0 1892 this.notifyListeners("onWidgetCreated", widget.id);
michael@0 1893
michael@0 1894 if (widget.defaultArea) {
michael@0 1895 let area = gAreas.get(widget.defaultArea);
michael@0 1896 //XXXgijs this won't have any effect for legacy items. Sort of OK because
michael@0 1897 // consumers can modify currentset? Maybe?
michael@0 1898 if (area.has("defaultPlacements")) {
michael@0 1899 area.get("defaultPlacements").push(widget.id);
michael@0 1900 } else {
michael@0 1901 area.set("defaultPlacements", [widget.id]);
michael@0 1902 }
michael@0 1903 }
michael@0 1904
michael@0 1905 // Look through previously saved state to see if we're restoring a widget.
michael@0 1906 let seenAreas = new Set();
michael@0 1907 let widgetMightNeedAutoAdding = true;
michael@0 1908 for (let [area, placements] of gPlacements) {
michael@0 1909 seenAreas.add(area);
michael@0 1910 let areaIsRegistered = gAreas.has(area);
michael@0 1911 let index = gPlacements.get(area).indexOf(widget.id);
michael@0 1912 if (index != -1) {
michael@0 1913 widgetMightNeedAutoAdding = false;
michael@0 1914 if (areaIsRegistered) {
michael@0 1915 widget.currentArea = area;
michael@0 1916 widget.currentPosition = index;
michael@0 1917 }
michael@0 1918 break;
michael@0 1919 }
michael@0 1920 }
michael@0 1921
michael@0 1922 // Also look at saved state data directly in areas that haven't yet been
michael@0 1923 // restored. Can't rely on this for restored areas, as they may have
michael@0 1924 // changed.
michael@0 1925 if (widgetMightNeedAutoAdding && gSavedState) {
michael@0 1926 for (let area of Object.keys(gSavedState.placements)) {
michael@0 1927 if (seenAreas.has(area)) {
michael@0 1928 continue;
michael@0 1929 }
michael@0 1930
michael@0 1931 let areaIsRegistered = gAreas.has(area);
michael@0 1932 let index = gSavedState.placements[area].indexOf(widget.id);
michael@0 1933 if (index != -1) {
michael@0 1934 widgetMightNeedAutoAdding = false;
michael@0 1935 if (areaIsRegistered) {
michael@0 1936 widget.currentArea = area;
michael@0 1937 widget.currentPosition = index;
michael@0 1938 }
michael@0 1939 break;
michael@0 1940 }
michael@0 1941 }
michael@0 1942 }
michael@0 1943
michael@0 1944 // If we're restoring the widget to it's old placement, fire off the
michael@0 1945 // onWidgetAdded event - our own handler will take care of adding it to
michael@0 1946 // any build areas.
michael@0 1947 if (widget.currentArea) {
michael@0 1948 this.notifyListeners("onWidgetAdded", widget.id, widget.currentArea,
michael@0 1949 widget.currentPosition);
michael@0 1950 } else if (widgetMightNeedAutoAdding) {
michael@0 1951 let autoAdd = true;
michael@0 1952 try {
michael@0 1953 autoAdd = Services.prefs.getBoolPref(kPrefCustomizationAutoAdd);
michael@0 1954 } catch (e) {}
michael@0 1955
michael@0 1956 // If the widget doesn't have an existing placement, and it hasn't been
michael@0 1957 // seen before, then add it to its default area so it can be used.
michael@0 1958 // If the widget is not removable, we *have* to add it to its default
michael@0 1959 // area here.
michael@0 1960 let canBeAutoAdded = autoAdd && !gSeenWidgets.has(widget.id);
michael@0 1961 if (!widget.currentArea && (!widget.removable || canBeAutoAdded)) {
michael@0 1962 this.beginBatchUpdate();
michael@0 1963 try {
michael@0 1964 gSeenWidgets.add(widget.id);
michael@0 1965
michael@0 1966 if (widget.defaultArea) {
michael@0 1967 if (this.isAreaLazy(widget.defaultArea)) {
michael@0 1968 gFuturePlacements.get(widget.defaultArea).add(widget.id);
michael@0 1969 } else {
michael@0 1970 this.addWidgetToArea(widget.id, widget.defaultArea);
michael@0 1971 }
michael@0 1972 }
michael@0 1973 } finally {
michael@0 1974 this.endBatchUpdate(true);
michael@0 1975 }
michael@0 1976 }
michael@0 1977 }
michael@0 1978
michael@0 1979 this.notifyListeners("onWidgetAfterCreation", widget.id, widget.currentArea);
michael@0 1980 return widget.id;
michael@0 1981 },
michael@0 1982
michael@0 1983 createBuiltinWidget: function(aData) {
michael@0 1984 // This should only ever be called on startup, before any windows are
michael@0 1985 // opened - so we know there's no build areas to handle. Also, builtin
michael@0 1986 // widgets are expected to be (mostly) static, so shouldn't affect the
michael@0 1987 // current placement settings.
michael@0 1988 let widget = this.normalizeWidget(aData, CustomizableUI.SOURCE_BUILTIN);
michael@0 1989 if (!widget) {
michael@0 1990 ERROR("Error creating builtin widget: " + aData.id);
michael@0 1991 return;
michael@0 1992 }
michael@0 1993
michael@0 1994 LOG("Creating built-in widget with id: " + widget.id);
michael@0 1995 gPalette.set(widget.id, widget);
michael@0 1996 },
michael@0 1997
michael@0 1998 // Returns true if the area will eventually lazily restore (but hasn't yet).
michael@0 1999 isAreaLazy: function(aArea) {
michael@0 2000 if (gPlacements.has(aArea)) {
michael@0 2001 return false;
michael@0 2002 }
michael@0 2003 return gAreas.get(aArea).has("legacy");
michael@0 2004 },
michael@0 2005
michael@0 2006 //XXXunf Log some warnings here, when the data provided isn't up to scratch.
michael@0 2007 normalizeWidget: function(aData, aSource) {
michael@0 2008 let widget = {
michael@0 2009 implementation: aData,
michael@0 2010 source: aSource || "addon",
michael@0 2011 instances: new Map(),
michael@0 2012 currentArea: null,
michael@0 2013 removable: true,
michael@0 2014 overflows: true,
michael@0 2015 defaultArea: null,
michael@0 2016 shortcutId: null,
michael@0 2017 tooltiptext: null,
michael@0 2018 showInPrivateBrowsing: true,
michael@0 2019 };
michael@0 2020
michael@0 2021 if (typeof aData.id != "string" || !/^[a-z0-9-_]{1,}$/i.test(aData.id)) {
michael@0 2022 ERROR("Given an illegal id in normalizeWidget: " + aData.id);
michael@0 2023 return null;
michael@0 2024 }
michael@0 2025
michael@0 2026 delete widget.implementation.currentArea;
michael@0 2027 widget.implementation.__defineGetter__("currentArea", function() widget.currentArea);
michael@0 2028
michael@0 2029 const kReqStringProps = ["id"];
michael@0 2030 for (let prop of kReqStringProps) {
michael@0 2031 if (typeof aData[prop] != "string") {
michael@0 2032 ERROR("Missing required property '" + prop + "' in normalizeWidget: "
michael@0 2033 + aData.id);
michael@0 2034 return null;
michael@0 2035 }
michael@0 2036 widget[prop] = aData[prop];
michael@0 2037 }
michael@0 2038
michael@0 2039 const kOptStringProps = ["label", "tooltiptext", "shortcutId"];
michael@0 2040 for (let prop of kOptStringProps) {
michael@0 2041 if (typeof aData[prop] == "string") {
michael@0 2042 widget[prop] = aData[prop];
michael@0 2043 }
michael@0 2044 }
michael@0 2045
michael@0 2046 const kOptBoolProps = ["removable", "showInPrivateBrowsing", "overflows"];
michael@0 2047 for (let prop of kOptBoolProps) {
michael@0 2048 if (typeof aData[prop] == "boolean") {
michael@0 2049 widget[prop] = aData[prop];
michael@0 2050 }
michael@0 2051 }
michael@0 2052
michael@0 2053 if (aData.defaultArea && gAreas.has(aData.defaultArea)) {
michael@0 2054 widget.defaultArea = aData.defaultArea;
michael@0 2055 } else if (!widget.removable) {
michael@0 2056 ERROR("Widget '" + widget.id + "' is not removable but does not specify " +
michael@0 2057 "a valid defaultArea. That's not possible; it must specify a " +
michael@0 2058 "valid defaultArea as well.");
michael@0 2059 return null;
michael@0 2060 }
michael@0 2061
michael@0 2062 if ("type" in aData && gSupportedWidgetTypes.has(aData.type)) {
michael@0 2063 widget.type = aData.type;
michael@0 2064 } else {
michael@0 2065 widget.type = "button";
michael@0 2066 }
michael@0 2067
michael@0 2068 widget.disabled = aData.disabled === true;
michael@0 2069
michael@0 2070 this.wrapWidgetEventHandler("onBeforeCreated", widget);
michael@0 2071 this.wrapWidgetEventHandler("onClick", widget);
michael@0 2072 this.wrapWidgetEventHandler("onCreated", widget);
michael@0 2073
michael@0 2074 if (widget.type == "button") {
michael@0 2075 widget.onCommand = typeof aData.onCommand == "function" ?
michael@0 2076 aData.onCommand :
michael@0 2077 null;
michael@0 2078 } else if (widget.type == "view") {
michael@0 2079 if (typeof aData.viewId != "string") {
michael@0 2080 ERROR("Expected a string for widget " + widget.id + " viewId, but got "
michael@0 2081 + aData.viewId);
michael@0 2082 return null;
michael@0 2083 }
michael@0 2084 widget.viewId = aData.viewId;
michael@0 2085
michael@0 2086 this.wrapWidgetEventHandler("onViewShowing", widget);
michael@0 2087 this.wrapWidgetEventHandler("onViewHiding", widget);
michael@0 2088 } else if (widget.type == "custom") {
michael@0 2089 this.wrapWidgetEventHandler("onBuild", widget);
michael@0 2090 }
michael@0 2091
michael@0 2092 if (gPalette.has(widget.id)) {
michael@0 2093 return null;
michael@0 2094 }
michael@0 2095
michael@0 2096 return widget;
michael@0 2097 },
michael@0 2098
michael@0 2099 wrapWidgetEventHandler: function(aEventName, aWidget) {
michael@0 2100 if (typeof aWidget.implementation[aEventName] != "function") {
michael@0 2101 aWidget[aEventName] = null;
michael@0 2102 return;
michael@0 2103 }
michael@0 2104 aWidget[aEventName] = function(...aArgs) {
michael@0 2105 // Wrap inside a try...catch to properly log errors, until bug 862627 is
michael@0 2106 // fixed, which in turn might help bug 503244.
michael@0 2107 try {
michael@0 2108 // Don't copy the function to the normalized widget object, instead
michael@0 2109 // keep it on the original object provided to the API so that
michael@0 2110 // additional methods can be implemented and used by the event
michael@0 2111 // handlers.
michael@0 2112 return aWidget.implementation[aEventName].apply(aWidget.implementation,
michael@0 2113 aArgs);
michael@0 2114 } catch (e) {
michael@0 2115 Cu.reportError(e);
michael@0 2116 }
michael@0 2117 };
michael@0 2118 },
michael@0 2119
michael@0 2120 destroyWidget: function(aWidgetId) {
michael@0 2121 let widget = gPalette.get(aWidgetId);
michael@0 2122 if (!widget) {
michael@0 2123 gGroupWrapperCache.delete(aWidgetId);
michael@0 2124 for (let [window, ] of gBuildWindows) {
michael@0 2125 let windowCache = gSingleWrapperCache.get(window);
michael@0 2126 if (windowCache) {
michael@0 2127 windowCache.delete(aWidgetId);
michael@0 2128 }
michael@0 2129 }
michael@0 2130 return;
michael@0 2131 }
michael@0 2132
michael@0 2133 // Remove it from the default placements of an area if it was added there:
michael@0 2134 if (widget.defaultArea) {
michael@0 2135 let area = gAreas.get(widget.defaultArea);
michael@0 2136 if (area) {
michael@0 2137 let defaultPlacements = area.get("defaultPlacements");
michael@0 2138 // We can assume this is present because if a widget has a defaultArea,
michael@0 2139 // we automatically create a defaultPlacements array for that area.
michael@0 2140 let widgetIndex = defaultPlacements.indexOf(aWidgetId);
michael@0 2141 if (widgetIndex != -1) {
michael@0 2142 defaultPlacements.splice(widgetIndex, 1);
michael@0 2143 }
michael@0 2144 }
michael@0 2145 }
michael@0 2146
michael@0 2147 // This will not remove the widget from gPlacements - we want to keep the
michael@0 2148 // setting so the widget gets put back in it's old position if/when it
michael@0 2149 // returns.
michael@0 2150 for (let [window, ] of gBuildWindows) {
michael@0 2151 let windowCache = gSingleWrapperCache.get(window);
michael@0 2152 if (windowCache) {
michael@0 2153 windowCache.delete(aWidgetId);
michael@0 2154 }
michael@0 2155 let widgetNode = window.document.getElementById(aWidgetId) ||
michael@0 2156 window.gNavToolbox.palette.getElementsByAttribute("id", aWidgetId)[0];
michael@0 2157 if (widgetNode) {
michael@0 2158 let container = widgetNode.parentNode
michael@0 2159 this.notifyListeners("onWidgetBeforeDOMChange", widgetNode, null,
michael@0 2160 container, true);
michael@0 2161 widgetNode.remove();
michael@0 2162 this.notifyListeners("onWidgetAfterDOMChange", widgetNode, null,
michael@0 2163 container, true);
michael@0 2164 }
michael@0 2165 if (widget.type == "view") {
michael@0 2166 let viewNode = window.document.getElementById(widget.viewId);
michael@0 2167 if (viewNode) {
michael@0 2168 for (let eventName of kSubviewEvents) {
michael@0 2169 let handler = "on" + eventName;
michael@0 2170 if (typeof widget[handler] == "function") {
michael@0 2171 viewNode.removeEventListener(eventName, widget[handler], false);
michael@0 2172 }
michael@0 2173 }
michael@0 2174 }
michael@0 2175 }
michael@0 2176 }
michael@0 2177
michael@0 2178 gPalette.delete(aWidgetId);
michael@0 2179 gGroupWrapperCache.delete(aWidgetId);
michael@0 2180
michael@0 2181 this.notifyListeners("onWidgetDestroyed", aWidgetId);
michael@0 2182 },
michael@0 2183
michael@0 2184 getCustomizeTargetForArea: function(aArea, aWindow) {
michael@0 2185 let buildAreaNodes = gBuildAreas.get(aArea);
michael@0 2186 if (!buildAreaNodes) {
michael@0 2187 return null;
michael@0 2188 }
michael@0 2189
michael@0 2190 for (let node of buildAreaNodes) {
michael@0 2191 if (node.ownerDocument.defaultView === aWindow) {
michael@0 2192 return node.customizationTarget ? node.customizationTarget : node;
michael@0 2193 }
michael@0 2194 }
michael@0 2195
michael@0 2196 return null;
michael@0 2197 },
michael@0 2198
michael@0 2199 reset: function() {
michael@0 2200 gResetting = true;
michael@0 2201 this._resetUIState();
michael@0 2202
michael@0 2203 // Rebuild each registered area (across windows) to reflect the state that
michael@0 2204 // was reset above.
michael@0 2205 this._rebuildRegisteredAreas();
michael@0 2206
michael@0 2207 gResetting = false;
michael@0 2208 },
michael@0 2209
michael@0 2210 _resetUIState: function() {
michael@0 2211 try {
michael@0 2212 gUIStateBeforeReset.drawInTitlebar = Services.prefs.getBoolPref(kPrefDrawInTitlebar);
michael@0 2213 gUIStateBeforeReset.uiCustomizationState = Services.prefs.getCharPref(kPrefCustomizationState);
michael@0 2214 } catch(e) { }
michael@0 2215
michael@0 2216 this._resetExtraToolbars();
michael@0 2217
michael@0 2218 Services.prefs.clearUserPref(kPrefCustomizationState);
michael@0 2219 Services.prefs.clearUserPref(kPrefDrawInTitlebar);
michael@0 2220 LOG("State reset");
michael@0 2221
michael@0 2222 // Reset placements to make restoring default placements possible.
michael@0 2223 gPlacements = new Map();
michael@0 2224 gDirtyAreaCache = new Set();
michael@0 2225 gSeenWidgets = new Set();
michael@0 2226 // Clear the saved state to ensure that defaults will be used.
michael@0 2227 gSavedState = null;
michael@0 2228 // Restore the state for each area to its defaults
michael@0 2229 for (let [areaId,] of gAreas) {
michael@0 2230 this.restoreStateForArea(areaId);
michael@0 2231 }
michael@0 2232 },
michael@0 2233
michael@0 2234 _resetExtraToolbars: function(aFilter = null) {
michael@0 2235 let firstWindow = true; // Only need to unregister and persist once
michael@0 2236 for (let [win, ] of gBuildWindows) {
michael@0 2237 let toolbox = win.gNavToolbox;
michael@0 2238 for (let child of toolbox.children) {
michael@0 2239 let matchesFilter = !aFilter || aFilter == child.id;
michael@0 2240 if (child.hasAttribute("customindex") && matchesFilter) {
michael@0 2241 let toolbarId = "toolbar" + child.getAttribute("customindex");
michael@0 2242 toolbox.toolbarset.removeAttribute(toolbarId);
michael@0 2243 if (firstWindow) {
michael@0 2244 win.document.persist(toolbox.toolbarset.id, toolbarId);
michael@0 2245 // We have to unregister it properly to ensure we don't kill
michael@0 2246 // XUL widgets which might be in here
michael@0 2247 this.unregisterArea(child.id, true);
michael@0 2248 }
michael@0 2249 child.remove();
michael@0 2250 }
michael@0 2251 }
michael@0 2252 firstWindow = false;
michael@0 2253 }
michael@0 2254 },
michael@0 2255
michael@0 2256 _rebuildRegisteredAreas: function() {
michael@0 2257 for (let [areaId, areaNodes] of gBuildAreas) {
michael@0 2258 let placements = gPlacements.get(areaId);
michael@0 2259 let isFirstChangedToolbar = true;
michael@0 2260 for (let areaNode of areaNodes) {
michael@0 2261 this.buildArea(areaId, placements, areaNode);
michael@0 2262
michael@0 2263 let area = gAreas.get(areaId);
michael@0 2264 if (area.get("type") == CustomizableUI.TYPE_TOOLBAR) {
michael@0 2265 let defaultCollapsed = area.get("defaultCollapsed");
michael@0 2266 let win = areaNode.ownerDocument.defaultView;
michael@0 2267 if (defaultCollapsed !== null) {
michael@0 2268 win.setToolbarVisibility(areaNode, !defaultCollapsed, isFirstChangedToolbar);
michael@0 2269 }
michael@0 2270 }
michael@0 2271 isFirstChangedToolbar = false;
michael@0 2272 }
michael@0 2273 }
michael@0 2274 },
michael@0 2275
michael@0 2276 /**
michael@0 2277 * Undoes a previous reset, restoring the state of the UI to the state prior to the reset.
michael@0 2278 */
michael@0 2279 undoReset: function() {
michael@0 2280 if (gUIStateBeforeReset.uiCustomizationState == null ||
michael@0 2281 gUIStateBeforeReset.drawInTitlebar == null) {
michael@0 2282 return;
michael@0 2283 }
michael@0 2284 gUndoResetting = true;
michael@0 2285
michael@0 2286 let uiCustomizationState = gUIStateBeforeReset.uiCustomizationState;
michael@0 2287 let drawInTitlebar = gUIStateBeforeReset.drawInTitlebar;
michael@0 2288
michael@0 2289 // Need to clear the previous state before setting the prefs
michael@0 2290 // because pref observers may check if there is a previous UI state.
michael@0 2291 this._clearPreviousUIState();
michael@0 2292
michael@0 2293 Services.prefs.setCharPref(kPrefCustomizationState, uiCustomizationState);
michael@0 2294 Services.prefs.setBoolPref(kPrefDrawInTitlebar, drawInTitlebar);
michael@0 2295 this.loadSavedState();
michael@0 2296 // If the user just customizes toolbar/titlebar visibility, gSavedState will be null
michael@0 2297 // and we don't need to do anything else here:
michael@0 2298 if (gSavedState) {
michael@0 2299 for (let areaId of Object.keys(gSavedState.placements)) {
michael@0 2300 let placements = gSavedState.placements[areaId];
michael@0 2301 gPlacements.set(areaId, placements);
michael@0 2302 }
michael@0 2303 this._rebuildRegisteredAreas();
michael@0 2304 }
michael@0 2305
michael@0 2306 gUndoResetting = false;
michael@0 2307 },
michael@0 2308
michael@0 2309 _clearPreviousUIState: function() {
michael@0 2310 Object.getOwnPropertyNames(gUIStateBeforeReset).forEach((prop) => {
michael@0 2311 gUIStateBeforeReset[prop] = null;
michael@0 2312 });
michael@0 2313 },
michael@0 2314
michael@0 2315 removeExtraToolbar: function(aToolbarId) {
michael@0 2316 this._resetExtraToolbars(aToolbarId);
michael@0 2317 },
michael@0 2318
michael@0 2319 /**
michael@0 2320 * @param {String|Node} aWidget - widget ID or a widget node (preferred for performance).
michael@0 2321 * @return {Boolean} whether the widget is removable
michael@0 2322 */
michael@0 2323 isWidgetRemovable: function(aWidget) {
michael@0 2324 let widgetId;
michael@0 2325 let widgetNode;
michael@0 2326 if (typeof aWidget == "string") {
michael@0 2327 widgetId = aWidget;
michael@0 2328 } else {
michael@0 2329 widgetId = aWidget.id;
michael@0 2330 widgetNode = aWidget;
michael@0 2331 }
michael@0 2332 let provider = this.getWidgetProvider(widgetId);
michael@0 2333
michael@0 2334 if (provider == CustomizableUI.PROVIDER_API) {
michael@0 2335 return gPalette.get(widgetId).removable;
michael@0 2336 }
michael@0 2337
michael@0 2338 if (provider == CustomizableUI.PROVIDER_XUL) {
michael@0 2339 if (gBuildWindows.size == 0) {
michael@0 2340 // We don't have any build windows to look at, so just assume for now
michael@0 2341 // that its removable.
michael@0 2342 return true;
michael@0 2343 }
michael@0 2344
michael@0 2345 if (!widgetNode) {
michael@0 2346 // Pick any of the build windows to look at.
michael@0 2347 let [window,] = [...gBuildWindows][0];
michael@0 2348 [, widgetNode] = this.getWidgetNode(widgetId, window);
michael@0 2349 }
michael@0 2350 // If we don't have a node, we assume it's removable. This can happen because
michael@0 2351 // getWidgetProvider returns PROVIDER_XUL by default, but this will also happen
michael@0 2352 // for API-provided widgets which have been destroyed.
michael@0 2353 if (!widgetNode) {
michael@0 2354 return true;
michael@0 2355 }
michael@0 2356 return widgetNode.getAttribute("removable") == "true";
michael@0 2357 }
michael@0 2358
michael@0 2359 // Otherwise this is either a special widget, which is always removable, or
michael@0 2360 // an API widget which has already been removed from gPalette. Returning true
michael@0 2361 // here allows us to then remove its ID from any placements where it might
michael@0 2362 // still occur.
michael@0 2363 return true;
michael@0 2364 },
michael@0 2365
michael@0 2366 canWidgetMoveToArea: function(aWidgetId, aArea) {
michael@0 2367 let placement = this.getPlacementOfWidget(aWidgetId);
michael@0 2368 if (placement && placement.area != aArea) {
michael@0 2369 // Special widgets can't move to the menu panel.
michael@0 2370 if (this.isSpecialWidget(aWidgetId) && gAreas.has(aArea) &&
michael@0 2371 gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL) {
michael@0 2372 return false;
michael@0 2373 }
michael@0 2374 // For everything else, just return whether the widget is removable.
michael@0 2375 return this.isWidgetRemovable(aWidgetId);
michael@0 2376 }
michael@0 2377
michael@0 2378 return true;
michael@0 2379 },
michael@0 2380
michael@0 2381 ensureWidgetPlacedInWindow: function(aWidgetId, aWindow) {
michael@0 2382 let placement = this.getPlacementOfWidget(aWidgetId);
michael@0 2383 if (!placement) {
michael@0 2384 return false;
michael@0 2385 }
michael@0 2386 let areaNodes = gBuildAreas.get(placement.area);
michael@0 2387 if (!areaNodes) {
michael@0 2388 return false;
michael@0 2389 }
michael@0 2390 let container = [...areaNodes].filter((n) => n.ownerDocument.defaultView == aWindow);
michael@0 2391 if (!container.length) {
michael@0 2392 return false;
michael@0 2393 }
michael@0 2394 let existingNode = container[0].getElementsByAttribute("id", aWidgetId)[0];
michael@0 2395 if (existingNode) {
michael@0 2396 return true;
michael@0 2397 }
michael@0 2398
michael@0 2399 this.insertNodeInWindow(aWidgetId, container[0], true);
michael@0 2400 return true;
michael@0 2401 },
michael@0 2402
michael@0 2403 get inDefaultState() {
michael@0 2404 for (let [areaId, props] of gAreas) {
michael@0 2405 let defaultPlacements = props.get("defaultPlacements");
michael@0 2406 // Areas without default placements (like legacy ones?) get skipped
michael@0 2407 if (!defaultPlacements) {
michael@0 2408 continue;
michael@0 2409 }
michael@0 2410
michael@0 2411 let currentPlacements = gPlacements.get(areaId);
michael@0 2412 // We're excluding all of the placement IDs for items that do not exist,
michael@0 2413 // and items that have removable="false",
michael@0 2414 // because we don't want to consider them when determining if we're
michael@0 2415 // in the default state. This way, if an add-on introduces a widget
michael@0 2416 // and is then uninstalled, the leftover placement doesn't cause us to
michael@0 2417 // automatically assume that the buttons are not in the default state.
michael@0 2418 let buildAreaNodes = gBuildAreas.get(areaId);
michael@0 2419 if (buildAreaNodes && buildAreaNodes.size) {
michael@0 2420 let container = [...buildAreaNodes][0];
michael@0 2421 let removableOrDefault = (itemNodeOrItem) => {
michael@0 2422 let item = (itemNodeOrItem && itemNodeOrItem.id) || itemNodeOrItem;
michael@0 2423 let isRemovable = this.isWidgetRemovable(itemNodeOrItem);
michael@0 2424 let isInDefault = defaultPlacements.indexOf(item) != -1;
michael@0 2425 return isRemovable || isInDefault;
michael@0 2426 };
michael@0 2427 // Toolbars have a currentSet property which also deals correctly with overflown
michael@0 2428 // widgets (if any) - use that instead:
michael@0 2429 if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) {
michael@0 2430 let currentSet = container.currentSet;
michael@0 2431 currentPlacements = currentSet ? currentSet.split(',') : [];
michael@0 2432 currentPlacements = currentPlacements.filter(removableOrDefault);
michael@0 2433 } else {
michael@0 2434 // Clone the array so we don't modify the actual placements...
michael@0 2435 currentPlacements = [...currentPlacements];
michael@0 2436 currentPlacements = currentPlacements.filter((item) => {
michael@0 2437 let itemNode = container.getElementsByAttribute("id", item)[0];
michael@0 2438 return itemNode && removableOrDefault(itemNode || item);
michael@0 2439 });
michael@0 2440 }
michael@0 2441
michael@0 2442 if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) {
michael@0 2443 let attribute = container.getAttribute("type") == "menubar" ? "autohide" : "collapsed";
michael@0 2444 let collapsed = container.getAttribute(attribute) == "true";
michael@0 2445 let defaultCollapsed = props.get("defaultCollapsed");
michael@0 2446 if (defaultCollapsed !== null && collapsed != defaultCollapsed) {
michael@0 2447 LOG("Found " + areaId + " had non-default toolbar visibility (expected " + defaultCollapsed + ", was " + collapsed + ")");
michael@0 2448 return false;
michael@0 2449 }
michael@0 2450 }
michael@0 2451 }
michael@0 2452 LOG("Checking default state for " + areaId + ":\n" + currentPlacements.join(",") +
michael@0 2453 "\nvs.\n" + defaultPlacements.join(","));
michael@0 2454
michael@0 2455 if (currentPlacements.length != defaultPlacements.length) {
michael@0 2456 return false;
michael@0 2457 }
michael@0 2458
michael@0 2459 for (let i = 0; i < currentPlacements.length; ++i) {
michael@0 2460 if (currentPlacements[i] != defaultPlacements[i]) {
michael@0 2461 LOG("Found " + currentPlacements[i] + " in " + areaId + " where " +
michael@0 2462 defaultPlacements[i] + " was expected!");
michael@0 2463 return false;
michael@0 2464 }
michael@0 2465 }
michael@0 2466 }
michael@0 2467
michael@0 2468 if (Services.prefs.prefHasUserValue(kPrefDrawInTitlebar)) {
michael@0 2469 LOG(kPrefDrawInTitlebar + " pref is non-default");
michael@0 2470 return false;
michael@0 2471 }
michael@0 2472
michael@0 2473 return true;
michael@0 2474 },
michael@0 2475
michael@0 2476 setToolbarVisibility: function(aToolbarId, aIsVisible) {
michael@0 2477 // We only persist the attribute the first time.
michael@0 2478 let isFirstChangedToolbar = true;
michael@0 2479 for (let window of CustomizableUI.windows) {
michael@0 2480 let toolbar = window.document.getElementById(aToolbarId);
michael@0 2481 if (toolbar) {
michael@0 2482 window.setToolbarVisibility(toolbar, aIsVisible, isFirstChangedToolbar);
michael@0 2483 isFirstChangedToolbar = false;
michael@0 2484 }
michael@0 2485 }
michael@0 2486 },
michael@0 2487 };
michael@0 2488 Object.freeze(CustomizableUIInternal);
michael@0 2489
michael@0 2490 this.CustomizableUI = {
michael@0 2491 /**
michael@0 2492 * Constant reference to the ID of the menu panel.
michael@0 2493 */
michael@0 2494 get AREA_PANEL() "PanelUI-contents",
michael@0 2495 /**
michael@0 2496 * Constant reference to the ID of the navigation toolbar.
michael@0 2497 */
michael@0 2498 get AREA_NAVBAR() "nav-bar",
michael@0 2499 /**
michael@0 2500 * Constant reference to the ID of the menubar's toolbar.
michael@0 2501 */
michael@0 2502 get AREA_MENUBAR() "toolbar-menubar",
michael@0 2503 /**
michael@0 2504 * Constant reference to the ID of the tabstrip toolbar.
michael@0 2505 */
michael@0 2506 get AREA_TABSTRIP() "TabsToolbar",
michael@0 2507 /**
michael@0 2508 * Constant reference to the ID of the bookmarks toolbar.
michael@0 2509 */
michael@0 2510 get AREA_BOOKMARKS() "PersonalToolbar",
michael@0 2511 /**
michael@0 2512 * Constant reference to the ID of the addon-bar toolbar shim.
michael@0 2513 * Do not use, this will be removed as soon as reasonably possible.
michael@0 2514 * @deprecated
michael@0 2515 */
michael@0 2516 get AREA_ADDONBAR() "addon-bar",
michael@0 2517 /**
michael@0 2518 * Constant indicating the area is a menu panel.
michael@0 2519 */
michael@0 2520 get TYPE_MENU_PANEL() "menu-panel",
michael@0 2521 /**
michael@0 2522 * Constant indicating the area is a toolbar.
michael@0 2523 */
michael@0 2524 get TYPE_TOOLBAR() "toolbar",
michael@0 2525
michael@0 2526 /**
michael@0 2527 * Constant indicating a XUL-type provider.
michael@0 2528 */
michael@0 2529 get PROVIDER_XUL() "xul",
michael@0 2530 /**
michael@0 2531 * Constant indicating an API-type provider.
michael@0 2532 */
michael@0 2533 get PROVIDER_API() "api",
michael@0 2534 /**
michael@0 2535 * Constant indicating dynamic (special) widgets: spring, spacer, and separator.
michael@0 2536 */
michael@0 2537 get PROVIDER_SPECIAL() "special",
michael@0 2538
michael@0 2539 /**
michael@0 2540 * Constant indicating the widget is built-in
michael@0 2541 */
michael@0 2542 get SOURCE_BUILTIN() "builtin",
michael@0 2543 /**
michael@0 2544 * Constant indicating the widget is externally provided
michael@0 2545 * (e.g. by add-ons or other items not part of the builtin widget set).
michael@0 2546 */
michael@0 2547 get SOURCE_EXTERNAL() "external",
michael@0 2548
michael@0 2549 /**
michael@0 2550 * The class used to distinguish items that span the entire menu panel.
michael@0 2551 */
michael@0 2552 get WIDE_PANEL_CLASS() "panel-wide-item",
michael@0 2553 /**
michael@0 2554 * The (constant) number of columns in the menu panel.
michael@0 2555 */
michael@0 2556 get PANEL_COLUMN_COUNT() 3,
michael@0 2557
michael@0 2558 /**
michael@0 2559 * Constant indicating the reason the event was fired was a window closing
michael@0 2560 */
michael@0 2561 get REASON_WINDOW_CLOSED() "window-closed",
michael@0 2562 /**
michael@0 2563 * Constant indicating the reason the event was fired was an area being
michael@0 2564 * unregistered separately from window closing mechanics.
michael@0 2565 */
michael@0 2566 get REASON_AREA_UNREGISTERED() "area-unregistered",
michael@0 2567
michael@0 2568
michael@0 2569 /**
michael@0 2570 * An iteratable property of windows managed by CustomizableUI.
michael@0 2571 * Note that this can *only* be used as an iterator. ie:
michael@0 2572 * for (let window of CustomizableUI.windows) { ... }
michael@0 2573 */
michael@0 2574 windows: {
michael@0 2575 "@@iterator": function*() {
michael@0 2576 for (let [window,] of gBuildWindows)
michael@0 2577 yield window;
michael@0 2578 }
michael@0 2579 },
michael@0 2580
michael@0 2581 /**
michael@0 2582 * Add a listener object that will get fired for various events regarding
michael@0 2583 * customization.
michael@0 2584 *
michael@0 2585 * @param aListener the listener object to add
michael@0 2586 *
michael@0 2587 * Not all event handler methods need to be defined.
michael@0 2588 * CustomizableUI will catch exceptions. Events are dispatched
michael@0 2589 * synchronously on the UI thread, so if you can delay any/some of your
michael@0 2590 * processing, that is advisable. The following event handlers are supported:
michael@0 2591 * - onWidgetAdded(aWidgetId, aArea, aPosition)
michael@0 2592 * Fired when a widget is added to an area. aWidgetId is the widget that
michael@0 2593 * was added, aArea the area it was added to, and aPosition the position
michael@0 2594 * in which it was added.
michael@0 2595 * - onWidgetMoved(aWidgetId, aArea, aOldPosition, aNewPosition)
michael@0 2596 * Fired when a widget is moved within its area. aWidgetId is the widget
michael@0 2597 * that was moved, aArea the area it was moved in, aOldPosition its old
michael@0 2598 * position, and aNewPosition its new position.
michael@0 2599 * - onWidgetRemoved(aWidgetId, aArea)
michael@0 2600 * Fired when a widget is removed from its area. aWidgetId is the widget
michael@0 2601 * that was removed, aArea the area it was removed from.
michael@0 2602 *
michael@0 2603 * - onWidgetBeforeDOMChange(aNode, aNextNode, aContainer, aIsRemoval)
michael@0 2604 * Fired *before* a widget's DOM node is acted upon by CustomizableUI
michael@0 2605 * (to add, move or remove it). aNode is the DOM node changed, aNextNode
michael@0 2606 * the DOM node (if any) before which a widget will be inserted,
michael@0 2607 * aContainer the *actual* DOM container (could be an overflow panel in
michael@0 2608 * case of an overflowable toolbar), and aWasRemoval is true iff the
michael@0 2609 * action about to happen is the removal of the DOM node.
michael@0 2610 * - onWidgetAfterDOMChange(aNode, aNextNode, aContainer, aWasRemoval)
michael@0 2611 * Like onWidgetBeforeDOMChange, but fired after the change to the DOM
michael@0 2612 * node of the widget.
michael@0 2613 *
michael@0 2614 * - onWidgetReset(aNode, aContainer)
michael@0 2615 * Fired after a reset to default placements moves a widget's node to a
michael@0 2616 * different location. aNode is the widget's node, aContainer is the
michael@0 2617 * area it was moved into (NB: it might already have been there and been
michael@0 2618 * moved to a different position!)
michael@0 2619 * - onWidgetUndoMove(aNode, aContainer)
michael@0 2620 * Fired after undoing a reset to default placements moves a widget's
michael@0 2621 * node to a different location. aNode is the widget's node, aContainer
michael@0 2622 * is the area it was moved into (NB: it might already have been there
michael@0 2623 * and been moved to a different position!)
michael@0 2624 * - onAreaReset(aArea, aContainer)
michael@0 2625 * Fired after a reset to default placements is complete on an area's
michael@0 2626 * DOM node. Note that this is fired for each DOM node. aArea is the area
michael@0 2627 * that was reset, aContainer the DOM node that was reset.
michael@0 2628 *
michael@0 2629 * - onWidgetCreated(aWidgetId)
michael@0 2630 * Fired when a widget with id aWidgetId has been created, but before it
michael@0 2631 * is added to any placements or any DOM nodes have been constructed.
michael@0 2632 * Only fired for API-based widgets.
michael@0 2633 * - onWidgetAfterCreation(aWidgetId, aArea)
michael@0 2634 * Fired after a widget with id aWidgetId has been created, and has been
michael@0 2635 * added to either its default area or the area in which it was placed
michael@0 2636 * previously. If the widget has no default area and/or it has never
michael@0 2637 * been placed anywhere, aArea may be null. Only fired for API-based
michael@0 2638 * widgets.
michael@0 2639 * - onWidgetDestroyed(aWidgetId)
michael@0 2640 * Fired when widgets are destroyed. aWidgetId is the widget that is
michael@0 2641 * being destroyed. Only fired for API-based widgets.
michael@0 2642 * - onWidgetInstanceRemoved(aWidgetId, aDocument)
michael@0 2643 * Fired when a window is unloaded and a widget's instance is destroyed
michael@0 2644 * because of this. Only fired for API-based widgets.
michael@0 2645 *
michael@0 2646 * - onWidgetDrag(aWidgetId, aArea)
michael@0 2647 * Fired both when and after customize mode drag handling system tries
michael@0 2648 * to determine the width and height of widget aWidgetId when dragged to a
michael@0 2649 * different area. aArea will be the area the item is dragged to, or
michael@0 2650 * undefined after the measurements have been done and the node has been
michael@0 2651 * moved back to its 'regular' area.
michael@0 2652 *
michael@0 2653 * - onCustomizeStart(aWindow)
michael@0 2654 * Fired when opening customize mode in aWindow.
michael@0 2655 * - onCustomizeEnd(aWindow)
michael@0 2656 * Fired when exiting customize mode in aWindow.
michael@0 2657 *
michael@0 2658 * - onWidgetOverflow(aNode, aContainer)
michael@0 2659 * Fired when a widget's DOM node is overflowing its container, a toolbar,
michael@0 2660 * and will be displayed in the overflow panel.
michael@0 2661 * - onWidgetUnderflow(aNode, aContainer)
michael@0 2662 * Fired when a widget's DOM node is *not* overflowing its container, a
michael@0 2663 * toolbar, anymore.
michael@0 2664 * - onWindowOpened(aWindow)
michael@0 2665 * Fired when a window has been opened that is managed by CustomizableUI,
michael@0 2666 * once all of the prerequisite setup has been done.
michael@0 2667 * - onWindowClosed(aWindow)
michael@0 2668 * Fired when a window that has been managed by CustomizableUI has been
michael@0 2669 * closed.
michael@0 2670 * - onAreaNodeRegistered(aArea, aContainer)
michael@0 2671 * Fired after an area node is first built when it is registered. This
michael@0 2672 * is often when the window has opened, but in the case of add-ons,
michael@0 2673 * could fire when the node has just been registered with CustomizableUI
michael@0 2674 * after an add-on update or disable/enable sequence.
michael@0 2675 * - onAreaNodeUnregistered(aArea, aContainer, aReason)
michael@0 2676 * Fired when an area node is explicitly unregistered by an API caller,
michael@0 2677 * or by a window closing. The aReason parameter indicates which of
michael@0 2678 * these is the case.
michael@0 2679 */
michael@0 2680 addListener: function(aListener) {
michael@0 2681 CustomizableUIInternal.addListener(aListener);
michael@0 2682 },
michael@0 2683 /**
michael@0 2684 * Remove a listener added with addListener
michael@0 2685 * @param aListener the listener object to remove
michael@0 2686 */
michael@0 2687 removeListener: function(aListener) {
michael@0 2688 CustomizableUIInternal.removeListener(aListener);
michael@0 2689 },
michael@0 2690
michael@0 2691 /**
michael@0 2692 * Register a customizable area with CustomizableUI.
michael@0 2693 * @param aName the name of the area to register. Can only contain
michael@0 2694 * alphanumeric characters, dashes (-) and underscores (_).
michael@0 2695 * @param aProps the properties of the area. The following properties are
michael@0 2696 * recognized:
michael@0 2697 * - type: the type of area. Either TYPE_TOOLBAR (default) or
michael@0 2698 * TYPE_MENU_PANEL;
michael@0 2699 * - anchor: for a menu panel or overflowable toolbar, the
michael@0 2700 * anchoring node for the panel.
michael@0 2701 * - legacy: set to true if you want customizableui to
michael@0 2702 * automatically migrate the currentset attribute
michael@0 2703 * - overflowable: set to true if your toolbar is overflowable.
michael@0 2704 * This requires an anchor, and only has an
michael@0 2705 * effect for toolbars.
michael@0 2706 * - defaultPlacements: an array of widget IDs making up the
michael@0 2707 * default contents of the area
michael@0 2708 * - defaultCollapsed: (INTERNAL ONLY) applies if the type is TYPE_TOOLBAR, specifies
michael@0 2709 * if toolbar is collapsed by default (default to true).
michael@0 2710 * Specify null to ensure that reset/inDefaultArea don't care
michael@0 2711 * about a toolbar's collapsed state
michael@0 2712 */
michael@0 2713 registerArea: function(aName, aProperties) {
michael@0 2714 CustomizableUIInternal.registerArea(aName, aProperties);
michael@0 2715 },
michael@0 2716 /**
michael@0 2717 * Register a concrete node for a registered area. This method is automatically
michael@0 2718 * called from any toolbar in the main browser window that has its
michael@0 2719 * "customizable" attribute set to true. There should normally be no need to
michael@0 2720 * call it yourself.
michael@0 2721 *
michael@0 2722 * Note that ideally, you should register your toolbar using registerArea
michael@0 2723 * before any of the toolbars have their XBL bindings constructed (which
michael@0 2724 * will happen when they're added to the DOM and are not hidden). If you
michael@0 2725 * don't, and your toolbar has a defaultset attribute, CustomizableUI will
michael@0 2726 * register it automatically. If your toolbar does not have a defaultset
michael@0 2727 * attribute, the node will be saved for processing when you call
michael@0 2728 * registerArea. Note that CustomizableUI won't restore state in the area,
michael@0 2729 * allow the user to customize it in customize mode, or otherwise deal
michael@0 2730 * with it, until the area has been registered.
michael@0 2731 */
michael@0 2732 registerToolbarNode: function(aToolbar, aExistingChildren) {
michael@0 2733 CustomizableUIInternal.registerToolbarNode(aToolbar, aExistingChildren);
michael@0 2734 },
michael@0 2735 /**
michael@0 2736 * Register the menu panel node. This method should not be called by anyone
michael@0 2737 * apart from the built-in PanelUI.
michael@0 2738 * @param aPanel the panel DOM node being registered.
michael@0 2739 */
michael@0 2740 registerMenuPanel: function(aPanel) {
michael@0 2741 CustomizableUIInternal.registerMenuPanel(aPanel);
michael@0 2742 },
michael@0 2743 /**
michael@0 2744 * Unregister a customizable area. The inverse of registerArea.
michael@0 2745 *
michael@0 2746 * Unregistering an area will remove all the (removable) widgets in the
michael@0 2747 * area, which will return to the panel, and destroy all other traces
michael@0 2748 * of the area within CustomizableUI. Note that this means the *contents*
michael@0 2749 * of the area's DOM nodes will be moved to the panel or removed, but
michael@0 2750 * the area's DOM nodes *themselves* will stay.
michael@0 2751 *
michael@0 2752 * Furthermore, by default the placements of the area will be kept in the
michael@0 2753 * saved state (!) and restored if you re-register the area at a later
michael@0 2754 * point. This is useful for e.g. add-ons that get disabled and then
michael@0 2755 * re-enabled (e.g. when they update).
michael@0 2756 *
michael@0 2757 * You can override this last behaviour (and destroy the placements
michael@0 2758 * information in the saved state) by passing true for aDestroyPlacements.
michael@0 2759 *
michael@0 2760 * @param aName the name of the area to unregister
michael@0 2761 * @param aDestroyPlacements whether to destroy the placements information
michael@0 2762 * for the area, too.
michael@0 2763 */
michael@0 2764 unregisterArea: function(aName, aDestroyPlacements) {
michael@0 2765 CustomizableUIInternal.unregisterArea(aName, aDestroyPlacements);
michael@0 2766 },
michael@0 2767 /**
michael@0 2768 * Add a widget to an area.
michael@0 2769 * If the area to which you try to add is not known to CustomizableUI,
michael@0 2770 * this will throw.
michael@0 2771 * If the area to which you try to add has not yet been restored from its
michael@0 2772 * legacy state, this will postpone the addition.
michael@0 2773 * If the area to which you try to add is the same as the area in which
michael@0 2774 * the widget is currently placed, this will do the same as
michael@0 2775 * moveWidgetWithinArea.
michael@0 2776 * If the widget cannot be removed from its original location, this will
michael@0 2777 * no-op.
michael@0 2778 *
michael@0 2779 * This will fire an onWidgetAdded notification,
michael@0 2780 * and an onWidgetBeforeDOMChange and onWidgetAfterDOMChange notification
michael@0 2781 * for each window CustomizableUI knows about.
michael@0 2782 *
michael@0 2783 * @param aWidgetId the ID of the widget to add
michael@0 2784 * @param aArea the ID of the area to add the widget to
michael@0 2785 * @param aPosition the position at which to add the widget. If you do not
michael@0 2786 * pass a position, the widget will be added to the end
michael@0 2787 * of the area.
michael@0 2788 */
michael@0 2789 addWidgetToArea: function(aWidgetId, aArea, aPosition) {
michael@0 2790 CustomizableUIInternal.addWidgetToArea(aWidgetId, aArea, aPosition);
michael@0 2791 },
michael@0 2792 /**
michael@0 2793 * Remove a widget from its area. If the widget cannot be removed from its
michael@0 2794 * area, or is not in any area, this will no-op. Otherwise, this will fire an
michael@0 2795 * onWidgetRemoved notification, and an onWidgetBeforeDOMChange and
michael@0 2796 * onWidgetAfterDOMChange notification for each window CustomizableUI knows
michael@0 2797 * about.
michael@0 2798 *
michael@0 2799 * @param aWidgetId the ID of the widget to remove
michael@0 2800 */
michael@0 2801 removeWidgetFromArea: function(aWidgetId) {
michael@0 2802 CustomizableUIInternal.removeWidgetFromArea(aWidgetId);
michael@0 2803 },
michael@0 2804 /**
michael@0 2805 * Move a widget within an area.
michael@0 2806 * If the widget is not in any area, this will no-op.
michael@0 2807 * If the widget is already at the indicated position, this will no-op.
michael@0 2808 *
michael@0 2809 * Otherwise, this will move the widget and fire an onWidgetMoved notification,
michael@0 2810 * and an onWidgetBeforeDOMChange and onWidgetAfterDOMChange notification for
michael@0 2811 * each window CustomizableUI knows about.
michael@0 2812 *
michael@0 2813 * @param aWidgetId the ID of the widget to move
michael@0 2814 * @param aPosition the position to move the widget to.
michael@0 2815 * Negative values or values greater than the number of
michael@0 2816 * widgets will be interpreted to mean moving the widget to
michael@0 2817 * respectively the first or last position.
michael@0 2818 */
michael@0 2819 moveWidgetWithinArea: function(aWidgetId, aPosition) {
michael@0 2820 CustomizableUIInternal.moveWidgetWithinArea(aWidgetId, aPosition);
michael@0 2821 },
michael@0 2822 /**
michael@0 2823 * Ensure a XUL-based widget created in a window after areas were
michael@0 2824 * initialized moves to its correct position.
michael@0 2825 * This is roughly equivalent to manually looking up the position and using
michael@0 2826 * insertItem in the old API, but a lot less work for consumers.
michael@0 2827 * Always prefer this over using toolbar.insertItem (which might no-op
michael@0 2828 * because it delegates to addWidgetToArea) or, worse, moving items in the
michael@0 2829 * DOM yourself.
michael@0 2830 *
michael@0 2831 * @param aWidgetId the ID of the widget that was just created
michael@0 2832 * @param aWindow the window in which you want to ensure it was added.
michael@0 2833 *
michael@0 2834 * NB: why is this API per-window, you wonder? Because if you need this,
michael@0 2835 * presumably you yourself need to create the widget in all the windows
michael@0 2836 * and need to loop through them anyway.
michael@0 2837 */
michael@0 2838 ensureWidgetPlacedInWindow: function(aWidgetId, aWindow) {
michael@0 2839 return CustomizableUIInternal.ensureWidgetPlacedInWindow(aWidgetId, aWindow);
michael@0 2840 },
michael@0 2841 /**
michael@0 2842 * Start a batch update of items.
michael@0 2843 * During a batch update, the customization state is not saved to the user's
michael@0 2844 * preferences file, in order to reduce (possibly sync) IO.
michael@0 2845 * Calls to begin/endBatchUpdate may be nested.
michael@0 2846 *
michael@0 2847 * Callers should ensure that NO MATTER WHAT they call endBatchUpdate once
michael@0 2848 * for each call to beginBatchUpdate, even if there are exceptions in the
michael@0 2849 * code in the batch update. Otherwise, for the duration of the
michael@0 2850 * Firefox session, customization state is never saved. Typically, you
michael@0 2851 * would do this using a try...finally block.
michael@0 2852 */
michael@0 2853 beginBatchUpdate: function() {
michael@0 2854 CustomizableUIInternal.beginBatchUpdate();
michael@0 2855 },
michael@0 2856 /**
michael@0 2857 * End a batch update. See the documentation for beginBatchUpdate above.
michael@0 2858 *
michael@0 2859 * State is not saved if we believe it is identical to the last known
michael@0 2860 * saved state. State is only ever saved when all batch updates have
michael@0 2861 * finished (ie there has been 1 endBatchUpdate call for each
michael@0 2862 * beginBatchUpdate call). If any of the endBatchUpdate calls pass
michael@0 2863 * aForceDirty=true, we will flush to the prefs file.
michael@0 2864 *
michael@0 2865 * @param aForceDirty force CustomizableUI to flush to the prefs file when
michael@0 2866 * all batch updates have finished.
michael@0 2867 */
michael@0 2868 endBatchUpdate: function(aForceDirty) {
michael@0 2869 CustomizableUIInternal.endBatchUpdate(aForceDirty);
michael@0 2870 },
michael@0 2871 /**
michael@0 2872 * Create a widget.
michael@0 2873 *
michael@0 2874 * To create a widget, you should pass an object with its desired
michael@0 2875 * properties. The following properties are supported:
michael@0 2876 *
michael@0 2877 * - id: the ID of the widget (required).
michael@0 2878 * - type: a string indicating the type of widget. Possible types
michael@0 2879 * are:
michael@0 2880 * 'button' - for simple button widgets (the default)
michael@0 2881 * 'view' - for buttons that open a panel or subview,
michael@0 2882 * depending on where they are placed.
michael@0 2883 * 'custom' - for fine-grained control over the creation
michael@0 2884 * of the widget.
michael@0 2885 * - viewId: Only useful for views (and required there): the id of the
michael@0 2886 * <panelview> that should be shown when clicking the widget.
michael@0 2887 * - onBuild(aDoc): Only useful for custom widgets (and required there); a
michael@0 2888 * function that will be invoked with the document in which
michael@0 2889 * to build a widget. Should return the DOM node that has
michael@0 2890 * been constructed.
michael@0 2891 * - onBeforeCreated(aDoc): Attached to all non-custom widgets; a function
michael@0 2892 * that will be invoked before the widget gets a DOM node
michael@0 2893 * constructed, passing the document in which that will happen.
michael@0 2894 * This is useful especially for 'view' type widgets that need
michael@0 2895 * to construct their views on the fly (e.g. from bootstrapped
michael@0 2896 * add-ons)
michael@0 2897 * - onCreated(aNode): Attached to all widgets; a function that will be invoked
michael@0 2898 * whenever the widget has a DOM node constructed, passing the
michael@0 2899 * constructed node as an argument.
michael@0 2900 * - onCommand(aEvt): Only useful for button widgets; a function that will be
michael@0 2901 * invoked when the user activates the button.
michael@0 2902 * - onClick(aEvt): Attached to all widgets; a function that will be invoked
michael@0 2903 * when the user clicks the widget.
michael@0 2904 * - onViewShowing(aEvt): Only useful for views; a function that will be
michael@0 2905 * invoked when a user shows your view.
michael@0 2906 * - onViewHiding(aEvt): Only useful for views; a function that will be
michael@0 2907 * invoked when a user hides your view.
michael@0 2908 * - tooltiptext: string to use for the tooltip of the widget
michael@0 2909 * - label: string to use for the label of the widget
michael@0 2910 * - removable: whether the widget is removable (optional, default: true)
michael@0 2911 * NB: if you specify false here, you must provide a
michael@0 2912 * defaultArea, too.
michael@0 2913 * - overflows: whether widget can overflow when in an overflowable
michael@0 2914 * toolbar (optional, default: true)
michael@0 2915 * - defaultArea: default area to add the widget to
michael@0 2916 * (optional, default: none; required if non-removable)
michael@0 2917 * - shortcutId: id of an element that has a shortcut for this widget
michael@0 2918 * (optional, default: null). This is only used to display
michael@0 2919 * the shortcut as part of the tooltip for builtin widgets
michael@0 2920 * (which have strings inside
michael@0 2921 * customizableWidgets.properties). If you're in an add-on,
michael@0 2922 * you should not set this property.
michael@0 2923 * - showInPrivateBrowsing: whether to show the widget in private browsing
michael@0 2924 * mode (optional, default: true)
michael@0 2925 *
michael@0 2926 * @param aProperties the specifications for the widget.
michael@0 2927 * @return a wrapper around the created widget (see getWidget)
michael@0 2928 */
michael@0 2929 createWidget: function(aProperties) {
michael@0 2930 return CustomizableUIInternal.wrapWidget(
michael@0 2931 CustomizableUIInternal.createWidget(aProperties)
michael@0 2932 );
michael@0 2933 },
michael@0 2934 /**
michael@0 2935 * Destroy a widget
michael@0 2936 *
michael@0 2937 * If the widget is part of the default placements in an area, this will
michael@0 2938 * remove it from there. It will also remove any DOM instances. However,
michael@0 2939 * it will keep the widget in the placements for whatever area it was
michael@0 2940 * in at the time. You can remove it from there yourself by calling
michael@0 2941 * CustomizableUI.removeWidgetFromArea(aWidgetId).
michael@0 2942 *
michael@0 2943 * @param aWidgetId the ID of the widget to destroy
michael@0 2944 */
michael@0 2945 destroyWidget: function(aWidgetId) {
michael@0 2946 CustomizableUIInternal.destroyWidget(aWidgetId);
michael@0 2947 },
michael@0 2948 /**
michael@0 2949 * Get a wrapper object with information about the widget.
michael@0 2950 * The object provides the following properties
michael@0 2951 * (all read-only unless otherwise indicated):
michael@0 2952 *
michael@0 2953 * - id: the widget's ID;
michael@0 2954 * - type: the type of widget (button, view, custom). For
michael@0 2955 * XUL-provided widgets, this is always 'custom';
michael@0 2956 * - provider: the provider type of the widget, id est one of
michael@0 2957 * PROVIDER_API or PROVIDER_XUL;
michael@0 2958 * - forWindow(w): a method to obtain a single window wrapper for a widget,
michael@0 2959 * in the window w passed as the only argument;
michael@0 2960 * - instances: an array of all instances (single window wrappers)
michael@0 2961 * of the widget. This array is NOT live;
michael@0 2962 * - areaType: the type of the widget's current area
michael@0 2963 * - isGroup: true; will be false for wrappers around single widget nodes;
michael@0 2964 * - source: for API-provided widgets, whether they are built-in to
michael@0 2965 * Firefox or add-on-provided;
michael@0 2966 * - disabled: for API-provided widgets, whether the widget is currently
michael@0 2967 * disabled. NB: this property is writable, and will toggle
michael@0 2968 * all the widgets' nodes' disabled states;
michael@0 2969 * - label: for API-provied widgets, the label of the widget;
michael@0 2970 * - tooltiptext: for API-provided widgets, the tooltip of the widget;
michael@0 2971 * - showInPrivateBrowsing: for API-provided widgets, whether the widget is
michael@0 2972 * visible in private browsing;
michael@0 2973 *
michael@0 2974 * Single window wrappers obtained through forWindow(someWindow) or from the
michael@0 2975 * instances array have the following properties
michael@0 2976 * (all read-only unless otherwise indicated):
michael@0 2977 *
michael@0 2978 * - id: the widget's ID;
michael@0 2979 * - type: the type of widget (button, view, custom). For
michael@0 2980 * XUL-provided widgets, this is always 'custom';
michael@0 2981 * - provider: the provider type of the widget, id est one of
michael@0 2982 * PROVIDER_API or PROVIDER_XUL;
michael@0 2983 * - node: reference to the corresponding DOM node;
michael@0 2984 * - anchor: the anchor on which to anchor panels opened from this
michael@0 2985 * node. This will point to the overflow chevron on
michael@0 2986 * overflowable toolbars if and only if your widget node
michael@0 2987 * is overflowed, to the anchor for the panel menu
michael@0 2988 * if your widget is inside the panel menu, and to the
michael@0 2989 * node itself in all other cases;
michael@0 2990 * - overflowed: boolean indicating whether the node is currently in the
michael@0 2991 * overflow panel of the toolbar;
michael@0 2992 * - isGroup: false; will be true for the group widget;
michael@0 2993 * - label: for API-provided widgets, convenience getter for the
michael@0 2994 * label attribute of the DOM node;
michael@0 2995 * - tooltiptext: for API-provided widgets, convenience getter for the
michael@0 2996 * tooltiptext attribute of the DOM node;
michael@0 2997 * - disabled: for API-provided widgets, convenience getter *and setter*
michael@0 2998 * for the disabled state of this single widget. Note that
michael@0 2999 * you may prefer to use the group wrapper's getter/setter
michael@0 3000 * instead.
michael@0 3001 *
michael@0 3002 * @param aWidgetId the ID of the widget whose information you need
michael@0 3003 * @return a wrapper around the widget as described above, or null if the
michael@0 3004 * widget is known not to exist (anymore). NB: non-null return
michael@0 3005 * is no guarantee the widget exists because we cannot know in
michael@0 3006 * advance if a XUL widget exists or not.
michael@0 3007 */
michael@0 3008 getWidget: function(aWidgetId) {
michael@0 3009 return CustomizableUIInternal.wrapWidget(aWidgetId);
michael@0 3010 },
michael@0 3011 /**
michael@0 3012 * Get an array of widget wrappers (see getWidget) for all the widgets
michael@0 3013 * which are currently not in any area (so which are in the palette).
michael@0 3014 *
michael@0 3015 * @param aWindowPalette the palette (and by extension, the window) in which
michael@0 3016 * CustomizableUI should look. This matters because of
michael@0 3017 * course XUL-provided widgets could be available in
michael@0 3018 * some windows but not others, and likewise
michael@0 3019 * API-provided widgets might not exist in a private
michael@0 3020 * window (because of the showInPrivateBrowsing
michael@0 3021 * property).
michael@0 3022 *
michael@0 3023 * @return an array of widget wrappers (see getWidget)
michael@0 3024 */
michael@0 3025 getUnusedWidgets: function(aWindowPalette) {
michael@0 3026 return CustomizableUIInternal.getUnusedWidgets(aWindowPalette).map(
michael@0 3027 CustomizableUIInternal.wrapWidget,
michael@0 3028 CustomizableUIInternal
michael@0 3029 );
michael@0 3030 },
michael@0 3031 /**
michael@0 3032 * Get an array of all the widget IDs placed in an area. This is roughly
michael@0 3033 * equivalent to fetching the currentset attribute and splitting by commas
michael@0 3034 * in the legacy APIs. Modifying the array will not affect CustomizableUI.
michael@0 3035 *
michael@0 3036 * @param aArea the ID of the area whose placements you want to obtain.
michael@0 3037 * @return an array containing the widget IDs that are in the area.
michael@0 3038 *
michael@0 3039 * NB: will throw if called too early (before placements have been fetched)
michael@0 3040 * or if the area is not currently known to CustomizableUI.
michael@0 3041 */
michael@0 3042 getWidgetIdsInArea: function(aArea) {
michael@0 3043 if (!gAreas.has(aArea)) {
michael@0 3044 throw new Error("Unknown customization area: " + aArea);
michael@0 3045 }
michael@0 3046 if (!gPlacements.has(aArea)) {
michael@0 3047 throw new Error("Area not yet restored");
michael@0 3048 }
michael@0 3049
michael@0 3050 // We need to clone this, as we don't want to let consumers muck with placements
michael@0 3051 return [...gPlacements.get(aArea)];
michael@0 3052 },
michael@0 3053 /**
michael@0 3054 * Get an array of widget wrappers for all the widgets in an area. This is
michael@0 3055 * the same as calling getWidgetIdsInArea and .map() ing the result through
michael@0 3056 * CustomizableUI.getWidget. Careful: this means that if there are IDs in there
michael@0 3057 * which don't have corresponding DOM nodes (like in the old-style currentset
michael@0 3058 * attribute), there might be nulls in this array, or items for which
michael@0 3059 * wrapper.forWindow(win) will return null.
michael@0 3060 *
michael@0 3061 * @param aArea the ID of the area whose widgets you want to obtain.
michael@0 3062 * @return an array of widget wrappers and/or null values for the widget IDs
michael@0 3063 * placed in an area.
michael@0 3064 *
michael@0 3065 * NB: will throw if called too early (before placements have been fetched)
michael@0 3066 * or if the area is not currently known to CustomizableUI.
michael@0 3067 */
michael@0 3068 getWidgetsInArea: function(aArea) {
michael@0 3069 return this.getWidgetIdsInArea(aArea).map(
michael@0 3070 CustomizableUIInternal.wrapWidget,
michael@0 3071 CustomizableUIInternal
michael@0 3072 );
michael@0 3073 },
michael@0 3074 /**
michael@0 3075 * Obtain an array of all the area IDs known to CustomizableUI.
michael@0 3076 * This array is created for you, so is modifiable without CustomizableUI
michael@0 3077 * being affected.
michael@0 3078 */
michael@0 3079 get areas() {
michael@0 3080 return [area for ([area, props] of gAreas)];
michael@0 3081 },
michael@0 3082 /**
michael@0 3083 * Check what kind of area (toolbar or menu panel) an area is. This is
michael@0 3084 * useful if you have a widget that needs to behave differently depending
michael@0 3085 * on its location. Note that widget wrappers have a convenience getter
michael@0 3086 * property (areaType) for this purpose.
michael@0 3087 *
michael@0 3088 * @param aArea the ID of the area whose type you want to know
michael@0 3089 * @return TYPE_TOOLBAR or TYPE_MENU_PANEL depending on the area, null if
michael@0 3090 * the area is unknown.
michael@0 3091 */
michael@0 3092 getAreaType: function(aArea) {
michael@0 3093 let area = gAreas.get(aArea);
michael@0 3094 return area ? area.get("type") : null;
michael@0 3095 },
michael@0 3096 /**
michael@0 3097 * Check if a toolbar is collapsed by default.
michael@0 3098 *
michael@0 3099 * @param aArea the ID of the area whose default-collapsed state you want to know.
michael@0 3100 * @return `true` or `false` depending on the area, null if the area is unknown,
michael@0 3101 * or its collapsed state cannot normally be controlled by the user
michael@0 3102 */
michael@0 3103 isToolbarDefaultCollapsed: function(aArea) {
michael@0 3104 let area = gAreas.get(aArea);
michael@0 3105 return area ? area.get("defaultCollapsed") : null;
michael@0 3106 },
michael@0 3107 /**
michael@0 3108 * Obtain the DOM node that is the customize target for an area in a
michael@0 3109 * specific window.
michael@0 3110 *
michael@0 3111 * Areas can have a customization target that does not correspond to the
michael@0 3112 * node itself. In particular, toolbars that have a customizationtarget
michael@0 3113 * attribute set will have their customization target set to that node.
michael@0 3114 * This means widgets will end up in the customization target, not in the
michael@0 3115 * DOM node with the ID that corresponds to the area ID. This is useful
michael@0 3116 * because it lets you have fixed content in a toolbar (e.g. the panel
michael@0 3117 * menu item in the navbar) and have all the customizable widgets use
michael@0 3118 * the customization target.
michael@0 3119 *
michael@0 3120 * Using this API yourself is discouraged; you should generally not need
michael@0 3121 * to be asking for the DOM container node used for a particular area.
michael@0 3122 * In particular, if you're wanting to check it in relation to a widget's
michael@0 3123 * node, your DOM node might not be a direct child of the customize target
michael@0 3124 * in a window if, for instance, the window is in customization mode, or if
michael@0 3125 * this is an overflowable toolbar and the widget has been overflowed.
michael@0 3126 *
michael@0 3127 * @param aArea the ID of the area whose customize target you want to have
michael@0 3128 * @param aWindow the window where you want to fetch the DOM node.
michael@0 3129 * @return the customize target DOM node for aArea in aWindow
michael@0 3130 */
michael@0 3131 getCustomizeTargetForArea: function(aArea, aWindow) {
michael@0 3132 return CustomizableUIInternal.getCustomizeTargetForArea(aArea, aWindow);
michael@0 3133 },
michael@0 3134 /**
michael@0 3135 * Reset the customization state back to its default.
michael@0 3136 *
michael@0 3137 * This is the nuclear option. You should never call this except if the user
michael@0 3138 * explicitly requests it. Firefox does this when the user clicks the
michael@0 3139 * "Restore Defaults" button in customize mode.
michael@0 3140 */
michael@0 3141 reset: function() {
michael@0 3142 CustomizableUIInternal.reset();
michael@0 3143 },
michael@0 3144
michael@0 3145 /**
michael@0 3146 * Undo the previous reset, can only be called immediately after a reset.
michael@0 3147 * @return a promise that will be resolved when the operation is complete.
michael@0 3148 */
michael@0 3149 undoReset: function() {
michael@0 3150 CustomizableUIInternal.undoReset();
michael@0 3151 },
michael@0 3152
michael@0 3153 /**
michael@0 3154 * Remove a custom toolbar added in a previous version of Firefox or using
michael@0 3155 * an add-on. NB: only works on the customizable toolbars generated by
michael@0 3156 * the toolbox itself. Intended for use from CustomizeMode, not by
michael@0 3157 * other consumers.
michael@0 3158 * @param aToolbarId the ID of the toolbar to remove
michael@0 3159 */
michael@0 3160 removeExtraToolbar: function(aToolbarId) {
michael@0 3161 CustomizableUIInternal.removeExtraToolbar(aToolbarId);
michael@0 3162 },
michael@0 3163
michael@0 3164 /**
michael@0 3165 * Can the last Restore Defaults operation be undone.
michael@0 3166 *
michael@0 3167 * @return A boolean stating whether an undo of the
michael@0 3168 * Restore Defaults can be performed.
michael@0 3169 */
michael@0 3170 get canUndoReset() {
michael@0 3171 return gUIStateBeforeReset.uiCustomizationState != null ||
michael@0 3172 gUIStateBeforeReset.drawInTitlebar != null;
michael@0 3173 },
michael@0 3174
michael@0 3175 /**
michael@0 3176 * Get the placement of a widget. This is by far the best way to obtain
michael@0 3177 * information about what the state of your widget is. The internals of
michael@0 3178 * this call are cheap (no DOM necessary) and you will know where the user
michael@0 3179 * has put your widget.
michael@0 3180 *
michael@0 3181 * @param aWidgetId the ID of the widget whose placement you want to know
michael@0 3182 * @return
michael@0 3183 * {
michael@0 3184 * area: "somearea", // The ID of the area where the widget is placed
michael@0 3185 * position: 42 // the index in the placements array corresponding to
michael@0 3186 * // your widget.
michael@0 3187 * }
michael@0 3188 *
michael@0 3189 * OR
michael@0 3190 *
michael@0 3191 * null // if the widget is not placed anywhere (ie in the palette)
michael@0 3192 */
michael@0 3193 getPlacementOfWidget: function(aWidgetId) {
michael@0 3194 return CustomizableUIInternal.getPlacementOfWidget(aWidgetId, true);
michael@0 3195 },
michael@0 3196 /**
michael@0 3197 * Check if a widget can be removed from the area it's in.
michael@0 3198 *
michael@0 3199 * Note that if you're wanting to move the widget somewhere, you should
michael@0 3200 * generally be checking canWidgetMoveToArea, because that will return
michael@0 3201 * true if the widget is already in the area where you want to move it (!).
michael@0 3202 *
michael@0 3203 * NB: oh, also, this method might lie if the widget in question is a
michael@0 3204 * XUL-provided widget and there are no windows open, because it
michael@0 3205 * can obviously not check anything in this case. It will return
michael@0 3206 * true. You will be able to move the widget elsewhere. However,
michael@0 3207 * once the user reopens a window, the widget will move back to its
michael@0 3208 * 'proper' area automagically.
michael@0 3209 *
michael@0 3210 * @param aWidgetId a widget ID or DOM node to check
michael@0 3211 * @return true if the widget can be removed from its area,
michael@0 3212 * false otherwise.
michael@0 3213 */
michael@0 3214 isWidgetRemovable: function(aWidgetId) {
michael@0 3215 return CustomizableUIInternal.isWidgetRemovable(aWidgetId);
michael@0 3216 },
michael@0 3217 /**
michael@0 3218 * Check if a widget can be moved to a particular area. Like
michael@0 3219 * isWidgetRemovable but better, because it'll return true if the widget
michael@0 3220 * is already in the right area.
michael@0 3221 *
michael@0 3222 * @param aWidgetId the widget ID or DOM node you want to move somewhere
michael@0 3223 * @param aArea the area ID you want to move it to.
michael@0 3224 * @return true if this is possible, false if it is not. The same caveats as
michael@0 3225 * for isWidgetRemovable apply, however, if no windows are open.
michael@0 3226 */
michael@0 3227 canWidgetMoveToArea: function(aWidgetId, aArea) {
michael@0 3228 return CustomizableUIInternal.canWidgetMoveToArea(aWidgetId, aArea);
michael@0 3229 },
michael@0 3230 /**
michael@0 3231 * Whether we're in a default state. Note that non-removable non-default
michael@0 3232 * widgets and non-existing widgets are not taken into account in determining
michael@0 3233 * whether we're in the default state.
michael@0 3234 *
michael@0 3235 * NB: this is a property with a getter. The getter is NOT cheap, because
michael@0 3236 * it does smart things with non-removable non-default items, non-existent
michael@0 3237 * items, and so forth. Please don't call unless necessary.
michael@0 3238 */
michael@0 3239 get inDefaultState() {
michael@0 3240 return CustomizableUIInternal.inDefaultState;
michael@0 3241 },
michael@0 3242
michael@0 3243 /**
michael@0 3244 * Set a toolbar's visibility state in all windows.
michael@0 3245 * @param aToolbarId the toolbar whose visibility should be adjusted
michael@0 3246 * @param aIsVisible whether the toolbar should be visible
michael@0 3247 */
michael@0 3248 setToolbarVisibility: function(aToolbarId, aIsVisible) {
michael@0 3249 CustomizableUIInternal.setToolbarVisibility(aToolbarId, aIsVisible);
michael@0 3250 },
michael@0 3251
michael@0 3252 /**
michael@0 3253 * Get a localized property off a (widget?) object.
michael@0 3254 *
michael@0 3255 * NB: this is unlikely to be useful unless you're in Firefox code, because
michael@0 3256 * this code uses the builtin widget stringbundle, and can't be told
michael@0 3257 * to use add-on-provided strings. It's mainly here as convenience for
michael@0 3258 * custom builtin widgets that build their own DOM but use the same
michael@0 3259 * stringbundle as the other builtin widgets.
michael@0 3260 *
michael@0 3261 * @param aWidget the object whose property we should use to fetch a
michael@0 3262 * localizable string;
michael@0 3263 * @param aProp the property on the object to use for the fetching;
michael@0 3264 * @param aFormatArgs (optional) any extra arguments to use for a formatted
michael@0 3265 * string;
michael@0 3266 * @param aDef (optional) the default to return if we don't find the
michael@0 3267 * string in the stringbundle;
michael@0 3268 *
michael@0 3269 * @return the localized string, or aDef if the string isn't in the bundle.
michael@0 3270 * If no default is provided,
michael@0 3271 * if aProp exists on aWidget, we'll return that,
michael@0 3272 * otherwise we'll return the empty string
michael@0 3273 *
michael@0 3274 */
michael@0 3275 getLocalizedProperty: function(aWidget, aProp, aFormatArgs, aDef) {
michael@0 3276 return CustomizableUIInternal.getLocalizedProperty(aWidget, aProp,
michael@0 3277 aFormatArgs, aDef);
michael@0 3278 },
michael@0 3279 /**
michael@0 3280 * Given a node, walk up to the first panel in its ancestor chain, and
michael@0 3281 * close it.
michael@0 3282 *
michael@0 3283 * @param aNode a node whose panel should be closed;
michael@0 3284 */
michael@0 3285 hidePanelForNode: function(aNode) {
michael@0 3286 CustomizableUIInternal.hidePanelForNode(aNode);
michael@0 3287 },
michael@0 3288 /**
michael@0 3289 * Check if a widget is a "special" widget: a spring, spacer or separator.
michael@0 3290 *
michael@0 3291 * @param aWidgetId the widget ID to check.
michael@0 3292 * @return true if the widget is 'special', false otherwise.
michael@0 3293 */
michael@0 3294 isSpecialWidget: function(aWidgetId) {
michael@0 3295 return CustomizableUIInternal.isSpecialWidget(aWidgetId);
michael@0 3296 },
michael@0 3297 /**
michael@0 3298 * Add listeners to a panel that will close it. For use from the menu panel
michael@0 3299 * and overflowable toolbar implementations, unlikely to be useful for
michael@0 3300 * consumers.
michael@0 3301 *
michael@0 3302 * @param aPanel the panel to which listeners should be attached.
michael@0 3303 */
michael@0 3304 addPanelCloseListeners: function(aPanel) {
michael@0 3305 CustomizableUIInternal.addPanelCloseListeners(aPanel);
michael@0 3306 },
michael@0 3307 /**
michael@0 3308 * Remove close listeners that have been added to a panel with
michael@0 3309 * addPanelCloseListeners. For use from the menu panel and overflowable
michael@0 3310 * toolbar implementations, unlikely to be useful for consumers.
michael@0 3311 *
michael@0 3312 * @param aPanel the panel from which listeners should be removed.
michael@0 3313 */
michael@0 3314 removePanelCloseListeners: function(aPanel) {
michael@0 3315 CustomizableUIInternal.removePanelCloseListeners(aPanel);
michael@0 3316 },
michael@0 3317 /**
michael@0 3318 * Notify listeners a widget is about to be dragged to an area. For use from
michael@0 3319 * Customize Mode only, do not use otherwise.
michael@0 3320 *
michael@0 3321 * @param aWidgetId the ID of the widget that is being dragged to an area.
michael@0 3322 * @param aArea the ID of the area to which the widget is being dragged.
michael@0 3323 */
michael@0 3324 onWidgetDrag: function(aWidgetId, aArea) {
michael@0 3325 CustomizableUIInternal.notifyListeners("onWidgetDrag", aWidgetId, aArea);
michael@0 3326 },
michael@0 3327 /**
michael@0 3328 * Notify listeners that a window is entering customize mode. For use from
michael@0 3329 * Customize Mode only, do not use otherwise.
michael@0 3330 * @param aWindow the window entering customize mode
michael@0 3331 */
michael@0 3332 notifyStartCustomizing: function(aWindow) {
michael@0 3333 CustomizableUIInternal.notifyListeners("onCustomizeStart", aWindow);
michael@0 3334 },
michael@0 3335 /**
michael@0 3336 * Notify listeners that a window is exiting customize mode. For use from
michael@0 3337 * Customize Mode only, do not use otherwise.
michael@0 3338 * @param aWindow the window exiting customize mode
michael@0 3339 */
michael@0 3340 notifyEndCustomizing: function(aWindow) {
michael@0 3341 CustomizableUIInternal.notifyListeners("onCustomizeEnd", aWindow);
michael@0 3342 },
michael@0 3343
michael@0 3344 /**
michael@0 3345 * Notify toolbox(es) of a particular event. If you don't pass aWindow,
michael@0 3346 * all toolboxes will be notified. For use from Customize Mode only,
michael@0 3347 * do not use otherwise.
michael@0 3348 * @param aEvent the name of the event to send.
michael@0 3349 * @param aDetails optional, the details of the event.
michael@0 3350 * @param aWindow optional, the window in which to send the event.
michael@0 3351 */
michael@0 3352 dispatchToolboxEvent: function(aEvent, aDetails={}, aWindow=null) {
michael@0 3353 CustomizableUIInternal.dispatchToolboxEvent(aEvent, aDetails, aWindow);
michael@0 3354 },
michael@0 3355
michael@0 3356 /**
michael@0 3357 * Check whether an area is overflowable.
michael@0 3358 *
michael@0 3359 * @param aAreaId the ID of an area to check for overflowable-ness
michael@0 3360 * @return true if the area is overflowable, false otherwise.
michael@0 3361 */
michael@0 3362 isAreaOverflowable: function(aAreaId) {
michael@0 3363 let area = gAreas.get(aAreaId);
michael@0 3364 return area ? area.get("type") == this.TYPE_TOOLBAR && area.get("overflowable")
michael@0 3365 : false;
michael@0 3366 },
michael@0 3367 /**
michael@0 3368 * Obtain a string indicating the place of an element. This is intended
michael@0 3369 * for use from customize mode; You should generally use getPlacementOfWidget
michael@0 3370 * instead, which is cheaper because it does not use the DOM.
michael@0 3371 *
michael@0 3372 * @param aElement the DOM node whose place we need to check
michael@0 3373 * @return "toolbar" if the node is in a toolbar, "panel" if it is in the
michael@0 3374 * menu panel, "palette" if it is in the (visible!) customization
michael@0 3375 * palette, undefined otherwise.
michael@0 3376 */
michael@0 3377 getPlaceForItem: function(aElement) {
michael@0 3378 let place;
michael@0 3379 let node = aElement;
michael@0 3380 while (node && !place) {
michael@0 3381 if (node.localName == "toolbar")
michael@0 3382 place = "toolbar";
michael@0 3383 else if (node.id == CustomizableUI.AREA_PANEL)
michael@0 3384 place = "panel";
michael@0 3385 else if (node.id == "customization-palette")
michael@0 3386 place = "palette";
michael@0 3387
michael@0 3388 node = node.parentNode;
michael@0 3389 }
michael@0 3390 return place;
michael@0 3391 },
michael@0 3392
michael@0 3393 /**
michael@0 3394 * Check if a toolbar is builtin or not.
michael@0 3395 * @param aToolbarId the ID of the toolbar you want to check
michael@0 3396 */
michael@0 3397 isBuiltinToolbar: function(aToolbarId) {
michael@0 3398 return CustomizableUIInternal._builtinToolbars.has(aToolbarId);
michael@0 3399 },
michael@0 3400 };
michael@0 3401 Object.freeze(this.CustomizableUI);
michael@0 3402 Object.freeze(this.CustomizableUI.windows);
michael@0 3403
michael@0 3404 /**
michael@0 3405 * All external consumers of widgets are really interacting with these wrappers
michael@0 3406 * which provide a common interface.
michael@0 3407 */
michael@0 3408
michael@0 3409 /**
michael@0 3410 * WidgetGroupWrapper is the common interface for interacting with an entire
michael@0 3411 * widget group - AKA, all instances of a widget across a series of windows.
michael@0 3412 * This particular wrapper is only used for widgets created via the provider
michael@0 3413 * API.
michael@0 3414 */
michael@0 3415 function WidgetGroupWrapper(aWidget) {
michael@0 3416 this.isGroup = true;
michael@0 3417
michael@0 3418 const kBareProps = ["id", "source", "type", "disabled", "label", "tooltiptext",
michael@0 3419 "showInPrivateBrowsing"];
michael@0 3420 for (let prop of kBareProps) {
michael@0 3421 let propertyName = prop;
michael@0 3422 this.__defineGetter__(propertyName, function() aWidget[propertyName]);
michael@0 3423 }
michael@0 3424
michael@0 3425 this.__defineGetter__("provider", function() CustomizableUI.PROVIDER_API);
michael@0 3426
michael@0 3427 this.__defineSetter__("disabled", function(aValue) {
michael@0 3428 aValue = !!aValue;
michael@0 3429 aWidget.disabled = aValue;
michael@0 3430 for (let [,instance] of aWidget.instances) {
michael@0 3431 instance.disabled = aValue;
michael@0 3432 }
michael@0 3433 });
michael@0 3434
michael@0 3435 this.forWindow = function WidgetGroupWrapper_forWindow(aWindow) {
michael@0 3436 let wrapperMap;
michael@0 3437 if (!gSingleWrapperCache.has(aWindow)) {
michael@0 3438 wrapperMap = new Map();
michael@0 3439 gSingleWrapperCache.set(aWindow, wrapperMap);
michael@0 3440 } else {
michael@0 3441 wrapperMap = gSingleWrapperCache.get(aWindow);
michael@0 3442 }
michael@0 3443 if (wrapperMap.has(aWidget.id)) {
michael@0 3444 return wrapperMap.get(aWidget.id);
michael@0 3445 }
michael@0 3446
michael@0 3447 let instance = aWidget.instances.get(aWindow.document);
michael@0 3448 if (!instance &&
michael@0 3449 (aWidget.showInPrivateBrowsing || !PrivateBrowsingUtils.isWindowPrivate(aWindow))) {
michael@0 3450 instance = CustomizableUIInternal.buildWidget(aWindow.document,
michael@0 3451 aWidget);
michael@0 3452 }
michael@0 3453
michael@0 3454 let wrapper = new WidgetSingleWrapper(aWidget, instance);
michael@0 3455 wrapperMap.set(aWidget.id, wrapper);
michael@0 3456 return wrapper;
michael@0 3457 };
michael@0 3458
michael@0 3459 this.__defineGetter__("instances", function() {
michael@0 3460 // Can't use gBuildWindows here because some areas load lazily:
michael@0 3461 let placement = CustomizableUIInternal.getPlacementOfWidget(aWidget.id);
michael@0 3462 if (!placement) {
michael@0 3463 return [];
michael@0 3464 }
michael@0 3465 let area = placement.area;
michael@0 3466 let buildAreas = gBuildAreas.get(area);
michael@0 3467 if (!buildAreas) {
michael@0 3468 return [];
michael@0 3469 }
michael@0 3470 return [this.forWindow(node.ownerDocument.defaultView) for (node of buildAreas)];
michael@0 3471 });
michael@0 3472
michael@0 3473 this.__defineGetter__("areaType", function() {
michael@0 3474 let areaProps = gAreas.get(aWidget.currentArea);
michael@0 3475 return areaProps && areaProps.get("type");
michael@0 3476 });
michael@0 3477
michael@0 3478 Object.freeze(this);
michael@0 3479 }
michael@0 3480
michael@0 3481 /**
michael@0 3482 * A WidgetSingleWrapper is a wrapper around a single instance of a widget in
michael@0 3483 * a particular window.
michael@0 3484 */
michael@0 3485 function WidgetSingleWrapper(aWidget, aNode) {
michael@0 3486 this.isGroup = false;
michael@0 3487
michael@0 3488 this.node = aNode;
michael@0 3489 this.provider = CustomizableUI.PROVIDER_API;
michael@0 3490
michael@0 3491 const kGlobalProps = ["id", "type"];
michael@0 3492 for (let prop of kGlobalProps) {
michael@0 3493 this[prop] = aWidget[prop];
michael@0 3494 }
michael@0 3495
michael@0 3496 const kNodeProps = ["label", "tooltiptext"];
michael@0 3497 for (let prop of kNodeProps) {
michael@0 3498 let propertyName = prop;
michael@0 3499 // Look at the node for these, instead of the widget data, to ensure the
michael@0 3500 // wrapper always reflects this live instance.
michael@0 3501 this.__defineGetter__(propertyName,
michael@0 3502 function() aNode.getAttribute(propertyName));
michael@0 3503 }
michael@0 3504
michael@0 3505 this.__defineGetter__("disabled", function() aNode.disabled);
michael@0 3506 this.__defineSetter__("disabled", function(aValue) {
michael@0 3507 aNode.disabled = !!aValue;
michael@0 3508 });
michael@0 3509
michael@0 3510 this.__defineGetter__("anchor", function() {
michael@0 3511 let anchorId;
michael@0 3512 // First check for an anchor for the area:
michael@0 3513 let placement = CustomizableUIInternal.getPlacementOfWidget(aWidget.id);
michael@0 3514 if (placement) {
michael@0 3515 anchorId = gAreas.get(placement.area).get("anchor");
michael@0 3516 }
michael@0 3517 if (!anchorId) {
michael@0 3518 anchorId = aNode.getAttribute("cui-anchorid");
michael@0 3519 }
michael@0 3520
michael@0 3521 return anchorId ? aNode.ownerDocument.getElementById(anchorId)
michael@0 3522 : aNode;
michael@0 3523 });
michael@0 3524
michael@0 3525 this.__defineGetter__("overflowed", function() {
michael@0 3526 return aNode.getAttribute("overflowedItem") == "true";
michael@0 3527 });
michael@0 3528
michael@0 3529 Object.freeze(this);
michael@0 3530 }
michael@0 3531
michael@0 3532 /**
michael@0 3533 * XULWidgetGroupWrapper is the common interface for interacting with an entire
michael@0 3534 * widget group - AKA, all instances of a widget across a series of windows.
michael@0 3535 * This particular wrapper is only used for widgets created via the old-school
michael@0 3536 * XUL method (overlays, or programmatically injecting toolbaritems, or other
michael@0 3537 * such things).
michael@0 3538 */
michael@0 3539 //XXXunf Going to need to hook this up to some events to keep it all live.
michael@0 3540 function XULWidgetGroupWrapper(aWidgetId) {
michael@0 3541 this.isGroup = true;
michael@0 3542 this.id = aWidgetId;
michael@0 3543 this.type = "custom";
michael@0 3544 this.provider = CustomizableUI.PROVIDER_XUL;
michael@0 3545
michael@0 3546 this.forWindow = function XULWidgetGroupWrapper_forWindow(aWindow) {
michael@0 3547 let wrapperMap;
michael@0 3548 if (!gSingleWrapperCache.has(aWindow)) {
michael@0 3549 wrapperMap = new Map();
michael@0 3550 gSingleWrapperCache.set(aWindow, wrapperMap);
michael@0 3551 } else {
michael@0 3552 wrapperMap = gSingleWrapperCache.get(aWindow);
michael@0 3553 }
michael@0 3554 if (wrapperMap.has(aWidgetId)) {
michael@0 3555 return wrapperMap.get(aWidgetId);
michael@0 3556 }
michael@0 3557
michael@0 3558 let instance = aWindow.document.getElementById(aWidgetId);
michael@0 3559 if (!instance) {
michael@0 3560 // Toolbar palettes aren't part of the document, so elements in there
michael@0 3561 // won't be found via document.getElementById().
michael@0 3562 instance = aWindow.gNavToolbox.palette.getElementsByAttribute("id", aWidgetId)[0];
michael@0 3563 }
michael@0 3564
michael@0 3565 let wrapper = new XULWidgetSingleWrapper(aWidgetId, instance, aWindow.document);
michael@0 3566 wrapperMap.set(aWidgetId, wrapper);
michael@0 3567 return wrapper;
michael@0 3568 };
michael@0 3569
michael@0 3570 this.__defineGetter__("areaType", function() {
michael@0 3571 let placement = CustomizableUIInternal.getPlacementOfWidget(aWidgetId);
michael@0 3572 if (!placement) {
michael@0 3573 return null;
michael@0 3574 }
michael@0 3575
michael@0 3576 let areaProps = gAreas.get(placement.area);
michael@0 3577 return areaProps && areaProps.get("type");
michael@0 3578 });
michael@0 3579
michael@0 3580 this.__defineGetter__("instances", function() {
michael@0 3581 return [this.forWindow(win) for ([win,] of gBuildWindows)];
michael@0 3582 });
michael@0 3583
michael@0 3584 Object.freeze(this);
michael@0 3585 }
michael@0 3586
michael@0 3587 /**
michael@0 3588 * A XULWidgetSingleWrapper is a wrapper around a single instance of a XUL
michael@0 3589 * widget in a particular window.
michael@0 3590 */
michael@0 3591 function XULWidgetSingleWrapper(aWidgetId, aNode, aDocument) {
michael@0 3592 this.isGroup = false;
michael@0 3593
michael@0 3594 this.id = aWidgetId;
michael@0 3595 this.type = "custom";
michael@0 3596 this.provider = CustomizableUI.PROVIDER_XUL;
michael@0 3597
michael@0 3598 let weakDoc = Cu.getWeakReference(aDocument);
michael@0 3599 // If we keep a strong ref, the weak ref will never die, so null it out:
michael@0 3600 aDocument = null;
michael@0 3601
michael@0 3602 this.__defineGetter__("node", function() {
michael@0 3603 // If we've set this to null (further down), we're sure there's nothing to
michael@0 3604 // be gotten here, so bail out early:
michael@0 3605 if (!weakDoc) {
michael@0 3606 return null;
michael@0 3607 }
michael@0 3608 if (aNode) {
michael@0 3609 // Return the last known node if it's still in the DOM...
michael@0 3610 if (aNode.ownerDocument.contains(aNode)) {
michael@0 3611 return aNode;
michael@0 3612 }
michael@0 3613 // ... or the toolbox
michael@0 3614 let toolbox = aNode.ownerDocument.defaultView.gNavToolbox;
michael@0 3615 if (toolbox && toolbox.palette && aNode.parentNode == toolbox.palette) {
michael@0 3616 return aNode;
michael@0 3617 }
michael@0 3618 // If it isn't, clear the cached value and fall through to the "slow" case:
michael@0 3619 aNode = null;
michael@0 3620 }
michael@0 3621
michael@0 3622 let doc = weakDoc.get();
michael@0 3623 if (doc) {
michael@0 3624 // Store locally so we can cache the result:
michael@0 3625 aNode = CustomizableUIInternal.findWidgetInWindow(aWidgetId, doc.defaultView);
michael@0 3626 return aNode;
michael@0 3627 }
michael@0 3628 // The weakref to the document is dead, we're done here forever more:
michael@0 3629 weakDoc = null;
michael@0 3630 return null;
michael@0 3631 });
michael@0 3632
michael@0 3633 this.__defineGetter__("anchor", function() {
michael@0 3634 let anchorId;
michael@0 3635 // First check for an anchor for the area:
michael@0 3636 let placement = CustomizableUIInternal.getPlacementOfWidget(aWidgetId);
michael@0 3637 if (placement) {
michael@0 3638 anchorId = gAreas.get(placement.area).get("anchor");
michael@0 3639 }
michael@0 3640
michael@0 3641 let node = this.node;
michael@0 3642 if (!anchorId && node) {
michael@0 3643 anchorId = node.getAttribute("cui-anchorid");
michael@0 3644 }
michael@0 3645
michael@0 3646 return (anchorId && node) ? node.ownerDocument.getElementById(anchorId) : node;
michael@0 3647 });
michael@0 3648
michael@0 3649 this.__defineGetter__("overflowed", function() {
michael@0 3650 let node = this.node;
michael@0 3651 if (!node) {
michael@0 3652 return false;
michael@0 3653 }
michael@0 3654 return node.getAttribute("overflowedItem") == "true";
michael@0 3655 });
michael@0 3656
michael@0 3657 Object.freeze(this);
michael@0 3658 }
michael@0 3659
michael@0 3660 const LAZY_RESIZE_INTERVAL_MS = 200;
michael@0 3661
michael@0 3662 function OverflowableToolbar(aToolbarNode) {
michael@0 3663 this._toolbar = aToolbarNode;
michael@0 3664 this._collapsed = new Map();
michael@0 3665 this._enabled = true;
michael@0 3666
michael@0 3667 this._toolbar.setAttribute("overflowable", "true");
michael@0 3668 let doc = this._toolbar.ownerDocument;
michael@0 3669 this._target = this._toolbar.customizationTarget;
michael@0 3670 this._list = doc.getElementById(this._toolbar.getAttribute("overflowtarget"));
michael@0 3671 this._list.toolbox = this._toolbar.toolbox;
michael@0 3672 this._list.customizationTarget = this._list;
michael@0 3673
michael@0 3674 let window = this._toolbar.ownerDocument.defaultView;
michael@0 3675 if (window.gBrowserInit.delayedStartupFinished) {
michael@0 3676 this.init();
michael@0 3677 } else {
michael@0 3678 Services.obs.addObserver(this, "browser-delayed-startup-finished", false);
michael@0 3679 }
michael@0 3680 }
michael@0 3681
michael@0 3682 OverflowableToolbar.prototype = {
michael@0 3683 initialized: false,
michael@0 3684 _forceOnOverflow: false,
michael@0 3685
michael@0 3686 observe: function(aSubject, aTopic, aData) {
michael@0 3687 if (aTopic == "browser-delayed-startup-finished" &&
michael@0 3688 aSubject == this._toolbar.ownerDocument.defaultView) {
michael@0 3689 Services.obs.removeObserver(this, "browser-delayed-startup-finished");
michael@0 3690 this.init();
michael@0 3691 }
michael@0 3692 },
michael@0 3693
michael@0 3694 init: function() {
michael@0 3695 let doc = this._toolbar.ownerDocument;
michael@0 3696 let window = doc.defaultView;
michael@0 3697 window.addEventListener("resize", this);
michael@0 3698 window.gNavToolbox.addEventListener("customizationstarting", this);
michael@0 3699 window.gNavToolbox.addEventListener("aftercustomization", this);
michael@0 3700
michael@0 3701 let chevronId = this._toolbar.getAttribute("overflowbutton");
michael@0 3702 this._chevron = doc.getElementById(chevronId);
michael@0 3703 this._chevron.addEventListener("command", this);
michael@0 3704
michael@0 3705 let panelId = this._toolbar.getAttribute("overflowpanel");
michael@0 3706 this._panel = doc.getElementById(panelId);
michael@0 3707 this._panel.addEventListener("popuphiding", this);
michael@0 3708 CustomizableUIInternal.addPanelCloseListeners(this._panel);
michael@0 3709
michael@0 3710 CustomizableUI.addListener(this);
michael@0 3711
michael@0 3712 // The 'overflow' event may have been fired before init was called.
michael@0 3713 if (this._toolbar.overflowedDuringConstruction) {
michael@0 3714 this.onOverflow(this._toolbar.overflowedDuringConstruction);
michael@0 3715 this._toolbar.overflowedDuringConstruction = null;
michael@0 3716 }
michael@0 3717
michael@0 3718 this.initialized = true;
michael@0 3719 },
michael@0 3720
michael@0 3721 uninit: function() {
michael@0 3722 this._toolbar.removeEventListener("overflow", this._toolbar);
michael@0 3723 this._toolbar.removeEventListener("underflow", this._toolbar);
michael@0 3724 this._toolbar.removeAttribute("overflowable");
michael@0 3725
michael@0 3726 if (!this.initialized) {
michael@0 3727 Services.obs.removeObserver(this, "browser-delayed-startup-finished");
michael@0 3728 return;
michael@0 3729 }
michael@0 3730
michael@0 3731 this._disable();
michael@0 3732
michael@0 3733 let window = this._toolbar.ownerDocument.defaultView;
michael@0 3734 window.removeEventListener("resize", this);
michael@0 3735 window.gNavToolbox.removeEventListener("customizationstarting", this);
michael@0 3736 window.gNavToolbox.removeEventListener("aftercustomization", this);
michael@0 3737 this._chevron.removeEventListener("command", this);
michael@0 3738 this._panel.removeEventListener("popuphiding", this);
michael@0 3739 CustomizableUI.removeListener(this);
michael@0 3740 CustomizableUIInternal.removePanelCloseListeners(this._panel);
michael@0 3741 },
michael@0 3742
michael@0 3743 handleEvent: function(aEvent) {
michael@0 3744 switch(aEvent.type) {
michael@0 3745 case "resize":
michael@0 3746 this._onResize(aEvent);
michael@0 3747 break;
michael@0 3748 case "command":
michael@0 3749 if (aEvent.target == this._chevron) {
michael@0 3750 this._onClickChevron(aEvent);
michael@0 3751 } else {
michael@0 3752 this._panel.hidePopup();
michael@0 3753 }
michael@0 3754 break;
michael@0 3755 case "popuphiding":
michael@0 3756 this._onPanelHiding(aEvent);
michael@0 3757 break;
michael@0 3758 case "customizationstarting":
michael@0 3759 this._disable();
michael@0 3760 break;
michael@0 3761 case "aftercustomization":
michael@0 3762 this._enable();
michael@0 3763 break;
michael@0 3764 }
michael@0 3765 },
michael@0 3766
michael@0 3767 show: function() {
michael@0 3768 let deferred = Promise.defer();
michael@0 3769 if (this._panel.state == "open") {
michael@0 3770 deferred.resolve();
michael@0 3771 return deferred.promise;
michael@0 3772 }
michael@0 3773 let doc = this._panel.ownerDocument;
michael@0 3774 this._panel.hidden = false;
michael@0 3775 let contextMenu = doc.getElementById(this._panel.getAttribute("context"));
michael@0 3776 gELS.addSystemEventListener(contextMenu, 'command', this, true);
michael@0 3777 let anchor = doc.getAnonymousElementByAttribute(this._chevron, "class", "toolbarbutton-icon");
michael@0 3778 this._panel.openPopup(anchor || this._chevron);
michael@0 3779 this._chevron.open = true;
michael@0 3780
michael@0 3781 this._panel.addEventListener("popupshown", function onPopupShown() {
michael@0 3782 this.removeEventListener("popupshown", onPopupShown);
michael@0 3783 deferred.resolve();
michael@0 3784 });
michael@0 3785
michael@0 3786 return deferred.promise;
michael@0 3787 },
michael@0 3788
michael@0 3789 _onClickChevron: function(aEvent) {
michael@0 3790 if (this._chevron.open) {
michael@0 3791 this._panel.hidePopup();
michael@0 3792 this._chevron.open = false;
michael@0 3793 } else {
michael@0 3794 this.show();
michael@0 3795 }
michael@0 3796 },
michael@0 3797
michael@0 3798 _onPanelHiding: function(aEvent) {
michael@0 3799 this._chevron.open = false;
michael@0 3800 let doc = aEvent.target.ownerDocument;
michael@0 3801 let contextMenu = doc.getElementById(this._panel.getAttribute("context"));
michael@0 3802 gELS.removeSystemEventListener(contextMenu, 'command', this, true);
michael@0 3803 },
michael@0 3804
michael@0 3805 onOverflow: function(aEvent) {
michael@0 3806 if (!this._enabled ||
michael@0 3807 (aEvent && aEvent.target != this._toolbar.customizationTarget))
michael@0 3808 return;
michael@0 3809
michael@0 3810 let child = this._target.lastChild;
michael@0 3811
michael@0 3812 while (child && this._target.scrollLeftMax > 0) {
michael@0 3813 let prevChild = child.previousSibling;
michael@0 3814
michael@0 3815 if (child.getAttribute("overflows") != "false") {
michael@0 3816 this._collapsed.set(child.id, this._target.clientWidth);
michael@0 3817 child.setAttribute("overflowedItem", true);
michael@0 3818 child.setAttribute("cui-anchorid", this._chevron.id);
michael@0 3819 CustomizableUIInternal.notifyListeners("onWidgetOverflow", child, this._target);
michael@0 3820
michael@0 3821 this._list.insertBefore(child, this._list.firstChild);
michael@0 3822 if (!this._toolbar.hasAttribute("overflowing")) {
michael@0 3823 CustomizableUI.addListener(this);
michael@0 3824 }
michael@0 3825 this._toolbar.setAttribute("overflowing", "true");
michael@0 3826 }
michael@0 3827 child = prevChild;
michael@0 3828 };
michael@0 3829
michael@0 3830 let win = this._target.ownerDocument.defaultView;
michael@0 3831 win.UpdateUrlbarSearchSplitterState();
michael@0 3832 },
michael@0 3833
michael@0 3834 _onResize: function(aEvent) {
michael@0 3835 if (!this._lazyResizeHandler) {
michael@0 3836 this._lazyResizeHandler = new DeferredTask(this._onLazyResize.bind(this),
michael@0 3837 LAZY_RESIZE_INTERVAL_MS);
michael@0 3838 }
michael@0 3839 this._lazyResizeHandler.arm();
michael@0 3840 },
michael@0 3841
michael@0 3842 _moveItemsBackToTheirOrigin: function(shouldMoveAllItems) {
michael@0 3843 let placements = gPlacements.get(this._toolbar.id);
michael@0 3844 while (this._list.firstChild) {
michael@0 3845 let child = this._list.firstChild;
michael@0 3846 let minSize = this._collapsed.get(child.id);
michael@0 3847
michael@0 3848 if (!shouldMoveAllItems &&
michael@0 3849 minSize &&
michael@0 3850 this._target.clientWidth <= minSize) {
michael@0 3851 return;
michael@0 3852 }
michael@0 3853
michael@0 3854 this._collapsed.delete(child.id);
michael@0 3855 let beforeNodeIndex = placements.indexOf(child.id) + 1;
michael@0 3856 // If this is a skipintoolbarset item, meaning it doesn't occur in the placements list,
michael@0 3857 // we're inserting it at the end. This will mean first-in, first-out (more or less)
michael@0 3858 // leading to as little change in order as possible.
michael@0 3859 if (beforeNodeIndex == 0) {
michael@0 3860 beforeNodeIndex = placements.length;
michael@0 3861 }
michael@0 3862 let inserted = false;
michael@0 3863 for (; beforeNodeIndex < placements.length; beforeNodeIndex++) {
michael@0 3864 let beforeNode = this._target.getElementsByAttribute("id", placements[beforeNodeIndex])[0];
michael@0 3865 if (beforeNode) {
michael@0 3866 this._target.insertBefore(child, beforeNode);
michael@0 3867 inserted = true;
michael@0 3868 break;
michael@0 3869 }
michael@0 3870 }
michael@0 3871 if (!inserted) {
michael@0 3872 this._target.appendChild(child);
michael@0 3873 }
michael@0 3874 child.removeAttribute("cui-anchorid");
michael@0 3875 child.removeAttribute("overflowedItem");
michael@0 3876 CustomizableUIInternal.notifyListeners("onWidgetUnderflow", child, this._target);
michael@0 3877 }
michael@0 3878
michael@0 3879 let win = this._target.ownerDocument.defaultView;
michael@0 3880 win.UpdateUrlbarSearchSplitterState();
michael@0 3881
michael@0 3882 if (!this._collapsed.size) {
michael@0 3883 this._toolbar.removeAttribute("overflowing");
michael@0 3884 CustomizableUI.removeListener(this);
michael@0 3885 }
michael@0 3886 },
michael@0 3887
michael@0 3888 _onLazyResize: function() {
michael@0 3889 if (!this._enabled)
michael@0 3890 return;
michael@0 3891
michael@0 3892 if (this._target.scrollLeftMax > 0) {
michael@0 3893 this.onOverflow();
michael@0 3894 } else {
michael@0 3895 this._moveItemsBackToTheirOrigin();
michael@0 3896 }
michael@0 3897 },
michael@0 3898
michael@0 3899 _disable: function() {
michael@0 3900 this._enabled = false;
michael@0 3901 this._moveItemsBackToTheirOrigin(true);
michael@0 3902 if (this._lazyResizeHandler) {
michael@0 3903 this._lazyResizeHandler.disarm();
michael@0 3904 }
michael@0 3905 },
michael@0 3906
michael@0 3907 _enable: function() {
michael@0 3908 this._enabled = true;
michael@0 3909 this.onOverflow();
michael@0 3910 },
michael@0 3911
michael@0 3912 onWidgetBeforeDOMChange: function(aNode, aNextNode, aContainer) {
michael@0 3913 if (aContainer != this._target && aContainer != this._list) {
michael@0 3914 return;
michael@0 3915 }
michael@0 3916 // When we (re)move an item, update all the items that come after it in the list
michael@0 3917 // with the minsize *of the item before the to-be-removed node*. This way, we
michael@0 3918 // ensure that we try to move items back as soon as that's possible.
michael@0 3919 if (aNode.parentNode == this._list) {
michael@0 3920 let updatedMinSize;
michael@0 3921 if (aNode.previousSibling) {
michael@0 3922 updatedMinSize = this._collapsed.get(aNode.previousSibling.id);
michael@0 3923 } else {
michael@0 3924 // Force (these) items to try to flow back into the bar:
michael@0 3925 updatedMinSize = 1;
michael@0 3926 }
michael@0 3927 let nextItem = aNode.nextSibling;
michael@0 3928 while (nextItem) {
michael@0 3929 this._collapsed.set(nextItem.id, updatedMinSize);
michael@0 3930 nextItem = nextItem.nextSibling;
michael@0 3931 }
michael@0 3932 }
michael@0 3933 },
michael@0 3934
michael@0 3935 onWidgetAfterDOMChange: function(aNode, aNextNode, aContainer) {
michael@0 3936 if (aContainer != this._target && aContainer != this._list) {
michael@0 3937 return;
michael@0 3938 }
michael@0 3939
michael@0 3940 let nowInBar = aNode.parentNode == aContainer;
michael@0 3941 let nowOverflowed = aNode.parentNode == this._list;
michael@0 3942 let wasOverflowed = this._collapsed.has(aNode.id);
michael@0 3943
michael@0 3944 // If this wasn't overflowed before...
michael@0 3945 if (!wasOverflowed) {
michael@0 3946 // ... but it is now, then we added to the overflow panel. Exciting stuff:
michael@0 3947 if (nowOverflowed) {
michael@0 3948 // NB: we're guaranteed that it has a previousSibling, because if it didn't,
michael@0 3949 // we would have added it to the toolbar instead. See getOverflowedNextNode.
michael@0 3950 let prevId = aNode.previousSibling.id;
michael@0 3951 let minSize = this._collapsed.get(prevId);
michael@0 3952 this._collapsed.set(aNode.id, minSize);
michael@0 3953 aNode.setAttribute("cui-anchorid", this._chevron.id);
michael@0 3954 aNode.setAttribute("overflowedItem", true);
michael@0 3955 CustomizableUIInternal.notifyListeners("onWidgetOverflow", aNode, this._target);
michael@0 3956 }
michael@0 3957 // If it is not overflowed and not in the toolbar, and was not overflowed
michael@0 3958 // either, it moved out of the toolbar. That means there's now space in there!
michael@0 3959 // Let's try to move stuff back:
michael@0 3960 else if (!nowInBar) {
michael@0 3961 this._moveItemsBackToTheirOrigin(true);
michael@0 3962 }
michael@0 3963 // If it's in the toolbar now, then we don't care. An overflow event may
michael@0 3964 // fire afterwards; that's ok!
michael@0 3965 }
michael@0 3966 // If it used to be overflowed...
michael@0 3967 else {
michael@0 3968 // ... and isn't anymore, let's remove our bookkeeping:
michael@0 3969 if (!nowOverflowed) {
michael@0 3970 this._collapsed.delete(aNode.id);
michael@0 3971 aNode.removeAttribute("cui-anchorid");
michael@0 3972 aNode.removeAttribute("overflowedItem");
michael@0 3973 CustomizableUIInternal.notifyListeners("onWidgetUnderflow", aNode, this._target);
michael@0 3974
michael@0 3975 if (!this._collapsed.size) {
michael@0 3976 this._toolbar.removeAttribute("overflowing");
michael@0 3977 CustomizableUI.removeListener(this);
michael@0 3978 }
michael@0 3979 }
michael@0 3980 // but if it still is, it must have changed places. Bookkeep:
michael@0 3981 else {
michael@0 3982 if (aNode.previousSibling) {
michael@0 3983 let prevId = aNode.previousSibling.id;
michael@0 3984 let minSize = this._collapsed.get(prevId);
michael@0 3985 this._collapsed.set(aNode.id, minSize);
michael@0 3986 } else {
michael@0 3987 // If it's now the first item in the overflow list,
michael@0 3988 // maybe we can return it:
michael@0 3989 this._moveItemsBackToTheirOrigin();
michael@0 3990 }
michael@0 3991 }
michael@0 3992 }
michael@0 3993 },
michael@0 3994
michael@0 3995 findOverflowedInsertionPoints: function(aNode) {
michael@0 3996 let newNodeCanOverflow = aNode.getAttribute("overflows") != "false";
michael@0 3997 let areaId = this._toolbar.id;
michael@0 3998 let placements = gPlacements.get(areaId);
michael@0 3999 let nodeIndex = placements.indexOf(aNode.id);
michael@0 4000 let nodeBeforeNewNodeIsOverflown = false;
michael@0 4001
michael@0 4002 let loopIndex = -1;
michael@0 4003 while (++loopIndex < placements.length) {
michael@0 4004 let nextNodeId = placements[loopIndex];
michael@0 4005 if (loopIndex > nodeIndex) {
michael@0 4006 if (newNodeCanOverflow && this._collapsed.has(nextNodeId)) {
michael@0 4007 let nextNode = this._list.getElementsByAttribute("id", nextNodeId).item(0);
michael@0 4008 if (nextNode) {
michael@0 4009 return [this._list, nextNode];
michael@0 4010 }
michael@0 4011 }
michael@0 4012 if (!nodeBeforeNewNodeIsOverflown || !newNodeCanOverflow) {
michael@0 4013 let nextNode = this._target.getElementsByAttribute("id", nextNodeId).item(0);
michael@0 4014 if (nextNode) {
michael@0 4015 return [this._target, nextNode];
michael@0 4016 }
michael@0 4017 }
michael@0 4018 } else if (loopIndex < nodeIndex && this._collapsed.has(nextNodeId)) {
michael@0 4019 nodeBeforeNewNodeIsOverflown = true;
michael@0 4020 }
michael@0 4021 }
michael@0 4022
michael@0 4023 let containerForAppending = (this._collapsed.size && newNodeCanOverflow) ?
michael@0 4024 this._list : this._target;
michael@0 4025 return [containerForAppending, null];
michael@0 4026 },
michael@0 4027
michael@0 4028 getContainerFor: function(aNode) {
michael@0 4029 if (aNode.getAttribute("overflowedItem") == "true") {
michael@0 4030 return this._list;
michael@0 4031 }
michael@0 4032 return this._target;
michael@0 4033 },
michael@0 4034 };
michael@0 4035
michael@0 4036 CustomizableUIInternal.initialize();

mercurial