From b15ad949b50cc5872eca0dd44f1f51e0b23a7b66 Mon Sep 17 00:00:00 2001 From: Will Yang Date: Sun, 10 Apr 2022 15:54:16 -0500 Subject: [PATCH] Undo / Redo Functionality Adds the ability for the user to undo/redo changes made to the zoom and selection. Adds undo and redo buttons to the toolbar. Signed-off-by: Will Yang --- packages/base/src/signals/signal-manager.ts | 10 ++ .../components/trace-context-component.tsx | 55 ++++++++- .../utils/unit-controller-history-handler.ts | 108 ++++++++++++++++++ .../zoom-pan-shortcuts-table.tsx | 19 +++ .../trace-viewer-toolbar-commands.ts | 12 ++ .../trace-viewer-toolbar-contribution.tsx | 52 +++++++-- 6 files changed, 242 insertions(+), 14 deletions(-) create mode 100644 packages/react-components/src/components/utils/unit-controller-history-handler.ts diff --git a/packages/base/src/signals/signal-manager.ts b/packages/base/src/signals/signal-manager.ts index 5a5646118..fd9e5961a 100644 --- a/packages/base/src/signals/signal-manager.ts +++ b/packages/base/src/signals/signal-manager.ts @@ -23,6 +23,8 @@ export declare interface SignalManager { fireMarkerSetsFetchedSignal(): void; fireMarkerCategoryClosedSignal(payload: { traceViewerId: string, markerCategory: string }): void; fireTraceServerStartedSignal(): void; + fireUndoSignal(): void; + fireRedoSignal(): void; } export const Signals = { @@ -42,6 +44,8 @@ export const Signals = { TRACEVIEWERTAB_ACTIVATED: 'widget activated', UPDATE_ZOOM: 'update zoom', RESET_ZOOM: 'reset zoom', + UNDO: 'undo', + REDO: 'redo', MARKER_CATEGORIES_FETCHED: 'marker categories fetched', MARKERSETS_FETCHED: 'markersets fetched', MARKER_CATEGORY_CLOSED: 'marker category closed', @@ -106,6 +110,12 @@ export class SignalManager extends EventEmitter implements SignalManager { fireTraceServerStartedSignal(): void { this.emit(Signals.TRACE_SERVER_STARTED); } + fireUndoSignal(): void { + this.emit(Signals.UNDO); + } + fireRedoSignal(): void { + this.emit(Signals.REDO); + } } let instance: SignalManager = new SignalManager(); diff --git a/packages/react-components/src/components/trace-context-component.tsx b/packages/react-components/src/components/trace-context-component.tsx index b1655eedb..f1bd7d982 100644 --- a/packages/react-components/src/components/trace-context-component.tsx +++ b/packages/react-components/src/components/trace-context-component.tsx @@ -27,10 +27,11 @@ import { TooltipXYComponent } from './tooltip-xy-component'; import { BIMath } from 'timeline-chart/lib/bigint-utils'; import { DataTreeOutputComponent } from './datatree-output-component'; import { cloneDeep } from 'lodash'; +import { UnitControllerHistoryHandler } from './utils/unit-controller-history-handler'; const ResponsiveGridLayout = WidthProvider(Responsive); -interface TraceContextProps { +export interface TraceContextProps { tspClient: TspClient; experiment: Experiment; outputs: OutputDescriptor[]; @@ -45,7 +46,7 @@ interface TraceContextProps { persistedState?: PersistedState; } -interface TraceContextState { +export interface TraceContextState { timeOffset: bigint; currentRange: TimeRange; currentViewRange: TimeRange; @@ -78,6 +79,7 @@ export class TraceContextComponent extends React.Component; private tooltipXYComponent: React.RefObject; private traceContextContainer: React.RefObject; @@ -158,6 +160,7 @@ export class TraceContextComponent extends React.Component { @@ -318,14 +327,14 @@ export class TraceContextComponent extends React.Component ({ currentTimeSelection: new TimeRange(range.start, range.end, prevState.timeOffset) - })); + }), () => this.updateHistory()); } } private handleViewRangeChange(viewRange: TimelineChart.TimeGraphRange) { this.setState(prevState => ({ currentViewRange: new TimeRange(viewRange.start, viewRange.end, prevState.timeOffset) - })); + }), () => this.updateHistory()); } private onContextMenu(event: React.MouseEvent) { @@ -348,6 +357,7 @@ export class TraceContextComponent extends React.Component this.onContextMenu(event)} onKeyDown={event => this.onKeyDown(event)} + onKeyUp={event => this.onKeyUp(event)} ref={this.traceContextContainer}> @@ -355,8 +365,8 @@ export class TraceContextComponent extends React.Component; } - private onKeyDown(key: React.KeyboardEvent) { - switch (key.key) { + private onKeyDown(event: React.KeyboardEvent) { + switch (event.key) { case '+': case '=': { this.zoomButton(true); @@ -370,6 +380,27 @@ export class TraceContextComponent extends React.Component; } + private undoHistory = (): void => { + this.historyHandler.undo(); + }; + + private redoHistory = (): void => { + this.historyHandler.redo(); + }; + + private updateHistory = (): void => { + this.historyHandler.addCurrentState(); + }; + private generateGridLayout(): void { let existingTimeScaleLayouts: Array = []; let existingNonTimeScaleLayouts: Array = []; diff --git a/packages/react-components/src/components/utils/unit-controller-history-handler.ts b/packages/react-components/src/components/utils/unit-controller-history-handler.ts new file mode 100644 index 000000000..3e43e8ea6 --- /dev/null +++ b/packages/react-components/src/components/utils/unit-controller-history-handler.ts @@ -0,0 +1,108 @@ +import { TimelineChart } from 'timeline-chart/lib/time-graph-model'; +import { TimeGraphUnitController } from 'timeline-chart/lib/time-graph-unit-controller'; + +export interface HistoryItem { + selectionRange: TimelineChart.TimeGraphRange | undefined; + viewRange: TimelineChart.TimeGraphRange; +} + +export class UnitControllerHistoryHandler { + protected history: Array; + protected index = 0; + protected maxAllowedIndex = 0; + protected timeout?: ReturnType; + protected unitController: TimeGraphUnitController; + protected restoring = false; + + constructor(uC: TimeGraphUnitController) { + this.history = []; + this.unitController = uC; + } + + public addCurrentState(): void { + const { selectionRange, viewRange } = this.unitController; + this.enqueueItem({ selectionRange, viewRange }); + } + + public undo(): void { + if (this.canUndo) { + this.index--; + this.restore(); + } + } + + public redo(): void { + if (this.canRedo) { + this.index++; + this.restore(); + } + } + + public clear(): void { + this.index = 0; + this.maxAllowedIndex = 0; + } + + private enqueueItem(item: HistoryItem): void { + /** + * Since scrolling with the scroll-bar or dragging handle triggers many changes per second + * we don't want to actually push if another request comes in quick succession. + * + * Don't add anything if we are currently restoring. + */ + if (this.restoring) { + return; + } + if (this.timeout) { + clearTimeout(this.timeout); + } + this.timeout = setTimeout(() => this.add(item), 500); + } + + private add(item: HistoryItem): void { + const isDuplicate = this.isEntryDuplicate(item); + if (!isDuplicate) { + this.index++; + this.maxAllowedIndex = this.index; + this.history[this.index] = item; + } + } + + private restore(): void { + this.restoring = true; + const { selectionRange, viewRange } = this.history[this.index]; + this.unitController.selectionRange = selectionRange; + this.unitController.viewRange = viewRange; + setTimeout(() => this.restoring = false, 500); + } + + private isEntryDuplicate(item: HistoryItem): boolean { + // Checks if stack entry is same as previous entry. + if (this.index === 0) { + return false; + } + let oneIsDifferent = false; + const { selectionRange: itemSR, viewRange: itemVR } = item; + const { selectionRange: prevSR, viewRange: prevVR } = this.history[this.index]; + const check = (value1: BigInt | undefined, value2: BigInt | undefined) => { + if (oneIsDifferent) { + return; + } + oneIsDifferent = (value1 !== value2); + }; + check(itemSR?.start, prevSR?.start); + check(itemSR?.end, prevSR?.end); + check(itemVR.start, prevVR.start); + check(itemVR.end, prevVR.end); + return !oneIsDifferent; + } + + private get canRedo(): boolean { + return this.index < this.maxAllowedIndex; + } + + private get canUndo(): boolean { + return this.index > 1; + } + +} diff --git a/theia-extensions/viewer-prototype/src/browser/trace-explorer/trace-explorer-sub-widgets/trace-explorer-keyboard-shortcuts/zoom-pan-shortcuts-table.tsx b/theia-extensions/viewer-prototype/src/browser/trace-explorer/trace-explorer-sub-widgets/trace-explorer-keyboard-shortcuts/zoom-pan-shortcuts-table.tsx index 181bf093c..20bcc871d 100644 --- a/theia-extensions/viewer-prototype/src/browser/trace-explorer/trace-explorer-sub-widgets/trace-explorer-keyboard-shortcuts/zoom-pan-shortcuts-table.tsx +++ b/theia-extensions/viewer-prototype/src/browser/trace-explorer/trace-explorer-sub-widgets/trace-explorer-keyboard-shortcuts/zoom-pan-shortcuts-table.tsx @@ -58,6 +58,25 @@ export class ZoomPanShortcutsTable extends React.Component {
+ + + + + + + +
Undo + CTRL + Z +
Redo + CTRL + SHIFT + Z + or + CTRL + Y + +
Zoom to selected range diff --git a/theia-extensions/viewer-prototype/src/browser/trace-viewer/trace-viewer-toolbar-commands.ts b/theia-extensions/viewer-prototype/src/browser/trace-viewer/trace-viewer-toolbar-commands.ts index cb07e1445..980b61ef8 100644 --- a/theia-extensions/viewer-prototype/src/browser/trace-viewer/trace-viewer-toolbar-commands.ts +++ b/theia-extensions/viewer-prototype/src/browser/trace-viewer/trace-viewer-toolbar-commands.ts @@ -14,6 +14,18 @@ export namespace TraceViewerToolbarCommands { iconClass: 'fa fa-minus-square-o fa-lg', }; + export const UNDO: Command = { + id: 'trace.viewer.toolbar.undo', + label: 'Undo', + iconClass: 'fa fa-undo fa-lg', + }; + + export const REDO: Command = { + id: 'trace.viewer.toolbar.redo', + label: 'Redo', + iconClass: 'fa fa-repeat fa-lg', + }; + export const RESET: Command = { id: 'trace.viewer.toolbar.reset', label: 'Reset', diff --git a/theia-extensions/viewer-prototype/src/browser/trace-viewer/trace-viewer-toolbar-contribution.tsx b/theia-extensions/viewer-prototype/src/browser/trace-viewer/trace-viewer-toolbar-contribution.tsx index 5730a920a..d51071cb5 100644 --- a/theia-extensions/viewer-prototype/src/browser/trace-viewer/trace-viewer-toolbar-contribution.tsx +++ b/theia-extensions/viewer-prototype/src/browser/trace-viewer/trace-viewer-toolbar-contribution.tsx @@ -69,6 +69,30 @@ export class TraceViewerToolbarContribution implements TabBarToolbarContribution signalManager().fireUpdateZoomSignal(false); } }); + registry.registerCommand( + TraceViewerToolbarCommands.UNDO, { + isVisible: (widget: Widget) => { + if (widget instanceof TraceViewerWidget) { + return widget.isTimeRelatedChartOpened(); + } + return false; + }, + execute: () => { + signalManager().fireUndoSignal(); + } + }); + registry.registerCommand( + TraceViewerToolbarCommands.REDO, { + isVisible: (widget: Widget) => { + if (widget instanceof TraceViewerWidget) { + return widget.isTimeRelatedChartOpened(); + } + return false; + }, + execute: () => { + signalManager().fireRedoSignal(); + } + }); registry.registerCommand( TraceViewerToolbarCommands.RESET, { isVisible: (widget: Widget) => { @@ -121,23 +145,35 @@ export class TraceViewerToolbarContribution implements TabBarToolbarContribution } registerToolbarItems(registry: TabBarToolbarRegistry): void { + registry.registerItem({ + id: TraceViewerToolbarCommands.UNDO.id, + command: TraceViewerToolbarCommands.UNDO.id, + tooltip: TraceViewerToolbarCommands.UNDO.label, + priority: 1, + }); + registry.registerItem({ + id: TraceViewerToolbarCommands.REDO.id, + command: TraceViewerToolbarCommands.REDO.id, + tooltip: TraceViewerToolbarCommands.REDO.label, + priority: 2, + }); registry.registerItem({ id: TraceViewerToolbarCommands.ZOOM_IN.id, command: TraceViewerToolbarCommands.ZOOM_IN.id, tooltip: TraceViewerToolbarCommands.ZOOM_IN.label, - priority: 1, + priority: 3, }); registry.registerItem({ id: TraceViewerToolbarCommands.ZOOM_OUT.id, command: TraceViewerToolbarCommands.ZOOM_OUT.id, tooltip: TraceViewerToolbarCommands.ZOOM_OUT.label, - priority: 2, + priority: 4, }); registry.registerItem({ id: TraceViewerToolbarCommands.RESET.id, command: TraceViewerToolbarCommands.RESET.id, tooltip: TraceViewerToolbarCommands.RESET.label, - priority: 3, + priority: 5, }); registry.registerItem({ id: TraceViewerToolbarCommands.FILTER.id, @@ -185,7 +221,7 @@ export class TraceViewerToolbarContribution implements TabBarToolbarContribution }); }}> , - priority: 4, + priority: 6, group: 'navigation', onDidChange: this.onMarkerCategoriesChangedEvent, }); @@ -233,7 +269,7 @@ export class TraceViewerToolbarContribution implements TabBarToolbarContribution }); }}> , - priority: 5, + priority: 7, group: 'navigation', onDidChange: this.onMakerSetsChangedEvent, }); @@ -241,19 +277,19 @@ export class TraceViewerToolbarContribution implements TabBarToolbarContribution id: TraceViewerToolbarCommands.OPEN_TRACE.id, command: TraceViewerToolbarCommands.OPEN_TRACE.id, tooltip: TraceViewerToolbarCommands.OPEN_TRACE.label, - priority: 6, + priority: 8, }); registry.registerItem({ id: TraceViewerToolbarCommands.CHARTS_CHEATSHEET.id, command: TraceViewerToolbarCommands.CHARTS_CHEATSHEET.id, tooltip: TraceViewerToolbarCommands.CHARTS_CHEATSHEET.label, - priority: 7, + priority: 9, }); registry.registerItem({ id: TraceViewerToolbarCommands.SERVER_CHECK.id, command: TraceViewerToolbarCommands.SERVER_CHECK.id, tooltip: TraceViewerToolbarCommands.SERVER_CHECK.label, - priority: 7, + priority: 10, }); } }