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