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