Skip to content

Commit

Permalink
複数選択:選択だけ実装 (#1470)
Browse files Browse the repository at this point in the history
Co-authored-by: Hiroshiba <hihokaruta@gmail.com>
Co-authored-by: Hiroshiba <Hiroshiba@users.noreply.github.com>
  • Loading branch information
3 people authored Sep 1, 2023
1 parent 0723a89 commit 1343325
Show file tree
Hide file tree
Showing 10 changed files with 511 additions and 52 deletions.
253 changes: 207 additions & 46 deletions src/components/AudioCell.vue

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions src/components/SettingDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,33 @@
>
</q-toggle>
</q-card-actions>
<q-card-actions
v-if="!isProduction"
class="q-px-md q-py-none bg-surface"
>
<div>複数選択</div>
<div aria-label="複数のテキスト欄を選択できるようにします。">
<q-icon name="help_outline" size="sm" class="help-hover-icon">
<q-tooltip
:delay="500"
anchor="center right"
self="center left"
transition-show="jump-right"
transition-hide="jump-left"
>
複数のテキスト欄を選択できるようにします。
</q-tooltip>
</q-icon>
</div>
<q-space />
<q-toggle
:model-value="experimentalSetting.enableMultiSelect"
@update:model-value="
changeExperimentalSetting('enableMultiSelect', $event)
"
>
</q-toggle>
</q-card-actions>
</q-card>
<q-card flat class="setting-card">
<q-card-actions>
Expand Down Expand Up @@ -895,6 +922,7 @@ import { computed, ref } from "vue";
import FileNamePatternDialog from "./FileNamePatternDialog.vue";
import { useStore } from "@/store";
import {
isProduction,
SavingSetting,
EngineSetting,
ExperimentalSetting,
Expand Down
33 changes: 33 additions & 0 deletions src/store/audio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,17 @@ export const audioStore = createPartialStore<AudioStoreTypes>({
},
},

SELECTED_AUDIO_KEYS: {
getter(state) {
return (
// undo/redoで消えていることがあるためフィルタする
state._selectedAudioKeys?.filter((audioKey) =>
state.audioKeys.includes(audioKey)
) || []
);
},
},

HAVE_AUDIO_QUERY: {
getter: (state) => (audioKey: AudioKey) => {
return state.audioItems[audioKey]?.query != undefined;
Expand Down Expand Up @@ -521,6 +532,28 @@ export const audioStore = createPartialStore<AudioStoreTypes>({
},
},

SET_SELECTED_AUDIO_KEYS: {
mutation(state, { audioKeys }: { audioKeys?: AudioKey[] }) {
state._selectedAudioKeys = audioKeys;
},
action(
{ state, commit, getters },
{ audioKeys }: { audioKeys?: AudioKey[] }
) {
const uniqueAudioKeys = new Set(audioKeys);
if (
getters.ACTIVE_AUDIO_KEY &&
!uniqueAudioKeys.has(getters.ACTIVE_AUDIO_KEY)
) {
throw new Error("selectedAudioKeys must include activeAudioKey");
}
const sortedAudioKeys = state.audioKeys.filter((audioKey) =>
uniqueAudioKeys.has(audioKey)
);
commit("SET_SELECTED_AUDIO_KEYS", { audioKeys: sortedAudioKeys });
},
},

SET_AUDIO_PLAY_START_POINT: {
mutation(state, { startPoint }: { startPoint?: number }) {
state.audioPlayStartPoint = startPoint;
Expand Down
1 change: 1 addition & 0 deletions src/store/setting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const settingStoreState: SettingStoreState = {
enableInterrogativeUpspeak: false,
enableMorphing: false,
enableMultiEngine: false,
enableMultiSelect: false,
},
splitTextWhenPaste: "PERIOD_AND_NEW_LINE",
splitterPosition: {
Expand Down
10 changes: 10 additions & 0 deletions src/store/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export type AudioStoreState = {
audioKeys: AudioKey[];
audioStates: Record<AudioKey, AudioState>;
_activeAudioKey?: AudioKey;
_selectedAudioKeys?: AudioKey[];
audioPlayStartPoint?: number;
nowPlayingContinuously: boolean;
};
Expand All @@ -139,6 +140,10 @@ export type AudioStoreTypes = {
getter: AudioKey | undefined;
};

SELECTED_AUDIO_KEYS: {
getter: AudioKey[];
};

HAVE_AUDIO_QUERY: {
getter(audioKey: AudioKey): boolean;
};
Expand Down Expand Up @@ -192,6 +197,11 @@ export type AudioStoreTypes = {
action(payload: { audioKey?: AudioKey }): void;
};

SET_SELECTED_AUDIO_KEYS: {
mutation: { audioKeys?: AudioKey[] };
action(payload: { audioKeys?: AudioKey[] }): void;
};

SET_AUDIO_PLAY_START_POINT: {
mutation: { startPoint?: number };
action(payload: { startPoint?: number }): void;
Expand Down
2 changes: 2 additions & 0 deletions src/type/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { IpcSOData } from "./ipc";
import { AltPortInfos } from "@/store/type";
import { Result } from "@/type/result";

export const isProduction = import.meta.env.MODE === "production";
export const isElectron = import.meta.env.VITE_TARGET === "electron";
export const isBrowser = import.meta.env.VITE_TARGET === "browser";

Expand Down Expand Up @@ -497,6 +498,7 @@ export const experimentalSettingSchema = z.object({
enableInterrogativeUpspeak: z.boolean().default(false),
enableMorphing: z.boolean().default(false),
enableMultiEngine: z.boolean().default(false),
enableMultiSelect: z.boolean().default(false),
});

export type ExperimentalSetting = z.infer<typeof experimentalSettingSchema>;
Expand Down
20 changes: 14 additions & 6 deletions src/views/EditorHome.vue
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ const hotkeyMap = new Map<HotkeyAction, () => HotkeyReturnType>([
"テキスト欄にフォーカスを戻す",
() => {
if (activeAudioKey.value !== undefined) {
focusCell({ audioKey: activeAudioKey.value });
focusCell({ audioKey: activeAudioKey.value, focusTarget: "textField" });
}
return false; // this is the same with event.preventDefault()
},
Expand Down Expand Up @@ -406,7 +406,7 @@ const addAudioItem = async () => {
audioItem,
prevAudioKey: activeAudioKey.value,
});
audioCellRefs[newAudioKey].focusTextField();
audioCellRefs[newAudioKey].focusCell({ focusTarget: "textField" });
};
const duplicateAudioItem = async () => {
const prevAudioKey = activeAudioKey.value;
Expand All @@ -420,7 +420,7 @@ const duplicateAudioItem = async () => {
audioItem: cloneDeep(prevAudioItem),
prevAudioKey: activeAudioKey.value,
});
audioCellRefs[newAudioKey].focusTextField();
audioCellRefs[newAudioKey].focusCell({ focusTarget: "textField" });
};
// Pane
Expand Down Expand Up @@ -472,8 +472,16 @@ watch(shouldShowPanes, (val, old) => {
});
// セルをフォーカス
const focusCell = ({ audioKey }: { audioKey: AudioKey }) => {
audioCellRefs[audioKey].focusTextField();
const focusCell = ({
audioKey,
focusTarget,
}: {
audioKey: AudioKey;
focusTarget?: "root" | "textField";
}) => {
audioCellRefs[audioKey].focusCell({
focusTarget: focusTarget ?? "textField",
});
};
// Electronのデフォルトのundo/redoを無効化
Expand Down Expand Up @@ -562,7 +570,7 @@ onMounted(async () => {
const newAudioKey = await store.dispatch("REGISTER_AUDIO_ITEM", {
audioItem,
});
focusCell({ audioKey: newAudioKey });
focusCell({ audioKey: newAudioKey, focusTarget: "textField" });
// 最初の話者を初期化
store.dispatch("SETUP_SPEAKER", {
Expand Down
194 changes: 194 additions & 0 deletions tests/e2e/browser/複数選択.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { test, expect, Page } from "@playwright/test";
import { toggleSetting, navigateToMain } from "../navigators";

test.beforeEach(async ({ page }) => {
const BASE_URL = "http://localhost:5173/#/home";
await page.setViewportSize({ width: 800, height: 600 });
await page.goto(BASE_URL);

await navigateToMain(page);
await page.waitForTimeout(100);
await toggleSetting(page, "複数選択");

await addAudioCells(page, 3);
});

const ctrlLike = process.platform === "darwin" ? "Meta" : "Control";

type SelectedStatus = {
active: number;
selected: number[];
};
/**
* アクティブなAudioCellと選択されているAudioCellを取得する。
* 戻り値のインデックスは1から始まる。(nth-childのインデックスと揃えるため)
*/
async function getSelectedStatus(page: Page): Promise<SelectedStatus> {
const selectedAudioKeys = await page.evaluate(() => {
const audioCells = [...document.querySelectorAll(".audio-cell")];
let active: number | undefined;
const selected: number[] = [];
for (let i = 0; i < audioCells.length; i++) {
const audioCell = audioCells[i];
if (audioCell.classList.contains("active")) {
active = i + 1;
}
if (audioCell.classList.contains("selected")) {
selected.push(i + 1);
}
}
if (active === undefined) {
throw new Error("No active audio cell");
}

return { active, selected };
});
return selectedAudioKeys;
}

async function addAudioCells(page: Page, count: number) {
for (let i = 0; i < count; i++) {
await page.getByRole("button", { name: "テキストを追加" }).click();
await page.waitForTimeout(100);
}
}

test("複数選択:マウス周り", async ({ page }) => {
let selectedStatus: SelectedStatus;

// 複数選択していない状態でactiveのAudioCellをクリックしても何も起こらない
await page.locator(".audio-cell:nth-child(1)").click();

await page.waitForTimeout(100);
selectedStatus = await getSelectedStatus(page);
expect(selectedStatus.active).toBe(1);
expect(selectedStatus.selected).toEqual([1]);

// Shift+クリックは前回選択していたAudioCellから今回クリックしたAudioCellまでを選択する
await page.locator(".audio-cell:nth-child(2)").click();
await page.keyboard.down("Shift");
await page.locator(".audio-cell:nth-child(4)").click();
await page.keyboard.up("Shift");

await page.waitForTimeout(100);
selectedStatus = await getSelectedStatus(page);
expect(selectedStatus.active).toBe(4);
expect(selectedStatus.selected).toEqual([2, 3, 4]);

// ただのクリックはactiveAudioKeyとselectedAudioKeysをクリックしたAudioCellだけにする
await page.locator(".audio-cell:nth-child(2)").click();

await page.waitForTimeout(100);
selectedStatus = await getSelectedStatus(page);
expect(selectedStatus.active).toBe(2);
expect(selectedStatus.selected).toEqual([2]);

if (process.platform === "darwin" && !!process.env.CI) {
// なぜかCmd(Meta)+クリックが動かないのでスキップする
// FIXME: 動くようにする
return;
}

// Ctrl+クリックは選択範囲を追加する
await page.keyboard.down(ctrlLike);
await page.locator(".audio-cell:nth-child(4)").click();
await page.keyboard.up(ctrlLike);
await page.waitForTimeout(100);

selectedStatus = await getSelectedStatus(page);
expect(selectedStatus.active).toBe(4);
expect(selectedStatus.selected).toEqual([2, 4]);

// Ctrl+クリックは選択範囲から削除する
await page.keyboard.down(ctrlLike);
await page.locator(".audio-cell:nth-child(2)").click();
await page.keyboard.up(ctrlLike);
await page.waitForTimeout(100);

selectedStatus = await getSelectedStatus(page);
expect(selectedStatus.active).toBe(4);
expect(selectedStatus.selected).toEqual([4]);

// activeのAudioCellをCtrl+クリックすると選択範囲から削除して次のselectedのAudioCellをactiveにする
await page.keyboard.down(ctrlLike);
await page.locator(".audio-cell:nth-child(2)").click();
await page.locator(".audio-cell:nth-child(2)").click();
await page.keyboard.up(ctrlLike);
await page.waitForTimeout(100);

selectedStatus = await getSelectedStatus(page);
expect(selectedStatus.active).toBe(4);
expect(selectedStatus.selected).toEqual([4]);

// selected内のCharacterButtonをクリックしても選択範囲は変わらない
await page.locator(".audio-cell:nth-child(2)").click();
await page.keyboard.down("Shift");
await page.locator(".audio-cell:nth-child(4)").click();
await page.keyboard.up("Shift");

await page.locator(".audio-cell:nth-child(2) .character-button").click();

await page.waitForTimeout(100);
selectedStatus = await getSelectedStatus(page);
expect(selectedStatus.active).toBe(4);
expect(selectedStatus.selected).toEqual([2, 3, 4]);

// selected外のCharacterButtonをクリックすると選択範囲をそのAudioCellだけにする
await page.locator(".audio-cell:nth-child(1)").click();

await page.waitForTimeout(100);
selectedStatus = await getSelectedStatus(page);
expect(selectedStatus.active).toBe(1);
expect(selectedStatus.selected).toEqual([1]);
});

test("複数選択:キーボード", async ({ page }) => {
let selectedStatus: SelectedStatus;
// Shift+下で下方向を選択範囲にする
await page.locator(".audio-cell:nth-child(2)").click();
await page.keyboard.down("Shift");
await page.keyboard.press("ArrowDown");
await page.keyboard.up("Shift");
await page.waitForTimeout(100);

selectedStatus = await getSelectedStatus(page);
expect(selectedStatus.active).toBe(3);
expect(selectedStatus.selected).toEqual([2, 3]);

// ただの下で下方向をactiveにして他の選択を解除する
await page.keyboard.press("ArrowDown");

await page.waitForTimeout(100);
selectedStatus = await getSelectedStatus(page);
expect(selectedStatus.active).toBe(4);
expect(selectedStatus.selected).toEqual([4]);

// Shift+上で上方向を選択範囲にする
await page.keyboard.down("Shift");
await page.keyboard.press("ArrowUp");
await page.keyboard.up("Shift");
await page.waitForTimeout(100);

selectedStatus = await getSelectedStatus(page);
expect(selectedStatus.active).toBe(3);
expect(selectedStatus.selected).toEqual([3, 4]);

// ただの上で上方向をactiveにして他の選択を解除する
await page.keyboard.press("ArrowUp");
await page.waitForTimeout(100);

selectedStatus = await getSelectedStatus(page);
expect(selectedStatus.active).toBe(2);
expect(selectedStatus.selected).toEqual([2]);

// EnterでactiveのAudioCellのテキストフィールドにフォーカスし、複数選択を解除する
await page.keyboard.down("Shift");
await page.keyboard.press("ArrowDown");
await page.keyboard.up("Shift");
await page.keyboard.press("Enter");

await page.waitForTimeout(100);
selectedStatus = await getSelectedStatus(page);
expect(selectedStatus.active).toBe(3);
expect(selectedStatus.selected).toEqual([3]);
});
Loading

0 comments on commit 1343325

Please sign in to comment.