diff --git a/DoodleBUGS/package-lock.json b/DoodleBUGS/package-lock.json index 603ed73a8..e06ad630f 100644 --- a/DoodleBUGS/package-lock.json +++ b/DoodleBUGS/package-lock.json @@ -20,6 +20,7 @@ "cytoscape-no-overlap": "^1.0.1", "cytoscape-snap-to-grid": "^2.0.0", "cytoscape-svg": "^0.4.0", + "cytoscape-undo-redo": "^1.3.3", "pinia": "^3.0.2", "vue": "^3.5.13" }, @@ -2731,6 +2732,15 @@ "cytoscape": "^3.2.0" } }, + "node_modules/cytoscape-undo-redo": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/cytoscape-undo-redo/-/cytoscape-undo-redo-1.3.3.tgz", + "integrity": "sha512-BjTXe0Oiytj4+UtYkkwYHG3BILi6MZD1MuZ+06kopgEtiyumcMEqX53zHMhY90kqJMPwLfpcrlmKmJtNAa2v2Q==", + "license": "MIT", + "peerDependencies": { + "cytoscape": "^3.3.0" + } + }, "node_modules/d3-dispatch": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz", diff --git a/DoodleBUGS/package.json b/DoodleBUGS/package.json index 3a9154fd2..071c04ca4 100644 --- a/DoodleBUGS/package.json +++ b/DoodleBUGS/package.json @@ -24,6 +24,7 @@ "cytoscape-no-overlap": "^1.0.1", "cytoscape-snap-to-grid": "^2.0.0", "cytoscape-svg": "^0.4.0", + "cytoscape-undo-redo": "^1.3.3", "pinia": "^3.0.2", "vue": "^3.5.13" }, diff --git a/DoodleBUGS/src/components/canvas/CanvasToolbar.vue b/DoodleBUGS/src/components/canvas/CanvasToolbar.vue index 6b34c8d81..9f7625e2c 100644 --- a/DoodleBUGS/src/components/canvas/CanvasToolbar.vue +++ b/DoodleBUGS/src/components/canvas/CanvasToolbar.vue @@ -1,5 +1,6 @@ + + \ No newline at end of file diff --git a/DoodleBUGS/src/composables/useGraphElements.ts b/DoodleBUGS/src/composables/useGraphElements.ts index 7d795a3ff..df662424f 100644 --- a/DoodleBUGS/src/composables/useGraphElements.ts +++ b/DoodleBUGS/src/composables/useGraphElements.ts @@ -1,9 +1,11 @@ import { computed, ref } from 'vue'; import { useGraphStore } from '../stores/graphStore'; -import type { GraphElement } from '../types'; +import { useGraphInstance } from './useGraphInstance'; +import type { GraphElement, GraphNode } from '../types'; export function useGraphElements() { const graphStore = useGraphStore(); + const { getCyInstance, getUndoRedoInstance } = useGraphInstance(); const selectedElement = ref(null); @@ -17,8 +19,32 @@ export function useGraphElements() { }); const addElement = (newElement: GraphElement) => { + // Add to Vue store first for immediate UI update elements.value = [...elements.value, newElement]; selectedElement.value = newElement; + + // Add to cytoscape - let cytoscape-undo-redo handle the tracking + const cy = getCyInstance(); + const ur = getUndoRedoInstance(); + + if (cy) { + try { + const cyElement = cy.add({ + group: newElement.type === 'node' ? 'nodes' : 'edges', + data: newElement, + position: newElement.type === 'node' ? (newElement as GraphNode).position : undefined + }); + + console.log('Element added to cytoscape:', newElement.id); + + // Manually trigger undo-redo tracking if needed + if (ur) { + console.log('Undo-redo instance available for tracking'); + } + } catch (error) { + console.warn('Failed to add element to cytoscape:', error); + } + } }; const updateElement = (updatedElement: GraphElement) => { @@ -30,7 +56,40 @@ export function useGraphElements() { } }; - const deleteElement = (elementId: string) => { + const deleteElement = (elementId: string, visited = new Set()) => { + const elementToDelete = elements.value.find(el => el.id === elementId); + if (!elementToDelete) return; + + // Remove from Vue store for immediate UI update + deleteElementManually(elementId, visited); + + // Remove from cytoscape - let cytoscape-undo-redo handle the tracking + const cy = getCyInstance(); + const ur = getUndoRedoInstance(); + + if (cy) { + try { + const element = cy.getElementById(elementId); + if (element.length > 0) { + element.remove(); + console.log('Element removed from cytoscape:', elementId); + + if (ur) { + console.log('Undo-redo instance available for tracking removal'); + } + } + } catch (error) { + console.warn('Failed to remove element from cytoscape:', error); + } + } + }; + + const deleteElementManually = (elementId: string, visited = new Set()) => { + if (visited.has(elementId)) { + return; + } + visited.add(elementId); + const elementToDelete = elements.value.find(el => el.id === elementId); if (!elementToDelete) return; diff --git a/DoodleBUGS/src/composables/useGraphInstance.ts b/DoodleBUGS/src/composables/useGraphInstance.ts index 32e8dae52..69c05389e 100644 --- a/DoodleBUGS/src/composables/useGraphInstance.ts +++ b/DoodleBUGS/src/composables/useGraphInstance.ts @@ -10,6 +10,8 @@ import cola from 'cytoscape-cola'; import klay from 'cytoscape-klay'; import { useCompoundDragDrop } from './useCompoundDragDrop'; import svg from 'cytoscape-svg'; +// @ts-ignore +import undoRedo from 'cytoscape-undo-redo'; // NOTE: Do NOT register gridGuide or contextMenus - they break iPad/mobile touch events cytoscape.use(dagre); @@ -17,8 +19,11 @@ cytoscape.use(fcose); cytoscape.use(cola); cytoscape.use(klay); cytoscape.use(svg); +// @ts-ignore +cytoscape.use(undoRedo); let cyInstance: Core | null = null; +let undoRedoInstance: any = null; export function useGraphInstance() { const initCytoscape = (container: HTMLElement, initialElements: ElementDefinition[]): Core => { @@ -173,6 +178,22 @@ export function useGraphInstance() { }); */ + // Initialize undo-redo functionality safely + try { + undoRedoInstance = (cyInstance as any).undoRedo({ + isDebug: true, // Set to true for debugging + undoableDrag: true, // Allow dragging to be undoable + stackSizeLimit: undefined, // Unlimited stack size + ready: () => { + // This function is called when undo-redo is ready + console.log('Undo-redo functionality initialized successfully'); + } + }); + } catch (error) { + console.warn('Undo-redo initialization failed:', error); + undoRedoInstance = null; + } + return cyInstance; }; @@ -180,10 +201,57 @@ export function useGraphInstance() { if (cy) { cy.destroy(); cyInstance = null; + undoRedoInstance = null; } }; const getCyInstance = (): Core | null => cyInstance; - return { initCytoscape, destroyCytoscape, getCyInstance }; + const getUndoRedoInstance = (): any => undoRedoInstance; + + const undo = (): boolean => { + if (undoRedoInstance) { + return undoRedoInstance.undo(); + } + return false; + }; + + const redo = (): boolean => { + if (undoRedoInstance) { + return undoRedoInstance.redo(); + } + return false; + }; + + const isUndoStackEmpty = (): boolean => { + if (undoRedoInstance) { + return undoRedoInstance.isUndoStackEmpty(); + } + return true; + }; + + const isRedoStackEmpty = (): boolean => { + if (undoRedoInstance) { + return undoRedoInstance.isRedoStackEmpty(); + } + return true; + }; + + const resetUndoRedoStack = (): void => { + if (undoRedoInstance) { + undoRedoInstance.reset(); + } + }; + + return { + initCytoscape, + destroyCytoscape, + getCyInstance, + getUndoRedoInstance, + undo, + redo, + isUndoStackEmpty, + isRedoStackEmpty, + resetUndoRedoStack + }; } diff --git a/DoodleBUGS/src/composables/useUndoRedo.ts b/DoodleBUGS/src/composables/useUndoRedo.ts new file mode 100644 index 000000000..9a8c0263b --- /dev/null +++ b/DoodleBUGS/src/composables/useUndoRedo.ts @@ -0,0 +1,103 @@ +import { ref, computed } from 'vue'; +import { useGraphInstance } from './useGraphInstance'; +import { useGraphStore } from '../stores/graphStore'; +import type { GraphElement, GraphNode, GraphEdge } from '../types'; + +export function useUndoRedo() { + const { undo, redo, isUndoStackEmpty, isRedoStackEmpty, resetUndoRedoStack, getCyInstance, getUndoRedoInstance } = useGraphInstance(); + const graphStore = useGraphStore(); + + // Reactive state for UI updates + const canUndo = ref(false); + const canRedo = ref(false); + + // Update the state by checking the actual undo-redo instance + const updateUndoRedoState = () => { + try { + const newCanUndo = !isUndoStackEmpty(); + const newCanRedo = !isRedoStackEmpty(); + + if (newCanUndo !== canUndo.value || newCanRedo !== canRedo.value) { + canUndo.value = newCanUndo; + canRedo.value = newCanRedo; + console.log(`Undo/Redo state updated: undo=${newCanUndo}, redo=${newCanRedo}`); + } + } catch (error) { + console.warn('Failed to update undo/redo state:', error); + canUndo.value = false; + canRedo.value = false; + } + }; + + // Perform undo operation + const performUndo = (): boolean => { + console.log('Attempting undo...'); + try { + const result = undo(); + console.log(`Undo result: ${result}`); + updateUndoRedoState(); + return result; + } catch (error) { + console.error('Undo failed:', error); + return false; + } + }; + + // Perform redo operation + const performRedo = (): boolean => { + console.log('Attempting redo...'); + try { + const result = redo(); + console.log(`Redo result: ${result}`); + updateUndoRedoState(); + return result; + } catch (error) { + console.error('Redo failed:', error); + return false; + } + }; + + // Reset undo/redo stacks + const resetStacks = (): void => { + try { + resetUndoRedoStack(); + updateUndoRedoState(); + console.log('Reset undo/redo stacks'); + } catch (error) { + console.warn('Failed to reset stacks:', error); + } + }; + + // Keyboard shortcuts handler + const handleKeyboardShortcuts = (event: KeyboardEvent): void => { + // Only handle shortcuts if not typing in an input field + const target = event.target as HTMLElement; + if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.contentEditable === 'true') { + return; + } + + // Ctrl+Z for undo + if (event.ctrlKey && event.key === 'z' && !event.shiftKey) { + event.preventDefault(); + console.log('Keyboard shortcut: Ctrl+Z (Undo)'); + performUndo(); + } + // Ctrl+Shift+Z or Ctrl+Y for redo + else if ((event.ctrlKey && event.shiftKey && event.key === 'Z') || + (event.ctrlKey && event.key === 'y')) { + event.preventDefault(); + console.log('Keyboard shortcut: Ctrl+Shift+Z or Ctrl+Y (Redo)'); + performRedo(); + } + }; + + return { + canUndo: computed(() => canUndo.value), + canRedo: computed(() => canRedo.value), + performUndo, + performRedo, + resetStacks, + updateUndoRedoState, + handleKeyboardShortcuts + }; +} \ No newline at end of file diff --git a/DoodleBUGS/src/types/cytoscape-undo-redo.d.ts b/DoodleBUGS/src/types/cytoscape-undo-redo.d.ts new file mode 100644 index 000000000..8d9acf61e --- /dev/null +++ b/DoodleBUGS/src/types/cytoscape-undo-redo.d.ts @@ -0,0 +1,31 @@ +declare module 'cytoscape-undo-redo' { + import { Core } from 'cytoscape'; + + interface UndoRedoOptions { + isDebug?: boolean; + actions?: { + [key: string]: { + undo: (arg: any) => any; + redo: (arg: any) => any; + }; + }; + undoableDrag?: boolean; + stackSizeLimit?: number; + ready?: () => void; + } + + interface UndoRedoInstance { + do: (actionName: string, args: any) => any; + undo: () => boolean; + redo: () => boolean; + reset: () => void; + isUndoStackEmpty: () => boolean; + isRedoStackEmpty: () => boolean; + getUndoStackSize: () => number; + getRedoStackSize: () => number; + } + + function undoRedo(options?: UndoRedoOptions): (cy: Core) => UndoRedoInstance; + + export = undoRedo; +} \ No newline at end of file diff --git a/DoodleBUGS/src/types/index.ts b/DoodleBUGS/src/types/index.ts index c2755da09..f32b90b40 100644 --- a/DoodleBUGS/src/types/index.ts +++ b/DoodleBUGS/src/types/index.ts @@ -13,8 +13,8 @@ export interface GraphNode { id: string; name: string; type: 'node'; - nodeType: NodeType; - position: { x: number; y: number; }; + nodeType?: string; + position?: { x: number; y: number }; parent?: string; // Properties from definitions