xref: /aosp_15_r20/external/perfetto/ui/src/core/omnibox_manager.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
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 {OmniboxManager, PromptChoices} from '../public/omnibox';
16import {raf} from './raf_scheduler';
17
18export enum OmniboxMode {
19  Search,
20  Query,
21  Command,
22  Prompt,
23}
24
25interface Prompt {
26  text: string;
27  options?: ReadonlyArray<{key: string; displayName: string}>;
28  resolve(result: unknown): void;
29}
30
31const defaultMode = OmniboxMode.Search;
32
33export class OmniboxManagerImpl implements OmniboxManager {
34  private _mode = defaultMode;
35  private _focusOmniboxNextRender = false;
36  private _pendingCursorPlacement?: number;
37  private _pendingPrompt?: Prompt;
38  private _omniboxSelectionIndex = 0;
39  private _forceShortTextSearch = false;
40  private _textForMode = new Map<OmniboxMode, string>();
41  private _statusMessageContainer: {msg?: string} = {};
42
43  get mode(): OmniboxMode {
44    return this._mode;
45  }
46
47  get pendingPrompt(): Prompt | undefined {
48    return this._pendingPrompt;
49  }
50
51  get text(): string {
52    return this._textForMode.get(this._mode) ?? '';
53  }
54
55  get selectionIndex(): number {
56    return this._omniboxSelectionIndex;
57  }
58
59  get focusOmniboxNextRender(): boolean {
60    return this._focusOmniboxNextRender;
61  }
62
63  get pendingCursorPlacement(): number | undefined {
64    return this._pendingCursorPlacement;
65  }
66
67  get forceShortTextSearch() {
68    return this._forceShortTextSearch;
69  }
70
71  setText(value: string): void {
72    this._textForMode.set(this._mode, value);
73  }
74
75  setSelectionIndex(index: number): void {
76    this._omniboxSelectionIndex = index;
77  }
78
79  focus(cursorPlacement?: number): void {
80    this._focusOmniboxNextRender = true;
81    this._pendingCursorPlacement = cursorPlacement;
82    raf.scheduleFullRedraw();
83  }
84
85  clearFocusFlag(): void {
86    this._focusOmniboxNextRender = false;
87    this._pendingCursorPlacement = undefined;
88  }
89
90  setMode(mode: OmniboxMode, focus = true): void {
91    this._mode = mode;
92    this._focusOmniboxNextRender = focus;
93    this._omniboxSelectionIndex = 0;
94    this.rejectPendingPrompt();
95    raf.scheduleFullRedraw();
96  }
97
98  showStatusMessage(msg: string, durationMs = 2000) {
99    const statusMessageContainer: {msg?: string} = {msg};
100    if (durationMs > 0) {
101      setTimeout(() => {
102        statusMessageContainer.msg = undefined;
103        raf.scheduleFullRedraw();
104      }, durationMs);
105    }
106    this._statusMessageContainer = statusMessageContainer;
107    raf.scheduleFullRedraw();
108  }
109
110  get statusMessage(): string | undefined {
111    return this._statusMessageContainer.msg;
112  }
113
114  // Start a prompt. If options are supplied, the user must pick one from the
115  // list, otherwise the input is free-form text.
116  prompt(text: string): Promise<string | undefined>;
117  prompt(
118    text: string,
119    options?: ReadonlyArray<string>,
120  ): Promise<string | undefined>;
121  prompt<T>(text: string, options?: PromptChoices<T>): Promise<T | undefined>;
122  prompt<T>(
123    text: string,
124    choices?: ReadonlyArray<string> | PromptChoices<T>,
125  ): Promise<string | T | undefined> {
126    this._mode = OmniboxMode.Prompt;
127    this._omniboxSelectionIndex = 0;
128    this.rejectPendingPrompt();
129    this._focusOmniboxNextRender = true;
130    raf.scheduleFullRedraw();
131
132    if (choices && 'getName' in choices) {
133      return new Promise<T | undefined>((resolve) => {
134        const choiceMap = new Map(
135          choices.values.map((choice) => [choices.getName(choice), choice]),
136        );
137        this._pendingPrompt = {
138          text,
139          options: Array.from(choiceMap.keys()).map((key) => ({
140            key,
141            displayName: key,
142          })),
143          resolve: (key: string) => resolve(choiceMap.get(key)),
144        };
145      });
146    }
147
148    return new Promise<string | undefined>((resolve) => {
149      this._pendingPrompt = {
150        text,
151        options: choices?.map((value) => ({key: value, displayName: value})),
152        resolve,
153      };
154    });
155  }
156
157  // Resolve the pending prompt with a value to return to the prompter.
158  resolvePrompt(value: string): void {
159    if (this._pendingPrompt) {
160      this._pendingPrompt.resolve(value);
161      this._pendingPrompt = undefined;
162    }
163    this.setMode(OmniboxMode.Search);
164  }
165
166  // Reject the prompt outright. Doing this will force the owner of the prompt
167  // promise to catch, so only do this when things go seriously wrong.
168  // Use |resolvePrompt(null)| to indicate cancellation.
169  rejectPrompt(): void {
170    this.rejectPendingPrompt();
171    this.setMode(OmniboxMode.Search);
172  }
173
174  reset(focus = true): void {
175    this.setMode(defaultMode, focus);
176    this._omniboxSelectionIndex = 0;
177    this._statusMessageContainer = {};
178    raf.scheduleFullRedraw();
179  }
180
181  private rejectPendingPrompt() {
182    if (this._pendingPrompt) {
183      this._pendingPrompt.resolve(undefined);
184      this._pendingPrompt = undefined;
185    }
186  }
187}
188