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