/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; this.EXPORTED_SYMBOLS = ["PanelWideWidgetTracker"]; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI", "resource:///modules/CustomizableUI.jsm"); var gPanel = CustomizableUI.AREA_PANEL; // We keep track of the widget placements for the panel locally: var gPanelPlacements = []; // All the wide widgets we know of: var gWideWidgets = new Set(); // All the widgets we know of: var gSeenWidgets = new Set(); var PanelWideWidgetTracker = { // Listeners used to validate panel contents whenever they change: onWidgetAdded: function(aWidgetId, aArea, aPosition) { if (aArea == gPanel) { gPanelPlacements = CustomizableUI.getWidgetIdsInArea(gPanel); let moveForward = this.shouldMoveForward(aWidgetId, aPosition); this.adjustWidgets(aWidgetId, moveForward); } }, onWidgetMoved: function(aWidgetId, aArea, aOldPosition, aNewPosition) { if (aArea == gPanel) { gPanelPlacements = CustomizableUI.getWidgetIdsInArea(gPanel); let moveForward = this.shouldMoveForward(aWidgetId, aNewPosition); this.adjustWidgets(aWidgetId, moveForward); } }, onWidgetRemoved: function(aWidgetId, aPrevArea) { if (aPrevArea == gPanel) { gPanelPlacements = CustomizableUI.getWidgetIdsInArea(gPanel); this.adjustWidgets(aWidgetId, false); } }, onWidgetReset: function(aWidgetId) { gPanelPlacements = CustomizableUI.getWidgetIdsInArea(gPanel); }, // Listener to keep abreast of any new nodes. We use the DOM one because // we need access to the actual node's classlist, so we can't use the ones above. // Furthermore, onWidgetCreated only fires for API-based widgets, not for XUL ones. onWidgetAfterDOMChange: function(aNode, aNextNode, aContainer) { if (!gSeenWidgets.has(aNode.id)) { if (aNode.classList.contains(CustomizableUI.WIDE_PANEL_CLASS)) { gWideWidgets.add(aNode.id); } gSeenWidgets.add(aNode.id); } }, // When widgets get destroyed, we remove them from our sets of stuff we care about: onWidgetDestroyed: function(aWidgetId) { gSeenWidgets.delete(aWidgetId); gWideWidgets.delete(aWidgetId); }, shouldMoveForward: function(aWidgetId, aPosition) { let currentWidgetAtPosition = gPanelPlacements[aPosition + 1]; let rv = gWideWidgets.has(currentWidgetAtPosition) && !gWideWidgets.has(aWidgetId); // We might now think we can move forward, but for that we need at least 2 more small // widgets to be present: if (rv) { let furtherWidgets = gPanelPlacements.slice(aPosition + 2); let realWidgets = 0; if (furtherWidgets.length >= 2) { while (furtherWidgets.length && realWidgets < 2) { let w = furtherWidgets.shift(); if (!gWideWidgets.has(w) && this.checkWidgetStatus(w)) { realWidgets++; } } } if (realWidgets < 2) { rv = false; } } return rv; }, adjustWidgets: function(aWidgetId, aMoveForwards) { if (this.adjusting) { return; } this.adjusting = true; let widgetsAffected = gPanelPlacements.filter((w) => gWideWidgets.has(w)); // If we're moving the wide widgets forwards (down/to the right in the panel) // we want to start with the last widgets. Otherwise we move widgets over other wide // widgets, which might mess up their order. Likewise, if moving backwards we should start with // the first widget and work our way down/right from there. let compareFn = aMoveForwards ? ((a, b) => a < b) : ((a, b) => a > b); widgetsAffected.sort((a, b) => compareFn(gPanelPlacements.indexOf(a), gPanelPlacements.indexOf(b))); for (let widget of widgetsAffected) { this.adjustPosition(widget, aMoveForwards); } this.adjusting = false; }, // This function is called whenever an item gets moved in the menu panel. It // adjusts the position of widgets within the panel to prevent "gaps" between // wide widgets that could be filled up with single column widgets adjustPosition: function(aWidgetId, aMoveForwards) { // Make sure that there are n % columns = 0 narrow buttons before the widget. let placementIndex = gPanelPlacements.indexOf(aWidgetId); let prevSiblingCount = 0; let fixedPos = null; while (placementIndex--) { let thisWidgetId = gPanelPlacements[placementIndex]; if (gWideWidgets.has(thisWidgetId)) { continue; } let widgetStatus = this.checkWidgetStatus(thisWidgetId); if (!widgetStatus) { continue; } if (widgetStatus == "public-only") { fixedPos = !fixedPos ? placementIndex : Math.min(fixedPos, placementIndex); prevSiblingCount = 0; } else { prevSiblingCount++; } } if (fixedPos !== null || prevSiblingCount % CustomizableUI.PANEL_COLUMN_COUNT) { let desiredPos = (fixedPos !== null) ? fixedPos : gPanelPlacements.indexOf(aWidgetId); let desiredChange = -(prevSiblingCount % CustomizableUI.PANEL_COLUMN_COUNT); if (aMoveForwards && fixedPos == null) { // +1 because otherwise we'd count ourselves: desiredChange = CustomizableUI.PANEL_COLUMN_COUNT + desiredChange + 1; } desiredPos += desiredChange; CustomizableUI.moveWidgetWithinArea(aWidgetId, desiredPos); } }, /* * Check whether a widget id is actually known anywhere. * @returns false if the widget doesn't exist, * "public-only" if it's not shown in private windows * "real" if it does exist and is shown even in private windows */ checkWidgetStatus: function(aWidgetId) { let widgetWrapper = CustomizableUI.getWidget(aWidgetId); // This widget might not actually exist: if (!widgetWrapper) { return false; } // This widget might still not actually exist: if (widgetWrapper.provider == CustomizableUI.PROVIDER_XUL && widgetWrapper.instances.length == 0) { return false; } // Or it might only be there some of the time: if (widgetWrapper.provider == CustomizableUI.PROVIDER_API && widgetWrapper.showInPrivateBrowsing === false) { return "public-only"; } return "real"; }, init: function() { // Initialize our local placements copy and register the listener gPanelPlacements = CustomizableUI.getWidgetIdsInArea(gPanel); CustomizableUI.addListener(this); }, };