feat: 添加帮助面板

This commit is contained in:
2021-10-06 12:15:58 +08:00
parent cfb73205b1
commit 65a63081fc
11 changed files with 304 additions and 240 deletions
+81 -13
View File
@@ -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,18 +148,42 @@ 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] ||
@@ -150,21 +191,48 @@ const handleKeyPress = wrappedAction((ev) => {
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 = (
<Menu>
<Menu.Item key="create" icon={<PlusOutlined />}>
+23 -31
View File
@@ -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,32 +30,35 @@ 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 (
<Menu onClick={handleMenu}>
<Menu.Item key="octave-up" icon={<ArrowUpOutlined />}>
升高一个八度8
</Menu.Item>
<Menu.Item key="octave-down" icon={<ArrowDownOutlined />}>
降低一个八度Shift + 8
</Menu.Item>
<Menu.Item key="underline-add" icon={<PlusOutlined />}>
添加增减时线U
</Menu.Item>
<Menu.Item key="underline-remove" icon={<MinusOutlined />}>
减少增减时线Shift + U
</Menu.Item>
// ENHANCE: 分音符类型显示菜单项
<Menu onClick={handleMenu} style={{ minWidth: "80px" }}>
{
<Menu.Item key="break-underline" icon={<StopOutlined />}>
{notation.breakUnderline ? "在此处延续增减时线" : "在此处打断增减时线"}
{notation.breakUnderline
? "在此处延续增减时线"
: "在此处打断增减时线"}
</Menu.Item>
}
{
<Menu.Item key="tie" icon={<RadiusSettingOutlined />}>
{notation.tieTo ? "删除连音线" : "从此处添加连音线到..."}
</Menu.Item>
}
<Menu.Item key="delete" icon={<DeleteOutlined />}>
删除
</Menu.Item>
</Menu>
);
+27 -26
View File
@@ -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 (
<Menu onClick={handleMenu}>
<Menu.Item key="addNotation" icon={<PlusOutlined />}>
添加符号
</Menu.Item>
<Menu.Item key="addParagraph" icon={<EnterOutlined />}>
<Menu.Item key="add" icon={<EnterOutlined />}>
添加段落
</Menu.Item>
<Menu.SubMenu key="justify" title="两端对齐" icon={<MenuOutlined />}>
<Menu.Item key="justify-auto">自动</Menu.Item>
<Menu.Item key="justify-enable">启用</Menu.Item>
<Menu.Item key="justify-disable">禁用</Menu.Item>
</Menu.SubMenu>
<Menu.Item danger key="delete" icon={<DeleteOutlined />}>
删除段落
</Menu.Item>
+2 -145
View File
@@ -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);
+6
View File
@@ -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;
+25 -4
View File
@@ -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,
};
+26 -5
View File
@@ -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 (
<Modal
visible={visible}
title="配置"
onCancel={handleClose}
footer={
<Button key="ok" type="primary" onClick={handleClose}>
footer={[
<Button key="reset" onClick={resetConfig}>
重置为默认
</Button>,
<Button key="close" type="primary" onClick={handleClose}>
关闭
</Button>
}
</Button>,
]}
>
<Form labelAlign="right" labelCol={{ span: 4 }}>
<Form.Item label="画布大小">
@@ -63,6 +83,7 @@ function ConfigModal({ visible, onVisibleChange }) {
/>
</Input.Group>
</Form.Item>
<ImmediateNumberConfigItem label="顶部边距" propertyName="marginTop" />
<ImmediateNumberConfigItem
label="左右边距"
propertyName="marginHorizontal"
+22 -8
View File
@@ -8,6 +8,7 @@ import {
} from "@ant-design/icons";
import { observer } from "mobx-react-lite";
import { useCallback, useEffect, useState } from "react";
import state from "../../store/state";
import store from "../../store/global";
import P, {
calcParagraphAboveOffset,
@@ -20,15 +21,16 @@ import {
handleClick,
handleKeyPress,
} from "../../menu/editor";
import { unwrappedAction } from "../../store/history";
import Canvas from "../Canvas";
import ConfigModal from "../ConfigModal";
import Header from "../Header";
import HelpModal from "../HelpModal";
import Paragraph from "../Paragraph";
import Row from "../Row";
import Styles from "./index.module.css";
function Editor() {
const [isConfigModalVisible, setIsConfigModalVisible] = useState(false);
const [isHelpModalVisible, setIsHelpModalVisible] = useState(false);
const heightCache = [];
function accumulate(index) {
@@ -43,19 +45,27 @@ function Editor() {
height += calcParagraphAboveOffset(paragraphs[index]);
return height;
}
const handleChangeConfigModalVisibility = useCallback(
unwrappedAction((visible) => {
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() {
</Button>
</div>
</div>
<Canvas>
<Canvas onClick={handleClick}>
<Header />
<Row offsetX={store.marginHorizontal} offsetY={P.headerOffsetY}>
{store.paragraphs.map((p, i) => {
@@ -120,9 +130,13 @@ function Editor() {
</Row>
</Canvas>
<ConfigModal
visible={isConfigModalVisible}
onVisibleChange={setIsConfigModalVisible}
visible={state.configDialogVisible}
onVisibleChange={handleChangeConfigModalVisibility}
></ConfigModal>
<HelpModal
visible={state.helpDialogVisible}
onVisibleChange={handleChangeHelpModalVisibility}
></HelpModal>
</div>
);
}
+69 -3
View File
@@ -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 (
<li className={Styles.listItem}>
<kbd className={Styles.kbd}>{kbd}</kbd>
<span>{desc}</span>
</li>
);
}
function HelpModal({ visible, onVisibleChange }) {
const handleClose = useCallback(() => {
onVisibleChange?.call(null, false);
}, [onVisibleChange]);
return (
<Modal
width="100%"
height="90%"
style={{ top: "4%" }}
visible={visible}
title="帮助"
onCancel={handleClose}
footer={
<Button key="close" type="primary" onClick={handleClose}>
确定
</Button>
}
>
<h2>快捷键</h2>
<h3>全局</h3>
<ul className={Styles.list}>
<KeyboardDescItem kbd="" desc="显示此帮助" />
<KeyboardDescItem kbd="Ctrl+Z" desc="撤销" />
<KeyboardDescItem kbd="Ctrl+Shift+Z" desc="重做" />
<KeyboardDescItem kbd="h j k l" desc="移动焦点" />
</ul>
<h3>仅选中符号时</h3>
<h4>输入</h4>
<ul className={Styles.list}>
<KeyboardDescItem kbd="0~7, -" desc="输入音符" />
<KeyboardDescItem kbd="|" desc="输入小节线" />
<KeyboardDescItem kbd="." desc="输入附点" />
<KeyboardDescItem kbd="~" desc="颤音符号" />
<KeyboardDescItem kbd="u / U" desc="增减时线" />
<KeyboardDescItem kbd="8 / *" desc="八度圆点" />
<KeyboardDescItem kbd="# / b" desc="音符升/降" />
</ul>
<h4>操作</h4>
<ul className={Styles.list}>
<KeyboardDescItem kbd="Enter" desc="移动到下一个音符" />
<KeyboardDescItem kbd="Shift+Enter" desc="移动到上一个音符" />
<KeyboardDescItem kbd="Ctrl+Enter" desc="在当前位置后插入音符" />
<KeyboardDescItem kbd="Delete" desc="删除当前音符" />
<KeyboardDescItem kbd="=" desc="段落是否两端对齐" />
<KeyboardDescItem kbd="h j k l" desc="Vi风格移动焦点" />
</ul>
<h2>提示</h2>
<ul className={Styles.list}>
<li>段落上可以右键单击弹出操作菜单</li>
<li>增减时线默认连续可在音符右键菜单中选择打断</li>
</ul>
</Modal>
);
}
// TODO: do this
export default HelpModal;
+19
View File
@@ -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;
}
-1
View File
@@ -120,7 +120,6 @@ function Notation({ offsetX, notation, paragraph }) {
></circle>
));
};
return (
<EditableContent
inputType="select"