1*6dbdd20aSAndroid Build Coastguard Worker// Copyright (C) 2019 The Android Open Source Project 2*6dbdd20aSAndroid Build Coastguard Worker// 3*6dbdd20aSAndroid Build Coastguard Worker// Licensed under the Apache License, Version 2.0 (the "License"); 4*6dbdd20aSAndroid Build Coastguard Worker// you may not use size file except in compliance with the License. 5*6dbdd20aSAndroid Build Coastguard Worker// You may obtain a copy of the License at 6*6dbdd20aSAndroid Build Coastguard Worker// 7*6dbdd20aSAndroid Build Coastguard Worker// http://www.apache.org/licenses/LICENSE-2.0 8*6dbdd20aSAndroid Build Coastguard Worker// 9*6dbdd20aSAndroid Build Coastguard Worker// Unless required by applicable law or agreed to in writing, software 10*6dbdd20aSAndroid Build Coastguard Worker// distributed under the License is distributed on an "AS IS" BASIS, 11*6dbdd20aSAndroid Build Coastguard Worker// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12*6dbdd20aSAndroid Build Coastguard Worker// See the License for the specific language governing permissions and 13*6dbdd20aSAndroid Build Coastguard Worker// limitations under the License. 14*6dbdd20aSAndroid Build Coastguard Worker 15*6dbdd20aSAndroid Build Coastguard Workerimport m from 'mithril'; 16*6dbdd20aSAndroid Build Coastguard Workerimport {currentTargetOffset} from '../base/dom_utils'; 17*6dbdd20aSAndroid Build Coastguard Workerimport {Icons} from '../base/semantic_icons'; 18*6dbdd20aSAndroid Build Coastguard Workerimport {randomColor} from '../components/colorizer'; 19*6dbdd20aSAndroid Build Coastguard Workerimport {SpanNote, Note} from '../public/note'; 20*6dbdd20aSAndroid Build Coastguard Workerimport {raf} from '../core/raf_scheduler'; 21*6dbdd20aSAndroid Build Coastguard Workerimport {Button, ButtonBar} from '../widgets/button'; 22*6dbdd20aSAndroid Build Coastguard Workerimport {TRACK_SHELL_WIDTH} from './css_constants'; 23*6dbdd20aSAndroid Build Coastguard Workerimport {getMaxMajorTicks, generateTicks, TickType} from './gridline_helper'; 24*6dbdd20aSAndroid Build Coastguard Workerimport {Size2D} from '../base/geom'; 25*6dbdd20aSAndroid Build Coastguard Workerimport {Panel} from './panel_container'; 26*6dbdd20aSAndroid Build Coastguard Workerimport {Timestamp} from '../components/widgets/timestamp'; 27*6dbdd20aSAndroid Build Coastguard Workerimport {assertUnreachable} from '../base/logging'; 28*6dbdd20aSAndroid Build Coastguard Workerimport {DetailsPanel} from '../public/details_panel'; 29*6dbdd20aSAndroid Build Coastguard Workerimport {TimeScale} from '../base/time_scale'; 30*6dbdd20aSAndroid Build Coastguard Workerimport {canvasClip} from '../base/canvas_utils'; 31*6dbdd20aSAndroid Build Coastguard Workerimport {Selection} from '../public/selection'; 32*6dbdd20aSAndroid Build Coastguard Workerimport {TraceImpl} from '../core/trace_impl'; 33*6dbdd20aSAndroid Build Coastguard Worker 34*6dbdd20aSAndroid Build Coastguard Workerconst FLAG_WIDTH = 16; 35*6dbdd20aSAndroid Build Coastguard Workerconst AREA_TRIANGLE_WIDTH = 10; 36*6dbdd20aSAndroid Build Coastguard Workerconst FLAG = `\uE153`; 37*6dbdd20aSAndroid Build Coastguard Worker 38*6dbdd20aSAndroid Build Coastguard Workerfunction toSummary(s: string) { 39*6dbdd20aSAndroid Build Coastguard Worker const newlineIndex = s.indexOf('\n') > 0 ? s.indexOf('\n') : s.length; 40*6dbdd20aSAndroid Build Coastguard Worker return s.slice(0, Math.min(newlineIndex, s.length, 16)); 41*6dbdd20aSAndroid Build Coastguard Worker} 42*6dbdd20aSAndroid Build Coastguard Worker 43*6dbdd20aSAndroid Build Coastguard Workerfunction getStartTimestamp(note: Note | SpanNote) { 44*6dbdd20aSAndroid Build Coastguard Worker const noteType = note.noteType; 45*6dbdd20aSAndroid Build Coastguard Worker switch (noteType) { 46*6dbdd20aSAndroid Build Coastguard Worker case 'SPAN': 47*6dbdd20aSAndroid Build Coastguard Worker return note.start; 48*6dbdd20aSAndroid Build Coastguard Worker case 'DEFAULT': 49*6dbdd20aSAndroid Build Coastguard Worker return note.timestamp; 50*6dbdd20aSAndroid Build Coastguard Worker default: 51*6dbdd20aSAndroid Build Coastguard Worker assertUnreachable(noteType); 52*6dbdd20aSAndroid Build Coastguard Worker } 53*6dbdd20aSAndroid Build Coastguard Worker} 54*6dbdd20aSAndroid Build Coastguard Worker 55*6dbdd20aSAndroid Build Coastguard Workerexport class NotesPanel implements Panel { 56*6dbdd20aSAndroid Build Coastguard Worker readonly kind = 'panel'; 57*6dbdd20aSAndroid Build Coastguard Worker readonly selectable = false; 58*6dbdd20aSAndroid Build Coastguard Worker private readonly trace: TraceImpl; 59*6dbdd20aSAndroid Build Coastguard Worker private timescale?: TimeScale; // The timescale from the last render() 60*6dbdd20aSAndroid Build Coastguard Worker private hoveredX: null | number = null; 61*6dbdd20aSAndroid Build Coastguard Worker private mouseDragging = false; 62*6dbdd20aSAndroid Build Coastguard Worker 63*6dbdd20aSAndroid Build Coastguard Worker constructor(trace: TraceImpl) { 64*6dbdd20aSAndroid Build Coastguard Worker this.trace = trace; 65*6dbdd20aSAndroid Build Coastguard Worker } 66*6dbdd20aSAndroid Build Coastguard Worker 67*6dbdd20aSAndroid Build Coastguard Worker render(): m.Children { 68*6dbdd20aSAndroid Build Coastguard Worker const allCollapsed = this.trace.workspace.flatTracks.every( 69*6dbdd20aSAndroid Build Coastguard Worker (n) => n.collapsed, 70*6dbdd20aSAndroid Build Coastguard Worker ); 71*6dbdd20aSAndroid Build Coastguard Worker 72*6dbdd20aSAndroid Build Coastguard Worker return m( 73*6dbdd20aSAndroid Build Coastguard Worker '.notes-panel', 74*6dbdd20aSAndroid Build Coastguard Worker { 75*6dbdd20aSAndroid Build Coastguard Worker onmousedown: () => { 76*6dbdd20aSAndroid Build Coastguard Worker // If the user clicks & drags, very likely they just want to measure 77*6dbdd20aSAndroid Build Coastguard Worker // the time horizontally, not set a flag. This debouncing is done to 78*6dbdd20aSAndroid Build Coastguard Worker // avoid setting accidental flags like measuring the time on the brush 79*6dbdd20aSAndroid Build Coastguard Worker // timeline. 80*6dbdd20aSAndroid Build Coastguard Worker this.mouseDragging = false; 81*6dbdd20aSAndroid Build Coastguard Worker }, 82*6dbdd20aSAndroid Build Coastguard Worker onclick: (e: MouseEvent) => { 83*6dbdd20aSAndroid Build Coastguard Worker if (!this.mouseDragging) { 84*6dbdd20aSAndroid Build Coastguard Worker const x = currentTargetOffset(e).x - TRACK_SHELL_WIDTH; 85*6dbdd20aSAndroid Build Coastguard Worker this.onClick(x); 86*6dbdd20aSAndroid Build Coastguard Worker e.stopPropagation(); 87*6dbdd20aSAndroid Build Coastguard Worker } 88*6dbdd20aSAndroid Build Coastguard Worker }, 89*6dbdd20aSAndroid Build Coastguard Worker onmousemove: (e: MouseEvent) => { 90*6dbdd20aSAndroid Build Coastguard Worker this.mouseDragging = true; 91*6dbdd20aSAndroid Build Coastguard Worker this.hoveredX = currentTargetOffset(e).x - TRACK_SHELL_WIDTH; 92*6dbdd20aSAndroid Build Coastguard Worker raf.scheduleCanvasRedraw(); 93*6dbdd20aSAndroid Build Coastguard Worker }, 94*6dbdd20aSAndroid Build Coastguard Worker onmouseenter: (e: MouseEvent) => { 95*6dbdd20aSAndroid Build Coastguard Worker this.hoveredX = currentTargetOffset(e).x - TRACK_SHELL_WIDTH; 96*6dbdd20aSAndroid Build Coastguard Worker raf.scheduleCanvasRedraw(); 97*6dbdd20aSAndroid Build Coastguard Worker }, 98*6dbdd20aSAndroid Build Coastguard Worker onmouseout: () => { 99*6dbdd20aSAndroid Build Coastguard Worker this.hoveredX = null; 100*6dbdd20aSAndroid Build Coastguard Worker this.trace.timeline.hoveredNoteTimestamp = undefined; 101*6dbdd20aSAndroid Build Coastguard Worker }, 102*6dbdd20aSAndroid Build Coastguard Worker }, 103*6dbdd20aSAndroid Build Coastguard Worker m( 104*6dbdd20aSAndroid Build Coastguard Worker ButtonBar, 105*6dbdd20aSAndroid Build Coastguard Worker {className: 'pf-toolbar'}, 106*6dbdd20aSAndroid Build Coastguard Worker m(Button, { 107*6dbdd20aSAndroid Build Coastguard Worker onclick: (e: Event) => { 108*6dbdd20aSAndroid Build Coastguard Worker e.preventDefault(); 109*6dbdd20aSAndroid Build Coastguard Worker if (allCollapsed) { 110*6dbdd20aSAndroid Build Coastguard Worker this.trace.commands.runCommand( 111*6dbdd20aSAndroid Build Coastguard Worker 'perfetto.CoreCommands#ExpandAllGroups', 112*6dbdd20aSAndroid Build Coastguard Worker ); 113*6dbdd20aSAndroid Build Coastguard Worker } else { 114*6dbdd20aSAndroid Build Coastguard Worker this.trace.commands.runCommand( 115*6dbdd20aSAndroid Build Coastguard Worker 'perfetto.CoreCommands#CollapseAllGroups', 116*6dbdd20aSAndroid Build Coastguard Worker ); 117*6dbdd20aSAndroid Build Coastguard Worker } 118*6dbdd20aSAndroid Build Coastguard Worker }, 119*6dbdd20aSAndroid Build Coastguard Worker title: allCollapsed ? 'Expand all' : 'Collapse all', 120*6dbdd20aSAndroid Build Coastguard Worker icon: allCollapsed ? 'unfold_more' : 'unfold_less', 121*6dbdd20aSAndroid Build Coastguard Worker compact: true, 122*6dbdd20aSAndroid Build Coastguard Worker }), 123*6dbdd20aSAndroid Build Coastguard Worker m(Button, { 124*6dbdd20aSAndroid Build Coastguard Worker onclick: (e: Event) => { 125*6dbdd20aSAndroid Build Coastguard Worker e.preventDefault(); 126*6dbdd20aSAndroid Build Coastguard Worker this.trace.workspace.pinnedTracks.forEach((t) => 127*6dbdd20aSAndroid Build Coastguard Worker this.trace.workspace.unpinTrack(t), 128*6dbdd20aSAndroid Build Coastguard Worker ); 129*6dbdd20aSAndroid Build Coastguard Worker raf.scheduleFullRedraw(); 130*6dbdd20aSAndroid Build Coastguard Worker }, 131*6dbdd20aSAndroid Build Coastguard Worker title: 'Clear all pinned tracks', 132*6dbdd20aSAndroid Build Coastguard Worker icon: 'clear_all', 133*6dbdd20aSAndroid Build Coastguard Worker compact: true, 134*6dbdd20aSAndroid Build Coastguard Worker }), 135*6dbdd20aSAndroid Build Coastguard Worker // TODO(stevegolton): Re-introduce this when we fix track filtering 136*6dbdd20aSAndroid Build Coastguard Worker // m(TextInput, { 137*6dbdd20aSAndroid Build Coastguard Worker // placeholder: 'Filter tracks...', 138*6dbdd20aSAndroid Build Coastguard Worker // title: 139*6dbdd20aSAndroid Build Coastguard Worker // 'Track filter - enter one or more comma-separated search terms', 140*6dbdd20aSAndroid Build Coastguard Worker // value: this.trace.state.trackFilterTerm, 141*6dbdd20aSAndroid Build Coastguard Worker // oninput: (e: Event) => { 142*6dbdd20aSAndroid Build Coastguard Worker // const filterTerm = (e.target as HTMLInputElement).value; 143*6dbdd20aSAndroid Build Coastguard Worker // this.trace.dispatch(Actions.setTrackFilterTerm({filterTerm})); 144*6dbdd20aSAndroid Build Coastguard Worker // }, 145*6dbdd20aSAndroid Build Coastguard Worker // }), 146*6dbdd20aSAndroid Build Coastguard Worker // m(Button, { 147*6dbdd20aSAndroid Build Coastguard Worker // type: 'reset', 148*6dbdd20aSAndroid Build Coastguard Worker // icon: 'backspace', 149*6dbdd20aSAndroid Build Coastguard Worker // onclick: () => { 150*6dbdd20aSAndroid Build Coastguard Worker // this.trace.dispatch( 151*6dbdd20aSAndroid Build Coastguard Worker // Actions.setTrackFilterTerm({filterTerm: undefined}), 152*6dbdd20aSAndroid Build Coastguard Worker // ); 153*6dbdd20aSAndroid Build Coastguard Worker // }, 154*6dbdd20aSAndroid Build Coastguard Worker // title: 'Clear track filter', 155*6dbdd20aSAndroid Build Coastguard Worker // }), 156*6dbdd20aSAndroid Build Coastguard Worker ), 157*6dbdd20aSAndroid Build Coastguard Worker ); 158*6dbdd20aSAndroid Build Coastguard Worker } 159*6dbdd20aSAndroid Build Coastguard Worker 160*6dbdd20aSAndroid Build Coastguard Worker renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D) { 161*6dbdd20aSAndroid Build Coastguard Worker ctx.fillStyle = '#999'; 162*6dbdd20aSAndroid Build Coastguard Worker ctx.fillRect(TRACK_SHELL_WIDTH - 2, 0, 2, size.height); 163*6dbdd20aSAndroid Build Coastguard Worker 164*6dbdd20aSAndroid Build Coastguard Worker const trackSize = {...size, width: size.width - TRACK_SHELL_WIDTH}; 165*6dbdd20aSAndroid Build Coastguard Worker 166*6dbdd20aSAndroid Build Coastguard Worker ctx.save(); 167*6dbdd20aSAndroid Build Coastguard Worker ctx.translate(TRACK_SHELL_WIDTH, 0); 168*6dbdd20aSAndroid Build Coastguard Worker canvasClip(ctx, 0, 0, trackSize.width, trackSize.height); 169*6dbdd20aSAndroid Build Coastguard Worker this.renderPanel(ctx, trackSize); 170*6dbdd20aSAndroid Build Coastguard Worker ctx.restore(); 171*6dbdd20aSAndroid Build Coastguard Worker } 172*6dbdd20aSAndroid Build Coastguard Worker 173*6dbdd20aSAndroid Build Coastguard Worker private renderPanel(ctx: CanvasRenderingContext2D, size: Size2D): void { 174*6dbdd20aSAndroid Build Coastguard Worker let aNoteIsHovered = false; 175*6dbdd20aSAndroid Build Coastguard Worker 176*6dbdd20aSAndroid Build Coastguard Worker const visibleWindow = this.trace.timeline.visibleWindow; 177*6dbdd20aSAndroid Build Coastguard Worker const timescale = new TimeScale(visibleWindow, { 178*6dbdd20aSAndroid Build Coastguard Worker left: 0, 179*6dbdd20aSAndroid Build Coastguard Worker right: size.width, 180*6dbdd20aSAndroid Build Coastguard Worker }); 181*6dbdd20aSAndroid Build Coastguard Worker const timespan = visibleWindow.toTimeSpan(); 182*6dbdd20aSAndroid Build Coastguard Worker 183*6dbdd20aSAndroid Build Coastguard Worker this.timescale = timescale; 184*6dbdd20aSAndroid Build Coastguard Worker 185*6dbdd20aSAndroid Build Coastguard Worker if (size.width > 0 && timespan.duration > 0n) { 186*6dbdd20aSAndroid Build Coastguard Worker const maxMajorTicks = getMaxMajorTicks(size.width); 187*6dbdd20aSAndroid Build Coastguard Worker const offset = this.trace.timeline.timestampOffset(); 188*6dbdd20aSAndroid Build Coastguard Worker const tickGen = generateTicks(timespan, maxMajorTicks, offset); 189*6dbdd20aSAndroid Build Coastguard Worker for (const {type, time} of tickGen) { 190*6dbdd20aSAndroid Build Coastguard Worker const px = Math.floor(timescale.timeToPx(time)); 191*6dbdd20aSAndroid Build Coastguard Worker if (type === TickType.MAJOR) { 192*6dbdd20aSAndroid Build Coastguard Worker ctx.fillRect(px, 0, 1, size.height); 193*6dbdd20aSAndroid Build Coastguard Worker } 194*6dbdd20aSAndroid Build Coastguard Worker } 195*6dbdd20aSAndroid Build Coastguard Worker } 196*6dbdd20aSAndroid Build Coastguard Worker 197*6dbdd20aSAndroid Build Coastguard Worker ctx.textBaseline = 'bottom'; 198*6dbdd20aSAndroid Build Coastguard Worker ctx.font = '10px Helvetica'; 199*6dbdd20aSAndroid Build Coastguard Worker 200*6dbdd20aSAndroid Build Coastguard Worker for (const note of this.trace.notes.notes.values()) { 201*6dbdd20aSAndroid Build Coastguard Worker const timestamp = getStartTimestamp(note); 202*6dbdd20aSAndroid Build Coastguard Worker // TODO(hjd): We should still render area selection marks in viewport is 203*6dbdd20aSAndroid Build Coastguard Worker // *within* the area (e.g. both lhs and rhs are out of bounds). 204*6dbdd20aSAndroid Build Coastguard Worker if ( 205*6dbdd20aSAndroid Build Coastguard Worker (note.noteType === 'DEFAULT' && 206*6dbdd20aSAndroid Build Coastguard Worker !visibleWindow.contains(note.timestamp)) || 207*6dbdd20aSAndroid Build Coastguard Worker (note.noteType === 'SPAN' && 208*6dbdd20aSAndroid Build Coastguard Worker !visibleWindow.overlaps(note.start, note.end)) 209*6dbdd20aSAndroid Build Coastguard Worker ) { 210*6dbdd20aSAndroid Build Coastguard Worker continue; 211*6dbdd20aSAndroid Build Coastguard Worker } 212*6dbdd20aSAndroid Build Coastguard Worker const currentIsHovered = 213*6dbdd20aSAndroid Build Coastguard Worker this.hoveredX !== null && this.hitTestNote(this.hoveredX, note); 214*6dbdd20aSAndroid Build Coastguard Worker if (currentIsHovered) aNoteIsHovered = true; 215*6dbdd20aSAndroid Build Coastguard Worker 216*6dbdd20aSAndroid Build Coastguard Worker const selection = this.trace.selection.selection; 217*6dbdd20aSAndroid Build Coastguard Worker const isSelected = selection.kind === 'note' && selection.id === note.id; 218*6dbdd20aSAndroid Build Coastguard Worker const x = timescale.timeToPx(timestamp); 219*6dbdd20aSAndroid Build Coastguard Worker const left = Math.floor(x); 220*6dbdd20aSAndroid Build Coastguard Worker 221*6dbdd20aSAndroid Build Coastguard Worker // Draw flag or marker. 222*6dbdd20aSAndroid Build Coastguard Worker if (note.noteType === 'SPAN') { 223*6dbdd20aSAndroid Build Coastguard Worker this.drawAreaMarker( 224*6dbdd20aSAndroid Build Coastguard Worker ctx, 225*6dbdd20aSAndroid Build Coastguard Worker left, 226*6dbdd20aSAndroid Build Coastguard Worker Math.floor(timescale.timeToPx(note.end)), 227*6dbdd20aSAndroid Build Coastguard Worker note.color, 228*6dbdd20aSAndroid Build Coastguard Worker isSelected, 229*6dbdd20aSAndroid Build Coastguard Worker ); 230*6dbdd20aSAndroid Build Coastguard Worker } else { 231*6dbdd20aSAndroid Build Coastguard Worker this.drawFlag(ctx, left, size.height, note.color, isSelected); 232*6dbdd20aSAndroid Build Coastguard Worker } 233*6dbdd20aSAndroid Build Coastguard Worker 234*6dbdd20aSAndroid Build Coastguard Worker if (note.text) { 235*6dbdd20aSAndroid Build Coastguard Worker const summary = toSummary(note.text); 236*6dbdd20aSAndroid Build Coastguard Worker const measured = ctx.measureText(summary); 237*6dbdd20aSAndroid Build Coastguard Worker // Add a white semi-transparent background for the text. 238*6dbdd20aSAndroid Build Coastguard Worker ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; 239*6dbdd20aSAndroid Build Coastguard Worker ctx.fillRect( 240*6dbdd20aSAndroid Build Coastguard Worker left + FLAG_WIDTH + 2, 241*6dbdd20aSAndroid Build Coastguard Worker size.height + 2, 242*6dbdd20aSAndroid Build Coastguard Worker measured.width + 2, 243*6dbdd20aSAndroid Build Coastguard Worker -12, 244*6dbdd20aSAndroid Build Coastguard Worker ); 245*6dbdd20aSAndroid Build Coastguard Worker ctx.fillStyle = '#3c4b5d'; 246*6dbdd20aSAndroid Build Coastguard Worker ctx.fillText(summary, left + FLAG_WIDTH + 3, size.height + 1); 247*6dbdd20aSAndroid Build Coastguard Worker } 248*6dbdd20aSAndroid Build Coastguard Worker } 249*6dbdd20aSAndroid Build Coastguard Worker 250*6dbdd20aSAndroid Build Coastguard Worker // A real note is hovered so we don't need to see the preview line. 251*6dbdd20aSAndroid Build Coastguard Worker // TODO(hjd): Change cursor to pointer here. 252*6dbdd20aSAndroid Build Coastguard Worker if (aNoteIsHovered) { 253*6dbdd20aSAndroid Build Coastguard Worker this.trace.timeline.hoveredNoteTimestamp = undefined; 254*6dbdd20aSAndroid Build Coastguard Worker } 255*6dbdd20aSAndroid Build Coastguard Worker 256*6dbdd20aSAndroid Build Coastguard Worker // View preview note flag when hovering on notes panel. 257*6dbdd20aSAndroid Build Coastguard Worker if (!aNoteIsHovered && this.hoveredX !== null) { 258*6dbdd20aSAndroid Build Coastguard Worker const timestamp = timescale.pxToHpTime(this.hoveredX).toTime(); 259*6dbdd20aSAndroid Build Coastguard Worker if (visibleWindow.contains(timestamp)) { 260*6dbdd20aSAndroid Build Coastguard Worker this.trace.timeline.hoveredNoteTimestamp = timestamp; 261*6dbdd20aSAndroid Build Coastguard Worker const x = timescale.timeToPx(timestamp); 262*6dbdd20aSAndroid Build Coastguard Worker const left = Math.floor(x); 263*6dbdd20aSAndroid Build Coastguard Worker this.drawFlag(ctx, left, size.height, '#aaa', /* fill */ true); 264*6dbdd20aSAndroid Build Coastguard Worker } 265*6dbdd20aSAndroid Build Coastguard Worker } 266*6dbdd20aSAndroid Build Coastguard Worker 267*6dbdd20aSAndroid Build Coastguard Worker ctx.restore(); 268*6dbdd20aSAndroid Build Coastguard Worker } 269*6dbdd20aSAndroid Build Coastguard Worker 270*6dbdd20aSAndroid Build Coastguard Worker private drawAreaMarker( 271*6dbdd20aSAndroid Build Coastguard Worker ctx: CanvasRenderingContext2D, 272*6dbdd20aSAndroid Build Coastguard Worker x: number, 273*6dbdd20aSAndroid Build Coastguard Worker xEnd: number, 274*6dbdd20aSAndroid Build Coastguard Worker color: string, 275*6dbdd20aSAndroid Build Coastguard Worker fill: boolean, 276*6dbdd20aSAndroid Build Coastguard Worker ) { 277*6dbdd20aSAndroid Build Coastguard Worker ctx.fillStyle = color; 278*6dbdd20aSAndroid Build Coastguard Worker ctx.strokeStyle = color; 279*6dbdd20aSAndroid Build Coastguard Worker const topOffset = 10; 280*6dbdd20aSAndroid Build Coastguard Worker // Don't draw in the track shell section. 281*6dbdd20aSAndroid Build Coastguard Worker if (x >= 0) { 282*6dbdd20aSAndroid Build Coastguard Worker // Draw left triangle. 283*6dbdd20aSAndroid Build Coastguard Worker ctx.beginPath(); 284*6dbdd20aSAndroid Build Coastguard Worker ctx.moveTo(x, topOffset); 285*6dbdd20aSAndroid Build Coastguard Worker ctx.lineTo(x, topOffset + AREA_TRIANGLE_WIDTH); 286*6dbdd20aSAndroid Build Coastguard Worker ctx.lineTo(x + AREA_TRIANGLE_WIDTH, topOffset); 287*6dbdd20aSAndroid Build Coastguard Worker ctx.lineTo(x, topOffset); 288*6dbdd20aSAndroid Build Coastguard Worker if (fill) ctx.fill(); 289*6dbdd20aSAndroid Build Coastguard Worker ctx.stroke(); 290*6dbdd20aSAndroid Build Coastguard Worker } 291*6dbdd20aSAndroid Build Coastguard Worker // Draw right triangle. 292*6dbdd20aSAndroid Build Coastguard Worker ctx.beginPath(); 293*6dbdd20aSAndroid Build Coastguard Worker ctx.moveTo(xEnd, topOffset); 294*6dbdd20aSAndroid Build Coastguard Worker ctx.lineTo(xEnd, topOffset + AREA_TRIANGLE_WIDTH); 295*6dbdd20aSAndroid Build Coastguard Worker ctx.lineTo(xEnd - AREA_TRIANGLE_WIDTH, topOffset); 296*6dbdd20aSAndroid Build Coastguard Worker ctx.lineTo(xEnd, topOffset); 297*6dbdd20aSAndroid Build Coastguard Worker if (fill) ctx.fill(); 298*6dbdd20aSAndroid Build Coastguard Worker ctx.stroke(); 299*6dbdd20aSAndroid Build Coastguard Worker 300*6dbdd20aSAndroid Build Coastguard Worker // Start line after track shell section, join triangles. 301*6dbdd20aSAndroid Build Coastguard Worker const startDraw = Math.max(x, 0); 302*6dbdd20aSAndroid Build Coastguard Worker ctx.beginPath(); 303*6dbdd20aSAndroid Build Coastguard Worker ctx.moveTo(startDraw, topOffset); 304*6dbdd20aSAndroid Build Coastguard Worker ctx.lineTo(xEnd, topOffset); 305*6dbdd20aSAndroid Build Coastguard Worker ctx.stroke(); 306*6dbdd20aSAndroid Build Coastguard Worker } 307*6dbdd20aSAndroid Build Coastguard Worker 308*6dbdd20aSAndroid Build Coastguard Worker private drawFlag( 309*6dbdd20aSAndroid Build Coastguard Worker ctx: CanvasRenderingContext2D, 310*6dbdd20aSAndroid Build Coastguard Worker x: number, 311*6dbdd20aSAndroid Build Coastguard Worker height: number, 312*6dbdd20aSAndroid Build Coastguard Worker color: string, 313*6dbdd20aSAndroid Build Coastguard Worker fill?: boolean, 314*6dbdd20aSAndroid Build Coastguard Worker ) { 315*6dbdd20aSAndroid Build Coastguard Worker const prevFont = ctx.font; 316*6dbdd20aSAndroid Build Coastguard Worker const prevBaseline = ctx.textBaseline; 317*6dbdd20aSAndroid Build Coastguard Worker ctx.textBaseline = 'alphabetic'; 318*6dbdd20aSAndroid Build Coastguard Worker // Adjust height for icon font. 319*6dbdd20aSAndroid Build Coastguard Worker ctx.font = '24px Material Symbols Sharp'; 320*6dbdd20aSAndroid Build Coastguard Worker ctx.fillStyle = color; 321*6dbdd20aSAndroid Build Coastguard Worker ctx.strokeStyle = color; 322*6dbdd20aSAndroid Build Coastguard Worker // The ligatures have padding included that means the icon is not drawn 323*6dbdd20aSAndroid Build Coastguard Worker // exactly at the x value. This adjusts for that. 324*6dbdd20aSAndroid Build Coastguard Worker const iconPadding = 6; 325*6dbdd20aSAndroid Build Coastguard Worker if (fill) { 326*6dbdd20aSAndroid Build Coastguard Worker ctx.fillText(FLAG, x - iconPadding, height + 2); 327*6dbdd20aSAndroid Build Coastguard Worker } else { 328*6dbdd20aSAndroid Build Coastguard Worker ctx.strokeText(FLAG, x - iconPadding, height + 2.5); 329*6dbdd20aSAndroid Build Coastguard Worker } 330*6dbdd20aSAndroid Build Coastguard Worker ctx.font = prevFont; 331*6dbdd20aSAndroid Build Coastguard Worker ctx.textBaseline = prevBaseline; 332*6dbdd20aSAndroid Build Coastguard Worker } 333*6dbdd20aSAndroid Build Coastguard Worker 334*6dbdd20aSAndroid Build Coastguard Worker private onClick(x: number) { 335*6dbdd20aSAndroid Build Coastguard Worker if (!this.timescale) { 336*6dbdd20aSAndroid Build Coastguard Worker return; 337*6dbdd20aSAndroid Build Coastguard Worker } 338*6dbdd20aSAndroid Build Coastguard Worker 339*6dbdd20aSAndroid Build Coastguard Worker // Select the hovered note, or create a new single note & select it 340*6dbdd20aSAndroid Build Coastguard Worker if (x < 0) return; 341*6dbdd20aSAndroid Build Coastguard Worker for (const note of this.trace.notes.notes.values()) { 342*6dbdd20aSAndroid Build Coastguard Worker if (this.hoveredX !== null && this.hitTestNote(this.hoveredX, note)) { 343*6dbdd20aSAndroid Build Coastguard Worker this.trace.selection.selectNote({id: note.id}); 344*6dbdd20aSAndroid Build Coastguard Worker return; 345*6dbdd20aSAndroid Build Coastguard Worker } 346*6dbdd20aSAndroid Build Coastguard Worker } 347*6dbdd20aSAndroid Build Coastguard Worker const timestamp = this.timescale.pxToHpTime(x).toTime(); 348*6dbdd20aSAndroid Build Coastguard Worker const color = randomColor(); 349*6dbdd20aSAndroid Build Coastguard Worker const noteId = this.trace.notes.addNote({timestamp, color}); 350*6dbdd20aSAndroid Build Coastguard Worker this.trace.selection.selectNote({id: noteId}); 351*6dbdd20aSAndroid Build Coastguard Worker } 352*6dbdd20aSAndroid Build Coastguard Worker 353*6dbdd20aSAndroid Build Coastguard Worker private hitTestNote(x: number, note: SpanNote | Note): boolean { 354*6dbdd20aSAndroid Build Coastguard Worker if (!this.timescale) { 355*6dbdd20aSAndroid Build Coastguard Worker return false; 356*6dbdd20aSAndroid Build Coastguard Worker } 357*6dbdd20aSAndroid Build Coastguard Worker 358*6dbdd20aSAndroid Build Coastguard Worker const timescale = this.timescale; 359*6dbdd20aSAndroid Build Coastguard Worker const noteX = timescale.timeToPx(getStartTimestamp(note)); 360*6dbdd20aSAndroid Build Coastguard Worker if (note.noteType === 'SPAN') { 361*6dbdd20aSAndroid Build Coastguard Worker return ( 362*6dbdd20aSAndroid Build Coastguard Worker (noteX <= x && x < noteX + AREA_TRIANGLE_WIDTH) || 363*6dbdd20aSAndroid Build Coastguard Worker (timescale.timeToPx(note.end) > x && 364*6dbdd20aSAndroid Build Coastguard Worker x > timescale.timeToPx(note.end) - AREA_TRIANGLE_WIDTH) 365*6dbdd20aSAndroid Build Coastguard Worker ); 366*6dbdd20aSAndroid Build Coastguard Worker } else { 367*6dbdd20aSAndroid Build Coastguard Worker const width = FLAG_WIDTH; 368*6dbdd20aSAndroid Build Coastguard Worker return noteX <= x && x < noteX + width; 369*6dbdd20aSAndroid Build Coastguard Worker } 370*6dbdd20aSAndroid Build Coastguard Worker } 371*6dbdd20aSAndroid Build Coastguard Worker} 372*6dbdd20aSAndroid Build Coastguard Worker 373*6dbdd20aSAndroid Build Coastguard Workerexport class NotesEditorTab implements DetailsPanel { 374*6dbdd20aSAndroid Build Coastguard Worker constructor(private trace: TraceImpl) {} 375*6dbdd20aSAndroid Build Coastguard Worker 376*6dbdd20aSAndroid Build Coastguard Worker render(selection: Selection) { 377*6dbdd20aSAndroid Build Coastguard Worker if (selection.kind !== 'note') { 378*6dbdd20aSAndroid Build Coastguard Worker return undefined; 379*6dbdd20aSAndroid Build Coastguard Worker } 380*6dbdd20aSAndroid Build Coastguard Worker 381*6dbdd20aSAndroid Build Coastguard Worker const id = selection.id; 382*6dbdd20aSAndroid Build Coastguard Worker 383*6dbdd20aSAndroid Build Coastguard Worker const note = this.trace.notes.getNote(id); 384*6dbdd20aSAndroid Build Coastguard Worker if (note === undefined) { 385*6dbdd20aSAndroid Build Coastguard Worker return m('.', `No Note with id ${id}`); 386*6dbdd20aSAndroid Build Coastguard Worker } 387*6dbdd20aSAndroid Build Coastguard Worker const startTime = getStartTimestamp(note); 388*6dbdd20aSAndroid Build Coastguard Worker return m( 389*6dbdd20aSAndroid Build Coastguard Worker '.notes-editor-panel', 390*6dbdd20aSAndroid Build Coastguard Worker { 391*6dbdd20aSAndroid Build Coastguard Worker key: id, // Every note shoul get its own brand new DOM. 392*6dbdd20aSAndroid Build Coastguard Worker }, 393*6dbdd20aSAndroid Build Coastguard Worker m( 394*6dbdd20aSAndroid Build Coastguard Worker '.notes-editor-panel-heading-bar', 395*6dbdd20aSAndroid Build Coastguard Worker m( 396*6dbdd20aSAndroid Build Coastguard Worker '.notes-editor-panel-heading', 397*6dbdd20aSAndroid Build Coastguard Worker `Annotation at `, 398*6dbdd20aSAndroid Build Coastguard Worker m(Timestamp, {ts: startTime}), 399*6dbdd20aSAndroid Build Coastguard Worker ), 400*6dbdd20aSAndroid Build Coastguard Worker m('input[type=text]', { 401*6dbdd20aSAndroid Build Coastguard Worker oncreate: (v: m.VnodeDOM) => { 402*6dbdd20aSAndroid Build Coastguard Worker // NOTE: due to bad design decisions elsewhere this component is 403*6dbdd20aSAndroid Build Coastguard Worker // rendered every time the mouse moves on the canvas. We cannot set 404*6dbdd20aSAndroid Build Coastguard Worker // `value: note.text` as an input as that will clobber the input 405*6dbdd20aSAndroid Build Coastguard Worker // value as we move the mouse. 406*6dbdd20aSAndroid Build Coastguard Worker const inputElement = v.dom as HTMLInputElement; 407*6dbdd20aSAndroid Build Coastguard Worker inputElement.value = note.text; 408*6dbdd20aSAndroid Build Coastguard Worker inputElement.focus(); 409*6dbdd20aSAndroid Build Coastguard Worker }, 410*6dbdd20aSAndroid Build Coastguard Worker onchange: (e: InputEvent) => { 411*6dbdd20aSAndroid Build Coastguard Worker const newText = (e.target as HTMLInputElement).value; 412*6dbdd20aSAndroid Build Coastguard Worker this.trace.notes.changeNote(id, {text: newText}); 413*6dbdd20aSAndroid Build Coastguard Worker }, 414*6dbdd20aSAndroid Build Coastguard Worker }), 415*6dbdd20aSAndroid Build Coastguard Worker m( 416*6dbdd20aSAndroid Build Coastguard Worker 'span.color-change', 417*6dbdd20aSAndroid Build Coastguard Worker `Change color: `, 418*6dbdd20aSAndroid Build Coastguard Worker m('input[type=color]', { 419*6dbdd20aSAndroid Build Coastguard Worker value: note.color, 420*6dbdd20aSAndroid Build Coastguard Worker onchange: (e: Event) => { 421*6dbdd20aSAndroid Build Coastguard Worker const newColor = (e.target as HTMLInputElement).value; 422*6dbdd20aSAndroid Build Coastguard Worker this.trace.notes.changeNote(id, {color: newColor}); 423*6dbdd20aSAndroid Build Coastguard Worker }, 424*6dbdd20aSAndroid Build Coastguard Worker }), 425*6dbdd20aSAndroid Build Coastguard Worker ), 426*6dbdd20aSAndroid Build Coastguard Worker m(Button, { 427*6dbdd20aSAndroid Build Coastguard Worker label: 'Remove', 428*6dbdd20aSAndroid Build Coastguard Worker icon: Icons.Delete, 429*6dbdd20aSAndroid Build Coastguard Worker onclick: () => this.trace.notes.removeNote(id), 430*6dbdd20aSAndroid Build Coastguard Worker }), 431*6dbdd20aSAndroid Build Coastguard Worker ), 432*6dbdd20aSAndroid Build Coastguard Worker ); 433*6dbdd20aSAndroid Build Coastguard Worker } 434*6dbdd20aSAndroid Build Coastguard Worker} 435