From 244d48728c3e278e51f6a4bc55ab8f860af71c21 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Wed, 2 Jul 2025 09:16:16 +0200 Subject: [PATCH] Refactor canvas interactions and update keyboard shortcuts Reorganizes mouse and keyboard event handling in CanvasInteractions for clearer priority and improved usability. Adds global keyboard shortcuts for undo, redo, copy, and paste. Updates context-sensitive shortcuts to support both Delete and Backspace for layer removal. Refactors CanvasLayers to use delayed state saving via requestSaveState. Updates CanvasView shortcut documentation to reflect these changes and clarify mouse/keyboard actions. --- js/CanvasInteractions.js | 84 +++++++++++++++++++++++++++++----------- js/CanvasLayers.js | 10 ++--- js/CanvasView.js | 4 +- 3 files changed, 68 insertions(+), 30 deletions(-) diff --git a/js/CanvasInteractions.js b/js/CanvasInteractions.js index 656626e..03b97e3 100644 --- a/js/CanvasInteractions.js +++ b/js/CanvasInteractions.js @@ -70,20 +70,33 @@ export class CanvasInteractions { const worldCoords = this.canvas.getMouseWorldCoordinates(e); const viewCoords = this.canvas.getMouseViewCoordinates(e); - if (e.button === 2) { // Obsługa prawego przycisku myszy + // --- Ostateczna, poprawna kolejność sprawdzania --- + + // 1. Akcje globalne z modyfikatorami (mają najwyższy priorytet) + if (e.shiftKey && e.ctrlKey) { + this.startCanvasMove(worldCoords); + return; + } + if (e.shiftKey) { + this.startCanvasResize(worldCoords); + return; + } + + // 2. Inne przyciski myszy + if (e.button === 2) { // Prawy przycisk myszy const clickedLayerResult = this.canvas.canvasLayers.getLayerAtPosition(worldCoords.x, worldCoords.y); if (clickedLayerResult && this.canvas.selectedLayers.includes(clickedLayerResult.layer)) { e.preventDefault(); this.canvas.canvasLayers.showBlendModeMenu(viewCoords.x, viewCoords.y); - return; } + return; } - - if (e.button !== 0) { // Ignoruj inne przyciski niż lewy i prawy + if (e.button !== 0) { // Środkowy przycisk this.startPanning(e); return; } + // 3. Interakcje z elementami na płótnie (lewy przycisk) const transformTarget = this.canvas.canvasLayers.getHandleAtPosition(worldCoords.x, worldCoords.y); if (transformTarget) { this.startLayerTransform(transformTarget.layer, transformTarget.handle, worldCoords); @@ -92,17 +105,17 @@ export class CanvasInteractions { const clickedLayerResult = this.canvas.canvasLayers.getLayerAtPosition(worldCoords.x, worldCoords.y); if (clickedLayerResult) { - // Zaznacz warstwę i przygotuj się do potencjalnego przeciągania this.prepareForDrag(clickedLayerResult.layer, worldCoords); return; } - // Jeśli nie kliknięto na nic, rozpocznij panoramowanie lub wyczyść zaznaczenie + // 4. Domyślna akcja na tle (lewy przycisk bez modyfikatorów) this.startPanningOrClearSelection(e); } handleMouseMove(e) { const worldCoords = this.canvas.getMouseWorldCoordinates(e); + this.canvas.lastMousePosition = worldCoords; // Zawsze aktualizuj ostatnią pozycję myszy // Sprawdź, czy rozpocząć przeciąganie if (this.interaction.mode === 'potential-drag') { @@ -284,10 +297,45 @@ export class CanvasInteractions { e.preventDefault(); } - if (this.canvas.selectedLayer) { + // Globalne skróty (Undo/Redo/Copy/Paste) + if (e.ctrlKey || e.metaKey) { + let handled = true; + switch (e.key.toLowerCase()) { + case 'z': + if (e.shiftKey) { + this.canvas.redo(); + } else { + this.canvas.undo(); + } + break; + case 'y': + this.canvas.redo(); + break; + case 'c': + if (this.canvas.selectedLayers.length > 0) { + this.canvas.canvasLayers.copySelectedLayers(); + } + break; + case 'v': + this.canvas.canvasLayers.handlePaste('mouse'); + break; + default: + handled = false; + break; + } + if (handled) { + e.preventDefault(); + e.stopPropagation(); + return; + } + } + + // Skróty kontekstowe (zależne od zaznaczenia) + if (this.canvas.selectedLayers.length > 0) { const step = e.shiftKey ? 10 : 1; let needsRender = false; + // Używamy e.code dla spójności i niezależności od układu klawiatury const movementKeys = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'BracketLeft', 'BracketRight']; if (movementKeys.includes(e.code)) { e.preventDefault(); @@ -300,11 +348,11 @@ export class CanvasInteractions { if (e.code === 'ArrowDown') this.canvas.selectedLayers.forEach(l => l.y += step); if (e.code === 'BracketLeft') this.canvas.selectedLayers.forEach(l => l.rotation -= step); if (e.code === 'BracketRight') this.canvas.selectedLayers.forEach(l => l.rotation += step); - + needsRender = true; } - if (e.key === 'Delete') { + if (e.key === 'Delete' || e.key === 'Backspace') { e.preventDefault(); e.stopPropagation(); this.canvas.removeSelectedLayers(); @@ -312,7 +360,7 @@ export class CanvasInteractions { } if (needsRender) { - this.canvas.render(); // Tylko renderuj, nie zapisuj stanu + this.canvas.render(); } } } @@ -393,19 +441,8 @@ export class CanvasInteractions { } startPanningOrClearSelection(e) { - const worldCoords = this.canvas.getMouseWorldCoordinates(e); - - // Sprawdź modyfikatory dla akcji na tle - if (e.shiftKey) { - this.startCanvasResize(worldCoords); - return; - } - if (e.altKey) { - this.startCanvasMove(worldCoords); - return; - } - - // Domyślna akcja: wyczyść zaznaczenie i rozpocznij panoramowanie + // Ta funkcja jest teraz wywoływana tylko gdy kliknięto na tło bez modyfikatorów. + // Domyślna akcja: wyczyść zaznaczenie i rozpocznij panoramowanie. if (!this.interaction.isCtrlPressed) { this.canvas.updateSelection([]); } @@ -469,6 +506,7 @@ export class CanvasInteractions { this.canvas.viewport.y -= finalY; } this.canvas.render(); + this.canvas.saveState(); } startPanning(e) { diff --git a/js/CanvasLayers.js b/js/CanvasLayers.js index c4a2332..1077139 100644 --- a/js/CanvasLayers.js +++ b/js/CanvasLayers.js @@ -275,7 +275,7 @@ export class CanvasLayers { } this.canvas.render(); - this.canvas.saveState(); + this.canvas.requestSaveState(); // Użyj opóźnionego zapisu log.info(`Moved ${layersToMove.length} layer(s).`); } @@ -301,7 +301,7 @@ export class CanvasLayers { layer.height *= scale; }); this.canvas.render(); - this.canvas.saveState(); + this.canvas.requestSaveState(); // Użyj opóźnionego zapisu } /** @@ -315,7 +315,7 @@ export class CanvasLayers { layer.rotation += angle; }); this.canvas.render(); - this.canvas.saveState(); + this.canvas.requestSaveState(); // Użyj opóźnionego zapisu } getLayerAtPosition(worldX, worldY) { @@ -371,7 +371,7 @@ export class CanvasLayers { await Promise.all(promises); this.canvas.render(); - this.canvas.saveState(); + this.canvas.requestSaveState(); // Użyj opóźnionego zapisu } async mirrorVertical() { @@ -399,7 +399,7 @@ export class CanvasLayers { await Promise.all(promises); this.canvas.render(); - this.canvas.saveState(); + this.canvas.requestSaveState(); // Użyj opóźnionego zapisu } async getLayerImageData(layer) { diff --git a/js/CanvasView.js b/js/CanvasView.js index 71dc1f5..c0f30a0 100644 --- a/js/CanvasView.js +++ b/js/CanvasView.js @@ -429,7 +429,7 @@ async function createCanvasWidget(node, widget, app) { Mouse WheelZoom view in/out Shift + Click (background)Start resizing canvas area Shift + Ctrl + ClickStart moving entire canvas - Double Click (background)Deselect all layers + Single Click (background)Deselect all layers

Clipboard & I/O

@@ -444,7 +444,7 @@ async function createCanvasWidget(node, widget, app) { Click + DragMove selected layer(s) Ctrl + ClickAdd/Remove layer from selection Alt + DragClone selected layer(s) - Shift + ClickShow blend mode & opacity menu + Right ClickShow blend mode & opacity menu Mouse WheelScale layer (snaps to grid) Ctrl + Mouse WheelFine-scale layer Shift + Mouse WheelRotate layer by 5°