Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions TeXmacs/plugins/lang/dic/en_US/zh_CN.scm
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,7 @@
("field" "区域")
("figure" "图")
("file name" "文件名")
("File not found, removed from recent list" "文件未找到,已从最近列表中移除")
("file not found" "无此文件")
("file type" "文件类型")
("file" "文件")
Expand Down
53 changes: 53 additions & 0 deletions devel/216_38.md
Original file line number Diff line number Diff line change
@@ -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 悬浮于主窗口之上且不影响焦点。
17 changes: 17 additions & 0 deletions src/Plugins/Qt/qt_file_page.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
#include <memory>

#include "qt_dpi_utils.hpp"
#include "qt_floating_toast.hpp"
#include "qt_utilities.hpp"
#include "s7_tm.hpp"
#include "sys_utils.hpp"
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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) * ")");
Expand Down
134 changes: 134 additions & 0 deletions src/Plugins/Qt/qt_floating_toast.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@

/******************************************************************************
* 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 <http://www.gnu.org/licenses/gpl-3.0.html>.
******************************************************************************/

#include "qt_floating_toast.hpp"
#include "qt_dpi_utils.hpp"

#include <QApplication>
#include <QHBoxLayout>
#include <QLabel>
#include <QPainter>
#include <QPropertyAnimation>
#include <QScreen>
#include <QTimer>

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 ();
QRect geo = window->geometry ();
int x = geo.x () + (geo.width () - width ()) / 2;
int y = geo.y () + (geo.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);
if (fadeConnection_) disconnect (fadeConnection_);
fadeConnection_= 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);
}
53 changes: 53 additions & 0 deletions src/Plugins/Qt/qt_floating_toast.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@

/******************************************************************************
* MODULE : qt_floating_toast.hpp
* 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 <http://www.gnu.org/licenses/gpl-3.0.html>.
******************************************************************************/

#ifndef QT_FLOATING_TOAST_HPP
#define QT_FLOATING_TOAST_HPP

#include <QWidget>

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;
QMetaObject::Connection fadeConnection_{};
Type type_= Success;
};

#endif
Loading