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