Added context menu for easier editing
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user