Revision control
++ // Copyright (C) 2016 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++ // Qt-Security score:significant reason:default
++
++ #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(":/qt-project.org/imports/QtQuick/VirtualKeyboard/Styles/Builtin/%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);
++ updateAnchorHandlePosition();
++ updateCursorHandlePosition();
++ 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->globalPosition().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 curHandleTopCenter = focusWindow->mapToGlobal(QPoint(h.rect.x() + qRound((qreal)h.rect.width() / 2), h.rect.top())); // ### map to desktoppanel
++ const QPoint delta = mousePos - curHandleTopCenter;
++ 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->scenePosition(); 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;
++ 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->position(), me->scenePosition(), me->globalPosition(),
++ 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->globalPosition().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