xref: /aosp_15_r20/external/perfetto/ui/src/public/workspace.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1*6dbdd20aSAndroid Build Coastguard Worker// Copyright (C) 2024 The Android Open Source Project
2*6dbdd20aSAndroid Build Coastguard Worker//
3*6dbdd20aSAndroid Build Coastguard Worker// Licensed under the Apache License, Version 2.0 (the "License");
4*6dbdd20aSAndroid Build Coastguard Worker// you may not use this file except in compliance with the License.
5*6dbdd20aSAndroid Build Coastguard Worker// You may obtain a copy of the License at
6*6dbdd20aSAndroid Build Coastguard Worker//
7*6dbdd20aSAndroid Build Coastguard Worker//      http://www.apache.org/licenses/LICENSE-2.0
8*6dbdd20aSAndroid Build Coastguard Worker//
9*6dbdd20aSAndroid Build Coastguard Worker// Unless required by applicable law or agreed to in writing, software
10*6dbdd20aSAndroid Build Coastguard Worker// distributed under the License is distributed on an "AS IS" BASIS,
11*6dbdd20aSAndroid Build Coastguard Worker// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12*6dbdd20aSAndroid Build Coastguard Worker// See the License for the specific language governing permissions and
13*6dbdd20aSAndroid Build Coastguard Worker// limitations under the License.
14*6dbdd20aSAndroid Build Coastguard Worker
15*6dbdd20aSAndroid Build Coastguard Workerimport {assertTrue} from '../base/logging';
16*6dbdd20aSAndroid Build Coastguard Worker
17*6dbdd20aSAndroid Build Coastguard Workerexport interface WorkspaceManager {
18*6dbdd20aSAndroid Build Coastguard Worker  // This is the same of ctx.workspace, exposed for consistency also here.
19*6dbdd20aSAndroid Build Coastguard Worker  readonly currentWorkspace: Workspace;
20*6dbdd20aSAndroid Build Coastguard Worker  readonly all: ReadonlyArray<Workspace>;
21*6dbdd20aSAndroid Build Coastguard Worker  createEmptyWorkspace(displayName: string): Workspace;
22*6dbdd20aSAndroid Build Coastguard Worker  switchWorkspace(workspace: Workspace): void;
23*6dbdd20aSAndroid Build Coastguard Worker}
24*6dbdd20aSAndroid Build Coastguard Worker
25*6dbdd20aSAndroid Build Coastguard Workerlet sessionUniqueIdCounter = 0;
26*6dbdd20aSAndroid Build Coastguard Worker
27*6dbdd20aSAndroid Build Coastguard Worker/**
28*6dbdd20aSAndroid Build Coastguard Worker * Creates a short ID which is unique to this instance of the UI.
29*6dbdd20aSAndroid Build Coastguard Worker *
30*6dbdd20aSAndroid Build Coastguard Worker * The advantage of using this over uuidv4() is that the ids produced are
31*6dbdd20aSAndroid Build Coastguard Worker * significantly shorter, saving memory and making them more human
32*6dbdd20aSAndroid Build Coastguard Worker * read/write-able which helps when debugging.
33*6dbdd20aSAndroid Build Coastguard Worker *
34*6dbdd20aSAndroid Build Coastguard Worker * Note: The ID range will reset every time the UI is restarted, so be careful
35*6dbdd20aSAndroid Build Coastguard Worker * not rely on these IDs in any medium that can survive between UI instances.
36*6dbdd20aSAndroid Build Coastguard Worker *
37*6dbdd20aSAndroid Build Coastguard Worker * TODO(stevegolton): We could possibly move this into its own module and use it
38*6dbdd20aSAndroid Build Coastguard Worker * everywhere where session-unique ids are required.
39*6dbdd20aSAndroid Build Coastguard Worker */
40*6dbdd20aSAndroid Build Coastguard Workerfunction createSessionUniqueId(): string {
41*6dbdd20aSAndroid Build Coastguard Worker  // Return the counter in base36 (0-z) to keep the string as short as possible
42*6dbdd20aSAndroid Build Coastguard Worker  // but still human readable.
43*6dbdd20aSAndroid Build Coastguard Worker  return (sessionUniqueIdCounter++).toString(36);
44*6dbdd20aSAndroid Build Coastguard Worker}
45*6dbdd20aSAndroid Build Coastguard Worker
46*6dbdd20aSAndroid Build Coastguard Worker/**
47*6dbdd20aSAndroid Build Coastguard Worker * Describes generic parent track node functionality - i.e. any entity that can
48*6dbdd20aSAndroid Build Coastguard Worker * contain child TrackNodes, providing methods to add, remove, and access child
49*6dbdd20aSAndroid Build Coastguard Worker * nodes.
50*6dbdd20aSAndroid Build Coastguard Worker *
51*6dbdd20aSAndroid Build Coastguard Worker * This class is abstract because, while it can technically be instantiated on
52*6dbdd20aSAndroid Build Coastguard Worker * its own (no abstract methods/properties), it can't and shouldn't be
53*6dbdd20aSAndroid Build Coastguard Worker * instantiated anywhere in practice - all APIs require either a TrackNode or a
54*6dbdd20aSAndroid Build Coastguard Worker * Workspace.
55*6dbdd20aSAndroid Build Coastguard Worker *
56*6dbdd20aSAndroid Build Coastguard Worker * Thus, it serves two purposes:
57*6dbdd20aSAndroid Build Coastguard Worker * 1. Avoiding duplication between Workspace and TrackNode, which is an internal
58*6dbdd20aSAndroid Build Coastguard Worker *    implementation detail of this module.
59*6dbdd20aSAndroid Build Coastguard Worker * 2. Providing a typescript interface for a generic TrackNode container class,
60*6dbdd20aSAndroid Build Coastguard Worker *    which otherwise you might have to achieve using `Workspace | TrackNode`
61*6dbdd20aSAndroid Build Coastguard Worker *    which is uglier.
62*6dbdd20aSAndroid Build Coastguard Worker *
63*6dbdd20aSAndroid Build Coastguard Worker * If you find yourself using this as a Javascript class in external code, e.g.
64*6dbdd20aSAndroid Build Coastguard Worker * `instance of TrackNodeContainer`, you're probably doing something wrong.
65*6dbdd20aSAndroid Build Coastguard Worker */
66*6dbdd20aSAndroid Build Coastguard Worker
67*6dbdd20aSAndroid Build Coastguard Workerexport interface TrackNodeArgs {
68*6dbdd20aSAndroid Build Coastguard Worker  title: string;
69*6dbdd20aSAndroid Build Coastguard Worker  id: string;
70*6dbdd20aSAndroid Build Coastguard Worker  uri: string;
71*6dbdd20aSAndroid Build Coastguard Worker  headless: boolean;
72*6dbdd20aSAndroid Build Coastguard Worker  sortOrder: number;
73*6dbdd20aSAndroid Build Coastguard Worker  collapsed: boolean;
74*6dbdd20aSAndroid Build Coastguard Worker  isSummary: boolean;
75*6dbdd20aSAndroid Build Coastguard Worker  removable: boolean;
76*6dbdd20aSAndroid Build Coastguard Worker}
77*6dbdd20aSAndroid Build Coastguard Worker
78*6dbdd20aSAndroid Build Coastguard Worker/**
79*6dbdd20aSAndroid Build Coastguard Worker * A base class for any node with children (i.e. a group or a workspace).
80*6dbdd20aSAndroid Build Coastguard Worker */
81*6dbdd20aSAndroid Build Coastguard Workerexport class TrackNode {
82*6dbdd20aSAndroid Build Coastguard Worker  // Immutable unique (within the workspace) ID of this track node. Used for
83*6dbdd20aSAndroid Build Coastguard Worker  // efficiently retrieving this node object from a workspace. Note: This is
84*6dbdd20aSAndroid Build Coastguard Worker  // different to |uri| which is used to reference a track to render on the
85*6dbdd20aSAndroid Build Coastguard Worker  // track. If this means nothing to you, don't bother using it.
86*6dbdd20aSAndroid Build Coastguard Worker  public readonly id: string;
87*6dbdd20aSAndroid Build Coastguard Worker
88*6dbdd20aSAndroid Build Coastguard Worker  // A human readable string for this track - displayed in the track shell.
89*6dbdd20aSAndroid Build Coastguard Worker  // TODO(stevegolton): Make this optional, so that if we implement a string for
90*6dbdd20aSAndroid Build Coastguard Worker  // this track then we can implement it here as well.
91*6dbdd20aSAndroid Build Coastguard Worker  public title: string;
92*6dbdd20aSAndroid Build Coastguard Worker
93*6dbdd20aSAndroid Build Coastguard Worker  // The URI of the track content to display here.
94*6dbdd20aSAndroid Build Coastguard Worker  public uri?: string;
95*6dbdd20aSAndroid Build Coastguard Worker
96*6dbdd20aSAndroid Build Coastguard Worker  // Optional sort order, which workspaces may or may not take advantage of for
97*6dbdd20aSAndroid Build Coastguard Worker  // sorting when displaying the workspace.
98*6dbdd20aSAndroid Build Coastguard Worker  public sortOrder?: number;
99*6dbdd20aSAndroid Build Coastguard Worker
100*6dbdd20aSAndroid Build Coastguard Worker  // Don't show the header at all for this track, just show its un-nested
101*6dbdd20aSAndroid Build Coastguard Worker  // children. This is helpful to group together tracks that logically belong to
102*6dbdd20aSAndroid Build Coastguard Worker  // the same group (e.g. all ftrace cpu tracks) and ease the job of
103*6dbdd20aSAndroid Build Coastguard Worker  // sorting/grouping plugins.
104*6dbdd20aSAndroid Build Coastguard Worker  public headless: boolean;
105*6dbdd20aSAndroid Build Coastguard Worker
106*6dbdd20aSAndroid Build Coastguard Worker  // If true, this track is to be used as a summary for its children. When the
107*6dbdd20aSAndroid Build Coastguard Worker  // group is expanded the track will become sticky to the top of the viewport
108*6dbdd20aSAndroid Build Coastguard Worker  // to provide context for the tracks within, and the content of this track
109*6dbdd20aSAndroid Build Coastguard Worker  // shall be omitted. It will also be squashed down to a smaller height to save
110*6dbdd20aSAndroid Build Coastguard Worker  // vertical space.
111*6dbdd20aSAndroid Build Coastguard Worker  public isSummary: boolean;
112*6dbdd20aSAndroid Build Coastguard Worker
113*6dbdd20aSAndroid Build Coastguard Worker  // If true, this node will be removable by the user. It will show a little
114*6dbdd20aSAndroid Build Coastguard Worker  // close button in the track shell which the user can press to remove the
115*6dbdd20aSAndroid Build Coastguard Worker  // track from the workspace.
116*6dbdd20aSAndroid Build Coastguard Worker  public removable: boolean;
117*6dbdd20aSAndroid Build Coastguard Worker
118*6dbdd20aSAndroid Build Coastguard Worker  protected _collapsed = true;
119*6dbdd20aSAndroid Build Coastguard Worker  protected _children: Array<TrackNode> = [];
120*6dbdd20aSAndroid Build Coastguard Worker  protected readonly tracksById = new Map<string, TrackNode>();
121*6dbdd20aSAndroid Build Coastguard Worker  protected readonly tracksByUri = new Map<string, TrackNode>();
122*6dbdd20aSAndroid Build Coastguard Worker  private _parent?: TrackNode;
123*6dbdd20aSAndroid Build Coastguard Worker  public _workspace?: Workspace;
124*6dbdd20aSAndroid Build Coastguard Worker
125*6dbdd20aSAndroid Build Coastguard Worker  get parent(): TrackNode | undefined {
126*6dbdd20aSAndroid Build Coastguard Worker    return this._parent;
127*6dbdd20aSAndroid Build Coastguard Worker  }
128*6dbdd20aSAndroid Build Coastguard Worker
129*6dbdd20aSAndroid Build Coastguard Worker  constructor(args?: Partial<TrackNodeArgs>) {
130*6dbdd20aSAndroid Build Coastguard Worker    const {
131*6dbdd20aSAndroid Build Coastguard Worker      title = '',
132*6dbdd20aSAndroid Build Coastguard Worker      id = createSessionUniqueId(),
133*6dbdd20aSAndroid Build Coastguard Worker      uri,
134*6dbdd20aSAndroid Build Coastguard Worker      headless = false,
135*6dbdd20aSAndroid Build Coastguard Worker      sortOrder,
136*6dbdd20aSAndroid Build Coastguard Worker      collapsed = true,
137*6dbdd20aSAndroid Build Coastguard Worker      isSummary = false,
138*6dbdd20aSAndroid Build Coastguard Worker      removable = false,
139*6dbdd20aSAndroid Build Coastguard Worker    } = args ?? {};
140*6dbdd20aSAndroid Build Coastguard Worker
141*6dbdd20aSAndroid Build Coastguard Worker    this.id = id;
142*6dbdd20aSAndroid Build Coastguard Worker    this.uri = uri;
143*6dbdd20aSAndroid Build Coastguard Worker    this.headless = headless;
144*6dbdd20aSAndroid Build Coastguard Worker    this.title = title;
145*6dbdd20aSAndroid Build Coastguard Worker    this.sortOrder = sortOrder;
146*6dbdd20aSAndroid Build Coastguard Worker    this.isSummary = isSummary;
147*6dbdd20aSAndroid Build Coastguard Worker    this._collapsed = collapsed;
148*6dbdd20aSAndroid Build Coastguard Worker    this.removable = removable;
149*6dbdd20aSAndroid Build Coastguard Worker  }
150*6dbdd20aSAndroid Build Coastguard Worker
151*6dbdd20aSAndroid Build Coastguard Worker  /**
152*6dbdd20aSAndroid Build Coastguard Worker   * Remove this track from it's parent & unpin from the workspace if pinned.
153*6dbdd20aSAndroid Build Coastguard Worker   */
154*6dbdd20aSAndroid Build Coastguard Worker  remove(): void {
155*6dbdd20aSAndroid Build Coastguard Worker    this.workspace?.unpinTrack(this);
156*6dbdd20aSAndroid Build Coastguard Worker    this.parent?.removeChild(this);
157*6dbdd20aSAndroid Build Coastguard Worker  }
158*6dbdd20aSAndroid Build Coastguard Worker
159*6dbdd20aSAndroid Build Coastguard Worker  /**
160*6dbdd20aSAndroid Build Coastguard Worker   * Add this track to the list of pinned tracks in its parent workspace.
161*6dbdd20aSAndroid Build Coastguard Worker   *
162*6dbdd20aSAndroid Build Coastguard Worker   * Has no effect if this track is not added to a workspace.
163*6dbdd20aSAndroid Build Coastguard Worker   */
164*6dbdd20aSAndroid Build Coastguard Worker  pin(): void {
165*6dbdd20aSAndroid Build Coastguard Worker    this.workspace?.pinTrack(this);
166*6dbdd20aSAndroid Build Coastguard Worker  }
167*6dbdd20aSAndroid Build Coastguard Worker
168*6dbdd20aSAndroid Build Coastguard Worker  /**
169*6dbdd20aSAndroid Build Coastguard Worker   * Remove this track from the list of pinned tracks in its parent workspace.
170*6dbdd20aSAndroid Build Coastguard Worker   *
171*6dbdd20aSAndroid Build Coastguard Worker   * Has no effect if this track is not added to a workspace.
172*6dbdd20aSAndroid Build Coastguard Worker   */
173*6dbdd20aSAndroid Build Coastguard Worker  unpin(): void {
174*6dbdd20aSAndroid Build Coastguard Worker    this.workspace?.unpinTrack(this);
175*6dbdd20aSAndroid Build Coastguard Worker  }
176*6dbdd20aSAndroid Build Coastguard Worker
177*6dbdd20aSAndroid Build Coastguard Worker  /**
178*6dbdd20aSAndroid Build Coastguard Worker   * Returns true if this node is added to a workspace as is in the pinned track
179*6dbdd20aSAndroid Build Coastguard Worker   * list of that workspace.
180*6dbdd20aSAndroid Build Coastguard Worker   */
181*6dbdd20aSAndroid Build Coastguard Worker  get isPinned(): boolean {
182*6dbdd20aSAndroid Build Coastguard Worker    return Boolean(this.workspace?.hasPinnedTrack(this));
183*6dbdd20aSAndroid Build Coastguard Worker  }
184*6dbdd20aSAndroid Build Coastguard Worker
185*6dbdd20aSAndroid Build Coastguard Worker  /**
186*6dbdd20aSAndroid Build Coastguard Worker   * Find the closest visible ancestor TrackNode.
187*6dbdd20aSAndroid Build Coastguard Worker   *
188*6dbdd20aSAndroid Build Coastguard Worker   * Given the path from the root workspace to this node, find the fist one,
189*6dbdd20aSAndroid Build Coastguard Worker   * starting from the root, which is collapsed. This will be, from the user's
190*6dbdd20aSAndroid Build Coastguard Worker   * point of view, the closest ancestor of this node.
191*6dbdd20aSAndroid Build Coastguard Worker   *
192*6dbdd20aSAndroid Build Coastguard Worker   * Returns undefined if this node is actually visible.
193*6dbdd20aSAndroid Build Coastguard Worker   *
194*6dbdd20aSAndroid Build Coastguard Worker   * TODO(stevegolton): Should it return itself in this case?
195*6dbdd20aSAndroid Build Coastguard Worker   */
196*6dbdd20aSAndroid Build Coastguard Worker  findClosestVisibleAncestor(): TrackNode {
197*6dbdd20aSAndroid Build Coastguard Worker    // Build a path from the root workspace to this node
198*6dbdd20aSAndroid Build Coastguard Worker    const path: TrackNode[] = [];
199*6dbdd20aSAndroid Build Coastguard Worker    let node = this.parent;
200*6dbdd20aSAndroid Build Coastguard Worker    while (node) {
201*6dbdd20aSAndroid Build Coastguard Worker      path.unshift(node);
202*6dbdd20aSAndroid Build Coastguard Worker      node = node.parent;
203*6dbdd20aSAndroid Build Coastguard Worker    }
204*6dbdd20aSAndroid Build Coastguard Worker
205*6dbdd20aSAndroid Build Coastguard Worker    // Find the first collapsed track in the path starting from the root. This
206*6dbdd20aSAndroid Build Coastguard Worker    // is effectively the closest we can get to this node without expanding any
207*6dbdd20aSAndroid Build Coastguard Worker    // groups.
208*6dbdd20aSAndroid Build Coastguard Worker    return path.find((node) => node.collapsed) ?? this;
209*6dbdd20aSAndroid Build Coastguard Worker  }
210*6dbdd20aSAndroid Build Coastguard Worker
211*6dbdd20aSAndroid Build Coastguard Worker  /**
212*6dbdd20aSAndroid Build Coastguard Worker   * Expand all ancestor nodes.
213*6dbdd20aSAndroid Build Coastguard Worker   */
214*6dbdd20aSAndroid Build Coastguard Worker  reveal(): void {
215*6dbdd20aSAndroid Build Coastguard Worker    let parent = this.parent;
216*6dbdd20aSAndroid Build Coastguard Worker    while (parent) {
217*6dbdd20aSAndroid Build Coastguard Worker      parent.expand();
218*6dbdd20aSAndroid Build Coastguard Worker      parent = parent.parent;
219*6dbdd20aSAndroid Build Coastguard Worker    }
220*6dbdd20aSAndroid Build Coastguard Worker  }
221*6dbdd20aSAndroid Build Coastguard Worker
222*6dbdd20aSAndroid Build Coastguard Worker  /**
223*6dbdd20aSAndroid Build Coastguard Worker   * Find this node's root node - this may be a workspace or another node.
224*6dbdd20aSAndroid Build Coastguard Worker   */
225*6dbdd20aSAndroid Build Coastguard Worker  get rootNode(): TrackNode {
226*6dbdd20aSAndroid Build Coastguard Worker    let node: TrackNode = this;
227*6dbdd20aSAndroid Build Coastguard Worker    while (node.parent) {
228*6dbdd20aSAndroid Build Coastguard Worker      node = node.parent;
229*6dbdd20aSAndroid Build Coastguard Worker    }
230*6dbdd20aSAndroid Build Coastguard Worker    return node;
231*6dbdd20aSAndroid Build Coastguard Worker  }
232*6dbdd20aSAndroid Build Coastguard Worker
233*6dbdd20aSAndroid Build Coastguard Worker  /**
234*6dbdd20aSAndroid Build Coastguard Worker   * Find this node's workspace if it is attached to one.
235*6dbdd20aSAndroid Build Coastguard Worker   */
236*6dbdd20aSAndroid Build Coastguard Worker  get workspace(): Workspace | undefined {
237*6dbdd20aSAndroid Build Coastguard Worker    return this.rootNode._workspace;
238*6dbdd20aSAndroid Build Coastguard Worker  }
239*6dbdd20aSAndroid Build Coastguard Worker
240*6dbdd20aSAndroid Build Coastguard Worker  /**
241*6dbdd20aSAndroid Build Coastguard Worker   * Mark this node as un-collapsed, indicating its children should be rendered.
242*6dbdd20aSAndroid Build Coastguard Worker   */
243*6dbdd20aSAndroid Build Coastguard Worker  expand(): void {
244*6dbdd20aSAndroid Build Coastguard Worker    this._collapsed = false;
245*6dbdd20aSAndroid Build Coastguard Worker    this.fireOnChangeListener();
246*6dbdd20aSAndroid Build Coastguard Worker  }
247*6dbdd20aSAndroid Build Coastguard Worker
248*6dbdd20aSAndroid Build Coastguard Worker  /**
249*6dbdd20aSAndroid Build Coastguard Worker   * Mark this node as collapsed, indicating its children should not be
250*6dbdd20aSAndroid Build Coastguard Worker   * rendered.
251*6dbdd20aSAndroid Build Coastguard Worker   */
252*6dbdd20aSAndroid Build Coastguard Worker  collapse(): void {
253*6dbdd20aSAndroid Build Coastguard Worker    this._collapsed = true;
254*6dbdd20aSAndroid Build Coastguard Worker    this.fireOnChangeListener();
255*6dbdd20aSAndroid Build Coastguard Worker  }
256*6dbdd20aSAndroid Build Coastguard Worker
257*6dbdd20aSAndroid Build Coastguard Worker  /**
258*6dbdd20aSAndroid Build Coastguard Worker   * Toggle the collapsed state.
259*6dbdd20aSAndroid Build Coastguard Worker   */
260*6dbdd20aSAndroid Build Coastguard Worker  toggleCollapsed(): void {
261*6dbdd20aSAndroid Build Coastguard Worker    this._collapsed = !this._collapsed;
262*6dbdd20aSAndroid Build Coastguard Worker    this.fireOnChangeListener();
263*6dbdd20aSAndroid Build Coastguard Worker  }
264*6dbdd20aSAndroid Build Coastguard Worker
265*6dbdd20aSAndroid Build Coastguard Worker  /**
266*6dbdd20aSAndroid Build Coastguard Worker   * Whether this node is collapsed, indicating its children should be rendered.
267*6dbdd20aSAndroid Build Coastguard Worker   */
268*6dbdd20aSAndroid Build Coastguard Worker  get collapsed(): boolean {
269*6dbdd20aSAndroid Build Coastguard Worker    return this._collapsed;
270*6dbdd20aSAndroid Build Coastguard Worker  }
271*6dbdd20aSAndroid Build Coastguard Worker
272*6dbdd20aSAndroid Build Coastguard Worker  /**
273*6dbdd20aSAndroid Build Coastguard Worker   * Whether this node is expanded - i.e. not collapsed, indicating its children
274*6dbdd20aSAndroid Build Coastguard Worker   * should be rendered.
275*6dbdd20aSAndroid Build Coastguard Worker   */
276*6dbdd20aSAndroid Build Coastguard Worker  get expanded(): boolean {
277*6dbdd20aSAndroid Build Coastguard Worker    return !this._collapsed;
278*6dbdd20aSAndroid Build Coastguard Worker  }
279*6dbdd20aSAndroid Build Coastguard Worker
280*6dbdd20aSAndroid Build Coastguard Worker  /**
281*6dbdd20aSAndroid Build Coastguard Worker   * Returns the list of titles representing the full path from the root node to
282*6dbdd20aSAndroid Build Coastguard Worker   * the current node. This path consists only of node titles, workspaces are
283*6dbdd20aSAndroid Build Coastguard Worker   * omitted.
284*6dbdd20aSAndroid Build Coastguard Worker   */
285*6dbdd20aSAndroid Build Coastguard Worker  get fullPath(): ReadonlyArray<string> {
286*6dbdd20aSAndroid Build Coastguard Worker    let fullPath = [this.title];
287*6dbdd20aSAndroid Build Coastguard Worker    let parent = this.parent;
288*6dbdd20aSAndroid Build Coastguard Worker    while (parent) {
289*6dbdd20aSAndroid Build Coastguard Worker      // Ignore headless containers as they don't appear in the tree...
290*6dbdd20aSAndroid Build Coastguard Worker      if (!parent.headless && parent.title !== '') {
291*6dbdd20aSAndroid Build Coastguard Worker        fullPath = [parent.title, ...fullPath];
292*6dbdd20aSAndroid Build Coastguard Worker      }
293*6dbdd20aSAndroid Build Coastguard Worker      parent = parent.parent;
294*6dbdd20aSAndroid Build Coastguard Worker    }
295*6dbdd20aSAndroid Build Coastguard Worker    return fullPath;
296*6dbdd20aSAndroid Build Coastguard Worker  }
297*6dbdd20aSAndroid Build Coastguard Worker
298*6dbdd20aSAndroid Build Coastguard Worker  protected fireOnChangeListener(): void {
299*6dbdd20aSAndroid Build Coastguard Worker    this.workspace?.onchange(this.workspace);
300*6dbdd20aSAndroid Build Coastguard Worker  }
301*6dbdd20aSAndroid Build Coastguard Worker
302*6dbdd20aSAndroid Build Coastguard Worker  /**
303*6dbdd20aSAndroid Build Coastguard Worker   * True if this node has children, false otherwise.
304*6dbdd20aSAndroid Build Coastguard Worker   */
305*6dbdd20aSAndroid Build Coastguard Worker  get hasChildren(): boolean {
306*6dbdd20aSAndroid Build Coastguard Worker    return this._children.length > 0;
307*6dbdd20aSAndroid Build Coastguard Worker  }
308*6dbdd20aSAndroid Build Coastguard Worker
309*6dbdd20aSAndroid Build Coastguard Worker  /**
310*6dbdd20aSAndroid Build Coastguard Worker   * The ordered list of children belonging to this node.
311*6dbdd20aSAndroid Build Coastguard Worker   */
312*6dbdd20aSAndroid Build Coastguard Worker  get children(): ReadonlyArray<TrackNode> {
313*6dbdd20aSAndroid Build Coastguard Worker    return this._children;
314*6dbdd20aSAndroid Build Coastguard Worker  }
315*6dbdd20aSAndroid Build Coastguard Worker
316*6dbdd20aSAndroid Build Coastguard Worker  /**
317*6dbdd20aSAndroid Build Coastguard Worker   * Inserts a new child node considering it's sortOrder.
318*6dbdd20aSAndroid Build Coastguard Worker   *
319*6dbdd20aSAndroid Build Coastguard Worker   * The child will be added before the first child whose |sortOrder| is greater
320*6dbdd20aSAndroid Build Coastguard Worker   * than the child node's sort order, or at the end if one does not exist. If
321*6dbdd20aSAndroid Build Coastguard Worker   * |sortOrder| is omitted on either node in the comparison it is assumed to be
322*6dbdd20aSAndroid Build Coastguard Worker   * 0.
323*6dbdd20aSAndroid Build Coastguard Worker   *
324*6dbdd20aSAndroid Build Coastguard Worker   * @param child - The child node to add.
325*6dbdd20aSAndroid Build Coastguard Worker   */
326*6dbdd20aSAndroid Build Coastguard Worker  addChildInOrder(child: TrackNode): void {
327*6dbdd20aSAndroid Build Coastguard Worker    const insertPoint = this._children.find(
328*6dbdd20aSAndroid Build Coastguard Worker      (n) => (n.sortOrder ?? 0) > (child.sortOrder ?? 0),
329*6dbdd20aSAndroid Build Coastguard Worker    );
330*6dbdd20aSAndroid Build Coastguard Worker    if (insertPoint) {
331*6dbdd20aSAndroid Build Coastguard Worker      this.addChildBefore(child, insertPoint);
332*6dbdd20aSAndroid Build Coastguard Worker    } else {
333*6dbdd20aSAndroid Build Coastguard Worker      this.addChildLast(child);
334*6dbdd20aSAndroid Build Coastguard Worker    }
335*6dbdd20aSAndroid Build Coastguard Worker  }
336*6dbdd20aSAndroid Build Coastguard Worker
337*6dbdd20aSAndroid Build Coastguard Worker  /**
338*6dbdd20aSAndroid Build Coastguard Worker   * Add a new child node at the start of the list of children.
339*6dbdd20aSAndroid Build Coastguard Worker   *
340*6dbdd20aSAndroid Build Coastguard Worker   * @param child The new child node to add.
341*6dbdd20aSAndroid Build Coastguard Worker   */
342*6dbdd20aSAndroid Build Coastguard Worker  addChildLast(child: TrackNode): void {
343*6dbdd20aSAndroid Build Coastguard Worker    this.adopt(child);
344*6dbdd20aSAndroid Build Coastguard Worker    this._children.push(child);
345*6dbdd20aSAndroid Build Coastguard Worker    this.fireOnChangeListener();
346*6dbdd20aSAndroid Build Coastguard Worker  }
347*6dbdd20aSAndroid Build Coastguard Worker
348*6dbdd20aSAndroid Build Coastguard Worker  /**
349*6dbdd20aSAndroid Build Coastguard Worker   * Add a new child node at the end of the list of children.
350*6dbdd20aSAndroid Build Coastguard Worker   *
351*6dbdd20aSAndroid Build Coastguard Worker   * @param child The child node to add.
352*6dbdd20aSAndroid Build Coastguard Worker   */
353*6dbdd20aSAndroid Build Coastguard Worker  addChildFirst(child: TrackNode): void {
354*6dbdd20aSAndroid Build Coastguard Worker    this.adopt(child);
355*6dbdd20aSAndroid Build Coastguard Worker    this._children.unshift(child);
356*6dbdd20aSAndroid Build Coastguard Worker    this.fireOnChangeListener();
357*6dbdd20aSAndroid Build Coastguard Worker  }
358*6dbdd20aSAndroid Build Coastguard Worker
359*6dbdd20aSAndroid Build Coastguard Worker  /**
360*6dbdd20aSAndroid Build Coastguard Worker   * Add a new child node before an existing child node.
361*6dbdd20aSAndroid Build Coastguard Worker   *
362*6dbdd20aSAndroid Build Coastguard Worker   * @param child The child node to add.
363*6dbdd20aSAndroid Build Coastguard Worker   * @param referenceNode An existing child node. The new node will be added
364*6dbdd20aSAndroid Build Coastguard Worker   * before this node.
365*6dbdd20aSAndroid Build Coastguard Worker   */
366*6dbdd20aSAndroid Build Coastguard Worker  addChildBefore(child: TrackNode, referenceNode: TrackNode): void {
367*6dbdd20aSAndroid Build Coastguard Worker    if (child === referenceNode) return;
368*6dbdd20aSAndroid Build Coastguard Worker
369*6dbdd20aSAndroid Build Coastguard Worker    assertTrue(this.children.includes(referenceNode));
370*6dbdd20aSAndroid Build Coastguard Worker
371*6dbdd20aSAndroid Build Coastguard Worker    this.adopt(child);
372*6dbdd20aSAndroid Build Coastguard Worker
373*6dbdd20aSAndroid Build Coastguard Worker    const indexOfReference = this.children.indexOf(referenceNode);
374*6dbdd20aSAndroid Build Coastguard Worker    this._children.splice(indexOfReference, 0, child);
375*6dbdd20aSAndroid Build Coastguard Worker    this.fireOnChangeListener();
376*6dbdd20aSAndroid Build Coastguard Worker  }
377*6dbdd20aSAndroid Build Coastguard Worker
378*6dbdd20aSAndroid Build Coastguard Worker  /**
379*6dbdd20aSAndroid Build Coastguard Worker   * Add a new child node after an existing child node.
380*6dbdd20aSAndroid Build Coastguard Worker   *
381*6dbdd20aSAndroid Build Coastguard Worker   * @param child The child node to add.
382*6dbdd20aSAndroid Build Coastguard Worker   * @param referenceNode An existing child node. The new node will be added
383*6dbdd20aSAndroid Build Coastguard Worker   * after this node.
384*6dbdd20aSAndroid Build Coastguard Worker   */
385*6dbdd20aSAndroid Build Coastguard Worker  addChildAfter(child: TrackNode, referenceNode: TrackNode): void {
386*6dbdd20aSAndroid Build Coastguard Worker    if (child === referenceNode) return;
387*6dbdd20aSAndroid Build Coastguard Worker
388*6dbdd20aSAndroid Build Coastguard Worker    assertTrue(this.children.includes(referenceNode));
389*6dbdd20aSAndroid Build Coastguard Worker
390*6dbdd20aSAndroid Build Coastguard Worker    this.adopt(child);
391*6dbdd20aSAndroid Build Coastguard Worker
392*6dbdd20aSAndroid Build Coastguard Worker    const indexOfReference = this.children.indexOf(referenceNode);
393*6dbdd20aSAndroid Build Coastguard Worker    this._children.splice(indexOfReference + 1, 0, child);
394*6dbdd20aSAndroid Build Coastguard Worker    this.fireOnChangeListener();
395*6dbdd20aSAndroid Build Coastguard Worker  }
396*6dbdd20aSAndroid Build Coastguard Worker
397*6dbdd20aSAndroid Build Coastguard Worker  /**
398*6dbdd20aSAndroid Build Coastguard Worker   * Remove a child node from this node.
399*6dbdd20aSAndroid Build Coastguard Worker   *
400*6dbdd20aSAndroid Build Coastguard Worker   * @param child The child node to remove.
401*6dbdd20aSAndroid Build Coastguard Worker   */
402*6dbdd20aSAndroid Build Coastguard Worker  removeChild(child: TrackNode): void {
403*6dbdd20aSAndroid Build Coastguard Worker    this._children = this.children.filter((x) => child !== x);
404*6dbdd20aSAndroid Build Coastguard Worker    child._parent = undefined;
405*6dbdd20aSAndroid Build Coastguard Worker    this.removeFromIndex(child);
406*6dbdd20aSAndroid Build Coastguard Worker    this.propagateRemoval(child);
407*6dbdd20aSAndroid Build Coastguard Worker    this.fireOnChangeListener();
408*6dbdd20aSAndroid Build Coastguard Worker  }
409*6dbdd20aSAndroid Build Coastguard Worker
410*6dbdd20aSAndroid Build Coastguard Worker  /**
411*6dbdd20aSAndroid Build Coastguard Worker   * The flattened list of all descendent nodes in depth first order.
412*6dbdd20aSAndroid Build Coastguard Worker   *
413*6dbdd20aSAndroid Build Coastguard Worker   * Use flatTracksUnordered if you don't care about track order, as it's more
414*6dbdd20aSAndroid Build Coastguard Worker   * efficient.
415*6dbdd20aSAndroid Build Coastguard Worker   */
416*6dbdd20aSAndroid Build Coastguard Worker  get flatTracksOrdered(): ReadonlyArray<TrackNode> {
417*6dbdd20aSAndroid Build Coastguard Worker    const tracks: TrackNode[] = [];
418*6dbdd20aSAndroid Build Coastguard Worker    this.collectFlatTracks(tracks);
419*6dbdd20aSAndroid Build Coastguard Worker    return tracks;
420*6dbdd20aSAndroid Build Coastguard Worker  }
421*6dbdd20aSAndroid Build Coastguard Worker
422*6dbdd20aSAndroid Build Coastguard Worker  private collectFlatTracks(tracks: TrackNode[]): void {
423*6dbdd20aSAndroid Build Coastguard Worker    for (let i = 0; i < this.children.length; ++i) {
424*6dbdd20aSAndroid Build Coastguard Worker      tracks.push(this.children[i]); // Push the current node before its children
425*6dbdd20aSAndroid Build Coastguard Worker      this.children[i].collectFlatTracks(tracks); // Recurse to collect child tracks
426*6dbdd20aSAndroid Build Coastguard Worker    }
427*6dbdd20aSAndroid Build Coastguard Worker  }
428*6dbdd20aSAndroid Build Coastguard Worker
429*6dbdd20aSAndroid Build Coastguard Worker  /**
430*6dbdd20aSAndroid Build Coastguard Worker   * The flattened list of all descendent nodes in no particular order.
431*6dbdd20aSAndroid Build Coastguard Worker   */
432*6dbdd20aSAndroid Build Coastguard Worker  get flatTracks(): ReadonlyArray<TrackNode> {
433*6dbdd20aSAndroid Build Coastguard Worker    return Array.from(this.tracksById.values());
434*6dbdd20aSAndroid Build Coastguard Worker  }
435*6dbdd20aSAndroid Build Coastguard Worker
436*6dbdd20aSAndroid Build Coastguard Worker  /**
437*6dbdd20aSAndroid Build Coastguard Worker   * Remove all children from this node.
438*6dbdd20aSAndroid Build Coastguard Worker   */
439*6dbdd20aSAndroid Build Coastguard Worker  clear(): void {
440*6dbdd20aSAndroid Build Coastguard Worker    this._children = [];
441*6dbdd20aSAndroid Build Coastguard Worker    this.tracksById.clear();
442*6dbdd20aSAndroid Build Coastguard Worker    this.fireOnChangeListener();
443*6dbdd20aSAndroid Build Coastguard Worker  }
444*6dbdd20aSAndroid Build Coastguard Worker
445*6dbdd20aSAndroid Build Coastguard Worker  /**
446*6dbdd20aSAndroid Build Coastguard Worker   * Find a track node by its id.
447*6dbdd20aSAndroid Build Coastguard Worker   *
448*6dbdd20aSAndroid Build Coastguard Worker   * Node: This is an O(1) operation.
449*6dbdd20aSAndroid Build Coastguard Worker   *
450*6dbdd20aSAndroid Build Coastguard Worker   * @param id The id of the node we want to find.
451*6dbdd20aSAndroid Build Coastguard Worker   * @returns The node or undefined if no such node exists.
452*6dbdd20aSAndroid Build Coastguard Worker   */
453*6dbdd20aSAndroid Build Coastguard Worker  getTrackById(id: string): TrackNode | undefined {
454*6dbdd20aSAndroid Build Coastguard Worker    return this.tracksById.get(id);
455*6dbdd20aSAndroid Build Coastguard Worker  }
456*6dbdd20aSAndroid Build Coastguard Worker
457*6dbdd20aSAndroid Build Coastguard Worker  /**
458*6dbdd20aSAndroid Build Coastguard Worker   * Find a track node via its URI.
459*6dbdd20aSAndroid Build Coastguard Worker   *
460*6dbdd20aSAndroid Build Coastguard Worker   * Node: This is an O(1) operation.
461*6dbdd20aSAndroid Build Coastguard Worker   *
462*6dbdd20aSAndroid Build Coastguard Worker   * @param uri The uri of the track to find.
463*6dbdd20aSAndroid Build Coastguard Worker   * @returns The node or undefined if no such node exists with this URI.
464*6dbdd20aSAndroid Build Coastguard Worker   */
465*6dbdd20aSAndroid Build Coastguard Worker  findTrackByUri(uri: string): TrackNode | undefined {
466*6dbdd20aSAndroid Build Coastguard Worker    return this.tracksByUri.get(uri);
467*6dbdd20aSAndroid Build Coastguard Worker  }
468*6dbdd20aSAndroid Build Coastguard Worker
469*6dbdd20aSAndroid Build Coastguard Worker  private adopt(child: TrackNode): void {
470*6dbdd20aSAndroid Build Coastguard Worker    if (child.parent) {
471*6dbdd20aSAndroid Build Coastguard Worker      child.parent.removeChild(child);
472*6dbdd20aSAndroid Build Coastguard Worker    }
473*6dbdd20aSAndroid Build Coastguard Worker    child._parent = this;
474*6dbdd20aSAndroid Build Coastguard Worker    this.addToIndex(child);
475*6dbdd20aSAndroid Build Coastguard Worker    this.propagateAddition(child);
476*6dbdd20aSAndroid Build Coastguard Worker  }
477*6dbdd20aSAndroid Build Coastguard Worker
478*6dbdd20aSAndroid Build Coastguard Worker  private addToIndex(child: TrackNode) {
479*6dbdd20aSAndroid Build Coastguard Worker    this.tracksById.set(child.id, child);
480*6dbdd20aSAndroid Build Coastguard Worker    for (const [id, node] of child.tracksById) {
481*6dbdd20aSAndroid Build Coastguard Worker      this.tracksById.set(id, node);
482*6dbdd20aSAndroid Build Coastguard Worker    }
483*6dbdd20aSAndroid Build Coastguard Worker
484*6dbdd20aSAndroid Build Coastguard Worker    child.uri && this.tracksByUri.set(child.uri, child);
485*6dbdd20aSAndroid Build Coastguard Worker    for (const [uri, node] of child.tracksByUri) {
486*6dbdd20aSAndroid Build Coastguard Worker      this.tracksByUri.set(uri, node);
487*6dbdd20aSAndroid Build Coastguard Worker    }
488*6dbdd20aSAndroid Build Coastguard Worker  }
489*6dbdd20aSAndroid Build Coastguard Worker
490*6dbdd20aSAndroid Build Coastguard Worker  private removeFromIndex(child: TrackNode) {
491*6dbdd20aSAndroid Build Coastguard Worker    this.tracksById.delete(child.id);
492*6dbdd20aSAndroid Build Coastguard Worker    for (const [id] of child.tracksById) {
493*6dbdd20aSAndroid Build Coastguard Worker      this.tracksById.delete(id);
494*6dbdd20aSAndroid Build Coastguard Worker    }
495*6dbdd20aSAndroid Build Coastguard Worker
496*6dbdd20aSAndroid Build Coastguard Worker    child.uri && this.tracksByUri.delete(child.uri);
497*6dbdd20aSAndroid Build Coastguard Worker    for (const [uri] of child.tracksByUri) {
498*6dbdd20aSAndroid Build Coastguard Worker      this.tracksByUri.delete(uri);
499*6dbdd20aSAndroid Build Coastguard Worker    }
500*6dbdd20aSAndroid Build Coastguard Worker  }
501*6dbdd20aSAndroid Build Coastguard Worker
502*6dbdd20aSAndroid Build Coastguard Worker  private propagateAddition(node: TrackNode): void {
503*6dbdd20aSAndroid Build Coastguard Worker    if (this.parent) {
504*6dbdd20aSAndroid Build Coastguard Worker      this.parent.addToIndex(node);
505*6dbdd20aSAndroid Build Coastguard Worker      this.parent.propagateAddition(node);
506*6dbdd20aSAndroid Build Coastguard Worker    }
507*6dbdd20aSAndroid Build Coastguard Worker  }
508*6dbdd20aSAndroid Build Coastguard Worker
509*6dbdd20aSAndroid Build Coastguard Worker  private propagateRemoval(node: TrackNode): void {
510*6dbdd20aSAndroid Build Coastguard Worker    if (this.parent) {
511*6dbdd20aSAndroid Build Coastguard Worker      this.parent.removeFromIndex(node);
512*6dbdd20aSAndroid Build Coastguard Worker      this.parent.propagateRemoval(node);
513*6dbdd20aSAndroid Build Coastguard Worker    }
514*6dbdd20aSAndroid Build Coastguard Worker  }
515*6dbdd20aSAndroid Build Coastguard Worker}
516*6dbdd20aSAndroid Build Coastguard Worker
517*6dbdd20aSAndroid Build Coastguard Worker/**
518*6dbdd20aSAndroid Build Coastguard Worker * Defines a workspace containing a track tree and a pinned area.
519*6dbdd20aSAndroid Build Coastguard Worker */
520*6dbdd20aSAndroid Build Coastguard Workerexport class Workspace {
521*6dbdd20aSAndroid Build Coastguard Worker  public title = '<untitled-workspace>';
522*6dbdd20aSAndroid Build Coastguard Worker  public readonly id: string;
523*6dbdd20aSAndroid Build Coastguard Worker  onchange: (w: Workspace) => void = () => {};
524*6dbdd20aSAndroid Build Coastguard Worker
525*6dbdd20aSAndroid Build Coastguard Worker  // Dummy node to contain the pinned tracks
526*6dbdd20aSAndroid Build Coastguard Worker  public readonly pinnedTracksNode = new TrackNode();
527*6dbdd20aSAndroid Build Coastguard Worker  public readonly tracks = new TrackNode();
528*6dbdd20aSAndroid Build Coastguard Worker
529*6dbdd20aSAndroid Build Coastguard Worker  get pinnedTracks(): ReadonlyArray<TrackNode> {
530*6dbdd20aSAndroid Build Coastguard Worker    return this.pinnedTracksNode.children;
531*6dbdd20aSAndroid Build Coastguard Worker  }
532*6dbdd20aSAndroid Build Coastguard Worker
533*6dbdd20aSAndroid Build Coastguard Worker  constructor() {
534*6dbdd20aSAndroid Build Coastguard Worker    this.id = createSessionUniqueId();
535*6dbdd20aSAndroid Build Coastguard Worker    this.pinnedTracksNode._workspace = this;
536*6dbdd20aSAndroid Build Coastguard Worker    this.tracks._workspace = this;
537*6dbdd20aSAndroid Build Coastguard Worker
538*6dbdd20aSAndroid Build Coastguard Worker    // Expanding these nodes makes the logic work
539*6dbdd20aSAndroid Build Coastguard Worker    this.pinnedTracksNode.expand();
540*6dbdd20aSAndroid Build Coastguard Worker    this.tracks.expand();
541*6dbdd20aSAndroid Build Coastguard Worker  }
542*6dbdd20aSAndroid Build Coastguard Worker
543*6dbdd20aSAndroid Build Coastguard Worker  /**
544*6dbdd20aSAndroid Build Coastguard Worker   * Reset the entire workspace including the pinned tracks.
545*6dbdd20aSAndroid Build Coastguard Worker   */
546*6dbdd20aSAndroid Build Coastguard Worker  clear(): void {
547*6dbdd20aSAndroid Build Coastguard Worker    this.pinnedTracksNode.clear();
548*6dbdd20aSAndroid Build Coastguard Worker    this.tracks.clear();
549*6dbdd20aSAndroid Build Coastguard Worker  }
550*6dbdd20aSAndroid Build Coastguard Worker
551*6dbdd20aSAndroid Build Coastguard Worker  /**
552*6dbdd20aSAndroid Build Coastguard Worker   * Adds a track node to this workspace's pinned area.
553*6dbdd20aSAndroid Build Coastguard Worker   */
554*6dbdd20aSAndroid Build Coastguard Worker  pinTrack(track: TrackNode): void {
555*6dbdd20aSAndroid Build Coastguard Worker    // Make a lightweight clone of this track - just the uri and the title.
556*6dbdd20aSAndroid Build Coastguard Worker    const cloned = new TrackNode({
557*6dbdd20aSAndroid Build Coastguard Worker      uri: track.uri,
558*6dbdd20aSAndroid Build Coastguard Worker      title: track.title,
559*6dbdd20aSAndroid Build Coastguard Worker      removable: track.removable,
560*6dbdd20aSAndroid Build Coastguard Worker    });
561*6dbdd20aSAndroid Build Coastguard Worker    this.pinnedTracksNode.addChildLast(cloned);
562*6dbdd20aSAndroid Build Coastguard Worker  }
563*6dbdd20aSAndroid Build Coastguard Worker
564*6dbdd20aSAndroid Build Coastguard Worker  /**
565*6dbdd20aSAndroid Build Coastguard Worker   * Removes a track node from this workspace's pinned area.
566*6dbdd20aSAndroid Build Coastguard Worker   */
567*6dbdd20aSAndroid Build Coastguard Worker  unpinTrack(track: TrackNode): void {
568*6dbdd20aSAndroid Build Coastguard Worker    const foundNode = this.pinnedTracksNode.children.find(
569*6dbdd20aSAndroid Build Coastguard Worker      (t) => t.uri === track.uri,
570*6dbdd20aSAndroid Build Coastguard Worker    );
571*6dbdd20aSAndroid Build Coastguard Worker    if (foundNode) {
572*6dbdd20aSAndroid Build Coastguard Worker      this.pinnedTracksNode.removeChild(foundNode);
573*6dbdd20aSAndroid Build Coastguard Worker    }
574*6dbdd20aSAndroid Build Coastguard Worker  }
575*6dbdd20aSAndroid Build Coastguard Worker
576*6dbdd20aSAndroid Build Coastguard Worker  /**
577*6dbdd20aSAndroid Build Coastguard Worker   * Check if this workspace has a pinned track with the same URI as |track|.
578*6dbdd20aSAndroid Build Coastguard Worker   */
579*6dbdd20aSAndroid Build Coastguard Worker  hasPinnedTrack(track: TrackNode): boolean {
580*6dbdd20aSAndroid Build Coastguard Worker    return this.pinnedTracksNode.flatTracks.some((p) => p.uri === track.uri);
581*6dbdd20aSAndroid Build Coastguard Worker  }
582*6dbdd20aSAndroid Build Coastguard Worker
583*6dbdd20aSAndroid Build Coastguard Worker  /**
584*6dbdd20aSAndroid Build Coastguard Worker   * Find a track node via its URI.
585*6dbdd20aSAndroid Build Coastguard Worker   *
586*6dbdd20aSAndroid Build Coastguard Worker   * Note: This in an O(N) operation where N is the number of nodes in the
587*6dbdd20aSAndroid Build Coastguard Worker   * workspace.
588*6dbdd20aSAndroid Build Coastguard Worker   *
589*6dbdd20aSAndroid Build Coastguard Worker   * @param uri The uri of the track to find.
590*6dbdd20aSAndroid Build Coastguard Worker   * @returns A reference to the track node if it exists in this workspace,
591*6dbdd20aSAndroid Build Coastguard Worker   * otherwise undefined.
592*6dbdd20aSAndroid Build Coastguard Worker   */
593*6dbdd20aSAndroid Build Coastguard Worker  findTrackByUri(uri: string): TrackNode | undefined {
594*6dbdd20aSAndroid Build Coastguard Worker    return this.tracks.flatTracks.find((t) => t.uri === uri);
595*6dbdd20aSAndroid Build Coastguard Worker  }
596*6dbdd20aSAndroid Build Coastguard Worker
597*6dbdd20aSAndroid Build Coastguard Worker  /**
598*6dbdd20aSAndroid Build Coastguard Worker   * Find a track by ID, also searching pinned tracks.
599*6dbdd20aSAndroid Build Coastguard Worker   */
600*6dbdd20aSAndroid Build Coastguard Worker  getTrackById(id: string): TrackNode | undefined {
601*6dbdd20aSAndroid Build Coastguard Worker    return (
602*6dbdd20aSAndroid Build Coastguard Worker      this.tracks.getTrackById(id) || this.pinnedTracksNode.getTrackById(id)
603*6dbdd20aSAndroid Build Coastguard Worker    );
604*6dbdd20aSAndroid Build Coastguard Worker  }
605*6dbdd20aSAndroid Build Coastguard Worker
606*6dbdd20aSAndroid Build Coastguard Worker  /**
607*6dbdd20aSAndroid Build Coastguard Worker   * The ordered list of children belonging to this node.
608*6dbdd20aSAndroid Build Coastguard Worker   */
609*6dbdd20aSAndroid Build Coastguard Worker  get children(): ReadonlyArray<TrackNode> {
610*6dbdd20aSAndroid Build Coastguard Worker    return this.tracks.children;
611*6dbdd20aSAndroid Build Coastguard Worker  }
612*6dbdd20aSAndroid Build Coastguard Worker
613*6dbdd20aSAndroid Build Coastguard Worker  /**
614*6dbdd20aSAndroid Build Coastguard Worker   * Inserts a new child node considering it's sortOrder.
615*6dbdd20aSAndroid Build Coastguard Worker   *
616*6dbdd20aSAndroid Build Coastguard Worker   * The child will be added before the first child whose |sortOrder| is greater
617*6dbdd20aSAndroid Build Coastguard Worker   * than the child node's sort order, or at the end if one does not exist. If
618*6dbdd20aSAndroid Build Coastguard Worker   * |sortOrder| is omitted on either node in the comparison it is assumed to be
619*6dbdd20aSAndroid Build Coastguard Worker   * 0.
620*6dbdd20aSAndroid Build Coastguard Worker   *
621*6dbdd20aSAndroid Build Coastguard Worker   * @param child - The child node to add.
622*6dbdd20aSAndroid Build Coastguard Worker   */
623*6dbdd20aSAndroid Build Coastguard Worker  addChildInOrder(child: TrackNode): void {
624*6dbdd20aSAndroid Build Coastguard Worker    this.tracks.addChildInOrder(child);
625*6dbdd20aSAndroid Build Coastguard Worker  }
626*6dbdd20aSAndroid Build Coastguard Worker
627*6dbdd20aSAndroid Build Coastguard Worker  /**
628*6dbdd20aSAndroid Build Coastguard Worker   * Add a new child node at the start of the list of children.
629*6dbdd20aSAndroid Build Coastguard Worker   *
630*6dbdd20aSAndroid Build Coastguard Worker   * @param child The new child node to add.
631*6dbdd20aSAndroid Build Coastguard Worker   */
632*6dbdd20aSAndroid Build Coastguard Worker  addChildLast(child: TrackNode): void {
633*6dbdd20aSAndroid Build Coastguard Worker    this.tracks.addChildLast(child);
634*6dbdd20aSAndroid Build Coastguard Worker  }
635*6dbdd20aSAndroid Build Coastguard Worker
636*6dbdd20aSAndroid Build Coastguard Worker  /**
637*6dbdd20aSAndroid Build Coastguard Worker   * Add a new child node at the end of the list of children.
638*6dbdd20aSAndroid Build Coastguard Worker   *
639*6dbdd20aSAndroid Build Coastguard Worker   * @param child The child node to add.
640*6dbdd20aSAndroid Build Coastguard Worker   */
641*6dbdd20aSAndroid Build Coastguard Worker  addChildFirst(child: TrackNode): void {
642*6dbdd20aSAndroid Build Coastguard Worker    this.tracks.addChildFirst(child);
643*6dbdd20aSAndroid Build Coastguard Worker  }
644*6dbdd20aSAndroid Build Coastguard Worker
645*6dbdd20aSAndroid Build Coastguard Worker  /**
646*6dbdd20aSAndroid Build Coastguard Worker   * Add a new child node before an existing child node.
647*6dbdd20aSAndroid Build Coastguard Worker   *
648*6dbdd20aSAndroid Build Coastguard Worker   * @param child The child node to add.
649*6dbdd20aSAndroid Build Coastguard Worker   * @param referenceNode An existing child node. The new node will be added
650*6dbdd20aSAndroid Build Coastguard Worker   * before this node.
651*6dbdd20aSAndroid Build Coastguard Worker   */
652*6dbdd20aSAndroid Build Coastguard Worker  addChildBefore(child: TrackNode, referenceNode: TrackNode): void {
653*6dbdd20aSAndroid Build Coastguard Worker    this.tracks.addChildBefore(child, referenceNode);
654*6dbdd20aSAndroid Build Coastguard Worker  }
655*6dbdd20aSAndroid Build Coastguard Worker
656*6dbdd20aSAndroid Build Coastguard Worker  /**
657*6dbdd20aSAndroid Build Coastguard Worker   * Add a new child node after an existing child node.
658*6dbdd20aSAndroid Build Coastguard Worker   *
659*6dbdd20aSAndroid Build Coastguard Worker   * @param child The child node to add.
660*6dbdd20aSAndroid Build Coastguard Worker   * @param referenceNode An existing child node. The new node will be added
661*6dbdd20aSAndroid Build Coastguard Worker   * after this node.
662*6dbdd20aSAndroid Build Coastguard Worker   */
663*6dbdd20aSAndroid Build Coastguard Worker  addChildAfter(child: TrackNode, referenceNode: TrackNode): void {
664*6dbdd20aSAndroid Build Coastguard Worker    this.tracks.addChildAfter(child, referenceNode);
665*6dbdd20aSAndroid Build Coastguard Worker  }
666*6dbdd20aSAndroid Build Coastguard Worker
667*6dbdd20aSAndroid Build Coastguard Worker  /**
668*6dbdd20aSAndroid Build Coastguard Worker   * Remove a child node from this node.
669*6dbdd20aSAndroid Build Coastguard Worker   *
670*6dbdd20aSAndroid Build Coastguard Worker   * @param child The child node to remove.
671*6dbdd20aSAndroid Build Coastguard Worker   */
672*6dbdd20aSAndroid Build Coastguard Worker  removeChild(child: TrackNode): void {
673*6dbdd20aSAndroid Build Coastguard Worker    this.tracks.removeChild(child);
674*6dbdd20aSAndroid Build Coastguard Worker  }
675*6dbdd20aSAndroid Build Coastguard Worker
676*6dbdd20aSAndroid Build Coastguard Worker  /**
677*6dbdd20aSAndroid Build Coastguard Worker   * The flattened list of all descendent nodes in depth first order.
678*6dbdd20aSAndroid Build Coastguard Worker   *
679*6dbdd20aSAndroid Build Coastguard Worker   * Use flatTracksUnordered if you don't care about track order, as it's more
680*6dbdd20aSAndroid Build Coastguard Worker   * efficient.
681*6dbdd20aSAndroid Build Coastguard Worker   */
682*6dbdd20aSAndroid Build Coastguard Worker  get flatTracksOrdered() {
683*6dbdd20aSAndroid Build Coastguard Worker    return this.tracks.flatTracksOrdered;
684*6dbdd20aSAndroid Build Coastguard Worker  }
685*6dbdd20aSAndroid Build Coastguard Worker
686*6dbdd20aSAndroid Build Coastguard Worker  /**
687*6dbdd20aSAndroid Build Coastguard Worker   * The flattened list of all descendent nodes in no particular order.
688*6dbdd20aSAndroid Build Coastguard Worker   */
689*6dbdd20aSAndroid Build Coastguard Worker  get flatTracks() {
690*6dbdd20aSAndroid Build Coastguard Worker    return this.tracks.flatTracks;
691*6dbdd20aSAndroid Build Coastguard Worker  }
692*6dbdd20aSAndroid Build Coastguard Worker}
693