• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2019 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use size 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 * as m from 'mithril';
16
17import {Actions} from '../common/actions';
18import {timeToString} from '../common/time';
19
20import {globals} from './globals';
21import {gridlines} from './gridline_helper';
22import {Panel, PanelSize} from './panel';
23import {TRACK_SHELL_WIDTH} from './track_constants';
24import {hsl} from 'color-convert';
25
26const FLAG_WIDTH = 16;
27const MOUSE_OFFSET = 6;
28const FLAG = `\uE153`;
29
30function toSummary(s: string) {
31  const newlineIndex = s.indexOf('\n') > 0 ? s.indexOf('\n') : s.length;
32  return s.slice(0, Math.min(newlineIndex, s.length, 16));
33}
34
35export class NotesPanel extends Panel {
36  hoveredX: null|number = null;
37
38  oncreate({dom}: m.CVnodeDOM) {
39    dom.addEventListener('mousemove', (e: Event) => {
40      this.hoveredX =
41        (e as MouseEvent).layerX - TRACK_SHELL_WIDTH - MOUSE_OFFSET;
42      globals.rafScheduler.scheduleRedraw();
43    }, {passive: true});
44    dom.addEventListener('mouseenter', (e: Event) => {
45      this.hoveredX =
46        (e as MouseEvent).layerX - TRACK_SHELL_WIDTH - MOUSE_OFFSET;
47      globals.rafScheduler.scheduleRedraw();
48    });
49    dom.addEventListener('mouseout', () => {
50      this.hoveredX = null;
51      globals.frontendLocalState.setShowNotePreview(false);
52      globals.rafScheduler.scheduleRedraw();
53    }, {passive: true});
54  }
55
56  view() {
57    return m(
58      '.notes-panel',
59      {
60        onclick: (e: MouseEvent) => {
61          this.onClick(e.layerX - TRACK_SHELL_WIDTH, e.layerY);
62          e.stopPropagation();
63        },
64      });
65  }
66
67  renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
68    const timeScale = globals.frontendLocalState.timeScale;
69    const range = globals.frontendLocalState.visibleWindowTime;
70    let aNoteIsHovered = false;
71
72    ctx.fillStyle = '#999';
73    ctx.fillRect(TRACK_SHELL_WIDTH - 1, 0, 2, size.height);
74    for (const xAndTime of gridlines(size.width, range, timeScale)) {
75      ctx.fillRect(xAndTime[0], 0, 1, size.height);
76    }
77
78    ctx.textBaseline = 'bottom';
79    ctx.font = '10px Helvetica';
80
81    for (const note of Object.values(globals.state.notes)) {
82      const timestamp = note.timestamp;
83      if (!timeScale.timeInBounds(timestamp)) continue;
84      const x = timeScale.timeToPx(timestamp);
85
86      const currentIsHovered =
87        this.hoveredX &&
88        x - MOUSE_OFFSET <= this.hoveredX &&
89        this.hoveredX < x - MOUSE_OFFSET + FLAG_WIDTH;
90      const selection = globals.state.currentSelection;
91      const isSelected = selection !== null && selection.kind === 'NOTE' &&
92                         selection.id === note.id;
93      const left = Math.floor(x + TRACK_SHELL_WIDTH);
94
95      // Draw flag.
96      if (!aNoteIsHovered && currentIsHovered) {
97        aNoteIsHovered = true;
98        this.drawFlag(ctx, left, size.height, note.color, isSelected);
99      } else if (isSelected) {
100        this.drawFlag(ctx, left, size.height, note.color, /* fill */ true);
101      } else {
102        this.drawFlag(ctx, left, size.height, note.color);
103      }
104
105      if (note.text) {
106        const summary = toSummary(note.text);
107        const measured = ctx.measureText(summary);
108        // Add a white semi-transparent background for the text.
109        ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
110        ctx.fillRect(
111            left + FLAG_WIDTH + 2, size.height, measured.width + 2, -12);
112        ctx.fillStyle = '#3c4b5d';
113        ctx.fillText(summary, left + FLAG_WIDTH + 3, size.height - 1);
114      }
115    }
116
117    // A real note is hovered so we don't need to see the preview line.
118    if (aNoteIsHovered) globals.frontendLocalState.setShowNotePreview(false);
119
120    // View preview note flag when hovering on notes panel.
121    if (!aNoteIsHovered && this.hoveredX !== null) {
122      const timestamp = timeScale.pxToTime(this.hoveredX);
123      if (timeScale.timeInBounds(timestamp)) {
124        globals.frontendLocalState.setHoveredTimestamp(timestamp);
125        globals.frontendLocalState.setShowNotePreview(true);
126        const x = timeScale.timeToPx(timestamp);
127        const left = Math.floor(x + TRACK_SHELL_WIDTH);
128        this.drawFlag(ctx, left, size.height, '#aaa', /* fill */ true);
129      }
130    }
131  }
132
133  private drawFlag(
134      ctx: CanvasRenderingContext2D, x: number, height: number, color: string,
135      fill?: boolean) {
136    const prevFont = ctx.font;
137    const prevBaseline = ctx.textBaseline;
138    ctx.textBaseline = 'alphabetic';
139    if (fill) {
140      ctx.font = '24px Material Icons';
141      ctx.fillStyle = color;
142      // Adjust height for icon font.
143      ctx.fillText(FLAG, x - MOUSE_OFFSET, height + 2);
144    } else {
145      ctx.strokeStyle = color;
146      ctx.font = '24px Material Icons';
147      // Adjust height for icon font.
148      ctx.strokeText(FLAG, x - MOUSE_OFFSET, height + 2.5);
149    }
150    ctx.font = prevFont;
151    ctx.textBaseline = prevBaseline;
152  }
153
154  private onClick(x: number, _: number) {
155    const timeScale = globals.frontendLocalState.timeScale;
156    const timestamp = timeScale.pxToTime(x - MOUSE_OFFSET);
157    for (const note of Object.values(globals.state.notes)) {
158      const noteX = timeScale.timeToPx(note.timestamp);
159      if (noteX <= x && x < noteX + FLAG_WIDTH) {
160        globals.dispatch(Actions.selectNote({id: note.id}));
161        return;
162      }
163    }
164    // 40 different random hues 9 degrees apart.
165    const hue = Math.floor(Math.random() * 40) * 9;
166    const color = '#' + hsl.hex([hue, 90, 30]);
167    globals.dispatch(Actions.addNote({timestamp, color}));
168  }
169}
170
171interface NotesEditorPanelAttrs {
172  id: string;
173}
174
175export class NotesEditorPanel extends Panel<NotesEditorPanelAttrs> {
176  view({attrs}: m.CVnode<NotesEditorPanelAttrs>) {
177    const note = globals.state.notes[attrs.id];
178    const startTime = note.timestamp - globals.state.traceTime.startSec;
179    return m(
180        '.notes-editor-panel',
181        m('.notes-editor-panel-heading-bar',
182          m('.notes-editor-panel-heading',
183            `Annotation at ${timeToString(startTime)}`),
184          m('input[type=text]', {
185            onkeydown: (e: Event) => {
186              e.stopImmediatePropagation();
187            },
188            value: note.text,
189            onchange: m.withAttr(
190                'value',
191                newText => {
192                  globals.dispatch(Actions.changeNoteText({
193                    id: attrs.id,
194                    newText,
195                  }));
196                }),
197          }),
198          m('span.color-change', `Change color: `, m('input[type=color]', {
199              value: note.color,
200              onchange: m.withAttr(
201                  'value',
202                  newColor => {
203                    globals.dispatch(Actions.changeNoteColor({
204                      id: attrs.id,
205                      newColor,
206                    }));
207                  }),
208            })),
209          m('button',
210            {
211              onclick: () =>
212                  globals.dispatch(Actions.removeNote({id: attrs.id})),
213            },
214            'Remove')), );
215  }
216
217  renderCanvas(_ctx: CanvasRenderingContext2D, _size: PanelSize) {}
218}
219