diff --git a/README.md b/README.md index 836e4a9..307ccb1 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 一个轻量的跨平台 Markdown 编辑器,基于 **Qt 5 / C++17** 构建。 -当前版本 **0.8.0**。 +当前版本 **0.8.1**。 ## 界面 @@ -29,6 +29,8 @@ - 代码块按语言做语法高亮:C / C++ / Python / JavaScript / TypeScript / Java / Rust / Go / JSON / Bash / SQL / YAML,关键字、字符串、数字、 注释、预处理指令分色;不认识的标记语言就按等宽纯文本显示 +- 代码框**左上角**是语言下拉菜单,选一下就会改写围栏上的语言标记, + 高亮立刻跟着变(选「纯文本」则去掉语言) - `Tab` / `Shift+Tab` 整行缩进,选中多行时批量缩进 - 自动换行(`Alt+Z`)、`Ctrl+滚轮` 或 `Ctrl++` / `Ctrl+-` 缩放字号 - 撤销 / 重做、剪切 / 复制 / 粘贴,菜单项会跟随可用状态自动灰显 diff --git a/VoiletMark.pro b/VoiletMark.pro index c2bdc89..9e90cb7 100644 --- a/VoiletMark.pro +++ b/VoiletMark.pro @@ -1,6 +1,6 @@ TEMPLATE = app TARGET = VoiletMark -VERSION = 0.8.0 +VERSION = 0.8.1 QT += core gui widgets printsupport diff --git a/docs/screenshot-dark.png b/docs/screenshot-dark.png index 81fc04f..cb25fa1 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 c8bafd0..4ce7f18 100644 Binary files a/docs/screenshot-light.png and b/docs/screenshot-light.png differ diff --git a/src/CodeHighlight.cpp b/src/CodeHighlight.cpp index 08bd8ae..1851496 100644 --- a/src/CodeHighlight.cpp +++ b/src/CodeHighlight.cpp @@ -163,6 +163,34 @@ const LanguageDef *definition(Language language) } // namespace +const QVector &languageEntries() +{ + static const QVector entries = { + {Language::C, QStringLiteral("c"), QStringLiteral("C")}, + {Language::Cpp, QStringLiteral("cpp"), QStringLiteral("C++")}, + {Language::Python, QStringLiteral("python"), QStringLiteral("Python")}, + {Language::JavaScript, QStringLiteral("js"), QStringLiteral("JavaScript")}, + {Language::TypeScript, QStringLiteral("ts"), QStringLiteral("TypeScript")}, + {Language::Java, QStringLiteral("java"), QStringLiteral("Java")}, + {Language::Rust, QStringLiteral("rust"), QStringLiteral("Rust")}, + {Language::Go, QStringLiteral("go"), QStringLiteral("Go")}, + {Language::Json, QStringLiteral("json"), QStringLiteral("JSON")}, + {Language::Bash, QStringLiteral("bash"), QStringLiteral("Bash")}, + {Language::Sql, QStringLiteral("sql"), QStringLiteral("SQL")}, + {Language::Yaml, QStringLiteral("yaml"), QStringLiteral("YAML")}, + }; + return entries; +} + +QString displayName(Language language) +{ + for (const LanguageEntry &entry : languageEntries()) { + if (entry.language == language) + return entry.name; + } + return QString(); +} + Language fromInfoString(const QString &info) { QString name = info.trimmed().toLower(); diff --git a/src/CodeHighlight.h b/src/CodeHighlight.h index c7f0272..d1e5834 100644 --- a/src/CodeHighlight.h +++ b/src/CodeHighlight.h @@ -34,6 +34,17 @@ struct Span { Token token = Token::Keyword; }; +struct LanguageEntry { + Language language; + QString info; // 写进围栏里的标记,例如 cpp + QString name; // 显示名,例如 C++ +}; + +/// 供语言下拉菜单使用(顺序即菜单顺序)。 +const QVector &languageEntries(); + +QString displayName(Language language); + /// 从 ```cpp 这类 info string 里识别语言(大小写不敏感)。 Language fromInfoString(const QString &info); diff --git a/src/MarkdownEditor.cpp b/src/MarkdownEditor.cpp index d4dd88c..8d7aa7b 100644 --- a/src/MarkdownEditor.cpp +++ b/src/MarkdownEditor.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -56,6 +57,14 @@ bool isFenceLine(const QTextBlock &block) return block.isValid() && block.text().trimmed().startsWith(fenceMarker()); } +QString fenceInfo(const QString &line) +{ + QString info = line.trimmed(); + static const QRegularExpression marker(QStringLiteral("^(?:```+|~~~+)")); + info.remove(marker); + return info.trimmed(); +} + class LineNumberArea : public QWidget { public: @@ -519,10 +528,23 @@ void MarkdownEditor::paintEvent(QPaintEvent *event) return; } - // 编辑模式:代码块套一个圆角框,右上角给个复制按钮。 + // 编辑模式:代码块套圆角框,左上角选语言,右上角复制。 const ThemeColors &colors = ThemeManager::instance()->colors(); const QRectF visible(event->rect()); + auto drawBadge = [&](const QRectF &badge, const QString &label, bool hovered, bool emphasized) { + if (!badge.isValid()) + return; + QFont small = font(); + small.setPointSizeF(qMax(7.0, font().pointSizeF() - 2.0)); + painter.setFont(small); + painter.setPen(QPen(hovered ? colors.accent : colors.border, 1)); + painter.setBrush(colors.editorBase); + painter.drawRoundedRect(badge, 4, 4); + painter.setPen(emphasized ? colors.accent : colors.placeholder); + painter.drawText(badge, Qt::AlignCenter, label); + }; + for (int index = 0; index < m_codeFences.size(); ++index) { const QRectF rect = codeFenceRect(index); if (!rect.isValid() || !rect.intersects(visible)) @@ -532,21 +554,12 @@ void MarkdownEditor::paintEvent(QPaintEvent *event) painter.setPen(QPen(colors.border, 1)); painter.drawRoundedRect(rect.adjusted(0.5, 0.5, -0.5, -0.5), 6, 6); - const QRectF button = copyButtonRect(index); - if (!button.isValid()) - continue; + drawBadge(languageRect(index), languageLabel(index) + QStringLiteral(" \u25be"), + m_hoveredLanguage == index, false); - QFont small = font(); - small.setPointSizeF(qMax(7.0, font().pointSizeF() - 2.0)); - painter.setFont(small); - - const bool hovered = (m_hoveredFence == index); const bool copied = (m_copiedFence == index); - painter.setPen(QPen(hovered ? colors.accent : colors.border, 1)); - painter.setBrush(colors.editorBase); - painter.drawRoundedRect(button, 4, 4); - painter.setPen(copied ? colors.accent : colors.placeholder); - painter.drawText(button, Qt::AlignCenter, copied ? tr("已复制") : tr("复制")); + drawBadge(copyButtonRect(index), copied ? tr("已复制") : tr("复制"), + m_hoveredFence == index, copied); } } @@ -569,6 +582,87 @@ QRectF MarkdownEditor::codeFenceRect(int index) const return QRectF(3, top, viewport()->width() - 6, bottom - top); } +QRectF MarkdownEditor::languageRect(int index) const +{ + const QRectF rect = codeFenceRect(index); + if (!rect.isValid()) + return QRectF(); + + QFont small = font(); + small.setPointSizeF(qMax(7.0, font().pointSizeF() - 2.0)); + const QFontMetricsF metrics(small); + const QString label = languageLabel(index) + QStringLiteral(" \u25be"); + const qreal width = metrics.horizontalAdvance(label) + 16; + const qreal height = metrics.height() + 6; + return QRectF(rect.left() + 8, rect.top() + 6, width, height); +} + +QString MarkdownEditor::languageLabel(int index) const +{ + if (index < 0 || index >= m_codeFences.size()) + return QString(); + const CodeHighlight::Language language = m_codeFences.at(index).language; + if (language == CodeHighlight::Language::None) + return tr("纯文本"); + return CodeHighlight::displayName(language); +} + +void MarkdownEditor::showLanguageMenu(int index) +{ + if (index < 0 || index >= m_codeFences.size()) + return; + + QMenu menu(this); + + QAction *plain = menu.addAction(tr("纯文本")); + plain->setData(QString()); + plain->setCheckable(true); + plain->setChecked(m_codeFences.at(index).language == CodeHighlight::Language::None); + menu.addSeparator(); + + for (const CodeHighlight::LanguageEntry &entry : CodeHighlight::languageEntries()) { + QAction *action = menu.addAction(entry.name); + action->setData(entry.info); + action->setCheckable(true); + action->setChecked(entry.language == m_codeFences.at(index).language); + } + + const QRectF badge = languageRect(index); + const QPoint origin = viewport()->mapToGlobal(badge.bottomLeft().toPoint()); + QAction *chosen = menu.exec(origin); + if (!chosen) + return; + + setCodeFenceLanguage(index, chosen->data().toString()); +} + +void MarkdownEditor::setCodeFenceLanguage(int index, const QString &info) +{ + if (index < 0 || index >= m_codeFences.size()) + return; + + const int blockNumber = m_codeFences.at(index).open; + QTextBlock block = document()->findBlockByNumber(blockNumber); + if (!block.isValid()) + return; + + static const QRegularExpression marker(QStringLiteral("^\\s{0,3}(?:```+|~~~+)")); + const QRegularExpressionMatch match = marker.match(block.text()); + if (!match.hasMatch()) + return; + + const QString newText = match.captured(0) + info; + if (newText == block.text()) + return; + + QTextCursor cursor(block); + cursor.beginEditBlock(); + cursor.setPosition(block.position()); + cursor.setPosition(block.position() + block.text().length(), QTextCursor::KeepAnchor); + cursor.insertText(newText); + cursor.endEditBlock(); +} + QRectF MarkdownEditor::copyButtonRect(int index) const { const QRectF rect = codeFenceRect(index); @@ -606,6 +700,7 @@ void MarkdownEditor::rebuildCodeFences() static const QRegularExpression fencePattern(QStringLiteral("^\\s{0,3}(?:```|~~~)")); bool inFence = false; int open = -1; + QString openInfo; for (QTextBlock block = document()->firstBlock(); block.isValid(); block = block.next()) { if (!fencePattern.match(block.text()).hasMatch()) @@ -613,10 +708,13 @@ void MarkdownEditor::rebuildCodeFences() if (!inFence) { inFence = true; open = block.blockNumber(); + openInfo = fenceInfo(block.text()); } else { CodeFence fence; fence.open = open; fence.close = block.blockNumber(); + fence.info = openInfo; + fence.language = CodeHighlight::fromInfoString(openInfo); m_codeFences.append(fence); inFence = false; open = -1; @@ -625,6 +723,8 @@ void MarkdownEditor::rebuildCodeFences() if (inFence && open >= 0) { CodeFence fence; fence.open = open; + fence.info = openInfo; + fence.language = CodeHighlight::fromInfoString(openInfo); m_codeFences.append(fence); } @@ -637,17 +737,25 @@ void MarkdownEditor::mouseMoveEvent(QMouseEvent *event) if (m_sourceMode) return; - int hovered = -1; + int hoveredCopy = -1; + int hoveredLanguage = -1; for (int index = 0; index < m_codeFences.size(); ++index) { + if (languageRect(index).contains(event->pos())) { + hoveredLanguage = index; + break; + } if (copyButtonRect(index).contains(event->pos())) { - hovered = index; + hoveredCopy = index; break; } } - if (hovered != m_hoveredFence) { - m_hoveredFence = hovered; - viewport()->setCursor(hovered >= 0 ? Qt::PointingHandCursor : Qt::IBeamCursor); + if (hoveredCopy != m_hoveredFence || hoveredLanguage != m_hoveredLanguage) { + m_hoveredFence = hoveredCopy; + m_hoveredLanguage = hoveredLanguage; + viewport()->setCursor((hoveredCopy >= 0 || hoveredLanguage >= 0) + ? Qt::PointingHandCursor + : Qt::IBeamCursor); viewport()->update(); } } @@ -655,6 +763,12 @@ void MarkdownEditor::mouseMoveEvent(QMouseEvent *event) void MarkdownEditor::mousePressEvent(QMouseEvent *event) { if (!m_sourceMode && event->button() == Qt::LeftButton) { + for (int index = 0; index < m_codeFences.size(); ++index) { + if (languageRect(index).contains(event->pos())) { + showLanguageMenu(index); + return; + } + } for (int index = 0; index < m_codeFences.size(); ++index) { if (!copyButtonRect(index).contains(event->pos())) continue; diff --git a/src/MarkdownEditor.h b/src/MarkdownEditor.h index aa2952c..a28cd20 100644 --- a/src/MarkdownEditor.h +++ b/src/MarkdownEditor.h @@ -1,5 +1,7 @@ #pragma once +#include "CodeHighlight.h" + #include #include @@ -66,7 +68,11 @@ private: void indentSelection(bool outdent); QRectF codeFenceRect(int index) const; QRectF copyButtonRect(int index) const; + QRectF languageRect(int index) const; QString codeFenceText(int index) const; + QString languageLabel(int index) const; + void showLanguageMenu(int index); + void setCodeFenceLanguage(int index, const QString &info); bool acceptsLocalFiles(const QMimeData *mime) const; void selectedBlockRange(int *firstBlock, int *lastBlock) const; void selectBlocks(int firstBlock, int lastBlock); @@ -76,6 +82,8 @@ private: struct CodeFence { int open = -1; // 开围栏所在行 int close = -1; // 闭围栏所在行,未闭合时为 -1 + QString info; // 围栏上的语言标记 + CodeHighlight::Language language = CodeHighlight::Language::None; }; QWidget *m_lineNumberArea = nullptr; @@ -87,5 +95,6 @@ private: QVector m_codeFences; QTimer *m_fenceTimer = nullptr; int m_hoveredFence = -1; + int m_hoveredLanguage = -1; int m_copiedFence = -1; }; diff --git a/translations/voiletmark_en.qm b/translations/voiletmark_en.qm index c848676..1e47e45 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 36a42bb..93de462 100644 --- a/translations/voiletmark_en.ts +++ b/translations/voiletmark_en.ts @@ -372,37 +372,43 @@ MarkdownEditor - - + + 已复制 Copied - - + + 复制 Copy + + + + 纯文本 + Plain text + QObject - + 浅色 Light - + 深色 Dark - + 按时段自动 Scheduled - + 跟随系统 Follow System