• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2022 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 m from 'mithril';
16import {v4 as uuidv4} from 'uuid';
17
18import {Actions} from '../common/actions';
19import {EngineProxy} from '../common/engine';
20import {Registry} from '../common/registry';
21import {globals} from './globals';
22
23import {Panel, PanelSize, PanelVNode} from './panel';
24
25export interface NewBottomTabArgs {
26  engine: EngineProxy;
27  tag?: string;
28  uuid: string;
29  config: {};
30}
31
32// Interface for allowing registration and creation of bottom tabs.
33// See comments on |TrackCreator| for more details.
34export interface BottomTabCreator {
35  readonly kind: string;
36
37  create(args: NewBottomTabArgs): BottomTab;
38}
39
40export const bottomTabRegistry = Registry.kindRegistry<BottomTabCreator>();
41
42// Period to wait for the newly-added tabs which are loading before showing
43// them to the user. This period is short enough to not be user-visible,
44// while being long enough for most of the simple queries to complete, reducing
45// flickering in the UI.
46const NEW_LOADING_TAB_DELAY_MS = 50;
47
48// An interface representing a bottom tab displayed on the panel in the bottom
49// of the ui (e.g. "Current Selection").
50//
51// The implementations of this class are provided by different plugins, which
52// register the implementations with bottomTabRegistry, keyed by a unique name
53// for each type of BottomTab.
54//
55// Lifetime: the instances of this class are owned by BottomTabPanel and exist
56// for as long as a tab header is shown to the user in the bottom tab list (with
57// minor exceptions, like a small grace period between when the tab is related).
58//
59// BottomTab implementations should pass the unique identifier(s) for the
60// content displayed via the |Config| and fetch additional details via Engine
61// instead of relying on getting the data from the global storage. For example,
62// for tabs corresponding to details of the selected objects on a track, a new
63// BottomTab should be created for each new selection.
64export abstract class BottomTabBase<Config = {}> {
65  // Config for this details panel. Should be serializable.
66  protected readonly config: Config;
67  // Engine for running queries and fetching additional data.
68  protected readonly engine: EngineProxy;
69  // Optional tag, which is used to ensure that only one tab
70  // with the same tag can exist - adding a new tab with the same tag
71  // (e.g. 'current_selection') would close the previous one. This
72  // also can be used to close existing tab.
73  readonly tag?: string;
74  // Unique id for this details panel. Can be used to close previously opened
75  // panel.
76  readonly uuid: string;
77
78  constructor(args: NewBottomTabArgs) {
79    this.config = args.config as Config;
80    this.engine = args.engine;
81    this.tag = args.tag;
82    this.uuid = args.uuid;
83  }
84
85  // Entry point for customisation of the displayed title for this panel.
86  abstract getTitle(): string;
87
88  // Generate a mithril node for this component.
89  abstract createPanelVnode(): PanelVNode;
90
91  // API for the tab to notify the TabList that it's still preparing the data.
92  // If true, adding a new tab will be delayed for a short while (~50ms) to
93  // reduce the flickering.
94  //
95  // Note: it's a "poll" rather than "push" API: there is no explicit API
96  // for the tabs to notify the tab list, as the tabs are expected to schedule
97  // global redraw anyway and the tab list will poll the tabs as necessary
98  // during the redraw.
99  isLoading(): boolean {
100    return false;
101  }
102}
103
104
105// BottomTabBase provides a more generic API allowing users to provide their
106// custom mithril component, which would allow them to listen to mithril
107// lifecycle events. Most cases, however, don't need them and BottomTab
108// provides a simplified API for the common case.
109export abstract class BottomTab<Config = {}> extends BottomTabBase<Config> {
110  constructor(args: NewBottomTabArgs) {
111    super(args);
112  }
113
114  // These methods are direct counterparts to renderCanvas and view with
115  // slightly changes names to prevent cases when `BottomTab` will
116  // be accidentally used a mithril component.
117  abstract renderTabCanvas(ctx: CanvasRenderingContext2D, size: PanelSize):
118      void;
119  abstract viewTab(): void|m.Children;
120
121  createPanelVnode(): m.Vnode<any, any> {
122    return m(
123        BottomTabAdapter,
124        {key: this.uuid, panel: this} as BottomTabAdapterAttrs);
125  }
126}
127
128interface BottomTabAdapterAttrs {
129  panel: BottomTab;
130}
131
132class BottomTabAdapter extends Panel<BottomTabAdapterAttrs> {
133  renderCanvas(
134      ctx: CanvasRenderingContext2D, size: PanelSize,
135      vnode: PanelVNode<BottomTabAdapterAttrs>): void {
136    vnode.attrs.panel.renderTabCanvas(ctx, size);
137  }
138
139  view(vnode: m.CVnode<BottomTabAdapterAttrs>): void|m.Children {
140    return vnode.attrs.panel.viewTab();
141  }
142}
143
144export type AddTabArgs = {
145  kind: string,
146  config: {},
147  tag?: string,
148  // Whether to make the new tab current. True by default.
149  select?: boolean;
150};
151
152export type AddTabResult =
153    {
154      uuid: string;
155    }
156
157// Shorthand for globals.bottomTabList.addTab(...) & redraw.
158// Ignored when bottomTabList does not exist (e.g. no trace is open in the UI).
159export function
160addTab(args: AddTabArgs) {
161  const tabList = globals.bottomTabList;
162  if (!tabList) {
163    return;
164  }
165  tabList.addTab(args);
166  globals.rafScheduler.scheduleFullRedraw();
167}
168
169
170// Shorthand for globals.bottomTabList.closeTabById(...) & redraw.
171// Ignored when bottomTabList does not exist (e.g. no trace is open in the UI).
172export function
173closeTab(uuid: string) {
174  const tabList = globals.bottomTabList;
175  if (!tabList) {
176    return;
177  }
178  tabList.closeTabById(uuid);
179  globals.rafScheduler.scheduleFullRedraw();
180}
181
182interface PendingTab {
183  tab: BottomTabBase, args: AddTabArgs, startTime: number,
184}
185
186function tabSelectionKey(tab: BottomTabBase) {
187  return tab.tag ?? tab.uuid;
188}
189
190export class BottomTabList {
191  private tabs: BottomTabBase[] = [];
192  private pendingTabs: PendingTab[] = [];
193  private engine: EngineProxy;
194  private scheduledFlushSetTimeoutId?: number;
195
196  constructor(engine: EngineProxy) {
197    this.engine = engine;
198  }
199
200  getTabs(): BottomTabBase[] {
201    this.flushPendingTabs();
202    return this.tabs;
203  }
204
205  // Add and create a new panel with given kind and config, replacing an
206  // existing panel with the same tag if needed. Returns the uuid of a newly
207  // created panel (which can be used in the future to close it).
208  addTab(args: AddTabArgs): AddTabResult {
209    const uuid = uuidv4();
210    const newPanel = bottomTabRegistry.get(args.kind).create({
211      engine: this.engine,
212      uuid,
213      config: args.config,
214      tag: args.tag,
215    });
216
217    this.pendingTabs.push({
218      tab: newPanel,
219      args,
220      startTime: window.performance.now(),
221    });
222    this.flushPendingTabs();
223
224    return {
225      uuid,
226    };
227  }
228
229  closeTabByTag(tag: string) {
230    const index = this.tabs.findIndex((tab) => tab.tag === tag);
231    if (index !== -1) {
232      this.removeTabAtIndex(index);
233    }
234    // User closing a tab by tag should affect pending tabs as well, as these
235    // tabs were requested to be added to the tab list before this call.
236    this.pendingTabs = this.pendingTabs.filter(({tab}) => tab.tag !== tag);
237  }
238
239  closeTabById(uuid: string) {
240    const index = this.tabs.findIndex((tab) => tab.uuid === uuid);
241    if (index !== -1) {
242      this.removeTabAtIndex(index);
243    }
244    // User closing a tab by id should affect pending tabs as well, as these
245    // tabs were requested to be added to the tab list before this call.
246    this.pendingTabs = this.pendingTabs.filter(({tab}) => tab.uuid !== uuid);
247  }
248
249  private removeTabAtIndex(index: number) {
250    const tab = this.tabs[index];
251    this.tabs.splice(index, 1);
252    // If the current tab was closed, select the tab to the right of it.
253    // If the closed tab was current and last in the tab list, select the tab
254    // that became last.
255    if (tab.uuid === globals.state.currentTab && this.tabs.length > 0) {
256      const newActiveIndex = index === this.tabs.length ? index - 1 : index;
257      globals.dispatch(Actions.setCurrentTab(
258          {tab: tabSelectionKey(this.tabs[newActiveIndex])}));
259    }
260    globals.rafScheduler.scheduleFullRedraw();
261  }
262
263  // Check the list of the pending tabs and add the ones that are ready
264  // (either tab.isLoading returns false or NEW_LOADING_TAB_DELAY_MS ms elapsed
265  // since this tab was added).
266  // Note: the pending tabs are stored in a queue to preserve the action order,
267  // which matters for cases like adding tabs with the same tag.
268  private flushPendingTabs() {
269    const currentTime = window.performance.now();
270    while (this.pendingTabs.length > 0) {
271      const {tab, args, startTime} = this.pendingTabs[0];
272
273      // This is a dirty hack^W^W low-lift solution for the world where some
274      // "current selection" panels are implemented by BottomTabs and some by
275      // details_panel.ts computing vnodes dynamically. Naive implementation
276      // will: a) stop showing the old panel (because
277      // globals.state.currentSelection changes). b) not showing the new
278      // 'current_selection' tab yet. This will result in temporary shifting
279      // focus to another tab (as no tab with 'current_selection' tag will
280      // exist).
281      //
282      // To counteract this, short-circuit this logic and when:
283      // a) no tag with 'current_selection' tag exists in the list of currently
284      // displayed tabs and b) we are adding a tab with 'current_selection' tag.
285      // add it immediately without waiting.
286      // TODO(altimin): Remove this once all places have switched to be using
287      // BottomTab to display panels.
288      const currentSelectionTabAlreadyExists =
289          this.tabs.filter((tab) => tab.tag === 'current_selection').length > 0;
290      const dirtyHackForCurrentSelectionApplies =
291          tab.tag === 'current_selection' && !currentSelectionTabAlreadyExists;
292
293      const elapsedTimeMs = currentTime - startTime;
294      if (tab.isLoading() && elapsedTimeMs < NEW_LOADING_TAB_DELAY_MS &&
295          !dirtyHackForCurrentSelectionApplies) {
296        this.schedulePendingTabsFlush(NEW_LOADING_TAB_DELAY_MS - elapsedTimeMs);
297        // The first tab is not ready yet, wait.
298        return;
299      }
300      this.pendingTabs.shift();
301
302      const index =
303          args.tag ? this.tabs.findIndex((tab) => tab.tag === args.tag) : -1;
304      if (index === -1) {
305        this.tabs.push(tab);
306      } else {
307        this.tabs[index] = tab;
308      }
309
310      if (args.select === undefined || args.select === true) {
311        globals.dispatch(Actions.setCurrentTab({tab: tabSelectionKey(tab)}));
312      }
313    }
314  }
315
316  private schedulePendingTabsFlush(waitTimeMs: number) {
317    if (this.scheduledFlushSetTimeoutId) {
318      // The flush is already pending, no action is required.
319      return;
320    }
321    setTimeout(() => {
322      this.scheduledFlushSetTimeoutId = undefined;
323      this.flushPendingTabs();
324    }, waitTimeMs);
325  }
326}
327