• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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