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 drawVerticalSelection, 42} from './vertical_line_helper'; 43 44interface Attrs { 45 trackGroupId: string; 46 selectable: boolean; 47} 48 49export class TrackGroupPanel extends Panel<Attrs> { 50 private readonly trackGroupId: string; 51 private shellWidth = 0; 52 private backgroundColor = '#ffffff'; // Updated from CSS later. 53 private summaryTrack: Track; 54 55 constructor({attrs}: m.CVnode<Attrs>) { 56 super(); 57 this.trackGroupId = attrs.trackGroupId; 58 const trackCreator = trackRegistry.get(this.summaryTrackState.kind); 59 this.summaryTrack = trackCreator.create(this.summaryTrackState); 60 } 61 62 get trackGroupState(): TrackGroupState { 63 return assertExists(globals.state.trackGroups[this.trackGroupId]); 64 } 65 66 get summaryTrackState(): TrackState { 67 return assertExists( 68 globals.state.tracks[this.trackGroupState.summaryTrackId]); 69 } 70 71 view({attrs}: m.CVnode<Attrs>) { 72 const collapsed = this.trackGroupState.collapsed; 73 let name = this.trackGroupState.name; 74 if (name[0] === '/') { 75 name = StripPathFromExecutable(name); 76 } 77 78 // The shell should be highlighted if the current search result is inside 79 // this track group. 80 let highlightClass = ''; 81 const searchIndex = globals.frontendLocalState.searchIndex; 82 if (searchIndex !== -1) { 83 const trackId = globals.currentSearchResults 84 .trackIds[globals.frontendLocalState.searchIndex]; 85 const parentTrackId = getContainingTrackId(globals.state, trackId); 86 if (parentTrackId === attrs.trackGroupId) { 87 highlightClass = 'flash'; 88 } 89 } 90 91 const selectedArea = globals.frontendLocalState.selectedArea.area; 92 const trackGroup = globals.state.trackGroups[attrs.trackGroupId]; 93 let checkBox = BLANK_CHECKBOX; 94 if (selectedArea) { 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 selectedArea ? m('i.material-icons.track-button', 128 { 129 onclick: (e: MouseEvent) => { 130 globals.frontendLocalState.toggleTrackSelection( 131 attrs.trackGroupId, true /*trackGroup*/); 132 e.stopPropagation(); 133 } 134 }, 135 checkBox) : 136 ''), 137 138 this.summaryTrack ? m(TrackContent, {track: this.summaryTrack}) : null); 139 } 140 141 oncreate(vnode: m.CVnodeDOM<Attrs>) { 142 this.onupdate(vnode); 143 } 144 145 onupdate({dom}: m.CVnodeDOM<Attrs>) { 146 const shell = assertExists(dom.querySelector('.shell')); 147 this.shellWidth = shell.getBoundingClientRect().width; 148 this.backgroundColor = 149 getComputedStyle(dom).getPropertyValue('--collapsed-background'); 150 } 151 152 highlightIfTrackSelected(ctx: CanvasRenderingContext2D, size: PanelSize) { 153 const localState = globals.frontendLocalState; 154 const area = localState.selectedArea.area; 155 if (area && area.tracks.includes(this.trackGroupId)) { 156 ctx.fillStyle = '#ebeef9'; 157 ctx.fillRect( 158 localState.timeScale.timeToPx(area.startSec) + this.shellWidth, 159 0, 160 localState.timeScale.deltaTimeToPx(area.endSec - area.startSec), 161 size.height); 162 } 163 } 164 165 renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) { 166 const collapsed = this.trackGroupState.collapsed; 167 if (!collapsed) return; 168 169 ctx.save(); 170 171 ctx.fillStyle = this.backgroundColor; 172 ctx.fillRect(0, 0, size.width, size.height); 173 174 this.highlightIfTrackSelected(ctx, size); 175 176 drawGridLines( 177 ctx, 178 globals.frontendLocalState.timeScale, 179 globals.frontendLocalState.visibleWindowTime, 180 size.width, 181 size.height); 182 183 ctx.translate(this.shellWidth, 0); 184 185 if (this.summaryTrack) { 186 this.summaryTrack.render(ctx); 187 } 188 ctx.restore(); 189 190 const localState = globals.frontendLocalState; 191 // Draw vertical line when hovering on the notes panel. 192 if (localState.hoveredNoteTimestamp !== -1) { 193 drawVerticalLineAtTime( 194 ctx, 195 localState.timeScale, 196 localState.hoveredNoteTimestamp, 197 size.height, 198 `#aaa`); 199 } 200 if (localState.hoveredLogsTimestamp !== -1) { 201 drawVerticalLineAtTime( 202 ctx, 203 localState.timeScale, 204 localState.hoveredLogsTimestamp, 205 size.height, 206 `rgb(52,69,150)`); 207 } 208 if (localState.selectedArea.area !== undefined && 209 !globals.frontendLocalState.selectingArea) { 210 drawVerticalSelection( 211 ctx, 212 localState.timeScale, 213 localState.selectedArea.area.startSec, 214 localState.selectedArea.area.endSec, 215 size.height, 216 `rgba(0,0,0,0.5)`); 217 } 218 if (globals.state.currentSelection !== null) { 219 if (globals.state.currentSelection.kind === 'NOTE') { 220 const note = globals.state.notes[globals.state.currentSelection.id]; 221 drawVerticalLineAtTime(ctx, 222 localState.timeScale, 223 note.timestamp, 224 size.height, 225 note.color); 226 if (note.noteType === 'AREA') { 227 drawVerticalLineAtTime( 228 ctx, 229 localState.timeScale, 230 note.area.endSec, 231 size.height, 232 note.color); 233 } 234 } 235 if (globals.state.currentSelection.kind === 'SLICE' && 236 globals.sliceDetails.wakeupTs !== undefined) { 237 drawVerticalLineAtTime( 238 ctx, 239 localState.timeScale, 240 globals.sliceDetails.wakeupTs, 241 size.height, 242 `black`); 243 } 244 } 245 // All marked areas should have semi-transparent vertical lines 246 // marking the start and end. 247 for (const note of Object.values(globals.state.notes)) { 248 if (note.noteType === 'AREA') { 249 const transparentNoteColor = 250 'rgba(' + hex.rgb(note.color.substr(1)).toString() + ', 0.65)'; 251 drawVerticalLineAtTime( 252 ctx, 253 localState.timeScale, 254 note.area.startSec, 255 size.height, 256 transparentNoteColor, 257 1); 258 drawVerticalLineAtTime( 259 ctx, 260 localState.timeScale, 261 note.area.endSec, 262 size.height, 263 transparentNoteColor, 264 1); 265 } 266 } 267 } 268} 269 270function StripPathFromExecutable(path: string) { 271 return path.split('/').slice(-1)[0]; 272} 273