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

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