479 lines
12 KiB
C++
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
|