ext-tabGroups.js (9188B)
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 file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 "use strict"; 5 6 ChromeUtils.defineESModuleGetters(this, { 7 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 8 }); 9 10 var { ExtensionError } = ExtensionUtils; 11 12 const spellColour = color => (color === "grey" ? "gray" : color); 13 14 /** 15 * @param {MozTabbrowserTabGroup} group Group to move. 16 * @param {DOMWindow} window Browser window to move to. 17 * @param {integer} index The desired position of the group within the window 18 * @returns {integer} The tab index that the group should move to, such that 19 * after the move operation, the group's position is at the given index. 20 */ 21 function adjustIndexForMove(group, window, index) { 22 let tabIndex = index < 0 ? window.gBrowser.tabs.length : index; 23 if (group.ownerGlobal === window) { 24 let group_tabs = group.tabs; 25 if (tabIndex > group_tabs[0]._tPos) { 26 // When group is moving to a higher index, we need to increase the 27 // index to account for the fact that the act of moving tab groups 28 // causes all following tabs to have a decreased index. 29 tabIndex += group_tabs.length; 30 } 31 } 32 tabIndex = Math.min(tabIndex, window.gBrowser.tabs.length); 33 34 let prevTab = tabIndex > 0 ? window.gBrowser.tabs.at(tabIndex - 1) : null; 35 let nextTab = window.gBrowser.tabs.at(tabIndex); 36 if (nextTab?.pinned) { 37 throw new ExtensionError( 38 "Cannot move the group to an index that is in the middle of pinned tabs." 39 ); 40 } 41 if (prevTab && nextTab?.group && prevTab.group === nextTab.group) { 42 throw new ExtensionError( 43 "Cannot move the group to an index that is in the middle of another group." 44 ); 45 } 46 47 return tabIndex; 48 } 49 50 this.tabGroups = class extends ExtensionAPIPersistent { 51 queryGroups({ collapsed, color, title, windowId } = {}) { 52 color = spellColour(color); 53 let glob = title != null && new MatchGlob(title); 54 let window = 55 windowId != null && windowTracker.getWindow(windowId, null, false); 56 return windowTracker 57 .browserWindows() 58 .filter( 59 win => 60 this.extension.canAccessWindow(win) && 61 (windowId == null || win === window) 62 ) 63 .flatMap(win => win.gBrowser.tabGroups) 64 .filter( 65 group => 66 (collapsed == null || group.collapsed === collapsed) && 67 (color == null || group.color === color) && 68 (title == null || glob.matches(group.name)) 69 ); 70 } 71 72 get(groupId) { 73 let gid = getInternalTabGroupIdForExtTabGroupId(groupId); 74 if (!gid) { 75 throw new ExtensionError(`No group with id: ${groupId}`); 76 } 77 for (let group of this.queryGroups()) { 78 if (group.id === gid) { 79 return group; 80 } 81 } 82 throw new ExtensionError(`No group with id: ${groupId}`); 83 } 84 85 convert(group) { 86 return { 87 collapsed: !!group.collapsed, 88 /** Internally we use "gray", but Chrome uses "grey" @see spellColour. */ 89 color: group.color === "gray" ? "grey" : group.color, 90 id: getExtTabGroupIdForInternalTabGroupId(group.id), 91 title: group.name, 92 windowId: windowTracker.getId(group.ownerGlobal), 93 }; 94 } 95 96 PERSISTENT_EVENTS = { 97 onCreated({ fire }) { 98 let onCreate = event => { 99 if (event.detail.isAdoptingGroup) { 100 // Tab group moved from a different window. 101 return; 102 } 103 if (!this.extension.canAccessWindow(event.originalTarget.ownerGlobal)) { 104 return; 105 } 106 fire.async(this.convert(event.originalTarget)); 107 }; 108 windowTracker.addListener("TabGroupCreate", onCreate); 109 return { 110 unregister() { 111 windowTracker.removeListener("TabGroupCreate", onCreate); 112 }, 113 convert(_fire) { 114 fire = _fire; 115 }, 116 }; 117 }, 118 onMoved({ fire }) { 119 let onMove = event => { 120 if (!this.extension.canAccessWindow(event.originalTarget.ownerGlobal)) { 121 return; 122 } 123 fire.async(this.convert(event.originalTarget)); 124 }; 125 let onCreate = event => { 126 if (!event.detail.isAdoptingGroup) { 127 // We are only interested in tab groups moved from a different window. 128 return; 129 } 130 if (!this.extension.canAccessWindow(event.originalTarget.ownerGlobal)) { 131 return; 132 } 133 fire.async(this.convert(event.originalTarget)); 134 }; 135 windowTracker.addListener("TabGroupMoved", onMove); 136 windowTracker.addListener("TabGroupCreate", onCreate); 137 return { 138 unregister() { 139 windowTracker.removeListener("TabGroupMoved", onMove); 140 windowTracker.removeListener("TabGroupCreate", onCreate); 141 }, 142 convert(_fire) { 143 fire = _fire; 144 }, 145 }; 146 }, 147 onRemoved({ fire }) { 148 let onRemove = event => { 149 if (event.originalTarget.removedByAdoption) { 150 // Tab group moved to a different window. 151 return; 152 } 153 if (!this.extension.canAccessWindow(event.originalTarget.ownerGlobal)) { 154 return; 155 } 156 fire.async(this.convert(event.originalTarget), { 157 isWindowClosing: false, 158 }); 159 }; 160 let onClosed = window => { 161 if (!this.extension.canAccessWindow(window)) { 162 return; 163 } 164 for (const group of window.gBrowser.tabGroups) { 165 fire.async(this.convert(group), { isWindowClosing: true }); 166 } 167 }; 168 windowTracker.addListener("TabGroupRemoved", onRemove); 169 windowTracker.addListener("domwindowclosed", onClosed); 170 return { 171 unregister() { 172 windowTracker.removeListener("TabGroupRemoved", onRemove); 173 windowTracker.removeListener("domwindowclosed", onClosed); 174 }, 175 convert(_fire) { 176 fire = _fire; 177 }, 178 }; 179 }, 180 onUpdated({ fire }) { 181 let onUpdate = event => { 182 if (!this.extension.canAccessWindow(event.originalTarget.ownerGlobal)) { 183 return; 184 } 185 fire.async(this.convert(event.originalTarget)); 186 }; 187 windowTracker.addListener("TabGroupCollapse", onUpdate); 188 windowTracker.addListener("TabGroupExpand", onUpdate); 189 windowTracker.addListener("TabGroupUpdate", onUpdate); 190 return { 191 unregister() { 192 windowTracker.removeListener("TabGroupCollapse", onUpdate); 193 windowTracker.removeListener("TabGroupExpand", onUpdate); 194 windowTracker.removeListener("TabGroupUpdate", onUpdate); 195 }, 196 convert(_fire) { 197 fire = _fire; 198 }, 199 }; 200 }, 201 }; 202 203 getAPI(context) { 204 const { windowManager } = this.extension; 205 return { 206 tabGroups: { 207 get: groupId => { 208 return this.convert(this.get(groupId)); 209 }, 210 211 move: (groupId, { index, windowId }) => { 212 let group = this.get(groupId); 213 let win = group.ownerGlobal; 214 215 if (windowId != null) { 216 win = windowTracker.getWindow(windowId, context); 217 if ( 218 PrivateBrowsingUtils.isWindowPrivate(group.ownerGlobal) !== 219 PrivateBrowsingUtils.isWindowPrivate(win) 220 ) { 221 throw new ExtensionError( 222 "Can't move groups between private and non-private windows" 223 ); 224 } 225 if (windowManager.getWrapper(win).type !== "normal") { 226 throw new ExtensionError( 227 "Groups can only be moved to normal windows." 228 ); 229 } 230 } 231 232 let tabIndex = adjustIndexForMove(group, win, index); 233 if (win !== group.ownerGlobal) { 234 group = win.gBrowser.adoptTabGroup(group, { tabIndex }); 235 } else { 236 win.gBrowser.moveTabTo(group, { tabIndex }); 237 } 238 return this.convert(group); 239 }, 240 241 query: query => { 242 return Array.from(this.queryGroups(query), group => 243 this.convert(group) 244 ); 245 }, 246 247 update: (groupId, { collapsed, color, title }) => { 248 let group = this.get(groupId); 249 if (collapsed != null) { 250 group.collapsed = collapsed; 251 } 252 if (color != null) { 253 group.color = spellColour(color); 254 } 255 if (title != null) { 256 group.name = title; 257 } 258 return this.convert(group); 259 }, 260 261 onCreated: new EventManager({ 262 context, 263 module: "tabGroups", 264 event: "onCreated", 265 extensionApi: this, 266 }).api(), 267 268 onMoved: new EventManager({ 269 context, 270 module: "tabGroups", 271 event: "onMoved", 272 extensionApi: this, 273 }).api(), 274 275 onRemoved: new EventManager({ 276 context, 277 module: "tabGroups", 278 event: "onRemoved", 279 extensionApi: this, 280 }).api(), 281 282 onUpdated: new EventManager({ 283 context, 284 module: "tabGroups", 285 event: "onUpdated", 286 extensionApi: this, 287 }).api(), 288 }, 289 }; 290 } 291 };