1// Copyright (C) 2024 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import {DetailsPanel} from '../public/details_panel'; 16import {TabDescriptor, TabManager} from '../public/tab'; 17import { 18 SplitPanelDrawerVisibility, 19 toggleVisibility, 20} from '../widgets/split_panel'; 21 22export interface ResolvedTab { 23 uri: string; 24 tab?: TabDescriptor; 25} 26 27export type TabPanelVisibility = 'COLLAPSED' | 'VISIBLE' | 'FULLSCREEN'; 28 29/** 30 * Stores tab & current selection section registries. 31 * Keeps track of tab lifecycles. 32 */ 33export class TabManagerImpl implements TabManager, Disposable { 34 private _registry = new Map<string, TabDescriptor>(); 35 private _defaultTabs = new Set<string>(); 36 private _detailsPanelRegistry = new Set<DetailsPanel>(); 37 private _instantiatedTabs = new Map<string, TabDescriptor>(); 38 private _openTabs: string[] = []; // URIs of the tabs open. 39 private _currentTab: string = 'current_selection'; 40 private _tabPanelVisibility = SplitPanelDrawerVisibility.COLLAPSED; 41 private _tabPanelVisibilityChanged = false; 42 43 [Symbol.dispose]() { 44 // Dispose of all tabs that are currently alive 45 for (const tab of this._instantiatedTabs.values()) { 46 this.disposeTab(tab); 47 } 48 this._instantiatedTabs.clear(); 49 } 50 51 registerTab(desc: TabDescriptor): Disposable { 52 this._registry.set(desc.uri, desc); 53 return { 54 [Symbol.dispose]: () => this._registry.delete(desc.uri), 55 }; 56 } 57 58 addDefaultTab(uri: string): Disposable { 59 this._defaultTabs.add(uri); 60 return { 61 [Symbol.dispose]: () => this._defaultTabs.delete(uri), 62 }; 63 } 64 65 resolveTab(uri: string): TabDescriptor | undefined { 66 return this._registry.get(uri); 67 } 68 69 showCurrentSelectionTab(): void { 70 this.showTab('current_selection'); 71 } 72 73 showTab(uri: string): void { 74 // Add tab, unless we're talking about the special current_selection tab 75 if (uri !== 'current_selection') { 76 // Add tab to tab list if not already 77 if (!this._openTabs.some((x) => x === uri)) { 78 this._openTabs.push(uri); 79 } 80 } 81 this._currentTab = uri; 82 83 // The first time that we show a tab, auto-expand the tab bottom panel. 84 // However, if the user has later collapsed the panel (hence if 85 // _tabPanelVisibilityChanged == true), don't insist and leave things as 86 // they are. 87 if ( 88 !this._tabPanelVisibilityChanged && 89 this._tabPanelVisibility === SplitPanelDrawerVisibility.COLLAPSED 90 ) { 91 this.setTabPanelVisibility(SplitPanelDrawerVisibility.VISIBLE); 92 } 93 } 94 95 // Hide a tab in the tab bar pick a new tab to show. 96 // Note: Attempting to hide the "current_selection" tab doesn't work. This tab 97 // is special and cannot be removed. 98 hideTab(uri: string): void { 99 // If the removed tab is the "current" tab, we must find a new tab to focus 100 if (uri === this._currentTab) { 101 // Remember the index of the current tab 102 const currentTabIdx = this._openTabs.findIndex((x) => x === uri); 103 104 // Remove the tab 105 this._openTabs = this._openTabs.filter((x) => x !== uri); 106 107 if (currentTabIdx !== -1) { 108 if (this._openTabs.length === 0) { 109 // No more tabs, use current selection 110 this._currentTab = 'current_selection'; 111 } else if (currentTabIdx < this._openTabs.length - 1) { 112 // Pick the tab to the right 113 this._currentTab = this._openTabs[currentTabIdx]; 114 } else { 115 // Pick the last tab 116 const lastTab = this._openTabs[this._openTabs.length - 1]; 117 this._currentTab = lastTab; 118 } 119 } 120 } else { 121 // Otherwise just remove the tab 122 this._openTabs = this._openTabs.filter((x) => x !== uri); 123 } 124 } 125 126 toggleTab(uri: string): void { 127 return this.isOpen(uri) ? this.hideTab(uri) : this.showTab(uri); 128 } 129 130 isOpen(uri: string): boolean { 131 return this._openTabs.find((x) => x == uri) !== undefined; 132 } 133 134 get currentTabUri(): string { 135 return this._currentTab; 136 } 137 138 get openTabsUri(): string[] { 139 return this._openTabs; 140 } 141 142 get tabs(): TabDescriptor[] { 143 return Array.from(this._registry.values()); 144 } 145 146 get defaultTabs(): string[] { 147 return Array.from(this._defaultTabs); 148 } 149 150 get detailsPanels(): DetailsPanel[] { 151 return Array.from(this._detailsPanelRegistry); 152 } 153 154 /** 155 * Resolves a list of URIs to tabs and manages tab lifecycles. 156 * @param tabUris List of tabs. 157 * @returns List of resolved tabs. 158 */ 159 resolveTabs(tabUris: string[]): ResolvedTab[] { 160 // Refresh the list of old tabs 161 const newTabs = new Map<string, TabDescriptor>(); 162 const tabs: ResolvedTab[] = []; 163 164 tabUris.forEach((uri) => { 165 const newTab = this._registry.get(uri); 166 tabs.push({uri, tab: newTab}); 167 168 if (newTab) { 169 newTabs.set(uri, newTab); 170 } 171 }); 172 173 // Call onShow() on any new tabs. 174 for (const [uri, tab] of newTabs) { 175 const oldTab = this._instantiatedTabs.get(uri); 176 if (!oldTab) { 177 this.initTab(tab); 178 } 179 } 180 181 // Call onHide() on any tabs that have been removed. 182 for (const [uri, tab] of this._instantiatedTabs) { 183 const newTab = newTabs.get(uri); 184 if (!newTab) { 185 this.disposeTab(tab); 186 } 187 } 188 189 this._instantiatedTabs = newTabs; 190 191 return tabs; 192 } 193 194 setTabPanelVisibility(visibility: SplitPanelDrawerVisibility): void { 195 this._tabPanelVisibility = visibility; 196 this._tabPanelVisibilityChanged = true; 197 } 198 199 toggleTabPanelVisibility(): void { 200 this.setTabPanelVisibility(toggleVisibility(this._tabPanelVisibility)); 201 } 202 203 get tabPanelVisibility() { 204 return this._tabPanelVisibility; 205 } 206 207 /** 208 * Call onShow() on this tab. 209 * @param tab The tab to initialize. 210 */ 211 private initTab(tab: TabDescriptor): void { 212 tab.onShow?.(); 213 } 214 215 /** 216 * Call onHide() and maybe remove from registry if tab is ephemeral. 217 * @param tab The tab to dispose. 218 */ 219 private disposeTab(tab: TabDescriptor): void { 220 // Attempt to call onHide 221 tab.onHide?.(); 222 223 // If ephemeral, also unregister the tab 224 if (tab.isEphemeral) { 225 this._registry.delete(tab.uri); 226 } 227 } 228} 229