1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#include <QtVirtualKeyboard/private/desktopinputselectioncontrol_p.h>
5#include <QtVirtualKeyboard/qvirtualkeyboardinputcontext.h>
6#include <QtVirtualKeyboard/private/qvirtualkeyboardinputcontext_p.h>
7#include <QtVirtualKeyboard/private/inputselectionhandle_p.h>
8#include <QtVirtualKeyboard/private/settings_p.h>
9#include <QtVirtualKeyboard/private/platforminputcontext_p.h>
10
11#include <QtCore/qpropertyanimation.h>
12#include <QtGui/qguiapplication.h>
13#include <QtGui/qstylehints.h>
14#include <QtGui/qimagereader.h>
15
16QT_BEGIN_NAMESPACE
17namespace QtVirtualKeyboard {
18
19DesktopInputSelectionControl::DesktopInputSelectionControl(QObject *parent, QVirtualKeyboardInputContext *inputContext)
20 : QObject(parent),
21 m_inputContext(inputContext),
22 m_anchorSelectionHandle(),
23 m_cursorSelectionHandle(),
24 m_handleState(HandleIsReleased),
25 m_enabled(false),
26 m_anchorHandleVisible(false),
27 m_cursorHandleVisible(false),
28 m_eventFilterEnabled(true),
29 m_handleWindowSize(40, 40*1.12) // because a finger patch is slightly taller than its width
30{
31 QWindow *focusWindow = QGuiApplication::focusWindow();
32 Q_ASSERT(focusWindow);
33 connect(sender: m_inputContext, signal: &QVirtualKeyboardInputContext::selectionControlVisibleChanged, context: this, slot: &DesktopInputSelectionControl::updateVisibility);
34}
35
36/*
37 * Includes the hit area surrounding the visual handle
38 */
39QRect DesktopInputSelectionControl::handleRectForCursorRect(const QRectF &cursorRect) const
40{
41 const int topMargin = (m_handleWindowSize.height() - m_handleImage.size().height())/2;
42 const QPoint pos(int(cursorRect.x() + (cursorRect.width() - m_handleWindowSize.width())/2),
43 int(cursorRect.bottom()) - topMargin);
44 return QRect(pos, m_handleWindowSize);
45}
46
47/*
48 * Includes the hit area surrounding the visual handle
49 */
50QRect DesktopInputSelectionControl::anchorHandleRect() const
51{
52 return handleRectForCursorRect(cursorRect: m_inputContext->anchorRectangle());
53}
54
55/*
56 * Includes the hit area surrounding the visual handle
57 */
58QRect DesktopInputSelectionControl::cursorHandleRect() const
59{
60 return handleRectForCursorRect(cursorRect: m_inputContext->cursorRectangle());
61}
62
63void DesktopInputSelectionControl::updateAnchorHandlePosition()
64{
65 if (QWindow *focusWindow = QGuiApplication::focusWindow()) {
66 const QPoint pos = focusWindow->mapToGlobal(pos: anchorHandleRect().topLeft());
67 m_anchorSelectionHandle->setPosition(pos);
68 }
69}
70
71void DesktopInputSelectionControl::updateCursorHandlePosition()
72{
73 if (QWindow *focusWindow = QGuiApplication::focusWindow()) {
74 const QPoint pos = focusWindow->mapToGlobal(pos: cursorHandleRect().topLeft());
75 m_cursorSelectionHandle->setPosition(pos);
76 }
77}
78
79void DesktopInputSelectionControl::updateVisibility()
80{
81 if (!m_enabled) {
82 // if VKB is hidden, we must hide the selection handles immediately,
83 // because it might mean that the application is shutting down.
84 m_anchorSelectionHandle->hide();
85 m_cursorSelectionHandle->hide();
86 m_anchorHandleVisible = false;
87 m_cursorHandleVisible = false;
88 return;
89 }
90 const bool wasAnchorVisible = m_anchorHandleVisible;
91 const bool wasCursorVisible = m_cursorHandleVisible;
92 const bool makeVisible = (m_inputContext->isSelectionControlVisible() || m_handleState == HandleIsMoving) && m_enabled;
93
94 m_anchorHandleVisible = makeVisible;
95 if (QWindow *focusWindow = QGuiApplication::focusWindow()) {
96 QRectF globalAnchorRectangle = m_inputContext->anchorRectangle();
97 QPoint tl = focusWindow->mapToGlobal(pos: globalAnchorRectangle.toRect().topLeft());
98 globalAnchorRectangle.moveTopLeft(p: tl);
99 m_anchorHandleVisible = m_anchorHandleVisible
100 && m_inputContext->anchorRectIntersectsClipRect()
101 && !(m_inputContext->priv()->keyboardRectangle().intersects(r: globalAnchorRectangle));
102 }
103
104 if (wasAnchorVisible != m_anchorHandleVisible) {
105 const qreal end = m_anchorHandleVisible ? 1 : 0;
106 if (m_anchorHandleVisible)
107 m_anchorSelectionHandle->show();
108 QPropertyAnimation *anim = new QPropertyAnimation(m_anchorSelectionHandle.data(), "opacity");
109 anim->setEndValue(end);
110 anim->start(policy: QAbstractAnimation::DeleteWhenStopped);
111 }
112
113 m_cursorHandleVisible = makeVisible;
114 if (QWindow *focusWindow = QGuiApplication::focusWindow()) {
115 QRectF globalCursorRectangle = m_inputContext->cursorRectangle();
116 QPoint tl = focusWindow->mapToGlobal(pos: globalCursorRectangle.toRect().topLeft());
117 globalCursorRectangle.moveTopLeft(p: tl);
118 m_cursorHandleVisible = m_cursorHandleVisible
119 && m_inputContext->cursorRectIntersectsClipRect()
120 && !(m_inputContext->priv()->keyboardRectangle().intersects(r: globalCursorRectangle));
121
122 }
123
124 if (wasCursorVisible != m_cursorHandleVisible) {
125 const qreal end = m_cursorHandleVisible ? 1 : 0;
126 if (m_cursorHandleVisible)
127 m_cursorSelectionHandle->show();
128 QPropertyAnimation *anim = new QPropertyAnimation(m_cursorSelectionHandle.data(), "opacity");
129 anim->setEndValue(end);
130 anim->start(policy: QAbstractAnimation::DeleteWhenStopped);
131 }
132}
133
134void DesktopInputSelectionControl::reloadGraphics()
135{
136 Settings *settings = Settings::instance();
137 const QString stylePath = QString::fromLatin1(ba: ":/qt-project.org/imports/QtQuick/VirtualKeyboard/Styles/Builtin/%1/images/selectionhandle-bottom.svg")
138 .arg(a: settings->styleName());
139 QImageReader imageReader(stylePath);
140 QSize sz = imageReader.size(); // SVG handler will return default size
141 sz.scale(w: 20, h: 20, mode: Qt::KeepAspectRatioByExpanding);
142 imageReader.setScaledSize(sz);
143 m_handleImage = imageReader.read();
144
145 m_anchorSelectionHandle->applyImage(windowSize: m_handleWindowSize); // applies m_handleImage for both selection handles
146 m_cursorSelectionHandle->applyImage(windowSize: m_handleWindowSize);
147}
148
149void DesktopInputSelectionControl::createHandles()
150{
151 if (QWindow *focusWindow = QGuiApplication::focusWindow()) {
152 Settings *settings = Settings::instance();
153 connect(sender: settings, signal: &Settings::styleChanged, context: this, slot: &DesktopInputSelectionControl::reloadGraphics);
154
155 m_anchorSelectionHandle = QSharedPointer<InputSelectionHandle>::create(arguments: this, arguments&: focusWindow);
156 m_cursorSelectionHandle = QSharedPointer<InputSelectionHandle>::create(arguments: this, arguments&: focusWindow);
157
158 reloadGraphics();
159 if (QCoreApplication *app = QCoreApplication::instance()) {
160 connect(sender: app, signal: &QCoreApplication::aboutToQuit,
161 context: this, slot: &DesktopInputSelectionControl::destroyHandles);
162 }
163 }
164}
165
166void DesktopInputSelectionControl::destroyHandles()
167{
168 m_anchorSelectionHandle.reset();
169 m_cursorSelectionHandle.reset();
170}
171
172void DesktopInputSelectionControl::setEnabled(bool enable)
173{
174 // setEnabled(true) just means that the handles _can_ be made visible
175 // This will typically be set when a input field gets focus (and having selection).
176 m_enabled = enable;
177 QWindow *focusWindow = QGuiApplication::focusWindow();
178 if (enable) {
179 connect(sender: m_inputContext, signal: &QVirtualKeyboardInputContext::anchorRectangleChanged, context: this, slot: &DesktopInputSelectionControl::updateAnchorHandlePosition);
180 connect(sender: m_inputContext, signal: &QVirtualKeyboardInputContext::cursorRectangleChanged, context: this, slot: &DesktopInputSelectionControl::updateCursorHandlePosition);
181 connect(sender: m_inputContext, signal: &QVirtualKeyboardInputContext::anchorRectIntersectsClipRectChanged, context: this, slot: &DesktopInputSelectionControl::updateVisibility);
182 connect(sender: m_inputContext, signal: &QVirtualKeyboardInputContext::cursorRectIntersectsClipRectChanged, context: this, slot: &DesktopInputSelectionControl::updateVisibility);
183 updateAnchorHandlePosition();
184 updateCursorHandlePosition();
185 if (focusWindow)
186 focusWindow->installEventFilter(filterObj: this);
187 } else {
188 if (focusWindow)
189 focusWindow->removeEventFilter(obj: this);
190 disconnect(sender: m_inputContext, signal: &QVirtualKeyboardInputContext::cursorRectIntersectsClipRectChanged, receiver: this, slot: &DesktopInputSelectionControl::updateVisibility);
191 disconnect(sender: m_inputContext, signal: &QVirtualKeyboardInputContext::anchorRectIntersectsClipRectChanged, receiver: this, slot: &DesktopInputSelectionControl::updateVisibility);
192 disconnect(sender: m_inputContext, signal: &QVirtualKeyboardInputContext::anchorRectangleChanged, receiver: this, slot: &DesktopInputSelectionControl::updateAnchorHandlePosition);
193 disconnect(sender: m_inputContext, signal: &QVirtualKeyboardInputContext::cursorRectangleChanged, receiver: this, slot: &DesktopInputSelectionControl::updateCursorHandlePosition);
194 }
195 updateVisibility();
196}
197
198QImage *DesktopInputSelectionControl::handleImage()
199{
200 return &m_handleImage;
201}
202
203bool DesktopInputSelectionControl::eventFilter(QObject *object, QEvent *event)
204{
205 QWindow *focusWindow = QGuiApplication::focusWindow();
206 if (!m_cursorSelectionHandle || !m_eventFilterEnabled || object != focusWindow)
207 return false;
208 const bool windowMoved = event->type() == QEvent::Move;
209 const bool windowResized = event->type() == QEvent::Resize;
210 if (windowMoved || windowResized) {
211 if (m_enabled) {
212 if (windowMoved) {
213 updateAnchorHandlePosition();
214 updateCursorHandlePosition();
215 }
216 updateVisibility();
217 }
218 } else if (event->type() == QEvent::MouseButtonPress) {
219 QMouseEvent *me = static_cast<QMouseEvent*>(event);
220 const QPoint mousePos = me->globalPosition().toPoint();
221
222 // calculate distances from mouse pos to each handle,
223 // then choose to interact with the nearest handle
224 struct SelectionHandleInfo {
225 qreal squaredDistance;
226 QPoint delta;
227 QRect rect;
228 };
229 SelectionHandleInfo handles[2];
230 handles[AnchorHandle].rect = anchorHandleRect();
231 handles[CursorHandle].rect = cursorHandleRect();
232
233 for (int i = 0; i <= CursorHandle; ++i) {
234 SelectionHandleInfo &h = handles[i];
235 QPoint curHandleTopCenter = focusWindow->mapToGlobal(pos: QPoint(h.rect.x() + qRound(d: (qreal)h.rect.width() / 2), h.rect.top())); // ### map to desktoppanel
236 const QPoint delta = mousePos - curHandleTopCenter;
237 h.delta = delta;
238 h.squaredDistance = QPoint::dotProduct(p1: delta, p2: delta);
239 }
240
241 // (squared) distances calculated, pick the closest handle
242 HandleType closestHandle = (handles[AnchorHandle].squaredDistance < handles[CursorHandle].squaredDistance ? AnchorHandle : CursorHandle);
243
244 // Can not be replaced with me->scenePosition(); because the event might be forwarded from the window of the handle
245 const QPoint windowPos = focusWindow->mapFromGlobal(pos: mousePos);
246 if (m_anchorHandleVisible && handles[closestHandle].rect.contains(p: windowPos)) {
247 m_currentDragHandle = closestHandle;
248 m_distanceBetweenMouseAndCursor = handles[closestHandle].delta;
249 m_handleState = HandleIsHeld;
250 m_handleDragStartedPosition = mousePos;
251 const QRect otherRect = handles[1 - closestHandle].rect;
252 m_otherSelectionPoint = QPoint(otherRect.x() + otherRect.width()/2, otherRect.top() - 4);
253
254 QMouseEvent *mouseEvent = new QMouseEvent(me->type(), me->position(), me->scenePosition(), me->globalPosition(),
255 me->button(), me->buttons(), me->modifiers(), me->source());
256 m_eventQueue.append(t: mouseEvent);
257 return true;
258 }
259 } else if (event->type() == QEvent::MouseMove) {
260 QMouseEvent *me = static_cast<QMouseEvent*>(event);
261 QPoint mousePos = me->globalPosition().toPoint();
262 if (m_handleState == HandleIsHeld) {
263 QPoint delta = m_handleDragStartedPosition - mousePos;
264 const int startDragDistance = QGuiApplication::styleHints()->startDragDistance();
265 if (QPoint::dotProduct(p1: delta, p2: delta) > startDragDistance * startDragDistance)
266 m_handleState = HandleIsMoving;
267 }
268 if (m_handleState == HandleIsMoving) {
269 QPoint cursorPos = mousePos - m_distanceBetweenMouseAndCursor;
270 cursorPos = focusWindow->mapFromGlobal(pos: cursorPos);
271 if (m_currentDragHandle == CursorHandle)
272 m_inputContext->setSelectionOnFocusObject(anchorPos: m_otherSelectionPoint, cursorPos);
273 else
274 m_inputContext->setSelectionOnFocusObject(anchorPos: cursorPos, cursorPos: m_otherSelectionPoint);
275 qDeleteAll(c: m_eventQueue);
276 m_eventQueue.clear();
277 return true;
278 }
279 } else if (event->type() == QEvent::MouseButtonRelease) {
280 if (m_handleState == HandleIsMoving) {
281 m_handleState = HandleIsReleased;
282 qDeleteAll(c: m_eventQueue);
283 m_eventQueue.clear();
284 return true;
285 } else {
286 if (QWindow *focusWindow = QGuiApplication::focusWindow()) {
287 // playback event queue. These are events that were not designated
288 // for the handles in hindsight.
289 // This is typically MousePress and MouseRelease (not interleaved with MouseMove)
290 // that should instead go through to the underlying input editor
291 m_eventFilterEnabled = false;
292 while (!m_eventQueue.isEmpty()) {
293 QMouseEvent *e = m_eventQueue.takeFirst();
294 QCoreApplication::sendEvent(receiver: focusWindow, event: e);
295 delete e;
296 }
297 m_eventFilterEnabled = true;
298 }
299 m_handleState = HandleIsReleased;
300 }
301 }
302 return false;
303}
304
305} // namespace QtVirtualKeyboard
306QT_END_NAMESPACE
307

source code of qtvirtualkeyboard/src/virtualkeyboard/desktopinputselectioncontrol.cpp