Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#1877 ソング: 編集メニューの追加とノートのコピー&ペーストの実装 #1903

Merged
merged 19 commits into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/components/Menu/MenuBar/BaseMenuBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ const props =
defineProps<{
/** 「ファイル」メニューのサブメニュー */
fileSubMenuData: MenuItemData[];
/** 「編集」メニューのサブメニュー */
editSubMenuData: MenuItemData[];
/** エディタの種類 */
editor: "talk" | "song";
}>();
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -327,6 +331,39 @@ const menudata = computed<MenuItemData[]>(() => [
},
],
},
{
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: "エンジン",
Expand Down
69 changes: 68 additions & 1 deletion src/components/Sing/MenuBar.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<template>
<BaseMenuBar editor="song" :file-sub-menu-data="fileSubMenuData" />
<BaseMenuBar
editor="song"
:file-sub-menu-data="fileSubMenuData"
:edit-sub-menu-data="editSubMenuData"
/>
</template>

<script setup lang="ts">
Expand All @@ -10,6 +14,7 @@ import { MenuItemData } from "@/components/Menu/type";

const store = useStore();
const uiLocked = computed(() => store.getters.UI_LOCKED);
const isNotesSelected = computed(() => store.state.selectedNoteIds.size > 0);

const importMidiFile = async () => {
if (uiLocked.value) return;
Expand Down Expand Up @@ -53,4 +58,66 @@ const fileSubMenuData: MenuItemData[] = [
disableWhenUiLocked: true,
},
];

const editSubMenuData: MenuItemData[] = [
{ type: "separator" },
{
type: "button",
label: "コピー",
onClick: () => {
if (uiLocked.value) return;
store.dispatch("COPY_NOTES_TO_CLIPBOARD");
},
disableWhenUiLocked: true,
disabled: !isNotesSelected.value,
},
{
type: "button",
label: "切り取り",
onClick: () => {
if (uiLocked.value) return;
store.dispatch("COMMAND_CUT_NOTES_TO_CLIPBOARD");
},
disableWhenUiLocked: true,
disabled: !isNotesSelected.value,
},
{
type: "button",
label: "貼り付け",
onClick: () => {
if (uiLocked.value) return;
store.dispatch("COMMAND_PASTE_NOTES_FROM_CLIPBOARD");
},
disableWhenUiLocked: true,
},
{ type: "separator" },
{
type: "button",
label: "すべて選択",
onClick: () => {
if (uiLocked.value) return;
store.dispatch("SELECT_ALL_NOTES");
},
disableWhenUiLocked: true,
},
{
type: "button",
label: "選択解除",
onClick: () => {
if (uiLocked.value) return;
store.dispatch("DESELECT_ALL_NOTES");
},
disableWhenUiLocked: true,
},
{ type: "separator" },
{
type: "button",
label: "クオンタイズ",
onClick: () => {
if (uiLocked.value) return;
store.dispatch("COMMAND_QUANTIZE_SELECTED_NOTES");
},
disableWhenUiLocked: true,
},
];
</script>
149 changes: 149 additions & 0 deletions src/components/Sing/ScoreSequencer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
@mouseleave="onMouseLeave"
@wheel="onWheel"
@scroll="onScroll"
@contextmenu.prevent
>
<!-- キャラクター全身 -->
<CharacterPortrait />
Expand Down Expand Up @@ -208,6 +209,7 @@
}"
@input="setZoomY"
/>
<ContextMenu ref="contextMenu" :menudata="contextMenuData" />
</div>
</template>

Expand All @@ -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 {
Expand Down Expand Up @@ -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";
Expand All @@ -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));
Expand Down Expand Up @@ -744,6 +754,15 @@ const onMouseDown = (event: MouseEvent) => {
if (!isSelfEventTarget(event)) {
return;
}

// macOSの場合、Ctrl+クリックが右クリックのため、その場合はノートを追加しない
if (isMac && event.ctrlKey && event.button === 0) {
return;
}

// TODO: メニューが表示されている場合はメニュー非表示のみ行いたい
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ちょっと考えたのですが同意見です!

よくある挙動だとコンテキストメニュー開いた後のクリックも普通のクリックと同じ扱いにする(ボタンを押すなど)のですが、どこをクリックしても何かが発動するシーケンサーの場合は話は別なのかなぁと思いました。


// 選択中のノートが無い場合、プレビューを開始しノートIDをリセット
if (event.button === 0) {
if (event.shiftKey) {
isRectSelecting.value = true;
Expand Down Expand Up @@ -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<InstanceType<typeof ContextMenu>>();

const contextMenuData = ref<ContextMenuItemData[]>([
{
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: "クオンタイズ",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ここの名称、初心者にはかなり難しそうですね。。。。
まあこれを必要とする人はわりと玄人だと思うので大丈夫そうではありますが・・・

ちょっと初心者向けのワードを考えてみました。
やることは「ノートをリズムに合わせる」ですが、それだと長いので、「ノート整列」とか「整列」とか「タイミング合わせ」とか「タイミング調整」とかでしょうか。
長いけど「タイミング調整(クオンタイズ)」とか・・・・?

うーん、とりあえずクオンタイズのままで!

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,
},
]);
</script>

<style scoped lang="scss">
Expand Down
39 changes: 35 additions & 4 deletions src/components/Sing/SequencerNote.vue
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,9 @@ import {
tickToBaseX,
noteNumberToBaseY,
} from "@/sing/viewHelper";
import ContextMenu from "@/components/Menu/ContextMenu.vue";
import { MenuItemButton } from "@/components/Menu/type";
import ContextMenu, {
ContextMenuItemData,
} from "@/components/Menu/ContextMenu.vue";

type NoteState = "NORMAL" | "SELECTED";

Expand Down Expand Up @@ -163,13 +164,43 @@ const showPitch = computed(() => {
return state.experimentalSetting.showPitchInSongEditor;
});
const contextMenu = ref<InstanceType<typeof ContextMenu>>();
const contextMenuData = ref<[MenuItemButton]>([
const contextMenuData = ref<ContextMenuItemData[]>([
{
type: "button",
label: "コピー",
onClick: async () => {
contextMenu.value?.hide();
await store.dispatch("COPY_NOTES_TO_CLIPBOARD");
},
disableWhenUiLocked: true,
},
{
type: "button",
label: "切り取り",
onClick: async () => {
contextMenu.value?.hide();
await store.dispatch("COMMAND_CUT_NOTES_TO_CLIPBOARD");
},
disableWhenUiLocked: true,
},
{ type: "separator" },
{
type: "button",
label: "クオンタイズ",
disabled: !props.isSelected,
onClick: async () => {
contextMenu.value?.hide();
await store.dispatch("COMMAND_QUANTIZE_SELECTED_NOTES");
},
disableWhenUiLocked: true,
},
{ type: "separator" },
{
type: "button",
label: "削除",
onClick: async () => {
contextMenu.value?.hide();
store.dispatch("COMMAND_REMOVE_SELECTED_NOTES");
await store.dispatch("COMMAND_REMOVE_SELECTED_NOTES");
},
disableWhenUiLocked: true,
},
Expand Down
Loading
Loading