UNPKG

chrome-devtools-frontend

Version:
237 lines (213 loc) • 7.95 kB
// Copyright 2024 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as i18n from '../../core/i18n/i18n.js'; import * as Platform from '../../core/platform/platform.js'; import * as Trace from '../../models/trace/trace.js'; import * as TraceBounds from '../../services/trace_bounds/trace_bounds.js'; import type {AnnotationModifiedEvent} from './ModificationsManager.js'; import type * as Overlays from './overlays/overlays.js'; import * as Utils from './utils/utils.js'; const UIStrings = { /** *@description text used to announce to a screen reader that they have entered the mode to edit the label */ srEnterLabelEditMode: 'Editing the annotation label text', /** *@description text used to announce to a screen reader that the entry label text has been updated *@example {Hello world} PH1 */ srLabelTextUpdated: 'Label updated to {PH1}', /** *@description text used to announce to a screen reader that the bounds of a time range annotation have been upodated *@example {13ms} PH1 *@example {20ms} PH2 */ srTimeRangeBoundsUpdated: 'Time range updated, starting at {PH1} and ending at {PH2}', /** *@description label for a time range overlay */ timeRange: 'time range', /** *@description label for a entry label overlay */ entryLabel: 'entry label', /** *@description label for a connected entries overlay */ entriesLink: 'connected entries', /** *@description screen reader text to announce that an annotation has been removed *@example {Entry Label} PH1 */ srAnnotationRemoved: 'The {PH1} annotation has been removed', /** *@description screen reader text to announce that an annotation has been added *@example {Entry Label} PH1 */ srAnnotationAdded: 'The {PH1} annotation has been added', /** *@description screen reader text to announce the two events that the connected entries annotation links to *@example {Paint} PH1 *@example {Function call} PH2 */ srEntriesLinked: 'The connected entries annotation now links from {PH1} to {PH2}', } as const; const str_ = i18n.i18n.registerUIStrings('panels/timeline/AnnotationHelpers.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export function getAnnotationEntries( annotation: Trace.Types.File.Annotation, ): Trace.Types.Events.Event[] { const entries: Trace.Types.Events.Event[] = []; switch (annotation.type) { case 'ENTRY_LABEL': entries.push(annotation.entry); break; case 'TIME_RANGE': break; case 'ENTRIES_LINK': entries.push(annotation.entryFrom); if (annotation.entryTo) { entries.push(annotation.entryTo); } break; default: Platform.assertNever(annotation, 'Unsupported annotation type'); } return entries; } /** * Gets a trace window that contains the given annotation. May return `null` * if there is no valid window (an ENTRIES_LINK without a `to` entry for * example.) */ export function getAnnotationWindow( annotation: Trace.Types.File.Annotation, ): Trace.Types.Timing.TraceWindowMicro|null { let annotationWindow: Trace.Types.Timing.TraceWindowMicro|null = null; const minVisibleEntryDuration = Trace.Types.Timing.Milli(1); switch (annotation.type) { case 'ENTRY_LABEL': { const eventDuration = annotation.entry.dur ?? Trace.Helpers.Timing.milliToMicro(minVisibleEntryDuration); annotationWindow = Trace.Helpers.Timing.traceWindowFromMicroSeconds( annotation.entry.ts, Trace.Types.Timing.Micro(annotation.entry.ts + eventDuration), ); break; } case 'TIME_RANGE': { annotationWindow = annotation.bounds; break; } case 'ENTRIES_LINK': { // If entryTo does not exist, the annotation is in the process of being created. // Do not allow to zoom into it in this case. if (!annotation.entryTo) { break; } const fromEventDuration = (annotation.entryFrom.dur) ?? minVisibleEntryDuration; const toEventDuration = annotation.entryTo.dur ?? minVisibleEntryDuration; // To choose window max, check which entry ends later const fromEntryEndTS = (annotation.entryFrom.ts + fromEventDuration); const toEntryEndTS = (annotation.entryTo.ts + toEventDuration); const maxTimestamp = Math.max(fromEntryEndTS, toEntryEndTS); annotationWindow = Trace.Helpers.Timing.traceWindowFromMicroSeconds( annotation.entryFrom.ts, Trace.Types.Timing.Micro(maxTimestamp), ); break; } default: Platform.assertNever(annotation, 'Unsupported annotation type'); } return annotationWindow; } export function isTimeRangeLabel(overlay: Overlays.Overlays.TimelineOverlay): overlay is Overlays.Overlays.TimeRangeLabel { return overlay.type === 'TIME_RANGE'; } export function isEntriesLink(overlay: Overlays.Overlays.TimelineOverlay): overlay is Overlays.Overlays.EntriesLink { return overlay.type === 'ENTRIES_LINK'; } export function isEntryLabel(overlay: Overlays.Overlays.TimelineOverlay): overlay is Overlays.Overlays.EntryLabel { return overlay.type === 'ENTRY_LABEL'; } function labelForOverlay(overlay: Overlays.Overlays.TimelineOverlay): string|null { if (isTimeRangeLabel(overlay) || isEntryLabel(overlay)) { return overlay.label; } return null; } export function ariaDescriptionForOverlay(overlay: Overlays.Overlays.TimelineOverlay): string|null { if (isTimeRangeLabel(overlay)) { return i18nString(UIStrings.timeRange); } if (isEntriesLink(overlay)) { return i18nString(UIStrings.entriesLink); } if (isEntryLabel(overlay)) { // Don't announce an empty label return overlay.label.length > 0 ? i18nString(UIStrings.entryLabel) : null; } // Not an annotation overlay: ignore. return null; } export function ariaAnnouncementForModifiedEvent(event: AnnotationModifiedEvent): string|null { const {overlay, action} = event; switch (action) { case 'Remove': { const text = ariaDescriptionForOverlay(overlay); if (text) { return (i18nString(UIStrings.srAnnotationRemoved, {PH1: text})); } break; } case 'Add': { const text = ariaDescriptionForOverlay(overlay); if (text) { return (i18nString(UIStrings.srAnnotationAdded, {PH1: text})); } break; } case 'UpdateLabel': { const label = labelForOverlay(overlay); if (label) { return i18nString(UIStrings.srLabelTextUpdated, {PH1: label}); } break; } case 'UpdateTimeRange': { if (overlay.type !== 'TIME_RANGE') { return ''; } const traceBounds = TraceBounds.TraceBounds.BoundsManager.instance().state()?.micro.entireTraceBounds; if (!traceBounds) { return ''; } const {min, max} = overlay.bounds; const minText = i18n.TimeUtilities.formatMicroSecondsAsMillisFixed( Trace.Types.Timing.Micro(min - traceBounds.min), ); const maxText = i18n.TimeUtilities.formatMicroSecondsAsMillisFixed(Trace.Types.Timing.Micro(max - traceBounds.min)); return i18nString(UIStrings.srTimeRangeBoundsUpdated, { PH1: minText, PH2: maxText, }); } case 'UpdateLinkToEntry': { if (isEntriesLink(overlay) && overlay.entryFrom && overlay.entryTo) { const from = Utils.EntryName.nameForEntry(overlay.entryFrom); const to = Utils.EntryName.nameForEntry(overlay.entryTo); return (i18nString(UIStrings.srEntriesLinked, {PH1: from, PH2: to})); } break; } case 'EnterLabelEditState': { return (i18nString(UIStrings.srEnterLabelEditMode)); } default: Platform.assertNever(action, 'Unsupported action for AnnotationModifiedEvent'); } return null; }