@@ -303,6 +290,8 @@
import { useAnimationFrames } from '@/composables/useAnimationFrames';
import { useGridMetrics } from '@/composables/useGridMetrics';
import { useBackgroundStyles } from '@/composables/useBackgroundStyles';
+ import { useContextMenu } from '@/composables/useContextMenu';
+ import SpriteContextMenu from '@/components/shared/SpriteContextMenu.vue';
import CopyToFrameModal from './utilities/CopyToFrameModal.vue';
const props = defineProps<{
@@ -319,6 +308,7 @@
(e: 'flipSprite', id: string, direction: 'horizontal' | 'vertical'): void;
(e: 'copySpriteToFrame', spriteId: string, targetLayerId: string, targetFrameIndex: number): void;
(e: 'replaceSprite', id: string, file: File): void;
+ (e: 'removeSprite', id: string): void;
(e: 'openPixelEditor', layerId: string, frameIndex: number): void;
}>();
@@ -368,11 +358,6 @@
const showAllSprites = ref(false);
const isDragOver = ref(false);
- const showContextMenu = ref(false);
- const contextMenuX = ref(0);
- const contextMenuY = ref(0);
- const contextMenuSpriteId = ref
(null);
- const contextMenuLayerId = ref(null);
const fileInput = ref(null);
const replacingSpriteId = ref(null);
@@ -654,40 +639,34 @@
);
watch(currentFrameIndex, () => {});
+ /* Context Menu */
+ const { isOpen: isContextMenuOpen, position: contextMenuPosition, contextData: contextMenuData, open: openContextMenuBase, close: closeContextMenu } = useContextMenu<{ spriteId: string; layerId: string }>();
+
const openContextMenu = (event: MouseEvent, sprite: Sprite, layerId: string) => {
- event.preventDefault();
- contextMenuSpriteId.value = sprite.id;
- contextMenuLayerId.value = layerId;
- contextMenuX.value = event.clientX;
- contextMenuY.value = event.clientY;
- showContextMenu.value = true;
+ openContextMenuBase(event, { spriteId: sprite.id, layerId });
};
- const hideContextMenu = () => {
- showContextMenu.value = false;
- contextMenuSpriteId.value = null;
- contextMenuLayerId.value = null;
- };
+ const contextMenuSpriteId = computed(() => contextMenuData.value?.spriteId || null);
+ const contextMenuLayerId = computed(() => contextMenuData.value?.layerId || null);
const rotateSpriteInMenu = (angle: number) => {
if (contextMenuSpriteId.value) {
emit('rotateSprite', contextMenuSpriteId.value, angle);
}
- hideContextMenu();
+ // Context menu closes automatically via component emit
};
const flipSpriteInMenu = (direction: 'horizontal' | 'vertical') => {
if (contextMenuSpriteId.value) {
emit('flipSprite', contextMenuSpriteId.value, direction);
}
- hideContextMenu();
};
const openCopyToFrameModal = () => {
if (contextMenuSpriteId.value) {
copyTargetLayerId.value = contextMenuLayerId.value || props.activeLayerId;
showCopyToFrameModal.value = true;
- showContextMenu.value = false;
+ closeContextMenu();
}
};
@@ -699,15 +678,15 @@
if (contextMenuSpriteId.value) {
emit('copySpriteToFrame', contextMenuSpriteId.value, targetLayerId, targetFrameIndex);
closeCopyToFrameModal();
- contextMenuSpriteId.value = null;
}
+ // We don't null contextMenuSpriteId here since it's computed now, but closing the menu does the trick effectively for the user flow.
};
const replaceSprite = () => {
if (contextMenuSpriteId.value && fileInput.value) {
replacingSpriteId.value = contextMenuSpriteId.value;
fileInput.value.click();
- hideContextMenu();
+ closeContextMenu();
}
};
@@ -716,8 +695,13 @@
if (input.files && input.files.length > 0) {
const file = input.files[0];
- if (file.type.startsWith('image/') && replacingSpriteId.value) {
- emit('replaceSprite', replacingSpriteId.value, file);
+ if (file.type.startsWith('image/')) {
+ if (replacingSpriteId.value) {
+ emit('replaceSprite', replacingSpriteId.value, file);
+ } else {
+ // Add sprite case - use dropSprite emit as it handles adding files to layer/frame
+ emit('dropSprite', props.activeLayerId, currentFrameIndex.value, [file]);
+ }
}
}
replacingSpriteId.value = null;
@@ -727,7 +711,21 @@
const openPixelEditor = () => {
if (contextMenuSpriteId.value && contextMenuLayerId.value) {
emit('openPixelEditor', contextMenuLayerId.value, currentFrameIndex.value);
- hideContextMenu();
+ closeContextMenu();
+ }
+ };
+
+ const addSprite = () => {
+ if (fileInput.value) {
+ replacingSpriteId.value = null;
+ fileInput.value.click();
+ closeContextMenu();
+ }
+ };
+ const removeSprite = () => {
+ if (contextMenuSpriteId.value) {
+ (emit as any)('removeSprite', contextMenuSpriteId.value);
+ closeContextMenu();
}
};
diff --git a/src/components/shared/SpriteContextMenu.vue b/src/components/shared/SpriteContextMenu.vue
new file mode 100644
index 0000000..1705e7d
--- /dev/null
+++ b/src/components/shared/SpriteContextMenu.vue
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/composables/useContextMenu.ts b/src/composables/useContextMenu.ts
new file mode 100644
index 0000000..7f9fb44
--- /dev/null
+++ b/src/composables/useContextMenu.ts
@@ -0,0 +1,32 @@
+import { ref } from 'vue';
+
+export interface ContextMenuPosition {
+ x: number;
+ y: number;
+}
+
+export function useContextMenu() {
+ const isOpen = ref(false);
+ const position = ref({ x: 0, y: 0 });
+ const contextData = ref(null);
+
+ const open = (event: MouseEvent, data: T) => {
+ event.preventDefault();
+ isOpen.value = true;
+ position.value = { x: event.clientX, y: event.clientY };
+ contextData.value = data;
+ };
+
+ const close = () => {
+ isOpen.value = false;
+ contextData.value = null;
+ };
+
+ return {
+ isOpen,
+ position,
+ contextData,
+ open,
+ close,
+ };
+}
diff --git a/src/views/EditorView.vue b/src/views/EditorView.vue
index fd01b77..ef02973 100644
--- a/src/views/EditorView.vue
+++ b/src/views/EditorView.vue
@@ -322,6 +322,7 @@
@rotate-sprite="rotateSprite"
@flip-sprite="flipSprite"
@copy-sprite-to-frame="copySpriteToFrame"
+ @remove-sprite="removeSprite"
@replace-sprite="replaceSprite"
@open-pixel-editor="openPixelEditor"
/>