1// Copyright (C) 2019 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 {Actions} from '../common/actions'; 16import {Area} from '../common/state'; 17 18import {Flow, globals} from './globals'; 19import {toggleHelp} from './help_modal'; 20import { 21 findUiTrackId, 22 horizontalScrollAndZoomToRange, 23 verticalScrollToTrack 24} from './scroll_helper'; 25import {executeSearch} from './search_handler'; 26 27const INSTANT_FOCUS_DURATION_S = 1 / 1e9; // 1 ns. 28type Direction = 'Forward'|'Backward'; 29 30// Handles all key events than are not handled by the 31// pan and zoom handler. 32export function handleKey(e: KeyboardEvent, down: boolean) { 33 const key = e.key.toLowerCase(); 34 const selection = globals.state.currentSelection; 35 if (down && 'm' === key) { 36 if (selection && selection.kind === 'AREA') { 37 globals.dispatch(Actions.toggleMarkCurrentArea({persistent: e.shiftKey})); 38 } else if (selection) { 39 lockSliceSpan(e.shiftKey); 40 } 41 } 42 if (down && 'f' === key) { 43 findCurrentSelection(); 44 } 45 if (down && 'v' === key) { 46 globals.dispatch(Actions.toggleVideo({})); 47 } 48 if (down && 'p' === key) { 49 globals.dispatch(Actions.toggleFlagPause({})); 50 } 51 if (down && 't' === key) { 52 globals.dispatch(Actions.toggleScrubbing({})); 53 if (globals.frontendLocalState.vidTimestamp < 0) { 54 globals.frontendLocalState.setVidTimestamp(Number.MAX_SAFE_INTEGER); 55 } else { 56 globals.frontendLocalState.setVidTimestamp(Number.MIN_SAFE_INTEGER); 57 } 58 } 59 if (down && 'b' === key && (e.ctrlKey || e.metaKey)) { 60 globals.frontendLocalState.toggleSidebar(); 61 } 62 if (down && '?' === key) { 63 toggleHelp(); 64 } 65 if (down && 'enter' === key) { 66 e.preventDefault(); 67 executeSearch(e.shiftKey); 68 } 69 if (down && 'escape' === key) { 70 globals.frontendLocalState.deselectArea(); 71 globals.makeSelection(Actions.deselect({})); 72 globals.dispatch(Actions.removeNote({id: '0'})); 73 } 74 if (down && ']' === key) { 75 if (e.ctrlKey) { 76 focusOtherFlow('Forward'); 77 } else { 78 moveByFocusedFlow('Forward'); 79 } 80 } 81 if (down && '[' === key) { 82 if (e.ctrlKey) { 83 focusOtherFlow('Backward'); 84 } else { 85 moveByFocusedFlow('Backward'); 86 } 87 } 88} 89 90// Search |boundFlows| for |flowId| and return the id following it. 91// Returns the first flow id if nothing was found or |flowId| was the last flow 92// in |boundFlows|, and -1 if |boundFlows| is empty 93function findAnotherFlowExcept(boundFlows: Flow[], flowId: number): number { 94 let selectedFlowFound = false; 95 96 if (boundFlows.length === 0) { 97 return -1; 98 } 99 100 for (const flow of boundFlows) { 101 if (selectedFlowFound) { 102 return flow.id; 103 } 104 105 if (flow.id === flowId) { 106 selectedFlowFound = true; 107 } 108 } 109 return boundFlows[0].id; 110} 111 112// Change focus to the next flow event (matching the direction) 113function focusOtherFlow(direction: Direction) { 114 if (!globals.state.currentSelection || 115 globals.state.currentSelection.kind !== 'CHROME_SLICE') { 116 return; 117 } 118 const sliceId = globals.state.currentSelection.id; 119 if (sliceId === -1) { 120 return; 121 } 122 123 const boundFlows = globals.connectedFlows.filter( 124 flow => flow.begin.sliceId === sliceId && direction === 'Forward' || 125 flow.end.sliceId === sliceId && direction === 'Backward'); 126 127 if (direction === 'Backward') { 128 const nextFlowId = findAnotherFlowExcept( 129 boundFlows, globals.frontendLocalState.focusedFlowIdLeft); 130 globals.frontendLocalState.setHighlightedFlowLeftId(nextFlowId); 131 } else { 132 const nextFlowId = findAnotherFlowExcept( 133 boundFlows, globals.frontendLocalState.focusedFlowIdRight); 134 globals.frontendLocalState.setHighlightedFlowRightId(nextFlowId); 135 } 136} 137 138// Select the slice connected to the flow in focus 139function moveByFocusedFlow(direction: Direction) { 140 if (!globals.state.currentSelection || 141 globals.state.currentSelection.kind !== 'CHROME_SLICE') { 142 return; 143 } 144 145 const sliceId = globals.state.currentSelection.id; 146 const flowId = 147 (direction === 'Backward' ? 148 globals.frontendLocalState.focusedFlowIdLeft : 149 globals.frontendLocalState.focusedFlowIdRight); 150 151 if (sliceId === -1 || flowId === -1) { 152 return; 153 } 154 155 // Find flow that is in focus and select corresponding slice 156 for (const flow of globals.connectedFlows) { 157 if (flow.id === flowId) { 158 const flowPoint = (direction === 'Backward' ? flow.begin : flow.end); 159 const uiTrackId = findUiTrackId(flowPoint.trackId); 160 if (uiTrackId) { 161 globals.makeSelection(Actions.selectChromeSlice( 162 {id: flowPoint.sliceId, trackId: uiTrackId, table: 'slice'})); 163 } 164 } 165 } 166} 167 168function findTimeRangeOfSelection() { 169 const selection = globals.state.currentSelection; 170 let startTs = -1; 171 let endTs = -1; 172 if (selection !== null) { 173 if (selection.kind === 'SLICE' || selection.kind === 'CHROME_SLICE') { 174 const slice = globals.sliceDetails; 175 if (slice.ts && slice.dur !== undefined && slice.dur > 0) { 176 startTs = slice.ts + globals.state.traceTime.startSec; 177 endTs = startTs + slice.dur; 178 } else if (slice.ts) { 179 startTs = slice.ts + globals.state.traceTime.startSec; 180 endTs = startTs + INSTANT_FOCUS_DURATION_S; 181 } 182 } else if (selection.kind === 'THREAD_STATE') { 183 const threadState = globals.threadStateDetails; 184 if (threadState.ts && threadState.dur) { 185 startTs = threadState.ts + globals.state.traceTime.startSec; 186 endTs = startTs + threadState.dur; 187 } 188 } else if (selection.kind === 'COUNTER') { 189 startTs = selection.leftTs; 190 endTs = selection.rightTs; 191 } 192 } 193 return {startTs, endTs}; 194} 195 196 197function lockSliceSpan(persistent = false) { 198 const range = findTimeRangeOfSelection(); 199 if (range.startTs !== -1 && range.endTs !== -1 && 200 globals.state.currentSelection !== null) { 201 const tracks = globals.state.currentSelection.trackId ? 202 [globals.state.currentSelection.trackId] : 203 []; 204 const area: Area = {startSec: range.startTs, endSec: range.endTs, tracks}; 205 globals.dispatch(Actions.markArea({area, persistent})); 206 } 207} 208 209function findCurrentSelection() { 210 const selection = globals.state.currentSelection; 211 if (selection === null) return; 212 213 const range = findTimeRangeOfSelection(); 214 if (range.startTs !== -1 && range.endTs !== -1) { 215 horizontalScrollAndZoomToRange(range.startTs, range.endTs); 216 } 217 218 if (selection.trackId) { 219 verticalScrollToTrack(selection.trackId, true); 220 } 221} 222