• 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 m from 'mithril';
16
17import {Icons} from '../base/semantic_icons';
18import {Actions} from '../common/actions';
19import {getContainingGroupKey} from '../common/state';
20import {TrackCacheEntry} from '../common/track_cache';
21import {TrackTags} from '../public';
22
23import {TRACK_SHELL_WIDTH} from './css_constants';
24import {globals} from './globals';
25import {drawGridLines} from './gridline_helper';
26import {PanelSize} from './panel';
27import {Panel} from './panel_container';
28import {
29  CrashButton,
30  renderChips,
31  renderHoveredCursorVertical,
32  renderHoveredNoteVertical,
33  renderNoteVerticals,
34  renderWakeupVertical,
35  TrackContent,
36} from './track_panel';
37import {canvasClip} from '../common/canvas_utils';
38import {Button} from '../widgets/button';
39
40interface Attrs {
41  groupKey: string;
42  title: string;
43  collapsed: boolean;
44  trackFSM?: TrackCacheEntry;
45  tags?: TrackTags;
46  labels?: string[];
47}
48
49export class TrackGroupPanel implements Panel {
50  readonly kind = 'panel';
51  readonly selectable = true;
52  readonly groupKey: string;
53
54  constructor(private attrs: Attrs) {
55    this.groupKey = attrs.groupKey;
56  }
57
58  render(): m.Children {
59    const {groupKey, title, labels, tags, collapsed, trackFSM} = this.attrs;
60
61    let name = title;
62    if (name[0] === '/') {
63      name = StripPathFromExecutable(name);
64    }
65
66    // The shell should be highlighted if the current search result is inside
67    // this track group.
68    let highlightClass = '';
69    const searchIndex = globals.state.searchIndex;
70    if (searchIndex !== -1) {
71      const trackKey = globals.currentSearchResults.trackKeys[searchIndex];
72      const containingGroupKey = getContainingGroupKey(globals.state, trackKey);
73      if (containingGroupKey === groupKey) {
74        highlightClass = 'flash';
75      }
76    }
77
78    const selection = globals.state.selection;
79
80    const trackGroup = globals.state.trackGroups[groupKey];
81    let checkBox = Icons.BlankCheckbox;
82    if (selection.kind === 'area') {
83      if (
84        selection.tracks.includes(groupKey) &&
85        trackGroup.tracks.every((id) => selection.tracks.includes(id))
86      ) {
87        checkBox = Icons.Checkbox;
88      } else if (
89        selection.tracks.includes(groupKey) ||
90        trackGroup.tracks.some((id) => selection.tracks.includes(id))
91      ) {
92        checkBox = Icons.IndeterminateCheckbox;
93      }
94    }
95
96    let child = null;
97    if (labels && labels.length > 0) {
98      child = labels.join(', ');
99    }
100
101    const error = trackFSM?.getError();
102
103    return m(
104      `.track-group-panel[collapsed=${collapsed}]`,
105      {
106        id: 'track_' + groupKey,
107        oncreate: () => this.onupdate(),
108        onupdate: () => this.onupdate(),
109      },
110      m(
111        `.shell`,
112        {
113          onclick: (e: MouseEvent) => {
114            if (e.defaultPrevented) return;
115            globals.dispatch(
116              Actions.toggleTrackGroupCollapsed({
117                groupKey,
118              }),
119            ),
120              e.stopPropagation();
121          },
122          class: `${highlightClass}`,
123        },
124        m(
125          '.fold-button',
126          m('i.material-icons', collapsed ? Icons.ExpandDown : Icons.ExpandUp),
127        ),
128        m(
129          '.title-wrapper',
130          m('h1.track-title', {title: name}, name, renderChips(tags)),
131          collapsed && child !== null ? m('h2.track-subtitle', child) : null,
132        ),
133        m(
134          '.track-buttons',
135          error && m(CrashButton, {error}),
136          selection.kind === 'area' &&
137            m(Button, {
138              onclick: (e: MouseEvent) => {
139                globals.dispatch(
140                  Actions.toggleTrackSelection({
141                    key: groupKey,
142                    isTrackGroup: true,
143                  }),
144                );
145                e.stopPropagation();
146              },
147              icon: checkBox,
148              compact: true,
149            }),
150        ),
151      ),
152      trackFSM
153        ? m(
154            TrackContent,
155            {
156              track: trackFSM.track,
157              hasError: Boolean(trackFSM.getError()),
158              height: this.attrs.trackFSM?.track.getHeight(),
159            },
160            !collapsed && child !== null ? m('span', child) : null,
161          )
162        : null,
163    );
164  }
165
166  private onupdate() {
167    if (this.attrs.trackFSM !== undefined) {
168      this.attrs.trackFSM.track.onFullRedraw?.();
169    }
170  }
171
172  highlightIfTrackSelected(ctx: CanvasRenderingContext2D, size: PanelSize) {
173    const {visibleTimeScale} = globals.timeline;
174    const selection = globals.state.selection;
175    if (selection.kind !== 'area') return;
176    const selectedAreaDuration = selection.end - selection.start;
177    if (selection.tracks.includes(this.groupKey)) {
178      ctx.fillStyle = 'rgba(131, 152, 230, 0.3)';
179      ctx.fillRect(
180        visibleTimeScale.timeToPx(selection.start) + TRACK_SHELL_WIDTH,
181        0,
182        visibleTimeScale.durationToPx(selectedAreaDuration),
183        size.height,
184      );
185    }
186  }
187
188  renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
189    const {collapsed, trackFSM: track} = this.attrs;
190
191    if (!collapsed) return;
192
193    ctx.save();
194    canvasClip(
195      ctx,
196      TRACK_SHELL_WIDTH,
197      0,
198      size.width - TRACK_SHELL_WIDTH,
199      size.height,
200    );
201    drawGridLines(ctx, size.width, size.height);
202
203    if (track) {
204      ctx.save();
205      ctx.translate(TRACK_SHELL_WIDTH, 0);
206      const trackSize = {...size, width: size.width - TRACK_SHELL_WIDTH};
207      if (!track.getError()) {
208        track.update();
209        track.track.render(ctx, trackSize);
210      }
211      ctx.restore();
212    }
213
214    this.highlightIfTrackSelected(ctx, size);
215
216    const {visibleTimeScale} = globals.timeline;
217    // Draw vertical line when hovering on the notes panel.
218    renderHoveredNoteVertical(ctx, visibleTimeScale, size);
219    renderHoveredCursorVertical(ctx, visibleTimeScale, size);
220    renderWakeupVertical(ctx, visibleTimeScale, size);
221    renderNoteVerticals(ctx, visibleTimeScale, size);
222
223    ctx.restore();
224  }
225}
226
227function StripPathFromExecutable(path: string) {
228  return path.split('/').slice(-1)[0];
229}
230