Added context menu for easier editing

This commit is contained in:
2025-09-15 21:02:15 +02:00
parent d1ddf5c256
commit 6d4622e109
4 changed files with 315 additions and 126 deletions

View File

@@ -1,4 +1,7 @@
<template>
<div class="p-2 bg-cyan-600 rounded w-full my-4">
<p>Developer's tip: Right click a sprite to open the context menu and add, replace or remove sprites.</p>
</div>
<div class="space-y-4">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 sm:gap-0">
<div class="flex flex-col sm:flex-row items-start sm:items-center gap-3 sm:gap-4 w-full sm:w-auto">
@@ -33,9 +36,40 @@
</div>
<div class="canvas-container touch-manipulation" :style="{ transform: `scale(${zoom})`, transformOrigin: 'top left' }">
<canvas ref="canvasRef" @mousedown="startDrag" @mousemove="drag" @mouseup="stopDrag" @mouseleave="stopDrag" @touchstart="handleTouchStart" @touchmove="handleTouchMove" @touchend="stopDrag" class="w-full" :style="settingsStore.pixelPerfect ? { 'image-rendering': 'pixelated' } : {}"></canvas>
<canvas
ref="canvasRef"
@mousedown="startDrag"
@mousemove="drag"
@mouseup="stopDrag"
@mouseleave="stopDrag"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="stopDrag"
@contextmenu.prevent
class="w-full"
:style="settingsStore.pixelPerfect ? { 'image-rendering': 'pixelated' } : {}"
></canvas>
</div>
</div>
<!-- Context Menu -->
<div v-if="showContextMenu" class="fixed bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg shadow-lg z-50 py-1" :style="{ left: contextMenuX + 'px', top: contextMenuY + 'px' }">
<button @click="addSprite" class="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 flex items-center gap-2">
<i class="fas fa-plus"></i>
Add sprite
</button>
<button v-if="contextMenuSpriteId" @click="replaceSprite" class="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 flex items-center gap-2">
<i class="fas fa-exchange-alt"></i>
Replace sprite
</button>
<button v-if="contextMenuSpriteId" @click="removeSprite" class="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-red-600 dark:text-red-400 flex items-center gap-2">
<i class="fas fa-trash"></i>
Remove sprite
</button>
</div>
<!-- Hidden file input for replace functionality -->
<input ref="fileInput" type="file" accept="image/*" class="hidden" @change="handleFileChange" />
</div>
</template>
@@ -66,6 +100,9 @@
const emit = defineEmits<{
(e: 'updateSprite', id: string, x: number, y: number): void;
(e: 'updateSpriteCell', id: string, newIndex: number): void;
(e: 'removeSprite', id: string): void;
(e: 'replaceSprite', id: string, file: File): void;
(e: 'addSprite', file: File): void;
}>();
// Get settings from store
@@ -90,6 +127,12 @@
const highlightCell = ref<CellPosition | null>(null);
const showAllSprites = ref(false);
const showContextMenu = ref(false);
const contextMenuX = ref(0);
const contextMenuY = ref(0);
const contextMenuSpriteId = ref<string | null>(null);
const replacingSpriteId = ref<string | null>(null);
const fileInput = ref<HTMLInputElement | null>(null);
const spritePositions = computed(() => {
const { maxWidth, maxHeight } = calculateMaxDimensions();
@@ -146,6 +189,27 @@
const startDrag = (event: MouseEvent) => {
if (!canvasRef.value) return;
// Hide context menu if open
showContextMenu.value = false;
// Handle right-click for context menu
if ('button' in event && (event as MouseEvent).button === 2) {
event.preventDefault();
const rect = canvasRef.value.getBoundingClientRect();
const scaleX = canvasRef.value.width / rect.width;
const scaleY = canvasRef.value.height / rect.height;
const mouseX = (event.clientX - rect.left) * scaleX;
const mouseY = (event.clientY - rect.top) * scaleY;
const clickedSprite = findSpriteAtPosition(mouseX, mouseY);
contextMenuSpriteId.value = clickedSprite?.id || null;
contextMenuX.value = event.clientX;
contextMenuY.value = event.clientY;
showContextMenu.value = true;
return;
}
// Ignore non-left mouse buttons (but allow touch-generated events without a button prop)
if ('button' in event && (event as MouseEvent).button !== 0) return;
@@ -312,6 +376,60 @@
}
};
const removeSprite = () => {
if (contextMenuSpriteId.value) {
emit('removeSprite', contextMenuSpriteId.value);
showContextMenu.value = false;
contextMenuSpriteId.value = null;
}
};
const replaceSprite = () => {
if (contextMenuSpriteId.value && fileInput.value) {
// Store the sprite ID separately so it persists after context menu closes
replacingSpriteId.value = contextMenuSpriteId.value;
fileInput.value.click();
// Hide context menu immediately since we've stored the ID
showContextMenu.value = false;
contextMenuSpriteId.value = null;
}
};
const addSprite = () => {
if (fileInput.value) {
fileInput.value.click();
// Hide context menu immediately
showContextMenu.value = false;
contextMenuSpriteId.value = null;
}
};
const handleFileChange = (event: Event) => {
const input = event.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
const file = input.files[0];
if (file.type.startsWith('image/')) {
if (replacingSpriteId.value) {
emit('replaceSprite', replacingSpriteId.value, file);
} else {
// Adding new sprite
emit('addSprite', file);
}
} else {
alert('Please select an image file.');
}
}
// Clean up after file selection
replacingSpriteId.value = null;
input.value = '';
};
const hideContextMenu = () => {
showContextMenu.value = false;
contextMenuSpriteId.value = null;
};
const handleTouchMove = (event: TouchEvent) => {
// Only prevent default when we're actually dragging
if (isDragging.value) {
@@ -466,10 +584,14 @@
// Listen for forceRedraw event from App.vue
window.addEventListener('forceRedraw', handleForceRedraw);
// Hide context menu when clicking elsewhere
document.addEventListener('click', hideContextMenu);
});
onUnmounted(() => {
window.removeEventListener('forceRedraw', handleForceRedraw);
document.removeEventListener('click', hideContextMenu);
});
// Handler for force redraw event