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