Krita Source Code Documentation
Loading...
Searching...
No Matches
KisLongPressEventFilter.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-3.0-or-later
4#include <QAbstractButton>
5#include <QAbstractScrollArea>
6#include <QAbstractSlider>
7#include <QAbstractSpinBox>
8#include <QApplication>
9#include <QComboBox>
10#include <QContextMenuEvent>
11#include <QCursor>
12#include <QLineEdit>
13#include <QMenu>
14#include <QMenuBar>
15#include <QMouseEvent>
16#include <QScopedValueRollback>
17#include <QScroller>
18#include <QStyleHints>
19#include <QTimer>
20#ifdef Q_OS_ANDROID
21#include <QtAndroid>
22#endif
23
25 : QObject(parent)
26{
27 m_timer = new QTimer(this);
28 m_timer->setTimerType(Qt::CoarseTimer);
29 m_timer->setSingleShot(true);
30 connect(m_timer, &QTimer::timeout, this, &KisLongPressEventFilter::triggerLongPress);
31#ifdef Q_OS_ANDROID
32 m_longPressTimeout =
33 QAndroidJniObject::callStaticMethod<jint>("org/krita/android/MainActivity", "getLongPressTimeout", "()I");
34#endif
35}
36
37bool KisLongPressEventFilter::eventFilter(QObject *watched, QEvent *event)
38{
39 if (!m_handlingEvent) {
40 QScopedValueRollback rollback(m_handlingEvent, true);
41 switch (event->type()) {
42 case QEvent::MouseButtonPress:
43 if (handleMousePress(qobject_cast<QWidget *>(watched), static_cast<QMouseEvent *>(event))) {
44 event->setAccepted(true);
45 return true;
46 }
47 break;
48 case QEvent::MouseMove:
49 if (handleMouseMove(static_cast<QMouseEvent *>(event))) {
50 return true;
51 }
52 break;
53 case QEvent::MouseButtonDblClick:
54 case QEvent::MouseButtonRelease:
55 flush();
56 break;
57 default:
58 break;
59 }
60 }
61 return QObject::eventFilter(watched, event);
62}
63
64bool KisLongPressEventFilter::handleMousePress(QWidget *target, const QMouseEvent *me)
65{
66 if (me->buttons() == Qt::LeftButton && me->modifiers() == Qt::NoModifier && isContextMenuTarget(target)) {
67 const QStyleHints *sh = qApp->styleHints();
68 long long distance = qMax(MINIMUM_DISTANCE, sh->startDragDistance());
70#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
71 m_pressLocalPos = me->pos();
72 m_pressGlobalPos = me->globalPos();
73#else
74 m_pressLocalPos = me->position().toPoint();
75 m_pressGlobalPos = me->globalPosition().toPoint();
76#endif
77 // Kinetic scrolling may have already delayed the input, so subtract
78 // that from the intended delay to avoid waiting for it twice. This may
79 // end up with a delay of zero, but that's okay. Also, the user may have
80 // already dragged far enough for the cursor to be outside of the
81 // long-press distance (it only checks the axes that are scrollable), in
82 // which case we bail out here.
83 int kineticScrollDelay = getKineticScrollDelay(target);
84 if (kineticScrollDelay == 0 || isWithinDistance(QCursor::pos())) {
86#ifdef Q_OS_ANDROID
87 int longPressInterval = m_longPressTimeout;
88#else
89 int longPressInterval = sh->mousePressAndHoldInterval();
90#endif
91 if (longPressInterval < MINIMUM_DELAY) {
92 longPressInterval = MINIMUM_DELAY;
93 }
94 // Kinetic scrolling may have already delayed the input, so subtract
95 // that from the intended delay to avoid waiting for it twice. This
96 // may end up with a delay of zero, but that's okay.
97 m_timer->start(qMax(0, longPressInterval - kineticScrollDelay));
98 return true;
99 }
100 }
101 cancel();
102 return false;
103}
104
106{
107 if (m_timer->isActive()) {
108#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
109 QPoint globalPos = me->globalPos();
110#else
111 QPoint globalPos = me->globalPosition().toPoint();
112#endif
113 if (isWithinDistance(globalPos)) {
114 return true;
115 } else {
116 flush();
117 }
118 }
119 return false;
120}
121
123{
124 QWidget *target = m_target.data();
125 cancel();
126 if (target && target->isVisible()) {
127 QMouseEvent event(QEvent::MouseButtonPress,
130 Qt::LeftButton,
131 Qt::LeftButton,
132 Qt::NoModifier);
133 qApp->sendEvent(target, &event);
134 }
135}
136
138{
139 m_timer->stop();
140 m_target.clear();
141}
142
143bool KisLongPressEventFilter::isWithinDistance(const QPoint &globalPos) const
144{
145 long long x = globalPos.x() - m_pressGlobalPos.x();
146 long long y = globalPos.y() - m_pressGlobalPos.y();
147 return (x * x) + (y * y) <= m_distanceSquared;
148}
149
151{
152 QWidget *target = m_target.data();
153 cancel();
155 QScopedValueRollback rollback(m_handlingEvent, true);
156 // First we synchronously send a right click press and release so that
157 // the target widget can update its state correctly. Afterwards we post
158 // the context menu event to it so that'll open. As far as I can tell,
159 // that's what Qt does when you right-click on a widget as well.
160 {
161 QMouseEvent pressEvent(QEvent::MouseButtonPress,
164 Qt::RightButton,
165 Qt::RightButton,
166 Qt::NoModifier);
167 qApp->sendEvent(target, &pressEvent);
168 }
169 {
170 QMouseEvent releaseEvent(QEvent::MouseButtonRelease,
173 Qt::RightButton,
174 Qt::NoButton,
175 Qt::NoModifier);
176 qApp->sendEvent(target, &releaseEvent);
177 }
178 qApp->postEvent(target, new QContextMenuEvent(QContextMenuEvent::Mouse, m_pressLocalPos, m_pressGlobalPos));
179 }
180}
181
183{
184 if (KisKineticScroller::getConfiguredGestureType() != QScroller::LeftMouseButtonGesture) {
185 return 0;
186 }
187
188 const QScroller *scroller = searchScroller(target);
189 if (!scroller) {
190 return 0;
191 }
192
193 qreal pressDelay = scroller->scrollerProperties().scrollMetric(QScrollerProperties::MousePressEventDelay).toReal();
194 if (pressDelay < 0.0) {
195 return 0;
196 }
197
198 return int(pressDelay * 1000.0);
199}
200
202{
203 while (target) {
204 if (QScroller::hasScroller(target)) {
205 return QScroller::scroller(target);
206 } else {
207 target = target->parentWidget();
208 }
209 }
210 return nullptr;
211}
212
214{
215 while (target && target->isVisible() && isLongPressableWidget(target)) {
216 switch (target->contextMenuPolicy()) {
217 case Qt::NoContextMenu:
218 target = target->parentWidget();
219 break;
220 case Qt::PreventContextMenu:
221 return false;
222 default:
223 return true;
224 }
225 }
226 return false;
227}
228
230{
231 QVariant prop = target->property(ENABLED_PROPERTY);
232 if (prop.isValid()) {
233 return prop.toBool();
234 }
235
236 // Several widget types don't have desirable long-press behavior. If they're
237 // our own widgets, we should just fix that in the widget, but we can"t
238 // really sensibly do that to Qt's widgets. Luckily however, those widgets
239 // really shouldn't have context menus in the first place, so we can just
240 // disregard them as long-press targets and be done with it.
241
242 // Buttons get depressed when you click and hold on them and won't pop back
243 // up if a context menu is opened during that. They may also have a menu
244 // attached to them that the user operates like a context menu.
245 if (qobject_cast<QAbstractButton *>(target)) {
246 return false;
247 }
248
249 // Slightly non-obvious, but this thing has many child classes, like text
250 // areas or the canvas, none of which have sensible context menus.
251 if (qobject_cast<QAbstractScrollArea *>(target)) {
252 return false;
253 }
254
255 // Sliders (including scrollbars) may get dragged slowly.
256 if (qobject_cast<QAbstractSlider *>(target)) {
257 return false;
258 }
259
260 // Spinners may get held down to keep a number spinning.
261 if (qobject_cast<QAbstractSpinBox *>(target)) {
262 return false;
263 }
264
265 // Combo boxes get held down to pick an item.
266 if (qobject_cast<QComboBox *>(target)) {
267 return false;
268 }
269
270 // Text editing will have its own long-press handling.
271 if (qobject_cast<QLineEdit *>(target)) {
272 return false;
273 }
274
275 // Menus don't have context menus.
276 if (qobject_cast<QMenu *>(target)) {
277 return false;
278 }
279
280 // Menu bars don't have context menus either.
281 if (qobject_cast<QMenuBar *>(target)) {
282 return false;
283 }
284
285 return true;
286}
KisMagneticGraph::vertex_descriptor target(typename KisMagneticGraph::edge_descriptor e, KisMagneticGraph g)
qreal distance(const QPointF &p1, const QPointF &p2)
static constexpr char ENABLED_PROPERTY[]
bool handleMouseMove(const QMouseEvent *me)
int getKineticScrollDelay(QWidget *target) const
bool handleMousePress(QWidget *target, const QMouseEvent *me)
static constexpr int MINIMUM_DISTANCE
KisLongPressEventFilter(QObject *parent=nullptr)
bool isWithinDistance(const QPoint &globalPos) const
static bool isLongPressableWidget(QWidget *target)
bool eventFilter(QObject *watched, QEvent *event) override
static const QScroller * searchScroller(QWidget *target)
static constexpr int MINIMUM_DELAY
static bool isContextMenuTarget(QWidget *target)
KRITAWIDGETUTILS_EXPORT QScroller::ScrollerGestureType getConfiguredGestureType()