Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions ui/src/assets/viewer_page.scss
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,13 @@
opacity: 0.9;
font-weight: 300;
}

.pf-note-badge {
position: absolute;
background: var(--pf-color-background);
color: var(--pf-color-text);
padding: 0px 1px;
border-radius: 10px;
font-size: 18px;
z-index: 10;
}
149 changes: 149 additions & 0 deletions ui/src/frontend/viewer_page/note_badge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// Copyright (C) 2019 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use size file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import m from 'mithril';
import { assertUnreachable } from '../../base/logging';
import { TimeScale } from '../../base/time_scale';
import { raf } from '../../core/raf_scheduler';
import {Note, SpanNote} from '../../public/note';
import { TraceImpl } from 'src/core/trace_impl';

export interface NoteBadgeAttrs {
timescale: TimeScale
note: Note | SpanNote;
xOffset?: number
onClickBadge?: (note: Note | SpanNote) => unknown;
onEnterBadge?: () => unknown;
onLeaveBadge?: () => unknown;
}

function getBadgeTimestamp(note: Note | SpanNote) {
const noteType = note.noteType;
switch (noteType) {
case 'SPAN':
return note.end;
case 'DEFAULT':
return note.timestamp;
default:
assertUnreachable(noteType);
}
}

export class NoteBadge implements m.ClassComponent<NoteBadgeAttrs> {
view({attrs, children}: m.Vnode<NoteBadgeAttrs>): m.Children {
const { timescale, note, onEnterBadge, onLeaveBadge, onClickBadge } = attrs;
const timestamp = getBadgeTimestamp(note);
const xOffset = (attrs.xOffset ?? 0) + 8;
const x = Math.floor(timescale.timeToPx(timestamp)) + xOffset;

return m(
'.pf-note-badge',
{
style: {
left: `${x}px`,
top: '-16px',
},
onmouseenter: () => {
onEnterBadge?.();
},
onmouseleave: () => {
onLeaveBadge?.();
},
onclick: (e: MouseEvent) => {
e.stopPropagation();
onClickBadge?.(attrs.note);
},
onmousedown: (e: MouseEvent) => {
e.stopPropagation();
},
},
children,
);
}
}

export class NoteDeleteBadgeState {
private badgedNote: Note | SpanNote | undefined;
private isBadgeHovered = false;
private badgeDismissalTimeout: ReturnType<typeof setTimeout> | undefined;

constructor(private readonly trace: TraceImpl) {}

get hovered(): boolean {
return this.isBadgeHovered;
}

updateHoveredNote(note: Note | SpanNote | null): void {
if (note !== null && this.badgedNote !== note) {
this.badgedNote = note;
this.clearDismissal();
} else if (!this.isBadgeHovered && this.badgedNote !== null) {
// Schedule tooltip dismissal if neither note nor tooltip is hovered
this.scheduleDismissal();
}
}

render(timescale: TimeScale, xOffset?: number): m.Children {
if (!this.badgedNote) {
return null;
}

return m(NoteBadge,
{
timescale,
note: this.badgedNote,
xOffset,
onEnterBadge: () => {
this.isBadgeHovered = true;
this.clearDismissal();
},
onLeaveBadge: () => {
this.isBadgeHovered = false;
// Schedule dismissal when leaving the badge
this.scheduleDismissal();
},
onClickBadge: note => this.removeNote(note.id)
},
m('.pf-icon', '\uE5C9'), // Material Symbols Sharp "cancel" icon
);
}

private scheduleDismissal(): void {
if (this.badgeDismissalTimeout) {
// Already scheduled
return;
}

this.badgeDismissalTimeout = setTimeout(() => {
this.badgedNote = undefined;
this.badgeDismissalTimeout = undefined;
raf.scheduleCanvasRedraw();
}, 300);
}

private clearDismissal(): void {
if (this.badgeDismissalTimeout) {
clearTimeout(this.badgeDismissalTimeout);
this.badgeDismissalTimeout = undefined;
}
}

private removeNote(id: string) {
this.trace.notes.removeNote(id);
this.badgedNote = undefined;
this.isBadgeHovered = false;
this.clearDismissal();
raf.scheduleCanvasRedraw();
}
}
18 changes: 14 additions & 4 deletions ui/src/frontend/viewer_page/notes_panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {TraceImpl} from '../../core/trace_impl';
import {Note, SpanNote} from '../../public/note';
import {COLOR_BORDER, TRACK_SHELL_WIDTH} from '../css_constants';
import {generateTicks, getMaxMajorTicks, TickType} from './gridline_helper';
import {NoteDeleteBadgeState} from './note_badge';
import {TimelineToolbar} from './timeline_toolbar';

const FLAG_WIDTH = 16;
Expand All @@ -49,20 +50,22 @@ function getStartTimestamp(note: Note | SpanNote) {

export class NotesPanel {
private readonly trace: TraceImpl;
private readonly noteDeleteBadge;
private timescale?: TimeScale; // The timescale from the last render()
private hoveredX: null | number = null;
private mouseDragging = false;
readonly height = 20;

constructor(trace: TraceImpl) {
this.trace = trace;
this.noteDeleteBadge = new NoteDeleteBadgeState(trace);
}

render(): m.Children {
return m(
'',
{
style: {height: `${this.height}px`},
style: {height: `${this.height}px`, position: 'relative'},
onmousedown: () => {
// If the user clicks & drags, very likely they just want to measure
// the time horizontally, not set a flag. This debouncing is done to
Expand Down Expand Up @@ -92,6 +95,7 @@ export class NotesPanel {
},
},
m(TimelineToolbar, {trace: this.trace}),
this.noteDeleteBadge.render(this.timescale!, TRACK_SHELL_WIDTH),
);
}

Expand Down Expand Up @@ -149,7 +153,13 @@ export class NotesPanel {
}
const currentIsHovered =
this.hoveredX !== null && this.hitTestNote(this.hoveredX, note);
if (currentIsHovered) aNoteIsHovered = true;

if (currentIsHovered) {
aNoteIsHovered = true;
this.noteDeleteBadge.updateHoveredNote(note);
} else {
this.noteDeleteBadge.updateHoveredNote(null);
}

const selection = this.trace.selection.selection;
const isSelected = selection.kind === 'note' && selection.id === note.id;
Expand Down Expand Up @@ -191,8 +201,8 @@ export class NotesPanel {
this.trace.timeline.hoveredNoteTimestamp = undefined;
}

// View preview note flag when hovering on notes panel.
if (!aNoteIsHovered && this.hoveredX !== null) {
// View preview note flag when hovering on notes panel but not a delete badge.
if (!aNoteIsHovered && this.hoveredX !== null && !this.noteDeleteBadge.hovered) {
const timestamp = timescale.pxToHpTime(this.hoveredX).toTime();
if (visibleWindow.contains(timestamp)) {
this.trace.timeline.hoveredNoteTimestamp = timestamp;
Expand Down