Skip to content

Commit

Permalink
Undo / Redo Functionality
Browse files Browse the repository at this point in the history
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 <william.yang@ericsson.com>
  • Loading branch information
williamsyang-work authored and PatrickTasse committed May 20, 2022
1 parent d633e97 commit b15ad94
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 14 deletions.
10 changes: 10 additions & 0 deletions packages/base/src/signals/signal-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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',
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand All @@ -45,7 +46,7 @@ interface TraceContextProps {
persistedState?: PersistedState;
}

interface TraceContextState {
export interface TraceContextState {
timeOffset: bigint;
currentRange: TimeRange;
currentViewRange: TimeRange;
Expand Down Expand Up @@ -78,6 +79,7 @@ export class TraceContextComponent extends React.Component<TraceContextProps, Tr
private readonly DEFAULT_CHART_OFFSET = 200;

private unitController: TimeGraphUnitController;
private historyHandler: UnitControllerHistoryHandler;
private tooltipComponent: React.RefObject<TooltipComponent>;
private tooltipXYComponent: React.RefObject<TooltipXYComponent>;
private traceContextContainer: React.RefObject<HTMLDivElement>;
Expand Down Expand Up @@ -158,6 +160,7 @@ export class TraceContextComponent extends React.Component<TraceContextProps, Tr
const nanos = zeroPad(theNumber % BigInt(1000));
return seconds + '.' + millis + ' ' + micros + ' ' + nanos;
};
this.historyHandler = new UnitControllerHistoryHandler(this.unitController);
if (this.props.persistedState?.currentTimeSelection) {
const { start, end } = this.props.persistedState.currentTimeSelection;
this.unitController.selectionRange = { start: BigInt(start), end: BigInt(end) };
Expand Down Expand Up @@ -197,6 +200,8 @@ export class TraceContextComponent extends React.Component<TraceContextProps, Tr
} else {
this.unitController.viewRange = { start: BigInt(0), end: this.state.experiment.end - this.state.timeOffset };
}
this.historyHandler.clear();
this.historyHandler.addCurrentState();
}

private async updateTrace() {
Expand Down Expand Up @@ -247,12 +252,16 @@ export class TraceContextComponent extends React.Component<TraceContextProps, Tr
signalManager().on(Signals.THEME_CHANGED, this.onBackgroundThemeChange);
signalManager().on(Signals.UPDATE_ZOOM, this.onUpdateZoom);
signalManager().on(Signals.RESET_ZOOM, this.onResetZoom);
signalManager().on(Signals.UNDO, this.undoHistory);
signalManager().on(Signals.REDO, this.redoHistory);
}

private unsubscribeToEvents() {
signalManager().off(Signals.THEME_CHANGED, this.onBackgroundThemeChange);
signalManager().off(Signals.UPDATE_ZOOM, this.onUpdateZoom);
signalManager().off(Signals.RESET_ZOOM, this.onResetZoom);
signalManager().off(Signals.UNDO, this.undoHistory);
signalManager().off(Signals.REDO, this.redoHistory);
}

async componentDidUpdate(prevProps: TraceContextProps): Promise<void> {
Expand Down Expand Up @@ -318,14 +327,14 @@ export class TraceContextComponent extends React.Component<TraceContextProps, Tr
});
this.setState(prevState => ({
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) {
Expand All @@ -348,15 +357,16 @@ export class TraceContextComponent extends React.Component<TraceContextProps, Tr
return <div className='trace-context-container'
onContextMenu={event => this.onContextMenu(event)}
onKeyDown={event => this.onKeyDown(event)}
onKeyUp={event => this.onKeyUp(event)}
ref={this.traceContextContainer}>
<TooltipComponent ref={this.tooltipComponent} />
<TooltipXYComponent ref={this.tooltipXYComponent} />
{shouldRenderOutputs ? this.renderOutputs() : this.renderPlaceHolder()}
</div>;
}

private onKeyDown(key: React.KeyboardEvent) {
switch (key.key) {
private onKeyDown(event: React.KeyboardEvent) {
switch (event.key) {
case '+':
case '=': {
this.zoomButton(true);
Expand All @@ -370,6 +380,27 @@ export class TraceContextComponent extends React.Component<TraceContextProps, Tr
}
}

private onKeyUp(event: React.KeyboardEvent): void {
if (event.ctrlKey) {
switch (event.key) {
case 'z': {
this.undoHistory();
break;
}
case 'Z': {
if (event.shiftKey) {
this.redoHistory();
break;
}
}
case 'y': {
this.redoHistory();
break;
}
}
}
}

private renderOutputs() {
this.generateGridLayout();
const chartWidth = Math.max(0, this.state.style.width - this.state.style.chartOffset);
Expand Down Expand Up @@ -480,6 +511,18 @@ export class TraceContextComponent extends React.Component<TraceContextProps, Tr
</div>;
}

private undoHistory = (): void => {
this.historyHandler.undo();
};

private redoHistory = (): void => {
this.historyHandler.redo();
};

private updateHistory = (): void => {
this.historyHandler.addCurrentState();
};

private generateGridLayout(): void {
let existingTimeScaleLayouts: Array<Layout> = [];
let existingNonTimeScaleLayouts: Array<Layout> = [];
Expand Down
Original file line number Diff line number Diff line change
@@ -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<HistoryItem>;
protected index = 0;
protected maxAllowedIndex = 0;
protected timeout?: ReturnType<typeof setTimeout>;
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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,25 @@ export class ZoomPanShortcutsTable extends React.Component {
<div className='shortcuts-table-column'>
<table>
<tbody>
<tr>
<td><i className='fa fa-undo fa-lg' /> Undo</td>
<td className='monaco-keybinding shortcuts-table-keybinding'>
<span className='monaco-keybinding-key'>CTRL</span>
<span className='monaco-keybinding-key'>Z</span>
</td>
</tr>
<tr>
<td><i className='fa fa-repeat fa-lg' /> Redo</td>
<td className='monaco-keybinding shortcuts-table-keybinding'>
<span className='monaco-keybinding-key'>CTRL</span>
<span className='monaco-keybinding-key'>SHIFT</span>
<span className='monaco-keybinding-key'>Z</span>
<span className='monaco-keybinding-seperator'>or</span>
<span className='monaco-keybinding-key'>CTRL</span>
<span className='monaco-keybinding-key'>Y</span>

</td>
</tr>
<tr>
<td>Zoom to selected range</td>
<td className='monaco-keybinding shortcuts-table-keybinding'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading

0 comments on commit b15ad94

Please sign in to comment.