Skip to content

Commit b512df5

Browse files
committed
Render fake tab item corresponding to a native tab group in the tab bar
1 parent 33503cb commit b512df5

9 files changed

Lines changed: 233 additions & 93 deletions

File tree

webextensions/background/api-tabs-listener.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1299,11 +1299,12 @@ browser.windows.onFocusChanged.addListener(windowId => {
12991299

13001300

13011301
async function onGroupCreated(group) {
1302+
log('onGroupCreated ', group);
13021303
const win = TabsStore.windows.get(group.windowId);
13031304
if (!win) {
13041305
throw new Error('tabGroups.onCreated is called before the owner window is tracked');
13051306
}
1306-
win.tabGroups.set(group.id, group);
1307+
win.tabGroups.set(group.id, new Tab(group));
13071308

13081309
SidebarConnection.sendMessage({
13091310
type: Constants.kCOMMAND_NOTIFY_TAB_GROUP_CREATED,
@@ -1316,14 +1317,15 @@ async function onGroupUpdated(group) {
13161317
if (mPromisedStarted)
13171318
await mPromisedStarted;
13181319

1320+
log('onGroupUpdated ', group);
13191321
const win = TabsStore.windows.get(group.windowId);
13201322
if (!win) {
13211323
throw new Error('tabGroups.onUpdated is called before the owner window is tracked');
13221324
}
1323-
win.tabGroups.set(group.id, {
1325+
win.tabGroups.get(group.id).raw = {
13241326
...win.tabGroups.get(group.id),
13251327
...group,
1326-
});
1328+
};
13271329

13281330
SidebarConnection.sendMessage({
13291331
type: Constants.kCOMMAND_NOTIFY_TAB_GROUP_UPDATED,
@@ -1336,10 +1338,12 @@ async function onGroupRemoved(group) {
13361338
if (mPromisedStarted)
13371339
await mPromisedStarted;
13381340

1341+
log('onGroupRemoved ', group);
13391342
const win = TabsStore.windows.get(group.windowId);
13401343
if (!win) {
13411344
throw new Error('tabGroups.onRemoved is called before the owner window is tracked');
13421345
}
1346+
win.tabGroups.get(group.id).destroy();
13431347
win.tabGroups.delete(group.id);
13441348

13451349
SidebarConnection.sendMessage({

webextensions/common/Tab.js

Lines changed: 124 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -66,22 +66,27 @@ browser.windows.onRemoved.addListener(windowId => {
6666
});
6767

6868
export default class Tab {
69-
constructor(tab) {
70-
const alreadyTracked = Tab.get(tab.id);
69+
constructor(raw) {
70+
const alreadyTracked = Tab.get(raw.id);
7171
if (alreadyTracked)
7272
return alreadyTracked.$TST;
7373

74-
log(`tab ${dumpTab(tab)} is newly tracked: `, tab);
74+
log(`tab ${dumpTab(raw)} is newly tracked: `, raw);
75+
76+
raw.$TST = this;
77+
this.raw = raw;
78+
this.id = raw.id;
79+
80+
const tab = this.tab;
7581

76-
tab.$TST = this;
77-
this.tab = tab;
78-
this.id = tab.id;
7982
this.trackedAt = Date.now();
80-
this.opened = new Promise((resolve, reject) => {
81-
const resolvers = mOpenedResolvers.get(tab.id) || new Set();
82-
resolvers.add({ resolve, reject });
83-
mOpenedResolvers.set(tab.id, resolvers);
84-
});
83+
this.opened = tab ?
84+
new Promise((resolve, reject) => {
85+
const resolvers = mOpenedResolvers.get(tab.id) || new Set();
86+
resolvers.add({ resolve, reject });
87+
mOpenedResolvers.set(tab.id, resolvers);
88+
}) :
89+
Promise.resolve(true);
8590

8691
// We should not change the shape of the object, so temporary data should be held in this map.
8792
this.temporaryMetadata = new Map();
@@ -138,59 +143,65 @@ export default class Tab {
138143
this.onUniqueIdGenerated = resolve;
139144
});
140145

141-
TabsStore.tabs.set(tab.id, tab);
146+
if (tab) {
147+
this.index = tab.index;
142148

143-
const win = TabsStore.windows.get(tab.windowId) || new Window(tab.windowId);
144-
win.trackTab(tab);
149+
TabsStore.tabs.set(tab.id, tab);
145150

146-
// Don't update indexes here, instead Window.prototype.trackTab()
147-
// updates indexes because indexes are bound to windows.
148-
// TabsStore.updateIndexesForTab(tab);
151+
const win = TabsStore.windows.get(tab.windowId) || new Window(tab.windowId);
152+
win.trackTab(tab);
149153

150-
if (tab.active) {
151-
TabsStore.activeTabInWindow.set(tab.windowId, tab);
152-
TabsStore.activeTabsInWindow.get(tab.windowId).add(tab);
153-
}
154-
else {
155-
TabsStore.activeTabsInWindow.get(tab.windowId).delete(tab);
156-
}
154+
// Don't update indexes here, instead Window.prototype.trackTab()
155+
// updates indexes because indexes are bound to windows.
156+
// TabsStore.updateIndexesForTab(tab);
157157

158-
const incompletelyTrackedTabsPerWindow = mIncompletelyTrackedTabs.get(tab.windowId) || new Set();
159-
incompletelyTrackedTabsPerWindow.add(tab);
160-
mIncompletelyTrackedTabs.set(tab.windowId, incompletelyTrackedTabsPerWindow);
161-
this.promisedUniqueId.then(() => {
162-
incompletelyTrackedTabsPerWindow.delete(tab);
163-
Tab.onTracked.dispatch(tab);
164-
});
158+
if (tab.active) {
159+
TabsStore.activeTabInWindow.set(tab.windowId, tab);
160+
TabsStore.activeTabsInWindow.get(tab.windowId).add(tab);
161+
}
162+
else {
163+
TabsStore.activeTabsInWindow.get(tab.windowId).delete(tab);
164+
}
165+
166+
const incompletelyTrackedTabsPerWindow = mIncompletelyTrackedTabs.get(tab.windowId) || new Set();
167+
incompletelyTrackedTabsPerWindow.add(tab);
168+
mIncompletelyTrackedTabs.set(tab.windowId, incompletelyTrackedTabsPerWindow);
169+
this.promisedUniqueId.then(() => {
170+
incompletelyTrackedTabsPerWindow.delete(tab);
171+
Tab.onTracked.dispatch(tab);
172+
});
173+
}
165174

166175
// We should initialize private properties with blank value for better performance with a fixed shape.
167176
this.delayedInheritSoundStateFromChildren = null;
168177
}
169178

170179
destroy() {
171-
mPromisedTrackedTabs.delete(`${this.id}:true`);
172-
mPromisedTrackedTabs.delete(`${this.id}:false`);
180+
if (this.tab) {
181+
mPromisedTrackedTabs.delete(`${this.id}:true`);
182+
mPromisedTrackedTabs.delete(`${this.id}:false`);
173183

174-
Tab.onDestroyed.dispatch(this.tab);
175-
this.detach();
184+
Tab.onDestroyed.dispatch(this.tab);
185+
this.detach();
176186

177-
if (this.temporaryMetadata.has('reservedCleanupNeedlessGroupTab')) {
178-
clearTimeout(this.temporaryMetadata.get('reservedCleanupNeedlessGroupTab'));
179-
this.temporaryMetadata.delete('reservedCleanupNeedlessGroupTab');
180-
}
187+
if (this.temporaryMetadata.has('reservedCleanupNeedlessGroupTab')) {
188+
clearTimeout(this.temporaryMetadata.get('reservedCleanupNeedlessGroupTab'));
189+
this.temporaryMetadata.delete('reservedCleanupNeedlessGroupTab');
190+
}
181191

182-
TabsStore.tabs.delete(this.id);
183-
if (this.uniqueId)
184-
TabsStore.tabsByUniqueId.delete(this.uniqueId.id);
192+
TabsStore.tabs.delete(this.id);
193+
if (this.uniqueId)
194+
TabsStore.tabsByUniqueId.delete(this.uniqueId.id);
185195

186-
TabsStore.removeTabFromIndexes(this.tab);
196+
TabsStore.removeTabFromIndexes(this.tab);
197+
}
187198

188199
if (this.element &&
189200
this.element.parentNode)
190201
this.element.parentNode.removeChild(this.element);
191202
this.unbindElement();
192-
// this.tab.$TST = null; // tab.$TST is used by destruction processes.
193-
this.tab = null;
203+
// this.raw.$TST = null; // raw.$TST is used by destruction processes.
204+
this.raw = null;
194205
this.promisedUniqueId = null;
195206
this.uniqueId = null;
196207
this.destroyed = true;
@@ -287,6 +298,28 @@ export default class Tab {
287298
});
288299
}
289300

301+
get type() {
302+
return 'color' in this.raw ? 'group' : 'tab';
303+
}
304+
305+
get renderingId() {
306+
return `${this.type}:${this.id}`;
307+
}
308+
309+
get tab() {
310+
if ('color' in this.raw) { // native tab group
311+
return null;
312+
}
313+
return this.raw;
314+
}
315+
316+
get rawGroup() {
317+
if ('color' in this.raw) {
318+
return this.raw;
319+
}
320+
return null;
321+
}
322+
290323

291324
//===================================================================
292325
// status of tab
@@ -596,7 +629,7 @@ export default class Tab {
596629
}
597630

598631
get defaultTooltipText() {
599-
return this.cookieStoreName ? `${this.tab.title} - ${this.cookieStoreName}` : this.tab.title;
632+
return this.cookieStoreName ? `${this.raw.title} - ${this.cookieStoreName}` : this.raw.title;
600633
}
601634

602635
generateTooltipText() {
@@ -617,12 +650,12 @@ export default class Tab {
617650
return this.cookieStoreName ?
618651
`<span class="title-line"
619652
><span class="title"
620-
>${sanitizeForHTMLText(this.tab.title)}</span
653+
>${sanitizeForHTMLText(this.raw.title)}</span
621654
><span class="cookieStoreName"
622655
>${sanitizeForHTMLText(this.cookieStoreName)}</span></span>` :
623656
`<span class="title-line"
624657
><span class="title"
625-
>${sanitizeForHTMLText(this.tab.title)}</span></span>`;
658+
>${sanitizeForHTMLText(this.raw.title)}</span></span>`;
626659
}
627660

628661
generateTooltipHtmlWithDescendants() {
@@ -1919,6 +1952,19 @@ export default class Tab {
19191952
}
19201953

19211954

1955+
onNativeGroupModified() {
1956+
if (this.tab.groupId == -1) {
1957+
TabsStore.removeNativelyGroupedTab(this.tab);
1958+
}
1959+
else {
1960+
TabsStore.addNativelyGroupedTab(this.tab);
1961+
}
1962+
1963+
const win = TabsStore.windows.get(this.tab.windowId);
1964+
win.updateNativeTabGroupItem(this.tab.groupId);
1965+
}
1966+
1967+
19221968
setAttribute(attribute, value) {
19231969
if (this.element)
19241970
this.element.setAttribute(attribute, value);
@@ -2910,12 +2956,31 @@ Tab.getSelectedTabs = (windowId = null, options = {}) => {
29102956
};
29112957

29122958
Tab.getVirtualScrollRenderableTabs = (windowId = null) => {
2913-
return TabsStore.queryAll({
2959+
const tabs = TabsStore.queryAll({
29142960
windowId,
29152961
tabs: TabsStore.getTabsMap(TabsStore.virtualScrollRenderableTabsInWindow, windowId),
29162962
skipMatching: true,
29172963
ordered: true,
29182964
});
2965+
if (TabsStore.nativelyGroupedTabsInWindow.get(windowId).size == 0) {
2966+
return tabs;
2967+
}
2968+
2969+
const win = TabsStore.windows.get(windowId);
2970+
const mixedTabs = [];
2971+
let previousGroupId = -1;
2972+
for (const tab of tabs) {
2973+
if (tab.groupId != previousGroupId &&
2974+
tab.groupId != -1) {
2975+
mixedTabs.push(win.tabGroups.get(tab.groupId));
2976+
//console.log('Tab.getVirtualScrollRenderableTabs: inserted group item, ', mixedTabs[mixedTabs.length-1]);
2977+
}
2978+
mixedTabs.push(tab);
2979+
previousGroupId = tab.groupId;
2980+
}
2981+
//console.log('Tab.getVirtualScrollRenderableTabs: ', mixedTabs);
2982+
2983+
return mixedTabs;
29192984
};
29202985

29212986
Tab.getNeedToBeSynchronizedTabs = (windowId = null, options = {}) => {
@@ -2992,6 +3057,16 @@ Tab.getRecycledTabs = (windowId = null, options = {}) => {
29923057
});
29933058
};
29943059

3060+
Tab.getNativeGroupMemberTabs = (windowId = null, groupId, options = {}) => {
3061+
return TabsStore.queryAll({
3062+
windowId,
3063+
tabs: TabsStore.getTabsMap(TabsStore.nativelyGroupedTabsInWindow, windowId),
3064+
living: true,
3065+
groupId,
3066+
...options
3067+
});
3068+
};
3069+
29953070

29963071
//===================================================================
29973072
// general tab events

webextensions/common/Window.js

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ export default class Window {
2929

3030
this.id = windowId;
3131
this.tabs = new Map();
32-
this.tabGroups = new Map((tabGroups || []).map(group => [group.id, group]));
32+
this.tabGroups = new Map();
33+
if (tabGroups) {
34+
this.initTabGroups(tabGroups);
35+
}
3336
this.order = [];
3437

3538
this.containerElement = null;
@@ -76,6 +79,10 @@ export default class Window {
7679
// We should initialize private properties with blank value for better performance with a fixed shape.
7780
this.delayedDestroy = null;
7881
}
82+
initTabGroups(tabGroups) {
83+
log(`initializing tabGroups of window ${this.id}: `, tabGroups);
84+
this.tabGroups = new Map((tabGroups || []).map(group => [group.id, (new Tab(group), group)]));
85+
}
7986

8087
destroy() {
8188
for (const tab of this.tabs.values()) {
@@ -278,15 +285,34 @@ export default class Window {
278285
}
279286
return {
280287
tabs,
281-
tabGroups: [...this.tabGroups.values()],
288+
tabGroups: [...this.tabGroups.values()].map(group => ({ ...group, $TST: null })),
282289
};
283290
}
291+
292+
updateNativeTabGroupItem(groupId) {
293+
const group = this.tabGroups.get(groupId);
294+
const members = Tab.getNativeGroupMemberTabs(this.id, groupId);
295+
log('updateNativeTabGroupItem: ', group, members);
296+
297+
if (members.length == 0) {
298+
return;
299+
}
300+
301+
const maybeNativeGroupTab = members[0].$TST.unsafePreviousTab;
302+
if (maybeNativeGroupTab?.rawGroup?.id == groupId) {
303+
log('updateNativeTabGroupItem: native tab group item exists: ', maybeNativeGroupTab);
304+
return;
305+
}
306+
}
284307
}
285308

286309
Window.onInitialized = new EventListenerManager();
287310

288311
Window.init = (windowId, tabGroups) => {
289312
const win = TabsStore.windows.get(windowId) || new Window(windowId, tabGroups);
313+
if (tabGroups && tabGroups.size != win.tabGroups.size) {
314+
win.initTabGroups(tabGroups);
315+
}
290316
Window.onInitialized.dispatch(win);
291317
return win;
292318
}

0 commit comments

Comments
 (0)