diff --git a/src/menu/editor.js b/src/menu/editor.js index ab8124a..ad69eaf 100644 --- a/src/menu/editor.js +++ b/src/menu/editor.js @@ -8,11 +8,10 @@ import { UndoOutlined, } from "@ant-design/icons"; import { Menu } from "antd"; -import { action } from "mobx"; -import Notation from "../view/Notation"; import state from "../store/state"; import store from "../store/global"; import { createNotation, isNote, notations as N } from "../util/notation"; +import { createParagraph } from "../util/paragraph"; import { findParagraphAndNotation } from "../util/editor"; import { go, runInWrappedAction, wrappedAction } from "../store/history"; @@ -38,15 +37,30 @@ const handleEditMenu = wrappedAction(({ key }) => { "【记谱者】 记谱", ]; break; + case "add-paragraph": { + store.paragraphs.push( + createParagraph({ + notations: [ + createNotation({ note: "0" }), + createNotation({ note: "0" }), + createNotation({ note: "0" }), + createNotation({ note: "0" }), + createNotation({ note: N.separator }), + ], + }) + ); + break; + } } }); const handleConvertMenu = wrappedAction(() => { - console.log(111); }); +// 画布上的点击事件 const handleClick = wrappedAction((ev) => { - if (state.shouldNotationBlurAfterClick) { + if (state.shouldNotationBlurAfterClick && state.selectedNotationKey) { + state.lastSelectedNotationKey = state.selectedNotationKey; state.selectedNotationKey = null; } state.shouldNotationBlurAfterClick = true; @@ -104,7 +118,7 @@ const handleKeyPress = wrappedAction((ev) => { notation.prefixSups.shift(); } break; - case inputKey === "b" && !ctrl && !shift: + case inputKey === "b" && !ctrl && !shift: { if (notation.prefixSups.indexOf("♭") !== 0) { if (notation.prefixSups[0] === "♯") { notation.prefixSups.shift(); @@ -114,14 +128,17 @@ const handleKeyPress = wrappedAction((ev) => { notation.prefixSups.shift(); } break; - case inputKey === "~" && !ctrl: + } + case inputKey === "~" && !ctrl: { if (notation.topDecorators.includes("~")) { notation.topDecorators.splice(notation.topDecorators.indexOf("~"), 1); } else { notation.topDecorators.push("~"); } break; - case inputKey === "enter" && !ctrl && !shift: + } + case inputKey === "l" && !ctrl && !shift: + case inputKey === "enter" && !ctrl && !shift: { if (paragraph.notations[notationIndex + 1]) { state.selectedNotationKey = paragraph.notations[notationIndex + 1].key; @@ -131,40 +148,91 @@ const handleKeyPress = wrappedAction((ev) => { state.selectedNotationKey = nextNotation.key; } break; - case inputKey === "enter" && !ctrl && shift: + } + case inputKey === "h" && !ctrl && !shift: + case inputKey === "enter" && !ctrl && shift: { if (paragraph.notations[notationIndex - 1]) { state.selectedNotationKey = paragraph.notations[notationIndex - 1].key; } break; - case inputKey === "enter" && ctrl && !shift: + } + case inputKey === "enter" && ctrl && !shift: { const newNotation = createNotation(); paragraph.notations.splice(notationIndex + 1, 0, newNotation); state.selectedNotationKey = newNotation.key; break; - case inputKey === "delete": + } + case inputKey === "j" && !ctrl && !shift: { + const nextParagraph = store.paragraphs[paragraphIndex + 1]; + if (nextParagraph?.notations?.length) { + const nextNotation = + nextParagraph.notations[notationIndex] || + nextParagraph.notations.at(-1); + state.selectedNotationKey = nextNotation.key; + } + break; + } + case inputKey === "k" && !ctrl && !shift: { + const prevParagraph = store.paragraphs[paragraphIndex - 1]; + if (prevParagraph?.notations?.length) { + const prevNotation = + prevParagraph.notations[notationIndex] || + prevParagraph.notations.at(-1); + state.selectedNotationKey = prevNotation.key; + } + break; + } + case inputKey === "delete" && !ctrl && !shift: { paragraph.notations.splice(notationIndex, 1); const currNotation = paragraph.notations[notationIndex] || paragraph.notations[notationIndex - 1]; state.selectedNotationKey = currNotation?.key; break; + } + case inputKey === "=" && !ctrl && !shift: { + if (typeof paragraph.alignJustify !== "boolean") { + paragraph.alignJustify = !( + paragraphIndex === + store.paragraphs.length - 1 + ); + } + paragraph.alignJustify = !paragraph.alignJustify; + break; + } + } + } else { + // 仅未选中符号时 + switch (true) { + case ["h", "j", "k", "l"].includes(inputKey): { + state.selectedNotationKey = + state.lastSelectedNotationKey || + store.paragraphs?.at(0)?.notations?.at(0).key; + } } } + // 全局 switch (true) { case inputKey === "z" && ctrl && !shift: - console.log("undo"); go(-1); break; case inputKey === "y" && ctrl && !shift: case inputKey === "z" && ctrl && shift: - console.log("redo"); go(1); break; + case inputKey === "?" && !ctrl: { + if (state.helpDialogVisible) { + state.helpDialogVisible = false; + } else { + state.configDialogVisible = false; + state.helpDialogVisible = true; + } + break; + } } }); - const fileMenu = ( }> diff --git a/src/menu/notation.js b/src/menu/notation.js index 3060f99..2850e5b 100644 --- a/src/menu/notation.js +++ b/src/menu/notation.js @@ -1,8 +1,10 @@ import { ArrowDownOutlined, ArrowUpOutlined, + DeleteOutlined, FontColorsOutlined, MinusOutlined, + PauseOutlined, PlusOutlined, RadiusSettingOutlined, StopOutlined, @@ -10,27 +12,14 @@ import { import { Menu, message } from "antd"; import state from "../store/state"; import { findParagraphAndNotation } from "../util/editor"; -import { isNote } from "../util/notation"; +import { isNote, isSeparator } from "../util/notation"; import { runInWrappedAction, wrappedAction } from "../store/history"; const getNotationContextMenu = (notation, paragraph) => { const hasTie = notation.tieTo; const handleMenu = wrappedAction(function ({ key }) { - switch (key) { - case "octave-up": - notation.octave += 1; - break; - case "octave-down": - notation.octave -= 1; - break; - case "underline-add": - notation.underline += 1; - break; - case "underline-remove": - notation.underline -= 1; - notation.underline = Math.max(0, notation.underline); - break; - case "tie": + switch (true) { + case key === "tie": if (!hasTie) { state.tieSourceKey = notation.key; return; @@ -41,31 +30,34 @@ const getNotationContextMenu = (notation, paragraph) => { } notation.tieTo = null; break; - case "break-underline": + case key === "break-underline": notation.breakUnderline = !notation.breakUnderline; break; + case key.startsWith("separator-"): { + const separator = key.split("-", 2)[1]; + notation.note = separator; + break; + } } }); return ( - - }> - 升高一个八度(8) - - }> - 降低一个八度(Shift + 8) - - }> - 添加增减时线(U) - - }> - 减少增减时线(Shift + U) - - }> - {notation.breakUnderline ? "在此处延续增减时线" : "在此处打断增减时线"} - - }> - {notation.tieTo ? "删除连音线" : "从此处添加连音线到..."} + // ENHANCE: 分音符类型显示菜单项 + + { + }> + {notation.breakUnderline + ? "在此处延续增减时线" + : "在此处打断增减时线"} + + } + { + }> + {notation.tieTo ? "删除连音线" : "从此处添加连音线到..."} + + } + }> + 删除 ); diff --git a/src/menu/paragraph.js b/src/menu/paragraph.js index 3edd018..f1edb12 100644 --- a/src/menu/paragraph.js +++ b/src/menu/paragraph.js @@ -1,44 +1,45 @@ -import { - DeleteOutlined, - EnterOutlined, - MenuOutlined, - PlusCircleOutlined, - PlusOutlined, -} from "@ant-design/icons"; +import { DeleteOutlined, EnterOutlined } from "@ant-design/icons"; import { Menu } from "antd"; -import state from "../store/state"; import store from "../store/global"; -import { go, runInWrappedAction, wrappedAction } from "../store/history"; +import { createNotation, notations } from "../util/notation"; +import { createParagraph } from "../util/paragraph"; +import { wrappedAction } from "../store/history"; function getParagraphMenuOptions(paragraph) { const handleMenu = wrappedAction(({ key }) => { - console.log(1111111111, key); switch (key) { - case "justify-auto": - paragraph.alignJustify = null; + case "add": { + const idx = store.paragraphs.findIndex((p) => p.key === paragraph.key); + const para = createParagraph({ + notations: [ + createNotation({ note: "0" }), + createNotation({ note: "0" }), + createNotation({ note: "0" }), + createNotation({ note: "0" }), + createNotation({ note: notations.separator }), + ], + }); + if (idx !== -1) { + store.paragraphs.splice(idx + 1, 0, para); + } else { + store.paragraphs.push(para); + } break; - case "justify-enable": - paragraph.alignJustify = true; - break; - case "justify-disable": - paragraph.alignJustify = false; + } + case "delete": + const idx = store.paragraphs.findIndex((p) => p.key === paragraph.key); + if (idx !== -1) { + store.paragraphs.splice(idx, 1); + } break; } }); return ( - }> - 添加符号 - - }> + }> 添加段落 - }> - 自动 - 启用 - 禁用 - }> 删除段落 diff --git a/src/store/global.js b/src/store/global.js index 0ba01a5..1bcf797 100644 --- a/src/store/global.js +++ b/src/store/global.js @@ -1,152 +1,9 @@ import React from "react"; import { observable } from "mobx"; -import { createNotation } from "../util/notation"; +import { getDefaultGlobalDataWidthNotations } from "../util/editor"; // 全局数据及配置,需要持久化 -let globalStore = observable({ - version: 1, - canvasWidth: 1024, - canvasHeight: 1448, - defaultFontSize: 16, - defaultSubFontSize: 12, - title: "简谱", - tone: "♭D", - marginHorizontal: 64, - marginTop: 32, - gapAfterTitle: 16, - gapAfterHeader: 32, - gapBetweenParagraph: 32, - gapBetweenNotation: 8, - beat: [4, 4], - speed: 75, - authors: ["Haven Mattuy 制谱", "杨瑞光 作词"], - paragraphs: [ - { - alignJustify: null, - notations: [ - { - note: "6", - octave: -2, - underline: 2, - dotted: true, - }, - { - note: "6", - octave: -1, - underline: 1, - dotted: false, - }, - { - note: "3", - octave: 0, - underline: 0, - dotted: false, - }, - { - note: "3", - octave: 0, - underline: 0, - dotted: false, - key: "hk", - // tieTo: "ml", - }, - { - note: "│", - octave: 0, - underline: 0, - dotted: false, - }, - { - key: "ml", - // tieTo: "hk", - note: "1", - octave: 0, - underline: 0, - dotted: false, - }, - { - note: "2", - // octave: -1, - // underline: 2, - dotted: false, - }, - { - note: "2", - octave: 2, - // underline: 1, - breakUnderline: true, - dotted: false, - }, - { - note: "2", - octave: 0, - underline: 0, - dotted: true, - }, - { - note: "─", - octave: 0, - underline: 0, - dotted: false, - }, - { - note: "│", - octave: 0, - underline: 0, - dotted: false, - }, - ].map((obj) => createNotation(obj)), - }, - { - notations: [ - { - note: "0", - octave: 0, - dotted: false, - }, - { - note: "4", - octave: 0, - dotted: false, - // prefixSups: ["♯"], - // topDecorators: ["※", "※", "※"], - }, - { - note: "2", - octave: 0, - dotted: false, - }, - ].map((obj) => createNotation(obj)), - alignJustify: false, - }, - { - notations: [ - { - note: "1", - octave: 2, - dotted: false, - }, - { - note: "2", - octave: 0, - dotted: true, - }, - { - note: "4", - octave: 1, - dotted: true, - prefixSups: ["♯"], - topDecorators: ["※"], - }, - { - note: "2", - octave: 0, - dotted: false, - }, - ].map((obj) => createNotation(obj)), - }, - ].map((obj) => ((obj.key = Math.random()), obj)), -}); +const globalStore = observable(getDefaultGlobalDataWidthNotations()); const GlobalContext = React.createContext(globalStore); diff --git a/src/store/state.js b/src/store/state.js index 8125d6b..b766c19 100644 --- a/src/store/state.js +++ b/src/store/state.js @@ -4,10 +4,16 @@ import { observable } from "mobx"; let globalState = observable({ // 当前选中的符号 selectedNotationKey: null, + // 记录上次选中的符号,之后可恢复焦点 + lastSelectedNotationKey: null, // 当前点击事件是否应该让符号失去焦点。应该在符号自身和菜单等点击事件中将其置为false shouldNotationBlurAfterClick: true, // 正在操作的连音线起点 tieSourceKey: null, + // 帮助弹窗是否可见 + helpDialogVisible: false, + // 配置项弹窗是否可见 + configDialogVisible: false, }); export default globalState; diff --git a/src/util/editor.js b/src/util/editor.js index f3c3139..361170c 100644 --- a/src/util/editor.js +++ b/src/util/editor.js @@ -1,10 +1,12 @@ import store from "../store/global"; +import { createNotation, notations } from "../util/notation"; +import { createParagraph } from "../util/paragraph"; import { VERSION } from "./version"; const initialData = { version: VERSION, - canvasWidth: 1024, - canvasHeight: 1448, + canvasWidth: 896, + canvasHeight: 1024, defaultFontSize: 16, defaultSubFontSize: 12, title: "无标题", @@ -21,9 +23,24 @@ const initialData = { notations: [], }; -function getInitialGlobalData() { +function getDefaultGlobalData() { return JSON.parse(JSON.stringify(initialData)); } +function getDefaultGlobalDataWidthNotations() { + const d = getDefaultGlobalData(); + d.paragraphs = [ + createParagraph({ + notations: [ + createNotation({ note: "0" }), + createNotation({ note: "0" }), + createNotation({ note: "0" }), + createNotation({ note: "0" }), + createNotation({ note: notations.separator }), + ], + }), + ]; + return d; +} // 根据key查找符号在全局记录中的位置 function findParagraphAndNotation(key) { @@ -42,4 +59,8 @@ function findParagraphAndNotation(key) { return res; } -export { getInitialGlobalData, findParagraphAndNotation }; +export { + getDefaultGlobalData, + findParagraphAndNotation, + getDefaultGlobalDataWidthNotations, +}; diff --git a/src/view/ConfigModal/index.js b/src/view/ConfigModal/index.js index 94d2c45..40baa09 100644 --- a/src/view/ConfigModal/index.js +++ b/src/view/ConfigModal/index.js @@ -2,7 +2,8 @@ import { Button, Form, Input, InputNumber, Modal } from "antd"; import { observer } from "mobx-react-lite"; import { useCallback } from "react"; import store from "../../store/global"; -import { unwrappedAction, wrappedAction } from "../../store/history"; +import { getDefaultGlobalData } from "../../util/editor"; +import { unwrappedAction } from "../../store/history"; const ImmediateNumberConfigInput = observer(({ propertyName, ...props }) => { const handleChange = unwrappedAction((ev) => { @@ -25,17 +26,36 @@ function ConfigModal({ visible, onVisibleChange }) { const handleClose = useCallback(() => { onVisibleChange?.call(null, false); }, [onVisibleChange]); + const resetConfig = useCallback( + unwrappedAction(() => { + const { + canvasWidth, + canvasHeight, + marginTop, + marginHorizontal, + } = getDefaultGlobalData(); + Object.assign(store, { + canvasWidth, + canvasHeight, + marginTop, + marginHorizontal, + }); + }) + ); return ( + footer={[ + , + - } + , + ]} >
@@ -63,6 +83,7 @@ function ConfigModal({ visible, onVisibleChange }) { /> + { + state.configDialogVisible = visible; + }, []) + ); + const handleChangeHelpModalVisibility = useCallback( + unwrappedAction((visible) => { + state.helpDialogVisible = visible; + }, []) + ); const handleShowConfigDialog = useCallback(() => { - setIsConfigModalVisible(true); + handleChangeConfigModalVisibility(true); }, []); const handleShowHelpDialog = useCallback(() => { - setIsHelpModalVisible(true); + handleChangeHelpModalVisibility(true); }, []); useEffect(() => { document.addEventListener("keydown", handleKeyPress); - document.addEventListener("click", handleClick); return () => { document.removeEventListener("keydown", handleKeyPress); - document.removeEventListener("click", handleClick); }; }, []); @@ -100,7 +110,7 @@ function Editor() { - +
{store.paragraphs.map((p, i) => { @@ -120,9 +130,13 @@ function Editor() { + ); } diff --git a/src/view/HelpModal/index.js b/src/view/HelpModal/index.js index ef68c9b..8a65a89 100644 --- a/src/view/HelpModal/index.js +++ b/src/view/HelpModal/index.js @@ -1,6 +1,72 @@ -function HelpModal({ visible, onVisibleChange }) { - return null; +import { Button, Modal } from "antd"; +import { useCallback } from "react"; +import state from "../../store/state"; +import { unwrappedAction } from "../../store/history"; +import Styles from "./index.module.css"; + +function KeyboardDescItem({ kbd, desc }) { + return ( +
  • + {kbd} + {desc} +
  • + ); +} + +function HelpModal({ visible, onVisibleChange }) { + const handleClose = useCallback(() => { + onVisibleChange?.call(null, false); + }, [onVisibleChange]); + + return ( + + 确定 + + } + > +

    快捷键

    +

    全局

    +
      + + + + +
    +

    仅选中符号时

    +

    输入

    +
      + + + + + + + +
    +

    操作

    +
      + + + + + + +
    +

    提示

    +
      +
    • 段落上可以右键单击弹出操作菜单
    • +
    • 增减时线默认连续,可在音符右键菜单中选择打断
    • +
    +
    + ); } -// TODO: do this export default HelpModal; diff --git a/src/view/HelpModal/index.module.css b/src/view/HelpModal/index.module.css new file mode 100644 index 0000000..90bb9a6 --- /dev/null +++ b/src/view/HelpModal/index.module.css @@ -0,0 +1,19 @@ +.list { +padding: 0; +list-style: none; +} +.listItem { + display: inline-block; + margin-top: 8px; + margin-right: 16px; + min-width: 260px; +} +.kbd { + display: inline-block; + margin-right: 8px; + border-radius: 4px; + background-color: lightgray; + padding: 4px; + width: 120px; + text-align: center; +} diff --git a/src/view/Notation/index.js b/src/view/Notation/index.js index b375806..de9d972 100644 --- a/src/view/Notation/index.js +++ b/src/view/Notation/index.js @@ -120,7 +120,6 @@ function Notation({ offsetX, notation, paragraph }) { > )); }; - return (