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