// 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 {Actions} from '../common/actions'; import {Area} from '../common/state'; import {TPTime} from '../common/time'; import {Flow, globals} from './globals'; import {toggleHelp} from './help_modal'; import { focusHorizontalRange, verticalScrollToTrack, } from './scroll_helper'; import {executeSearch} from './search_handler'; const INSTANT_FOCUS_DURATION = 1n; const INCOMPLETE_SLICE_DURATION = 30_000n; type Direction = 'Forward'|'Backward'; // Handles all key events than are not handled by the // pan and zoom handler. Returns true if the event was handled. export function handleKey(e: KeyboardEvent, down: boolean): boolean { const key = e.key.toLowerCase(); const selection = globals.state.currentSelection; const noModifiers = !(e.ctrlKey || e.metaKey || e.altKey || e.shiftKey); const ctrlOrMeta = (e.ctrlKey || e.metaKey) && !(e.altKey || e.shiftKey); // No other modifiers other than possibly Shift. const maybeShift = !(e.ctrlKey || e.metaKey || e.altKey); if (down && 'm' === key && maybeShift) { if (selection && selection.kind === 'AREA') { globals.dispatch(Actions.toggleMarkCurrentArea({persistent: e.shiftKey})); } else if (selection) { lockSliceSpan(e.shiftKey); } return true; } if (down && 'f' === key && noModifiers) { findCurrentSelection(); return true; } if (down && 'a' === key && ctrlOrMeta) { let tracksToSelect: string[] = []; const selection = globals.state.currentSelection; if (selection !== null && selection.kind === 'AREA') { const area = globals.state.areas[selection.areaId]; const coversEntireTimeRange = globals.state.traceTime.start === area.start && globals.state.traceTime.end === area.end; if (!coversEntireTimeRange) { // If the current selection is an area which does not cover the entire // time range, preserve the list of selected tracks and expand the time // range. tracksToSelect = area.tracks; } else { // If the entire time range is already covered, update the selection to // cover all tracks. tracksToSelect = Object.keys(globals.state.tracks); } } else { // If the current selection is not an area, select all. tracksToSelect = Object.keys(globals.state.tracks); } const {start, end} = globals.state.traceTime; globals.dispatch(Actions.selectArea({ area: { start, end, tracks: tracksToSelect, }, })); e.preventDefault(); return true; } if (down && 'b' === key && ctrlOrMeta) { globals.dispatch(Actions.toggleSidebar({})); return true; } if (down && '?' === key && maybeShift) { toggleHelp(); return true; } if (down && 'enter' === key && maybeShift) { e.preventDefault(); executeSearch(e.shiftKey); return true; } if (down && 'escape' === key) { globals.frontendLocalState.deselectArea(); globals.makeSelection(Actions.deselect({})); globals.dispatch(Actions.removeNote({id: '0'})); return true; } if (down && ']' === key && ctrlOrMeta) { focusOtherFlow('Forward'); return true; } if (down && ']' === key && noModifiers) { moveByFocusedFlow('Forward'); return true; } if (down && '[' === key && ctrlOrMeta) { focusOtherFlow('Backward'); return true; } if (down && '[' === key && noModifiers) { moveByFocusedFlow('Backward'); return true; } return false; } // Search |boundFlows| for |flowId| and return the id following it. // Returns the first flow id if nothing was found or |flowId| was the last flow // in |boundFlows|, and -1 if |boundFlows| is empty function findAnotherFlowExcept(boundFlows: Flow[], flowId: number): number { let selectedFlowFound = false; if (boundFlows.length === 0) { return -1; } for (const flow of boundFlows) { if (selectedFlowFound) { return flow.id; } if (flow.id === flowId) { selectedFlowFound = true; } } return boundFlows[0].id; } // Change focus to the next flow event (matching the direction) function focusOtherFlow(direction: Direction) { if (!globals.state.currentSelection || globals.state.currentSelection.kind !== 'CHROME_SLICE') { return; } const sliceId = globals.state.currentSelection.id; if (sliceId === -1) { return; } const boundFlows = globals.connectedFlows.filter( (flow) => flow.begin.sliceId === sliceId && direction === 'Forward' || flow.end.sliceId === sliceId && direction === 'Backward'); if (direction === 'Backward') { const nextFlowId = findAnotherFlowExcept(boundFlows, globals.state.focusedFlowIdLeft); globals.dispatch(Actions.setHighlightedFlowLeftId({flowId: nextFlowId})); } else { const nextFlowId = findAnotherFlowExcept(boundFlows, globals.state.focusedFlowIdRight); globals.dispatch(Actions.setHighlightedFlowRightId({flowId: nextFlowId})); } } // Select the slice connected to the flow in focus function moveByFocusedFlow(direction: Direction): void { if (!globals.state.currentSelection || globals.state.currentSelection.kind !== 'CHROME_SLICE') { return; } const sliceId = globals.state.currentSelection.id; const flowId = (direction === 'Backward' ? globals.state.focusedFlowIdLeft : globals.state.focusedFlowIdRight); if (sliceId === -1 || flowId === -1) { return; } // Find flow that is in focus and select corresponding slice for (const flow of globals.connectedFlows) { if (flow.id === flowId) { const flowPoint = (direction === 'Backward' ? flow.begin : flow.end); const uiTrackId = globals.state.uiTrackIdByTraceTrackId[flowPoint.trackId]; if (uiTrackId) { globals.makeSelection(Actions.selectChromeSlice({ id: flowPoint.sliceId, trackId: uiTrackId, table: 'slice', scroll: true, })); } } } } function findTimeRangeOfSelection(): {startTs: TPTime, endTs: TPTime} { const selection = globals.state.currentSelection; let startTs = -1n; let endTs = -1n; if (selection === null) { return {startTs, endTs}; } else if (selection.kind === 'SLICE' || selection.kind === 'CHROME_SLICE') { const slice = globals.sliceDetails; if (slice.ts && slice.dur !== undefined && slice.dur > 0) { startTs = slice.ts; endTs = startTs + slice.dur; } else if (slice.ts) { startTs = slice.ts; // This will handle either: // a)slice.dur === -1 -> unfinished slice // b)slice.dur === 0 -> instant event endTs = slice.dur === -1n ? startTs + INCOMPLETE_SLICE_DURATION : startTs + INSTANT_FOCUS_DURATION; } } else if (selection.kind === 'THREAD_STATE') { const threadState = globals.threadStateDetails; if (threadState.ts && threadState.dur) { startTs = threadState.ts; endTs = startTs + threadState.dur; } } else if (selection.kind === 'COUNTER') { startTs = selection.leftTs; endTs = selection.rightTs; } else if (selection.kind === 'AREA') { const selectedArea = globals.state.areas[selection.areaId]; if (selectedArea) { startTs = selectedArea.start; endTs = selectedArea.end; } } else if (selection.kind === 'NOTE') { const selectedNote = globals.state.notes[selection.id]; // Notes can either be default or area notes. Area notes are handled // above in the AREA case. if (selectedNote && selectedNote.noteType === 'DEFAULT') { startTs = selectedNote.timestamp; endTs = selectedNote.timestamp + INSTANT_FOCUS_DURATION; } } else if (selection.kind === 'LOG') { // TODO(hjd): Make focus selection work for logs. } else if ( selection.kind === 'DEBUG_SLICE' || selection.kind === 'TOP_LEVEL_SCROLL') { startTs = selection.start; if (selection.duration > 0) { endTs = startTs + selection.duration; } else { endTs = startTs + INSTANT_FOCUS_DURATION; } } return {startTs, endTs}; } function lockSliceSpan(persistent = false) { const range = findTimeRangeOfSelection(); if (range.startTs !== -1n && range.endTs !== -1n && globals.state.currentSelection !== null) { const tracks = globals.state.currentSelection.trackId ? [globals.state.currentSelection.trackId] : []; const area: Area = {start: range.startTs, end: range.endTs, tracks}; globals.dispatch(Actions.markArea({area, persistent})); } } export function findCurrentSelection() { const selection = globals.state.currentSelection; if (selection === null) return; const range = findTimeRangeOfSelection(); if (range.startTs !== -1n && range.endTs !== -1n) { focusHorizontalRange(range.startTs, range.endTs); } if (selection.trackId) { verticalScrollToTrack(selection.trackId, true); } }