feat: 优化输入体验

This commit is contained in:
2021-10-06 18:14:45 +08:00
parent c7a60dfd67
commit 2048a89d7e
17 changed files with 118 additions and 175 deletions
+1 -1
View File
@@ -104,7 +104,7 @@ const EditableContent = function (
))} ))}
</Menu> </Menu>
); );
}, [initialValue, options, handleConfirm]); }, [initialValue, options, handleConfirm, handleHideContextMenu, overlay]);
let renderContent, renderPopover; let renderContent, renderPopover;
switch (inputType) { switch (inputType) {
case "select": case "select":
+28 -15
View File
@@ -1,4 +1,3 @@
import { Button, Menu, Upload } from "antd";
import { import {
FolderOpenOutlined, FolderOpenOutlined,
HighlightOutlined, HighlightOutlined,
@@ -8,10 +7,14 @@ import {
SaveOutlined, SaveOutlined,
UndoOutlined, UndoOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { Menu } from "antd";
import state from "../store/state"; import state from "../store/state";
import store from "../store/global"; import store from "../store/global";
import { createNotation, isNote, notations as N } from "../util/notation"; import { createNotation, isNote, notations as N } from "../util/notation";
import { createParagraph } from "../util/paragraph"; import {
createParagraph,
createParagraphWithNotations,
} from "../util/paragraph";
import { exportFile, loadFile, saveFile } from "../util/file"; import { exportFile, loadFile, saveFile } from "../util/file";
import { findParagraphAndNotation } from "../util/editor"; import { findParagraphAndNotation } from "../util/editor";
import { import {
@@ -55,19 +58,11 @@ const handleEditMenu = wrappedAction(({ key }) => {
]; ];
break; break;
case "add-paragraph": { case "add-paragraph": {
store.paragraphs.push( store.paragraphs.push(createParagraphWithNotations());
createParagraph({
notations: [
createNotation({ note: "0" }),
createNotation({ note: "0" }),
createNotation({ note: "0" }),
createNotation({ note: "0" }),
createNotation({ note: N.separator }),
],
})
);
break; break;
} }
default:
break;
} }
}); });
const handleConvertMenu = wrappedAction(() => { const handleConvertMenu = wrappedAction(() => {
@@ -176,8 +171,19 @@ const handleKeyPress = wrappedAction((ev) => {
} }
case inputKey === "enter" && ctrl && !shift: { case inputKey === "enter" && ctrl && !shift: {
const newNotation = createNotation(); const newNotation = createNotation();
paragraph.notations.splice(notationIndex + 1, 0, newNotation); if (
state.selectedNotationKey = newNotation.key; notationIndex === paragraph.notations.length - 1 &&
paragraphIndex === store.paragraphs.length - 1
) {
// 在最后一个音符时ctrl+enter插入新段落并创建符号
store.paragraphs.push(createParagraph({ notations: [newNotation] }));
state.selectedNotationKey = store.paragraphs
.at(-1)
.notations.at(0).key;
} else {
paragraph.notations.splice(notationIndex + 1, 0, newNotation);
state.selectedNotationKey = newNotation.key;
}
break; break;
} }
case inputKey === "j" && !ctrl && !shift: { case inputKey === "j" && !ctrl && !shift: {
@@ -218,6 +224,8 @@ const handleKeyPress = wrappedAction((ev) => {
paragraph.alignJustify = !paragraph.alignJustify; paragraph.alignJustify = !paragraph.alignJustify;
break; break;
} }
default:
break;
} }
} else { } else {
// 仅未选中符号时 // 仅未选中符号时
@@ -226,7 +234,10 @@ const handleKeyPress = wrappedAction((ev) => {
state.selectedNotationKey = state.selectedNotationKey =
state.lastSelectedNotationKey || state.lastSelectedNotationKey ||
store.paragraphs?.at(0)?.notations?.at(0).key; store.paragraphs?.at(0)?.notations?.at(0).key;
break;
} }
default:
break;
} }
} }
// 全局 // 全局
@@ -261,6 +272,8 @@ const handleKeyPress = wrappedAction((ev) => {
} }
break; break;
} }
default:
break;
} }
}); });
+15 -61
View File
@@ -1,25 +1,19 @@
import { import {
ArrowDownOutlined,
ArrowUpOutlined,
DeleteOutlined, DeleteOutlined,
FontColorsOutlined,
MinusOutlined,
PauseOutlined,
PlusOutlined,
RadiusSettingOutlined, RadiusSettingOutlined,
StopOutlined, StopOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { Menu, message } from "antd"; import { Menu } from "antd";
import state from "../store/state"; import state from "../store/state";
import { findParagraphAndNotation } from "../util/editor"; import { findParagraphAndNotation } from "../util/editor";
import { isNote, isSeparator } from "../util/notation"; import { isNote } from "../util/notation";
import { runInWrappedAction, wrappedAction } from "../store/history"; import { runInWrappedAction, wrappedAction } from "../store/history";
const getNotationContextMenu = (notation, paragraph) => { const getNotationContextMenu = (notation, paragraph) => {
const hasTie = notation.tieTo; const hasTie = notation.tieTo;
const handleMenu = wrappedAction(function ({ key }) { const handleMenu = wrappedAction(function ({ key }) {
switch (true) { switch (true) {
case key === "tie": case key === "tie": {
if (!hasTie) { if (!hasTie) {
state.tieSourceKey = notation.key; state.tieSourceKey = notation.key;
return; return;
@@ -30,6 +24,7 @@ const getNotationContextMenu = (notation, paragraph) => {
} }
notation.tieTo = null; notation.tieTo = null;
break; break;
}
case key === "break-underline": case key === "break-underline":
notation.breakUnderline = !notation.breakUnderline; notation.breakUnderline = !notation.breakUnderline;
break; break;
@@ -38,76 +33,35 @@ const getNotationContextMenu = (notation, paragraph) => {
notation.note = separator; notation.note = separator;
break; break;
} }
default:
break;
} }
}); });
return ( return (
// ENHANCE: 分音符类型显示菜单项 // ENHANCE: 分音符类型显示菜单项
<Menu onClick={handleMenu} style={{ minWidth: "80px" }}> <Menu onClick={handleMenu} style={{ minWidth: "80px" }}>
{ {isNote(notation) && (
<Menu.Item key="break-underline" icon={<StopOutlined />}> <Menu.Item key="break-underline" icon={<StopOutlined />}>
{notation.breakUnderline {notation.breakUnderline
? "在此处延续增减时线" ? "在此处延续增减时线"
: "在此处打断增减时线"} : "在此处打断增减时线"}
</Menu.Item> </Menu.Item>
} )}
{ {isNote(notation) && (
<Menu.Item key="tie" icon={<RadiusSettingOutlined />}> <Menu.Item
key="tie"
icon={<RadiusSettingOutlined />}
notation={notation}
>
{notation.tieTo ? "删除连音线" : "从此处添加连音线到..."} {notation.tieTo ? "删除连音线" : "从此处添加连音线到..."}
</Menu.Item> </Menu.Item>
} )}
<Menu.Item key="delete" icon={<DeleteOutlined />}> <Menu.Item key="delete" icon={<DeleteOutlined />}>
删除 删除
</Menu.Item> </Menu.Item>
</Menu> </Menu>
); );
return [
{
key: "octaveUp",
text: "升高一个八度(8",
visible: isNote(notation),
onClick: wrappedAction(() => {
notation.octave += 1;
}),
},
{
key: "octaveDown",
text: "降低一个八度(Shift + 8",
visible: isNote(notation),
onClick: wrappedAction(() => {
notation.octave -= 1;
}),
},
{
key: "tie",
text: notation.tieTo ? "删除连音线" : "从此处添加连音线到...",
visible: isNote(notation),
onClick: wrappedAction(() => {}),
},
{
key: "break-underline",
text: notation.breakUnderline
? "在此处延续增减时线"
: "在此处打断增减时线",
visible: isNote(notation),
onClick: wrappedAction(() => {
notation.breakUnderline = !notation.breakUnderline;
}),
},
{
key: "delete",
text: "删除",
onClick: wrappedAction(() => {
const index = paragraph.notations.indexOf(notation);
if (index !== -1) {
paragraph.notations.splice(index, 1);
} else {
message.error("符号不存在");
}
}),
},
];
}; };
// ENHANCE: 使用EditableContent组件的overlay属性传入菜单 // ENHANCE: 使用EditableContent组件的overlay属性传入菜单
+2
View File
@@ -32,6 +32,8 @@ function getParagraphMenuOptions(paragraph) {
store.paragraphs.splice(idx, 1); store.paragraphs.splice(idx, 1);
} }
break; break;
default:
break;
} }
}); });
+1 -8
View File
@@ -1,4 +1,4 @@
import { action, intercept, keys, runInAction, toJS } from "mobx"; import { action, runInAction, toJS } from "mobx";
import globalStore from "./global"; import globalStore from "./global";
const storeHistory = []; const storeHistory = [];
@@ -43,12 +43,10 @@ function go(span) {
Math.max(0, cursor + span), Math.max(0, cursor + span),
storeHistory.length - 1 storeHistory.length - 1
); );
console.log("go ", span);
if (newCursor === cursor) { if (newCursor === cursor) {
return; return;
} }
cursor = newCursor; cursor = newCursor;
console.log("gone to ", newCursor);
Object.assign(globalStore, storeHistory[newCursor]); Object.assign(globalStore, storeHistory[newCursor]);
} }
@@ -64,11 +62,6 @@ function executeAction(actionFunc, ...args) {
if (!_isEqual(storeHistory[cursor], current)) { if (!_isEqual(storeHistory[cursor], current)) {
storeHistory[++cursor] = current; storeHistory[++cursor] = current;
storeHistory.length = cursor + 1; storeHistory.length = cursor + 1;
console.log(
"pushed history, length & cursor: ",
storeHistory.length,
cursor
);
if (storeHistory.length > 256) { if (storeHistory.length > 256) {
storeHistory.splice(0, 256 - storeHistory.length); storeHistory.splice(0, 256 - storeHistory.length);
} }
+4 -14
View File
@@ -1,6 +1,5 @@
import store from "../store/global"; import store from "../store/global";
import { createNotation, notations } from "../util/notation"; import { createParagraphWithNotations } from "../util/paragraph";
import { createParagraph } from "../util/paragraph";
import { VERSION } from "./version"; import { VERSION } from "./version";
const initialData = { const initialData = {
@@ -16,7 +15,7 @@ const initialData = {
gapAfterTitle: 16, gapAfterTitle: 16,
gapAfterHeader: 32, gapAfterHeader: 32,
gapBetweenParagraph: 32, gapBetweenParagraph: 32,
gapBetweenNotation: 8, gapBetweenNotation: 16,
beat: [4, 4], beat: [4, 4],
speed: 75, speed: 75,
authors: ["佚名 作词", "简谱编辑器 制谱"], authors: ["佚名 作词", "简谱编辑器 制谱"],
@@ -28,17 +27,7 @@ function getDefaultGlobalData() {
} }
function getDefaultGlobalDataWidthNotations() { function getDefaultGlobalDataWidthNotations() {
const d = getDefaultGlobalData(); const d = getDefaultGlobalData();
d.paragraphs = [ d.paragraphs = [createParagraphWithNotations()];
createParagraph({
notations: [
createNotation({ note: "0" }),
createNotation({ note: "0" }),
createNotation({ note: "0" }),
createNotation({ note: "0" }),
createNotation({ note: notations.separator }),
],
}),
];
return d; return d;
} }
@@ -55,6 +44,7 @@ function findParagraphAndNotation(key) {
res.paragraphIndex = i; res.paragraphIndex = i;
return true; return true;
} }
return false;
}); });
return res; return res;
} }
+2 -7
View File
@@ -1,6 +1,7 @@
import { message } from "antd"; import { message } from "antd";
import { remove, transaction } from "mobx"; import { remove, transaction } from "mobx";
import { toJS } from "mobx"; import { toJS } from "mobx";
import globalState from "../store/state";
import store from "../store/global"; import store from "../store/global";
import { clearHistory } from "../store/history"; import { clearHistory } from "../store/history";
import domtoimage from "./dom-to-image"; import domtoimage from "./dom-to-image";
@@ -20,9 +21,9 @@ async function loadFile(file) {
const data = await read(file); const data = await read(file);
if (!(data.version <= VERSION)) { if (!(data.version <= VERSION)) {
message.error("不支持的文件格式,请使用最新版本"); message.error("不支持的文件格式,请使用最新版本");
console.log(data);
return; return;
} }
globalState.lastSelectedNotationKey = globalState.selectedNotationKey = null;
clearHistory(); clearHistory();
transaction(() => { transaction(() => {
for (const key of Object.keys(store)) { for (const key of Object.keys(store)) {
@@ -44,12 +45,6 @@ function saveFile() {
} }
function exportFile() { function exportFile() {
// const canvas = document.createElement("canvas");
// const ctx = canvas.getContext("2d");
// const dpi = window.devicePixelRatio || 1;
// canvas.style.width = store.canvasWidth;
// canvas.style.height = store.canvasHeight;
// ctx.scale(dpi, dpi);
domtoimage.toPng(document.querySelector("#temp_svg")).then((dataUrl) => { domtoimage.toPng(document.querySelector("#temp_svg")).then((dataUrl) => {
const downloadLink = document.createElement("a"); const downloadLink = document.createElement("a");
downloadLink.download = (store.title || "未标题") + ".png"; downloadLink.download = (store.title || "未标题") + ".png";
+14 -1
View File
@@ -1,3 +1,5 @@
import { createNotation, notations as N } from "./notation";
// 新建段落 // 新建段落
function createParagraph(initial) { function createParagraph(initial) {
const p = { const p = {
@@ -12,5 +14,16 @@ function createParagraph(initial) {
} }
return p; return p;
} }
function createParagraphWithNotations() {
return createParagraph({
notations: [
createNotation({ note: "0" }),
createNotation({ note: "0" }),
createNotation({ note: "0" }),
createNotation({ note: "0" }),
createNotation({ note: N.separator }),
],
});
}
export { createParagraph }; export { createParagraph, createParagraphWithNotations };
+4 -18
View File
@@ -6,22 +6,6 @@ import store from "../store/global";
let canvas, context2D, defaultFontFamily; let canvas, context2D, defaultFontFamily;
let measureSvgEl, measureTextEl; let measureSvgEl, measureTextEl;
function _calcFontData(fontSize) {
const span = document.createElement("span");
span.style.fontFamily = window.getComputedStyle(document.body).fontFamily;
span.style.fontSize = `${fontSize}px`;
span.style.visibility = "hidden";
span.style.position = "absolute";
span.style.display = "inline-block";
span.textContent = "x";
document.body.appendChild(span);
const data = span.getBoundingClientRect();
return {
xWidth: data.width,
xHeight: data.height,
};
}
function _initializeCanvas(fontSize) { function _initializeCanvas(fontSize) {
if (!canvas) { if (!canvas) {
canvas = document.createElement("canvas"); canvas = document.createElement("canvas");
@@ -151,7 +135,8 @@ function calcParagraphContentHeight(paragraph) {
} }
let tieHeight = 0; let tieHeight = 0;
if (_hasTie(paragraph)) { if (_hasTie(paragraph)) {
tieHeight = placement.maxTieHeight; // FIXME: 应该计算连音线高度
// tieHeight = placement.maxTieHeight;
} }
// 段落高度不是最高的音符的高度,而是上边偏移最大的音符的上部分偏移量+下边偏移最 // 段落高度不是最高的音符的高度,而是上边偏移最大的音符的上部分偏移量+下边偏移最
// 大的音符的下部分偏移量 // 大的音符的下部分偏移量
@@ -184,7 +169,8 @@ function calcParagraphAboveOffset(paragraph) {
} }
let tieHeight = 0; let tieHeight = 0;
if (_hasTie(paragraph)) { if (_hasTie(paragraph)) {
tieHeight = placement.maxTieHeight; // FIXME: 应该计算连音线高度
// tieHeight = placement.maxTieHeight;
} }
const notationOffset = notations const notationOffset = notations
.map((n) => calcNotationAboveOffset(n)) .map((n) => calcNotationAboveOffset(n))
+28 -17
View File
@@ -1,4 +1,4 @@
import { Button, Form, Input, InputNumber, Modal } from "antd"; import { Button, Form, Input, Modal } from "antd";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useCallback } from "react"; import { useCallback } from "react";
import store from "../../store/global"; import store from "../../store/global";
@@ -26,25 +26,28 @@ function ConfigModal({ visible, onVisibleChange }) {
const handleClose = useCallback(() => { const handleClose = useCallback(() => {
onVisibleChange?.call(null, false); onVisibleChange?.call(null, false);
}, [onVisibleChange]); }, [onVisibleChange]);
const resetConfig = useCallback( const resetConfig = unwrappedAction(() => {
unwrappedAction(() => { const {
const { canvasWidth,
canvasWidth, canvasHeight,
canvasHeight, marginTop,
marginTop, marginHorizontal,
marginHorizontal, gapBetweenParagraph,
} = getDefaultGlobalData(); gapBetweenNotation,
Object.assign(store, { } = getDefaultGlobalData();
canvasWidth, Object.assign(store, {
canvasHeight, canvasWidth,
marginTop, canvasHeight,
marginHorizontal, marginTop,
}); marginHorizontal,
}) gapBetweenParagraph,
); gapBetweenNotation,
});
});
return ( return (
<Modal <Modal
style={{ top: 20 }}
visible={visible} visible={visible}
title="配置" title="配置"
onCancel={handleClose} onCancel={handleClose}
@@ -88,6 +91,14 @@ function ConfigModal({ visible, onVisibleChange }) {
label="左右边距" label="左右边距"
propertyName="marginHorizontal" propertyName="marginHorizontal"
/> />
<ImmediateNumberConfigItem
label="段间距"
propertyName="gapBetweenParagraph"
/>
<ImmediateNumberConfigItem
label="音符间距"
propertyName="gapBetweenNotation"
/>
</Form> </Form>
</Modal> </Modal>
); );
+11 -17
View File
@@ -7,7 +7,7 @@ import {
SettingOutlined, SettingOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useCallback, useEffect, useState } from "react"; import { useEffect } from "react";
import state from "../../store/state"; import state from "../../store/state";
import store from "../../store/global"; import store from "../../store/global";
import P, { import P, {
@@ -31,7 +31,6 @@ import Row from "../Row";
import Styles from "./index.module.css"; import Styles from "./index.module.css";
function Editor() { function Editor() {
const [isHelpModalVisible, setIsHelpModalVisible] = useState(false);
const heightCache = []; const heightCache = [];
function accumulate(index) { function accumulate(index) {
const paragraphs = store.paragraphs || []; const paragraphs = store.paragraphs || [];
@@ -45,23 +44,18 @@ function Editor() {
height += calcParagraphAboveOffset(paragraphs[index]); height += calcParagraphAboveOffset(paragraphs[index]);
return height; return height;
} }
const handleChangeConfigModalVisibility = useCallback( const handleChangeConfigModalVisibility = unwrappedAction((visible) => {
unwrappedAction((visible) => { state.configDialogVisible = visible;
state.configDialogVisible = visible; });
}, []) const handleChangeHelpModalVisibility = unwrappedAction((visible) => {
); state.helpDialogVisible = visible;
const handleChangeHelpModalVisibility = useCallback( });
unwrappedAction((visible) => { const handleShowConfigDialog = () => {
state.helpDialogVisible = visible;
}, [])
);
const handleShowConfigDialog = useCallback(() => {
handleChangeConfigModalVisibility(true); handleChangeConfigModalVisibility(true);
}, []); };
const handleShowHelpDialog = useCallback(() => { const handleShowHelpDialog = () => {
handleChangeHelpModalVisibility(true); handleChangeHelpModalVisibility(true);
}, []); };
useEffect(() => { useEffect(() => {
document.addEventListener("keydown", handleKeyPress); document.addEventListener("keydown", handleKeyPress);
return () => { return () => {
-1
View File
@@ -1,4 +1,3 @@
import { action } from "mobx";
import { message } from "antd"; import { message } from "antd";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import EditableContent from "../../../component/EditableContent"; import EditableContent from "../../../component/EditableContent";
-1
View File
@@ -1,6 +1,5 @@
import { DeleteOutlined, PlusOutlined } from "@ant-design/icons"; import { DeleteOutlined, PlusOutlined } from "@ant-design/icons";
import { Modal } from "antd"; import { Modal } from "antd";
import { action } from "mobx";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import EditableContent from "../../../component/EditableContent"; import EditableContent from "../../../component/EditableContent";
import P from "../../../util/placement"; import P from "../../../util/placement";
+1 -2
View File
@@ -1,6 +1,5 @@
import { action } from "mobx";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useEffect, useRef } from "react"; import { useRef } from "react";
import EditableContent from "../../../component/EditableContent"; import EditableContent from "../../../component/EditableContent";
import store from "../../../store/global"; import store from "../../../store/global";
import { wrappedAction } from "../../../store/history"; import { wrappedAction } from "../../../store/history";
-2
View File
@@ -1,7 +1,5 @@
import { Button, Modal } from "antd"; import { Button, Modal } from "antd";
import { useCallback } from "react"; import { useCallback } from "react";
import state from "../../store/state";
import { unwrappedAction } from "../../store/history";
import Styles from "./index.module.css"; import Styles from "./index.module.css";
function KeyboardDescItem({ kbd, desc }) { function KeyboardDescItem({ kbd, desc }) {
+6 -5
View File
@@ -1,4 +1,3 @@
import { action } from "mobx";
import { message } from "antd"; import { message } from "antd";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import EditableContent from "../../component/EditableContent"; import EditableContent from "../../component/EditableContent";
@@ -7,7 +6,7 @@ import store from "../../store/global";
import P, { calcSubTextWidth } from "../../util/placement"; import P, { calcSubTextWidth } from "../../util/placement";
import { findParagraphAndNotation } from "../../util/editor"; import { findParagraphAndNotation } from "../../util/editor";
import { getNotationContextMenu } from "../../menu/notation"; import { getNotationContextMenu } from "../../menu/notation";
import { notations } from "../../util/notation"; import { isNote, notations } from "../../util/notation";
import { wrappedAction } from "../../store/history"; import { wrappedAction } from "../../store/history";
import Row from "../Row"; import Row from "../Row";
import Text from "../Text"; import Text from "../Text";
@@ -40,11 +39,13 @@ function Notation({ offsetX, notation, paragraph }) {
state.tieSourceKey = null; state.tieSourceKey = null;
return; return;
} }
if (sourceParagraph.notations.includes(notation)) { if (!sourceParagraph.notations.includes(notation)) {
message.warn("只允许相同段落的音符相连!");
} else if (!isNote(notation)) {
message.warn("只允许在音符上添加连音线!");
} else {
sourceNotation.tieTo = notation.key; sourceNotation.tieTo = notation.key;
notation.tieTo = sourceNotation.key; notation.tieTo = sourceNotation.key;
} else {
message.warn("只允许相同段落的音符相连!");
} }
state.tieSourceKey = null; state.tieSourceKey = null;
} }
+1 -5
View File
@@ -8,15 +8,11 @@ import P, {
calcParagraphContentHeight, calcParagraphContentHeight,
calcParagraphWidth, calcParagraphWidth,
} from "../../util/placement"; } from "../../util/placement";
import { import { getParagraphMenuOptions } from "../../menu/paragraph";
getParagraphMenuOptions,
handleSelectParagraphMenu,
} from "../../menu/paragraph";
import Notation from "../Notation"; import Notation from "../Notation";
import Row from "../Row"; import Row from "../Row";
function Paragraph({ paragraph, offsetY, alignJustify }) { function Paragraph({ paragraph, offsetY, alignJustify }) {
console.log("render paragraph");
const notations = paragraph.notations || []; const notations = paragraph.notations || [];
const widthCache = []; const widthCache = [];
let itemFlexOffset = 0; let itemFlexOffset = 0;