diff options
author | Shawn Rutledge <shawn.rutledge@qt.io> | 2022-08-24 13:09:43 +0200 |
---|---|---|
committer | Qt Cherry-pick Bot <cherrypick_bot@qt-project.org> | 2022-08-29 18:48:14 +0000 |
commit | dfd6170ebb9ce1b1106d8de953029a04279c616c (patch) | |
tree | 71b200861d918048228c543dce70cf1a7309062c | |
parent | cbef5bae849d1d8c079e0b5fdd3d63fff5616ab8 (diff) |
TextArea: selectByMouse default=true, but not on touchscreens
When you drag a finger across a TextArea, it should not select text.
- your finger probably covers several characters, so you can't see where
you're selecting until afterwards
- if the item is in a Flickable, flicking by touch should be prioritized
- if flicking happens, the text cursor should not move; but to avoid
losing too much functionality, we allow it to move on release, if
the TextArea gets the release (i.e. if it still has the exclusive
grab)
- TextArea's pressed, pressAndHold and released signals continue to
behave the same with touch as with mouse
As with the TextEdit change in 90d3fac73a10b9363fd34e6757cc730d7a0c086c,
we now distinguish mouse events that are synthesized from non-mouse
devices and avoid mouse-like behaviors as described above, but there is
no behavior change if the event comes from an actual mouse or touchpad.
In QQuickPressHandler::mousePressEvent() we give the original event's
device to the delayed press event, now that we check the device to
distinguish "real" mouse events from those that are synthesized from
touch.
[ChangeLog][Controls][TextArea] The selectByMouse property is now
enabled by default, but no longer enables selecting by dragging your
finger across text on a touchscreen. Platforms that are optimized for
touchscreens normally use special text-selection handles, which interact
with Qt via QInputMethod. You can opt out of the behavior change by
setting the environment variable
QT_QUICK_CONTROLS_TEXT_SELECTION_BEHAVIOR=old.
Task-number: QTBUG-10684
Task-number: QTBUG-38934
Task-number: QTBUG-90494
Task-number: QTBUG-101205
Change-Id: Icbe81e547b1cf8d5e3cc3ed922f12c7b411ca658
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: Richard Moe Gustavsen <richard.gustavsen@qt.io>
(cherry picked from commit 3ebb3540df9729d336d90225d3b4c1d2ca182e8e)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
10 files changed, 271 insertions, 2 deletions
diff --git a/src/quick/items/qquicktextedit.cpp b/src/quick/items/qquicktextedit.cpp index ba8d8a1fc4..3bc1db006a 100644 --- a/src/quick/items/qquicktextedit.cpp +++ b/src/quick/items/qquicktextedit.cpp @@ -3377,7 +3377,7 @@ void QQuickTextEdit::setOldSelectionDefault() setKeepMouseGrab(false); d->control->setTextInteractionFlags(d->control->textInteractionFlags() & ~Qt::TextSelectableByMouse); d->control->setTouchDragSelectionEnabled(true); - qCDebug(lcTextEdit, "pre-6.4 behavior chosen by import version: selectByMouse defaults false; if enabled, touchscreen acts like a mouse"); + qCDebug(lcTextEdit, "pre-6.4 behavior chosen: selectByMouse defaults false; if enabled, touchscreen acts like a mouse"); } // TODO in 6.7.0: remove the note about versions prior to 6.4 in selectByMouse() documentation diff --git a/src/quicktemplates2/qquickpresshandler.cpp b/src/quicktemplates2/qquickpresshandler.cpp index b1c2208e99..d752bb36f1 100644 --- a/src/quicktemplates2/qquickpresshandler.cpp +++ b/src/quicktemplates2/qquickpresshandler.cpp @@ -20,7 +20,7 @@ void QQuickPressHandler::mousePressEvent(QMouseEvent *event) if (Qt::LeftButton == (event->buttons() & Qt::LeftButton)) { timer.start(QGuiApplication::styleHints()->mousePressAndHoldInterval(), control); delayedMousePressEvent = new QMouseEvent(event->type(), event->position().toPoint(), event->globalPosition().toPoint(), - event->button(), event->buttons(), event->modifiers()); + event->button(), event->buttons(), event->modifiers(), event->pointingDevice()); } else { timer.stop(); } diff --git a/src/quicktemplates2/qquicktextarea.cpp b/src/quicktemplates2/qquicktextarea.cpp index 48bdcddb64..0572746d14 100644 --- a/src/quicktemplates2/qquicktextarea.cpp +++ b/src/quicktemplates2/qquicktextarea.cpp @@ -19,6 +19,8 @@ QT_BEGIN_NAMESPACE +using namespace Qt::StringLiterals; + /*! \qmltype TextArea \inherits TextEdit @@ -509,6 +511,10 @@ QQuickTextArea::QQuickTextArea(QQuickItem *parent) QObjectPrivate::connect(this, &QQuickTextEdit::readOnlyChanged, d, &QQuickTextAreaPrivate::readOnlyChanged); +#if QT_VERSION < QT_VERSION_CHECK(7, 0, 0) + if (qEnvironmentVariable("QT_QUICK_CONTROLS_TEXT_SELECTION_BEHAVIOR") == u"old"_s) + QQuickTextEdit::setOldSelectionDefault(); +#endif } QQuickTextArea::~QQuickTextArea() diff --git a/tests/auto/quickcontrols2/CMakeLists.txt b/tests/auto/quickcontrols2/CMakeLists.txt index 5bd05b95ee..ce391638f4 100644 --- a/tests/auto/quickcontrols2/CMakeLists.txt +++ b/tests/auto/quickcontrols2/CMakeLists.txt @@ -30,6 +30,7 @@ add_subdirectory(qquickmenubar) add_subdirectory(qquickninepatchimage) add_subdirectory(qquickpopup) add_subdirectory(qquickstyle) +add_subdirectory(qquicktextarea) add_subdirectory(qquickuniversalstyle) add_subdirectory(qquickuniversalstyleconf) add_subdirectory(revisions) diff --git a/tests/auto/quickcontrols2/qquicktextarea/CMakeLists.txt b/tests/auto/quickcontrols2/qquicktextarea/CMakeLists.txt new file mode 100644 index 0000000000..553a1588ae --- /dev/null +++ b/tests/auto/quickcontrols2/qquicktextarea/CMakeLists.txt @@ -0,0 +1,46 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +##################################################################### +## tst_qquicktextarea Test: +##################################################################### + +# Collect test data +file(GLOB_RECURSE test_data_glob + RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} + data/*) +list(APPEND test_data ${test_data_glob}) + +qt_internal_add_test(tst_qquicktextarea + SOURCES + tst_qquicktextarea.cpp + DEFINES + QQC2_IMPORT_PATH=\\\"${CMAKE_CURRENT_SOURCE_DIR}/../../../../src/quickcontrols2\\\" + PUBLIC_LIBRARIES + Qt::CorePrivate + Qt::Gui + Qt::GuiPrivate + Qt::QmlPrivate + Qt::QuickControls2 + Qt::QuickControls2Private + Qt::QuickControlsTestUtilsPrivate + Qt::QuickPrivate + Qt::QuickTemplates2Private + Qt::QuickTest + Qt::QuickTestUtilsPrivate + Qt::TestPrivate + TESTDATA ${test_data} +) + +## Scopes: +##################################################################### + +qt_internal_extend_target(tst_qquicktextarea CONDITION ANDROID OR IOS + DEFINES + QT_QMLTEST_DATADIR=\\\":/data\\\" +) + +qt_internal_extend_target(tst_qquicktextarea CONDITION NOT ANDROID AND NOT IOS + DEFINES + QT_QMLTEST_DATADIR=\\\"${CMAKE_CURRENT_SOURCE_DIR}/data\\\" +) diff --git a/tests/auto/quickcontrols2/qquicktextarea/data/mouseselection_default.qml b/tests/auto/quickcontrols2/qquicktextarea/data/mouseselection_default.qml new file mode 100644 index 0000000000..cb51b80545 --- /dev/null +++ b/tests/auto/quickcontrols2/qquicktextarea/data/mouseselection_default.qml @@ -0,0 +1,6 @@ +import QtQuick.Controls + +TextArea { + text: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" +} + diff --git a/tests/auto/quickcontrols2/qquicktextarea/data/mouseselection_old_default.qml b/tests/auto/quickcontrols2/qquicktextarea/data/mouseselection_old_default.qml new file mode 100644 index 0000000000..1401be36c1 --- /dev/null +++ b/tests/auto/quickcontrols2/qquicktextarea/data/mouseselection_old_default.qml @@ -0,0 +1,6 @@ +import QtQuick +import QtQuick.Controls 6.3 + +TextArea { + text: "0123456789 ABCDEFGHIJKLMNOPQRSTUVWXYZ" +} diff --git a/tests/auto/quickcontrols2/qquicktextarea/data/mouseselection_old_overridden.qml b/tests/auto/quickcontrols2/qquicktextarea/data/mouseselection_old_overridden.qml new file mode 100644 index 0000000000..c0e6fb60a8 --- /dev/null +++ b/tests/auto/quickcontrols2/qquicktextarea/data/mouseselection_old_overridden.qml @@ -0,0 +1,7 @@ +import QtQuick +import QtQuick.Controls 6.3 + +TextArea { + selectByMouse: true + text: "0123456789 ABCDEFGHIJKLMNOPQRSTUVWXYZ" +} diff --git a/tests/auto/quickcontrols2/qquicktextarea/data/twoInAColumn.qml b/tests/auto/quickcontrols2/qquicktextarea/data/twoInAColumn.qml new file mode 100644 index 0000000000..944f5598ac --- /dev/null +++ b/tests/auto/quickcontrols2/qquicktextarea/data/twoInAColumn.qml @@ -0,0 +1,35 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +ColumnLayout { + height: 200 + width: 400 + spacing: -6 + Rectangle { + border.color: top.activeFocus ? "steelblue" : "lightgrey" + Layout.fillHeight: true + Layout.fillWidth: true + Layout.margins: 6 + TextArea { + id: top + objectName: "top" + text: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + anchors.fill: parent + verticalAlignment: TextArea.AlignTop + } + } + Rectangle { + border.color: bottom.activeFocus ? "steelblue" : "lightgrey" + Layout.fillHeight: true + Layout.fillWidth: true + Layout.margins: 6 + TextArea { + id: bottom + objectName: "bottom" + text: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + anchors.fill: parent + verticalAlignment: TextArea.AlignTop + } + } +} diff --git a/tests/auto/quickcontrols2/qquicktextarea/tst_qquicktextarea.cpp b/tests/auto/quickcontrols2/qquicktextarea/tst_qquicktextarea.cpp new file mode 100644 index 0000000000..f9f31d9c3a --- /dev/null +++ b/tests/auto/quickcontrols2/qquicktextarea/tst_qquicktextarea.cpp @@ -0,0 +1,162 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include <QtTest/qtest.h> +#include <QtTest/qsignalspy.h> +#include <QtTest/qtesttouch.h> + +#include <QtGui/qfontmetrics.h> +#include <QtGui/private/qguiapplication_p.h> +#include <QtGui/qpa/qplatformintegration.h> +#include <QtGui/qtestsupport_gui.h> +#include <QtQuick/qquickview.h> +#include <QtQuickTestUtils/private/qmlutils_p.h> +#include <QtQuickTestUtils/private/viewtestutils_p.h> +#include <QtQuickTemplates2/private/qquicktextarea_p.h> +#include <QtQuickControlsTestUtils/private/qtest_quickcontrols_p.h> + +class tst_QQuickTextArea : public QQmlDataTest +{ + Q_OBJECT + +public: + tst_QQuickTextArea(); + +private slots: + void initTestCase() override; + void touchscreenDoesNotSelect_data(); + void touchscreenDoesNotSelect(); + void touchscreenSetsFocusAndMovesCursor(); + +private: + static bool hasWindowActivation(); + QScopedPointer<QPointingDevice> touchDevice = QScopedPointer<QPointingDevice>(QTest::createTouchDevice()); +}; + +tst_QQuickTextArea::tst_QQuickTextArea() + : QQmlDataTest(QT_QMLTEST_DATADIR) +{ +} + +void tst_QQuickTextArea::initTestCase() +{ + QQmlDataTest::initTestCase(); + qputenv("QML_NO_TOUCH_COMPRESSION", "1"); +} + +void tst_QQuickTextArea::touchscreenDoesNotSelect_data() +{ + QTest::addColumn<QUrl>("src"); + QTest::addColumn<bool>("setEnv"); + QTest::addColumn<bool>("selectByMouse"); + QTest::addColumn<bool>("selectByTouch"); + QTest::newRow("new default") << testFileUrl("mouseselection_default.qml") << false << true << false; +#if QT_VERSION < QT_VERSION_CHECK(7, 0, 0) + QTest::newRow("putenv") << testFileUrl("mouseselection_default.qml") << true << false << false; + QTest::newRow("old_import") << testFileUrl("mouseselection_old_default.qml") << false << true << false; + QTest::newRow("old+putenv") << testFileUrl("mouseselection_old_default.qml") << true << false << false; + QTest::newRow("old+putenv+selectByMouse") << testFileUrl("mouseselection_old_overridden.qml") << true << true << true; +#endif +} + +void tst_QQuickTextArea::touchscreenDoesNotSelect() +{ + QFETCH(QUrl, src); + QFETCH(bool, setEnv); + QFETCH(bool, selectByMouse); + QFETCH(bool, selectByTouch); + + if (setEnv) + qputenv("QT_QUICK_CONTROLS_TEXT_SELECTION_BEHAVIOR", "old"); + else + qunsetenv("QT_QUICK_CONTROLS_TEXT_SELECTION_BEHAVIOR"); + + QQuickView window; + QVERIFY(QQuickTest::showView(window, src)); + + QQuickTextEdit *textEditObject = qobject_cast<QQuickTextEdit *>(window.rootObject()); + QVERIFY(textEditObject != nullptr); + QCOMPARE(textEditObject->selectByMouse(), selectByMouse); + textEditObject->setSelectByMouse(true); // enable selection with pre-6.4 import version + QVERIFY(textEditObject->selectedText().isEmpty()); + + if (selectByMouse) { + // press-drag-and-release from x1 to x2 + int x1 = 10; + int x2 = 70; + int y = QFontMetrics(textEditObject->font()).height() / 2; + QTest::touchEvent(&window, touchDevice.data()).press(0, QPoint(x1,y), &window); + QTest::touchEvent(&window, touchDevice.data()).move(0, QPoint(x2,y), &window); + QTest::touchEvent(&window, touchDevice.data()).release(0, QPoint(x2,y), &window); + QQuickTouchUtils::flush(&window); + // if the import version is old enough, fall back to old behavior: touch swipe _does_ select text if selectByMouse is true + QCOMPARE(textEditObject->selectedText().isEmpty(), !selectByTouch); + } +} + +void tst_QQuickTextArea::touchscreenSetsFocusAndMovesCursor() +{ + if (!hasWindowActivation()) + QSKIP("Window activation is not supported"); + qunsetenv("QT_QUICK_CONTROLS_TEXT_SELECTION_BEHAVIOR"); + + QQuickView window; + QVERIFY(QQuickTest::showView(window, testFileUrl("twoInAColumn.qml"))); + window.requestActivate(); + QVERIFY(QTest::qWaitForWindowActive(&window)); + + QQuickTextEdit *top = window.rootObject()->findChild<QQuickTextEdit*>("top"); + QVERIFY(top); + QQuickTextEdit *bottom = window.rootObject()->findChild<QQuickTextEdit*>("bottom"); + QVERIFY(bottom); + const auto len = bottom->text().length(); + + // tap the bottom field + const qreal yOffset = bottom->topPadding() + 6; // where to tap or drag to hit the text + QPoint p1 = bottom->mapToScene({60, yOffset}).toPoint(); + QTest::touchEvent(&window, touchDevice.data()).press(0, p1, &window); + QQuickTouchUtils::flush(&window); + // text cursor is at 0 by default, on press + QCOMPARE(bottom->cursorPosition(), 0); + // the focus changes and the cursor moves after release (not after press, as in TextEdit) + QTest::touchEvent(&window, touchDevice.data()).release(0, p1, &window); + QQuickTouchUtils::flush(&window); + QCOMPARE(qApp->focusObject(), bottom); + QTRY_COMPARE_GT(bottom->cursorPosition(), 0); + + // typing a character inserts it at the cursor position + QVERIFY(!bottom->text().contains('q')); + QTest::keyClick(&window, Qt::Key_Q); + QCOMPARE(bottom->text().length(), len + 1); + QCOMPARE_GT(bottom->text().indexOf('q'), 0); + + // press-drag-and-release from p1 to p2 on the top field + p1 = top->mapToScene({0, yOffset}).toPoint(); + QPoint p2 = top->mapToScene({76, yOffset}).toPoint(); + QTest::touchEvent(&window, touchDevice.data()).press(0, p1, &window); + QQuickTouchUtils::flush(&window); + QTest::touchEvent(&window, touchDevice.data()).move(0, p2, &window); + QQuickTouchUtils::flush(&window); + QTest::touchEvent(&window, touchDevice.data()).release(0, p2, &window); + QQuickTouchUtils::flush(&window); + QCOMPARE(qApp->focusObject(), top); + QVERIFY(top->selectedText().isEmpty()); + QCOMPARE_GT(top->cursorPosition(), 0); + + // touch-drag did not select text, but mouse-drag from p2 back to p1 + // does select the first part of the text, and leave the cursor at the beginning + QTest::mousePress(&window, Qt::LeftButton, {}, p2); + QTest::mouseMove(&window, p1); + QTest::mouseRelease(&window, Qt::LeftButton, {}, p1); + QCOMPARE(top->cursorPosition(), 0); + QCOMPARE_GT(top->selectedText().size(), 0); +} + +bool tst_QQuickTextArea::hasWindowActivation() +{ + return (QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::WindowActivation)); +} + +QTEST_QUICKCONTROLS_MAIN(tst_QQuickTextArea) + +#include "tst_qquicktextarea.moc" |