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