Skip to content

Commit 7207782

Browse files
nschimmeclaude
andcommitted
Add tap-and-hold as the touch equivalent of right-click cancel
Touchscreens have no secondary button, so there was no way to cancel a pending/completed connection or room selection on touch -- only 2-finger pinch/pan was handled in touchEvent; single-finger touch fell straight through to Qt's synthesized mouse events with no equivalent of right-click. Track a single-finger touch's start position and time; if held roughly in place (within 15px) for 500ms without lifting, fire the same cancel path as right-click. A normal tap still flows through unchanged since the synthesized mouse press/release isn't touched. Factored the cancel logic (previously duplicated for connections) into a shared cancelPendingSelection(), and used it to fix the same plain-right-click-never-reaches-Cancel bug for SELECT_ROOMS that was already fixed for the connection modes -- its "Cancel" block in the mode switch was equally unreachable via a plain right-click. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AHGhPpZVqQD9o39ryopvXu
1 parent 0f5e949 commit 7207782

3 files changed

Lines changed: 76 additions & 11 deletions

File tree

src/display/MapCanvasData.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include "prespammedpath.h"
1818

1919
#include <cassert>
20+
#include <chrono>
2021
#include <map>
2122
#include <memory>
2223
#include <optional>
@@ -242,6 +243,15 @@ struct NODISCARD PinchState
242243
float lastFactor = 1.f;
243244
};
244245

246+
// Tracks a single-finger touch to detect tap-and-hold, the touch equivalent of a
247+
// right-click, since touchscreens have no secondary button to cancel a selection with.
248+
struct NODISCARD TouchHoldState
249+
{
250+
glm::vec2 startPos;
251+
std::chrono::steady_clock::time_point startTime;
252+
bool fired = false;
253+
};
254+
245255
struct NODISCARD MagnificationState
246256
{
247257
float lastValue = 1.f;
@@ -300,6 +310,7 @@ struct NODISCARD MapCanvasInputState
300310
// and each other, so they are managed independently.
301311
std::optional<PinchState> m_pinchState;
302312
std::optional<MagnificationState> m_magnificationState;
313+
std::optional<TouchHoldState> m_touchHoldState;
303314

304315
SharedRoomSelection m_roomSelection;
305316

src/display/mapcanvas.cpp

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include "connectionselection.h"
2626

2727
#include <array>
28+
#include <chrono>
2829
#include <cmath>
2930
#include <cstddef>
3031
#include <cstdint>
@@ -48,6 +49,9 @@
4849
namespace {
4950
constexpr float GESTURE_EPSILON = 1e-6f;
5051
constexpr float PINCH_DISTANCE_THRESHOLD = 1e-3f;
52+
// Touch equivalent of a right-click: a single finger held roughly in place this long.
53+
constexpr auto TAP_HOLD_DURATION = std::chrono::milliseconds(500);
54+
constexpr float TAP_HOLD_MAX_MOVEMENT_PIXELS = 15.f;
5155
} // namespace
5256

5357
using NonOwningPointer = MapCanvas *;
@@ -158,6 +162,29 @@ void MapCanvas::slot_setCanvasMouseMode(const CanvasMouseModeEnum mode)
158162
selectionChanged();
159163
}
160164

