From b0533199def61a833b43fce4ae0b4de96108d9fd Mon Sep 17 00:00:00 2001 From: sevenc-nanashi Date: Thu, 17 Oct 2024 20:38:33 +0900 Subject: [PATCH 001/107] =?UTF-8?q?Add:=20=E5=8F=B3=E3=82=AF=E3=83=AA?= =?UTF-8?q?=E3=83=83=E3=82=AF=E3=83=A1=E3=83=8B=E3=83=A5=E3=83=BC=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Sing/SequencerRuler.vue | 66 +++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 7 deletions(-) diff --git a/src/components/Sing/SequencerRuler.vue b/src/components/Sing/SequencerRuler.vue index 9ddae01849..3b4d3cdc74 100644 --- a/src/components/Sing/SequencerRuler.vue +++ b/src/components/Sing/SequencerRuler.vue @@ -1,5 +1,15 @@ + + + + diff --git a/src/components/Sing/SequencerRuler.vue b/src/components/Sing/SequencerRuler.vue index 3b4d3cdc74..b99fca1f56 100644 --- a/src/components/Sing/SequencerRuler.vue +++ b/src/components/Sing/SequencerRuler.vue @@ -155,11 +155,11 @@ const getTickFromMouseEvent = (event: MouseEvent) => { return baseXToTick(baseX, tpqn.value); }; -const onClick = (event: MouseEvent) => { - void store.dispatch("DESELECT_ALL_NOTES"); +const onClick = async (event: MouseEvent) => { + await store.dispatch("DESELECT_ALL_NOTES"); const ticks = getTickFromMouseEvent(event); - void store.dispatch("SET_PLAYHEAD_POSITION", { position: ticks }); + await store.dispatch("SET_PLAYHEAD_POSITION", { position: ticks }); }; const sequencerRuler = ref(null); @@ -221,14 +221,18 @@ const contextMenuHeader = computed(() => { return `${currentMeasure}小節目`; }); -const onContextMenu = (event: MouseEvent) => { +const onContextMenu = async (event: MouseEvent) => { + await onClick(event); contextMenuTick.value = getTickFromMouseEvent(event); }; const contextMenudata: ContextMenuItemData[] = [ { type: "button", label: "BPM変化を挿入", - onClick: () => {}, + onClick: async () => { + const { promise, resolve } = Promise.withResolvers(); + + }, disableWhenUiLocked: true, }, { From 98bfb093ff96a696ce2355da0568f84207b59ebf Mon Sep 17 00:00:00 2001 From: sevenc-nanashi Date: Fri, 18 Oct 2024 19:45:02 +0900 Subject: [PATCH 003/107] =?UTF-8?q?Add:=20=E3=81=82=E3=82=8B=E7=A8=8B?= =?UTF-8?q?=E5=BA=A6Story=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...empoOrTimeSignatureChangeDialog.stories.ts | 154 ++++++++++++++++- .../TempoOrTimeSignatureChangeDialog.vue | 155 ++++++++++++++++-- src/components/Sing/ToolBar/ToolBar.vue | 12 +- src/sing/domain.ts | 2 +- 4 files changed, 299 insertions(+), 24 deletions(-) diff --git a/src/components/Dialog/TempoOrTimeSignatureChangeDialog.stories.ts b/src/components/Dialog/TempoOrTimeSignatureChangeDialog.stories.ts index 60729ca2fd..39c302790c 100644 --- a/src/components/Dialog/TempoOrTimeSignatureChangeDialog.stories.ts +++ b/src/components/Dialog/TempoOrTimeSignatureChangeDialog.stories.ts @@ -19,16 +19,161 @@ const meta: Meta = { export default meta; type Story = StoryObj; -export const Opened: Story = { - name: "開いている", +export const CreateOpened: Story = { + name: "開いている:追加", args: { modelValue: true, + timeSignatureChange: undefined, + tempoChange: undefined, + }, +}; +export const ChangeOpened: Story = { + name: "開いている:変更", + args: { + modelValue: true, + timeSignatureChange: { + beats: 4, + beatType: 4, + }, + tempoChange: { + bpm: 120, + }, + }, +}; + +export const CannotCloseWithoutCreate: Story = { + name: "作成:追加されない状態だと閉じられない", + args: { ...CreateOpened.args }, + play: async () => { + const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う + + const button = canvas.getByRole("button", { name: /追加する/ }); + await expect(button).toBeDisabled(); + }, +}; +export const CannotCloseWIthoutChange: Story = { + name: "変更:変更されない状態だと閉じられない", + args: { ...ChangeOpened.args }, + play: async () => { + const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う + + const button = canvas.getByRole("button", { name: /変更する/ }); + await expect(button).toBeDisabled(); + }, +}; +export const SayChangeIfExist: Story = { + name: "変更:どちらかが変更されていれば閉じられる", + args: { ...ChangeOpened.args }, + play: async (context) => { + const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う + + const changeInput = async (text: string, value: string) => { + const input = canvas.getByLabelText(text); + await userEvent.clear(input); + await userEvent.type(input, value); + }; + + const okButton = canvas.getByRole("button", { name: /変更する/ }); + await context.step("テンポを変更する", async () => { + await changeInput("テンポ", "100"); + + await expect(okButton).toBeEnabled(); + + await changeInput("テンポ", "120"); + + await expect(okButton).toBeDisabled(); + }); + + await context.step("拍子を変更する", async () => { + await changeInput("拍子の分子", "3"); + + await expect(okButton).toBeEnabled(); + + await changeInput("拍子の分子", "4"); + + await expect(okButton).toBeDisabled(); + }); + + await context.step("テンポの存在を変更する", async () => { + const tempoToggle = canvas.getByRole("switch", { + name: /テンポ変更の有無/, + }); + await userEvent.click(tempoToggle); + + await expect(okButton).toBeEnabled(); + + await userEvent.click(tempoToggle); + + await expect(okButton).toBeDisabled(); + }); + + await context.step("拍子の存在を変更する", async () => { + const timeSignatureToggle = canvas.getByRole("switch", { + name: /拍子変更の有無/, + }); + await userEvent.click(timeSignatureToggle); + + await expect(okButton).toBeEnabled(); + + await userEvent.click(timeSignatureToggle); + + await expect(okButton).toBeDisabled(); + }); + }, +}; + +export const ClickOk: Story = { + name: "OKボタンを押す:作成", + args: { ...CreateOpened.args }, + play: async ({ args }) => { + const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う + + const bpmToggle = canvas.getByRole("switch", { name: /テンポ変更の有無/ }); + await userEvent.click(bpmToggle); + const bpmInput = canvas.getByLabelText("テンポ"); + await userEvent.clear(bpmInput); + await userEvent.type(bpmInput, "100"); + + const button = canvas.getByRole("button", { name: /追加する/ }); + await userEvent.click(button); + + await expect(args["onOk"]).toBeCalledWith({ + timeSignatureChange: undefined, + tempoChange: { + bpm: 100, + }, + }); + await expect(args["onUpdate:modelValue"]).toBeCalledWith(false); + }, +}; + +export const ClickDelete: Story = { + name: "OKボタンを押す:削除", + args: { ...ChangeOpened.args }, + play: async ({ args }) => { + const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う + + const bpmToggle = canvas.getByRole("switch", { name: /テンポ変更の有無/ }); + await userEvent.click(bpmToggle); + const timeSignatureToggle = canvas.getByRole("switch", { + name: /拍子変更の有無/, + }); + await userEvent.click(timeSignatureToggle); + + const button = canvas.getByRole("button", { name: /削除する/ }); + await userEvent.click(button); + + await expect(args["onOk"]).toBeCalledWith({ + timeSignatureChange: undefined, + tempoChange: undefined, + }); + await expect(args["onUpdate:modelValue"]).toBeCalledWith(false); }, }; export const CancelClose: Story = { name: "キャンセルボタンを押す", - args: { ...Opened.args }, + args: { ...ChangeOpened.args }, play: async ({ args }) => { const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う @@ -43,4 +188,7 @@ export const CancelClose: Story = { export const Closed: Story = { name: "閉じている", + args: { + modelValue: false, + }, }; diff --git a/src/components/Dialog/TempoOrTimeSignatureChangeDialog.vue b/src/components/Dialog/TempoOrTimeSignatureChangeDialog.vue index 0c9c8e70f4..84e41e32c1 100644 --- a/src/components/Dialog/TempoOrTimeSignatureChangeDialog.vue +++ b/src/components/Dialog/TempoOrTimeSignatureChangeDialog.vue @@ -2,7 +2,9 @@ -
テンポ・拍子変更
+
+ {{ changeExisted ? "テンポ・拍子の編集" : "テンポ・拍子の追加" }} +
@@ -11,7 +13,53 @@
テンポ
- (テンポ変更) + + +
+ +
拍子
+ + +
+ / +
+ +
@@ -21,7 +69,6 @@ diff --git a/src/components/Dialog/TempoOrTimeSignatureChangeDialog/CommonDialog.vue b/src/components/Dialog/TempoOrTimeSignatureChangeDialog/CommonDialog.vue new file mode 100644 index 0000000000..dd4e06fd98 --- /dev/null +++ b/src/components/Dialog/TempoOrTimeSignatureChangeDialog/CommonDialog.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/src/components/Dialog/TempoOrTimeSignatureChangeDialog/TempoChangeDialog.stories.ts b/src/components/Dialog/TempoOrTimeSignatureChangeDialog/TempoChangeDialog.stories.ts new file mode 100644 index 0000000000..ff7d4b9a58 --- /dev/null +++ b/src/components/Dialog/TempoOrTimeSignatureChangeDialog/TempoChangeDialog.stories.ts @@ -0,0 +1,101 @@ +import { userEvent, within, expect, fn } from "@storybook/test"; +import { Meta, StoryObj } from "@storybook/vue3"; + +import TempoChangeDialog from "./TempoChangeDialog.vue"; + +const meta: Meta = { + component: TempoChangeDialog, + args: { + modelValue: true, + tempoChange: undefined, + mode: "add", + + onOk: fn(), + onHide: fn(), + }, + tags: ["!autodocs"], // ダイアログ系はautodocsのプレビューが正しく表示されないので無効化 +}; + +export default meta; +type Story = StoryObj; + +export const CreateOpened: Story = { + name: "開いている:追加", + args: { + modelValue: true, + mode: "add", + }, +}; +export const ChangeOpened: Story = { + name: "開いている:変更", + args: { + modelValue: true, + tempoChange: { + bpm: 120, + }, + mode: "edit", + }, +}; + +export const ClickOk: Story = { + name: "OKボタンを押す:追加", + args: { ...CreateOpened.args }, + play: async ({ args }) => { + const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う + + const input = canvas.getByLabelText("BPM"); + await userEvent.clear(input); + await userEvent.type(input, "100"); + + const button = canvas.getByRole("button", { name: /追加する/ }); + await userEvent.click(button); + + await expect(args["onOk"]).toBeCalledWith({ + tempoChange: { + bpm: 100, + }, + }); + }, +}; + +export const ClickDelete: Story = { + name: "OKボタンを押す:編集", + args: { ...ChangeOpened.args }, + play: async ({ args }) => { + const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う + + const input = canvas.getByLabelText("BPM"); + await userEvent.clear(input); + await userEvent.type(input, "100"); + + const button = canvas.getByRole("button", { name: /変更する/ }); + await userEvent.click(button); + + await expect(args["onOk"]).toBeCalledWith({ + tempoChange: { + bpm: 100, + }, + }); + }, +}; + +export const CancelClose: Story = { + name: "キャンセルボタンを押す", + args: { ...ChangeOpened.args }, + play: async ({ args }) => { + const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う + + const button = canvas.getByRole("button", { name: /キャンセル/ }); + await userEvent.click(button); + + await expect(args["onOk"]).not.toBeCalled(); + }, +}; + +export const Closed: Story = { + name: "閉じている", + tags: ["skip-screenshot"], + args: { + modelValue: false, + }, +}; diff --git a/src/components/Dialog/TempoOrTimeSignatureChangeDialog/TempoChangeDialog.vue b/src/components/Dialog/TempoOrTimeSignatureChangeDialog/TempoChangeDialog.vue new file mode 100644 index 0000000000..4a39750b02 --- /dev/null +++ b/src/components/Dialog/TempoOrTimeSignatureChangeDialog/TempoChangeDialog.vue @@ -0,0 +1,59 @@ + + + + + diff --git a/src/components/Dialog/TempoOrTimeSignatureChangeDialog/TimeSignatureChangeDialog.stories.ts b/src/components/Dialog/TempoOrTimeSignatureChangeDialog/TimeSignatureChangeDialog.stories.ts new file mode 100644 index 0000000000..01f3f20789 --- /dev/null +++ b/src/components/Dialog/TempoOrTimeSignatureChangeDialog/TimeSignatureChangeDialog.stories.ts @@ -0,0 +1,119 @@ +import { userEvent, within, expect, fn, queries } from "@storybook/test"; +import { Meta, StoryObj } from "@storybook/vue3"; +// eslint-disable-next-line storybook/use-storybook-testing-library -- BoundFunctionsは@testing-library/domの型定義にしかない +import type { BoundFunctions } from "@testing-library/dom"; + +import TimeSignatureChangeDialog from "./TimeSignatureChangeDialog.vue"; + +const meta: Meta = { + component: TimeSignatureChangeDialog, + args: { + modelValue: true, + timeSignatureChange: undefined, + mode: "add", + + onOk: fn(), + onHide: fn(), + }, + tags: ["!autodocs"], // ダイアログ系はautodocsのプレビューが正しく表示されないので無効化 +}; +const findOption = (canvas: BoundFunctions, text: string) => { + const maybeElement = canvas + .getAllByRole("option") + .find((el) => el.textContent === text); + if (!maybeElement) throw new Error("Element not found"); + return maybeElement; +}; + +export default meta; +type Story = StoryObj; + +export const CreateOpened: Story = { + name: "開いている:追加", + args: { + modelValue: true, + mode: "add", + }, +}; +export const ChangeOpened: Story = { + name: "開いている:変更", + args: { + modelValue: true, + timeSignatureChange: { + beats: 4, + beatType: 4, + }, + mode: "edit", + }, +}; + +export const ClickOk: Story = { + name: "OKボタンを押す:追加", + args: { ...CreateOpened.args }, + play: async ({ args }) => { + const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う + + const selectRoot = canvas.getByLabelText("拍子の分子"); + await userEvent.click(selectRoot); + await new Promise((resolve) => setTimeout(resolve, 0)); // メニューが開くのを待つ + + const option = findOption(canvas, "3"); + await userEvent.click(option); + + const button = canvas.getByRole("button", { name: /追加する/ }); + await userEvent.click(button); + + await expect(args["onOk"]).toBeCalledWith({ + timeSignatureChange: { + beats: 3, + beatType: 4, + }, + }); + }, +}; + +export const ClickDelete: Story = { + name: "OKボタンを押す:編集", + args: { ...ChangeOpened.args }, + play: async ({ args }) => { + const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う + + const selectRoot = canvas.getByLabelText("拍子の分子"); + await userEvent.click(selectRoot); + await new Promise((resolve) => setTimeout(resolve, 0)); // メニューが開くのを待つ + + const option = findOption(canvas, "6"); + await userEvent.click(option); + + const button = canvas.getByRole("button", { name: /変更する/ }); + await userEvent.click(button); + + await expect(args["onOk"]).toBeCalledWith({ + timeSignatureChange: { + beats: 6, + beatType: 4, + }, + }); + }, +}; + +export const CancelClose: Story = { + name: "キャンセルボタンを押す", + args: { ...ChangeOpened.args }, + play: async ({ args }) => { + const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う + + const button = canvas.getByRole("button", { name: /キャンセル/ }); + await userEvent.click(button); + + await expect(args["onOk"]).not.toBeCalled(); + }, +}; + +export const Closed: Story = { + name: "閉じている", + tags: ["skip-screenshot"], + args: { + modelValue: false, + }, +}; diff --git a/src/components/Dialog/TempoOrTimeSignatureChangeDialog/TimeSignatureChangeDialog.vue b/src/components/Dialog/TempoOrTimeSignatureChangeDialog/TimeSignatureChangeDialog.vue new file mode 100644 index 0000000000..7c05864216 --- /dev/null +++ b/src/components/Dialog/TempoOrTimeSignatureChangeDialog/TimeSignatureChangeDialog.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/src/components/Sing/SequencerRuler/Presentation.vue b/src/components/Sing/SequencerRuler/Presentation.vue index 6865330392..074eb76282 100644 --- a/src/components/Sing/SequencerRuler/Presentation.vue +++ b/src/components/Sing/SequencerRuler/Presentation.vue @@ -117,6 +117,8 @@ import { ExtractPropTypes, ComponentPublicInstance, useTemplateRef, + DefineComponent, + Component, } from "vue"; import { ComponentProps } from "vue-component-type-helpers"; import { Dialog } from "quasar"; @@ -132,9 +134,8 @@ import ContextMenu, { ContextMenuItemData, } from "@/components/Menu/ContextMenu.vue"; import { UnreachableError } from "@/type/utility"; -import TempoOrTimeSignatureChangeDialog, { - TempoOrTimeSignatureChangeDialogResult, -} from "@/components/Dialog/TempoOrTimeSignatureChangeDialog.vue"; +import TempoChangeDialog from "@/components/Dialog/TempoOrTimeSignatureChangeDialog/TempoChangeDialog.vue"; +import TimeSignatureChangeDialog from "@/components/Dialog/TempoOrTimeSignatureChangeDialog/TimeSignatureChangeDialog.vue"; const props = defineProps<{ offset: number; @@ -399,25 +400,17 @@ const timeSignatureChangeExists = computed( () => currentTimeSignature.value.measureNumber === currentMeasure.value, ); -const showTempoOrTimeSignatureChangeDialog = async ( - componentProps: ExtractPropTypes, +const showDialog = async ( + component: T, + componentProps: ExtractPropTypes, ) => { - const { promise, resolve } = Promise.withResolvers< - TempoOrTimeSignatureChangeDialogResult | "cancelled" - >(); - - const lastTempo = props.tempos.findLast((tempo) => { - return tempo.position <= playheadTicks.value; - }); - if (!lastTempo) { - throw new UnreachableError("assert: At least one tempo exists."); - } + const { promise, resolve } = Promise.withResolvers(); Dialog.create({ - component: TempoOrTimeSignatureChangeDialog, + component, componentProps, }) - .onOk((result: TempoOrTimeSignatureChangeDialogResult) => { + .onOk((result: R) => { resolve(result); }) .onCancel(() => { @@ -429,48 +422,32 @@ const showTempoOrTimeSignatureChangeDialog = async ( return; } - if (result.tempoChange) { - emit("setTempo", { - ...result.tempoChange, - position: playheadTicks.value, - }); - } else if (!result.tempoChange && tempoChangeExists.value) { - emit("removeTempo", playheadTicks.value); - } - - if (result.timeSignatureChange) { - emit("setTimeSignature", { - ...result.timeSignatureChange, - measureNumber: currentMeasure.value, - }); - } else if (!result.timeSignatureChange && timeSignatureChangeExists.value) { - emit("removeTimeSignature", currentMeasure.value); - } + return result; }; -const contextMenudata = computed(() => { - const canDeleteTempo = !( - currentTempo.value.position === 0 && tempoChangeExists.value - ); - const canDeleteTimeSignature = !( - currentTimeSignature.value.measureNumber === 1 && - timeSignatureChangeExists.value - ); - return [ +const contextMenudata = computed(() => + [ { type: "button", label: tempoChangeExists.value ? `BPM変化を編集` : "BPM変化を挿入", - onClick: () => { - void showTempoOrTimeSignatureChangeDialog({ + onClick: async () => { + const result = await showDialog< + typeof TempoChangeDialog, + { + tempoChange: Omit; + } + >(TempoChangeDialog, { timeSignatureChange: timeSignatureChangeExists.value ? currentTimeSignature.value : undefined, - tempoChange: { - bpm: currentTempo.value.bpm, - }, mode: tempoChangeExists.value ? "edit" : "add", - canDeleteTempo, - canDeleteTimeSignature, + }); + if (!result) { + return; + } + emit("setTempo", { + ...result.tempoChange, + position: playheadTicks.value, }); }, disableWhenUiLocked: true, @@ -480,22 +457,30 @@ const contextMenudata = computed(() => { label: timeSignatureChangeExists.value ? `拍子変化を編集` : "拍子変化を挿入", - onClick: () => { - void showTempoOrTimeSignatureChangeDialog({ - tempoChange: tempoChangeExists.value ? currentTempo.value : undefined, - timeSignatureChange: { - beats: currentTimeSignature.value.beats, - beatType: currentTimeSignature.value.beatType, - }, + onClick: async () => { + const result = await showDialog< + typeof TimeSignatureChangeDialog, + { + timeSignatureChange: Omit; + } + >(TimeSignatureChangeDialog, { + timeSignatureChange: timeSignatureChangeExists.value + ? currentTimeSignature.value + : undefined, mode: timeSignatureChangeExists.value ? "edit" : "add", - canDeleteTempo, - canDeleteTimeSignature, + }); + if (!result) { + return; + } + emit("setTimeSignature", { + ...result.timeSignatureChange, + measureNumber: currentMeasure.value, }); }, disableWhenUiLocked: true, }, ]; -}); +); diff --git a/src/components/Sing/KeypointDialog/TempoChangeDialog.stories.ts b/src/components/Sing/KeypointDialog/TempoChangeDialog.stories.ts new file mode 100644 index 0000000000..1027e0e929 --- /dev/null +++ b/src/components/Sing/KeypointDialog/TempoChangeDialog.stories.ts @@ -0,0 +1,101 @@ +import { userEvent, within, expect, fn } from "@storybook/test"; +import { Meta, StoryObj } from "@storybook/vue3"; + +import TempoChangeDialog from "./TempoChangeDialog.vue"; + +const meta: Meta = { + component: TempoChangeDialog, + args: { + modelValue: true, + tempoChange: undefined, + mode: "add", + + onOk: fn(), + onHide: fn(), + }, + tags: ["!autodocs"], // ダイアログ系はautodocsのプレビューが正しく表示されないので無効化 +}; + +export default meta; +type Story = StoryObj; + +export const CreateOpened: Story = { + name: "開いている:追加", + args: { + modelValue: true, + mode: "add", + }, +}; +export const ChangeOpened: Story = { + name: "開いている:変更", + args: { + modelValue: true, + tempoChange: { + bpm: 120, + }, + mode: "edit", + }, +}; + +export const ClickOk: Story = { + name: "OKボタンを押す:追加", + args: { ...CreateOpened.args }, + play: async ({ args }) => { + const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う + + const input = canvas.getByLabelText("テンポ"); + await userEvent.clear(input); + await userEvent.type(input, "100"); + + const button = canvas.getByRole("button", { name: /追加する/ }); + await userEvent.click(button); + + await expect(args["onOk"]).toBeCalledWith({ + tempoChange: { + bpm: 100, + }, + }); + }, +}; + +export const ClickDelete: Story = { + name: "OKボタンを押す:編集", + args: { ...ChangeOpened.args }, + play: async ({ args }) => { + const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う + + const input = canvas.getByLabelText("テンポ"); + await userEvent.clear(input); + await userEvent.type(input, "100"); + + const button = canvas.getByRole("button", { name: /変更する/ }); + await userEvent.click(button); + + await expect(args["onOk"]).toBeCalledWith({ + tempoChange: { + bpm: 100, + }, + }); + }, +}; + +export const CancelClose: Story = { + name: "キャンセルボタンを押す", + args: { ...ChangeOpened.args }, + play: async ({ args }) => { + const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う + + const button = canvas.getByRole("button", { name: /キャンセル/ }); + await userEvent.click(button); + + await expect(args["onOk"]).not.toBeCalled(); + }, +}; + +export const Closed: Story = { + name: "閉じている", + tags: ["skip-screenshot"], + args: { + modelValue: false, + }, +}; diff --git a/src/components/Sing/KeypointDialog/TempoChangeDialog.vue b/src/components/Sing/KeypointDialog/TempoChangeDialog.vue new file mode 100644 index 0000000000..7a7282c007 --- /dev/null +++ b/src/components/Sing/KeypointDialog/TempoChangeDialog.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/src/components/Sing/KeypointDialog/TimeSignatureChangeDialog.stories.ts b/src/components/Sing/KeypointDialog/TimeSignatureChangeDialog.stories.ts new file mode 100644 index 0000000000..e4441ff424 --- /dev/null +++ b/src/components/Sing/KeypointDialog/TimeSignatureChangeDialog.stories.ts @@ -0,0 +1,120 @@ +import { userEvent, within, expect, fn, queries } from "@storybook/test"; +import { Meta, StoryObj } from "@storybook/vue3"; +// eslint-disable-next-line storybook/use-storybook-testing-library -- BoundFunctionsは@testing-library/domの型定義にしかない +import type { BoundFunctions } from "@testing-library/dom"; + +import TimeSignatureChangeDialog from "./TimeSignatureChangeDialog.vue"; + +const meta: Meta = { + component: TimeSignatureChangeDialog, + args: { + modelValue: true, + timeSignatureChange: undefined, + mode: "add", + + onOk: fn(), + onHide: fn(), + }, + tags: ["!autodocs"], // ダイアログ系はautodocsのプレビューが正しく表示されないので無効化 +}; +const findOption = async ( + canvas: BoundFunctions, + text: string, +) => { + const maybeElement = await canvas + .findAllByRole("option") + .then((els) => els.find((el) => el.textContent === text)); + if (!maybeElement) throw new Error("Element not found"); + return maybeElement; +}; + +export default meta; +type Story = StoryObj; + +export const CreateOpened: Story = { + name: "開いている:追加", + args: { + modelValue: true, + mode: "add", + }, +}; +export const ChangeOpened: Story = { + name: "開いている:変更", + args: { + modelValue: true, + timeSignatureChange: { + beats: 4, + beatType: 4, + }, + mode: "edit", + }, +}; + +export const ClickOk: Story = { + name: "OKボタンを押す:追加", + args: { ...CreateOpened.args }, + play: async ({ args }) => { + const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う + + const selectRoot = canvas.getByLabelText("拍子の分子"); + await userEvent.click(selectRoot); + + const option = await findOption(canvas, "3"); + await userEvent.click(option); + + const button = canvas.getByRole("button", { name: /追加する/ }); + await userEvent.click(button); + + await expect(args["onOk"]).toBeCalledWith({ + timeSignatureChange: { + beats: 3, + beatType: 4, + }, + }); + }, +}; + +export const ClickDelete: Story = { + name: "OKボタンを押す:編集", + args: { ...ChangeOpened.args }, + play: async ({ args }) => { + const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う + + const selectRoot = canvas.getByLabelText("拍子の分子"); + await userEvent.click(selectRoot); + + const option = await findOption(canvas, "6"); + await userEvent.click(option); + + const button = canvas.getByRole("button", { name: /変更する/ }); + await userEvent.click(button); + + await expect(args["onOk"]).toBeCalledWith({ + timeSignatureChange: { + beats: 6, + beatType: 4, + }, + }); + }, +}; + +export const CancelClose: Story = { + name: "キャンセルボタンを押す", + args: { ...ChangeOpened.args }, + play: async ({ args }) => { + const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う + + const button = canvas.getByRole("button", { name: /キャンセル/ }); + await userEvent.click(button); + + await expect(args["onOk"]).not.toBeCalled(); + }, +}; + +export const Closed: Story = { + name: "閉じている", + tags: ["skip-screenshot"], + args: { + modelValue: false, + }, +}; diff --git a/src/components/Sing/KeypointDialog/TimeSignatureChangeDialog.vue b/src/components/Sing/KeypointDialog/TimeSignatureChangeDialog.vue new file mode 100644 index 0000000000..49cd1b8b89 --- /dev/null +++ b/src/components/Sing/KeypointDialog/TimeSignatureChangeDialog.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/src/components/Sing/SequencerRuler/Presentation.vue b/src/components/Sing/SequencerRuler/Presentation.vue index 3ea6eac281..4f68f19272 100644 --- a/src/components/Sing/SequencerRuler/Presentation.vue +++ b/src/components/Sing/SequencerRuler/Presentation.vue @@ -201,7 +201,7 @@ import ContextMenu, { import { UnreachableError } from "@/type/utility"; import TempoChangeDialog from "@/components/Dialog/TempoOrTimeSignatureChangeDialog/TempoChangeDialog.vue"; import TimeSignatureChangeDialog from "@/components/Dialog/TempoOrTimeSignatureChangeDialog/TimeSignatureChangeDialog.vue"; -import { FontSpecification, predictTextWidth } from "@/domain/dom"; +import { FontSpecification, predictTextWidth } from "@/helpers/textWidth"; import { createLogger } from "@/domain/frontend/log"; const props = defineProps<{ @@ -212,7 +212,6 @@ const props = defineProps<{ timeSignatures: TimeSignature[]; sequencerZoomX: number; snapType: number; - uiLocked: boolean; }>(); const playheadTicks = defineModel("playheadTicks", { @@ -220,7 +219,6 @@ const playheadTicks = defineModel("playheadTicks", { }); const emit = defineEmits<{ deselectAllNotes: []; - setTempo: [tempo: Tempo]; removeTempo: [position: number]; setTimeSignature: [timeSignature: TimeSignature]; diff --git a/src/domain/dom.ts b/src/domain/dom.ts index fcd883350f..8d4e512c6a 100644 --- a/src/domain/dom.ts +++ b/src/domain/dom.ts @@ -40,38 +40,3 @@ export function setThemeToCss(theme: ThemeConf) { export function setFontToCss(font: EditorFontType) { document.body.setAttribute("data-editor-font", font); } - -let textWidthTempCanvas: HTMLCanvasElement | undefined; -let textWidthTempContext: CanvasRenderingContext2D | undefined; - -export type FontSpecification = { - fontSize: number; - fontFamily: string; - fontWeight: string; -}; -const getTextWidthCacheKey = (text: string, font: FontSpecification) => - `${text}-${font.fontFamily}-${font.fontWeight}-${font.fontSize}`; - -const textWidthCache = new Map(); -/** - * 特定のフォントでの文字列の描画幅を取得する。 - * @see https://stackoverflow.com/a/21015393 - */ -export function predictTextWidth(text: string, font: FontSpecification) { - const key = getTextWidthCacheKey(text, font); - const maybeCached = textWidthCache.get(key); - if (maybeCached != undefined) { - return maybeCached; - } - if (!textWidthTempCanvas) { - textWidthTempCanvas = document.createElement("canvas"); - textWidthTempContext = textWidthTempCanvas.getContext("2d") ?? undefined; - } - if (!textWidthTempContext) { - throw new Error("Failed to get 2d context"); - } - textWidthTempContext.font = `${font.fontWeight} ${font.fontSize}px ${font.fontFamily}`; - const metrics = textWidthTempContext.measureText(text); - textWidthCache.set(key, metrics.width); - return metrics.width; -} diff --git a/src/domain/frontend/log.ts b/src/domain/frontend/log.ts index 7d34ac005b..4cb2aa8598 100644 --- a/src/domain/frontend/log.ts +++ b/src/domain/frontend/log.ts @@ -3,7 +3,7 @@ export function createLogger(scope: string) { const createInner = ( - method: "logInfo" | "logError" | "logWarn", + method: "logInfo" | "logWarn" | "logError", fallbackMethod: "info" | "warn" | "error", ) => (...args: unknown[]) => { @@ -16,7 +16,7 @@ export function createLogger(scope: string) { }; return { info: createInner("logInfo", "info"), - error: createInner("logError", "error"), warn: createInner("logWarn", "warn"), + error: createInner("logError", "error"), }; } diff --git a/src/helpers/textWidth.ts b/src/helpers/textWidth.ts new file mode 100644 index 0000000000..6e9945819f --- /dev/null +++ b/src/helpers/textWidth.ts @@ -0,0 +1,34 @@ +let textWidthTempCanvas: HTMLCanvasElement | undefined; +let textWidthTempContext: CanvasRenderingContext2D | undefined; + +export type FontSpecification = { + fontSize: number; + fontFamily: string; + fontWeight: string; +}; +const getTextWidthCacheKey = (text: string, font: FontSpecification) => + `${text}-${font.fontFamily}-${font.fontWeight}-${font.fontSize}`; + +const textWidthCache = new Map(); +/** + * 特定のフォントでの文字列の描画幅を取得する。 + * @see https://stackoverflow.com/a/21015393 + */ +export function predictTextWidth(text: string, font: FontSpecification) { + const key = getTextWidthCacheKey(text, font); + const maybeCached = textWidthCache.get(key); + if (maybeCached != undefined) { + return maybeCached; + } + if (!textWidthTempCanvas) { + textWidthTempCanvas = document.createElement("canvas"); + textWidthTempContext = textWidthTempCanvas.getContext("2d") ?? undefined; + } + if (!textWidthTempContext) { + throw new Error("Failed to get 2d context"); + } + textWidthTempContext.font = `${font.fontWeight} ${font.fontSize}px ${font.fontFamily}`; + const metrics = textWidthTempContext.measureText(text); + textWidthCache.set(key, metrics.width); + return metrics.width; +} From 6441c058746edf53888ff6a259d2110b7ea2d447 Mon Sep 17 00:00:00 2001 From: sevenc-nanashi Date: Thu, 21 Nov 2024 18:10:16 +0900 Subject: [PATCH 076/107] =?UTF-8?q?Refactor:=20useSequencerGrid=E3=81=AB?= =?UTF-8?q?=E5=88=87=E3=82=8A=E5=87=BA=E3=81=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sing/SequencerGrid/Presentation.vue | 47 ++++----------- .../Sing/SequencerRuler/Presentation.vue | 44 +++----------- src/composables/useSequencerGridPattern.ts | 60 +++++++++++++++++++ 3 files changed, 77 insertions(+), 74 deletions(-) create mode 100644 src/composables/useSequencerGridPattern.ts diff --git a/src/components/Sing/SequencerGrid/Presentation.vue b/src/components/Sing/SequencerGrid/Presentation.vue index 00b04bb76b..6cf3d893b5 100644 --- a/src/components/Sing/SequencerGrid/Presentation.vue +++ b/src/components/Sing/SequencerGrid/Presentation.vue @@ -38,8 +38,8 @@ :id="`sequencer-grid-pattern-${patternIndex}`" :key="`pattern-${patternIndex}`" patternUnits="userSpaceOnUse" - :x="gridPatterns[patternIndex].x" - :width="gridPatterns[patternIndex].patternWidth" + :x="pattern.x" + :width="pattern.patternWidth" :height="gridCellHeight * 12" > @@ -54,7 +54,7 @@ /> diff --git a/src/components/Sing/KeypointDialog/TempoChangeDialog.stories.ts b/src/components/Sing/KeypointDialog/TempoChangeDialog.stories.ts deleted file mode 100644 index 1027e0e929..0000000000 --- a/src/components/Sing/KeypointDialog/TempoChangeDialog.stories.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { userEvent, within, expect, fn } from "@storybook/test"; -import { Meta, StoryObj } from "@storybook/vue3"; - -import TempoChangeDialog from "./TempoChangeDialog.vue"; - -const meta: Meta = { - component: TempoChangeDialog, - args: { - modelValue: true, - tempoChange: undefined, - mode: "add", - - onOk: fn(), - onHide: fn(), - }, - tags: ["!autodocs"], // ダイアログ系はautodocsのプレビューが正しく表示されないので無効化 -}; - -export default meta; -type Story = StoryObj; - -export const CreateOpened: Story = { - name: "開いている:追加", - args: { - modelValue: true, - mode: "add", - }, -}; -export const ChangeOpened: Story = { - name: "開いている:変更", - args: { - modelValue: true, - tempoChange: { - bpm: 120, - }, - mode: "edit", - }, -}; - -export const ClickOk: Story = { - name: "OKボタンを押す:追加", - args: { ...CreateOpened.args }, - play: async ({ args }) => { - const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う - - const input = canvas.getByLabelText("テンポ"); - await userEvent.clear(input); - await userEvent.type(input, "100"); - - const button = canvas.getByRole("button", { name: /追加する/ }); - await userEvent.click(button); - - await expect(args["onOk"]).toBeCalledWith({ - tempoChange: { - bpm: 100, - }, - }); - }, -}; - -export const ClickDelete: Story = { - name: "OKボタンを押す:編集", - args: { ...ChangeOpened.args }, - play: async ({ args }) => { - const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う - - const input = canvas.getByLabelText("テンポ"); - await userEvent.clear(input); - await userEvent.type(input, "100"); - - const button = canvas.getByRole("button", { name: /変更する/ }); - await userEvent.click(button); - - await expect(args["onOk"]).toBeCalledWith({ - tempoChange: { - bpm: 100, - }, - }); - }, -}; - -export const CancelClose: Story = { - name: "キャンセルボタンを押す", - args: { ...ChangeOpened.args }, - play: async ({ args }) => { - const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う - - const button = canvas.getByRole("button", { name: /キャンセル/ }); - await userEvent.click(button); - - await expect(args["onOk"]).not.toBeCalled(); - }, -}; - -export const Closed: Story = { - name: "閉じている", - tags: ["skip-screenshot"], - args: { - modelValue: false, - }, -}; diff --git a/src/components/Sing/KeypointDialog/TempoChangeDialog.vue b/src/components/Sing/KeypointDialog/TempoChangeDialog.vue deleted file mode 100644 index 7a7282c007..0000000000 --- a/src/components/Sing/KeypointDialog/TempoChangeDialog.vue +++ /dev/null @@ -1,54 +0,0 @@ - - - - - diff --git a/src/components/Sing/KeypointDialog/TimeSignatureChangeDialog.stories.ts b/src/components/Sing/KeypointDialog/TimeSignatureChangeDialog.stories.ts deleted file mode 100644 index e4441ff424..0000000000 --- a/src/components/Sing/KeypointDialog/TimeSignatureChangeDialog.stories.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { userEvent, within, expect, fn, queries } from "@storybook/test"; -import { Meta, StoryObj } from "@storybook/vue3"; -// eslint-disable-next-line storybook/use-storybook-testing-library -- BoundFunctionsは@testing-library/domの型定義にしかない -import type { BoundFunctions } from "@testing-library/dom"; - -import TimeSignatureChangeDialog from "./TimeSignatureChangeDialog.vue"; - -const meta: Meta = { - component: TimeSignatureChangeDialog, - args: { - modelValue: true, - timeSignatureChange: undefined, - mode: "add", - - onOk: fn(), - onHide: fn(), - }, - tags: ["!autodocs"], // ダイアログ系はautodocsのプレビューが正しく表示されないので無効化 -}; -const findOption = async ( - canvas: BoundFunctions, - text: string, -) => { - const maybeElement = await canvas - .findAllByRole("option") - .then((els) => els.find((el) => el.textContent === text)); - if (!maybeElement) throw new Error("Element not found"); - return maybeElement; -}; - -export default meta; -type Story = StoryObj; - -export const CreateOpened: Story = { - name: "開いている:追加", - args: { - modelValue: true, - mode: "add", - }, -}; -export const ChangeOpened: Story = { - name: "開いている:変更", - args: { - modelValue: true, - timeSignatureChange: { - beats: 4, - beatType: 4, - }, - mode: "edit", - }, -}; - -export const ClickOk: Story = { - name: "OKボタンを押す:追加", - args: { ...CreateOpened.args }, - play: async ({ args }) => { - const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う - - const selectRoot = canvas.getByLabelText("拍子の分子"); - await userEvent.click(selectRoot); - - const option = await findOption(canvas, "3"); - await userEvent.click(option); - - const button = canvas.getByRole("button", { name: /追加する/ }); - await userEvent.click(button); - - await expect(args["onOk"]).toBeCalledWith({ - timeSignatureChange: { - beats: 3, - beatType: 4, - }, - }); - }, -}; - -export const ClickDelete: Story = { - name: "OKボタンを押す:編集", - args: { ...ChangeOpened.args }, - play: async ({ args }) => { - const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う - - const selectRoot = canvas.getByLabelText("拍子の分子"); - await userEvent.click(selectRoot); - - const option = await findOption(canvas, "6"); - await userEvent.click(option); - - const button = canvas.getByRole("button", { name: /変更する/ }); - await userEvent.click(button); - - await expect(args["onOk"]).toBeCalledWith({ - timeSignatureChange: { - beats: 6, - beatType: 4, - }, - }); - }, -}; - -export const CancelClose: Story = { - name: "キャンセルボタンを押す", - args: { ...ChangeOpened.args }, - play: async ({ args }) => { - const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う - - const button = canvas.getByRole("button", { name: /キャンセル/ }); - await userEvent.click(button); - - await expect(args["onOk"]).not.toBeCalled(); - }, -}; - -export const Closed: Story = { - name: "閉じている", - tags: ["skip-screenshot"], - args: { - modelValue: false, - }, -}; diff --git a/src/components/Sing/KeypointDialog/TimeSignatureChangeDialog.vue b/src/components/Sing/KeypointDialog/TimeSignatureChangeDialog.vue deleted file mode 100644 index 49cd1b8b89..0000000000 --- a/src/components/Sing/KeypointDialog/TimeSignatureChangeDialog.vue +++ /dev/null @@ -1,86 +0,0 @@ - - - - - From dc08dc15d0840aeb3986a802ccaabe6291bf9c67 Mon Sep 17 00:00:00 2001 From: sevenc-nanashi Date: Thu, 21 Nov 2024 18:16:45 +0900 Subject: [PATCH 081/107] =?UTF-8?q?Change:=20=E3=83=80=E3=82=A4=E3=82=A2?= =?UTF-8?q?=E3=83=AD=E3=82=B0=E3=82=92=E5=B0=8F=E3=81=95=E3=81=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dialog/TempoOrTimeSignatureChangeDialog/CommonDialog.vue | 4 ++-- .../TempoOrTimeSignatureChangeDialog/TempoChangeDialog.vue | 5 ----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/components/Dialog/TempoOrTimeSignatureChangeDialog/CommonDialog.vue b/src/components/Dialog/TempoOrTimeSignatureChangeDialog/CommonDialog.vue index 40cef5cf18..107e1fcea3 100644 --- a/src/components/Dialog/TempoOrTimeSignatureChangeDialog/CommonDialog.vue +++ b/src/components/Dialog/TempoOrTimeSignatureChangeDialog/CommonDialog.vue @@ -78,8 +78,8 @@ const handleCancel = () => {