diff --git a/tools/chatgui/src/message_notification.cpp b/tools/chatgui/src/message_notification.cpp new file mode 100644 index 00000000..47cb912f --- /dev/null +++ b/tools/chatgui/src/message_notification.cpp @@ -0,0 +1,127 @@ +#include "message_notification.h" +#include "user_activity_monitor.h" +#include "debug_ui.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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(); +} diff --git a/tools/chatgui/src/message_notification.h b/tools/chatgui/src/message_notification.h new file mode 100644 index 00000000..ed0a8677 --- /dev/null +++ b/tools/chatgui/src/message_notification.h @@ -0,0 +1,32 @@ +/* Одно уведомление о сообщениях: не берёт фокус, ждёт активности и скрывается через 10 секунд. + * Новое сообщение заменяет содержимое и отменяет прежний отсчёт. activated передаёт ключ канала/DM владельцу. */ +#pragma once + +#include +#include + +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; +};