• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2018 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 * as m from 'mithril';
16
17import {Actions} from '../common/actions';
18import {TimeSpan} from '../common/time';
19
20import {TRACK_SHELL_WIDTH} from './css_constants';
21import {DetailsPanel} from './details_panel';
22import {globals} from './globals';
23import {NotesPanel} from './notes_panel';
24import {OverviewTimelinePanel} from './overview_timeline_panel';
25import {createPage} from './pages';
26import {PanAndZoomHandler} from './pan_and_zoom_handler';
27import {AnyAttrsVnode, PanelContainer} from './panel_container';
28import {TickmarkPanel} from './tickmark_panel';
29import {TimeAxisPanel} from './time_axis_panel';
30import {computeZoom} from './time_scale';
31import {TimeSelectionPanel} from './time_selection_panel';
32import {DISMISSED_PANNING_HINT_KEY} from './topbar';
33import {TrackGroupPanel} from './track_group_panel';
34import {TrackPanel} from './track_panel';
35
36const SIDEBAR_WIDTH = 256;
37
38// Checks if the mousePos is within 3px of the start or end of the
39// current selected time range.
40function onTimeRangeBoundary(mousePos: number): 'START'|'END'|null {
41  const selection = globals.state.currentSelection;
42  if (selection !== null && selection.kind === 'AREA') {
43    // If frontend selectedArea exists then we are in the process of editing the
44    // time range and need to use that value instead.
45    const area = globals.frontendLocalState.selectedArea ?
46        globals.frontendLocalState.selectedArea :
47        globals.state.areas[selection.areaId];
48    const start = globals.frontendLocalState.timeScale.timeToPx(area.startSec);
49    const end = globals.frontendLocalState.timeScale.timeToPx(area.endSec);
50    const startDrag = mousePos - TRACK_SHELL_WIDTH;
51    const startDistance = Math.abs(start - startDrag);
52    const endDistance = Math.abs(end - startDrag);
53    const range = 3 * window.devicePixelRatio;
54    // We might be within 3px of both boundaries but we should choose
55    // the closest one.
56    if (startDistance < range && startDistance <= endDistance) return 'START';
57    if (endDistance < range && endDistance <= startDistance) return 'END';
58  }
59  return null;
60}
61
62/**
63 * Top-most level component for the viewer page. Holds tracks, brush timeline,
64 * panels, and everything else that's part of the main trace viewer page.
65 */
66class TraceViewer implements m.ClassComponent {
67  private onResize: () => void = () => {};
68  private zoomContent?: PanAndZoomHandler;
69  // Used to prevent global deselection if a pan/drag select occurred.
70  private keepCurrentSelection = false;
71
72  oncreate(vnode: m.CVnodeDOM) {
73    const frontendLocalState = globals.frontendLocalState;
74    const updateDimensions = () => {
75      const rect = vnode.dom.getBoundingClientRect();
76      frontendLocalState.updateLocalLimits(
77          0,
78          rect.width - TRACK_SHELL_WIDTH -
79              frontendLocalState.getScrollbarWidth());
80    };
81
82    updateDimensions();
83
84    // TODO: Do resize handling better.
85    this.onResize = () => {
86      updateDimensions();
87      globals.rafScheduler.scheduleFullRedraw();
88    };
89
90    // Once ResizeObservers are out, we can stop accessing the window here.
91    window.addEventListener('resize', this.onResize);
92
93    const panZoomEl =
94        vnode.dom.querySelector('.pan-and-zoom-content') as HTMLElement;
95
96    this.zoomContent = new PanAndZoomHandler({
97      element: panZoomEl,
98      contentOffsetX: SIDEBAR_WIDTH,
99      onPanned: (pannedPx: number) => {
100        this.keepCurrentSelection = true;
101        const traceTime = globals.state.traceTime;
102        const vizTime = globals.frontendLocalState.visibleWindowTime;
103        const origDelta = vizTime.duration;
104        const tDelta = frontendLocalState.timeScale.deltaPxToDuration(pannedPx);
105        let tStart = vizTime.start + tDelta;
106        let tEnd = vizTime.end + tDelta;
107        if (tStart < traceTime.startSec) {
108          tStart = traceTime.startSec;
109          tEnd = tStart + origDelta;
110        } else if (tEnd > traceTime.endSec) {
111          tEnd = traceTime.endSec;
112          tStart = tEnd - origDelta;
113        }
114        frontendLocalState.updateVisibleTime(new TimeSpan(tStart, tEnd));
115        // If the user has panned they no longer need the hint.
116        localStorage.setItem(DISMISSED_PANNING_HINT_KEY, 'true');
117        globals.rafScheduler.scheduleRedraw();
118      },
119      onZoomed: (zoomedPositionPx: number, zoomRatio: number) => {
120        // TODO(hjd): Avoid hardcoding TRACK_SHELL_WIDTH.
121        // TODO(hjd): Improve support for zooming in overview timeline.
122        const span = frontendLocalState.visibleWindowTime;
123        const scale = frontendLocalState.timeScale;
124        const zoomPx = zoomedPositionPx - TRACK_SHELL_WIDTH;
125        const newSpan = computeZoom(scale, span, 1 - zoomRatio, zoomPx);
126        frontendLocalState.updateVisibleTime(newSpan);
127        globals.rafScheduler.scheduleRedraw();
128      },
129      editSelection: (currentPx: number) => {
130        return onTimeRangeBoundary(currentPx) !== null;
131      },
132      onSelection: (
133          dragStartX: number,
134          dragStartY: number,
135          prevX: number,
136          currentX: number,
137          currentY: number,
138          editing: boolean) => {
139        const traceTime = globals.state.traceTime;
140        const scale = frontendLocalState.timeScale;
141        this.keepCurrentSelection = true;
142        if (editing) {
143          const selection = globals.state.currentSelection;
144          if (selection !== null && selection.kind === 'AREA') {
145            const area = globals.frontendLocalState.selectedArea ?
146                globals.frontendLocalState.selectedArea :
147                globals.state.areas[selection.areaId];
148            let newTime = scale.pxToTime(currentX - TRACK_SHELL_WIDTH);
149            // Have to check again for when one boundary crosses over the other.
150            const curBoundary = onTimeRangeBoundary(prevX);
151            if (curBoundary == null) return;
152            const keepTime =
153                curBoundary === 'START' ? area.endSec : area.startSec;
154            // Don't drag selection outside of current screen.
155            if (newTime < keepTime) {
156              newTime = Math.max(newTime, scale.pxToTime(scale.startPx));
157            } else {
158              newTime = Math.min(newTime, scale.pxToTime(scale.endPx));
159            }
160            // When editing the time range we always use the saved tracks,
161            // since these will not change.
162            frontendLocalState.selectArea(
163                Math.max(Math.min(keepTime, newTime), traceTime.startSec),
164                Math.min(Math.max(keepTime, newTime), traceTime.endSec),
165                globals.state.areas[selection.areaId].tracks);
166          }
167        } else {
168          let startPx = Math.min(dragStartX, currentX) - TRACK_SHELL_WIDTH;
169          let endPx = Math.max(dragStartX, currentX) - TRACK_SHELL_WIDTH;
170          if (startPx < 0 && endPx < 0) return;
171          startPx = Math.max(startPx, scale.startPx);
172          endPx = Math.min(endPx, scale.endPx);
173          frontendLocalState.selectArea(
174              scale.pxToTime(startPx), scale.pxToTime(endPx));
175          frontendLocalState.areaY.start = dragStartY;
176          frontendLocalState.areaY.end = currentY;
177        }
178        globals.rafScheduler.scheduleRedraw();
179      },
180      endSelection: (edit: boolean) => {
181        globals.frontendLocalState.areaY.start = undefined;
182        globals.frontendLocalState.areaY.end = undefined;
183        const area = globals.frontendLocalState.selectedArea;
184        // If we are editing we need to pass the current id through to ensure
185        // the marked area with that id is also updated.
186        if (edit) {
187          const selection = globals.state.currentSelection;
188          if (selection !== null && selection.kind === 'AREA' && area) {
189            globals.dispatch(
190                Actions.editArea({area, areaId: selection.areaId}));
191          }
192        } else if (area) {
193          globals.makeSelection(Actions.selectArea({area}));
194        }
195        // Now the selection has ended we stored the final selected area in the
196        // global state and can remove the in progress selection from the
197        // frontendLocalState.
198        globals.frontendLocalState.deselectArea();
199        // Full redraw to color track shell.
200        globals.rafScheduler.scheduleFullRedraw();
201      }
202    });
203  }
204
205  onremove() {
206    window.removeEventListener('resize', this.onResize);
207    if (this.zoomContent) this.zoomContent.shutdown();
208  }
209
210  view() {
211    const scrollingPanels: AnyAttrsVnode[] = globals.state.scrollingTracks.map(
212        id => m(TrackPanel, {key: id, id, selectable: true}));
213
214    for (const group of Object.values(globals.state.trackGroups)) {
215      scrollingPanels.push(m(TrackGroupPanel, {
216        trackGroupId: group.id,
217        key: `trackgroup-${group.id}`,
218        selectable: true,
219      }));
220      if (group.collapsed) continue;
221      // The first track is the summary track, and is displayed as part of the
222      // group panel, we don't want to display it twice so we start from 1.
223      for (let i = 1; i < group.tracks.length; ++i) {
224        const id = group.tracks[i];
225        scrollingPanels.push(m(TrackPanel, {
226          key: `track-${group.id}-${id}`,
227          id,
228          selectable: true,
229        }));
230      }
231    }
232
233    return m(
234        '.page',
235        m('.split-panel',
236          m('.pan-and-zoom-content',
237            {
238              onclick: () => {
239                // We don't want to deselect when panning/drag selecting.
240                if (this.keepCurrentSelection) {
241                  this.keepCurrentSelection = false;
242                  return;
243                }
244                globals.makeSelection(Actions.deselect({}));
245              }
246            },
247            m('.pinned-panel-container', m(PanelContainer, {
248                doesScroll: false,
249                panels: [
250                  m(OverviewTimelinePanel, {key: 'overview'}),
251                  m(TimeAxisPanel, {key: 'timeaxis'}),
252                  m(TimeSelectionPanel, {key: 'timeselection'}),
253                  m(NotesPanel, {key: 'notes'}),
254                  m(TickmarkPanel, {key: 'searchTickmarks'}),
255                  ...globals.state.pinnedTracks.map(
256                      id => m(TrackPanel, {key: id, id, selectable: true})),
257                ],
258                kind: 'OVERVIEW',
259              })),
260            m('.scrolling-panel-container', m(PanelContainer, {
261                doesScroll: true,
262                panels: scrollingPanels,
263                kind: 'TRACKS',
264              })))),
265        m(DetailsPanel));
266  }
267}
268
269export const ViewerPage = createPage({
270  view() {
271    return m(TraceViewer);
272  }
273});
274