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
42 QT_BEGIN_NAMESPACE
43 namespace QtVirtualKeyboard {
44
DesktopInputSelectionControl(QObject * parent,QVirtualKeyboardInputContext * inputContext)45 DesktopInputSelectionControl::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(m_inputContext, &QVirtualKeyboardInputContext::selectionControlVisibleChanged, this, &DesktopInputSelectionControl::updateVisibility);
60 }
61
62 /*
63 * Includes the hit area surrounding the visual handle
64 */
handleRectForCursorRect(const QRectF & cursorRect) const65 QRect 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 */
anchorHandleRect() const76 QRect DesktopInputSelectionControl::anchorHandleRect() const
77 {
78 return handleRectForCursorRect(m_inputContext->anchorRectangle());
79 }
80
81 /*
82 * Includes the hit area surrounding the visual handle
83 */
cursorHandleRect() const84 QRect DesktopInputSelectionControl::cursorHandleRect() const
85 {
86 return handleRectForCursorRect(m_inputContext->cursorRectangle());
87 }
88
updateAnchorHandlePosition()89 void DesktopInputSelectionControl::updateAnchorHandlePosition()
90 {
91 if (QWindow *focusWindow = QGuiApplication::focusWindow()) {
92 const QPoint pos = focusWindow->mapToGlobal(anchorHandleRect().topLeft());
93 m_anchorSelectionHandle->setPosition(pos);
94 }
95 }
96
updateCursorHandlePosition()97 void DesktopInputSelectionControl::updateCursorHandlePosition()
98 {
99 if (QWindow *focusWindow = QGuiApplication::focusWindow()) {
100 const QPoint pos = focusWindow->mapToGlobal(cursorHandleRect().topLeft());
101 m_cursorSelectionHandle->setPosition(pos);
102 }
103 }
104
updateVisibility()105 void 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(globalAnchorRectangle.toRect().topLeft());
124 globalAnchorRectangle.moveTopLeft(tl);
125 m_anchorHandleVisible = m_anchorHandleVisible
126 && m_inputContext->anchorRectIntersectsClipRect()
127 && !(m_inputContext->priv()->keyboardRectangle().intersects(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(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(globalCursorRectangle.toRect().topLeft());
143 globalCursorRectangle.moveTopLeft(tl);
144 m_cursorHandleVisible = m_cursorHandleVisible
145 && m_inputContext->cursorRectIntersectsClipRect()
146 && !(m_inputContext->priv()->keyboardRectangle().intersects(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(QAbstractAnimation::DeleteWhenStopped);
157 }
158 }
159
reloadGraphics()160 void DesktopInputSelectionControl::reloadGraphics()
161 {
162 Settings *settings = Settings::instance();
163 const QString stylePath = QString::fromLatin1(":/QtQuick/VirtualKeyboard/content/styles/%1/images/selectionhandle-bottom.svg")
164 .arg(settings->styleName());
165 QImageReader imageReader(stylePath);
166 QSize sz = imageReader.size(); // SVG handler will return default size
167 sz.scale(20, 20, Qt::KeepAspectRatioByExpanding);
168 imageReader.setScaledSize(sz);
169 m_handleImage = imageReader.read();
170
171 m_anchorSelectionHandle->applyImage(m_handleWindowSize); // applies m_handleImage for both selection handles
172 m_cursorSelectionHandle->applyImage(m_handleWindowSize);
173 }
174
createHandles()175 void DesktopInputSelectionControl::createHandles()
176 {
177 if (QWindow *focusWindow = QGuiApplication::focusWindow()) {
178 Settings *settings = Settings::instance();
179 connect(settings, &Settings::styleChanged, this, &DesktopInputSelectionControl::reloadGraphics);
180
181 m_anchorSelectionHandle = QSharedPointer<InputSelectionHandle>::create(this, focusWindow);
182 m_cursorSelectionHandle = QSharedPointer<InputSelectionHandle>::create(this, focusWindow);
183
184 reloadGraphics();
185 if (QCoreApplication *app = QCoreApplication::instance()) {
186 connect(app, &QCoreApplication::aboutToQuit,
187 this, &DesktopInputSelectionControl::destroyHandles);
188 }
189 }
190 }
191
destroyHandles()192 void DesktopInputSelectionControl::destroyHandles()
193 {
194 m_anchorSelectionHandle.reset();
195 m_cursorSelectionHandle.reset();
196 }
197
setEnabled(bool enable)198 void 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(m_inputContext, &QVirtualKeyboardInputContext::anchorRectangleChanged, this, &DesktopInputSelectionControl::updateAnchorHandlePosition);
206 connect(m_inputContext, &QVirtualKeyboardInputContext::cursorRectangleChanged, this, &DesktopInputSelectionControl::updateCursorHandlePosition);
207 connect(m_inputContext, &QVirtualKeyboardInputContext::anchorRectIntersectsClipRectChanged, this, &DesktopInputSelectionControl::updateVisibility);
208 connect(m_inputContext, &QVirtualKeyboardInputContext::cursorRectIntersectsClipRectChanged, this, &DesktopInputSelectionControl::updateVisibility);
209 if (focusWindow)
210 focusWindow->installEventFilter(this);
211 } else {
212 if (focusWindow)
213 focusWindow->removeEventFilter(this);
214 disconnect(m_inputContext, &QVirtualKeyboardInputContext::cursorRectIntersectsClipRectChanged, this, &DesktopInputSelectionControl::updateVisibility);
215 disconnect(m_inputContext, &QVirtualKeyboardInputContext::anchorRectIntersectsClipRectChanged, this, &DesktopInputSelectionControl::updateVisibility);
216 disconnect(m_inputContext, &QVirtualKeyboardInputContext::anchorRectangleChanged, this, &DesktopInputSelectionControl::updateAnchorHandlePosition);
217 disconnect(m_inputContext, &QVirtualKeyboardInputContext::cursorRectangleChanged, this, &DesktopInputSelectionControl::updateCursorHandlePosition);
218 }
219 updateVisibility();
220 }
221
handleImage()222 QImage *DesktopInputSelectionControl::handleImage()
223 {
224 return &m_handleImage;
225 }
226
eventFilter(QObject * object,QEvent * event)227 bool 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(h.rect.center()); // ### map to desktoppanel
260 const QPoint delta = mousePos - curHandleCenter;
261 h.delta = delta;
262 h.squaredDistance = QPoint::dotProduct(delta, 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(mousePos);
270 if (m_anchorHandleVisible && handles[closestHandle].rect.contains(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(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(delta, delta) > startDragDistance * startDragDistance)
290 m_handleState = HandleIsMoving;
291 }
292 if (m_handleState == HandleIsMoving) {
293 QPoint cursorPos = mousePos - m_distanceBetweenMouseAndCursor;
294 cursorPos = focusWindow->mapFromGlobal(cursorPos);
295 if (m_currentDragHandle == CursorHandle)
296 m_inputContext->setSelectionOnFocusObject(m_otherSelectionPoint, cursorPos);
297 else
298 m_inputContext->setSelectionOnFocusObject(cursorPos, m_otherSelectionPoint);
299 qDeleteAll(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(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(focusWindow, e);
319 delete e;
320 }
321 m_eventFilterEnabled = true;
322 }
323 m_handleState = HandleIsReleased;
324 }
325 }
326 return false;
327 }
328
329 } // namespace QtVirtualKeyboard
330 QT_END_NAMESPACE
331