From 01f495f9530ced0b971f642ee6d0eeba49bd25a4 Mon Sep 17 00:00:00 2001 From: Yuki Date: Fri, 8 May 2026 10:50:08 +0800 Subject: [PATCH 1/4] wip --- TeXmacs/plugins/lang/dic/en_US/zh_CN.scm | 1 + src/Plugins/Qt/qt_file_page.cpp | 17 +++ src/Plugins/Qt/qt_floating_toast.cpp | 133 +++++++++++++++++++++++ src/Plugins/Qt/qt_floating_toast.hpp | 52 +++++++++ 4 files changed, 203 insertions(+) create mode 100644 src/Plugins/Qt/qt_floating_toast.cpp create mode 100644 src/Plugins/Qt/qt_floating_toast.hpp diff --git a/TeXmacs/plugins/lang/dic/en_US/zh_CN.scm b/TeXmacs/plugins/lang/dic/en_US/zh_CN.scm index 26e16dd2c0..69f148873e 100644 --- a/TeXmacs/plugins/lang/dic/en_US/zh_CN.scm +++ b/TeXmacs/plugins/lang/dic/en_US/zh_CN.scm @@ -820,6 +820,7 @@ ("field" "区域") ("figure" "图") ("file name" "文件名") +("File not found, removed from recent list" "文件未找到,已从最近列表中移除") ("file not found" "无此文件") ("file type" "文件类型") ("file" "文件") diff --git a/src/Plugins/Qt/qt_file_page.cpp b/src/Plugins/Qt/qt_file_page.cpp index eb6d9db063..4e2366f6d9 100644 --- a/src/Plugins/Qt/qt_file_page.cpp +++ b/src/Plugins/Qt/qt_file_page.cpp @@ -38,6 +38,7 @@ #include #include "qt_dpi_utils.hpp" +#include "qt_floating_toast.hpp" #include "qt_utilities.hpp" #include "s7_tm.hpp" #include "sys_utils.hpp" @@ -437,6 +438,14 @@ QtFilePage::loadRecentDocs () { } recentPaths.removeDuplicates (); + QStringList existingPaths; + for (const QString& path : recentPaths) { + if (QFile::exists (path)) { + existingPaths.append (path); + } + } + recentPaths= existingPaths; + QString filePath= getRecentDocsFilePath (); QFile file (filePath); if (!file.open (QIODevice::ReadOnly)) { @@ -685,6 +694,14 @@ QtFilePage::onRecentDocClicked (QListWidgetItem* item) { QString path= item->data (Qt::UserRole).toString (); if (path.isEmpty ()) return; + if (!QFile::exists (path)) { + QtFloatingToast::showToast ( + recentList_, qt_translate ("File not found, removed from recent list"), + 3000, QtFloatingToast::Error); + removeRecentDoc (path); + return; + } + addRecentDoc (path); eval_scheme ("(load-document " * qt_scheme_quote_utf8 (path) * ")"); diff --git a/src/Plugins/Qt/qt_floating_toast.cpp b/src/Plugins/Qt/qt_floating_toast.cpp new file mode 100644 index 0000000000..cfcd8846e3 --- /dev/null +++ b/src/Plugins/Qt/qt_floating_toast.cpp @@ -0,0 +1,133 @@ + +/****************************************************************************** + * MODULE : qt_floating_toast.cpp + * DESCRIPTION: Floating toast implementation + * 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_floating_toast.hpp" +#include "qt_dpi_utils.hpp" + +#include +#include +#include +#include +#include +#include +#include + +QtFloatingToast::QtFloatingToast (QWidget* parent) + : QWidget (parent, + Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::Tool) { + setAttribute (Qt::WA_TranslucentBackground); + + label_= new QLabel (this); + label_->setAlignment (Qt::AlignCenter); + label_->setWordWrap (true); + label_->setStyleSheet ("QLabel { color: #ffffff; }"); + label_->setFont (DpiUtils::scaledFont (label_->font (), 14)); + + layout_= new QHBoxLayout (this); + layout_->setContentsMargins (0, 0, 0, 0); + layout_->addWidget (label_, 0, Qt::AlignCenter); + + hideTimer_= new QTimer (this); + hideTimer_->setSingleShot (true); + connect (hideTimer_, &QTimer::timeout, this, &QtFloatingToast::startFadeOut); + + fadeAnimation_= new QPropertyAnimation (this, "windowOpacity"); + fadeAnimation_->setDuration (200); +} + +QtFloatingToast::~QtFloatingToast ()= default; + +void +QtFloatingToast::showAbove (QWidget* anchorWidget, const QString& message, + int durationMs, Type type) { + if (!anchorWidget) return; + + type_= type; + label_->setText (message); + label_->adjustSize (); + + int padX= DpiUtils::scaled (20); + int padY= DpiUtils::scaled (10); + layout_->setContentsMargins (padX, padY, padX, padY); + + adjustSize (); + updatePosition (anchorWidget); + + setWindowOpacity (0.0); + show (); + raise (); + startFadeIn (); + + hideTimer_->start (durationMs); +} + +void +QtFloatingToast::showToast (QWidget* anchorWidget, const QString& message, + int durationMs, Type type) { + if (!anchorWidget) return; + auto* toast= new QtFloatingToast (anchorWidget->window ()); + toast->showAbove (anchorWidget, message, durationMs, type); +} + +void +QtFloatingToast::updatePosition (QWidget* anchorWidget) { + if (!anchorWidget) return; + QWidget* window= anchorWidget->window (); + int x = (window->width () - width ()) / 2; + int y = (window->height () - height ()) / 8; + move (x, y); +} + +void +QtFloatingToast::startFadeIn () { + fadeAnimation_->stop (); + fadeAnimation_->setStartValue (0.0); + fadeAnimation_->setEndValue (1.0); + fadeAnimation_->start (); +} + +void +QtFloatingToast::startFadeOut () { + fadeAnimation_->stop (); + fadeAnimation_->setStartValue (1.0); + fadeAnimation_->setEndValue (0.0); + disconnect (fadeAnimation_, &QPropertyAnimation::finished, nullptr, nullptr); + connect (fadeAnimation_, &QPropertyAnimation::finished, this, + &QObject::deleteLater); + fadeAnimation_->start (); +} + +void +QtFloatingToast::paintEvent (QPaintEvent* event) { + QPainter painter (this); + painter.setRenderHint (QPainter::Antialiasing); + + QRectF rect = this->rect ().adjusted (1, 1, -1, -1); + int radius= DpiUtils::scaled (8); + + painter.setPen (Qt::NoPen); + QColor bg; + switch (type_) { + case Success: + bg= QColor (46, 125, 50, 220); + break; + case Warning: + bg= QColor (245, 124, 0, 220); + break; + case Error: + bg= QColor (198, 40, 40, 220); + break; + default: + bg= QColor (50, 50, 50, 220); + } + painter.setBrush (bg); + painter.drawRoundedRect (rect, radius, radius); +} diff --git a/src/Plugins/Qt/qt_floating_toast.hpp b/src/Plugins/Qt/qt_floating_toast.hpp new file mode 100644 index 0000000000..1055d31652 --- /dev/null +++ b/src/Plugins/Qt/qt_floating_toast.hpp @@ -0,0 +1,52 @@ + +/****************************************************************************** + * MODULE : qt_floating_toast.cpp + * DESCRIPTION: Floating toast implementation + * 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_FLOATING_TOAST_HPP +#define QT_FLOATING_TOAST_HPP + +#include + +class QLabel; +class QHBoxLayout; +class QPropertyAnimation; +class QTimer; + +class QtFloatingToast : public QWidget { + Q_OBJECT + +public: + enum Type { Success, Warning, Error }; + + explicit QtFloatingToast (QWidget* parent= nullptr); + ~QtFloatingToast (); + + void showAbove (QWidget* anchorWidget, const QString& message, + int durationMs= 3000, Type type= Success); + + static void showToast (QWidget* anchorWidget, const QString& message, + int durationMs= 3000, Type type= Success); + +protected: + void paintEvent (QPaintEvent* event) override; + +private: + void updatePosition (QWidget* anchorWidget); + void startFadeIn (); + void startFadeOut (); + + QLabel* label_ = nullptr; + QHBoxLayout* layout_ = nullptr; + QPropertyAnimation* fadeAnimation_= nullptr; + QTimer* hideTimer_ = nullptr; + Type type_ = Success; +}; + +#endif From 3742ee8c9b25d2a055ff90eede1d4c44bc1cb5b8 Mon Sep 17 00:00:00 2001 From: Yuki Date: Fri, 8 May 2026 11:00:30 +0800 Subject: [PATCH 2/4] wip --- devel/216_38.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 devel/216_38.md diff --git a/devel/216_38.md b/devel/216_38.md new file mode 100644 index 0000000000..f6a5e61dd5 --- /dev/null +++ b/devel/216_38.md @@ -0,0 +1,53 @@ +# 216_38 最近文档列表增加文件不存在提示及自动清理 + +## 如何测试 + +1. 编译:`xmake b stem` +2. 启动 Mogan,打开启动页 **File** 页面。 +3. 正常打开若干个文档,使其出现在 **Recent** 最近文档列表中。 +4. 手动在文件系统中删除或移动其中某一个文档。 +5. 重新打开启动页(或切换标签页后切回),确认该文档已从 **Recent** 列表中自动消失。 +6. 若未重新打开启动页,直接点击该已失效的文档项: + - 确认弹出红色 Toast 提示 **"File not found, removed from recent list"**(中文:**"文件未找到,已从最近列表中移除"**)。 + - 确认该文档项从列表中即时移除。 + - 确认未触发文档加载,程序无卡死或异常。 +7. 点击其他正常的最近文档项,确认仍可正常打开。 +8. 切换中英文界面,确认 Toast 提示文案正确。 + +## 2026/05/08 实现说明 + +### What + +为启动页 File 页面的 **Recent** 最近文档列表引入文件存在性校验机制:加载时自动剔除已失效路径,点击时通过浮动 Toast 轻量提示用户并即时移除该条目。同时为后续全局复用新增了 `QtFloatingToast` 组件。 + +#### 修改文件 + +**src/Plugins/Qt/qt_floating_toast.hpp / qt_floating_toast.cpp(新增)** +- 实现无边框浮动 Toast 提示组件,支持 `Success` / `Warning` / `Error` 三种类型。 +- 使用 `QPropertyAnimation` 实现 200ms 淡入/淡出效果,`QTimer` 控制显示时长。 +- 静态工厂方法 `showToast` 自动管理组件生命周期(淡出完成后 `deleteLater`)。 +- `paintEvent` 根据类型绘制不同颜色的圆角矩形背景: + - `Success`:绿色 `#2e7d32` + - `Warning`:橙色 `#f57c00` + - `Error`:红色 `#c62828` +- 显示位置基于锚定窗口居中偏上(`window->height() / 8`),使用 `DpiUtils` 进行 DPI 适配。 + +**src/Plugins/Qt/qt_file_page.cpp** +- `loadRecentDocs()`:在加载最近文档路径后,新增 `QFile::exists` 过滤,仅保留磁盘上仍存在的文件,自动静默清理失效条目。 +- `onRecentDocClicked()`:点击时若检测到文件已不存在,调用 `QtFloatingToast::showToast` 显示 3 秒 Error 类型提示,随后调用 `removeRecentDoc` 从列表移除并直接返回,避免尝试加载不存在的文件。 + +**TeXmacs/plugins/lang/dic/en_US/zh_CN.scm** +- 新增翻译:`("File not found, removed from recent list" "文件未找到,已从最近列表中移除")`。 + +### Why + +1. **体验缺陷**:此前用户点击最近列表中已被删除或移动的文件时,Mogan 无任何反馈,既未提示错误,也未清理列表,用户会困惑为何点击无响应。 +2. **列表污染**:随着使用时间的推移,最近文档列表会积累大量失效路径(如文件被删除、移动、重命名),手动清理无入口。 +3. **轻量提示**:相比阻塞式弹窗(`QMessageBox`),Toast 提示不中断用户操作流,更适合"仅告知结果"的场景,且与现代化 UI 习惯一致。 +4. **组件复用**:将 Toast 封装为独立组件 `QtFloatingToast`,便于后续在其他场景(如保存成功、网络异常等)直接复用,避免重复造轮子。 + +### How + +- **双重保障**:加载时过滤(静默清理)+ 点击时检测(即时提示),既保证列表首次呈现即为有效数据,也覆盖了"列表已渲染后文件才被删除"的边界场景。 +- **生命周期自管理**:`showToast` 使用 `new QtFloatingToast` 创建实例,在 `startFadeOut` 的 `finished` 信号中绑定 `deleteLater`,无需调用方关心内存释放。 +- **无边框置顶窗口**:使用 `Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::Tool` 配合 `WA_TranslucentBackground`,确保 Toast 悬浮于主窗口之上且不影响焦点。 From e6f06cab2738fbcd829b33e20145aa88f2cefb43 Mon Sep 17 00:00:00 2001 From: Yuki Date: Fri, 8 May 2026 11:10:03 +0800 Subject: [PATCH 3/4] wip --- src/Plugins/Qt/qt_floating_toast.cpp | 11 ++++++----- src/Plugins/Qt/qt_floating_toast.hpp | 11 ++++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/Plugins/Qt/qt_floating_toast.cpp b/src/Plugins/Qt/qt_floating_toast.cpp index cfcd8846e3..5738273d12 100644 --- a/src/Plugins/Qt/qt_floating_toast.cpp +++ b/src/Plugins/Qt/qt_floating_toast.cpp @@ -81,8 +81,9 @@ void QtFloatingToast::updatePosition (QWidget* anchorWidget) { if (!anchorWidget) return; QWidget* window= anchorWidget->window (); - int x = (window->width () - width ()) / 2; - int y = (window->height () - height ()) / 8; + QRect geo = window->geometry (); + int x = geo.x () + (geo.width () - width ()) / 2; + int y = geo.y () + (geo.height () - height ()) / 8; move (x, y); } @@ -99,9 +100,9 @@ QtFloatingToast::startFadeOut () { fadeAnimation_->stop (); fadeAnimation_->setStartValue (1.0); fadeAnimation_->setEndValue (0.0); - disconnect (fadeAnimation_, &QPropertyAnimation::finished, nullptr, nullptr); - connect (fadeAnimation_, &QPropertyAnimation::finished, this, - &QObject::deleteLater); + if (fadeConnection_) disconnect (fadeConnection_); + fadeConnection_= connect (fadeAnimation_, &QPropertyAnimation::finished, this, + &QObject::deleteLater); fadeAnimation_->start (); } diff --git a/src/Plugins/Qt/qt_floating_toast.hpp b/src/Plugins/Qt/qt_floating_toast.hpp index 1055d31652..14c50ab2a8 100644 --- a/src/Plugins/Qt/qt_floating_toast.hpp +++ b/src/Plugins/Qt/qt_floating_toast.hpp @@ -42,11 +42,12 @@ class QtFloatingToast : public QWidget { void startFadeIn (); void startFadeOut (); - QLabel* label_ = nullptr; - QHBoxLayout* layout_ = nullptr; - QPropertyAnimation* fadeAnimation_= nullptr; - QTimer* hideTimer_ = nullptr; - Type type_ = Success; + QLabel* label_ = nullptr; + QHBoxLayout* layout_ = nullptr; + QPropertyAnimation* fadeAnimation_= nullptr; + QTimer* hideTimer_ = nullptr; + QMetaObject::Connection fadeConnection_; + Type type_= Success; }; #endif From 8db4c509ff8f42ec278b7b2dbeda2aeadb9ac8f1 Mon Sep 17 00:00:00 2001 From: Yuki Date: Fri, 8 May 2026 11:17:25 +0800 Subject: [PATCH 4/4] wip --- src/Plugins/Qt/qt_floating_toast.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Plugins/Qt/qt_floating_toast.hpp b/src/Plugins/Qt/qt_floating_toast.hpp index 14c50ab2a8..f5d6159866 100644 --- a/src/Plugins/Qt/qt_floating_toast.hpp +++ b/src/Plugins/Qt/qt_floating_toast.hpp @@ -1,6 +1,6 @@ /****************************************************************************** - * MODULE : qt_floating_toast.cpp + * MODULE : qt_floating_toast.hpp * DESCRIPTION: Floating toast implementation * COPYRIGHT : (C) 2026 Yuki Lu ******************************************************************************* @@ -46,7 +46,7 @@ class QtFloatingToast : public QWidget { QHBoxLayout* layout_ = nullptr; QPropertyAnimation* fadeAnimation_= nullptr; QTimer* hideTimer_ = nullptr; - QMetaObject::Connection fadeConnection_; + QMetaObject::Connection fadeConnection_{}; Type type_= Success; };