From e3286d409f36150e85b9e4a30e20a57b8df1651b Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sun, 19 Jul 2026 09:05:55 +0300 Subject: [PATCH] =?UTF-8?q?chatgui:=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA=D0=B0?= =?UTF-8?q?=20+=20=D0=B4=D0=BB=D1=8F=20invite=20=D0=B2=20=D0=BA=D0=B0?= =?UTF-8?q?=D0=BD=D0=B0=D0=BB=D0=B0=D1=85,=20=D0=B0=D0=B2=D1=82=D0=BE?= =?UTF-8?q?=D0=BA=D0=BE=D0=BF=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D1=81=D1=81=D1=8B=D0=BB=D0=BA=D0=B8,=20=D0=B0=D0=B2?= =?UTF-8?q?=D1=82=D0=BE=D0=BF=D0=BE=D0=B4=D0=BA=D0=BB=D1=8E=D1=87=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=B8=D0=B7=20=D0=B1=D1=83=D1=84=D0=B5?= =?UTF-8?q?=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Замена контекстного меню 'Пригласить' на кнопку + (18x18) справа снизу каждого канала с hover-эффектом - InviteDialog: авто-копирование ссылки в буфер (Clipboard + Selection) при открытии - JoinDialog: параметр autoJoinLink — если в буфере валидная utun:// ссылка, диалог сразу стартует подключение --- tools/chatgui/src/channeldelegate.cpp | 30 ++++++++++++++- tools/chatgui/src/channeldelegate.h | 6 ++- tools/chatgui/src/channellist.cpp | 55 ++++++++++++++++++--------- tools/chatgui/src/channellist.h | 3 -- tools/chatgui/src/invitedialog.cpp | 11 ++++++ tools/chatgui/src/joindialog.cpp | 8 +++- tools/chatgui/src/joindialog.h | 3 +- tools/chatgui/src/mainwindow.cpp | 18 +++++++-- 8 files changed, 107 insertions(+), 27 deletions(-) diff --git a/tools/chatgui/src/channeldelegate.cpp b/tools/chatgui/src/channeldelegate.cpp index b8a43820..9381aeec 100644 --- a/tools/chatgui/src/channeldelegate.cpp +++ b/tools/chatgui/src/channeldelegate.cpp @@ -5,6 +5,13 @@ ChannelDelegate::ChannelDelegate(QObject *parent) : QStyledItemDelegate(parent) {} +QRect ChannelDelegate::inviteButtonRect(const QRect &itemRect) { + const int margin = 6; + return QRect(itemRect.right() - margin - InviteBtnSize, + itemRect.bottom() - margin - InviteBtnSize, + InviteBtnSize, InviteBtnSize); +} + void ChannelDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { painter->save(); @@ -90,7 +97,8 @@ void ChannelDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio if (option.state & QStyle::State_Selected) nickColor = nickColor.lighter(140); QRect msgRect(textRect.left(), nameRect.bottom() + 4, - textRect.width(), option.rect.bottom() - nameRect.bottom() - margin); + textRect.right() - InviteBtnSize - 6 - textRect.left(), + option.rect.bottom() - nameRect.bottom() - margin); QFontMetrics msgFm(msgFont); int colon = lastMsg.indexOf(QString::fromUtf8(": ")); @@ -117,6 +125,26 @@ void ChannelDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio } } + /* ── invite plus button (bottom-right) ── */ + QRect plusRect = inviteButtonRect(option.rect); + bool plusHovered = index.data(ChannelInviteBtnHoveredRole).toInt() != 0; + painter->setRenderHint(QPainter::Antialiasing); + if (plusHovered) { + painter->setBrush(QColor(0x33, 0x90, 0xEC)); + painter->setPen(Qt::NoPen); + painter->drawEllipse(plusRect); + painter->setPen(QPen(Qt::white, 2)); + } else { + painter->setBrush(QColor(0x33, 0x90, 0xEC, 110)); + painter->setPen(Qt::NoPen); + painter->drawEllipse(plusRect); + painter->setPen(QPen(QColor(0x33, 0x90, 0xEC), 2)); + } + painter->drawLine(plusRect.center().x(), plusRect.top() + 5, + plusRect.center().x(), plusRect.bottom() - 5); + painter->drawLine(plusRect.left() + 5, plusRect.center().y(), + plusRect.right() - 5, plusRect.center().y()); + painter->restore(); } diff --git a/tools/chatgui/src/channeldelegate.h b/tools/chatgui/src/channeldelegate.h index 30f59090..0e1f4bf8 100644 --- a/tools/chatgui/src/channeldelegate.h +++ b/tools/chatgui/src/channeldelegate.h @@ -8,7 +8,8 @@ enum ChannelDataRole { ChannelLastMessageRole = Qt::UserRole + 1, ChannelChannelIdRole = Qt::UserRole + 2, ChannelPeersOnlineRole = Qt::UserRole + 3, /* int: count of online peers */ - ChannelAutoConnectActiveRole = Qt::UserRole + 4 /* bool: 1 if auto-connect in progress */ + ChannelAutoConnectActiveRole = Qt::UserRole + 4, /* bool: 1 if auto-connect in progress */ + ChannelInviteBtnHoveredRole = Qt::UserRole + 5 /* bool: 1 when mouse hovers invite button */ }; class ChannelDelegate : public QStyledItemDelegate { @@ -20,4 +21,7 @@ public: const QModelIndex &index) const override; QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; + + static constexpr int InviteBtnSize = 18; + static QRect inviteButtonRect(const QRect &itemRect); }; diff --git a/tools/chatgui/src/channellist.cpp b/tools/chatgui/src/channellist.cpp index 880f891b..637902cd 100644 --- a/tools/chatgui/src/channellist.cpp +++ b/tools/chatgui/src/channellist.cpp @@ -7,17 +7,43 @@ #include #include #include -#include #include -/* QListView that prevents deselection */ +/* QListView that prevents deselection, handles invite plus-button clicks */ class ChannelListView : public QListView { + Q_OBJECT public: using QListView::QListView; + +signals: + void inviteClicked(const QString& channelId); + protected: void mousePressEvent(QMouseEvent* e) override { - if (indexAt(e->pos()).isValid()) - QListView::mousePressEvent(e); + QModelIndex idx = indexAt(e->pos()); + if (!idx.isValid()) return; + QRect plusRect = ChannelDelegate::inviteButtonRect(visualRect(idx)); + if (plusRect.contains(e->pos())) { + emit inviteClicked(idx.data(ChannelChannelIdRole).toString()); + return; + } + QListView::mousePressEvent(e); + } + void mouseMoveEvent(QMouseEvent* e) override { + int prevRow = m_hoveredInviteRow; + m_hoveredInviteRow = -1; + QModelIndex idx = indexAt(e->pos()); + if (idx.isValid()) { + QRect plusRect = ChannelDelegate::inviteButtonRect(visualRect(idx)); + if (plusRect.contains(e->pos())) + m_hoveredInviteRow = idx.row(); + } + if (m_hoveredInviteRow != prevRow) { + model()->setData(model()->index(prevRow, 0), 0, ChannelInviteBtnHoveredRole); + model()->setData(model()->index(m_hoveredInviteRow, 0), 1, ChannelInviteBtnHoveredRole); + } + setCursor(m_hoveredInviteRow >= 0 ? Qt::PointingHandCursor : Qt::ArrowCursor); + QListView::mouseMoveEvent(e); } QItemSelectionModel::SelectionFlags selectionCommand(const QModelIndex& idx, const QEvent* e) const override { if (idx.isValid()) @@ -29,6 +55,9 @@ protected: if (selectionModel() && !selectionModel()->hasSelection() && currentIndex().isValid()) selectionModel()->select(currentIndex(), QItemSelectionModel::ClearAndSelect); } + +private: + int m_hoveredInviteRow = -1; }; static QIcon makeChannelIcon(const QColor &color, const QString &letter) { @@ -70,7 +99,7 @@ ChannelList::ChannelList(DbManager* db, QWidget *parent) m_listView->setSelectionMode(QAbstractItemView::SingleSelection); m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_listView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); - m_listView->setContextMenuPolicy(Qt::CustomContextMenu); + m_listView->setMouseTracking(true); layout->addWidget(m_listView); auto *footer = new QWidget(this); @@ -119,8 +148,8 @@ ChannelList::ChannelList(DbManager* db, QWidget *parent) emit channelSelected(cid); } }); - connect(m_listView, &QListView::customContextMenuRequested, - this, &ChannelList::onContextMenu); + connect(static_cast(m_listView), &ChannelListView::inviteClicked, + this, &ChannelList::inviteRequested); } void ChannelList::loadChannels() { @@ -174,16 +203,6 @@ void ChannelList::selectChannel(const QString& channelId) { } } -void ChannelList::onContextMenu(const QPoint& pos) { - QModelIndex index = m_listView->indexAt(pos); - if (!index.isValid()) return; - QString cid = index.data(ChannelChannelIdRole).toString(); - QMenu menu(this); - QAction *inviteAction = menu.addAction("Пригласить"); - if (menu.exec(m_listView->viewport()->mapToGlobal(pos)) == inviteAction) - emit inviteRequested(cid); -} - void ChannelList::setChannelPeersOnline(const QString& channelId, int count) { for (int row = 0; row < m_model->rowCount(); row++) { QStandardItem* item = m_model->item(row); @@ -202,3 +221,5 @@ void ChannelList::setAutoConnectActive(bool active) { if (item) item->setData(active ? 1 : 0, ChannelAutoConnectActiveRole); } } + +#include "channellist.moc" diff --git a/tools/chatgui/src/channellist.h b/tools/chatgui/src/channellist.h index 2a9cc6ae..4faaa80f 100644 --- a/tools/chatgui/src/channellist.h +++ b/tools/chatgui/src/channellist.h @@ -24,9 +24,6 @@ signals: void createGroupRequested(); void settingsRequested(); -private slots: - void onContextMenu(const QPoint& pos); - private: QListView *m_listView; QStandardItemModel *m_model; diff --git a/tools/chatgui/src/invitedialog.cpp b/tools/chatgui/src/invitedialog.cpp index 4685f791..3c7372b9 100644 --- a/tools/chatgui/src/invitedialog.cpp +++ b/tools/chatgui/src/invitedialog.cpp @@ -7,6 +7,7 @@ #include #include #include +#include InviteDialog::InviteDialog(const QString& link, QWidget* parent) : QDialog(parent), m_link(link) @@ -50,4 +51,14 @@ InviteDialog::InviteDialog(const QString& link, QWidget* parent) auto *closeBtn = new QPushButton("OK", this); connect(closeBtn, &QPushButton::clicked, this, &QDialog::accept); layout->addWidget(closeBtn, 0, Qt::AlignRight); + + QTimer::singleShot(0, this, [this]() { + QClipboard* cb = QApplication::clipboard(); + cb->clear(QClipboard::Clipboard); + cb->setText(m_link, QClipboard::Clipboard); + if (cb->supportsSelection()) { + cb->clear(QClipboard::Selection); + cb->setText(m_link, QClipboard::Selection); + } + }); } diff --git a/tools/chatgui/src/joindialog.cpp b/tools/chatgui/src/joindialog.cpp index 3d9c522b..c7a40e3a 100644 --- a/tools/chatgui/src/joindialog.cpp +++ b/tools/chatgui/src/joindialog.cpp @@ -18,7 +18,8 @@ extern "C" { #include "../../../lib/sha256.h" } -JoinDialog::JoinDialog(UtunNode* node, DbManager* db, QWidget* parent) +JoinDialog::JoinDialog(UtunNode* node, DbManager* db, QWidget* parent, + const QString& autoJoinLink) : QDialog(parent) , m_node(node) , m_db(db) @@ -115,6 +116,11 @@ JoinDialog::JoinDialog(UtunNode* node, DbManager* db, QWidget* parent) JoinDialog* dlg = qobject_cast(QApplication::activeModalWidget()); if (dlg) dlg->onConnectResult(nodeId, channelId, result); }); + + if (!autoJoinLink.isEmpty()) { + m_linkEdit->setText(autoJoinLink); + QTimer::singleShot(0, this, &JoinDialog::onConnectClicked); + } } JoinDialog::~JoinDialog() { diff --git a/tools/chatgui/src/joindialog.h b/tools/chatgui/src/joindialog.h index de8d53f1..4fba54a1 100644 --- a/tools/chatgui/src/joindialog.h +++ b/tools/chatgui/src/joindialog.h @@ -13,7 +13,8 @@ class DbManager; class JoinDialog : public QDialog { Q_OBJECT public: - explicit JoinDialog(UtunNode* node, DbManager* db, QWidget* parent = nullptr); + explicit JoinDialog(UtunNode* node, DbManager* db, QWidget* parent = nullptr, + const QString& autoJoinLink = QString()); ~JoinDialog() override; signals: diff --git a/tools/chatgui/src/mainwindow.cpp b/tools/chatgui/src/mainwindow.cpp index 5cb928c2..71a928b5 100644 --- a/tools/chatgui/src/mainwindow.cpp +++ b/tools/chatgui/src/mainwindow.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -253,10 +254,21 @@ void MainWindow::showJoinDialog() { QMessageBox::warning(this, "Error", "Node is not running."); return; } - JoinDialog dlg(m_node, m_db, this); - if (dlg.exec() == QDialog::Accepted) { - m_channelList->loadChannels(); + + QString autoLink; + QClipboard* cb = QApplication::clipboard(); + QString clipText = cb->text(QClipboard::Clipboard).trimmed(); + if (clipText.startsWith("utun://") && decodeInviteLink(clipText).error.isEmpty()) + autoLink = clipText; + else if (cb->supportsSelection()) { + QString selText = cb->text(QClipboard::Selection).trimmed(); + if (selText.startsWith("utun://") && decodeInviteLink(selText).error.isEmpty()) + autoLink = selText; } + + JoinDialog dlg(m_node, m_db, this, autoLink); + if (dlg.exec() == QDialog::Accepted) + m_channelList->loadChannels(); } void MainWindow::showSettings() {