1// Copyright (C) 2018 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 * as m from 'mithril'; 16 17import {Actions} from '../common/actions'; 18import {QueryResponse} from '../common/queries'; 19import {EngineConfig} from '../common/state'; 20 21import {globals} from './globals'; 22 23const QUERY_ID = 'quicksearch'; 24 25let selResult = 0; 26let numResults = 0; 27let mode: 'search'|'command' = 'search'; 28let omniboxValue = ''; 29 30function clearOmniboxResults() { 31 globals.queryResults.delete(QUERY_ID); 32 globals.dispatch(Actions.deleteQuery({queryId: QUERY_ID})); 33} 34 35function onKeyDown(e: Event) { 36 e.stopPropagation(); 37 const key = (e as KeyboardEvent).key; 38 39 // Avoid that the global 'a', 'd', 'w', 's' handler sees these keystrokes. 40 // TODO: this seems a bug in the pan_and_zoom_handler.ts. 41 if (key === 'ArrowUp' || key === 'ArrowDown') { 42 e.preventDefault(); 43 return; 44 } 45 const txt = (e.target as HTMLInputElement); 46 omniboxValue = txt.value; 47 if (key === ':' && txt.value === '') { 48 mode = 'command'; 49 globals.rafScheduler.scheduleFullRedraw(); 50 e.preventDefault(); 51 return; 52 } 53 if (key === 'Escape' && mode === 'command') { 54 txt.value = ''; 55 mode = 'search'; 56 globals.rafScheduler.scheduleFullRedraw(); 57 return; 58 } 59 if (key === 'Backspace' && txt.value.length === 0 && mode === 'command') { 60 mode = 'search'; 61 globals.rafScheduler.scheduleFullRedraw(); 62 return; 63 } 64} 65 66function onKeyUp(e: Event) { 67 e.stopPropagation(); 68 const key = (e as KeyboardEvent).key; 69 const txt = e.target as HTMLInputElement; 70 omniboxValue = txt.value; 71 if (key === 'ArrowUp' || key === 'ArrowDown') { 72 selResult += (key === 'ArrowUp') ? -1 : 1; 73 selResult = Math.max(selResult, 0); 74 selResult = Math.min(selResult, numResults - 1); 75 e.preventDefault(); 76 globals.rafScheduler.scheduleFullRedraw(); 77 return; 78 } 79 if (txt.value.length <= 0 || key === 'Escape') { 80 clearOmniboxResults(); 81 globals.rafScheduler.scheduleFullRedraw(); 82 return; 83 } 84 if (mode === 'search') { 85 const name = txt.value.replace(/'/g, '\\\'').replace(/[*]/g, '%'); 86 const query = `select str from strings where str like '%${name}%' limit 10`; 87 globals.dispatch( 88 Actions.executeQuery({engineId: '0', queryId: QUERY_ID, query})); 89 } 90 if (mode === 'command' && key === 'Enter') { 91 globals.dispatch(Actions.executeQuery( 92 {engineId: '0', queryId: 'command', query: txt.value})); 93 } 94} 95 96 97class Omnibox implements m.ClassComponent { 98 oncreate(vnode: m.VnodeDOM) { 99 const txt = vnode.dom.querySelector('input') as HTMLInputElement; 100 txt.addEventListener('blur', clearOmniboxResults); 101 txt.addEventListener('keydown', onKeyDown); 102 txt.addEventListener('keyup', onKeyUp); 103 } 104 105 view() { 106 const msgTTL = globals.state.status.timestamp + 1 - Date.now() / 1e3; 107 let enginesAreBusy = false; 108 for (const engine of Object.values(globals.state.engines)) { 109 enginesAreBusy = enginesAreBusy || !engine.ready; 110 } 111 112 if (msgTTL > 0 || enginesAreBusy) { 113 setTimeout( 114 () => globals.rafScheduler.scheduleFullRedraw(), msgTTL * 1000); 115 return m( 116 `.omnibox.message-mode`, 117 m(`input[placeholder=${globals.state.status.msg}][readonly]`, { 118 value: '', 119 })); 120 } 121 122 // TODO(primiano): handle query results here. 123 const results = []; 124 const resp = globals.queryResults.get(QUERY_ID) as QueryResponse; 125 if (resp !== undefined) { 126 numResults = resp.rows ? resp.rows.length : 0; 127 for (let i = 0; i < resp.rows.length; i++) { 128 const clazz = (i === selResult) ? '.selected' : ''; 129 results.push(m(`div${clazz}`, resp.rows[i][resp.columns[0]])); 130 } 131 } 132 const placeholder = { 133 search: 'Search or type : to enter command mode', 134 command: 'e.g., select * from sched left join thread using(utid) limit 10' 135 }; 136 137 const commandMode = mode === 'command'; 138 return m( 139 `.omnibox${commandMode ? '.command-mode' : ''}`, 140 m(`input[placeholder=${placeholder[mode]}]`, { 141 onchange: m.withAttr('value', v => omniboxValue = v), 142 value: omniboxValue, 143 }), 144 m('.omnibox-results', results)); 145 } 146} 147 148export class Topbar implements m.ClassComponent { 149 view() { 150 const progBar = []; 151 const engine: EngineConfig = globals.state.engines['0']; 152 if (globals.state.queries[QUERY_ID] !== undefined || 153 (engine !== undefined && !engine.ready)) { 154 progBar.push(m('.progress')); 155 } 156 return m('.topbar', m(Omnibox), ...progBar); 157 } 158} 159