| /**************************************************************************** |
| ** |
| ** Copyright (C) 2016 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the Qt Virtual Keyboard module of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:GPL$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 3 or (at your option) any later version |
| ** approved by the KDE Free Qt Foundation. The licenses are as published by |
| ** the Free Software Foundation and appearing in the file LICENSE.GPL3 |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| #include <QtVirtualKeyboard/private/desktopinputselectioncontrol_p.h> |
| #include <QtVirtualKeyboard/qvirtualkeyboardinputcontext.h> |
| #include <QtVirtualKeyboard/private/qvirtualkeyboardinputcontext_p.h> |
| #include <QtVirtualKeyboard/private/inputselectionhandle_p.h> |
| #include <QtVirtualKeyboard/private/settings_p.h> |
| #include <QtVirtualKeyboard/private/platforminputcontext_p.h> |
| |
| #include <QtCore/qpropertyanimation.h> |
| #include <QtGui/qguiapplication.h> |
| #include <QtGui/qstylehints.h> |
| #include <QtGui/qimagereader.h> |
| |
| QT_BEGIN_NAMESPACE |
| namespace QtVirtualKeyboard { |
| |
| DesktopInputSelectionControl::DesktopInputSelectionControl(QObject *parent, QVirtualKeyboardInputContext *inputContext) |
| : QObject(parent), |
| m_inputContext(inputContext), |
| m_anchorSelectionHandle(), |
| m_cursorSelectionHandle(), |
| m_handleState(HandleIsReleased), |
| m_enabled(false), |
| m_anchorHandleVisible(false), |
| m_cursorHandleVisible(false), |
| m_eventFilterEnabled(true), |
| m_handleWindowSize(40, 40*1.12) // because a finger patch is slightly taller than its width |
| { |
| QWindow *focusWindow = QGuiApplication::focusWindow(); |
| Q_ASSERT(focusWindow); |
| connect(m_inputContext, &QVirtualKeyboardInputContext::selectionControlVisibleChanged, this, &DesktopInputSelectionControl::updateVisibility); |
| } |
| |
| /* |
| * Includes the hit area surrounding the visual handle |
| */ |
| QRect DesktopInputSelectionControl::handleRectForCursorRect(const QRectF &cursorRect) const |
| { |
| const int topMargin = (m_handleWindowSize.height() - m_handleImage.size().height())/2; |
| const QPoint pos(int(cursorRect.x() + (cursorRect.width() - m_handleWindowSize.width())/2), |
| int(cursorRect.bottom()) - topMargin); |
| return QRect(pos, m_handleWindowSize); |
| } |
| |
| /* |
| * Includes the hit area surrounding the visual handle |
| */ |
| QRect DesktopInputSelectionControl::anchorHandleRect() const |
| { |
| return handleRectForCursorRect(m_inputContext->anchorRectangle()); |
| } |
| |
| /* |
| * Includes the hit area surrounding the visual handle |
| */ |
| QRect DesktopInputSelectionControl::cursorHandleRect() const |
| { |
| return handleRectForCursorRect(m_inputContext->cursorRectangle()); |
| } |
| |
| void DesktopInputSelectionControl::updateAnchorHandlePosition() |
| { |
| if (QWindow *focusWindow = QGuiApplication::focusWindow()) { |
| const QPoint pos = focusWindow->mapToGlobal(anchorHandleRect().topLeft()); |
| m_anchorSelectionHandle->setPosition(pos); |
| } |
| } |
| |
| void DesktopInputSelectionControl::updateCursorHandlePosition() |
| { |
| if (QWindow *focusWindow = QGuiApplication::focusWindow()) { |
| const QPoint pos = focusWindow->mapToGlobal(cursorHandleRect().topLeft()); |
| m_cursorSelectionHandle->setPosition(pos); |
| } |
| } |
| |
| void DesktopInputSelectionControl::updateVisibility() |
| { |
| if (!m_enabled) { |
| // if VKB is hidden, we must hide the selection handles immediately, |
| // because it might mean that the application is shutting down. |
| m_anchorSelectionHandle->hide(); |
| m_cursorSelectionHandle->hide(); |
| m_anchorHandleVisible = false; |
| m_cursorHandleVisible = false; |
| return; |
| } |
| const bool wasAnchorVisible = m_anchorHandleVisible; |
| const bool wasCursorVisible = m_cursorHandleVisible; |
| const bool makeVisible = (m_inputContext->isSelectionControlVisible() || m_handleState == HandleIsMoving) && m_enabled; |
| |
| m_anchorHandleVisible = makeVisible; |
| if (QWindow *focusWindow = QGuiApplication::focusWindow()) { |
| QRectF globalAnchorRectangle = m_inputContext->anchorRectangle(); |
| QPoint tl = focusWindow->mapToGlobal(globalAnchorRectangle.toRect().topLeft()); |
| globalAnchorRectangle.moveTopLeft(tl); |
| m_anchorHandleVisible = m_anchorHandleVisible |
| && m_inputContext->anchorRectIntersectsClipRect() |
| && !(m_inputContext->priv()->keyboardRectangle().intersects(globalAnchorRectangle)); |
| } |
| |
| if (wasAnchorVisible != m_anchorHandleVisible) { |
| const qreal end = m_anchorHandleVisible ? 1 : 0; |
| if (m_anchorHandleVisible) |
| m_anchorSelectionHandle->show(); |
| QPropertyAnimation *anim = new QPropertyAnimation(m_anchorSelectionHandle.data(), "opacity"); |
| anim->setEndValue(end); |
| anim->start(QAbstractAnimation::DeleteWhenStopped); |
| } |
| |
| m_cursorHandleVisible = makeVisible; |
| if (QWindow *focusWindow = QGuiApplication::focusWindow()) { |
| QRectF globalCursorRectangle = m_inputContext->cursorRectangle(); |
| QPoint tl = focusWindow->mapToGlobal(globalCursorRectangle.toRect().topLeft()); |
| globalCursorRectangle.moveTopLeft(tl); |
| m_cursorHandleVisible = m_cursorHandleVisible |
| && m_inputContext->cursorRectIntersectsClipRect() |
| && !(m_inputContext->priv()->keyboardRectangle().intersects(globalCursorRectangle)); |
| |
| } |
| |
| if (wasCursorVisible != m_cursorHandleVisible) { |
| const qreal end = m_cursorHandleVisible ? 1 : 0; |
| if (m_cursorHandleVisible) |
| m_cursorSelectionHandle->show(); |
| QPropertyAnimation *anim = new QPropertyAnimation(m_cursorSelectionHandle.data(), "opacity"); |
| anim->setEndValue(end); |
| anim->start(QAbstractAnimation::DeleteWhenStopped); |
| } |
| } |
| |
| void DesktopInputSelectionControl::reloadGraphics() |
| { |
| Settings *settings = Settings::instance(); |
| const QString stylePath = QString::fromLatin1(":/QtQuick/VirtualKeyboard/content/styles/%1/images/selectionhandle-bottom.svg") |
| .arg(settings->styleName()); |
| QImageReader imageReader(stylePath); |
| QSize sz = imageReader.size(); // SVG handler will return default size |
| sz.scale(20, 20, Qt::KeepAspectRatioByExpanding); |
| imageReader.setScaledSize(sz); |
| m_handleImage = imageReader.read(); |
| |
| m_anchorSelectionHandle->applyImage(m_handleWindowSize); // applies m_handleImage for both selection handles |
| m_cursorSelectionHandle->applyImage(m_handleWindowSize); |
| } |
| |
| void DesktopInputSelectionControl::createHandles() |
| { |
| if (QWindow *focusWindow = QGuiApplication::focusWindow()) { |
| Settings *settings = Settings::instance(); |
| connect(settings, &Settings::styleChanged, this, &DesktopInputSelectionControl::reloadGraphics); |
| |
| m_anchorSelectionHandle = QSharedPointer<InputSelectionHandle>::create(this, focusWindow); |
| m_cursorSelectionHandle = QSharedPointer<InputSelectionHandle>::create(this, focusWindow); |
| |
| reloadGraphics(); |
| if (QCoreApplication *app = QCoreApplication::instance()) { |
| connect(app, &QCoreApplication::aboutToQuit, |
| this, &DesktopInputSelectionControl::destroyHandles); |
| } |
| } |
| } |
| |
| void DesktopInputSelectionControl::destroyHandles() |
| { |
| m_anchorSelectionHandle.reset(); |
| m_cursorSelectionHandle.reset(); |
| } |
| |
| void DesktopInputSelectionControl::setEnabled(bool enable) |
| { |
| // setEnabled(true) just means that the handles _can_ be made visible |
| // This will typically be set when a input field gets focus (and having selection). |
| m_enabled = enable; |
| QWindow *focusWindow = QGuiApplication::focusWindow(); |
| if (enable) { |
| connect(m_inputContext, &QVirtualKeyboardInputContext::anchorRectangleChanged, this, &DesktopInputSelectionControl::updateAnchorHandlePosition); |
| connect(m_inputContext, &QVirtualKeyboardInputContext::cursorRectangleChanged, this, &DesktopInputSelectionControl::updateCursorHandlePosition); |
| connect(m_inputContext, &QVirtualKeyboardInputContext::anchorRectIntersectsClipRectChanged, this, &DesktopInputSelectionControl::updateVisibility); |
| connect(m_inputContext, &QVirtualKeyboardInputContext::cursorRectIntersectsClipRectChanged, this, &DesktopInputSelectionControl::updateVisibility); |
| if (focusWindow) |
| focusWindow->installEventFilter(this); |
| } else { |
| if (focusWindow) |
| focusWindow->removeEventFilter(this); |
| disconnect(m_inputContext, &QVirtualKeyboardInputContext::cursorRectIntersectsClipRectChanged, this, &DesktopInputSelectionControl::updateVisibility); |
| disconnect(m_inputContext, &QVirtualKeyboardInputContext::anchorRectIntersectsClipRectChanged, this, &DesktopInputSelectionControl::updateVisibility); |
| disconnect(m_inputContext, &QVirtualKeyboardInputContext::anchorRectangleChanged, this, &DesktopInputSelectionControl::updateAnchorHandlePosition); |
| disconnect(m_inputContext, &QVirtualKeyboardInputContext::cursorRectangleChanged, this, &DesktopInputSelectionControl::updateCursorHandlePosition); |
| } |
| updateVisibility(); |
| } |
| |
| QImage *DesktopInputSelectionControl::handleImage() |
| { |
| return &m_handleImage; |
| } |
| |
| bool DesktopInputSelectionControl::eventFilter(QObject *object, QEvent *event) |
| { |
| QWindow *focusWindow = QGuiApplication::focusWindow(); |
| if (!m_cursorSelectionHandle || !m_eventFilterEnabled || object != focusWindow) |
| return false; |
| const bool windowMoved = event->type() == QEvent::Move; |
| const bool windowResized = event->type() == QEvent::Resize; |
| if (windowMoved || windowResized) { |
| if (m_enabled) { |
| if (windowMoved) { |
| updateAnchorHandlePosition(); |
| updateCursorHandlePosition(); |
| } |
| updateVisibility(); |
| } |
| } else if (event->type() == QEvent::MouseButtonPress) { |
| QMouseEvent *me = static_cast<QMouseEvent*>(event); |
| const QPoint mousePos = me->screenPos().toPoint(); |
| |
| // calculate distances from mouse pos to each handle, |
| // then choose to interact with the nearest handle |
| struct SelectionHandleInfo { |
| qreal squaredDistance; |
| QPoint delta; |
| QRect rect; |
| }; |
| SelectionHandleInfo handles[2]; |
| handles[AnchorHandle].rect = anchorHandleRect(); |
| handles[CursorHandle].rect = cursorHandleRect(); |
| |
| for (int i = 0; i <= CursorHandle; ++i) { |
| SelectionHandleInfo &h = handles[i]; |
| QPoint curHandleCenter = focusWindow->mapToGlobal(h.rect.center()); // ### map to desktoppanel |
| const QPoint delta = mousePos - curHandleCenter; |
| h.delta = delta; |
| h.squaredDistance = QPoint::dotProduct(delta, delta); |
| } |
| |
| // (squared) distances calculated, pick the closest handle |
| HandleType closestHandle = (handles[AnchorHandle].squaredDistance < handles[CursorHandle].squaredDistance ? AnchorHandle : CursorHandle); |
| |
| // Can not be replaced with me->windowPos(); because the event might be forwarded from the window of the handle |
| const QPoint windowPos = focusWindow->mapFromGlobal(mousePos); |
| if (m_anchorHandleVisible && handles[closestHandle].rect.contains(windowPos)) { |
| m_currentDragHandle = closestHandle; |
| m_distanceBetweenMouseAndCursor = handles[closestHandle].delta - QPoint(0, m_handleWindowSize.height()/2 + 4); |
| m_handleState = HandleIsHeld; |
| m_handleDragStartedPosition = mousePos; |
| const QRect otherRect = handles[1 - closestHandle].rect; |
| m_otherSelectionPoint = QPoint(otherRect.x() + otherRect.width()/2, otherRect.top() - 4); |
| |
| QMouseEvent *mouseEvent = new QMouseEvent(me->type(), me->localPos(), me->windowPos(), me->screenPos(), |
| me->button(), me->buttons(), me->modifiers(), me->source()); |
| m_eventQueue.append(mouseEvent); |
| return true; |
| } |
| } else if (event->type() == QEvent::MouseMove) { |
| QMouseEvent *me = static_cast<QMouseEvent*>(event); |
| QPoint mousePos = me->screenPos().toPoint(); |
| if (m_handleState == HandleIsHeld) { |
| QPoint delta = m_handleDragStartedPosition - mousePos; |
| const int startDragDistance = QGuiApplication::styleHints()->startDragDistance(); |
| if (QPoint::dotProduct(delta, delta) > startDragDistance * startDragDistance) |
| m_handleState = HandleIsMoving; |
| } |
| if (m_handleState == HandleIsMoving) { |
| QPoint cursorPos = mousePos - m_distanceBetweenMouseAndCursor; |
| cursorPos = focusWindow->mapFromGlobal(cursorPos); |
| if (m_currentDragHandle == CursorHandle) |
| m_inputContext->setSelectionOnFocusObject(m_otherSelectionPoint, cursorPos); |
| else |
| m_inputContext->setSelectionOnFocusObject(cursorPos, m_otherSelectionPoint); |
| qDeleteAll(m_eventQueue); |
| m_eventQueue.clear(); |
| return true; |
| } |
| } else if (event->type() == QEvent::MouseButtonRelease) { |
| if (m_handleState == HandleIsMoving) { |
| m_handleState = HandleIsReleased; |
| qDeleteAll(m_eventQueue); |
| m_eventQueue.clear(); |
| return true; |
| } else { |
| if (QWindow *focusWindow = QGuiApplication::focusWindow()) { |
| // playback event queue. These are events that were not designated |
| // for the handles in hindsight. |
| // This is typically MousePress and MouseRelease (not interleaved with MouseMove) |
| // that should instead go through to the underlying input editor |
| m_eventFilterEnabled = false; |
| while (!m_eventQueue.isEmpty()) { |
| QMouseEvent *e = m_eventQueue.takeFirst(); |
| QCoreApplication::sendEvent(focusWindow, e); |
| delete e; |
| } |
| m_eventFilterEnabled = true; |
| } |
| m_handleState = HandleIsReleased; |
| } |
| } |
| return false; |
| } |
| |
| } // namespace QtVirtualKeyboard |
| QT_END_NAMESPACE |