From a7a7e4b5a19256d4d92a86bd0fc8ca7a481c19e5 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 03:54:03 +0100 Subject: [PATCH] Cancel member actions when a click becomes a drag --- tools/chatgui/src/accountdelegate.cpp | 13 ++++++++- tools/chatgui/src/accountdelegate.h | 18 +++++++++---- tools/chatgui/tests/test_invite_links.cpp | 33 +++++++++++++++++++++++ 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/tools/chatgui/src/accountdelegate.cpp b/tools/chatgui/src/accountdelegate.cpp index c7da53ef..766ad69c 100644 --- a/tools/chatgui/src/accountdelegate.cpp +++ b/tools/chatgui/src/accountdelegate.cpp @@ -158,6 +158,16 @@ void AccountDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio bool AccountDelegate::editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option, const QModelIndex& index) { Q_UNUSED(model); + if (event->type() == QEvent::MouseMove) { + const auto* mouse = static_cast(event); + if (m_pressedIndex.isValid() && (!(mouse->buttons() & Qt::LeftButton) || + (mouse->pos() - m_pressedPosition).manhattanLength() >= ClickDistance)) { + GUI_DEBUG("AccountDelegate: action cancelled row=%d action=%d distance=%d left_down=%d", m_pressedIndex.row(), + int(m_pressedAction), (mouse->pos() - m_pressedPosition).manhattanLength(), bool(mouse->buttons() & Qt::LeftButton)); + m_pressedIndex = QPersistentModelIndex(); m_pressedAction = Action::None; + } + return false; + } if (event->type() != QEvent::MouseButtonPress && event->type() != QEvent::MouseButtonRelease && event->type() != QEvent::MouseButtonDblClick) return false; auto* mouse = static_cast(event); @@ -172,9 +182,10 @@ bool AccountDelegate::editorEvent(QEvent* event, QAbstractItemModel* model, cons if (event->type() == QEvent::MouseButtonDblClick) return action != Action::None; const bool handled = action != Action::None || m_pressedIndex.isValid(); const bool activate = enabled && action != Action::None && action == m_pressedAction && m_pressedIndex == index && - (mouse->pos() - m_pressedPosition).manhattanLength() < 5; + (mouse->pos() - m_pressedPosition).manhattanLength() < ClickDistance; m_pressedIndex = QPersistentModelIndex(); m_pressedAction = Action::None; if (activate) { + GUI_DEBUG("AccountDelegate: action activated row=%d action=%d", index.row(), int(action)); if (action == Action::Avatar) emit avatarRequested(index); else if (action == Action::RadioMute) emit radioMuteRequested(index); else if (action == Action::RadioCall) emit radioCallRequested(index); diff --git a/tools/chatgui/src/accountdelegate.h b/tools/chatgui/src/accountdelegate.h index efe6f033..4818744a 100644 --- a/tools/chatgui/src/accountdelegate.h +++ b/tools/chatgui/src/accountdelegate.h @@ -1,5 +1,11 @@ /* Строка участника: аватар, имя и три действия — звук рации, вызов рации, обычный звонок. - * Вызывает signals только после клика без перетаскивания; вызовы доступны online-пирам. */ + * GUI-поток, модель принадлежит view. DisplayRole — имя, DecorationRole — аватар, + * UserRole+1 — подпись, +2 — self, +3 — CHAT_MEMBER_FLAG_*, +4 — storage; + * MemberListModel::OnlineRole/RadioSpeakingRole определяют доступность и индикацию. + * Аватар доступен всем, mute — чужим участникам независимо от online, вызовы — online-пирам. + * Собственная строка показывает состояние рации без кнопок mute/вызова. + * Сигнал только при LeftButton release в том же действии/строке после press: + * MouseMove со смещением >=5 по Manhattan или без LeftButton отменяет жест, включая возврат курсора назад. */ #pragma once #include @@ -8,26 +14,28 @@ class AccountDelegate : public QStyledItemDelegate { Q_OBJECT public: - explicit AccountDelegate(QObject *parent = nullptr); + explicit AccountDelegate(QObject *parent = nullptr); // parent владеет по правилам QObject void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; - QSize sizeHint(const QStyleOptionViewItem &option, + QSize sizeHint(const QStyleOptionViewItem &option, // 160 × MemberListModel::kRowHeight, независимо от данных const QModelIndex &index) const override; bool editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option, const QModelIndex& index) override; bool helpEvent(QHelpEvent* event, QAbstractItemView* view, const QStyleOptionViewItem& option, const QModelIndex& index) override; signals: + // index заимствует модель; для отложенной работы получатель сохраняет node ID, не QModelIndex через reset. void radioCallRequested(const QModelIndex& index); void callRequested(const QModelIndex& index); void avatarRequested(const QModelIndex& index); void radioMuteRequested(const QModelIndex& index); private: + static constexpr int ClickDistance = 5; enum class Action { None, Avatar, RadioMute, RadioCall, Call }; static QRect actionRect(const QRect& row, Action action); static Action actionAt(const QPoint& position, const QRect& row, bool self); QPersistentModelIndex m_pressedIndex; /* строка, на которой начался клик */ - QPoint m_pressedPosition; - Action m_pressedAction = Action::None; + QPoint m_pressedPosition; // координаты viewport при press + Action m_pressedAction = Action::None; // ожидаемое действие; сбрасывается при release/отмене жеста }; diff --git a/tools/chatgui/tests/test_invite_links.cpp b/tools/chatgui/tests/test_invite_links.cpp index 1ab90fbd..2ebec6a6 100644 --- a/tools/chatgui/tests/test_invite_links.cpp +++ b/tools/chatgui/tests/test_invite_links.cpp @@ -483,6 +483,39 @@ private slots: QCOMPARE(pixels(known | busy, true, QColor("#4CAF50")), 0); } + void draggingAwayAndBackDoesNotCallMember_data() { + QTest::addColumn("action"); + QTest::newRow("avatar") << 0; + QTest::newRow("radio-call") << 1; + QTest::newRow("call") << 2; + QTest::newRow("radio-mute") << 3; + } + + void draggingAwayAndBackDoesNotCallMember() { + QFETCH(int, action); + QScopedValueRollback diagnostic(g_debug_config.category_levels[DEBUG_CATEGORY_GENERAL], DEBUG_LEVEL_DEBUG); + QStandardItemModel model; + auto* peer = new QStandardItem("peer"); model.appendRow(peer); + peer->setData(true, MemberListModel::OnlineRole); + QListView view; AccountDelegate delegate; + view.setModel(&model); view.setItemDelegate(&delegate); view.resize(280, 140); view.show(); + QTest::qWait(30); + const auto signal = action == 0 ? &AccountDelegate::avatarRequested : action == 1 ? &AccountDelegate::radioCallRequested : + action == 2 ? &AccountDelegate::callRequested : &AccountDelegate::radioMuteRequested; + QSignalSpy activated(&delegate, signal); + const QRect row = view.visualRect(model.index(0, 0)); + const QPoint radio(action == 0 ? row.left() + 21 : row.right() - (action == 1 ? 50 : action == 2 ? 17 : 82), row.center().y()); + QTest::mousePress(view.viewport(), Qt::LeftButton, Qt::NoModifier, radio); + for (QPoint point : {radio + QPoint(-15, 0), radio}) { + QMouseEvent move(QEvent::MouseMove, point, view.viewport()->mapToGlobal(point), Qt::NoButton, Qt::LeftButton, Qt::NoModifier); + QApplication::sendEvent(view.viewport(), &move); + } + QTest::mouseRelease(view.viewport(), Qt::LeftButton, Qt::NoModifier, radio); + QCOMPARE(activated.count(), 0); + QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, radio); + QCOMPARE(activated.count(), 1); + } + void radioMuteButtonTargetsMemberAndChangesColor() { QStandardItemModel model; auto* other = new QStandardItem("other"); other->setData(false, Qt::UserRole + 2); model.appendRow(other);