// Copyright (C) 2019 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import m from 'mithril'; import {globals} from './globals'; import { KeyboardLayoutMap, nativeKeyboardLayoutMap, NotSupportedError, } from './keyboard_layout_map'; import {showModal} from './modal'; import {KeyMapping} from './pan_and_zoom_handler'; import {Spinner} from './widgets/spinner'; export function toggleHelp() { globals.logging.logEvent('User Actions', 'Show help'); showHelp(); } function keycap(glyph: m.Children): m.Children { return m('.keycap', glyph); } // A fallback keyboard map based on the QWERTY keymap. Converts keyboard event // codes to their associated glyphs on an English QWERTY keyboard. class EnglishQwertyKeyboardLayoutMap implements KeyboardLayoutMap { get(code: string): string { // Converts 'KeyX' -> 'x' return code.replace(/^Key([A-Z])$/, '$1').toLowerCase(); } } class KeyMappingsHelp implements m.ClassComponent { private keyMap?: KeyboardLayoutMap; oninit() { nativeKeyboardLayoutMap() .then((keyMap: KeyboardLayoutMap) => { this.keyMap = keyMap; globals.rafScheduler.scheduleFullRedraw(); }) .catch((e) => { if (e instanceof NotSupportedError || e.toString().includes('SecurityError')) { // Keyboard layout is unavailable. Since showing the keyboard // mappings correct for the user's keyboard layout is a nice-to- // have, and users with non-QWERTY layouts are usually aware of the // fact that the are using non-QWERTY layouts, we resort to showing // English QWERTY mappings as a best-effort approach. // The alternative would be to show key mappings for all keyboard // layouts which is not feasible. this.keyMap = new EnglishQwertyKeyboardLayoutMap(); globals.rafScheduler.scheduleFullRedraw(); } else { // Something unexpected happened. Either the browser doesn't conform // to the keyboard API spec, or the keyboard API spec has changed! throw e; } }); } view(_: m.Vnode): m.Children { const ctrlOrCmd = window.navigator.platform.indexOf('Mac') !== -1 ? 'Cmd' : 'Ctrl'; const queryPageInstructions = globals.hideSidebar ? [] : [ m('h2', 'Making SQL queries from the query page'), m('table', m('tr', m('td', keycap('Ctrl'), ' + ', keycap('Enter')), m('td', 'Execute query')), m('tr', m('td', keycap('Ctrl'), ' + ', keycap('Enter'), ' (with selection)'), m('td', 'Execute selection'))), ]; const sidebarInstructions = globals.hideSidebar ? [] : [m('tr', m('td', keycap(ctrlOrCmd), ' + ', keycap('b')), m('td', 'Toggle display of sidebar'))]; return m( '.help', m('h2', 'Navigation'), m( 'table', m( 'tr', m('td', this.codeToKeycap(KeyMapping.KEY_ZOOM_IN), '/', this.codeToKeycap(KeyMapping.KEY_ZOOM_OUT)), m('td', 'Zoom in/out'), ), m( 'tr', m('td', this.codeToKeycap(KeyMapping.KEY_PAN_LEFT), '/', this.codeToKeycap(KeyMapping.KEY_PAN_RIGHT)), m('td', 'Pan left/right'), ), ), m('h2', 'Mouse Controls'), m('table', m('tr', m('td', 'Click'), m('td', 'Select event')), m('tr', m('td', 'Ctrl + Scroll wheel'), m('td', 'Zoom in/out')), m('tr', m('td', 'Click + Drag'), m('td', 'Select area')), m('tr', m('td', 'Shift + Click + Drag'), m('td', 'Pan left/right'))), m('h2', 'Making SQL queries from the viewer page'), m('table', m('tr', m('td', keycap(':'), ' in the (empty) search box'), m('td', 'Switch to query input')), m('tr', m('td', keycap('Enter')), m('td', 'Execute query')), m('tr', m('td', keycap('Ctrl'), ' + ', keycap('Enter')), m('td', 'Execute query and pin output ' + '(output will not be replaced by regular query input)'))), ...queryPageInstructions, m('h2', 'Other'), m( 'table', m('tr', m('td', keycap('f'), ' (with event selected)'), m('td', 'Scroll + zoom to current selection')), m('tr', m('td', keycap('['), '/', keycap(']'), ' (with event selected)'), m('td', 'Select next/previous slice that is connected by a flow.', m('br'), 'If there are multiple flows,' + 'the one that is in focus (bold) is selected')), m('tr', m('td', keycap(ctrlOrCmd), ' + ', keycap('['), '/', keycap(']'), ' (with event selected)'), m('td', 'Switch focus to another flow')), m('tr', m('td', keycap('m'), ' (with event or area selected)'), m('td', 'Mark the area (temporarily)')), m('tr', m('td', keycap('Shift'), ' + ', keycap('m'), ' (with event or area selected)'), m('td', 'Mark the area (persistently)')), m('tr', m('td', keycap(ctrlOrCmd), ' + ', keycap('a')), m('td', 'Select all')), ...sidebarInstructions, m('tr', m('td', keycap('?')), m('td', 'Show help')), )); } private codeToKeycap(code: string): m.Children { if (this.keyMap) { return keycap(this.keyMap.get(code)); } else { return keycap(m(Spinner)); } } } function showHelp() { showModal({ title: 'Perfetto Help', content: () => m(KeyMappingsHelp), buttons: [], }); }