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