Files
Renderive/YSGraphic_Core/Qt/Message.cpp
T
2026-06-16 13:33:27 +08:00

479 lines
12 KiB
C++

#include "Message.h"
#include <QApplication>
#include <QLabel>
#include <QVBoxLayout>
#include <QGraphicsOpacityEffect>
#include <QGraphicsDropShadowEffect>
#include <QPropertyAnimation>
#include <QVariantAnimation>
#include <QTimer>
#include <QProgressBar>
#include <QPainter>
#include <QGraphicsScene>
#include <QGraphicsPixmapItem>
#include <QGraphicsBlurEffect>
#include <QRandomGenerator>
#include <algorithm>
#include <QDebug>
namespace Psc::Message {
static Config global_config;
class Toast;
class ResizeWatcher;
static QHash<QWidget*, QVector<QPointer<Toast>>> containers;
static QHash<QWidget*, QPointer<ResizeWatcher>> watchers;
static QHash<QWidget*, QQueue<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 ThemeColors {
QColor bg;
QColor text;
QColor progress_bg;
QColor progress_fg;
};
static ThemeColors 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;
}
ThemeColors 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 ThemeColors& 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{};
ThemeColors 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].isEmpty())
return;
auto item = queues[top].dequeue();
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.removeFirst();
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].enqueue({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 Message