feat: v0.8.1 —— 代码框左上角加语言下拉菜单
- 编辑模式下每个代码框左上角显示当前语言(纯文本 / C++ / Python …), 点开是一个带勾选标记的语言菜单 - 选中后改写围栏上的语言标记(例如 ```cpp → ```python), 高亮立即跟着变;选「纯文本」则去掉语言标记 - 下拉与复制按钮都是画在编辑器视口里的,跟着滚动重算,源码模式下都不显示 - 「纯文本」加入中英文翻译 - 截图与 README 同步
This commit is contained in:
@@ -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+-` 缩放字号
|
||||
- 撤销 / 重做、剪切 / 复制 / 粘贴,菜单项会跟随可用状态自动灰显
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
TEMPLATE = app
|
||||
TARGET = VoiletMark
|
||||
VERSION = 0.8.0
|
||||
VERSION = 0.8.1
|
||||
|
||||
QT += core gui widgets printsupport
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
@@ -163,6 +163,34 @@ const LanguageDef *definition(Language language)
|
||||
|
||||
} // namespace
|
||||
|
||||
const QVector<LanguageEntry> &languageEntries()
|
||||
{
|
||||
static const QVector<LanguageEntry> 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();
|
||||
|
||||
@@ -34,6 +34,17 @@ struct Span {
|
||||
Token token = Token::Keyword;
|
||||
};
|
||||
|
||||
struct LanguageEntry {
|
||||
Language language;
|
||||
QString info; // 写进围栏里的标记,例如 cpp
|
||||
QString name; // 显示名,例如 C++
|
||||
};
|
||||
|
||||
/// 供语言下拉菜单使用(顺序即菜单顺序)。
|
||||
const QVector<LanguageEntry> &languageEntries();
|
||||
|
||||
QString displayName(Language language);
|
||||
|
||||
/// 从 ```cpp 这类 info string 里识别语言(大小写不敏感)。
|
||||
Language fromInfoString(const QString &info);
|
||||
|
||||
|
||||
+133
-19
@@ -12,6 +12,7 @@
|
||||
#include <QFontMetricsF>
|
||||
#include <QGuiApplication>
|
||||
#include <QKeyEvent>
|
||||
#include <QMenu>
|
||||
#include <QMouseEvent>
|
||||
#include <QMimeData>
|
||||
#include <QPainter>
|
||||
@@ -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;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "CodeHighlight.h"
|
||||
|
||||
#include <QPlainTextEdit>
|
||||
#include <QVector>
|
||||
|
||||
@@ -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<CodeFence> m_codeFences;
|
||||
QTimer *m_fenceTimer = nullptr;
|
||||
int m_hoveredFence = -1;
|
||||
int m_hoveredLanguage = -1;
|
||||
int m_copiedFence = -1;
|
||||
};
|
||||
|
||||
Binary file not shown.
@@ -372,37 +372,43 @@
|
||||
<context>
|
||||
<name>MarkdownEditor</name>
|
||||
<message>
|
||||
<location filename="../src/MarkdownEditor.cpp" line="549" />
|
||||
<location filename="../src/MarkdownEditor.cpp" line="581" />
|
||||
<location filename="../src/MarkdownEditor.cpp" line="561" />
|
||||
<location filename="../src/MarkdownEditor.cpp" line="675" />
|
||||
<source>已复制</source>
|
||||
<translation>Copied</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/MarkdownEditor.cpp" line="549" />
|
||||
<location filename="../src/MarkdownEditor.cpp" line="581" />
|
||||
<location filename="../src/MarkdownEditor.cpp" line="561" />
|
||||
<location filename="../src/MarkdownEditor.cpp" line="675" />
|
||||
<source>复制</source>
|
||||
<translation>Copy</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/MarkdownEditor.cpp" line="606" />
|
||||
<location filename="../src/MarkdownEditor.cpp" line="617" />
|
||||
<source>纯文本</source>
|
||||
<translation>Plain text</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>QObject</name>
|
||||
<message>
|
||||
<location filename="../src/ThemeManager.cpp" line="354" />
|
||||
<location filename="../src/ThemeManager.cpp" line="364" />
|
||||
<source>浅色</source>
|
||||
<translation>Light</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/ThemeManager.cpp" line="355" />
|
||||
<location filename="../src/ThemeManager.cpp" line="365" />
|
||||
<source>深色</source>
|
||||
<translation>Dark</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/ThemeManager.cpp" line="356" />
|
||||
<location filename="../src/ThemeManager.cpp" line="366" />
|
||||
<source>按时段自动</source>
|
||||
<translation>Scheduled</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/ThemeManager.cpp" line="359" />
|
||||
<location filename="../src/ThemeManager.cpp" line="369" />
|
||||
<source>跟随系统</source>
|
||||
<translation>Follow System</translation>
|
||||
</message>
|
||||
|
||||
Reference in New Issue
Block a user