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