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 | |
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(sender: m_inputContext, signal: &QVirtualKeyboardInputContext::selectionControlVisibleChanged, receiver: this, slot: &DesktopInputSelectionControl::updateVisibility); |
60 | } |
61 | |
62 | /* |
63 | * Includes the hit area surrounding the visual handle |
64 | */ |
65 | 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 | */ |
76 | QRect DesktopInputSelectionControl::anchorHandleRect() const |
77 | { |
78 | return handleRectForCursorRect(cursorRect: m_inputContext->anchorRectangle()); |
79 | } |
80 | |
81 | /* |
82 | * Includes the hit area surrounding the visual handle |
83 | */ |
84 | QRect DesktopInputSelectionControl::cursorHandleRect() const |
85 | { |
86 | return handleRectForCursorRect(cursorRect: m_inputContext->cursorRectangle()); |
87 | } |
88 | |
89 | void 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 | |
97 | void 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 | |
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(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 | |
160 | void 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 | |
175 | void 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 | |
192 | void DesktopInputSelectionControl::destroyHandles() |
193 | { |
194 | m_anchorSelectionHandle.reset(); |
195 | m_cursorSelectionHandle.reset(); |
196 | } |
197 | |
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(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 | |
222 | QImage *DesktopInputSelectionControl::handleImage() |
223 | { |
224 | return &m_handleImage; |
225 | } |
226 | |
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(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 |
330 | QT_END_NAMESPACE |
331 | |