diff --git a/src/gui/kernel/qplatformintegration.cpp b/src/gui/kernel/qplatformintegration.cpp index 151151de23..448d670209 100644 --- a/src/gui/kernel/qplatformintegration.cpp +++ b/src/gui/kernel/qplatformintegration.cpp @@ -418,6 +418,8 @@ QVariant QPlatformIntegration::styleHint(StyleHint hint) const return QPlatformTheme::defaultThemeHint(QPlatformTheme::UiEffects); case WheelScrollLines: return QPlatformTheme::defaultThemeHint(QPlatformTheme::WheelScrollLines); + case MouseQuickSelectionThreshold: + return QPlatformTheme::defaultThemeHint(QPlatformTheme::MouseQuickSelectionThreshold); } return 0; diff --git a/src/gui/kernel/qplatformintegration.h b/src/gui/kernel/qplatformintegration.h index a7d9a87502..37884e1f78 100644 --- a/src/gui/kernel/qplatformintegration.h +++ b/src/gui/kernel/qplatformintegration.h @@ -164,6 +164,7 @@ public: UiEffects, WheelScrollLines, ShowShortcutsInContextMenus, + MouseQuickSelectionThreshold }; virtual QVariant styleHint(StyleHint hint) const; diff --git a/src/gui/kernel/qplatformtheme.cpp b/src/gui/kernel/qplatformtheme.cpp index 1856952805..277d976dde 100644 --- a/src/gui/kernel/qplatformtheme.cpp +++ b/src/gui/kernel/qplatformtheme.cpp @@ -559,6 +559,8 @@ QVariant QPlatformTheme::defaultThemeHint(ThemeHint hint) dist = defaultThemeHint(MouseDoubleClickDistance).toInt(&ok) * 2; return QVariant(ok ? dist : 10); } + case MouseQuickSelectionThreshold: + return QVariant(10); } return QVariant(); } diff --git a/src/gui/kernel/qplatformtheme.h b/src/gui/kernel/qplatformtheme.h index 87873d446f..1d6049a98d 100644 --- a/src/gui/kernel/qplatformtheme.h +++ b/src/gui/kernel/qplatformtheme.h @@ -117,7 +117,8 @@ public: WheelScrollLines, TouchDoubleTapDistance, ShowShortcutsInContextMenus, - IconFallbackSearchPaths + IconFallbackSearchPaths, + MouseQuickSelectionThreshold }; enum DialogType { diff --git a/src/gui/kernel/qstylehints.cpp b/src/gui/kernel/qstylehints.cpp index 0850228ee5..b2d968c046 100644 --- a/src/gui/kernel/qstylehints.cpp +++ b/src/gui/kernel/qstylehints.cpp @@ -79,6 +79,7 @@ public: , m_tabFocusBehavior(-1) , m_uiEffects(-1) , m_wheelScrollLines(-1) + , m_mouseQuickSelectionThreshold(-1) {} int m_mouseDoubleClickInterval; @@ -90,6 +91,7 @@ public: int m_tabFocusBehavior; int m_uiEffects; int m_wheelScrollLines; + int m_mouseQuickSelectionThreshold; }; /*! @@ -537,4 +539,38 @@ void QStyleHints::setWheelScrollLines(int scrollLines) emit wheelScrollLinesChanged(scrollLines); } +/*! + Sets the mouse quick selection threshold. + \internal + \sa mouseQuickSelectionThreshold() + \since 5.11 +*/ +void QStyleHints::setMouseQuickSelectionThreshold(int threshold) +{ + Q_D(QStyleHints); + if (d->m_mouseQuickSelectionThreshold == threshold) + return; + d->m_mouseQuickSelectionThreshold = threshold; + emit mouseDoubleClickIntervalChanged(threshold); +} + +/*! + \property QStyleHints::mouseQuickSelectionThreshold + \brief Quick selection mouse threshold in QLineEdit. + + This property defines how much the mouse cursor should be moved along the y axis + to trigger a quick selection during a normal QLineEdit text selection. + + If the property value is less than or equal to 0, the quick selection feature is disabled. + + \since 5.11 +*/ +int QStyleHints::mouseQuickSelectionThreshold() const +{ + Q_D(const QStyleHints); + if (d->m_mouseQuickSelectionThreshold >= 0) + return d->m_mouseQuickSelectionThreshold; + return themeableHint(QPlatformTheme::MouseQuickSelectionThreshold, QPlatformIntegration::MouseQuickSelectionThreshold).toInt(); +} + QT_END_NAMESPACE diff --git a/src/gui/kernel/qstylehints.h b/src/gui/kernel/qstylehints.h index 2c2e048b15..7b0683e9b1 100644 --- a/src/gui/kernel/qstylehints.h +++ b/src/gui/kernel/qstylehints.h @@ -73,6 +73,7 @@ class Q_GUI_EXPORT QStyleHints : public QObject Q_PROPERTY(bool singleClickActivation READ singleClickActivation STORED false CONSTANT FINAL) Q_PROPERTY(bool useHoverEffects READ useHoverEffects WRITE setUseHoverEffects NOTIFY useHoverEffectsChanged FINAL) Q_PROPERTY(int wheelScrollLines READ wheelScrollLines NOTIFY wheelScrollLinesChanged FINAL) + Q_PROPERTY(int mouseQuickSelectionThreshold READ mouseQuickSelectionThreshold WRITE setMouseQuickSelectionThreshold NOTIFY mouseQuickSelectionThresholdChanged FINAL) public: void setMouseDoubleClickInterval(int mouseDoubleClickInterval); @@ -104,6 +105,8 @@ public: void setUseHoverEffects(bool useHoverEffects); int wheelScrollLines() const; void setWheelScrollLines(int scrollLines); + void setMouseQuickSelectionThreshold(int threshold); + int mouseQuickSelectionThreshold() const; Q_SIGNALS: void cursorFlashTimeChanged(int cursorFlashTime); @@ -115,6 +118,7 @@ Q_SIGNALS: void tabFocusBehaviorChanged(Qt::TabFocusBehavior tabFocusBehavior); void useHoverEffectsChanged(bool useHoverEffects); void wheelScrollLinesChanged(int scrollLines); + void mouseQuickSelectionThresholdChanged(int threshold); private: friend class QGuiApplication; diff --git a/src/widgets/widgets/qlineedit.cpp b/src/widgets/widgets/qlineedit.cpp index 156b0e331c..e3b348f0ef 100644 --- a/src/widgets/widgets/qlineedit.cpp +++ b/src/widgets/widgets/qlineedit.cpp @@ -1466,6 +1466,8 @@ bool QLineEdit::event(QEvent * e) #endif } else if (e->type() == QEvent::Resize) { d->positionSideWidgets(); + } else if (e->type() == QEvent::StyleChange) { + d->initMouseYThreshold(); } #ifdef QT_KEYPAD_NAVIGATION if (QApplication::keypadNavigationEnabled()) { @@ -1546,7 +1548,17 @@ void QLineEdit::mouseMoveEvent(QMouseEvent * e) const bool select = (d->imHints & Qt::ImhNoPredictiveText); #endif #ifndef QT_NO_IM - if (d->control->composeMode() && select) { + if (d->mouseYThreshold > 0 && e->pos().y() > d->mousePressPos.y() + d->mouseYThreshold) { + if (layoutDirection() == Qt::RightToLeft) + d->control->home(select); + else + d->control->end(select); + } else if (d->mouseYThreshold > 0 && e->pos().y() + d->mouseYThreshold < d->mousePressPos.y()) { + if (layoutDirection() == Qt::RightToLeft) + d->control->end(select); + else + d->control->home(select); + } else if (d->control->composeMode() && select) { int startPos = d->xToPos(d->mousePressPos.x()); int currentPos = d->xToPos(e->pos().x()); if (startPos != currentPos) diff --git a/src/widgets/widgets/qlineedit_p.cpp b/src/widgets/widgets/qlineedit_p.cpp index c66b842223..6a8af53c97 100644 --- a/src/widgets/widgets/qlineedit_p.cpp +++ b/src/widgets/widgets/qlineedit_p.cpp @@ -56,6 +56,7 @@ #endif #include #include +#include #include QT_BEGIN_NAMESPACE @@ -232,6 +233,13 @@ void QLineEditPrivate::init(const QString& txt) q->setAcceptDrops(true); q->setAttribute(Qt::WA_MacShowFocusRect); + + initMouseYThreshold(); +} + +void QLineEditPrivate::initMouseYThreshold() +{ + mouseYThreshold = QGuiApplication::styleHints()->mouseQuickSelectionThreshold(); } QRect QLineEditPrivate::adjustedContentsRect() const diff --git a/src/widgets/widgets/qlineedit_p.h b/src/widgets/widgets/qlineedit_p.h index a3f549ad31..39f670b0b0 100644 --- a/src/widgets/widgets/qlineedit_p.h +++ b/src/widgets/widgets/qlineedit_p.h @@ -141,7 +141,7 @@ public: dragEnabled(0), clickCausedFocus(0), hscroll(0), vscroll(0), alignment(Qt::AlignLeading | Qt::AlignVCenter), leftTextMargin(0), topTextMargin(0), rightTextMargin(0), bottomTextMargin(0), - lastTextSize(0) + lastTextSize(0), mouseYThreshold(0) { } @@ -155,6 +155,7 @@ public: QPointer selectAllAction; #endif void init(const QString&); + void initMouseYThreshold(); QRect adjustedControlRect(const QRect &) const; @@ -253,6 +254,7 @@ private: SideWidgetEntryList leadingSideWidgets; SideWidgetEntryList trailingSideWidgets; int lastTextSize; + int mouseYThreshold; }; Q_DECLARE_TYPEINFO(QLineEditPrivate::SideWidgetEntry, Q_PRIMITIVE_TYPE); Q_DECLARE_TYPEINFO(QLineEditPrivate::SideWidgetLocation, Q_PRIMITIVE_TYPE); diff --git a/tests/auto/widgets/widgets/qlineedit/tst_qlineedit.cpp b/tests/auto/widgets/widgets/qlineedit/tst_qlineedit.cpp index 68e6f0267e..1513025f16 100644 --- a/tests/auto/widgets/widgets/qlineedit/tst_qlineedit.cpp +++ b/tests/auto/widgets/widgets/qlineedit/tst_qlineedit.cpp @@ -67,6 +67,8 @@ #include "../../../shared/platforminputcontext.h" #include +Q_LOGGING_CATEGORY(lcTests, "qt.widgets.tests") + QT_BEGIN_NAMESPACE class QPainter; QT_END_NAMESPACE @@ -300,8 +302,8 @@ private slots: void shortcutOverrideOnReadonlyLineEdit_data(); void shortcutOverrideOnReadonlyLineEdit(); void QTBUG59957_clearButtonLeftmostAction(); - void QTBUG_60319_setInputMaskCheckImSurroundingText(); + void testQuickSelectionWithMouse(); protected slots: void editingFinished(); @@ -4699,5 +4701,90 @@ void tst_QLineEdit::QTBUG_60319_setInputMaskCheckImSurroundingText() QCOMPARE(surroundingText.length(), cursorPosition); } +void tst_QLineEdit::testQuickSelectionWithMouse() +{ + const auto text = QStringLiteral("This is quite a long line of text."); + const auto prefix = QStringLiteral("Th"); + const auto suffix = QStringLiteral("t."); + QVERIFY(text.startsWith(prefix)); + QVERIFY(text.endsWith(suffix)); + + QLineEdit lineEdit; + lineEdit.setText(text); + lineEdit.show(); + + const QPoint center = lineEdit.contentsRect().center(); + + // Normal mouse selection from left to right, y doesn't change. + QTest::mousePress(lineEdit.windowHandle(), Qt::LeftButton, Qt::NoModifier, center); + QTest::mouseMove(lineEdit.windowHandle(), center + QPoint(20, 0)); + qCDebug(lcTests) << "Selected text:" << lineEdit.selectedText(); + QVERIFY(!lineEdit.selectedText().isEmpty()); + QVERIFY(!lineEdit.selectedText().endsWith(suffix)); + + // Normal mouse selection from left to right, y change is below threshold. + QTest::mousePress(lineEdit.windowHandle(), Qt::LeftButton, Qt::NoModifier, center); + QTest::mouseMove(lineEdit.windowHandle(), center + QPoint(20, 5)); + qCDebug(lcTests) << "Selected text:" << lineEdit.selectedText(); + QVERIFY(!lineEdit.selectedText().isEmpty()); + QVERIFY(!lineEdit.selectedText().endsWith(suffix)); + + // Normal mouse selection from right to left, y doesn't change. + QTest::mousePress(lineEdit.windowHandle(), Qt::LeftButton, Qt::NoModifier, center); + QTest::mouseMove(lineEdit.windowHandle(), center + QPoint(-20, 0)); + qCDebug(lcTests) << "Selected text:" << lineEdit.selectedText(); + QVERIFY(!lineEdit.selectedText().isEmpty()); + QVERIFY(!lineEdit.selectedText().startsWith(prefix)); + + // Normal mouse selection from right to left, y change is below threshold. + QTest::mousePress(lineEdit.windowHandle(), Qt::LeftButton, Qt::NoModifier, center); + QTest::mouseMove(lineEdit.windowHandle(), center + QPoint(-20, -5)); + qCDebug(lcTests) << "Selected text:" << lineEdit.selectedText(); + QVERIFY(!lineEdit.selectedText().isEmpty()); + QVERIFY(!lineEdit.selectedText().startsWith(prefix)); + + const int offset = QGuiApplication::styleHints()->mouseQuickSelectionThreshold() + 1; + + // Select the whole right half. + QTest::mousePress(lineEdit.windowHandle(), Qt::LeftButton, Qt::NoModifier, center); + QTest::mouseMove(lineEdit.windowHandle(), center + QPoint(1, offset)); + qCDebug(lcTests) << "Selected text:" << lineEdit.selectedText(); + QVERIFY(lineEdit.selectedText().endsWith(suffix)); + + // Select the whole left half. + QTest::mousePress(lineEdit.windowHandle(), Qt::LeftButton, Qt::NoModifier, center); + QTest::mouseMove(lineEdit.windowHandle(), center + QPoint(1, -offset)); + qCDebug(lcTests) << "Selected text:" << lineEdit.selectedText(); + QVERIFY(lineEdit.selectedText().startsWith(prefix)); + + // Normal selection -> quick selection -> back to normal selection. + QTest::mousePress(lineEdit.windowHandle(), Qt::LeftButton, Qt::NoModifier, center); + QTest::mouseMove(lineEdit.windowHandle(), center + QPoint(20, 0)); + const auto partialSelection = lineEdit.selectedText(); + qCDebug(lcTests) << "Selected text:" << lineEdit.selectedText(); + QVERIFY(!partialSelection.endsWith(suffix)); + QTest::mouseMove(lineEdit.windowHandle(), center + QPoint(20, offset)); + qCDebug(lcTests) << "Selected text:" << lineEdit.selectedText(); + QVERIFY(lineEdit.selectedText().endsWith(suffix)); + QTest::mouseMove(lineEdit.windowHandle(), center + QPoint(20, 0)); + qCDebug(lcTests) << "Selected text:" << lineEdit.selectedText(); +#ifdef Q_PROCESSOR_ARM + QEXPECT_FAIL("", "Currently fails on gcc-armv7, needs investigation.", Continue); +#endif + QCOMPARE(lineEdit.selectedText(), partialSelection); + + lineEdit.setLayoutDirection(Qt::RightToLeft); + + // Select the whole left half (RTL layout). + QTest::mousePress(lineEdit.windowHandle(), Qt::LeftButton, Qt::NoModifier, center); + QTest::mouseMove(lineEdit.windowHandle(), center + QPoint(1, offset)); + QVERIFY(lineEdit.selectedText().startsWith(prefix)); + + // Select the whole right half (RTL layout). + QTest::mousePress(lineEdit.windowHandle(), Qt::LeftButton, Qt::NoModifier, center); + QTest::mouseMove(lineEdit.windowHandle(), center + QPoint(1, -offset)); + QVERIFY(lineEdit.selectedText().endsWith(suffix)); +} + QTEST_MAIN(tst_QLineEdit) #include "tst_qlineedit.moc"