2 changed files with 159 additions and 0 deletions
@ -0,0 +1,127 @@ |
|||||||
|
#include "message_notification.h" |
||||||
|
#include "user_activity_monitor.h" |
||||||
|
#include "debug_ui.h" |
||||||
|
|
||||||
|
#include <QCoreApplication> |
||||||
|
#include <QGuiApplication> |
||||||
|
#include <QScreen> |
||||||
|
#include <QLabel> |
||||||
|
#include <QPushButton> |
||||||
|
#include <QHBoxLayout> |
||||||
|
#include <QVBoxLayout> |
||||||
|
#include <QMouseEvent> |
||||||
|
#include <QHideEvent> |
||||||
|
#include <QCloseEvent> |
||||||
|
|
||||||
|
MessageNotification::MessageNotification(QWidget* parent) |
||||||
|
: QWidget(parent, Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::WindowDoesNotAcceptFocus) |
||||||
|
, m_activity(new UserActivityMonitor(this)) |
||||||
|
{ |
||||||
|
setObjectName("messageNotification"); |
||||||
|
setAttribute(Qt::WA_ShowWithoutActivating); |
||||||
|
#ifdef Q_OS_MACOS |
||||||
|
setAttribute(Qt::WA_MacAlwaysShowToolWindow); |
||||||
|
#endif |
||||||
|
setFocusPolicy(Qt::NoFocus); |
||||||
|
setCursor(Qt::PointingHandCursor); |
||||||
|
setStyleSheet("#messageNotification { background: #353535; border: 1px solid #777; border-radius: 8px; }" |
||||||
|
"#messageNotification QLabel { color: #eee; border: none; }" |
||||||
|
"#messageNotification QPushButton { color: #eee; background: transparent; border: none; padding: 4px; }" |
||||||
|
"#messageNotification QPushButton:hover { background: #555; }"); |
||||||
|
auto* layout = new QVBoxLayout(this); |
||||||
|
layout->setContentsMargins(14, 10, 10, 14); |
||||||
|
auto* heading = new QHBoxLayout(); |
||||||
|
auto* title = new QLabel(QCoreApplication::translate("ChatGui", "Incoming message"), this); |
||||||
|
title->setAttribute(Qt::WA_TransparentForMouseEvents); |
||||||
|
heading->addWidget(title, 1); |
||||||
|
auto* closeButton = new QPushButton(QStringLiteral("✕"), this); |
||||||
|
closeButton->setObjectName("notificationCloseButton"); |
||||||
|
closeButton->setToolTip(QCoreApplication::translate("ChatGui", "Close")); |
||||||
|
closeButton->setFocusPolicy(Qt::NoFocus); |
||||||
|
heading->addWidget(closeButton); |
||||||
|
layout->addLayout(heading); |
||||||
|
m_body = new QLabel(this); |
||||||
|
m_body->setTextFormat(Qt::PlainText); |
||||||
|
m_body->setWordWrap(true); |
||||||
|
m_body->setAttribute(Qt::WA_TransparentForMouseEvents); |
||||||
|
layout->addWidget(m_body); |
||||||
|
m_hideTimer.setSingleShot(true); |
||||||
|
m_hideTimer.setInterval(10000); |
||||||
|
m_hideTimer.setTimerType(Qt::PreciseTimer); |
||||||
|
connect(closeButton, &QPushButton::clicked, this, [this] { dismiss("close-button"); }); |
||||||
|
connect(&m_hideTimer, &QTimer::timeout, this, [this] { dismiss("activity-timeout"); }); |
||||||
|
connect(m_activity, &UserActivityMonitor::activity, this, [this] { |
||||||
|
if (!isVisible() || m_hideTimer.isActive()) return; |
||||||
|
GUI_DEBUG("notification: first activity target=%s hide_in_ms=%d", qPrintable(m_target), m_hideTimer.interval()); |
||||||
|
m_hideTimer.start(); |
||||||
|
}); |
||||||
|
GUI_DEBUG("notification: initialized platform=%s hide_after_activity_ms=%d", |
||||||
|
qPrintable(QGuiApplication::platformName()), m_hideTimer.interval()); |
||||||
|
if (QGuiApplication::platformName().startsWith("wayland")) |
||||||
|
GUI_WARN("notification: native Wayland compositor controls placement and stacking; position near tray is best effort"); |
||||||
|
} |
||||||
|
|
||||||
|
MessageNotification::~MessageNotification() { |
||||||
|
m_activity->stop(); |
||||||
|
GUI_DEBUG("notification: destroyed target=%s visible=%d timer_active=%d", |
||||||
|
qPrintable(m_target), int(isVisible()), int(m_hideTimer.isActive())); |
||||||
|
} |
||||||
|
|
||||||
|
void MessageNotification::showMessage(const QString& target, const QString& sender, const QString& channel, const QRect& trayGeometry) { |
||||||
|
QScreen* screen = trayGeometry.isEmpty() ? nullptr : QGuiApplication::screenAt(trayGeometry.center()); |
||||||
|
if (!screen && parentWidget()) screen = parentWidget()->screen(); |
||||||
|
if (!screen) screen = QGuiApplication::primaryScreen(); |
||||||
|
if (!screen || screen->availableGeometry().isEmpty()) { |
||||||
|
GUI_ERROR("notification: no available screen target=%s", qPrintable(target)); |
||||||
|
dismiss("screen-unavailable"); |
||||||
|
return; |
||||||
|
} |
||||||
|
const QRect area = screen->availableGeometry(); |
||||||
|
const bool replacing = isVisible(); |
||||||
|
m_hideTimer.stop(); |
||||||
|
m_target = target; |
||||||
|
m_body->setText(QCoreApplication::translate("ChatGui", "New message from %1").arg(sender) + '\n' + channel); |
||||||
|
setFixedWidth(qMin(360, qMax(1, area.width() - 24))); |
||||||
|
adjustSize(); |
||||||
|
int x = area.right() - width() - 11, y = area.bottom() - height() - 11; |
||||||
|
if (!trayGeometry.isEmpty()) { |
||||||
|
x = trayGeometry.center().x() - width() / 2; |
||||||
|
y = trayGeometry.center().y() < area.center().y() ? trayGeometry.bottom() + 8 : trayGeometry.top() - height() - 8; |
||||||
|
} |
||||||
|
x = qBound(area.left(), x, qMax(area.left(), area.right() - width() + 1)); |
||||||
|
y = qBound(area.top(), y, qMax(area.top(), area.bottom() - height() + 1)); |
||||||
|
move(x, y); |
||||||
|
show(); |
||||||
|
m_activity->start(); |
||||||
|
GUI_INFO("notification: %s target=%s screen=%s position=%d,%d tray_anchor=%d waiting_for_activity", |
||||||
|
replacing ? "updated" : "shown", qPrintable(target), qPrintable(screen->name()), x, y, int(!trayGeometry.isEmpty())); |
||||||
|
} |
||||||
|
|
||||||
|
void MessageNotification::dismiss(const char* reason) { |
||||||
|
if (isVisible()) GUI_INFO("notification: hidden target=%s reason=%s", qPrintable(m_target), reason); |
||||||
|
m_hideTimer.stop(); |
||||||
|
m_activity->stop(); |
||||||
|
hide(); |
||||||
|
} |
||||||
|
|
||||||
|
void MessageNotification::mouseReleaseEvent(QMouseEvent* event) { |
||||||
|
if (event->button() == Qt::LeftButton && rect().contains(event->pos())) { |
||||||
|
const QString target = m_target; |
||||||
|
dismiss("open-chat"); |
||||||
|
emit activated(target); |
||||||
|
event->accept(); |
||||||
|
return; |
||||||
|
} |
||||||
|
QWidget::mouseReleaseEvent(event); |
||||||
|
} |
||||||
|
|
||||||
|
void MessageNotification::hideEvent(QHideEvent* event) { |
||||||
|
m_hideTimer.stop(); |
||||||
|
m_activity->stop(); |
||||||
|
QWidget::hideEvent(event); |
||||||
|
} |
||||||
|
|
||||||
|
void MessageNotification::closeEvent(QCloseEvent* event) { |
||||||
|
dismiss("close-event"); |
||||||
|
event->ignore(); |
||||||
|
} |
||||||
@ -0,0 +1,32 @@ |
|||||||
|
/* Одно уведомление о сообщениях: не берёт фокус, ждёт активности и скрывается через 10 секунд.
|
||||||
|
* Новое сообщение заменяет содержимое и отменяет прежний отсчёт. activated передаёт ключ канала/DM владельцу. */ |
||||||
|
#pragma once |
||||||
|
|
||||||
|
#include <QWidget> |
||||||
|
#include <QTimer> |
||||||
|
|
||||||
|
class QLabel; |
||||||
|
class UserActivityMonitor; |
||||||
|
|
||||||
|
class MessageNotification : public QWidget { |
||||||
|
Q_OBJECT |
||||||
|
public: |
||||||
|
explicit MessageNotification(QWidget* parent = nullptr); |
||||||
|
~MessageNotification() override; |
||||||
|
void showMessage(const QString& target, const QString& sender, const QString& channel, const QRect& trayGeometry); |
||||||
|
void dismiss(const char* reason); |
||||||
|
|
||||||
|
signals: |
||||||
|
void activated(const QString& target); |
||||||
|
|
||||||
|
protected: |
||||||
|
void mouseReleaseEvent(QMouseEvent* event) override; |
||||||
|
void hideEvent(QHideEvent* event) override; |
||||||
|
void closeEvent(QCloseEvent* event) override; |
||||||
|
|
||||||
|
private: |
||||||
|
QString m_target; |
||||||
|
QLabel* m_body = nullptr; |
||||||
|
UserActivityMonitor* m_activity = nullptr; |
||||||
|
QTimer m_hideTimer; |
||||||
|
}; |
||||||
Loading…
Reference in new issue