diff --git a/js/CanvasInteractions.js b/js/CanvasInteractions.js index 4290a78..cc17e60 100644 --- a/js/CanvasInteractions.js +++ b/js/CanvasInteractions.js @@ -94,6 +94,16 @@ export class CanvasInteractions { this.canvas.canvas.style.border = ''; } } + isEditableElement(target) { + if (!(target instanceof HTMLElement)) { + return false; + } + if (target.isContentEditable) { + return true; + } + const editableSelector = 'input, textarea, select, [contenteditable="true"]'; + return !!target.closest(editableSelector); + } setupEventListeners() { this.canvas.canvas.addEventListener('mousedown', this.onMouseDown); this.canvas.canvas.addEventListener('mousemove', this.onMouseMove); @@ -538,6 +548,9 @@ export class CanvasInteractions { this.interaction.isShiftPressed = true; if (e.key === 'Alt') this.interaction.isAltPressed = true; + if (this.isEditableElement(e.target) || this.isEditableElement(document.activeElement)) { + return; + } // Check if canvas is focused before handling any shortcuts const shouldHandle = this.canvas.isMouseOver || this.canvas.canvas.contains(document.activeElement) || diff --git a/js/CanvasLayersPanel.js b/js/CanvasLayersPanel.js index 0e2f46c..adca438 100644 --- a/js/CanvasLayersPanel.js +++ b/js/CanvasLayersPanel.js @@ -118,7 +118,19 @@ export class CanvasLayersPanel { this.setupControlButtons(); this.setupMasterVisibilityToggle(); // Dodaj listener dla klawiatury, aby usuwanie działało z panelu + const isEditableTarget = (target) => { + if (!(target instanceof HTMLElement)) { + return false; + } + if (target.isContentEditable) { + return true; + } + return !!target.closest('input, textarea, select, [contenteditable="true"]'); + }; this.container.addEventListener('keydown', (e) => { + if (isEditableTarget(e.target)) { + return; + } if (e.key === 'Delete' || e.key === 'Backspace') { e.preventDefault(); e.stopPropagation(); diff --git a/js/CanvasView.js b/js/CanvasView.js index b9ce8a4..9967114 100644 --- a/js/CanvasView.js +++ b/js/CanvasView.js @@ -1,6 +1,8 @@ // @ts-ignore import { app } from "../../scripts/app.js"; // @ts-ignore +import { ChangeTracker } from "../../scripts/changeTracker.js"; +// @ts-ignore import { $el } from "../../scripts/ui.js"; import { addStylesheet, getUrl, loadTemplate } from "./utils/ResourceManager.js"; import { Canvas } from "./Canvas.js"; @@ -12,6 +14,52 @@ import { showErrorNotification, showSuccessNotification, showInfoNotification, s import { iconLoader, LAYERFORGE_TOOLS } from "./utils/IconLoader.js"; import { setupSAMDetectorHook } from "./SAMDetectorIntegration.js"; const log = createModuleLogger('Canvas_view'); +const LAYERFORGE_CHANGE_TRACKER_PATCH_FLAG = '__layerForgeUndoRedoPatched'; +const LAYERFORGE_SHORTCUT_ACTIVE_ATTR = 'data-layerforge-shortcuts-active'; +const isLayerForgeEditableElement = (target) => { + if (!(target instanceof HTMLElement)) { + return false; + } + if (target.isContentEditable) { + return true; + } + return !!target.closest('.painterMainContainer input, .painterMainContainer textarea, .painterMainContainer select, .painterMainContainer [contenteditable="true"]'); +}; +const isLayerForgeShortcutContextElement = (target) => { + return target instanceof HTMLElement && !!target.closest('.painterMainContainer'); +}; +const isLayerForgeShortcutContextActive = (event) => { + if (event && isLayerForgeShortcutContextElement(event.target)) { + return true; + } + if (isLayerForgeShortcutContextElement(document.activeElement)) { + return true; + } + return !!document.querySelector(`.painterMainContainer[${LAYERFORGE_SHORTCUT_ACTIVE_ATTR}="true"]`); +}; +const isLayerForgeEditableFocused = () => { + return isLayerForgeEditableElement(document.activeElement); +}; +const patchLayerForgeChangeTrackerUndoRedo = () => { + const prototype = ChangeTracker?.prototype; + if (!prototype || prototype[LAYERFORGE_CHANGE_TRACKER_PATCH_FLAG] || typeof prototype.undoRedo !== 'function') { + return; + } + const originalUndoRedo = prototype.undoRedo; + prototype.undoRedo = async function (event) { + if (isLayerForgeShortcutContextActive(event)) { + return false; + } + return await originalUndoRedo.call(this, event); + }; + Object.defineProperty(prototype, LAYERFORGE_CHANGE_TRACKER_PATCH_FLAG, { + value: true, + configurable: false, + enumerable: false, + writable: false + }); +}; +patchLayerForgeChangeTrackerUndoRedo(); async function createCanvasWidget(node, widget, app) { const canvas = new Canvas(node, widget, { onStateChange: () => updateOutput(node, canvas) @@ -906,6 +954,70 @@ async function createCanvasWidget(node, widget, app) { height: "100%" } }, [controlPanel, canvasContainer, layersPanelContainer]); + const stopEditableClipboardLeak = (event) => { + if (isLayerForgeEditableElement(event.target) || isLayerForgeEditableFocused()) { + event.stopPropagation(); + event.stopImmediatePropagation(); + } + }; + mainContainer.addEventListener('copy', stopEditableClipboardLeak); + mainContainer.addEventListener('cut', stopEditableClipboardLeak); + mainContainer.addEventListener('paste', stopEditableClipboardLeak); + const setShortcutContextActive = (active) => { + if (active) { + mainContainer.setAttribute(LAYERFORGE_SHORTCUT_ACTIVE_ATTR, 'true'); + } + else { + mainContainer.removeAttribute(LAYERFORGE_SHORTCUT_ACTIVE_ATTR); + } + }; + const handleShortcutContextFocusIn = () => { + setShortcutContextActive(true); + }; + const handleShortcutContextFocusOut = () => { + requestAnimationFrame(() => { + if (!mainContainer.contains(document.activeElement)) { + setShortcutContextActive(false); + } + }); + }; + const handleShortcutContextPointerEnter = () => { + setShortcutContextActive(true); + }; + const handleShortcutContextPointerLeave = () => { + if (!mainContainer.contains(document.activeElement)) { + setShortcutContextActive(false); + } + }; + const handleRootUndoRedo = (event) => { + if (isLayerForgeEditableElement(event.target)) { + return; + } + const isPrimaryModifier = (event.ctrlKey || event.metaKey) && !event.altKey; + if (!isPrimaryModifier) { + return; + } + const key = event.key.toLowerCase(); + const isUndo = key === 'z' && !event.shiftKey; + const isRedo = key === 'y' || (key === 'z' && event.shiftKey); + if (!isUndo && !isRedo) { + return; + } + event.preventDefault(); + event.stopPropagation(); + event.stopImmediatePropagation(); + if (isRedo) { + canvas.redo(); + } + else { + canvas.undo(); + } + }; + mainContainer.addEventListener('focusin', handleShortcutContextFocusIn); + mainContainer.addEventListener('focusout', handleShortcutContextFocusOut); + mainContainer.addEventListener('pointerenter', handleShortcutContextPointerEnter); + mainContainer.addEventListener('pointerleave', handleShortcutContextPointerLeave); + mainContainer.addEventListener('keydown', handleRootUndoRedo, true); if (node.addDOMWidget) { node.addDOMWidget("mainContainer", "widget", mainContainer); } @@ -1029,7 +1141,18 @@ async function createCanvasWidget(node, widget, app) { } return { canvas: canvas, - panel: controlPanel + panel: controlPanel, + destroy: () => { + mainContainer.removeEventListener('copy', stopEditableClipboardLeak); + mainContainer.removeEventListener('cut', stopEditableClipboardLeak); + mainContainer.removeEventListener('paste', stopEditableClipboardLeak); + mainContainer.removeEventListener('focusin', handleShortcutContextFocusIn); + mainContainer.removeEventListener('focusout', handleShortcutContextFocusOut); + mainContainer.removeEventListener('pointerenter', handleShortcutContextPointerEnter); + mainContainer.removeEventListener('pointerleave', handleShortcutContextPointerLeave); + mainContainer.removeEventListener('keydown', handleRootUndoRedo, true); + mainContainer.removeAttribute(LAYERFORGE_SHORTCUT_ACTIVE_ATTR); + } }; } const canvasNodeInstances = new Map(); diff --git a/src/CanvasInteractions.ts b/src/CanvasInteractions.ts index bc1004a..fc1f6e9 100644 --- a/src/CanvasInteractions.ts +++ b/src/CanvasInteractions.ts @@ -163,6 +163,19 @@ export class CanvasInteractions { } } + private isEditableElement(target: EventTarget | null): boolean { + if (!(target instanceof HTMLElement)) { + return false; + } + + if (target.isContentEditable) { + return true; + } + + const editableSelector = 'input, textarea, select, [contenteditable="true"]'; + return !!target.closest(editableSelector); + } + setupEventListeners(): void { this.canvas.canvas.addEventListener('mousedown', this.onMouseDown as EventListener); this.canvas.canvas.addEventListener('mousemove', this.onMouseMove as EventListener); @@ -666,6 +679,10 @@ export class CanvasInteractions { if (e.key === 'Shift') this.interaction.isShiftPressed = true; if (e.key === 'Alt') this.interaction.isAltPressed = true; + if (this.isEditableElement(e.target) || this.isEditableElement(document.activeElement)) { + return; + } + // Check if canvas is focused before handling any shortcuts const shouldHandle = this.canvas.isMouseOver || this.canvas.canvas.contains(document.activeElement) || diff --git a/src/CanvasLayersPanel.ts b/src/CanvasLayersPanel.ts index 6626a5e..a33d9b8 100644 --- a/src/CanvasLayersPanel.ts +++ b/src/CanvasLayersPanel.ts @@ -139,7 +139,23 @@ export class CanvasLayersPanel { this.setupMasterVisibilityToggle(); // Dodaj listener dla klawiatury, aby usuwanie działało z panelu + const isEditableTarget = (target: EventTarget | null): boolean => { + if (!(target instanceof HTMLElement)) { + return false; + } + + if (target.isContentEditable) { + return true; + } + + return !!target.closest('input, textarea, select, [contenteditable="true"]'); + }; + this.container.addEventListener('keydown', (e: KeyboardEvent) => { + if (isEditableTarget(e.target)) { + return; + } + if (e.key === 'Delete' || e.key === 'Backspace') { e.preventDefault(); e.stopPropagation(); diff --git a/src/CanvasView.ts b/src/CanvasView.ts index ec1890a..3faabf7 100644 --- a/src/CanvasView.ts +++ b/src/CanvasView.ts @@ -5,6 +5,8 @@ import {api} from "../../scripts/api.js"; // @ts-ignore import {ComfyApp} from "../../scripts/app.js"; // @ts-ignore +import {ChangeTracker} from "../../scripts/changeTracker.js"; +// @ts-ignore import {$el} from "../../scripts/ui.js"; import { addStylesheet, getUrl, loadTemplate } from "./utils/ResourceManager.js"; @@ -21,6 +23,66 @@ import type { ComfyNode, Layer, AddMode } from './types'; const log = createModuleLogger('Canvas_view'); +const LAYERFORGE_CHANGE_TRACKER_PATCH_FLAG = '__layerForgeUndoRedoPatched'; +const LAYERFORGE_SHORTCUT_ACTIVE_ATTR = 'data-layerforge-shortcuts-active'; + +const isLayerForgeEditableElement = (target: EventTarget | null): boolean => { + if (!(target instanceof HTMLElement)) { + return false; + } + + if (target.isContentEditable) { + return true; + } + + return !!target.closest('.painterMainContainer input, .painterMainContainer textarea, .painterMainContainer select, .painterMainContainer [contenteditable="true"]'); +}; + +const isLayerForgeShortcutContextElement = (target: EventTarget | null): boolean => { + return target instanceof HTMLElement && !!target.closest('.painterMainContainer'); +}; + +const isLayerForgeShortcutContextActive = (event?: KeyboardEvent): boolean => { + if (event && isLayerForgeShortcutContextElement(event.target)) { + return true; + } + + if (isLayerForgeShortcutContextElement(document.activeElement)) { + return true; + } + + return !!document.querySelector(`.painterMainContainer[${LAYERFORGE_SHORTCUT_ACTIVE_ATTR}="true"]`); +}; + +const isLayerForgeEditableFocused = (): boolean => { + return isLayerForgeEditableElement(document.activeElement); +}; + +const patchLayerForgeChangeTrackerUndoRedo = (): void => { + const prototype = ChangeTracker?.prototype as any; + if (!prototype || prototype[LAYERFORGE_CHANGE_TRACKER_PATCH_FLAG] || typeof prototype.undoRedo !== 'function') { + return; + } + + const originalUndoRedo = prototype.undoRedo; + prototype.undoRedo = async function (event: KeyboardEvent) { + if (isLayerForgeShortcutContextActive(event)) { + return false; + } + + return await originalUndoRedo.call(this, event); + }; + + Object.defineProperty(prototype, LAYERFORGE_CHANGE_TRACKER_PATCH_FLAG, { + value: true, + configurable: false, + enumerable: false, + writable: false + }); +}; + +patchLayerForgeChangeTrackerUndoRedo(); + interface CanvasWidget { canvas: Canvas; panel: HTMLDivElement; @@ -1027,6 +1089,81 @@ $el("label.clipboard-switch.mask-switch", { } }, [controlPanel, canvasContainer, layersPanelContainer]) as HTMLDivElement; + const stopEditableClipboardLeak = (event: ClipboardEvent) => { + if (isLayerForgeEditableElement(event.target) || isLayerForgeEditableFocused()) { + event.stopPropagation(); + event.stopImmediatePropagation(); + } + }; + + mainContainer.addEventListener('copy', stopEditableClipboardLeak); + mainContainer.addEventListener('cut', stopEditableClipboardLeak); + mainContainer.addEventListener('paste', stopEditableClipboardLeak); + + const setShortcutContextActive = (active: boolean) => { + if (active) { + mainContainer.setAttribute(LAYERFORGE_SHORTCUT_ACTIVE_ATTR, 'true'); + } else { + mainContainer.removeAttribute(LAYERFORGE_SHORTCUT_ACTIVE_ATTR); + } + }; + + const handleShortcutContextFocusIn = () => { + setShortcutContextActive(true); + }; + + const handleShortcutContextFocusOut = () => { + requestAnimationFrame(() => { + if (!mainContainer.contains(document.activeElement)) { + setShortcutContextActive(false); + } + }); + }; + + const handleShortcutContextPointerEnter = () => { + setShortcutContextActive(true); + }; + + const handleShortcutContextPointerLeave = () => { + if (!mainContainer.contains(document.activeElement)) { + setShortcutContextActive(false); + } + }; + + const handleRootUndoRedo = (event: KeyboardEvent) => { + if (isLayerForgeEditableElement(event.target)) { + return; + } + + const isPrimaryModifier = (event.ctrlKey || event.metaKey) && !event.altKey; + if (!isPrimaryModifier) { + return; + } + + const key = event.key.toLowerCase(); + const isUndo = key === 'z' && !event.shiftKey; + const isRedo = key === 'y' || (key === 'z' && event.shiftKey); + if (!isUndo && !isRedo) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + event.stopImmediatePropagation(); + + if (isRedo) { + canvas.redo(); + } else { + canvas.undo(); + } + }; + + mainContainer.addEventListener('focusin', handleShortcutContextFocusIn); + mainContainer.addEventListener('focusout', handleShortcutContextFocusOut); + mainContainer.addEventListener('pointerenter', handleShortcutContextPointerEnter); + mainContainer.addEventListener('pointerleave', handleShortcutContextPointerLeave); + mainContainer.addEventListener('keydown', handleRootUndoRedo, true); + if (node.addDOMWidget) { node.addDOMWidget("mainContainer", "widget", mainContainer); } @@ -1181,7 +1318,18 @@ $el("label.clipboard-switch.mask-switch", { return { canvas: canvas, - panel: controlPanel + panel: controlPanel, + destroy: () => { + mainContainer.removeEventListener('copy', stopEditableClipboardLeak); + mainContainer.removeEventListener('cut', stopEditableClipboardLeak); + mainContainer.removeEventListener('paste', stopEditableClipboardLeak); + mainContainer.removeEventListener('focusin', handleShortcutContextFocusIn); + mainContainer.removeEventListener('focusout', handleShortcutContextFocusOut); + mainContainer.removeEventListener('pointerenter', handleShortcutContextPointerEnter); + mainContainer.removeEventListener('pointerleave', handleShortcutContextPointerLeave); + mainContainer.removeEventListener('keydown', handleRootUndoRedo, true); + mainContainer.removeAttribute(LAYERFORGE_SHORTCUT_ACTIVE_ATTR); + } }; }