|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 "use strict"; |
|
6 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; |
|
7 |
|
8 this.EXPORTED_SYMBOLS = ["PanelWideWidgetTracker"]; |
|
9 |
|
10 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
11 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI", |
|
12 "resource:///modules/CustomizableUI.jsm"); |
|
13 |
|
14 let gModuleName = "[PanelWideWidgetTracker]"; |
|
15 #include logging.js |
|
16 |
|
17 let gPanel = CustomizableUI.AREA_PANEL; |
|
18 // We keep track of the widget placements for the panel locally: |
|
19 let gPanelPlacements = []; |
|
20 |
|
21 // All the wide widgets we know of: |
|
22 let gWideWidgets = new Set(); |
|
23 // All the widgets we know of: |
|
24 let gSeenWidgets = new Set(); |
|
25 |
|
26 let PanelWideWidgetTracker = { |
|
27 // Listeners used to validate panel contents whenever they change: |
|
28 onWidgetAdded: function(aWidgetId, aArea, aPosition) { |
|
29 if (aArea == gPanel) { |
|
30 gPanelPlacements = CustomizableUI.getWidgetIdsInArea(gPanel); |
|
31 let moveForward = this.shouldMoveForward(aWidgetId, aPosition); |
|
32 this.adjustWidgets(aWidgetId, moveForward); |
|
33 } |
|
34 }, |
|
35 onWidgetMoved: function(aWidgetId, aArea, aOldPosition, aNewPosition) { |
|
36 if (aArea == gPanel) { |
|
37 gPanelPlacements = CustomizableUI.getWidgetIdsInArea(gPanel); |
|
38 let moveForward = this.shouldMoveForward(aWidgetId, aNewPosition); |
|
39 this.adjustWidgets(aWidgetId, moveForward); |
|
40 } |
|
41 }, |
|
42 onWidgetRemoved: function(aWidgetId, aPrevArea) { |
|
43 if (aPrevArea == gPanel) { |
|
44 gPanelPlacements = CustomizableUI.getWidgetIdsInArea(gPanel); |
|
45 let pos = gPanelPlacements.indexOf(aWidgetId); |
|
46 this.adjustWidgets(aWidgetId, false); |
|
47 } |
|
48 }, |
|
49 onWidgetReset: function(aWidgetId) { |
|
50 gPanelPlacements = CustomizableUI.getWidgetIdsInArea(gPanel); |
|
51 }, |
|
52 // Listener to keep abreast of any new nodes. We use the DOM one because |
|
53 // we need access to the actual node's classlist, so we can't use the ones above. |
|
54 // Furthermore, onWidgetCreated only fires for API-based widgets, not for XUL ones. |
|
55 onWidgetAfterDOMChange: function(aNode, aNextNode, aContainer) { |
|
56 if (!gSeenWidgets.has(aNode.id)) { |
|
57 if (aNode.classList.contains(CustomizableUI.WIDE_PANEL_CLASS)) { |
|
58 gWideWidgets.add(aNode.id); |
|
59 } |
|
60 gSeenWidgets.add(aNode.id); |
|
61 } |
|
62 }, |
|
63 // When widgets get destroyed, we remove them from our sets of stuff we care about: |
|
64 onWidgetDestroyed: function(aWidgetId) { |
|
65 gSeenWidgets.delete(aWidgetId); |
|
66 gWideWidgets.delete(aWidgetId); |
|
67 }, |
|
68 shouldMoveForward: function(aWidgetId, aPosition) { |
|
69 let currentWidgetAtPosition = gPanelPlacements[aPosition + 1]; |
|
70 let rv = gWideWidgets.has(currentWidgetAtPosition) && !gWideWidgets.has(aWidgetId); |
|
71 // We might now think we can move forward, but for that we need at least 2 more small |
|
72 // widgets to be present: |
|
73 if (rv) { |
|
74 let furtherWidgets = gPanelPlacements.slice(aPosition + 2); |
|
75 let realWidgets = 0; |
|
76 if (furtherWidgets.length >= 2) { |
|
77 while (furtherWidgets.length && realWidgets < 2) { |
|
78 let w = furtherWidgets.shift(); |
|
79 if (!gWideWidgets.has(w) && this.checkWidgetStatus(w)) { |
|
80 realWidgets++; |
|
81 } |
|
82 } |
|
83 } |
|
84 if (realWidgets < 2) { |
|
85 rv = false; |
|
86 } |
|
87 } |
|
88 return rv; |
|
89 }, |
|
90 adjustWidgets: function(aWidgetId, aMoveForwards) { |
|
91 if (this.adjusting) { |
|
92 return; |
|
93 } |
|
94 this.adjusting = true; |
|
95 let widgetsAffected = [w for (w of gPanelPlacements) if (gWideWidgets.has(w))]; |
|
96 // If we're moving the wide widgets forwards (down/to the right in the panel) |
|
97 // we want to start with the last widgets. Otherwise we move widgets over other wide |
|
98 // widgets, which might mess up their order. Likewise, if moving backwards we should start with |
|
99 // the first widget and work our way down/right from there. |
|
100 let compareFn = aMoveForwards ? (function(a, b) a < b) : (function(a, b) a > b) |
|
101 widgetsAffected.sort(function(a, b) compareFn(gPanelPlacements.indexOf(a), |
|
102 gPanelPlacements.indexOf(b))); |
|
103 for (let widget of widgetsAffected) { |
|
104 this.adjustPosition(widget, aMoveForwards); |
|
105 } |
|
106 this.adjusting = false; |
|
107 }, |
|
108 // This function is called whenever an item gets moved in the menu panel. It |
|
109 // adjusts the position of widgets within the panel to prevent "gaps" between |
|
110 // wide widgets that could be filled up with single column widgets |
|
111 adjustPosition: function(aWidgetId, aMoveForwards) { |
|
112 // Make sure that there are n % columns = 0 narrow buttons before the widget. |
|
113 let placementIndex = gPanelPlacements.indexOf(aWidgetId); |
|
114 let prevSiblingCount = 0; |
|
115 let fixedPos = null; |
|
116 while (placementIndex--) { |
|
117 let thisWidgetId = gPanelPlacements[placementIndex]; |
|
118 if (gWideWidgets.has(thisWidgetId)) { |
|
119 continue; |
|
120 } |
|
121 let widgetStatus = this.checkWidgetStatus(thisWidgetId); |
|
122 if (!widgetStatus) { |
|
123 continue; |
|
124 } |
|
125 if (widgetStatus == "public-only") { |
|
126 fixedPos = !fixedPos ? placementIndex : Math.min(fixedPos, placementIndex); |
|
127 prevSiblingCount = 0; |
|
128 } else { |
|
129 prevSiblingCount++; |
|
130 } |
|
131 } |
|
132 |
|
133 if (fixedPos !== null || prevSiblingCount % CustomizableUI.PANEL_COLUMN_COUNT) { |
|
134 let desiredPos = (fixedPos !== null) ? fixedPos : gPanelPlacements.indexOf(aWidgetId); |
|
135 let desiredChange = -(prevSiblingCount % CustomizableUI.PANEL_COLUMN_COUNT); |
|
136 if (aMoveForwards && fixedPos == null) { |
|
137 // +1 because otherwise we'd count ourselves: |
|
138 desiredChange = CustomizableUI.PANEL_COLUMN_COUNT + desiredChange + 1; |
|
139 } |
|
140 desiredPos += desiredChange; |
|
141 CustomizableUI.moveWidgetWithinArea(aWidgetId, desiredPos); |
|
142 } |
|
143 }, |
|
144 |
|
145 /* |
|
146 * Check whether a widget id is actually known anywhere. |
|
147 * @returns false if the widget doesn't exist, |
|
148 * "public-only" if it's not shown in private windows |
|
149 * "real" if it does exist and is shown even in private windows |
|
150 */ |
|
151 checkWidgetStatus: function(aWidgetId) { |
|
152 let widgetWrapper = CustomizableUI.getWidget(aWidgetId); |
|
153 // This widget might not actually exist: |
|
154 if (!widgetWrapper) { |
|
155 return false; |
|
156 } |
|
157 // This widget might still not actually exist: |
|
158 if (widgetWrapper.provider == CustomizableUI.PROVIDER_XUL && |
|
159 widgetWrapper.instances.length == 0) { |
|
160 return false; |
|
161 } |
|
162 |
|
163 // Or it might only be there some of the time: |
|
164 if (widgetWrapper.provider == CustomizableUI.PROVIDER_API && |
|
165 widgetWrapper.showInPrivateBrowsing === false) { |
|
166 return "public-only"; |
|
167 } |
|
168 return "real"; |
|
169 }, |
|
170 |
|
171 init: function() { |
|
172 // Initialize our local placements copy and register the listener |
|
173 gPanelPlacements = CustomizableUI.getWidgetIdsInArea(gPanel); |
|
174 CustomizableUI.addListener(this); |
|
175 }, |
|
176 }; |