Cancel member actions when a click becomes a drag

This commit is contained in:
evgeny committed 2026-10-27 03:54:03 +01:00
1 parent 1820128f5b
commit a7a7e4b5a1
3 files changed
+58 -6

No files matched your search

+12 -1
View File
@@ -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<QMouseEvent*>(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<QMouseEvent*>(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);
+13 -5
View File
@@ -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 <QStyledItemDelegate>
@@ -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/отмене жеста
};
+33
View File
@@ -483,6 +483,39 @@ private slots:
QCOMPARE(pixels(known | busy, true, QColor("#4CAF50")), 0);
}
void draggingAwayAndBackDoesNotCallMember_data() {
QTest::addColumn<int>("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<debug_level_t> 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);