diff --git a/README.md b/README.md index 948a8ef..65a492f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 一个轻量的跨平台 Markdown 编辑器,基于 **Qt 5 / C++17** 构建。 -当前版本 **0.10.5**。 +当前版本 **0.11.0**。 ## 界面 @@ -43,6 +43,9 @@ - **文件 → 打开文件夹…**(`Ctrl+Shift+O`)选一个目录,递归扫描 `*.md` / `*.markdown` / `*.mdown` / `*.mkd` +- 侧栏顶部两个页签:**文件**(默认)/ **目录** +- **目录**页把当前文档的 1–6 级标题列成大纲(代码块里的 `#` 不算), + 按层级缩进,点一下跳到对应行;编辑时自动刷新 - 状态栏左下角的「侧栏」按钮或 `Ctrl+B` 显示 / 隐藏 - 点文件名即打开,当前文件在列表里高亮 - 每 5 秒自动扫描一次:目录里新增 / 删除的 Markdown 会自动出现在列表里 diff --git a/VoiletMark.pro b/VoiletMark.pro index 6a88d0c..f73dc4a 100644 --- a/VoiletMark.pro +++ b/VoiletMark.pro @@ -1,6 +1,6 @@ TEMPLATE = app TARGET = VoiletMark -VERSION = 0.10.6 +VERSION = 0.11.0 QT += core gui widgets diff --git a/docs/screenshot-dark.png b/docs/screenshot-dark.png index 80f2d2e..aa5ac32 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 b6ea274..a5af041 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 cecb0b9..8fc9592 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 2213903..871d44d 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -322,17 +323,65 @@ void MainWindow::createSidebar() layout->setContentsMargins(8, 8, 8, 8); layout->setSpacing(6); + // 侧栏两种模式:文件 / 目录 + QHBoxLayout *modeLayout = new QHBoxLayout(); + modeLayout->setContentsMargins(0, 0, 0, 0); + modeLayout->setSpacing(4); + + auto makeModeButton = [this, content](const QString &text) { + QToolButton *button = new QToolButton(content); + button->setText(text); + button->setCheckable(true); + button->setCursor(Qt::PointingHandCursor); + button->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + return button; + }; + + m_fileModeButton = makeModeButton(tr("文件")); + m_tocModeButton = makeModeButton(tr("目录")); + m_fileModeButton->setChecked(true); + modeLayout->addWidget(m_fileModeButton); + modeLayout->addWidget(m_tocModeButton); + layout->addLayout(modeLayout); + + connect(m_fileModeButton, &QToolButton::clicked, this, [this] { switchSidebarMode(0); }); + connect(m_tocModeButton, &QToolButton::clicked, this, [this] { switchSidebarMode(1); }); + m_folderLabel = new QLabel(tr("未打开文件夹"), content); m_folderLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred); layout->addWidget(m_folderLabel); - m_fileList = new QListWidget(content); + m_sidebarStack = new QStackedWidget(content); + + m_fileList = new QListWidget(m_sidebarStack); m_fileList->setFrameShape(QFrame::NoFrame); m_fileList->setUniformItemSizes(true); m_fileList->setTextElideMode(Qt::ElideMiddle); m_fileList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_fileList->setContextMenuPolicy(Qt::CustomContextMenu); - layout->addWidget(m_fileList, 1); + m_sidebarStack->addWidget(m_fileList); + + m_tocList = new QListWidget(m_sidebarStack); + m_tocList->setFrameShape(QFrame::NoFrame); + m_tocList->setTextElideMode(Qt::ElideRight); + m_tocList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_sidebarStack->addWidget(m_tocList); + + layout->addWidget(m_sidebarStack, 1); + + connect(m_tocList, &QListWidget::itemClicked, this, &MainWindow::goToTocItem); + + m_tocTimer = new QTimer(this); + m_tocTimer->setSingleShot(true); + m_tocTimer->setInterval(250); + connect(m_tocTimer, &QTimer::timeout, this, [this] { + if (m_sidebarMode == 1) + rebuildToc(); + }); + connect(m_editor, &QPlainTextEdit::textChanged, this, [this] { + if (m_sidebarMode == 1) + m_tocTimer->start(); + }); // ---- Git 工具条:只有打开的是仓库时才显示 ---- m_gitBar = new QWidget(content); @@ -532,6 +581,10 @@ void MainWindow::loadSettings() m_themeDarkAction->setChecked(mode == ThemeManager::Mode::Dark); m_themeScheduleAction->setChecked(mode == ThemeManager::Mode::Schedule); + const int sidebarMode = + settings.value(QStringLiteral("window/sidebarMode"), 0).toInt(); + switchSidebarMode(sidebarMode); + m_folderPath = settings.value(QStringLiteral("files/folder")).toString(); if (!m_folderPath.isEmpty() && QFileInfo(m_folderPath).isDir()) refreshFolder(); @@ -1223,7 +1276,7 @@ void MainWindow::updateGitState() m_git->setWorkingDirectory(m_gitRoot.isEmpty() ? m_folderPath : m_gitRoot); if (m_gitBar) { - m_gitBar->setVisible(!m_gitRoot.isEmpty()); + m_gitBar->setVisible(!m_gitRoot.isEmpty() && m_sidebarMode == 0); m_gitStatusLabel->setText(m_gitBranch.isEmpty() ? tr("Git 仓库") : m_gitBranch); m_gitStatusLabel->setToolTip(m_gitRoot); } @@ -1285,6 +1338,89 @@ void MainWindow::onGitFinished(bool ok, const QString &label, const QString &out } } +void MainWindow::switchSidebarMode(int mode) +{ + m_sidebarMode = qBound(0, mode, 1); + + if (m_sidebarStack) + m_sidebarStack->setCurrentIndex(m_sidebarMode); + if (m_fileModeButton && m_tocModeButton) { + m_fileModeButton->setChecked(m_sidebarMode == 0); + m_tocModeButton->setChecked(m_sidebarMode == 1); + } + if (m_folderLabel) + m_folderLabel->setVisible(m_sidebarMode == 0); + if (m_gitBar) + m_gitBar->setVisible(m_sidebarMode == 0 && !m_gitRoot.isEmpty()); + + if (m_sidebarMode == 1) + rebuildToc(); + + QSettings().setValue(QStringLiteral("window/sidebarMode"), m_sidebarMode); +} + +void MainWindow::rebuildToc() +{ + if (!m_tocList || !m_editor) + return; + + m_tocList->clear(); + + static const QRegularExpression headingPattern( + QStringLiteral(R"(^\s{0,3}(#{1,6})\s+(.+?)\s*#*\s*$)")); + static const QRegularExpression fencePattern( + QStringLiteral("^\\s{0,3}(?:\\x60\\x60\\x60|~~~)")); + + bool inFence = false; + QTextBlock block = m_editor->document()->firstBlock(); + while (block.isValid()) { + const QString text = block.text(); + + if (fencePattern.match(text).hasMatch()) { + inFence = !inFence; + block = block.next(); + continue; + } + + if (!inFence) { + const QRegularExpressionMatch match = headingPattern.match(text); + if (match.hasMatch()) { + const int level = match.capturedLength(1); + QListWidgetItem *item = new QListWidgetItem( + QString(level - 1, QChar(0x2003)) + match.captured(2).trimmed(), m_tocList); + item->setData(Qt::UserRole, block.blockNumber()); + item->setToolTip(match.captured(2).trimmed()); + if (level <= 2) { + QFont font = item->font(); + font.setBold(true); + item->setFont(font); + } else { + QFont font = item->font(); + font.setPointSizeF(qMax(7.0, font.pointSizeF() - 0.5)); + item->setFont(font); + } + } + } + + block = block.next(); + } + + if (m_tocList->count() == 0) { + QListWidgetItem *empty = new QListWidgetItem(tr("(没有标题)"), m_tocList); + empty->setFlags(Qt::NoItemFlags); + } +} + +void MainWindow::goToTocItem(QListWidgetItem *item) +{ + if (!item) + return; + const QVariant data = item->data(Qt::UserRole); + if (!data.isValid()) + return; + m_editor->goToBlock(data.toInt()); +} + void MainWindow::gitPull() { if (m_gitRoot.isEmpty()) diff --git a/src/MainWindow.h b/src/MainWindow.h index 0b2c75d..2dc2df2 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -9,6 +9,8 @@ class QActionGroup; class QDockWidget; class QLabel; class QPushButton; +class QStackedWidget; +class QToolButton; class GitRunner; class QListWidget; class QListWidgetItem; @@ -69,6 +71,10 @@ private: void gitIgnoreFile(const QString &path); void gitRemoveRemote(const QString &path); void gitCommit(); + + void switchSidebarMode(int mode); + void rebuildToc(); + void goToTocItem(QListWidgetItem *item); void gitPull(); void gitFetch(); void gitPush(); @@ -102,6 +108,13 @@ private: QToolButton *m_sourceButton = nullptr; QToolButton *m_sidebarButton = nullptr; + QStackedWidget *m_sidebarStack = nullptr; + QToolButton *m_fileModeButton = nullptr; + QToolButton *m_tocModeButton = nullptr; + QListWidget *m_tocList = nullptr; + QTimer *m_tocTimer = nullptr; + int m_sidebarMode = 0; + GitRunner *m_git = nullptr; QWidget *m_gitBar = nullptr; QLabel *m_gitStatusLabel = nullptr; diff --git a/src/MarkdownEditor.cpp b/src/MarkdownEditor.cpp index 42d0c24..2b5adbc 100644 --- a/src/MarkdownEditor.cpp +++ b/src/MarkdownEditor.cpp @@ -367,6 +367,19 @@ void MarkdownEditor::selectBlocks(int firstBlock, int lastBlock) setTextCursor(cursor); } +void MarkdownEditor::goToBlock(int blockNumber) +{ + const QTextBlock block = document()->findBlockByNumber(blockNumber); + if (!block.isValid()) + return; + + QTextCursor cursor(block); + cursor.movePosition(QTextCursor::StartOfBlock); + setTextCursor(cursor); + centerCursor(); + setFocus(); +} + void MarkdownEditor::setHeadingLevel(int level) { level = qBound(0, level, 6); diff --git a/src/MarkdownEditor.h b/src/MarkdownEditor.h index 91cba08..e3b1cf3 100644 --- a/src/MarkdownEditor.h +++ b/src/MarkdownEditor.h @@ -39,6 +39,9 @@ public: /// 把选中内容包成围栏代码块;如果已经围起来了则拆掉。 void toggleCodeBlock(); + /// 把光标移到指定行并居中显示(目录跳转用)。 + void goToBlock(int blockNumber); + signals: void zoomStepRequested(int direction); void fileDropped(const QString &path); diff --git a/translations/voiletmark_en.qm b/translations/voiletmark_en.qm index 883921a..12a7f8f 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 745523b..373e5c3 100644 --- a/translations/voiletmark_en.ts +++ b/translations/voiletmark_en.ts @@ -1,10 +1,9 @@ - - + GitRunner - + (超时,已中止) @@ -14,447 +13,464 @@ 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 - - + + 目录 + Outline + + + + 最近打开 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 + 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) - + + 正在导出 PDF… + Exporting PDF… + + + + 导出失败 Export Failed - - 没能写出「%1」,换个位置再试一次。 - Could not write "%1". Try another location. + + 文档排版结果异常,已中止导出。 + The document layout looks broken; export was aborted. - + + 没能写出「%1」,换个位置再试一次。 + Could not write "%1". Try another location. + + + 已导出 %1 Exported to %1 - - - + + + 无法保存文件 Cannot Save File - + 写入「%1」失败:%2 - Could not write "%1": %2 + Could not write "%1": %2 - + 写入「%1」时发生错误。 - An error occurred while writing "%1". + An error occurred while writing "%1". - + 提交「%1」失败:%2 - Could not commit "%1": %2 + Could not commit "%1": %2 - + 已保存 %1 Saved to %1 - + VoiletMark VoiletMark - + 「%1」有未保存的修改,是否保存? - "%1" has unsaved changes. Save them? + "%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,119 +479,124 @@ %2 - + (没有输出) (no output) - + %1完成 %1 finished - - 暂存区是空的,先点「暂存」把所有改动加进去。 - Nothing is staged yet. Click "Stage" first to add all changes. + + (没有标题) + (no headings) - + + 暂存区是空的,先点「暂存」把所有改动加进去。 + Nothing is staged yet. Click "Stage" first to add all changes. + + + 提交说明: Commit message: - + 添加到提交 Add to commit - + 「%1」已经在 .gitignore 里了 - "%1" is already in .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" is not tracked by git, so the remote never had it. - + 从仓库删除「%1」并推送到远端? 本地文件会保留,远端和别人 pull 下来的仓库里会删掉它。 - Remove "%1" from the repository and push? + Remove "%1" from the repository and push? -Your local file is kept, but the remote copy and everyone else's will be deleted. +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 @@ -583,40 +604,40 @@ Your local file is kept, but the remote copy and everyone else's will be de MarkdownEditor - - + + 已复制 Copied - - + + 复制 Copy - - + + 纯文本 Plain text - + 插入行 Insert row - + 删除行 Delete row - + 插入列 Insert column - + 删除列 Delete column @@ -624,24 +645,24 @@ Your local file is kept, but the remote copy and everyone else's will be de QObject - + 浅色 Light - + 深色 Dark - + 按时段自动 Scheduled - + 跟随系统 Follow System - + \ No newline at end of file