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 {hex} from 'color-convert';
16import * as m from 'mithril';
17
18import {assertExists} from '../base/logging';
19import {Actions} from '../common/actions';
20import {
21  getContainingTrackId,
22  TrackGroupState,
23  TrackState
24} from '../common/state';
25
26import {globals} from './globals';
27import {drawGridLines} from './gridline_helper';
28import {
29  BLANK_CHECKBOX,
30  CHECKBOX,
31  EXPAND_DOWN,
32  EXPAND_UP,
33  INDETERMINATE_CHECKBOX
34} from './icons';
35import {Panel, PanelSize} from './panel';
36import {Track} from './track';
37import {TrackContent} from './track_panel';
38import {trackRegistry} from './track_registry';
39import {
40  drawVerticalLineAtTime,
41} from './vertical_line_helper';
42
43interface Attrs {
44  trackGroupId: string;
45  selectable: boolean;
46}
47
48export class TrackGroupPanel extends Panel<Attrs> {
49  private readonly trackGroupId: string;
50  private shellWidth = 0;
51  private backgroundColor = '#ffffff';  // Updated from CSS later.
52  private summaryTrack: Track;
53
54  constructor({attrs}: m.CVnode<Attrs>) {
55    super();
56    this.trackGroupId = attrs.trackGroupId;
57    const trackCreator = trackRegistry.get(this.summaryTrackState.kind);
58    this.summaryTrack = trackCreator.create(this.summaryTrackState);
59  }
60
61  get trackGroupState(): TrackGroupState {
62    return assertExists(globals.state.trackGroups[this.trackGroupId]);
63  }
64
65  get summaryTrackState(): TrackState {
66    return assertExists(globals.state.tracks[this.trackGroupState.tracks[0]]);
67  }
68
69  view({attrs}: m.CVnode<Attrs>) {
70    const collapsed = this.trackGroupState.collapsed;
71    let name = this.trackGroupState.name;
72    if (name[0] === '/') {
73      name = StripPathFromExecutable(name);
74    }
75
76    // The shell should be highlighted if the current search result is inside
77    // this track group.
78    let highlightClass = '';
79    const searchIndex = globals.frontendLocalState.searchIndex;
80    if (searchIndex !== -1) {
81      const trackId = globals.currentSearchResults
82                          .trackIds[globals.frontendLocalState.searchIndex];
83      const parentTrackId = getContainingTrackId(globals.state, trackId);
84      if (parentTrackId === attrs.trackGroupId) {
85        highlightClass = 'flash';
86      }
87    }
88
89    const selection = globals.state.currentSelection;
90
91    const trackGroup = globals.state.trackGroups[attrs.trackGroupId];
92    let checkBox = BLANK_CHECKBOX;
93    if (selection !== null && selection.kind === 'AREA') {
94      const selectedArea = globals.state.areas[selection.areaId];
95      if (selectedArea.tracks.includes(attrs.trackGroupId) &&
96          trackGroup.tracks.every(id => selectedArea.tracks.includes(id))) {
97        checkBox = CHECKBOX;
98      } else if (
99          selectedArea.tracks.includes(attrs.trackGroupId) ||
100          trackGroup.tracks.some(id => selectedArea.tracks.includes(id))) {
101        checkBox = INDETERMINATE_CHECKBOX;
102      }
103    }
104
105    return m(
106        `.track-group-panel[collapsed=${collapsed}]`,
107        {id: 'track_' + this.trackGroupId},
108        m(`.shell`,
109          {
110            onclick: (e: MouseEvent) => {
111              globals.dispatch(Actions.toggleTrackGroupCollapsed({
112                trackGroupId: attrs.trackGroupId,
113              })),
114                  e.stopPropagation();
115            },
116            class: `${highlightClass}`,
117          },
118
119          m('.fold-button',
120            m('i.material-icons',
121              this.trackGroupState.collapsed ? EXPAND_DOWN : EXPAND_UP)),
122          m('h1',
123            {
124              title: name,
125            },
126            name),
127          selection && selection.kind === 'AREA' ?
128              m('i.material-icons.track-button',
129                {
130                  onclick: (e: MouseEvent) => {
131                    globals.dispatch(Actions.toggleTrackSelection(
132                        {id: attrs.trackGroupId, isTrackGroup: true}));
133                    e.stopPropagation();
134                  }
135                },
136                checkBox) :
137              ''),
138
139        this.summaryTrack ? m(TrackContent, {track: this.summaryTrack}) : null);
140  }
141
142  oncreate(vnode: m.CVnodeDOM<Attrs>) {
143    this.onupdate(vnode);
144  }
145
146  onupdate({dom}: m.CVnodeDOM<Attrs>) {
147    const shell = assertExists(dom.querySelector('.shell'));
148    this.shellWidth = shell.getBoundingClientRect().width;
149    // TODO(andrewbb): move this to css_constants
150    if (this.trackGroupState.collapsed) {
151      this.backgroundColor =
152          getComputedStyle(dom).getPropertyValue('--collapsed-background');
153    } else {
154      this.backgroundColor =
155          getComputedStyle(dom).getPropertyValue('--expanded-background');
156    }
157  }
158
159  highlightIfTrackSelected(ctx: CanvasRenderingContext2D, size: PanelSize) {
160    const localState = globals.frontendLocalState;
161    const selection = globals.state.currentSelection;
162    if (!selection || selection.kind !== 'AREA') return;
163    const selectedArea = globals.state.areas[selection.areaId];
164    if (selectedArea.tracks.includes(this.trackGroupId)) {
165      ctx.fillStyle = 'rgba(131, 152, 230, 0.3)';
166      ctx.fillRect(
167          localState.timeScale.timeToPx(selectedArea.startSec) +
168              this.shellWidth,
169          0,
170          localState.timeScale.deltaTimeToPx(
171              selectedArea.endSec - selectedArea.startSec),
172          size.height);
173    }
174  }
175
176  renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
177    const collapsed = this.trackGroupState.collapsed;
178
179    ctx.fillStyle = this.backgroundColor;
180    ctx.fillRect(0, 0, size.width, size.height);
181
182    if (!collapsed) return;
183
184    this.highlightIfTrackSelected(ctx, size);
185
186    drawGridLines(
187        ctx,
188        globals.frontendLocalState.timeScale,
189        globals.frontendLocalState.visibleWindowTime,
190        size.width,
191        size.height);
192
193    ctx.save();
194    ctx.translate(this.shellWidth, 0);
195    if (this.summaryTrack) {
196      this.summaryTrack.render(ctx);
197    }
198    ctx.restore();
199
200    this.highlightIfTrackSelected(ctx, size);
201
202    const localState = globals.frontendLocalState;
203    // Draw vertical line when hovering on the notes panel.
204    if (localState.hoveredNoteTimestamp !== -1) {
205      drawVerticalLineAtTime(
206          ctx,
207          localState.timeScale,
208          localState.hoveredNoteTimestamp,
209          size.height,
210          `#aaa`);
211    }
212    if (localState.hoveredLogsTimestamp !== -1) {
213      drawVerticalLineAtTime(
214          ctx,
215          localState.timeScale,
216          localState.hoveredLogsTimestamp,
217          size.height,
218          `#344596`);
219    }
220    if (globals.state.currentSelection !== null) {
221      if (globals.state.currentSelection.kind === 'NOTE') {
222        const note = globals.state.notes[globals.state.currentSelection.id];
223        if (note.noteType === 'DEFAULT') {
224          drawVerticalLineAtTime(
225              ctx,
226              localState.timeScale,
227              note.timestamp,
228              size.height,
229              note.color);
230        }
231      }
232      if (globals.state.currentSelection.kind === 'SLICE' &&
233          globals.sliceDetails.wakeupTs !== undefined) {
234        drawVerticalLineAtTime(
235            ctx,
236            localState.timeScale,
237            globals.sliceDetails.wakeupTs,
238            size.height,
239            `black`);
240      }
241    }
242    // All marked areas should have semi-transparent vertical lines
243    // marking the start and end.
244    for (const note of Object.values(globals.state.notes)) {
245      if (note.noteType === 'AREA') {
246        const transparentNoteColor =
247            'rgba(' + hex.rgb(note.color.substr(1)).toString() + ', 0.65)';
248        drawVerticalLineAtTime(
249            ctx,
250            localState.timeScale,
251            globals.state.areas[note.areaId].startSec,
252            size.height,
253            transparentNoteColor,
254            1);
255        drawVerticalLineAtTime(
256            ctx,
257            localState.timeScale,
258            globals.state.areas[note.areaId].endSec,
259            size.height,
260            transparentNoteColor,
261            1);
262      }
263    }
264  }
265}
266
267function StripPathFromExecutable(path: string) {
268  return path.split('/').slice(-1)[0];
269}
270