feat: v0.6.0 —— 选中语言(中/英)即时切换,导出不再有预览窗口

导出
- 去掉导出预览对话框:Ctrl+E 直接弹保存对话框,选路径即导出
- 导出失败才弹提示,成功只在状态栏显示一行

多语言
- 新增 LanguageManager:中文(zh_CN)/ 英文(en),视图 → 语言 切换,
  立即生效并记住选择
- 英文翻译由 translations/voiletmark_en.ts(69 条)编译成 .qm 内嵌
- 打包 Qt 官方的 qtbase_zh_CN.qm,文件对话框 / 消息框的按钮也跟着变中文
- 欢迎文档随语言切换(新增英文版)
- retranslateUi 负责菜单、动作、状态栏、侧栏文案的即时刷新

其它
- 删除 ExportDialog 与对应截图,README 同步
This commit is contained in:
2026-10-22 18:07:31 +08:00
parent 2f8726ad47
commit 5d1fb0fab7
14 changed files with 766 additions and 189 deletions
+216 -42
View File
@@ -1,7 +1,8 @@
#include "MainWindow.h"
#include "ExportDialog.h"
#include "LanguageManager.h"
#include "MarkdownEditor.h"
#include "MarkdownRenderer.h"
#include "ThemeManager.h"
#include <QAction>
@@ -21,7 +22,10 @@
#include <QMenu>
#include <QMenuBar>
#include <QMessageBox>
#include <QPageLayout>
#include <QPageSize>
#include <QPixmap>
#include <QPrinter>
#include <QSaveFile>
#include <QSettings>
#include <QSignalBlocker>
@@ -83,6 +87,49 @@ int main()
---
)VOILETMARK";
const char kWelcomeTextEn[] = R"VOILETMARK(
# Welcome to VoiletMark
A lightweight cross-platform Markdown editor built with **Qt 5** and C++.
## Quick start
- `Ctrl+O` opens a file, `Ctrl+S` saves, `Ctrl+Shift+S` saves as
- Drag a Markdown file into the window to open it
- `Ctrl+1` – `Ctrl+6` set heading levels, `Ctrl+/` wraps a code block
- `Ctrl+E` exports the document to PDF (A4)
## Modes
The default view is the editor: Markdown markers are hidden, headings are sized,
and the line under the cursor reveals its raw markers.
Click `<src>` in the status bar to see the source with line numbers.
## Theme
Follows the system light / dark setting by default;
switch it under **View → Theme**.
> Quotes, lists, `inline code`, **bold**, *italic*, ~~strikethrough~~
> and [links](https://example.com) all render in place.
- [x] Syntax highlighting
- [x] Folder sidebar
- [x] PDF export
```cpp
#include <cstdio>
int main()
{
std::printf("Hello, VoiletMark!\n");
return 0;
}
```
---
)VOILETMARK";
} // namespace
MainWindow::MainWindow(QWidget *parent)
@@ -109,6 +156,8 @@ MainWindow::MainWindow(QWidget *parent)
this, &MainWindow::changeZoom);
connect(m_editor, &MarkdownEditor::fileDropped,
this, &MainWindow::openPath);
connect(LanguageManager::instance(), &LanguageManager::languageChanged,
this, &MainWindow::retranslateUi);
showWelcomeDocument();
updateEditActionStates();
@@ -266,40 +315,40 @@ void MainWindow::createSidebar()
void MainWindow::createMenus()
{
QMenu *fileMenu = menuBar()->addMenu(tr("文件"));
fileMenu->addAction(m_newAction);
fileMenu->addAction(m_openAction);
fileMenu->addAction(m_openFolderAction);
m_recentMenu = fileMenu->addMenu(tr("最近打开"));
fileMenu->addSeparator();
fileMenu->addAction(m_saveAction);
fileMenu->addAction(m_saveAsAction);
fileMenu->addAction(m_exportPdfAction);
fileMenu->addSeparator();
fileMenu->addAction(m_quitAction);
m_fileMenu = menuBar()->addMenu(tr("文件"));
m_fileMenu->addAction(m_newAction);
m_fileMenu->addAction(m_openAction);
m_fileMenu->addAction(m_openFolderAction);
m_recentMenu = m_fileMenu->addMenu(tr("最近打开"));
m_fileMenu->addSeparator();
m_fileMenu->addAction(m_saveAction);
m_fileMenu->addAction(m_saveAsAction);
m_fileMenu->addAction(m_exportPdfAction);
m_fileMenu->addSeparator();
m_fileMenu->addAction(m_quitAction);
QMenu *editMenu = menuBar()->addMenu(tr("编辑"));
editMenu->addAction(m_undoAction);
editMenu->addAction(m_redoAction);
editMenu->addSeparator();
editMenu->addAction(m_cutAction);
editMenu->addAction(m_copyAction);
editMenu->addAction(m_pasteAction);
editMenu->addSeparator();
editMenu->addAction(m_selectAllAction);
m_editMenu = menuBar()->addMenu(tr("编辑"));
m_editMenu->addAction(m_undoAction);
m_editMenu->addAction(m_redoAction);
m_editMenu->addSeparator();
m_editMenu->addAction(m_cutAction);
m_editMenu->addAction(m_copyAction);
m_editMenu->addAction(m_pasteAction);
m_editMenu->addSeparator();
m_editMenu->addAction(m_selectAllAction);
QMenu *viewMenu = menuBar()->addMenu(tr("视图"));
viewMenu->addAction(m_sidebarAction);
viewMenu->addAction(m_sourceModeAction);
viewMenu->addSeparator();
viewMenu->addAction(m_wrapAction);
viewMenu->addSeparator();
viewMenu->addAction(m_zoomInAction);
viewMenu->addAction(m_zoomOutAction);
viewMenu->addAction(m_zoomResetAction);
viewMenu->addSeparator();
m_viewMenu = menuBar()->addMenu(tr("视图"));
m_viewMenu->addAction(m_sidebarAction);
m_viewMenu->addAction(m_sourceModeAction);
m_viewMenu->addSeparator();
m_viewMenu->addAction(m_wrapAction);
m_viewMenu->addSeparator();
m_viewMenu->addAction(m_zoomInAction);
m_viewMenu->addAction(m_zoomOutAction);
m_viewMenu->addAction(m_zoomResetAction);
m_viewMenu->addSeparator();
QMenu *themeMenu = viewMenu->addMenu(tr("主题"));
m_themeMenu = m_viewMenu->addMenu(tr("主题"));
QActionGroup *themeGroup = new QActionGroup(this);
themeGroup->setExclusive(true);
@@ -312,7 +361,7 @@ void MainWindow::createMenus()
m_themeScheduleAction}) {
action->setCheckable(true);
themeGroup->addAction(action);
themeMenu->addAction(action);
m_themeMenu->addAction(action);
}
m_themeSystemAction->setChecked(true);
@@ -325,16 +374,33 @@ void MainWindow::createMenus()
connect(m_themeScheduleAction, &QAction::triggered, this,
[this] { applyThemeMode(static_cast<int>(ThemeManager::Mode::Schedule)); });
QMenu *formatMenu = menuBar()->addMenu(tr("格式"));
QMenu *headingMenu = formatMenu->addMenu(tr("标题"));
m_languageMenu = m_viewMenu->addMenu(tr("语言"));
QActionGroup *languageGroup = new QActionGroup(this);
languageGroup->setExclusive(true);
m_languageChineseAction =
new QAction(LanguageManager::displayName(QStringLiteral("zh_CN")), this);
m_languageEnglishAction =
new QAction(LanguageManager::displayName(QStringLiteral("en")), this);
for (QAction *action : {m_languageChineseAction, m_languageEnglishAction}) {
action->setCheckable(true);
languageGroup->addAction(action);
m_languageMenu->addAction(action);
}
connect(m_languageChineseAction, &QAction::triggered, this,
[this] { applyLanguage(QStringLiteral("zh_CN")); });
connect(m_languageEnglishAction, &QAction::triggered, this,
[this] { applyLanguage(QStringLiteral("en")); });
m_formatMenu = menuBar()->addMenu(tr("格式"));
m_headingMenu = m_formatMenu->addMenu(tr("标题"));
for (QAction *headingAction : m_headingActions) {
if (headingAction)
headingMenu->addAction(headingAction);
m_headingMenu->addAction(headingAction);
}
formatMenu->addAction(m_codeBlockAction);
m_formatMenu->addAction(m_codeBlockAction);
QMenu *helpMenu = menuBar()->addMenu(tr("帮助"));
helpMenu->addAction(tr("关于 VoiletMark"), this, &MainWindow::showAbout);
m_helpMenu = menuBar()->addMenu(tr("帮助"));
m_aboutAction = m_helpMenu->addAction(tr("关于 VoiletMark"), this, &MainWindow::showAbout);
}
void MainWindow::createStatusBar()
@@ -446,9 +512,11 @@ QString MainWindow::startDirectory() const
void MainWindow::showWelcomeDocument()
{
QString text = QString::fromUtf8(kWelcomeText);
const bool english = LanguageManager::instance()->language() == QLatin1String("en");
QString text = QString::fromUtf8(english ? kWelcomeTextEn : kWelcomeText);
if (text.startsWith(QLatin1Char('\n')))
text.remove(0, 1);
m_welcomeText = text;
m_editor->setPlainText(text);
m_editor->applyBlockRhythm();
m_editor->document()->setModified(false);
@@ -544,8 +612,37 @@ bool MainWindow::saveFileAs()
void MainWindow::exportPdf()
{
ExportDialog dialog(m_editor->toPlainText(), displayName(), startDirectory(), this);
dialog.exec();
QString suggested = m_currentPath.isEmpty() ? tr("未命名")
: QFileInfo(m_currentPath).completeBaseName();
suggested += QStringLiteral(".pdf");
QString target = QFileDialog::getSaveFileName(
this, tr("导出 PDF"), QDir(startDirectory()).filePath(suggested),
tr("PDF 文件 (*.pdf)"));
if (target.isEmpty())
return;
if (!target.endsWith(QStringLiteral(".pdf"), Qt::CaseInsensitive))
target += QStringLiteral(".pdf");
QTextDocument document;
MarkdownRenderer::render(m_editor->toPlainText(), &document);
document.setPageSize(QSizeF(672.76, 1001.57));
QPrinter printer(QPrinter::HighResolution);
printer.setOutputFormat(QPrinter::PdfFormat);
printer.setOutputFileName(target);
printer.setPageSize(QPageSize(QPageSize::A4));
printer.setPageMargins(QMarginsF(16, 16, 16, 16), QPageLayout::Millimeter);
printer.setDocName(QFileInfo(target).completeBaseName());
document.print(&printer);
if (!QFileInfo::exists(target) || QFileInfo(target).size() <= 0) {
QMessageBox::warning(this, tr("导出失败"),
tr("没能写出「%1」,换个位置再试一次。")
.arg(QDir::toNativeSeparators(target)));
return;
}
statusBar()->showMessage(tr("已导出 %1").arg(QDir::toNativeSeparators(target)), 4000);
}
bool MainWindow::writeToFile(const QString &path)
@@ -667,6 +764,83 @@ void MainWindow::applyThemeMode(int mode)
ThemeManager::instance()->setMode(static_cast<ThemeManager::Mode>(mode), true);
}
void MainWindow::applyLanguage(const QString &code)
{
LanguageManager::instance()->setLanguage(code);
}
void MainWindow::retranslateUi()
{
m_fileMenu->setTitle(tr("文件"));
m_editMenu->setTitle(tr("编辑"));
m_viewMenu->setTitle(tr("视图"));
m_formatMenu->setTitle(tr("格式"));
m_helpMenu->setTitle(tr("帮助"));
m_recentMenu->setTitle(tr("最近打开"));
m_themeMenu->setTitle(tr("主题"));
m_headingMenu->setTitle(tr("标题"));
m_languageMenu->setTitle(tr("语言"));
m_newAction->setText(tr("新建"));
m_openAction->setText(tr("打开…"));
m_openFolderAction->setText(tr("打开文件夹…"));
m_saveAction->setText(tr("保存"));
m_saveAsAction->setText(tr("另存为…"));
m_exportPdfAction->setText(tr("导出 PDF…"));
m_quitAction->setText(tr("退出"));
m_undoAction->setText(tr("撤销"));
m_redoAction->setText(tr("重做"));
m_cutAction->setText(tr("剪切"));
m_copyAction->setText(tr("复制"));
m_pasteAction->setText(tr("粘贴"));
m_selectAllAction->setText(tr("全选"));
m_sidebarAction->setText(tr("侧栏"));
m_sourceModeAction->setText(tr("源代码模式"));
m_wrapAction->setText(tr("自动换行"));
m_zoomInAction->setText(tr("放大"));
m_zoomOutAction->setText(tr("缩小"));
m_zoomResetAction->setText(tr("实际大小"));
m_themeSystemAction->setText(ThemeManager::modeLabel(ThemeManager::Mode::System));
m_themeLightAction->setText(ThemeManager::modeLabel(ThemeManager::Mode::Light));
m_themeDarkAction->setText(ThemeManager::modeLabel(ThemeManager::Mode::Dark));
m_themeScheduleAction->setText(ThemeManager::modeLabel(ThemeManager::Mode::Schedule));
m_themeScheduleAction->setToolTip(tr("6:00–18:00 使用浅色,其余时间使用深色"));
for (int level = 1; level <= 6; ++level) {
if (m_headingActions[level - 1])
m_headingActions[level - 1]->setText(tr("标题 %1").arg(level));
}
m_codeBlockAction->setText(tr("代码块"));
m_aboutAction->setText(tr("关于 VoiletMark"));
if (m_sidebarButton) {
m_sidebarButton->setText(tr("侧栏"));
m_sidebarButton->setToolTip(tr("显示 / 隐藏文件夹侧栏(Ctrl+B)"));
}
if (m_sourceButton)
m_sourceButton->setToolTip(tr("源代码模式:关闭语法高亮与行号(Ctrl+Shift+M)"));
const QString language = LanguageManager::instance()->language();
m_languageChineseAction->setChecked(language == QLatin1String("zh_CN"));
m_languageEnglishAction->setChecked(language == QLatin1String("en"));
// 侧栏、最近打开、欢迎文档都跟着换
m_displayedFolder.clear();
refreshRecentMenu();
refreshFolder();
if (m_currentPath.isEmpty() && !m_editor->document()->isModified()
&& m_editor->toPlainText() == m_welcomeText) {
showWelcomeDocument();
}
updateWindowTitle();
updateStatusBar();
}
void MainWindow::showAbout()
{
QMessageBox box(this);