diff --git a/README.md b/README.md index 838ebf1..50640da 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 一个轻量的跨平台 Markdown 编辑器,基于 **Qt 5 / C++17** 构建。 -当前版本 **0.9.1**。 +当前版本 **0.10.0**。 ## 界面 @@ -29,8 +29,11 @@ - 代码块按语言做语法高亮:C / C++ / Python / JavaScript / TypeScript / Java / Rust / Go / JSON / Bash / SQL / YAML,关键字、字符串、数字、 注释、预处理指令分色;不认识的标记语言就按等宽纯文本显示 -- 代码框**左上角**是语言下拉菜单,选一下就会改写围栏上的语言标记, - 高亮立刻跟着变(选「纯文本」则去掉语言) +- 代码框**右下角**是语言下拉菜单,选一下就会改写围栏上的语言标记, + 高亮立刻跟着变(选「纯文本」则去掉语言);只有一行代码时它挪到复制按钮左边 +- 表格渲染成带网格的表格控件:双击单元格直接编辑,右键可**插入 / 删除行、插入 / 删除列**, + 改完立刻回写成 Markdown 表格,对齐方式(`:---:`)也会保留 +- 源代码模式下表格退回原始 Markdown 文本 - `Tab` / `Shift+Tab` 整行缩进,选中多行时批量缩进 - 自动换行(`Alt+Z`)、`Ctrl+滚轮` 或 `Ctrl++` / `Ctrl+-` 缩放字号 - 撤销 / 重做、剪切 / 复制 / 粘贴,菜单项会跟随可用状态自动灰显 diff --git a/VoiletMark.pro b/VoiletMark.pro index 8bf0a7f..63e6779 100644 --- a/VoiletMark.pro +++ b/VoiletMark.pro @@ -1,6 +1,6 @@ TEMPLATE = app TARGET = VoiletMark -VERSION = 0.9.1 +VERSION = 0.10.0 QT += core gui widgets printsupport diff --git a/docs/screenshot-dark.png b/docs/screenshot-dark.png index cb25fa1..80f2d2e 100644 Binary files a/docs/screenshot-dark.png and b/docs/screenshot-dark.png differ diff --git a/docs/screenshot-light.png b/docs/screenshot-light.png index 4ce7f18..b6ea274 100644 Binary files a/docs/screenshot-light.png and b/docs/screenshot-light.png differ diff --git a/docs/screenshot-source.png b/docs/screenshot-source.png index eea957d..cecb0b9 100644 Binary files a/docs/screenshot-source.png and b/docs/screenshot-source.png differ diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index e8b4bac..96f615d 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -76,6 +76,12 @@ const char kWelcomeText[] = R"VOILETMARK( 1. 有序列表项 2. 另一个有序列表项 +| 功能 | 状态 | +| --- | :---: | +| 语法高亮 | 完成 | +| 导出 PDF | 完成 | +| Git 同步 | 完成 | + - [x] 自动深浅色主题 - [x] Markdown 语法高亮 - [x] 导出 PDF @@ -119,6 +125,12 @@ switch it under **View → Theme**. > Quotes, lists, `inline code`, **bold**, *italic*, ~~strikethrough~~ > and [links](https://example.com) all render in place. +| Feature | Status | +| --- | :---: | +| Syntax highlighting | Done | +| PDF export | Done | +| Git sync | Done | + - [x] Syntax highlighting - [x] Folder sidebar - [x] PDF export diff --git a/src/MarkdownEditor.cpp b/src/MarkdownEditor.cpp index c2a162c..42d0c24 100644 --- a/src/MarkdownEditor.cpp +++ b/src/MarkdownEditor.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -20,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -123,8 +125,20 @@ MarkdownEditor::MarkdownEditor(QWidget *parent) m_fenceTimer = new QTimer(this); m_fenceTimer->setSingleShot(true); m_fenceTimer->setInterval(120); - connect(m_fenceTimer, &QTimer::timeout, this, &MarkdownEditor::rebuildCodeFences); + connect(m_fenceTimer, &QTimer::timeout, this, [this] { + rebuildCodeFences(); + if (m_updatingTable) + return; + if (m_skipTableRebuild) { + m_skipTableRebuild = false; + positionTableWidgets(); + return; + } + rebuildTables(); + }); connect(this, &QPlainTextEdit::textChanged, this, [this] { m_fenceTimer->start(); }); + connect(this, &QPlainTextEdit::updateRequest, this, + [this](const QRect &, int) { positionTableWidgets(); }); m_lineNumberArea->setVisible(m_sourceMode); applyEditorFont(); @@ -252,6 +266,7 @@ void MarkdownEditor::applyTheme() { updateCurrentLineHighlight(); m_lineNumberArea->update(); + rebuildTables(); viewport()->update(); } @@ -293,6 +308,7 @@ void MarkdownEditor::setSourceMode(bool enabled) applyEditorFont(); updateViewportMargins(); updateCurrentLineHighlight(); + rebuildTables(); viewport()->update(); emit sourceModeChanged(enabled); } @@ -594,6 +610,16 @@ QRectF MarkdownEditor::languageRect(int index) const const QString label = languageLabel(index) + QStringLiteral(" \u25be"); const qreal width = metrics.horizontalAdvance(label) + 16; const qreal height = metrics.height() + 6; + + // 只有一行代码时,框太矮,把语言徽标挪到复制按钮左边同一行。 + const CodeFence &fence = m_codeFences.at(index); + const int lastLine = fence.close >= 0 ? fence.close : document()->blockCount() - 1; + const int contentLines = lastLine - fence.open - 1; + if (contentLines <= 1) { + const QRectF copy = copyButtonRect(index); + return QRectF(copy.left() - width - 6, copy.top(), width, height); + } + return QRectF(rect.right() - width - 8, rect.bottom() - height - 6, width, height); } @@ -909,3 +935,337 @@ void MarkdownEditor::dropEvent(QDropEvent *event) } QPlainTextEdit::dropEvent(event); } + +// ---------------------------------------------------------------- 表格 + +namespace { +bool isTableSeparatorLine(const QString &text) +{ + static const QRegularExpression pattern( + QStringLiteral(R"(^\s*\|?\s*:?-{2,}:?\s*(?:\|\s*:?-{2,}:?\s*)+\|?\s*$)")); + return pattern.match(text).hasMatch(); +} + +QStringList splitTableRow(const QString &line) +{ + QString text = line.trimmed(); + if (text.startsWith(QLatin1Char('|'))) + text.remove(0, 1); + if (text.endsWith(QLatin1Char('|'))) + text.chop(1); + QStringList cells = text.split(QLatin1Char('|')); + for (QString &cell : cells) + cell = cell.trimmed(); + return cells; +} +} // namespace + +void MarkdownEditor::rebuildTables() +{ + for (TableInfo &table : m_tables) { + if (table.widget) { + table.widget->deleteLater(); + table.widget = nullptr; + } + } + m_tables.clear(); + + if (m_sourceMode || !document()) + return; + + QTextBlock block = document()->firstBlock(); + while (block.isValid()) { + if (!block.text().contains(QLatin1Char('|'))) { + block = block.next(); + continue; + } + const QTextBlock separator = block.next(); + if (!separator.isValid() || !isTableSeparatorLine(separator.text())) { + block = block.next(); + continue; + } + + TableInfo table; + table.firstBlock = block.blockNumber(); + + int last = separator.blockNumber(); + QTextBlock cursor = separator.next(); + while (cursor.isValid() && !cursor.text().trimmed().isEmpty() + && cursor.text().contains(QLatin1Char('|'))) { + last = cursor.blockNumber(); + cursor = cursor.next(); + } + table.lastBlock = last; + + for (const QString &cell : splitTableRow(separator.text())) { + const bool left = cell.startsWith(QLatin1Char(':')); + const bool right = cell.endsWith(QLatin1Char(':')); + if (left && right) + table.alignments.append(Qt::AlignHCenter); + else if (right) + table.alignments.append(Qt::AlignRight); + else + table.alignments.append(Qt::AlignLeft); + } + + m_tables.append(table); + block = document()->findBlockByNumber(last).next(); + } + + for (int index = 0; index < m_tables.size(); ++index) + createTableWidget(index); + + positionTableWidgets(); +} + +void MarkdownEditor::createTableWidget(int index) +{ + TableInfo &table = m_tables[index]; + const QTextBlock first = document()->findBlockByNumber(table.firstBlock); + if (!first.isValid()) + return; + + auto *widget = new QTableWidget(viewport()); + widget->setObjectName(QStringLiteral("markdownTable")); + widget->setFrameShape(QFrame::NoFrame); + widget->setShowGrid(true); + widget->setWordWrap(true); + widget->horizontalHeader()->hide(); + widget->verticalHeader()->hide(); + widget->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + widget->verticalHeader()->setSectionResizeMode(QHeaderView::Stretch); + widget->setSelectionMode(QAbstractItemView::SingleSelection); + widget->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::SelectedClicked + | QAbstractItemView::EditKeyPressed); + widget->setContextMenuPolicy(Qt::CustomContextMenu); + widget->setFont(font()); + + const ThemeColors &colors = ThemeManager::instance()->colors(); + widget->setStyleSheet( + QStringLiteral("QTableWidget { background: %1; color: %2; border: 1px solid %3;" + " border-radius: 6px; gridline-color: %3; }" + "QTableWidget::item { padding: 2px 6px; }" + "QTableWidget::item:selected { background: %4; color: %5; }") + .arg(colors.editorBase.name(), colors.editorText.name(), colors.border.name(), + colors.highlight.name(), colors.highlightedText.name())); + + { + const QSignalBlocker blocker(widget); + + const QStringList header = splitTableRow(first.text()); + widget->setColumnCount(qMax(1, header.size())); + widget->setRowCount(1); + + for (int column = 0; column < header.size(); ++column) { + auto *item = new QTableWidgetItem(header.at(column)); + QFont itemFont = item->font(); + itemFont.setBold(true); + item->setFont(itemFont); + item->setTextAlignment(table.alignments.value(column, Qt::AlignLeft) | Qt::AlignVCenter); + widget->setItem(0, column, item); + } + + int row = 1; + for (int number = table.firstBlock + 2; number <= table.lastBlock; ++number, ++row) { + const QStringList cells = splitTableRow(document()->findBlockByNumber(number).text()); + widget->insertRow(row); + for (int column = 0; column < widget->columnCount(); ++column) { + auto *item = new QTableWidgetItem(cells.value(column)); + item->setTextAlignment(table.alignments.value(column, Qt::AlignLeft) + | Qt::AlignVCenter); + widget->setItem(row, column, item); + } + } + } + + connect(widget, &QTableWidget::itemChanged, this, [this, widget](QTableWidgetItem *) { + if (m_updatingTable) + return; + const int current = tableIndexFor(widget); + if (current >= 0) + applyTableEdit(current); + }); + + connect(widget, &QWidget::customContextMenuRequested, this, [this, widget](const QPoint &pos) { + const int current = tableIndexFor(widget); + if (current < 0) + return; + + QMenu menu(this); + menu.addAction(tr("插入行"), this, [this, widget, current] { + const int row = qBound(1, widget->currentRow() + 1, widget->rowCount()); + { + const QSignalBlocker blocker(widget); + widget->insertRow(row); + for (int column = 0; column < widget->columnCount(); ++column) { + auto *item = new QTableWidgetItem; + item->setTextAlignment(m_tables.at(current).alignments.value(column, + Qt::AlignLeft) + | Qt::AlignVCenter); + widget->setItem(row, column, item); + } + } + applyTableEdit(current); + }); + menu.addAction(tr("删除行"), this, [this, widget, current] { + if (widget->rowCount() <= 2) + return; + { + const QSignalBlocker blocker(widget); + widget->removeRow(qMax(1, widget->currentRow())); + } + applyTableEdit(current); + }); + menu.addSeparator(); + menu.addAction(tr("插入列"), this, [this, widget, current] { + const int column = qBound(0, widget->currentColumn() + 1, widget->columnCount()); + { + const QSignalBlocker blocker(widget); + widget->insertColumn(column); + for (int row = 0; row < widget->rowCount(); ++row) { + auto *item = new QTableWidgetItem; + item->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); + widget->setItem(row, column, item); + } + if (column <= m_tables.at(current).alignments.size()) + m_tables[current].alignments.insert(column, Qt::AlignLeft); + } + applyTableEdit(current); + }); + menu.addAction(tr("删除列"), this, [this, widget, current] { + if (widget->columnCount() <= 1) + return; + const int column = qMax(0, widget->currentColumn()); + { + const QSignalBlocker blocker(widget); + widget->removeColumn(column); + if (column < m_tables.at(current).alignments.size()) + m_tables[current].alignments.remove(column); + } + applyTableEdit(current); + }); + + menu.exec(widget->viewport()->mapToGlobal(pos)); + }); + + table.widget = widget; +} + +int MarkdownEditor::tableIndexFor(const QTableWidget *widget) const +{ + for (int index = 0; index < m_tables.size(); ++index) { + if (m_tables.at(index).widget == widget) + return index; + } + return -1; +} + +void MarkdownEditor::applyTableEdit(int index) +{ + if (index < 0 || index >= m_tables.size()) + return; + TableInfo &table = m_tables[index]; + QTableWidget *widget = table.widget; + if (!widget) + return; + + const int columns = widget->columnCount(); + QStringList headerCells; + for (int column = 0; column < columns; ++column) { + const QTableWidgetItem *item = widget->item(0, column); + headerCells << (item ? item->text().trimmed() : QString()); + } + + QStringList separatorCells; + for (int column = 0; column < columns; ++column) { + switch (table.alignments.value(column, Qt::AlignLeft)) { + case Qt::AlignHCenter: + separatorCells << QStringLiteral(":---:"); + break; + case Qt::AlignRight: + separatorCells << QStringLiteral("---:"); + break; + default: + separatorCells << QStringLiteral("---"); + break; + } + } + + QStringList lines; + lines << (QStringLiteral("| ") + headerCells.join(QStringLiteral(" | ")) + QStringLiteral(" |")); + lines << (QStringLiteral("| ") + separatorCells.join(QStringLiteral(" | ")) + + QStringLiteral(" |")); + + for (int row = 1; row < widget->rowCount(); ++row) { + QStringList cells; + for (int column = 0; column < columns; ++column) { + const QTableWidgetItem *item = widget->item(row, column); + cells << (item ? item->text().trimmed() : QString()); + } + lines << (QStringLiteral("| ") + cells.join(QStringLiteral(" | ")) + QStringLiteral(" |")); + } + + replaceTableText(index, lines); +} + +void MarkdownEditor::replaceTableText(int index, const QStringList &lines) +{ + if (index < 0 || index >= m_tables.size() || lines.isEmpty()) + return; + + TableInfo &table = m_tables[index]; + const QTextBlock first = document()->findBlockByNumber(table.firstBlock); + const QTextBlock last = document()->findBlockByNumber(table.lastBlock); + if (!first.isValid() || !last.isValid()) + return; + + const int oldCount = table.lastBlock - table.firstBlock + 1; + const int delta = lines.size() - oldCount; + + m_updatingTable = true; + QTextCursor cursor(first); + cursor.beginEditBlock(); + cursor.setPosition(first.position()); + cursor.setPosition(last.position() + last.text().length(), QTextCursor::KeepAnchor); + cursor.insertText(lines.join(QLatin1Char('\n'))); + cursor.endEditBlock(); + m_updatingTable = false; + m_skipTableRebuild = true; + + table.lastBlock = table.firstBlock + lines.size() - 1; + for (int other = index + 1; other < m_tables.size(); ++other) { + m_tables[other].firstBlock += delta; + m_tables[other].lastBlock += delta; + } + + positionTableWidgets(); +} + +QRectF MarkdownEditor::tableRect(const TableInfo &table) const +{ + const QTextBlock first = document()->findBlockByNumber(table.firstBlock); + const QTextBlock last = document()->findBlockByNumber(table.lastBlock); + if (!first.isValid() || !last.isValid()) + return QRectF(); + + const QRectF top = blockBoundingGeometry(first).translated(contentOffset()); + const QRectF bottom = blockBoundingGeometry(last).translated(contentOffset()); + return QRectF(3, top.top() - 2, viewport()->width() - 6, bottom.bottom() - top.top() + 4); +} + +void MarkdownEditor::positionTableWidgets() +{ + const QRectF visible(viewport()->rect()); + for (TableInfo &table : m_tables) { + if (!table.widget) + continue; + const QRectF rect = tableRect(table); + if (!rect.isValid() || m_sourceMode) { + table.widget->hide(); + continue; + } + table.widget->setGeometry(rect.toRect()); + table.widget->setVisible(rect.intersects(visible)); + } +} + diff --git a/src/MarkdownEditor.h b/src/MarkdownEditor.h index a28cd20..91cba08 100644 --- a/src/MarkdownEditor.h +++ b/src/MarkdownEditor.h @@ -8,6 +8,7 @@ class MarkdownHighlighter; class QTextBlock; class QTimer; +class QTableWidget; /// 编辑器本体:等宽字体、行号栏、当前行高亮、Tab 缩进、 /// Ctrl+滚轮缩放,以及把拖入的文件转交给主窗口打开。 @@ -63,6 +64,8 @@ private slots: void rebuildCodeFences(); private: + struct TableInfo; + void applyEditorFont(); void updateViewportMargins(); void indentSelection(bool outdent); @@ -73,6 +76,15 @@ private: QString languageLabel(int index) const; void showLanguageMenu(int index); void setCodeFenceLanguage(int index, const QString &info); + + // ---- 表格 ---- + void rebuildTables(); + void positionTableWidgets(); + QRectF tableRect(const TableInfo &table) const; + void createTableWidget(int index); + void applyTableEdit(int index); + void replaceTableText(int index, const QStringList &lines); + int tableIndexFor(const QTableWidget *widget) const; bool acceptsLocalFiles(const QMimeData *mime) const; void selectedBlockRange(int *firstBlock, int *lastBlock) const; void selectBlocks(int firstBlock, int lastBlock); @@ -86,6 +98,13 @@ private: CodeHighlight::Language language = CodeHighlight::Language::None; }; + struct TableInfo { + int firstBlock = -1; + int lastBlock = -1; + QTableWidget *widget = nullptr; + QVector alignments; + }; + QWidget *m_lineNumberArea = nullptr; MarkdownHighlighter *m_highlighter = nullptr; int m_fontPointSize = 12; @@ -93,6 +112,9 @@ private: int m_activeBlock = -1; QVector m_codeFences; + QVector m_tables; + bool m_updatingTable = false; + bool m_skipTableRebuild = false; QTimer *m_fenceTimer = nullptr; int m_hoveredFence = -1; int m_hoveredLanguage = -1; diff --git a/translations/voiletmark_en.qm b/translations/voiletmark_en.qm index d3eab11..883921a 100644 Binary files a/translations/voiletmark_en.qm and b/translations/voiletmark_en.qm differ diff --git a/translations/voiletmark_en.ts b/translations/voiletmark_en.ts index 169deb3..bdfcaa0 100644 --- a/translations/voiletmark_en.ts +++ b/translations/voiletmark_en.ts @@ -13,448 +13,448 @@ MainWindow - - + + 新建 New - - + + 打开… Open… - - + + 保存 Save - - + + 另存为… Save As… - - + + 导出 PDF… Export PDF… - - + + 退出 Quit - - + + 撤销 Undo - - + + 重做 Redo - - + + 剪切 Cut - - + + 复制 Copy - - + + 粘贴 Paste - - + + 全选 Select All - - + + 自动换行 Word Wrap - - + + 放大 Zoom In - - + + 缩小 Zoom Out - - + + 实际大小 Actual Size - - + + 打开文件夹… Open Folder… - - - - + + + + 侧栏 Sidebar - - + + 源代码模式 Source Mode - - + + 代码块 Code Block - - + + 标题 %1 Heading %1 - + 文件夹 Folder - - + + 未打开文件夹 No folder open - - + + Git 仓库 Git repository - - + + 拉取 Pull - + git pull --ff-only git pull --ff-only - - + + 获取 Fetch - + git fetch --all --prune git fetch --all --prune - - - + + + 推送 Push - + git push git push - - + + 暂存 Stage - + git add -A git add -A - - - - - + + + + + 提交 Commit - + 把暂存区的内容提交 Commit the staged changes - - + + 文件 File - - + + 最近打开 Recent Files - - + + 编辑 Edit - - + + 视图 View - - + + 主题 Theme - - + + 6:00–18:00 使用浅色,其余时间使用深色 Light from 6:00 to 18:00, dark the rest of the day - - + + 语言 Language - - + + 格式 Format - - + + 标题 Heading - - + + 帮助 Help - - - + + + 关于 VoiletMark About VoiletMark - - + + 显示 / 隐藏文件夹侧栏(Ctrl+B) Show / hide the folder sidebar (Ctrl+B) - - + + 源代码模式:关闭语法高亮与行号(Ctrl+Shift+M) Source mode: turn off highlighting and line numbers (Ctrl+Shift+M) - - + + 未命名 Untitled - + 打开 Markdown 文件 Open Markdown File - + Markdown 文件 (*.md *.markdown *.mdown *.mkd);;文本文件 (*.txt);;所有文件 (*) Markdown files (*.md *.markdown *.mdown *.mkd);;Text files (*.txt);;All files (*) - + 无法打开文件 Cannot Open File - + 读取「%1」失败:%2 Could not read "%1": %2 - + 另存为 Save As - + Markdown 文件 (*.md);;文本文件 (*.txt);;所有文件 (*) Markdown files (*.md);;Text files (*.txt);;All files (*) - + 导出 PDF Export PDF - + PDF 文件 (*.pdf) PDF files (*.pdf) - + 导出失败 Export Failed - + 没能写出「%1」,换个位置再试一次。 Could not write "%1". Try another location. - + 已导出 %1 Exported to %1 - - + + 无法保存文件 Cannot Save File - + 写入「%1」失败:%2 Could not write "%1": %2 - + 写入「%1」时发生错误。 An error occurred while writing "%1". - + 提交「%1」失败:%2 Could not commit "%1": %2 - + 已保存 %1 Saved to %1 - + VoiletMark VoiletMark - + 「%1」有未保存的修改,是否保存? "%1" has unsaved changes. Save them? - + %1[*] — VoiletMark %1[*] — VoiletMark - + 行 %1,列 %2 Line %1, Col %2 - + 已选 %1 字 %1 selected - + %1 字 %1 chars - + <h3>VoiletMark %1</h3><p>一个轻量的跨平台 Markdown 编辑器。</p><p>基于 Qt %2 与 C++17 构建。</p> <h3>VoiletMark %1</h3><p>A lightweight cross-platform Markdown editor.</p><p>Built with Qt %2 and C++17.</p> - + (暂无记录) (none) - + 清空列表 Clear List - + 打开文件夹 Open Folder - + 文件 → 打开文件夹… File → Open Folder… - + (没有 Markdown 文件) (no Markdown files) - + 正在%1… %1… - + Git 操作失败 Git Operation Failed - + %1失败: %2 @@ -463,64 +463,64 @@ %2 - + (没有输出) (no output) - + %1完成 %1 finished - + 暂存区是空的,先点「暂存」把所有改动加进去。 Nothing is staged yet. Click "Stage" first to add all changes. - + 提交说明: Commit message: - + 添加到提交 Add to commit - + 「%1」已经在 .gitignore 里了 "%1" is already in .gitignore - + 忽略文件 Ignore File - + 打不开 .gitignore:%1 Cannot open .gitignore: %1 - + 已把 %1 写进 .gitignore Added %1 to .gitignore - - - + + + 删除远端 Delete from remote - + 「%1」没有被 git 跟踪,远端本来就没有它。 "%1" is not tracked by git, so the remote never had it. - + 从仓库删除「%1」并推送到远端? 本地文件会保留,远端和别人 pull 下来的仓库里会删掉它。 @@ -529,52 +529,52 @@ Your local file is kept, but the remote copy and everyone else's will be deleted. - + 删除并推送 Delete and push - + chore: 从仓库删除 %1 chore: remove %1 from repository - + 添加 Add - + git add 这个文件 git add this file - + 忽略 Ignore - + 把路径写进 .gitignore(不改仓库里已跟踪的文件) Add the path to .gitignore (does not untrack files already in the repo) - + git rm --cached 后提交并推送:远端删除,本地保留 git rm --cached, then commit and push: gone from the remote, kept locally - + 取消 Cancel - + 从仓库移除 Remove from repository - + 打开 Open @@ -582,23 +582,43 @@ Your local file is kept, but the remote copy and everyone else's will be deleted MarkdownEditor - - + + 已复制 Copied - - + + 复制 Copy - - + + 纯文本 Plain text + + + 插入行 + Insert row + + + + 删除行 + Delete row + + + + 插入列 + Insert column + + + + 删除列 + Delete column + QObject