Skip to content

Commit 3c0adc0

Browse files
authored
[216_38] recent修复,已删除文件自动清理 (#3288)
1 parent dda0bfc commit 3c0adc0

5 files changed

Lines changed: 258 additions & 0 deletions

File tree

TeXmacs/plugins/lang/dic/en_US/zh_CN.scm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,7 @@
821821
("field" "区域")
822822
("figure" "")
823823
("file name" "文件名")
824+
("File not found, removed from recent list" "文件未找到,已从最近列表中移除")
824825
("file not found" "无此文件")
825826
("file type" "文件类型")
826827
("file" "文件")

devel/216_38.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# 216_38 最近文档列表增加文件不存在提示及自动清理
2+
3+
## 如何测试
4+
5+
1. 编译:`xmake b stem`
6+
2. 启动 Mogan,打开启动页 **File** 页面。
7+
3. 正常打开若干个文档,使其出现在 **Recent** 最近文档列表中。
8+
4. 手动在文件系统中删除或移动其中某一个文档。
9+
5. 重新打开启动页(或切换标签页后切回),确认该文档已从 **Recent** 列表中自动消失。
10+
6. 若未重新打开启动页,直接点击该已失效的文档项:
11+
- 确认弹出红色 Toast 提示 **"File not found, removed from recent list"**(中文:**"文件未找到,已从最近列表中移除"**)。
12+
- 确认该文档项从列表中即时移除。
13+
- 确认未触发文档加载,程序无卡死或异常。
14+
7. 点击其他正常的最近文档项,确认仍可正常打开。
15+
8. 切换中英文界面,确认 Toast 提示文案正确。
16+
17+
## 2026/05/08 实现说明
18+
19+
### What
20+
21+
为启动页 File 页面的 **Recent** 最近文档列表引入文件存在性校验机制:加载时自动剔除已失效路径,点击时通过浮动 Toast 轻量提示用户并即时移除该条目。同时为后续全局复用新增了 `QtFloatingToast` 组件。
22+
23+
#### 修改文件
24+
25+
**src/Plugins/Qt/qt_floating_toast.hpp / qt_floating_toast.cpp(新增)**
26+
- 实现无边框浮动 Toast 提示组件,支持 `Success` / `Warning` / `Error` 三种类型。
27+
- 使用 `QPropertyAnimation` 实现 200ms 淡入/淡出效果,`QTimer` 控制显示时长。
28+
- 静态工厂方法 `showToast` 自动管理组件生命周期(淡出完成后 `deleteLater`)。
29+
- `paintEvent` 根据类型绘制不同颜色的圆角矩形背景:
30+
- `Success`:绿色 `#2e7d32`
31+
- `Warning`:橙色 `#f57c00`
32+
- `Error`:红色 `#c62828`
33+
- 显示位置基于锚定窗口居中偏上(`window->height() / 8`),使用 `DpiUtils` 进行 DPI 适配。
34+
35+
**src/Plugins/Qt/qt_file_page.cpp**
36+
- `loadRecentDocs()`:在加载最近文档路径后,新增 `QFile::exists` 过滤,仅保留磁盘上仍存在的文件,自动静默清理失效条目。
37+
- `onRecentDocClicked()`:点击时若检测到文件已不存在,调用 `QtFloatingToast::showToast` 显示 3 秒 Error 类型提示,随后调用 `removeRecentDoc` 从列表移除并直接返回,避免尝试加载不存在的文件。
38+
39+
**TeXmacs/plugins/lang/dic/en_US/zh_CN.scm**
40+
- 新增翻译:`("File not found, removed from recent list" "文件未找到,已从最近列表中移除")`
41+
42+
### Why
43+
44+
1. **体验缺陷**:此前用户点击最近列表中已被删除或移动的文件时,Mogan 无任何反馈,既未提示错误,也未清理列表,用户会困惑为何点击无响应。
45+
2. **列表污染**:随着使用时间的推移,最近文档列表会积累大量失效路径(如文件被删除、移动、重命名),手动清理无入口。
46+
3. **轻量提示**:相比阻塞式弹窗(`QMessageBox`),Toast 提示不中断用户操作流,更适合"仅告知结果"的场景,且与现代化 UI 习惯一致。
47+
4. **组件复用**:将 Toast 封装为独立组件 `QtFloatingToast`,便于后续在其他场景(如保存成功、网络异常等)直接复用,避免重复造轮子。
48+
49+
### How
50+
51+
- **双重保障**:加载时过滤(静默清理)+ 点击时检测(即时提示),既保证列表首次呈现即为有效数据,也覆盖了"列表已渲染后文件才被删除"的边界场景。
52+
- **生命周期自管理**`showToast` 使用 `new QtFloatingToast` 创建实例,在 `startFadeOut``finished` 信号中绑定 `deleteLater`,无需调用方关心内存释放。
53+
- **无边框置顶窗口**:使用 `Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::Tool` 配合 `WA_TranslucentBackground`,确保 Toast 悬浮于主窗口之上且不影响焦点。

src/Plugins/Qt/qt_file_page.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
#include <memory>
3939

4040
#include "qt_dpi_utils.hpp"
41+
#include "qt_floating_toast.hpp"
4142
#include "qt_utilities.hpp"
4243
#include "s7_tm.hpp"
4344
#include "sys_utils.hpp"
@@ -437,6 +438,14 @@ QtFilePage::loadRecentDocs () {
437438
}
438439
recentPaths.removeDuplicates ();
439440

441+
QStringList existingPaths;
442+
for (const QString& path : recentPaths) {
443+
if (QFile::exists (path)) {
444+
existingPaths.append (path);
445+
}
446+
}
447+
recentPaths= existingPaths;
448+
440449
QString filePath= getRecentDocsFilePath ();
441450
QFile file (filePath);
442451
if (!file.open (QIODevice::ReadOnly)) {
@@ -685,6 +694,14 @@ QtFilePage::onRecentDocClicked (QListWidgetItem* item) {
685694
QString path= item->data (Qt::UserRole).toString ();
686695
if (path.isEmpty ()) return;
687696

697+
if (!QFile::exists (path)) {
698+
QtFloatingToast::showToast (
699+
recentList_, qt_translate ("File not found, removed from recent list"),
700+
3000, QtFloatingToast::Error);
701+
removeRecentDoc (path);
702+
return;
703+
}
704+
688705
addRecentDoc (path);
689706

690707
eval_scheme ("(load-document " * qt_scheme_quote_utf8 (path) * ")");
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
2+
/******************************************************************************
3+
* MODULE : qt_floating_toast.cpp
4+
* DESCRIPTION: Floating toast implementation
5+
* COPYRIGHT : (C) 2026 Yuki Lu
6+
*******************************************************************************
7+
* This software falls under the GNU general public license version 3 or later.
8+
* It comes WITHOUT ANY WARRANTY WHATSOEVER. For details, see the file LICENSE
9+
* in the root directory or <http://www.gnu.org/licenses/gpl-3.0.html>.
10+
******************************************************************************/
11+
12+
#include "qt_floating_toast.hpp"
13+
#include "qt_dpi_utils.hpp"
14+
15+
#include <QApplication>
16+
#include <QHBoxLayout>
17+
#include <QLabel>
18+
#include <QPainter>
19+
#include <QPropertyAnimation>
20+
#include <QScreen>
21+
#include <QTimer>
22+
23+
QtFloatingToast::QtFloatingToast (QWidget* parent)
24+
: QWidget (parent,
25+
Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::Tool) {
26+
setAttribute (Qt::WA_TranslucentBackground);
27+
28+
label_= new QLabel (this);
29+
label_->setAlignment (Qt::AlignCenter);
30+
label_->setWordWrap (true);
31+
label_->setStyleSheet ("QLabel { color: #ffffff; }");
32+
label_->setFont (DpiUtils::scaledFont (label_->font (), 14));
33+
34+
layout_= new QHBoxLayout (this);
35+
layout_->setContentsMargins (0, 0, 0, 0);
36+
layout_->addWidget (label_, 0, Qt::AlignCenter);
37+
38+
hideTimer_= new QTimer (this);
39+
hideTimer_->setSingleShot (true);
40+
connect (hideTimer_, &QTimer::timeout, this, &QtFloatingToast::startFadeOut);
41+
42+
fadeAnimation_= new QPropertyAnimation (this, "windowOpacity");
43+
fadeAnimation_->setDuration (200);
44+
}
45+
46+
QtFloatingToast::~QtFloatingToast ()= default;
47+
48+
void
49+
QtFloatingToast::showAbove (QWidget* anchorWidget, const QString& message,
50+
int durationMs, Type type) {
51+
if (!anchorWidget) return;
52+
53+
type_= type;
54+
label_->setText (message);
55+
label_->adjustSize ();
56+
57+
int padX= DpiUtils::scaled (20);
58+
int padY= DpiUtils::scaled (10);
59+
layout_->setContentsMargins (padX, padY, padX, padY);
60+
61+
adjustSize ();
62+
updatePosition (anchorWidget);
63+
64+
setWindowOpacity (0.0);
65+
show ();
66+
raise ();
67+
startFadeIn ();
68+
69+
hideTimer_->start (durationMs);
70+
}
71+
72+
void
73+
QtFloatingToast::showToast (QWidget* anchorWidget, const QString& message,
74+
int durationMs, Type type) {
75+
if (!anchorWidget) return;
76+
auto* toast= new QtFloatingToast (anchorWidget->window ());
77+
toast->showAbove (anchorWidget, message, durationMs, type);
78+
}
79+
80+
void
81+
QtFloatingToast::updatePosition (QWidget* anchorWidget) {
82+
if (!anchorWidget) return;
83+
QWidget* window= anchorWidget->window ();
84+
QRect geo = window->geometry ();
85+
int x = geo.x () + (geo.width () - width ()) / 2;
86+
int y = geo.y () + (geo.height () - height ()) / 8;
87+
move (x, y);
88+
}
89+
90+
void
91+
QtFloatingToast::startFadeIn () {
92+
fadeAnimation_->stop ();
93+
fadeAnimation_->setStartValue (0.0);
94+
fadeAnimation_->setEndValue (1.0);
95+
fadeAnimation_->start ();
96+
}
97+
98+
void
99+
QtFloatingToast::startFadeOut () {
100+
fadeAnimation_->stop ();
101+
fadeAnimation_->setStartValue (1.0);
102+
fadeAnimation_->setEndValue (0.0);
103+
if (fadeConnection_) disconnect (fadeConnection_);
104+
fadeConnection_= connect (fadeAnimation_, &QPropertyAnimation::finished, this,
105+
&QObject::deleteLater);
106+
fadeAnimation_->start ();
107+
}
108+
109+
void
110+
QtFloatingToast::paintEvent (QPaintEvent* event) {
111+
QPainter painter (this);
112+
painter.setRenderHint (QPainter::Antialiasing);
113+
114+
QRectF rect = this->rect ().adjusted (1, 1, -1, -1);
115+
int radius= DpiUtils::scaled (8);
116+
117+
painter.setPen (Qt::NoPen);
118+
QColor bg;
119+
switch (type_) {
120+
case Success:
121+
bg= QColor (46, 125, 50, 220);
122+
break;
123+
case Warning:
124+
bg= QColor (245, 124, 0, 220);
125+
break;
126+
case Error:
127+
bg= QColor (198, 40, 40, 220);
128+
break;
129+
default:
130+
bg= QColor (50, 50, 50, 220);
131+
}
132+
painter.setBrush (bg);
133+
painter.drawRoundedRect (rect, radius, radius);
134+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
2+
/******************************************************************************
3+
* MODULE : qt_floating_toast.hpp
4+
* DESCRIPTION: Floating toast implementation
5+
* COPYRIGHT : (C) 2026 Yuki Lu
6+
*******************************************************************************
7+
* This software falls under the GNU general public license version 3 or later.
8+
* It comes WITHOUT ANY WARRANTY WHATSOEVER. For details, see the file LICENSE
9+
* in the root directory or <http://www.gnu.org/licenses/gpl-3.0.html>.
10+
******************************************************************************/
11+
12+
#ifndef QT_FLOATING_TOAST_HPP
13+
#define QT_FLOATING_TOAST_HPP
14+
15+
#include <QWidget>
16+
17+
class QLabel;
18+
class QHBoxLayout;
19+
class QPropertyAnimation;
20+
class QTimer;
21+
22+
class QtFloatingToast : public QWidget {
23+
Q_OBJECT
24+
25+
public:
26+
enum Type { Success, Warning, Error };
27+
28+
explicit QtFloatingToast (QWidget* parent= nullptr);
29+
~QtFloatingToast ();
30+
31+
void showAbove (QWidget* anchorWidget, const QString& message,
32+
int durationMs= 3000, Type type= Success);
33+
34+
static void showToast (QWidget* anchorWidget, const QString& message,
35+
int durationMs= 3000, Type type= Success);
36+
37+
protected:
38+
void paintEvent (QPaintEvent* event) override;
39+
40+
private:
41+
void updatePosition (QWidget* anchorWidget);
42+
void startFadeIn ();
43+
void startFadeOut ();
44+
45+
QLabel* label_ = nullptr;
46+
QHBoxLayout* layout_ = nullptr;
47+
QPropertyAnimation* fadeAnimation_= nullptr;
48+
QTimer* hideTimer_ = nullptr;
49+
QMetaObject::Connection fadeConnection_{};
50+
Type type_= Success;
51+
};
52+
53+
#endif

0 commit comments

Comments
 (0)