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'; 17import {TPTime} from '../common/time'; 18 19import {Flow, globals} from './globals'; 20import {toggleHelp} from './help_modal'; 21import { 22 focusHorizontalRange, 23 verticalScrollToTrack, 24} from './scroll_helper'; 25import {executeSearch} from './search_handler'; 26 27const INSTANT_FOCUS_DURATION = 1n; 28const INCOMPLETE_SLICE_DURATION = 30_000n; 29type Direction = 'Forward'|'Backward'; 30 31// Handles all key events than are not handled by the 32// pan and zoom handler. Returns true if the event was handled. 33export function handleKey(e: KeyboardEvent, down: boolean): boolean { 34 const key = e.key.toLowerCase(); 35 const selection = globals.state.currentSelection; 36 const noModifiers = !(e.ctrlKey || e.metaKey || e.altKey || e.shiftKey); 37 const ctrlOrMeta = (e.ctrlKey || e.metaKey) && !(e.altKey || e.shiftKey); 38 // No other modifiers other than possibly Shift. 39 const maybeShift = !(e.ctrlKey || e.metaKey || e.altKey); 40 41 if (down && 'm' === key && maybeShift) { 42 if (selection && selection.kind === 'AREA') { 43 globals.dispatch(Actions.toggleMarkCurrentArea({persistent: e.shiftKey})); 44 } else if (selection) { 45 lockSliceSpan(e.shiftKey); 46 } 47 return true; 48 } 49 if (down && 'f' === key && noModifiers) { 50 findCurrentSelection(); 51 return true; 52 } 53 if (down && 'a' === key && ctrlOrMeta) { 54 let tracksToSelect: string[] = []; 55 56 const selection = globals.state.currentSelection; 57 if (selection !== null && selection.kind === 'AREA') { 58 const area = globals.state.areas[selection.areaId]; 59 const coversEntireTimeRange = 60 globals.state.traceTime.start === area.start && 61 globals.state.traceTime.end === area.end; 62 if (!coversEntireTimeRange) { 63 // If the current selection is an area which does not cover the entire 64 // time range, preserve the list of selected tracks and expand the time 65 // range. 66 tracksToSelect = area.tracks; 67 } else { 68 // If the entire time range is already covered, update the selection to 69 // cover all tracks. 70 tracksToSelect = Object.keys(globals.state.tracks); 71 } 72 } else { 73 // If the current selection is not an area, select all. 74 tracksToSelect = Object.keys(globals.state.tracks); 75 } 76 const {start, end} = globals.state.traceTime; 77 globals.dispatch(Actions.selectArea({ 78 area: { 79 start, 80 end, 81 tracks: tracksToSelect, 82 }, 83 })); 84 e.preventDefault(); 85 return true; 86 } 87 if (down && 'b' === key && ctrlOrMeta) { 88 globals.dispatch(Actions.toggleSidebar({})); 89 return true; 90 } 91 if (down && '?' === key && maybeShift) { 92 toggleHelp(); 93 return true; 94 } 95 if (down && 'enter' === key && maybeShift) { 96 e.preventDefault(); 97 executeSearch(e.shiftKey); 98 return true; 99 } 100 if (down && 'escape' === key) { 101 globals.frontendLocalState.deselectArea(); 102 globals.makeSelection(Actions.deselect({})); 103 globals.dispatch(Actions.removeNote({id: '0'})); 104 return true; 105 } 106 if (down && ']' === key && ctrlOrMeta) { 107 focusOtherFlow('Forward'); 108 return true; 109 } 110 if (down && ']' === key && noModifiers) { 111 moveByFocusedFlow('Forward'); 112 return true; 113 } 114 if (down && '[' === key && ctrlOrMeta) { 115 focusOtherFlow('Backward'); 116 return true; 117 } 118 if (down && '[' === key && noModifiers) { 119 moveByFocusedFlow('Backward'); 120 return true; 121 } 122 return false; 123} 124 125// Search |boundFlows| for |flowId| and return the id following it. 126// Returns the first flow id if nothing was found or |flowId| was the last flow 127// in |boundFlows|, and -1 if |boundFlows| is empty 128function findAnotherFlowExcept(boundFlows: Flow[], flowId: number): number { 129 let selectedFlowFound = false; 130 131 if (boundFlows.length === 0) { 132 return -1; 133 } 134 135 for (const flow of boundFlows) { 136 if (selectedFlowFound) { 137 return flow.id; 138 } 139 140 if (flow.id === flowId) { 141 selectedFlowFound = true; 142 } 143 } 144 return boundFlows[0].id; 145} 146 147// Change focus to the next flow event (matching the direction) 148function focusOtherFlow(direction: Direction) { 149 if (!globals.state.currentSelection || 150 globals.state.currentSelection.kind !== 'CHROME_SLICE') { 151 return; 152 } 153 const sliceId = globals.state.currentSelection.id; 154 if (sliceId === -1) { 155 return; 156 } 157 158 const boundFlows = globals.connectedFlows.filter( 159 (flow) => flow.begin.sliceId === sliceId && direction === 'Forward' || 160 flow.end.sliceId === sliceId && direction === 'Backward'); 161 162 if (direction === 'Backward') { 163 const nextFlowId = 164 findAnotherFlowExcept(boundFlows, globals.state.focusedFlowIdLeft); 165 globals.dispatch(Actions.setHighlightedFlowLeftId({flowId: nextFlowId})); 166 } else { 167 const nextFlowId = 168 findAnotherFlowExcept(boundFlows, globals.state.focusedFlowIdRight); 169 globals.dispatch(Actions.setHighlightedFlowRightId({flowId: nextFlowId})); 170 } 171} 172 173// Select the slice connected to the flow in focus 174function moveByFocusedFlow(direction: Direction): void { 175 if (!globals.state.currentSelection || 176 globals.state.currentSelection.kind !== 'CHROME_SLICE') { 177 return; 178 } 179 180 const sliceId = globals.state.currentSelection.id; 181 const flowId = 182 (direction === 'Backward' ? globals.state.focusedFlowIdLeft : 183 globals.state.focusedFlowIdRight); 184 185 if (sliceId === -1 || flowId === -1) { 186 return; 187 } 188 189 // Find flow that is in focus and select corresponding slice 190 for (const flow of globals.connectedFlows) { 191 if (flow.id === flowId) { 192 const flowPoint = (direction === 'Backward' ? flow.begin : flow.end); 193 const uiTrackId = 194 globals.state.uiTrackIdByTraceTrackId[flowPoint.trackId]; 195 if (uiTrackId) { 196 globals.makeSelection(Actions.selectChromeSlice({ 197 id: flowPoint.sliceId, 198 trackId: uiTrackId, 199 table: 'slice', 200 scroll: true, 201 })); 202 } 203 } 204 } 205} 206 207function findTimeRangeOfSelection(): {startTs: TPTime, endTs: TPTime} { 208 const selection = globals.state.currentSelection; 209 let startTs = -1n; 210 let endTs = -1n; 211 if (selection === null) { 212 return {startTs, endTs}; 213 } else if (selection.kind === 'SLICE' || selection.kind === 'CHROME_SLICE') { 214 const slice = globals.sliceDetails; 215 if (slice.ts && slice.dur !== undefined && slice.dur > 0) { 216 startTs = slice.ts; 217 endTs = startTs + slice.dur; 218 } else if (slice.ts) { 219 startTs = slice.ts; 220 // This will handle either: 221 // a)slice.dur === -1 -> unfinished slice 222 // b)slice.dur === 0 -> instant event 223 endTs = slice.dur === -1n ? startTs + INCOMPLETE_SLICE_DURATION : 224 startTs + INSTANT_FOCUS_DURATION; 225 } 226 } else if (selection.kind === 'THREAD_STATE') { 227 const threadState = globals.threadStateDetails; 228 if (threadState.ts && threadState.dur) { 229 startTs = threadState.ts; 230 endTs = startTs + threadState.dur; 231 } 232 } else if (selection.kind === 'COUNTER') { 233 startTs = selection.leftTs; 234 endTs = selection.rightTs; 235 } else if (selection.kind === 'AREA') { 236 const selectedArea = globals.state.areas[selection.areaId]; 237 if (selectedArea) { 238 startTs = selectedArea.start; 239 endTs = selectedArea.end; 240 } 241 } else if (selection.kind === 'NOTE') { 242 const selectedNote = globals.state.notes[selection.id]; 243 // Notes can either be default or area notes. Area notes are handled 244 // above in the AREA case. 245 if (selectedNote && selectedNote.noteType === 'DEFAULT') { 246 startTs = selectedNote.timestamp; 247 endTs = selectedNote.timestamp + INSTANT_FOCUS_DURATION; 248 } 249 } else if (selection.kind === 'LOG') { 250 // TODO(hjd): Make focus selection work for logs. 251 } else if ( 252 selection.kind === 'DEBUG_SLICE' || 253 selection.kind === 'TOP_LEVEL_SCROLL') { 254 startTs = selection.start; 255 if (selection.duration > 0) { 256 endTs = startTs + selection.duration; 257 } else { 258 endTs = startTs + INSTANT_FOCUS_DURATION; 259 } 260 } 261 262 return {startTs, endTs}; 263} 264 265 266function lockSliceSpan(persistent = false) { 267 const range = findTimeRangeOfSelection(); 268 if (range.startTs !== -1n && range.endTs !== -1n && 269 globals.state.currentSelection !== null) { 270 const tracks = globals.state.currentSelection.trackId ? 271 [globals.state.currentSelection.trackId] : 272 []; 273 const area: Area = {start: range.startTs, end: range.endTs, tracks}; 274 globals.dispatch(Actions.markArea({area, persistent})); 275 } 276} 277 278export function findCurrentSelection() { 279 const selection = globals.state.currentSelection; 280 if (selection === null) return; 281 282 const range = findTimeRangeOfSelection(); 283 if (range.startTs !== -1n && range.endTs !== -1n) { 284 focusHorizontalRange(range.startTs, range.endTs); 285 } 286 287 if (selection.trackId) { 288 verticalScrollToTrack(selection.trackId, true); 289 } 290} 291