• 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 {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  }
83
84  clearFocusFlag(): void {
85    this._focusOmniboxNextRender = false;
86    this._pendingCursorPlacement = undefined;
87  }
88
89  setMode(mode: OmniboxMode, focus = true): void {
90    this._mode = mode;
91    this._focusOmniboxNextRender = focus;
92    this._omniboxSelectionIndex = 0;
93    this.rejectPendingPrompt();
94  }
95
96  showStatusMessage(msg: string, durationMs = 2000) {
97    const statusMessageContainer: {msg?: string} = {msg};
98    if (durationMs > 0) {
99      setTimeout(() => {
100        statusMessageContainer.msg = undefined;
101        raf.scheduleFullRedraw();
102      }, durationMs);
103    }
104    this._statusMessageContainer = statusMessageContainer;
105  }
106
107  get statusMessage(): string | undefined {
108    return this._statusMessageContainer.msg;
109  }
110
111  // Start a prompt. If options are supplied, the user must pick one from the
112  // list, otherwise the input is free-form text.
113  prompt(text: string): Promise<string | undefined>;
114  prompt(
115    text: string,
116    options?: ReadonlyArray<string>,
117  ): Promise<string | undefined>;
118  prompt<T>(text: string, options?: PromptChoices<T>): Promise<T | undefined>;
119  prompt<T>(
120    text: string,
121    choices?: ReadonlyArray<string> | PromptChoices<T>,
122  ): Promise<string | T | undefined> {
123    this._mode = OmniboxMode.Prompt;
124    this._omniboxSelectionIndex = 0;
125    this.rejectPendingPrompt();
126    this._focusOmniboxNextRender = true;
127
128    if (choices && 'getName' in choices) {
129      return new Promise<T | undefined>((resolve) => {
130        const choiceMap = new Map(
131          choices.values.map((choice) => [choices.getName(choice), choice]),
132        );
133        this._pendingPrompt = {
134          text,
135          options: Array.from(choiceMap.keys()).map((key) => ({
136            key,
137            displayName: key,
138          })),
139          resolve: (key: string) => resolve(choiceMap.get(key)),
140        };
141      });
142    }
143
144    return new Promise<string | undefined>((resolve) => {
145      this._pendingPrompt = {
146        text,
147        options: choices?.map((value) => ({key: value, displayName: value})),
148        resolve,
149      };
150    });
151  }
152
153  // Resolve the pending prompt with a value to return to the prompter.
154  resolvePrompt(value: string): void {
155    if (this._pendingPrompt) {
156      this._pendingPrompt.resolve(value);
157      this._pendingPrompt = undefined;
158    }
159    this.setMode(OmniboxMode.Search);
160  }
161
162  // Reject the prompt outright. Doing this will force the owner of the prompt
163  // promise to catch, so only do this when things go seriously wrong.
164  // Use |resolvePrompt(null)| to indicate cancellation.
165  rejectPrompt(): void {
166    this.rejectPendingPrompt();
167    this.setMode(OmniboxMode.Search);
168  }
169
170  reset(focus = true): void {
171    this.setMode(defaultMode, focus);
172    this._omniboxSelectionIndex = 0;
173    this._statusMessageContainer = {};
174  }
175
176  private rejectPendingPrompt() {
177    if (this._pendingPrompt) {
178      this._pendingPrompt.resolve(undefined);
179      this._pendingPrompt = undefined;
180    }
181  }
182}
183