diff --git a/src/components/Menu/MenuBar/BaseMenuBar.vue b/src/components/Menu/MenuBar/BaseMenuBar.vue index 3b7b3ea75f..26570a57d5 100644 --- a/src/components/Menu/MenuBar/BaseMenuBar.vue +++ b/src/components/Menu/MenuBar/BaseMenuBar.vue @@ -40,6 +40,8 @@ const props = defineProps<{ /** 「ファイル」メニューのサブメニュー */ fileSubMenuData: MenuItemData[]; + /** 「編集」メニューのサブメニュー */ + editSubMenuData: MenuItemData[]; /** エディタの種類 */ editor: "talk" | "song"; }>(); @@ -90,6 +92,8 @@ const titleText = computed( ? ` - Port: ${defaultEngineAltPortTo.value}` : "") ); +const canUndo = computed(() => store.getters.CAN_UNDO(props.editor)); +const canRedo = computed(() => store.getters.CAN_REDO(props.editor)); // FIXME: App.vue内に移動する watch(titleText, (newTitle) => { @@ -327,6 +331,39 @@ const menudata = computed(() => [ }, ], }, + { + type: "root", + label: "編集", + onClick: () => { + closeAllDialog(); + }, + disableWhenUiLocked: false, + subMenu: [ + { + type: "button", + label: "元に戻す", + onClick: async () => { + if (!uiLocked.value) { + await store.dispatch("UNDO", { editor: props.editor }); + } + }, + disabled: !canUndo.value, + disableWhenUiLocked: true, + }, + { + type: "button", + label: "やり直す", + onClick: async () => { + if (!uiLocked.value) { + await store.dispatch("REDO", { editor: props.editor }); + } + }, + disabled: !canRedo.value, + disableWhenUiLocked: true, + }, + ...props.editSubMenuData, + ], + }, { type: "root", label: "エンジン", diff --git a/src/components/Sing/MenuBar.vue b/src/components/Sing/MenuBar.vue index 578b08cabf..2f7b13a797 100644 --- a/src/components/Sing/MenuBar.vue +++ b/src/components/Sing/MenuBar.vue @@ -1,5 +1,9 @@ diff --git a/src/components/Sing/ScoreSequencer.vue b/src/components/Sing/ScoreSequencer.vue index a929d78aab..9a8605166d 100644 --- a/src/components/Sing/ScoreSequencer.vue +++ b/src/components/Sing/ScoreSequencer.vue @@ -28,6 +28,7 @@ @mouseleave="onMouseLeave" @wheel="onWheel" @scroll="onScroll" + @contextmenu.prevent > @@ -208,6 +209,7 @@ }" @input="setZoomY" /> + @@ -221,6 +223,10 @@ import { onDeactivated, } from "vue"; import { v4 as uuidv4 } from "uuid"; +import ContextMenu, { + ContextMenuItemData, +} from "@/components/Menu/ContextMenu.vue"; +import { isMac } from "@/type/preload"; import { useStore } from "@/store"; import { Note } from "@/store/type"; import { @@ -251,6 +257,7 @@ import SequencerPhraseIndicator from "@/components/Sing/SequencerPhraseIndicator import CharacterPortrait from "@/components/Sing/CharacterPortrait.vue"; import SequencerPitch from "@/components/Sing/SequencerPitch.vue"; import { isOnCommandOrCtrlKeyDown } from "@/store/utility"; +import { useHotkeyManager } from "@/plugins/hotkeyPlugin"; import { useShiftKey } from "@/composables/useModifierKey"; type PreviewMode = "ADD" | "MOVE" | "RESIZE_RIGHT" | "RESIZE_LEFT"; @@ -276,6 +283,9 @@ const timeSignatures = computed(() => state.timeSignatures); // ノート const notes = computed(() => store.getters.SELECTED_TRACK.notes); +const isNoteSelected = computed(() => { + return state.selectedNoteIds.size > 0; +}); const unselectedNotes = computed(() => { const selectedNoteIds = state.selectedNoteIds; return notes.value.filter((value) => !selectedNoteIds.has(value.id)); @@ -744,6 +754,15 @@ const onMouseDown = (event: MouseEvent) => { if (!isSelfEventTarget(event)) { return; } + + // macOSの場合、Ctrl+クリックが右クリックのため、その場合はノートを追加しない + if (isMac && event.ctrlKey && event.button === 0) { + return; + } + + // TODO: メニューが表示されている場合はメニュー非表示のみ行いたい + + // 選択中のノートが無い場合、プレビューを開始しノートIDをリセット if (event.button === 0) { if (event.shiftKey) { isRectSelecting.value = true; @@ -1146,6 +1165,136 @@ onDeactivated(() => { document.removeEventListener("keydown", handleKeydown); }); + +// コンテキストメニュー +// TODO: 分割する +const { registerHotkeyWithCleanup } = useHotkeyManager(); + +registerHotkeyWithCleanup({ + editor: "song", + name: "コピー", + callback: () => { + if (nowPreviewing.value) { + return; + } + if (state.selectedNoteIds.size === 0) { + return; + } + store.dispatch("COPY_NOTES_TO_CLIPBOARD"); + }, +}); + +registerHotkeyWithCleanup({ + editor: "song", + name: "切り取り", + callback: () => { + if (nowPreviewing.value) { + return; + } + if (state.selectedNoteIds.size === 0) { + return; + } + store.dispatch("COMMAND_CUT_NOTES_TO_CLIPBOARD"); + }, +}); + +registerHotkeyWithCleanup({ + editor: "song", + name: "貼り付け", + callback: () => { + if (nowPreviewing.value) { + return; + } + store.dispatch("COMMAND_PASTE_NOTES_FROM_CLIPBOARD"); + }, +}); + +registerHotkeyWithCleanup({ + editor: "song", + name: "すべて選択", + callback: () => { + if (nowPreviewing.value) { + return; + } + store.dispatch("SELECT_ALL_NOTES"); + }, +}); + +const contextMenu = ref>(); + +const contextMenuData = ref([ + { + type: "button", + label: "コピー", + onClick: async () => { + contextMenu.value?.hide(); + await store.dispatch("COPY_NOTES_TO_CLIPBOARD"); + }, + disabled: !isNoteSelected.value, + disableWhenUiLocked: true, + }, + { + type: "button", + label: "切り取り", + onClick: async () => { + contextMenu.value?.hide(); + await store.dispatch("COMMAND_CUT_NOTES_TO_CLIPBOARD"); + }, + disabled: !isNoteSelected.value, + disableWhenUiLocked: true, + }, + { + type: "button", + label: "貼り付け", + onClick: async () => { + contextMenu.value?.hide(); + await store.dispatch("COMMAND_PASTE_NOTES_FROM_CLIPBOARD"); + }, + disableWhenUiLocked: true, + }, + { type: "separator" }, + { + type: "button", + label: "すべて選択", + onClick: async () => { + contextMenu.value?.hide(); + await store.dispatch("SELECT_ALL_NOTES"); + }, + disableWhenUiLocked: true, + }, + { + type: "button", + label: "選択解除", + onClick: async () => { + contextMenu.value?.hide(); + await store.dispatch("DESELECT_ALL_NOTES"); + }, + disabled: !isNoteSelected.value, + disableWhenUiLocked: true, + }, + { type: "separator" }, + { + type: "button", + label: "クオンタイズ", + onClick: async () => { + contextMenu.value?.hide(); + await store.dispatch("COMMAND_QUANTIZE_SELECTED_NOTES"); + }, + disabled: !isNoteSelected.value, + disableWhenUiLocked: true, + }, + { type: "separator" }, + { + type: "button", + label: "削除", + onClick: async () => { + contextMenu.value?.hide(); + await store.dispatch("COMMAND_REMOVE_SELECTED_NOTES"); + }, + disabled: !isNoteSelected.value, + disableWhenUiLocked: true, + }, +]);