Files
Renderive/Widget/Qt/Message.cpp
T
2026-07-29 18:08:57 +08:00

373 lines
12 KiB
C++

#include "Message.h"
#include <QApplication>
#include <QDebug>
#include <QGraphicsBlurEffect>
#include <QGraphicsDropShadowEffect>
#include <QGraphicsOpacityEffect>
#include <QGraphicsPixmapItem>
#include <QGraphicsScene>
#include <QLabel>
#include <QPainter>
#include <QProgressBar>
#include <QPropertyAnimation>
#include <QRandomGenerator>
#include <QTimer>
#include <QVBoxLayout>
#include <QVariantAnimation>
#include <algorithm>
namespace Psc::Message {
static Config global_config;
class Toast;
class Resize_Watcher;
static std::unordered_map<QWidget*, std::vector<QPointer<Toast>>> containers;
static std::unordered_map<QWidget*, QPointer<Resize_Watcher>> watchers;
static std::unordered_map<QWidget*, std::queue<std::pair<Type, QString>>> queues;
/* -------------------- 工具函数 -------------------- */
static bool is_dark_palette() {
QColor c = QApplication::palette().window().color();
return c.lightness() < 128;
}
static QColor interpolate(const QColor& a, const QColor& b, double t) {
return QColor(
a.red() + (b.red() - a.red()) * t,
a.green() + (b.green() - a.green()) * t,
a.blue() + (b.blue() - a.blue()) * t,
a.alpha() + (b.alpha() - a.alpha()) * t);
}
struct Theme_Colors {
QColor bg;
QColor text;
QColor progress_bg;
QColor progress_fg;
};
static Theme_Colors theme_for(Type t) {
bool dark;
if (global_config.theme == Theme_Mode::Auto)
dark = is_dark_palette();
else
dark = (global_config.theme == Theme_Mode::Dark);
QColor base;
switch (t) {
case success:
base = QColor("#52C41A");
break;
case error:
base = QColor("#F5222D");
break;
case warning:
base = QColor("#FAAD14");
break;
default:
base = QColor("#1890FF");
break;
}
Theme_Colors c;
if (dark) {
c.bg = base.darker(250);
c.bg.setAlpha(210);
c.text = Qt::white;
c.progress_fg = base;
c.progress_bg = QColor(255, 255, 255, 30);
}
else {
c.bg = base.lighter(180);
c.bg.setAlpha(220);
c.text = QColor(30, 30, 30);
c.progress_fg = base.darker(110);
c.progress_bg = QColor(0, 0, 0, 20);
}
return c;
}
/* -------------------- 毛玻璃 -------------------- */
static QPixmap blur_pixmap(const QPixmap& src, int radius) {
QGraphicsScene scene;
QGraphicsPixmapItem item;
item.setPixmap(src);
QGraphicsBlurEffect blur;
blur.setBlurRadius(radius);
item.setGraphicsEffect(&blur);
scene.addItem(&item);
QImage result(src.size(), QImage::Format_ARGB32);
result.fill(Qt::transparent);
QPainter painter(&result);
scene.render(&painter);
return QPixmap::fromImage(result);
}
static QPixmap generate_noise(int w, int h) {
QImage img(w, h, QImage::Format_ARGB32);
for (int y = 0; y < h; ++y)
for (int x = 0; x < w; ++x) {
int gray = QRandomGenerator::global()->bounded(20);
img.setPixelColor(x, y, QColor(255, 255, 255, gray));
}
return QPixmap::fromImage(img);
}
/* -------------------- Toast -------------------- */
class Toast : public QWidget {
public:
Toast(Type t,
const QString& text,
QWidget* parent,
int duration) : QWidget(parent),
type(t),
duration_ms(duration) {
setAttribute(Qt::WA_DeleteOnClose);
setWindowFlags(Qt::FramelessWindowHint);
auto shadow = new QGraphicsDropShadowEffect(this);
shadow->setBlurRadius(30);
shadow->setOffset(0, 8);
shadow->setColor(QColor(0, 0, 0, 120));
setGraphicsEffect(shadow);
opacity = new QGraphicsOpacityEffect(this);
setGraphicsEffect(opacity);
opacity->setOpacity(0);
fade = new QPropertyAnimation(opacity, "opacity", this);
fade->setDuration(200 * global_config.animation_speed);
move_anim = new QPropertyAnimation(this, "pos", this);
move_anim->setDuration(400 * global_config.animation_speed);
move_anim->setEasingCurve(QEasingCurve::OutBack);
label = new QLabel(text, this);
label->setWordWrap(true);
label->setMaximumWidth(global_config.max_width);
progress = new QProgressBar(this);
progress->setRange(0, duration_ms);
progress->setValue(duration_ms);
progress->setTextVisible(false);
progress->setFixedHeight(3);
auto layout = new QVBoxLayout(this);
layout->setContentsMargins(16, 12, 16, 8);
layout->addWidget(label);
layout->addWidget(progress);
adjustSize();
apply_theme(theme_for(type));
start_timer();
}
void appear_from(const QPoint& start, const QPoint& end) {
prepare_blur();
move(start);
show();
fade->setStartValue(0);
fade->setEndValue(1);
fade->start();
move_anim->setStartValue(start);
move_anim->setEndValue(end);
move_anim->start();
}
void move_to(const QPoint& target) {
if (pos() == target)
return;
move_anim->stop();
move_anim->setStartValue(pos());
move_anim->setEndValue(target);
move_anim->start();
}
void disappear() {
fade->setStartValue(1);
fade->setEndValue(0);
connect(fade, &QPropertyAnimation::finished,
this, &QWidget::deleteLater);
fade->start();
}
protected:
void mousePressEvent(QMouseEvent*) override {
disappear();
}
void enterEvent(QEvent*) override {
if (timer)
timer->stop();
}
void leaveEvent(QEvent*) override {
if (timer)
timer->start();
}
void paintEvent(QPaintEvent* e) override {
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
QRectF r = rect();
QPainterPath path;
path.addRoundedRect(r, 12, 12);
// 1️⃣ 裁剪圆角区域
painter.setClipPath(path);
// 2️⃣ 画毛玻璃
if (global_config.enable_blur && !blurred_background.isNull()) {
painter.drawPixmap(r.toRect(), blurred_background);
}
// 3️⃣ 半透明背景
painter.fillPath(path, current_theme.bg);
// 4️⃣ Acrylic 噪点
if (global_config.acrylic_mode) {
painter.drawPixmap(r.toRect(),
generate_noise(width(), height()));
}
// 5️⃣ 解除裁剪
painter.setClipping(false);
// 6️⃣ 调用基类,让子控件正常绘制
QWidget::paintEvent(e);
}
private:
void prepare_blur() {
if (!global_config.enable_blur)
return;
QWidget* p = parentWidget();
if (!p)
return;
QPixmap grab = p->grab(geometry());
blurred_background = blur_pixmap(grab, 20);
}
void apply_theme(const Theme_Colors& c) {
current_theme = c;
label->setStyleSheet(QString(
"background:transparent;color:%1;")
.arg(c.text.name()));
progress->setStyleSheet(QString(
"QProgressBar{background:%1;border:none;}"
"QProgressBar::chunk{background:%2;border-radius:2px;}")
.arg(c.progress_bg.name(QColor::HexArgb))
.arg(c.progress_fg.name()));
}
void start_timer() {
if (!timer) {
timer = new QTimer(this);
timer->setInterval(16);
connect(timer, &QTimer::timeout, this, [this]() {
elapsed_ms += 16;
int remain = duration_ms - elapsed_ms;
progress->setValue(std::max(0, remain));
if (remain <= 0) {
timer->stop();
disappear();
}
});
}
timer->start();
}
Type type;
int duration_ms;
int elapsed_ms = 0;
QLabel* label{};
QProgressBar* progress{};
QTimer* timer{};
QGraphicsOpacityEffect* opacity{};
QPropertyAnimation* fade{};
QPropertyAnimation* move_anim{};
Theme_Colors current_theme;
QPixmap blurred_background;
};
/* -------------------- 布局 -------------------- */
static QWidget* top_parent(QWidget* w) {
while (w && w->parentWidget())
w = w->parentWidget();
return w;
}
static void relayout(QWidget* top, Corner corner) {
auto& list = containers[top];
constexpr int margin = 24;
constexpr int spacing = 16;
int y = (corner == Top_Left || corner == Top_Right)
? margin
: top->height() - margin;
for (auto& toast : list) {
if (!toast)
continue;
QPoint target;
if (corner == Top_Left) {
target = {margin, y};
y += toast->height() + spacing;
}
else if (corner == Top_Right) {
target = {top->width() - toast->width() - margin, y};
y += toast->height() + spacing;
}
else if (corner == Bottom_Left) {
y -= toast->height();
target = {margin, y};
y -= spacing;
}
else {
y -= toast->height();
target = {top->width() - toast->width() - margin, y};
y -= spacing;
}
toast->move_to(target);
}
}
static void process_queue(QWidget* top, Corner corner) {
if (queues[top].empty())
return;
auto item = std::move(queues[top].front());
queues[top].pop();
auto* toast = new Toast(
item.first,
item.second,
top,
global_config.default_duration);
auto& list = containers[top];
constexpr int margin = 24;
constexpr int spacing = 16;
// --------- 1️⃣ 计算最终位置 ----------
int y = (corner == Top_Left || corner == Top_Right)
? margin
: top->height() - margin;
for (auto& t : list) {
if (!t)
continue;
if (corner == Top_Left || corner == Top_Right)
y += t->height() + spacing;
else
y -= t->height() + spacing;
}
QPoint final_pos;
if (corner == Top_Left)
final_pos = QPoint(margin, y);
else if (corner == Top_Right)
final_pos = QPoint(top->width() - toast->width() - margin, y);
else if (corner == Bottom_Left)
final_pos = QPoint(margin, y - toast->height());
else
final_pos = QPoint(top->width() - toast->width() - margin,
y - toast->height());
// --------- 2️⃣ 立即定位到 final ----------
toast->move(final_pos);
// --------- 3️⃣ 加入容器 ----------
list.push_back(toast);
// --------- 4️⃣ 计算飞入起点 ----------
QPoint start = final_pos;
if (corner == Top_Right || corner == Bottom_Right)
start.setX(top->width());
else
start.setX(-toast->width());
// --------- 5️⃣ 飞入 ----------
toast->appear_from(start, final_pos);
// --------- 6️⃣ 超限处理 ----------
if (list.size() > global_config.max_count) {
auto old = list.front();
list.erase(list.begin());
if (old)
old->disappear();
}
// --------- 7️⃣ 旧 toast 重排 ----------
relayout(top, corner);
}
/* -------------------- 公共接口 -------------------- */
void show(Type type,
const QString& text,
QWidget* parent,
int,
Corner corner) {
if (!parent)
return;
QWidget* top = top_parent(parent);
if (!top)
return;
queues[top].push({type, text});
QTimer::singleShot(global_config.queue_interval,
top,
[top, corner]() {
process_queue(top, corner);
});
}
void set_config(const Config& cfg) {
global_config = cfg;
}
} // namespace Psc::Message