165+
void MapCanvas::cancelPendingSelection()
166+
{
167+
switch (m_canvasMouseMode) {
168+
case CanvasMouseModeEnum::CREATE_CONNECTIONS:
169+
case CanvasMouseModeEnum::CREATE_ONEWAY_CONNECTIONS:
170+
case CanvasMouseModeEnum::SELECT_CONNECTIONS:
171+
endInteraction();
172+
slot_clearConnectionSelection();
173+
break;
174+
case CanvasMouseModeEnum::SELECT_ROOMS:
175+
endInteraction();
176+
slot_clearRoomSelection();
177+
break;
178+
case CanvasMouseModeEnum::NONE:
179+
case CanvasMouseModeEnum::MOVE:
180+
case CanvasMouseModeEnum::RAYPICK_ROOMS:
181+
case CanvasMouseModeEnum::CREATE_ROOMS:
182+
case CanvasMouseModeEnum::SELECT_INFOMARKS:
183+
case CanvasMouseModeEnum::CREATE_INFOMARKS:
184+
break;
185+
}
186+
}
187+
161188
void MapCanvas::slot_setRoomSelection(const SigRoomSelection &selection)
162189
{
163190
if (!selection.isValid()) {
@@ -333,12 +360,38 @@ void MapCanvas::touchEvent(QTouchEvent *const event)
333360
endPanning();
334361
}
335362
event->accept();
336-
} else {
337-
if (points.size() > 2) {
338-
// Explicitly ignore more than 2 touch points for pinch zoom.
339-
qDebug() << "MapCanvas::touchEvent: ignoring" << points.size() << "touch points";
363+
} else if (points.size() == 1) {
364+
// Tap-and-hold: the touch equivalent of a right-click, since a touchscreen has
365+
// no secondary button to cancel a pending/completed selection with. The tap
366+
// itself still falls through to Qt's synthesized mouse press/release below, so
367+
// a normal tap keeps working as a first/second click.
368+
const auto &p = points[0];
369+
const glm::vec2 pos{p.position().x(), p.position().y()};
370+
371+
if (event->type() == QEvent::TouchBegin || p.state() == QEventPoint::Pressed) {
372+
m_touchHoldState = TouchHoldState{pos, std::chrono::steady_clock::now(), false};
373+
} else if (m_touchHoldState) {
374+
auto &hold = *m_touchHoldState;
375+
if (glm::distance(pos, hold.startPos) > TAP_HOLD_MAX_MOVEMENT_PIXELS) {
376+
m_touchHoldState.reset();
377+
} else if (!hold.fired
378+
&& std::chrono::steady_clock::now() - hold.startTime >= TAP_HOLD_DURATION) {
379+
hold.fired = true;
380+
cancelPendingSelection();
381+
}
382+
}
383+
384+
if (event->type() == QEvent::TouchEnd || p.state() == QEventPoint::Released) {
385+
m_touchHoldState.reset();
340386
}
341387

388+
endPinch();
389+
QOpenGLWindow::touchEvent(event);
390+
} else {
391+
// Explicitly ignore more than 2 touch points for pinch zoom.
392+
qDebug() << "MapCanvas::touchEvent: ignoring" << points.size() << "touch points";
393+
394+
m_touchHoldState.reset();
342395
endPinch();
343396
QOpenGLWindow::touchEvent(event);
344397
}
@@ -546,14 +599,11 @@ void MapCanvas::mousePressEvent(QMouseEvent *const event)
546599
slot_setInfomarkSelection(getInfomarkSelection(getSel1()));
547600

548601
selectionChanged();
549-
} else if (m_canvasMouseMode == CanvasMouseModeEnum::CREATE_CONNECTIONS
550-
|| m_canvasMouseMode == CanvasMouseModeEnum::CREATE_ONEWAY_CONNECTIONS
551-
|| m_canvasMouseMode == CanvasMouseModeEnum::SELECT_CONNECTIONS) {
602+
} else {
552603
// A plain right-click never reaches the mode switch below (this branch
553-
// returns first), so cancelling a pending/completed connection selection
554-
// has to happen here.
555-
endInteraction();
556-
slot_clearConnectionSelection();
604+
// returns first), so cancelling a pending/completed selection for modes
605+
// that support it has to happen here.
606+
cancelPendingSelection();
557607
}
558608
emit sig_customContextMenuRequested(event->position().toPoint());
559609
m_mouseRightPressed = false;

src/display/mapcanvas.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,10 @@ class NODISCARD_QOBJECT MapCanvas final : public QOpenGLWindow,
226226

227227
NODISCARD std::shared_ptr<InfomarkSelection> getInfomarkSelection(const MouseSel &sel);
228228

229+
// Cancels whatever pending/completed selection the current mode supports cancelling
230+
// (connection or room selection). Shared by right-click and touch tap-and-hold.
231+
void cancelPendingSelection();
232+
229233
public:
230234
void setMvp(const glm::mat4 &viewProj);
231235
void setViewportAndMvp(int width, int height);

0 commit comments

Comments
 (0)