From 36857a918f366a76f1665226de856b6c1e335701 Mon Sep 17 00:00:00 2001 From: Yuki Date: Thu, 9 Apr 2026 12:01:33 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E5=88=9B=E5=BB=BA=E4=B8=8D=E5=90=8C=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TeXmacs/misc/themes/liii-night.css | 30 -- TeXmacs/misc/themes/liii.css | 30 -- .../progs/startup-tab/startup-tab-file.scm | 60 +++ TeXmacs/progs/startup-tab/startup-tab.scm | 3 +- devel/216_5.md | 91 ++++ src/Plugins/Qt/qt_file_page.cpp | 404 ++++++++++++++++++ src/Plugins/Qt/qt_file_page.hpp | 113 +++++ src/Plugins/Qt/qt_startup_tab_widget.cpp | 133 ++---- src/Plugins/Qt/qt_startup_tab_widget.hpp | 14 +- 9 files changed, 719 insertions(+), 159 deletions(-) create mode 100644 TeXmacs/progs/startup-tab/startup-tab-file.scm create mode 100644 devel/216_5.md create mode 100644 src/Plugins/Qt/qt_file_page.cpp create mode 100644 src/Plugins/Qt/qt_file_page.hpp diff --git a/TeXmacs/misc/themes/liii-night.css b/TeXmacs/misc/themes/liii-night.css index a980be2efd..aaf4897e3c 100644 --- a/TeXmacs/misc/themes/liii-night.css +++ b/TeXmacs/misc/themes/liii-night.css @@ -1097,36 +1097,6 @@ QLabel#startup-tab-page-desc { margin-bottom: 24px; } -/* File页主按钮 - New Document */ -QPushButton#startup-tab-primary-btn { - background-color: #2791ad; - color: #ffffff; - border: none; - border-radius: 6px; - padding: 12px 32px; - font-size: 14px; - font-weight: bold; -} - -QPushButton#startup-tab-primary-btn:hover { - background-color: #1e7891; -} - -/* File页次按钮 - Open Document */ -QPushButton#startup-tab-secondary-btn { - background-color: transparent; - color: #ffffff; - border: 2px solid #2791ad; - border-radius: 6px; - padding: 12px 32px; - font-size: 14px; - font-weight: bold; -} - -QPushButton#startup-tab-secondary-btn:hover { - background-color: rgba(39, 145, 173, 0.2); -} - /* 模板使用按钮 - Template Use Button */ QPushButton#template-use-btn { background-color: #4CAF50; diff --git a/TeXmacs/misc/themes/liii.css b/TeXmacs/misc/themes/liii.css index e48db8131d..cacf46064c 100644 --- a/TeXmacs/misc/themes/liii.css +++ b/TeXmacs/misc/themes/liii.css @@ -1070,36 +1070,6 @@ QLabel#startup-tab-page-desc { margin-bottom: 24px; } -/* File页主按钮 - New Document */ -QPushButton#startup-tab-primary-btn { - background-color: #2791ad; - color: #ffffff; - border: none; - border-radius: 6px; - padding: 12px 32px; - font-size: 14px; - font-weight: bold; -} - -QPushButton#startup-tab-primary-btn:hover { - background-color: #215a6a; -} - -/* File页次按钮 - Open Document */ -QPushButton#startup-tab-secondary-btn { - background-color: #ffffff; - color: #215a6a; - border: 2px solid #2791ad; - border-radius: 6px; - padding: 12px 32px; - font-size: 14px; - font-weight: bold; -} - -QPushButton#startup-tab-secondary-btn:hover { - background-color: #e8f4f6; -} - /* 模板使用按钮 - Template Use Button */ QPushButton#template-use-btn { background-color: #4CAF50; diff --git a/TeXmacs/progs/startup-tab/startup-tab-file.scm b/TeXmacs/progs/startup-tab/startup-tab-file.scm new file mode 100644 index 0000000000..de459818aa --- /dev/null +++ b/TeXmacs/progs/startup-tab/startup-tab-file.scm @@ -0,0 +1,60 @@ + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; MODULE : startup-tab-file.scm +;; DESCRIPTION: Scheme bindings for startup tab file operations +;; COPYRIGHT : (C) 2026 Yuki Lu +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; This software falls under the GNU general public license version 3 or later. +;; It comes WITHOUT ANY WARRANTY WHATSOEVER. For details, see the file LICENSE +;; in the root directory or . +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(texmacs-module (startup-tab startup-tab-file) + (:use (texmacs texmacs tm-server)) + (:use (texmacs texmacs tm-files)) + (:use (utils library cursor))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Document creation with specific style +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(tm-define (new-document-with-style style-id) + ;; Create a new document with the specified style + ;; style-id: "generic", "beamer", "book", "exam", "letter", "article" + ;; Use with-buffer to ensure we're working in the correct buffer context + (with-default-view + (let ((buf (if (window-per-buffer?) (open-window) (new-buffer)))) + ;; Schedule style initialization after buffer is fully set up + (delayed + (:idle 100) + (with-buffer buf + (init-style style-id)))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; File operations wrappers +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(tm-define (startup-tab-file-open) + ;; Open file dialog wrapper + (open-document)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Recent documents management +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(tm-define (startup-tab-get-recent-docs) + ;; Get list of recent documents + ;; Returns: list of (filename path timestamp) tuples + '()) + +(tm-define (startup-tab-add-recent-doc path) + ;; Add a document to recent list + (noop)) + +(tm-define (startup-tab-clear-recent-doc path) + ;; Remove a specific document from recent list + (noop)) + +(tm-define (startup-tab-clear-all-recent) + ;; Clear all recent documents + (noop)) diff --git a/TeXmacs/progs/startup-tab/startup-tab.scm b/TeXmacs/progs/startup-tab/startup-tab.scm index f0d7bc3188..560a2eef2b 100644 --- a/TeXmacs/progs/startup-tab/startup-tab.scm +++ b/TeXmacs/progs/startup-tab/startup-tab.scm @@ -12,7 +12,8 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (texmacs-module (startup-tab startup-tab) - (:use (texmacs texmacs tm-files))) + (:use (texmacs texmacs tm-files)) + (:use (startup-tab startup-tab-file))) (tm-define (startup-tab-enabled?) #t) diff --git a/devel/216_5.md b/devel/216_5.md new file mode 100644 index 0000000000..b442b56d97 --- /dev/null +++ b/devel/216_5.md @@ -0,0 +1,91 @@ +# 216_5 启动标签页文件页完整实现 + +## 如何测试 +1. 编译时先输入 `xmake config -vD --startup_tab=true` +2. 启动 Mogan STEM,点击 "Mogan STEM" 标签页 +3. 测试左侧导航栏: + - **File**: 切换到文件页 + - **Template**: 切换到模板页(占位) + - **Open Folder**: 点击弹出文件选择对话框 + - **Settings**: 切换到设置页(占位) + - **Quit**: 退出程序 +4. File 页测试: + - **样式卡片区**: + - 单击卡片:高亮选中(蓝色边框) + - 双击卡片:创建对应样式的新文档 + - Generic 卡片有 "Default" 标签 + - **最近文档列表**(当前为占位实现): + - UI 已就绪,可展示最近文档列表 + - 点击/右键菜单功能待与文件打开历史集成后启用 + +## 2026/04/09 启动标签页文件页完整实现 + +### What +实现启动标签页「文件」页的完整功能,包括文档样式选择、最近文档管理和文件打开功能。 + +#### 新增文件: + +**src/Plugins/Qt/qt_file_page.hpp** (新增) +- `StyleCard` 类:样式卡片控件,支持单击选中、双击创建 +- `QtFilePage` 类:文件页面主类 + - 样式卡片区(Generic/Beamer/Book/Exam/Letter/Article) + - 最近文档列表(QListWidget) + - 样式卡片与最近文档的分隔线 + +**src/Plugins/Qt/qt_file_page.cpp** (新增) +- 样式卡片实现:100x120px 固定大小,首字母图标,名称标签 +- 最近文档列表(占位实现):UI 框架已就绪,待与文件打开历史集成 +- 文档创建:通过 Scheme 函数 `(new-document-with-style style-id)` + +**TeXmacs/progs/startup-tab/startup-tab-file.scm** (新增) +- `(new-document-with-style style-id)`: 创建指定样式的新文档 +- `(startup-tab-file-open)`: 打开文件对话框包装 + +#### 修改文件: + +**src/Plugins/Qt/qt_startup_tab_widget.hpp** (修改) +- 替换 `navRecentBtn_` 为 `navOpenFolderBtn_` +- 添加 `filePage_` 成员指针 +- 移除旧的新建/打开文件槽函数 + +**src/Plugins/Qt/qt_startup_tab_widget.cpp** (修改) +- File 页面使用 `QtFilePage` 替代简单占位 +- 左侧导航栏顺序:File | Template | Open Folder | Settings +- "Open Folder" 不在互斥按钮组,点击直接触发 `(open-document)` +- 移除旧的 `create_recent_page()` 实现 + +**TeXmacs/progs/startup-tab/startup-tab.scm** (修改) +- 添加模块依赖:`(startup-tab startup-tab-file)` + +**TeXmacs/misc/themes/liii.css(liii-night.css)** (修改) +- 删除不再使用的按钮样式:`startup-tab-primary-btn`, `startup-tab-secondary-btn` + +### Why +之前只实现了简单的 New/Open 按钮,需要在当前PR中: +1. 提供文档样式选择(Generic/Beamer/Book/Exam/Letter/Article) +2. 最近文档 UI 框架(数据集成待后续实现) +3. 将打开文件功能移到更合理的导航栏位置 +4. 优化文档创建流程(单击预览、双击创建) + +### How + +**核心逻辑**: + +1. **样式选择**: + - `StyleCard` 继承 `QWidget`,自定义绘制选中边框 + - 单击:`clicked()` 信号 → 更新选中状态 + - 双击:`doubleClicked()` 信号 → 创建文档 + +2. **文档创建**: + - C++: `createDocumentWithStyle(styleId)` → Scheme: `(new-document-with-style styleId)` + - Scheme: 使用 `with-default-view` + `new-buffer` 创建缓冲区 + - 通过 `delayed` + `init-style` 异步应用样式,避免初始化时序问题 + +3. **最近文档(占位实现)**: + - UI 框架:`QListWidget` 展示列表,`QJsonDocument` 存储格式 + - 数据结构预留:`[{"path": "...", "name": "...", "opened_at": "..."}, ...]` + - 待集成:与 `buffer-notify-recent` 等 Scheme 函数对接,实现真正的文件历史记录 + +4. **导航栏**: + - "Open Folder" 使用 `setCheckable(false)`,不加入 `QButtonGroup` + - 点击直接执行 `(open-document)`,不切换页面 diff --git a/src/Plugins/Qt/qt_file_page.cpp b/src/Plugins/Qt/qt_file_page.cpp new file mode 100644 index 0000000000..53a316e45f --- /dev/null +++ b/src/Plugins/Qt/qt_file_page.cpp @@ -0,0 +1,404 @@ + +/****************************************************************************** + * MODULE : qt_file_page.cpp + * DESCRIPTION: File page implementation for startup tab + * COPYRIGHT : (C) 2026 Yuki Lu + ******************************************************************************* + * This software falls under the GNU general public license version 3 or later. + * It comes WITHOUT ANY WARRANTY WHATSOEVER. For details, see the file LICENSE + * in the root directory or . + ******************************************************************************/ + +#include "qt_file_page.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "s7_tm.hpp" + +// 最多显示的最近文档数量 +static const int MAX_RECENT_DOCS= 10; + +/****************************************************************************** + * StyleCard 实现 + ******************************************************************************/ + +StyleCard::StyleCard (const DocStyle& style, QWidget* parent) + : QWidget (parent), styleId_ (style.id), isSelected_ (false) { + setFixedSize (100, 120); + setCursor (Qt::PointingHandCursor); + setFocusPolicy (Qt::NoFocus); + + QVBoxLayout* layout= new QVBoxLayout (this); + layout->setContentsMargins (8, 8, 8, 8); + layout->setSpacing (4); + layout->setAlignment (Qt::AlignCenter); + + // 预览图占位(使用 QLabel 作为图标容器) + iconLabel_= new QLabel (this); + iconLabel_->setFixedSize (64, 64); + iconLabel_->setAlignment (Qt::AlignCenter); + iconLabel_->setObjectName ("style-card-icon"); + // 显示样式ID的首字母作为占位 + iconLabel_->setText (style.id.left (1).toUpper ()); + layout->addWidget (iconLabel_, 0, Qt::AlignCenter); + + // 样式名称 + nameLabel_= new QLabel (style.name, this); + nameLabel_->setAlignment (Qt::AlignCenter); + nameLabel_->setObjectName ("style-card-name"); + layout->addWidget (nameLabel_, 0, Qt::AlignCenter); + + // "默认"标签 + if (style.isDefault) { + badgeLabel_= new QLabel (tr ("Default"), this); + badgeLabel_->setObjectName ("style-card-badge"); + badgeLabel_->setAlignment (Qt::AlignCenter); + layout->addWidget (badgeLabel_, 0, Qt::AlignCenter); + } + + setObjectName ("style-card"); +} + +void +StyleCard::setSelected (bool selected) { + if (isSelected_ != selected) { + isSelected_= selected; + setProperty ("selected", selected); + style ()->unpolish (this); + style ()->polish (this); + update (); + } +} + +void +StyleCard::mousePressEvent (QMouseEvent* event) { + if (event->button () == Qt::LeftButton) { + emit clicked (); + } + QWidget::mousePressEvent (event); +} + +void +StyleCard::mouseDoubleClickEvent (QMouseEvent* event) { + if (event->button () == Qt::LeftButton) { + emit doubleClicked (); + } + QWidget::mouseDoubleClickEvent (event); +} + +void +StyleCard::paintEvent (QPaintEvent* event) { + QPainter painter (this); + painter.setRenderHint (QPainter::Antialiasing); + + // 绘制背景 + QStyleOption opt; + opt.initFrom (this); + style ()->drawPrimitive (QStyle::PE_Widget, &opt, &painter, this); + + // 绘制选中边框 + if (isSelected_) { + painter.setPen (QPen (opt.palette.highlight (), 2)); + painter.setBrush (Qt::NoBrush); + painter.drawRoundedRect (rect ().adjusted (1, 1, -1, -1), 6, 6); + } +} + +/****************************************************************************** + * QtFilePage 实现 + ******************************************************************************/ + +QtFilePage::QtFilePage (QWidget* parent) : QWidget (parent) { + // 初始化样式列表 + styles_= {{"generic", tr ("Generic"), tr ("General purpose document"), true}, + {"beamer", tr ("Beamer"), tr ("Presentation slides"), false}, + {"book", tr ("Book"), tr ("Book format"), false}, + {"exam", tr ("Exam"), tr ("Examination paper"), false}, + {"letter", tr ("Letter"), tr ("Letter format"), false}, + {"article", tr ("Article"), tr ("Article format"), false}}; + + setupUI (); + loadRecentDocs (); +} + +QtFilePage::~QtFilePage ()= default; + +void +QtFilePage::setupUI () { + QVBoxLayout* mainLayout= new QVBoxLayout (this); + mainLayout->setContentsMargins (32, 32, 32, 32); + mainLayout->setSpacing (24); + + // 1. 样式选择区 + setupStyleCards (mainLayout); + + // 分隔线 + QFrame* line= new QFrame (this); + line->setFrameShape (QFrame::HLine); + line->setObjectName ("startup-tab-separator"); + mainLayout->addWidget (line); + + // 2. 最近文档区 + setupRecentDocs (mainLayout); + + mainLayout->addStretch (); +} + +void +QtFilePage::setupStyleCards (QVBoxLayout* layout) { + // 标题 + QLabel* title= new QLabel (tr ("Document Style"), this); + title->setObjectName ("startup-tab-section-title"); + layout->addWidget (title); + + // 样式卡片容器(水平流式布局) + QWidget* cardsContainer= new QWidget (this); + QHBoxLayout* cardsLayout = new QHBoxLayout (cardsContainer); + cardsLayout->setContentsMargins (0, 0, 0, 0); + cardsLayout->setSpacing (16); + cardsLayout->setAlignment (Qt::AlignLeft); + + for (const auto& style : styles_) { + StyleCard* card= new StyleCard (style, cardsContainer); + styleCards_.append (card); + cardsLayout->addWidget (card); + + connect (card, &StyleCard::clicked, this, [this, card] () { + // 单击:高亮选中 + if (selectedCard_ != card) { + if (selectedCard_) { + selectedCard_->setSelected (false); + } + selectedCard_= card; + card->setSelected (true); + } + }); + + connect (card, &StyleCard::doubleClicked, this, [this, card] () { + // 双击:创建文档 + createDocumentWithStyle (card->styleId ()); + }); + + // 默认选中 Generic + if (style.isDefault) { + card->setSelected (true); + selectedCard_= card; + } + } + + cardsLayout->addStretch (); + layout->addWidget (cardsContainer); +} + +void +QtFilePage::setupRecentDocs (QVBoxLayout* layout) { + // 标题 + QLabel* title= new QLabel (tr ("Recent Documents"), this); + title->setObjectName ("startup-tab-section-title"); + layout->addWidget (title); + + // 最近文档列表 + recentList_= new QListWidget (this); + recentList_->setObjectName ("recent-docs-list"); + recentList_->setFocusPolicy (Qt::NoFocus); + recentList_->setContextMenuPolicy (Qt::CustomContextMenu); + recentList_->setMaximumHeight (200); + + connect (recentList_, &QListWidget::itemClicked, this, + [this] (QListWidgetItem* item) { + if (item) onRecentDocClicked (item); + }); + connect (recentList_, &QListWidget::customContextMenuRequested, this, + &QtFilePage::onRecentDocContextMenu); + + layout->addWidget (recentList_); +} + +/****************************************************************************** + * 最近文档管理 + ******************************************************************************/ + +static QString +getRecentDocsFilePath () { + QString configDir= + QStandardPaths::writableLocation (QStandardPaths::AppConfigLocation); + QDir ().mkpath (configDir); + return QDir (configDir).filePath ("recent_documents.json"); +} + +void +QtFilePage::loadRecentDocs () { + recentDocs_.clear (); + recentList_->clear (); + + QString filePath= getRecentDocsFilePath (); + QFile file (filePath); + if (!file.open (QIODevice::ReadOnly)) { + return; + } + + QByteArray data= file.readAll (); + QJsonDocument doc = QJsonDocument::fromJson (data); + if (!doc.isObject ()) { + return; + } + + QJsonObject obj = doc.object (); + QJsonArray recentArray= obj["recent_documents"].toArray (); + + for (const auto& val : recentArray) { + QJsonObject docObj= val.toObject (); + RecentDoc doc; + doc.filePath= docObj["path"].toString (); + doc.fileName= docObj["name"].toString (); + if (doc.fileName.isEmpty ()) { + doc.fileName= QFileInfo (doc.filePath).fileName (); + } + doc.openedAt= + QDateTime::fromString (docObj["opened_at"].toString (), Qt::ISODate); + recentDocs_.append (doc); + } + + refreshRecentDocs (); +} + +void +QtFilePage::saveRecentDocs () { + QJsonArray recentArray; + for (const auto& doc : recentDocs_) { + QJsonObject obj; + obj["path"] = doc.filePath; + obj["name"] = doc.fileName; + obj["opened_at"]= doc.openedAt.toString (Qt::ISODate); + recentArray.append (obj); + } + + QJsonObject root; + root["recent_documents"]= recentArray; + + QJsonDocument doc (root); + QFile file (getRecentDocsFilePath ()); + if (file.open (QIODevice::WriteOnly)) { + file.write (doc.toJson ()); + } +} + +void +QtFilePage::refreshRecentDocs () { + recentList_->clear (); + + for (const auto& doc : recentDocs_) { + QString text= QString ("%1\n%2").arg (doc.fileName, doc.filePath); + auto* item= new QListWidgetItem (text); + item->setData (Qt::UserRole, doc.filePath); + item->setToolTip (doc.filePath); + recentList_->addItem (item); + } + + if (recentDocs_.isEmpty ()) { + auto* item= new QListWidgetItem (tr ("No recent documents")); + item->setFlags (item->flags () & ~Qt::ItemIsEnabled); + recentList_->addItem (item); + } +} + +void +QtFilePage::addRecentDoc (const QString& path) { + QString fileName= QFileInfo (path).fileName (); + + // 检查是否已存在 + for (auto it= recentDocs_.begin (); it != recentDocs_.end (); ++it) { + if (it->filePath == path) { + it->openedAt= QDateTime::currentDateTime (); + // 移到最前面 + RecentDoc doc= *it; + recentDocs_.erase (it); + recentDocs_.prepend (doc); + saveRecentDocs (); + refreshRecentDocs (); + return; + } + } + + // 添加新条目 + RecentDoc doc; + doc.filePath= path; + doc.fileName= fileName; + doc.openedAt= QDateTime::currentDateTime (); + recentDocs_.prepend (doc); + + // 限制数量 + while (recentDocs_.size () > MAX_RECENT_DOCS) { + recentDocs_.removeLast (); + } + + saveRecentDocs (); + refreshRecentDocs (); +} + +void +QtFilePage::removeRecentDoc (const QString& path) { + for (auto it= recentDocs_.begin (); it != recentDocs_.end (); ++it) { + if (it->filePath == path) { + recentDocs_.erase (it); + saveRecentDocs (); + refreshRecentDocs (); + return; + } + } +} + +/****************************************************************************** + * 事件处理 + ******************************************************************************/ + +void +QtFilePage::onRecentDocClicked (QListWidgetItem* item) { + if (!item) return; + + QString path= item->data (Qt::UserRole).toString (); + if (path.isEmpty ()) return; + + QString schemeCmd= QString ("(load-document \"%1\")").arg (path); + eval_scheme (schemeCmd.toUtf8 ().constData ()); +} + +void +QtFilePage::onRecentDocContextMenu (const QPoint& pos) { + QListWidgetItem* item= recentList_->itemAt (pos); + if (!item) return; + + QString path= item->data (Qt::UserRole).toString (); + if (path.isEmpty ()) return; + + QMenu menu (this); + QAction* removeAction= menu.addAction (tr ("Remove from list")); + + if (menu.exec (recentList_->mapToGlobal (pos)) == removeAction) { + removeRecentDoc (path); + } +} + +void +QtFilePage::createDocumentWithStyle (const QString& styleId) { + // 调用 Scheme 函数创建指定样式的文档 + QString schemeCmd= QString ("(new-document-with-style \"%1\")").arg (styleId); + eval_scheme (schemeCmd.toUtf8 ().constData ()); +} diff --git a/src/Plugins/Qt/qt_file_page.hpp b/src/Plugins/Qt/qt_file_page.hpp new file mode 100644 index 0000000000..1077a18696 --- /dev/null +++ b/src/Plugins/Qt/qt_file_page.hpp @@ -0,0 +1,113 @@ + +/****************************************************************************** + * MODULE : qt_file_page.hpp + * DESCRIPTION: File page for startup tab with style cards and recent documents + * COPYRIGHT : (C) 2026 Yuki Lu + ******************************************************************************* + * This software falls under the GNU general public license version 3 or later. + * It comes WITHOUT ANY WARRANTY WHATSOEVER. For details, see the file LICENSE + * in the root directory or . + ******************************************************************************/ + +#ifndef QT_FILE_PAGE_HPP +#define QT_FILE_PAGE_HPP + +#include +#include +#include +#include + +class QVBoxLayout; +class QHBoxLayout; +class QPushButton; +class QLabel; +class QListWidget; +class QListWidgetItem; +class QButtonGroup; + +/** + * @brief 文档样式信息 + */ +struct DocStyle { + QString id; // 样式ID: generic, beamer, book, exam, letter, article + QString name; // 显示名称 + QString description; // 描述 + bool isDefault; // 是否为默认样式 +}; + +/** + * @brief 最近文档条目 + */ +struct RecentDoc { + QString fileName; // 文件名 + QString filePath; // 完整路径 + QDateTime openedAt; // 最后打开时间 +}; + +/** + * @brief 样式卡片部件 + */ +class StyleCard : public QWidget { + Q_OBJECT + +public: + explicit StyleCard (const DocStyle& style, QWidget* parent= nullptr); + + QString styleId () const { return styleId_; } + void setSelected (bool selected); + bool isSelected () const { return isSelected_; } + +signals: + void clicked (); + void doubleClicked (); + +protected: + void mousePressEvent (QMouseEvent* event) override; + void mouseDoubleClickEvent (QMouseEvent* event) override; + void paintEvent (QPaintEvent* event) override; + +private: + QString styleId_; + bool isSelected_= false; + QLabel* iconLabel_ = nullptr; + QLabel* nameLabel_ = nullptr; + QLabel* badgeLabel_= nullptr; // "默认"标签 +}; + +/** + * @brief 文件页面 - 包含样式选择和最近文档 + */ +class QtFilePage : public QWidget { + Q_OBJECT + +public: + explicit QtFilePage (QWidget* parent= nullptr); + ~QtFilePage (); + + void refreshRecentDocs (); + +private: + void onRecentDocClicked (QListWidgetItem* item); + void onRecentDocContextMenu (const QPoint& pos); + +private: + void setupUI (); + void setupStyleCards (QVBoxLayout* layout); + void setupRecentDocs (QVBoxLayout* layout); + void loadRecentDocs (); + void saveRecentDocs (); + void addRecentDoc (const QString& path); + void removeRecentDoc (const QString& path); + void createDocumentWithStyle (const QString& styleId); + + // 样式卡片相关 + QList styles_; + QList styleCards_; + StyleCard* selectedCard_= nullptr; + + // 最近文档相关 + QList recentDocs_; + QListWidget* recentList_= nullptr; +}; + +#endif // QT_FILE_PAGE_HPP diff --git a/src/Plugins/Qt/qt_startup_tab_widget.cpp b/src/Plugins/Qt/qt_startup_tab_widget.cpp index 29213169a4..edf3d31e6a 100644 --- a/src/Plugins/Qt/qt_startup_tab_widget.cpp +++ b/src/Plugins/Qt/qt_startup_tab_widget.cpp @@ -10,6 +10,7 @@ ******************************************************************************/ #include "qt_startup_tab_widget.hpp" +#include "qt_file_page.hpp" #include "qt_settings_page.hpp" #include "qt_template_page.hpp" @@ -33,9 +34,9 @@ */ QTStartupTabWidget::QTStartupTabWidget (QWidget* parent) : QWidget (parent), currentEntry_ (Entry::File), navFileBtn_ (nullptr), - navTemplateBtn_ (nullptr), navRecentBtn_ (nullptr), + navTemplateBtn_ (nullptr), navOpenFolderBtn_ (nullptr), navSettingsBtn_ (nullptr), navQuitBtn_ (nullptr), - navButtonGroup_ (nullptr), settingsPage_ (nullptr) { + navButtonGroup_ (nullptr), filePage_ (nullptr), settingsPage_ (nullptr) { setMinimumSize (600, 400); setFocusPolicy (Qt::NoFocus); @@ -104,22 +105,21 @@ QTStartupTabWidget::setup_left_sidebar (QVBoxLayout* sidebarLayout) { navButtonGroup_->setExclusive (true); // 导航按钮(4个入口) - navFileBtn_ = create_nav_button ("File"); - navTemplateBtn_= create_nav_button ("Template"); - navRecentBtn_ = create_nav_button ("Recent"); - navSettingsBtn_= create_nav_button ("Settings"); + navFileBtn_ = create_nav_button ("File"); + navTemplateBtn_ = create_nav_button ("Template"); + navOpenFolderBtn_= create_nav_button ("Open Folder"); + navSettingsBtn_ = create_nav_button ("Settings"); - // 添加到按钮组和布局 + // 添加到按钮组和布局(Open Folder 不在互斥组中,因为它没有对应页面) navButtonGroup_->addButton (navFileBtn_, static_cast (Entry::File)); navButtonGroup_->addButton (navTemplateBtn_, static_cast (Entry::Template)); - navButtonGroup_->addButton (navRecentBtn_, static_cast (Entry::Recent)); navButtonGroup_->addButton (navSettingsBtn_, static_cast (Entry::Settings)); sidebarLayout->addWidget (navFileBtn_); sidebarLayout->addWidget (navTemplateBtn_); - sidebarLayout->addWidget (navRecentBtn_); + sidebarLayout->addWidget (navOpenFolderBtn_); sidebarLayout->addWidget (navSettingsBtn_); // 导航按钮点击事件:切换到对应页面 @@ -127,11 +127,14 @@ QTStartupTabWidget::setup_left_sidebar (QVBoxLayout* sidebarLayout) { [this] () { set_current_entry (Entry::File); }); connect (navTemplateBtn_, &QPushButton::clicked, this, [this] () { set_current_entry (Entry::Template); }); - connect (navRecentBtn_, &QPushButton::clicked, this, - [this] () { set_current_entry (Entry::Recent); }); + connect (navOpenFolderBtn_, &QPushButton::clicked, this, + &QTStartupTabWidget::on_file_open); connect (navSettingsBtn_, &QPushButton::clicked, this, [this] () { set_current_entry (Entry::Settings); }); + // Open Folder 不是 checkable 按钮(没有对应页面) + navOpenFolderBtn_->setCheckable (false); + // 弹性空间,将 Quit 按钮推到底部 sidebarLayout->addStretch (); sidebarLayout->addSpacing (24); @@ -170,16 +173,30 @@ QTStartupTabWidget::create_nav_button (const QString& text) { */ void QTStartupTabWidget::setup_right_content (QStackedWidget* stackedWidget) { - // 添加4个页面到堆叠控件 + // 添加3个页面到堆叠控件(OpenFolder没有页面,直接触发操作) stackedWidget->addWidget (create_file_page ()); // index 0 - File stackedWidget->addWidget (create_template_page ()); // index 1 - Template - stackedWidget->addWidget (create_recent_page ()); // index 2 - Recent - stackedWidget->addWidget (create_settings_page ()); // index 3 - Settings + stackedWidget->addWidget (create_settings_page ()); // index 2 - Settings // 入口切换时,同步切换堆叠控件的当前页面 + // 注意:OpenFolder 没有对应页面,需要调整索引映射 connect (this, &QTStartupTabWidget::entry_changed, stackedWidget, [stackedWidget] (QTStartupTabWidget::Entry entry) { - int index= static_cast (entry); + int index; + switch (entry) { + case QTStartupTabWidget::Entry::File: + index= 0; + break; + case QTStartupTabWidget::Entry::Template: + index= 1; + break; + case QTStartupTabWidget::Entry::Settings: + index= 2; + break; + default: + index= 0; + break; + } stackedWidget->setCurrentIndex (index); }); } @@ -188,50 +205,15 @@ QTStartupTabWidget::setup_right_content (QStackedWidget* stackedWidget) { * @brief 创建 File 页面 * @return File 页面控件 * - * 页面内容: - * - 标题 "File" - * - 说明文字 - * - New Document 按钮(主按钮) - * - Open Document 按钮(次按钮) + * 使用 QtFilePage 实现,包含: + * - 文档样式选择卡片 + * - 最近文档列表 + * - 打开文件按钮 */ QWidget* QTStartupTabWidget::create_file_page () { - QWidget* page = new QWidget (this); - QVBoxLayout* layout= new QVBoxLayout (page); - layout->setContentsMargins (32, 32, 32, 32); - - // 标题 - QLabel* title= new QLabel ("File", page); - title->setObjectName ("startup-tab-page-title"); - layout->addWidget (title); - - // 按钮行布局 - QHBoxLayout* btnLayout= new QHBoxLayout; - btnLayout->setSpacing (16); - - // New Document 按钮(主按钮) - QPushButton* newBtn= new QPushButton ("New Document", page); - newBtn->setObjectName ("startup-tab-primary-btn"); - newBtn->setFocusPolicy (Qt::NoFocus); - newBtn->setCursor (Qt::PointingHandCursor); - connect (newBtn, &QPushButton::clicked, this, - &QTStartupTabWidget::on_file_new); - btnLayout->addWidget (newBtn); - - // Open Document 按钮(次按钮) - QPushButton* openBtn= new QPushButton ("Open Document", page); - openBtn->setObjectName ("startup-tab-secondary-btn"); - openBtn->setFocusPolicy (Qt::NoFocus); - openBtn->setCursor (Qt::PointingHandCursor); - connect (openBtn, &QPushButton::clicked, this, - &QTStartupTabWidget::on_file_open); - btnLayout->addWidget (openBtn); - - btnLayout->addStretch (); - layout->addLayout (btnLayout); - - layout->addStretch (); - return page; + filePage_= new QtFilePage (this); + return filePage_; } /** @@ -260,28 +242,6 @@ QTStartupTabWidget::create_template_page () { return page; } -/** - * @brief 创建 Recent 页面(占位) - */ -QWidget* -QTStartupTabWidget::create_recent_page () { - QWidget* page = new QWidget (this); - QVBoxLayout* layout= new QVBoxLayout (page); - layout->setContentsMargins (32, 32, 32, 32); - - QLabel* title= new QLabel ("Recent Documents", page); - title->setObjectName ("startup-tab-page-title"); - layout->addWidget (title); - - QLabel* desc= new QLabel ( - "Coming soon: Recently opened documents will appear here.", page); - desc->setObjectName ("startup-tab-page-desc"); - layout->addWidget (desc); - - layout->addStretch (); - return page; -} - /** * @brief 创建 Settings 页面 */ @@ -306,12 +266,12 @@ QTStartupTabWidget::set_active_nav_button (Entry entry) { } /** - * @brief 新建文档 - * 调用 Scheme 函数 (new-document) + * @brief 退出程序 + * 调用 Scheme 函数 (quit-TeXmacs) */ void -QTStartupTabWidget::on_file_new () { - eval_scheme ("(new-document)"); +QTStartupTabWidget::on_app_quit () { + eval_scheme ("(quit-TeXmacs)"); } /** @@ -322,12 +282,3 @@ void QTStartupTabWidget::on_file_open () { eval_scheme ("(open-document)"); } - -/** - * @brief 退出程序 - * 调用 Scheme 函数 (quit-TeXmacs) - */ -void -QTStartupTabWidget::on_app_quit () { - eval_scheme ("(quit-TeXmacs)"); -} diff --git a/src/Plugins/Qt/qt_startup_tab_widget.hpp b/src/Plugins/Qt/qt_startup_tab_widget.hpp index 874c7f4186..7315f6ac8f 100644 --- a/src/Plugins/Qt/qt_startup_tab_widget.hpp +++ b/src/Plugins/Qt/qt_startup_tab_widget.hpp @@ -19,6 +19,7 @@ class QVBoxLayout; class QPushButton; class QStackedWidget; class QButtonGroup; +class QtFilePage; class QTSettingsPage; class QTTemplatePage; @@ -26,7 +27,7 @@ class QTStartupTabWidget : public QWidget { Q_OBJECT public: - enum class Entry { File, Template, Recent, Settings }; + enum class Entry { File, Template, OpenFolder, Settings }; public: explicit QTStartupTabWidget (QWidget* parent= nullptr); @@ -38,12 +39,9 @@ class QTStartupTabWidget : public QWidget { void entry_changed (Entry entry); private slots: - // File operations - void on_file_new (); - void on_file_open (); - // Application operation void on_app_quit (); + void on_file_open (); private: // 界面构建辅助函数 @@ -54,7 +52,6 @@ private slots: // 页面创建函数 QWidget* create_file_page (); QWidget* create_template_page (); - QWidget* create_recent_page (); QWidget* create_settings_page (); // 导航按钮状态管理 @@ -66,13 +63,16 @@ private slots: // Navigation buttons QPushButton* navFileBtn_; QPushButton* navTemplateBtn_; - QPushButton* navRecentBtn_; + QPushButton* navOpenFolderBtn_; QPushButton* navSettingsBtn_; QPushButton* navQuitBtn_; // 互斥按钮组 QButtonGroup* navButtonGroup_; + // 各页面实例 + QtFilePage* filePage_; + // 各页面实例 QTSettingsPage* settingsPage_; From 76385eda685bf4d8fe3bb3ed167ddc63954f0286 Mon Sep 17 00:00:00 2001 From: Yuki Date: Thu, 9 Apr 2026 12:07:35 +0800 Subject: [PATCH 2/4] wip --- src/Plugins/Qt/qt_file_page.cpp | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/Plugins/Qt/qt_file_page.cpp b/src/Plugins/Qt/qt_file_page.cpp index 53a316e45f..6e91703a6e 100644 --- a/src/Plugins/Qt/qt_file_page.cpp +++ b/src/Plugins/Qt/qt_file_page.cpp @@ -41,7 +41,13 @@ static const int MAX_RECENT_DOCS= 10; StyleCard::StyleCard (const DocStyle& style, QWidget* parent) : QWidget (parent), styleId_ (style.id), isSelected_ (false) { - setFixedSize (100, 120); + // 根据 DPI 计算尺寸,确保在高分辨率屏幕上显示正常 + int scale = physicalDpiX () >= 192 ? 2 : 1; // 简单的高 DPI 检测 + int width = 100 * scale; + int height = 120 * scale; + int iconSize= 64 * scale; + + setFixedSize (width, height); setCursor (Qt::PointingHandCursor); setFocusPolicy (Qt::NoFocus); @@ -52,7 +58,7 @@ StyleCard::StyleCard (const DocStyle& style, QWidget* parent) // 预览图占位(使用 QLabel 作为图标容器) iconLabel_= new QLabel (this); - iconLabel_->setFixedSize (64, 64); + iconLabel_->setFixedSize (iconSize, iconSize); iconLabel_->setAlignment (Qt::AlignCenter); iconLabel_->setObjectName ("style-card-icon"); // 显示样式ID的首字母作为占位 @@ -376,7 +382,12 @@ QtFilePage::onRecentDocClicked (QListWidgetItem* item) { QString path= item->data (Qt::UserRole).toString (); if (path.isEmpty ()) return; - QString schemeCmd= QString ("(load-document \"%1\")").arg (path); + // 转义路径中的双引号和反斜杠,防止 Scheme 注入 + QString escapedPath= path; + escapedPath.replace ("\\", "\\\\"); // 先替换反斜杠 + escapedPath.replace ("\"", "\\\""); // 再替换双引符 + + QString schemeCmd= QString ("(load-document \"%1\")").arg (escapedPath); eval_scheme (schemeCmd.toUtf8 ().constData ()); } @@ -398,6 +409,14 @@ QtFilePage::onRecentDocContextMenu (const QPoint& pos) { void QtFilePage::createDocumentWithStyle (const QString& styleId) { + // 验证 styleId 是预定义的合法值,防止注入攻击 + static const QStringList validStyles= {"generic", "beamer", "book", + "exam", "letter", "article"}; + if (!validStyles.contains (styleId)) { + qWarning () << "Invalid style ID:" << styleId; + return; + } + // 调用 Scheme 函数创建指定样式的文档 QString schemeCmd= QString ("(new-document-with-style \"%1\")").arg (styleId); eval_scheme (schemeCmd.toUtf8 ().constData ()); From 78194994de5d498e419372dd0f4d5fb3bbdc04c6 Mon Sep 17 00:00:00 2001 From: Yuki Date: Fri, 10 Apr 2026 09:59:30 +0800 Subject: [PATCH 3/4] wip --- src/Plugins/Qt/qt_file_page.cpp | 34 ++++++++++++++++----------------- src/Plugins/Qt/qt_file_page.hpp | 6 +++--- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/Plugins/Qt/qt_file_page.cpp b/src/Plugins/Qt/qt_file_page.cpp index 6e91703a6e..ccc5b9f839 100644 --- a/src/Plugins/Qt/qt_file_page.cpp +++ b/src/Plugins/Qt/qt_file_page.cpp @@ -30,6 +30,7 @@ #include #include +#include "qt_dpi_utils.hpp" #include "s7_tm.hpp" // 最多显示的最近文档数量 @@ -41,11 +42,11 @@ static const int MAX_RECENT_DOCS= 10; StyleCard::StyleCard (const DocStyle& style, QWidget* parent) : QWidget (parent), styleId_ (style.id), isSelected_ (false) { - // 根据 DPI 计算尺寸,确保在高分辨率屏幕上显示正常 - int scale = physicalDpiX () >= 192 ? 2 : 1; // 简单的高 DPI 检测 - int width = 100 * scale; - int height = 120 * scale; - int iconSize= 64 * scale; + // 使用 DpiUtils 计算缩放后的尺寸 + int scale = qRound (DpiUtils::scaleFactor ()); + int width = DpiUtils::scaled (100); + int height = DpiUtils::scaled (120); + int iconSize= DpiUtils::scaled (64); setFixedSize (width, height); setCursor (Qt::PointingHandCursor); @@ -94,19 +95,18 @@ StyleCard::setSelected (bool selected) { } void -StyleCard::mousePressEvent (QMouseEvent* event) { - if (event->button () == Qt::LeftButton) { - emit clicked (); - } - QWidget::mousePressEvent (event); +StyleCard::enterEvent (QEnterEvent* event) { + // 悬停时选中 + emit hovered (); + QWidget::enterEvent (event); } void -StyleCard::mouseDoubleClickEvent (QMouseEvent* event) { +StyleCard::mousePressEvent (QMouseEvent* event) { if (event->button () == Qt::LeftButton) { - emit doubleClicked (); + emit clicked (); } - QWidget::mouseDoubleClickEvent (event); + QWidget::mousePressEvent (event); } void @@ -186,8 +186,8 @@ QtFilePage::setupStyleCards (QVBoxLayout* layout) { styleCards_.append (card); cardsLayout->addWidget (card); - connect (card, &StyleCard::clicked, this, [this, card] () { - // 单击:高亮选中 + connect (card, &StyleCard::hovered, this, [this, card] () { + // 悬停时选中 if (selectedCard_ != card) { if (selectedCard_) { selectedCard_->setSelected (false); @@ -197,8 +197,8 @@ QtFilePage::setupStyleCards (QVBoxLayout* layout) { } }); - connect (card, &StyleCard::doubleClicked, this, [this, card] () { - // 双击:创建文档 + connect (card, &StyleCard::clicked, this, [this, card] () { + // 单击:创建文档 createDocumentWithStyle (card->styleId ()); }); diff --git a/src/Plugins/Qt/qt_file_page.hpp b/src/Plugins/Qt/qt_file_page.hpp index 1077a18696..5e1a3244ea 100644 --- a/src/Plugins/Qt/qt_file_page.hpp +++ b/src/Plugins/Qt/qt_file_page.hpp @@ -58,12 +58,12 @@ class StyleCard : public QWidget { bool isSelected () const { return isSelected_; } signals: - void clicked (); - void doubleClicked (); + void hovered (); // 悬停时触发(用于选中) + void clicked (); // 单击时触发(用于打开) protected: + void enterEvent (QEnterEvent* event) override; void mousePressEvent (QMouseEvent* event) override; - void mouseDoubleClickEvent (QMouseEvent* event) override; void paintEvent (QPaintEvent* event) override; private: From 63bbff2d4e97dcc6edf418a0dcd9155d91631395 Mon Sep 17 00:00:00 2001 From: Yuki Date: Fri, 10 Apr 2026 10:15:35 +0800 Subject: [PATCH 4/4] fix --- devel/216_5.md | 12 ++++++------ src/Plugins/Qt/qt_startup_tab_widget.cpp | 11 ++++++----- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/devel/216_5.md b/devel/216_5.md index b442b56d97..c3853c009a 100644 --- a/devel/216_5.md +++ b/devel/216_5.md @@ -11,8 +11,8 @@ - **Quit**: 退出程序 4. File 页测试: - **样式卡片区**: - - 单击卡片:高亮选中(蓝色边框) - - 双击卡片:创建对应样式的新文档 + - 鼠标悬停卡片:高亮选中(蓝色边框) + - 单击卡片:创建对应样式的新文档 - Generic 卡片有 "Default" 标签 - **最近文档列表**(当前为占位实现): - UI 已就绪,可展示最近文档列表 @@ -26,7 +26,7 @@ #### 新增文件: **src/Plugins/Qt/qt_file_page.hpp** (新增) -- `StyleCard` 类:样式卡片控件,支持单击选中、双击创建 +- `StyleCard` 类:样式卡片控件,支持悬停选中、单击创建 - `QtFilePage` 类:文件页面主类 - 样式卡片区(Generic/Beamer/Book/Exam/Letter/Article) - 最近文档列表(QListWidget) @@ -65,7 +65,7 @@ 1. 提供文档样式选择(Generic/Beamer/Book/Exam/Letter/Article) 2. 最近文档 UI 框架(数据集成待后续实现) 3. 将打开文件功能移到更合理的导航栏位置 -4. 优化文档创建流程(单击预览、双击创建) +4. 优化文档创建流程(悬停预览、单击创建) ### How @@ -73,8 +73,8 @@ 1. **样式选择**: - `StyleCard` 继承 `QWidget`,自定义绘制选中边框 - - 单击:`clicked()` 信号 → 更新选中状态 - - 双击:`doubleClicked()` 信号 → 创建文档 + - 悬停:`hovered()` 信号 → 更新选中状态 + - 单击:`clicked()` 信号 → 创建文档 2. **文档创建**: - C++: `createDocumentWithStyle(styleId)` → Scheme: `(new-document-with-style styleId)` diff --git a/src/Plugins/Qt/qt_startup_tab_widget.cpp b/src/Plugins/Qt/qt_startup_tab_widget.cpp index edf3d31e6a..f06d735c54 100644 --- a/src/Plugins/Qt/qt_startup_tab_widget.cpp +++ b/src/Plugins/Qt/qt_startup_tab_widget.cpp @@ -36,7 +36,8 @@ QTStartupTabWidget::QTStartupTabWidget (QWidget* parent) : QWidget (parent), currentEntry_ (Entry::File), navFileBtn_ (nullptr), navTemplateBtn_ (nullptr), navOpenFolderBtn_ (nullptr), navSettingsBtn_ (nullptr), navQuitBtn_ (nullptr), - navButtonGroup_ (nullptr), filePage_ (nullptr), settingsPage_ (nullptr) { + navButtonGroup_ (nullptr), filePage_ (nullptr), settingsPage_ (nullptr), + templatePage_ (nullptr) { setMinimumSize (600, 400); setFocusPolicy (Qt::NoFocus); @@ -221,11 +222,11 @@ QTStartupTabWidget::create_file_page () { */ QWidget* QTStartupTabWidget::create_template_page () { - QTTemplatePage* page= new QTTemplatePage (this); - page->initialize (); + templatePage_= new QTTemplatePage (this); + templatePage_->initialize (); // Connect template opened signal to load document - connect (page, &QTTemplatePage::templateOpened, this, + connect (templatePage_, &QTTemplatePage::templateOpened, this, [] (const QString& filePath) { // Escape special characters for Scheme string literal // Handle backslash (Windows paths) and double quote @@ -239,7 +240,7 @@ QTStartupTabWidget::create_template_page () { eval_scheme (utf8.constData ()); }); - return page; + return templatePage_; } /**