1 2// Copyright (C) 2019 The Android Open Source Project 3// 4// Licensed under the Apache License, Version 2.0 (the "License"); 5// you may not use this file except in compliance with the License. 6// You may obtain a copy of the License at 7// 8// http://www.apache.org/licenses/LICENSE-2.0 9// 10// Unless required by applicable law or agreed to in writing, software 11// distributed under the License is distributed on an "AS IS" BASIS, 12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13// See the License for the specific language governing permissions and 14// limitations under the License. 15 16import m from 'mithril'; 17 18import {globals} from './globals'; 19import { 20 KeyboardLayoutMap, 21 nativeKeyboardLayoutMap, 22 NotSupportedError, 23} from './keyboard_layout_map'; 24import {showModal} from './modal'; 25import {KeyMapping} from './pan_and_zoom_handler'; 26import {Spinner} from './widgets/spinner'; 27 28export function toggleHelp() { 29 globals.logging.logEvent('User Actions', 'Show help'); 30 showHelp(); 31} 32 33function keycap(glyph: m.Children): m.Children { 34 return m('.keycap', glyph); 35} 36 37// A fallback keyboard map based on the QWERTY keymap. Converts keyboard event 38// codes to their associated glyphs on an English QWERTY keyboard. 39class EnglishQwertyKeyboardLayoutMap implements KeyboardLayoutMap { 40 get(code: string): string { 41 // Converts 'KeyX' -> 'x' 42 return code.replace(/^Key([A-Z])$/, '$1').toLowerCase(); 43 } 44} 45 46class KeyMappingsHelp implements m.ClassComponent { 47 private keyMap?: KeyboardLayoutMap; 48 49 oninit() { 50 nativeKeyboardLayoutMap() 51 .then((keyMap: KeyboardLayoutMap) => { 52 this.keyMap = keyMap; 53 globals.rafScheduler.scheduleFullRedraw(); 54 }) 55 .catch((e) => { 56 if (e instanceof NotSupportedError || 57 e.toString().includes('SecurityError')) { 58 // Keyboard layout is unavailable. Since showing the keyboard 59 // mappings correct for the user's keyboard layout is a nice-to- 60 // have, and users with non-QWERTY layouts are usually aware of the 61 // fact that the are using non-QWERTY layouts, we resort to showing 62 // English QWERTY mappings as a best-effort approach. 63 // The alternative would be to show key mappings for all keyboard 64 // layouts which is not feasible. 65 this.keyMap = new EnglishQwertyKeyboardLayoutMap(); 66 globals.rafScheduler.scheduleFullRedraw(); 67 } else { 68 // Something unexpected happened. Either the browser doesn't conform 69 // to the keyboard API spec, or the keyboard API spec has changed! 70 throw e; 71 } 72 }); 73 } 74 75 view(_: m.Vnode): m.Children { 76 const ctrlOrCmd = 77 window.navigator.platform.indexOf('Mac') !== -1 ? 'Cmd' : 'Ctrl'; 78 79 const queryPageInstructions = globals.hideSidebar ? [] : [ 80 m('h2', 'Making SQL queries from the query page'), 81 m('table', 82 m('tr', 83 m('td', keycap('Ctrl'), ' + ', keycap('Enter')), 84 m('td', 'Execute query')), 85 m('tr', 86 m('td', keycap('Ctrl'), ' + ', keycap('Enter'), ' (with selection)'), 87 m('td', 'Execute selection'))), 88 ]; 89 90 const sidebarInstructions = globals.hideSidebar ? 91 [] : 92 [m('tr', 93 m('td', keycap(ctrlOrCmd), ' + ', keycap('b')), 94 m('td', 'Toggle display of sidebar'))]; 95 96 return m( 97 '.help', 98 m('h2', 'Navigation'), 99 m( 100 'table', 101 m( 102 'tr', 103 m('td', 104 this.codeToKeycap(KeyMapping.KEY_ZOOM_IN), 105 '/', 106 this.codeToKeycap(KeyMapping.KEY_ZOOM_OUT)), 107 m('td', 'Zoom in/out'), 108 ), 109 m( 110 'tr', 111 m('td', 112 this.codeToKeycap(KeyMapping.KEY_PAN_LEFT), 113 '/', 114 this.codeToKeycap(KeyMapping.KEY_PAN_RIGHT)), 115 m('td', 'Pan left/right'), 116 ), 117 ), 118 m('h2', 'Mouse Controls'), 119 m('table', 120 m('tr', m('td', 'Click'), m('td', 'Select event')), 121 m('tr', m('td', 'Ctrl + Scroll wheel'), m('td', 'Zoom in/out')), 122 m('tr', m('td', 'Click + Drag'), m('td', 'Select area')), 123 m('tr', m('td', 'Shift + Click + Drag'), m('td', 'Pan left/right'))), 124 m('h2', 'Making SQL queries from the viewer page'), 125 m('table', 126 m('tr', 127 m('td', keycap(':'), ' in the (empty) search box'), 128 m('td', 'Switch to query input')), 129 m('tr', m('td', keycap('Enter')), m('td', 'Execute query')), 130 m('tr', 131 m('td', keycap('Ctrl'), ' + ', keycap('Enter')), 132 m('td', 133 'Execute query and pin output ' + 134 '(output will not be replaced by regular query input)'))), 135 ...queryPageInstructions, 136 m('h2', 'Other'), 137 m( 138 'table', 139 m('tr', 140 m('td', keycap('f'), ' (with event selected)'), 141 m('td', 'Scroll + zoom to current selection')), 142 m('tr', 143 m('td', keycap('['), '/', keycap(']'), ' (with event selected)'), 144 m('td', 145 'Select next/previous slice that is connected by a flow.', 146 m('br'), 147 'If there are multiple flows,' + 148 'the one that is in focus (bold) is selected')), 149 m('tr', 150 m('td', 151 keycap(ctrlOrCmd), 152 ' + ', 153 keycap('['), 154 '/', 155 keycap(']'), 156 ' (with event selected)'), 157 m('td', 'Switch focus to another flow')), 158 m('tr', 159 m('td', keycap('m'), ' (with event or area selected)'), 160 m('td', 'Mark the area (temporarily)')), 161 m('tr', 162 m('td', 163 keycap('Shift'), 164 ' + ', 165 keycap('m'), 166 ' (with event or area selected)'), 167 m('td', 'Mark the area (persistently)')), 168 m('tr', 169 m('td', keycap(ctrlOrCmd), ' + ', keycap('a')), 170 m('td', 'Select all')), 171 ...sidebarInstructions, 172 m('tr', m('td', keycap('?')), m('td', 'Show help')), 173 )); 174 } 175 176 private codeToKeycap(code: string): m.Children { 177 if (this.keyMap) { 178 return keycap(this.keyMap.get(code)); 179 } else { 180 return keycap(m(Spinner)); 181 } 182 } 183} 184 185function showHelp() { 186 showModal({ 187 title: 'Perfetto Help', 188 content: () => m(KeyMappingsHelp), 189 buttons: [], 190 }); 191} 192