首次提交
This commit is contained in:
@@ -0,0 +1,479 @@
|
||||
#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
|
||||
Reference in New Issue
Block a user