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