QLineEdit: implement quick text selection by mouse

This is a standard feature in GtkEntry widgets or HTML
<input type="text"> elements. During a normal text selection by mouse
(LeftButton press + mouse move event), it's now possible to quickly
select all the text from the start of the selection to the end
of the line edit by moving the mouse cursor down.
By moving it up instead, all the text up to the start of the line edit
gets selected. If the layout direction is right-to-left, the semantic of
the mouse movement is inverted.

This feature is only enabled if the y() of the mouse move event is
bigger than a fixed threshold, to avoid unexpected selections in the
normal case. This threshold is set by the QPlatformTheme and a value
smaller than zero disables this feature.

The threshold is updated whenever the style or the screen changes.

[ChangeLog][QtWidgets][QLineEdit] Implemented quick text selection by
mouse in QLineEdit.

Change-Id: I4de33c2d11c033ec295de2b2ea81adf786324f4b
Reviewed-by: Shawn Rutledge <shawn.rutledge@qt.io>
This commit is contained in:
Elvis Angelaccio 2017-10-26 18:22:14 +02:00
parent 8db29d92df
commit 4772ac90fa
10 changed files with 159 additions and 4 deletions

View File

@ -418,6 +418,8 @@ QVariant QPlatformIntegration::styleHint(StyleHint hint) const
return QPlatformTheme::defaultThemeHint(QPlatformTheme::UiEffects); return QPlatformTheme::defaultThemeHint(QPlatformTheme::UiEffects);
case WheelScrollLines: case WheelScrollLines:
return QPlatformTheme::defaultThemeHint(QPlatformTheme::WheelScrollLines); return QPlatformTheme::defaultThemeHint(QPlatformTheme::WheelScrollLines);
case MouseQuickSelectionThreshold:
return QPlatformTheme::defaultThemeHint(QPlatformTheme::MouseQuickSelectionThreshold);
} }
return 0; return 0;

View File

@ -164,6 +164,7 @@ public:
UiEffects, UiEffects,
WheelScrollLines, WheelScrollLines,
ShowShortcutsInContextMenus, ShowShortcutsInContextMenus,
MouseQuickSelectionThreshold
}; };
virtual QVariant styleHint(StyleHint hint) const; virtual QVariant styleHint(StyleHint hint) const;

View File

@ -559,6 +559,8 @@ QVariant QPlatformTheme::defaultThemeHint(ThemeHint hint)
dist = defaultThemeHint(MouseDoubleClickDistance).toInt(&ok) * 2; dist = defaultThemeHint(MouseDoubleClickDistance).toInt(&ok) * 2;
return QVariant(ok ? dist : 10); return QVariant(ok ? dist : 10);
} }
case MouseQuickSelectionThreshold:
return QVariant(10);
} }
return QVariant(); return QVariant();
} }

View File

@ -117,7 +117,8 @@ public:
WheelScrollLines, WheelScrollLines,
TouchDoubleTapDistance, TouchDoubleTapDistance,
ShowShortcutsInContextMenus, ShowShortcutsInContextMenus,
IconFallbackSearchPaths IconFallbackSearchPaths,
MouseQuickSelectionThreshold
}; };
enum DialogType { enum DialogType {

View File

@ -79,6 +79,7 @@ public:
, m_tabFocusBehavior(-1) , m_tabFocusBehavior(-1)
, m_uiEffects(-1) , m_uiEffects(-1)
, m_wheelScrollLines(-1) , m_wheelScrollLines(-1)
, m_mouseQuickSelectionThreshold(-1)
{} {}
int m_mouseDoubleClickInterval; int m_mouseDoubleClickInterval;
@ -90,6 +91,7 @@ public:
int m_tabFocusBehavior; int m_tabFocusBehavior;
int m_uiEffects; int m_uiEffects;
int m_wheelScrollLines; int m_wheelScrollLines;
int m_mouseQuickSelectionThreshold;
}; };
/*! /*!
@ -537,4 +539,38 @@ void QStyleHints::setWheelScrollLines(int scrollLines)
emit wheelScrollLinesChanged(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 QT_END_NAMESPACE

View File

@ -73,6 +73,7 @@ class Q_GUI_EXPORT QStyleHints : public QObject
Q_PROPERTY(bool singleClickActivation READ singleClickActivation STORED false CONSTANT FINAL) Q_PROPERTY(bool singleClickActivation READ singleClickActivation STORED false CONSTANT FINAL)
Q_PROPERTY(bool useHoverEffects READ useHoverEffects WRITE setUseHoverEffects NOTIFY useHoverEffectsChanged FINAL) Q_PROPERTY(bool useHoverEffects READ useHoverEffects WRITE setUseHoverEffects NOTIFY useHoverEffectsChanged FINAL)
Q_PROPERTY(int wheelScrollLines READ wheelScrollLines NOTIFY wheelScrollLinesChanged FINAL) Q_PROPERTY(int wheelScrollLines READ wheelScrollLines NOTIFY wheelScrollLinesChanged FINAL)
Q_PROPERTY(int mouseQuickSelectionThreshold READ mouseQuickSelectionThreshold WRITE setMouseQuickSelectionThreshold NOTIFY mouseQuickSelectionThresholdChanged FINAL)
public: public:
void setMouseDoubleClickInterval(int mouseDoubleClickInterval); void setMouseDoubleClickInterval(int mouseDoubleClickInterval);
@ -104,6 +105,8 @@ public:
void setUseHoverEffects(bool useHoverEffects); void setUseHoverEffects(bool useHoverEffects);
int wheelScrollLines() const; int wheelScrollLines() const;
void setWheelScrollLines(int scrollLines); void setWheelScrollLines(int scrollLines);
void setMouseQuickSelectionThreshold(int threshold);
int mouseQuickSelectionThreshold() const;
Q_SIGNALS: Q_SIGNALS:
void cursorFlashTimeChanged(int cursorFlashTime); void cursorFlashTimeChanged(int cursorFlashTime);
@ -115,6 +118,7 @@ Q_SIGNALS:
void tabFocusBehaviorChanged(Qt::TabFocusBehavior tabFocusBehavior); void tabFocusBehaviorChanged(Qt::TabFocusBehavior tabFocusBehavior);
void useHoverEffectsChanged(bool useHoverEffects); void useHoverEffectsChanged(bool useHoverEffects);
void wheelScrollLinesChanged(int scrollLines); void wheelScrollLinesChanged(int scrollLines);
void mouseQuickSelectionThresholdChanged(int threshold);
private: private:
friend class QGuiApplication; friend class QGuiApplication;

View File

@ -1466,6 +1466,8 @@ bool QLineEdit::event(QEvent * e)
#endif #endif
} else if (e->type() == QEvent::Resize) { } else if (e->type() == QEvent::Resize) {
d->positionSideWidgets(); d->positionSideWidgets();
} else if (e->type() == QEvent::StyleChange) {
d->initMouseYThreshold();
} }
#ifdef QT_KEYPAD_NAVIGATION #ifdef QT_KEYPAD_NAVIGATION
if (QApplication::keypadNavigationEnabled()) { if (QApplication::keypadNavigationEnabled()) {
@ -1546,7 +1548,17 @@ void QLineEdit::mouseMoveEvent(QMouseEvent * e)
const bool select = (d->imHints & Qt::ImhNoPredictiveText); const bool select = (d->imHints & Qt::ImhNoPredictiveText);
#endif #endif
#ifndef QT_NO_IM #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 startPos = d->xToPos(d->mousePressPos.x());
int currentPos = d->xToPos(e->pos().x()); int currentPos = d->xToPos(e->pos().x());
if (startPos != currentPos) if (startPos != currentPos)

View File

@ -56,6 +56,7 @@
#endif #endif
#include <qpainter.h> #include <qpainter.h>
#include <qpropertyanimation.h> #include <qpropertyanimation.h>
#include <qstylehints.h>
#include <qvalidator.h> #include <qvalidator.h>
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
@ -232,6 +233,13 @@ void QLineEditPrivate::init(const QString& txt)
q->setAcceptDrops(true); q->setAcceptDrops(true);
q->setAttribute(Qt::WA_MacShowFocusRect); q->setAttribute(Qt::WA_MacShowFocusRect);
initMouseYThreshold();
}
void QLineEditPrivate::initMouseYThreshold()
{
mouseYThreshold = QGuiApplication::styleHints()->mouseQuickSelectionThreshold();
} }
QRect QLineEditPrivate::adjustedContentsRect() const QRect QLineEditPrivate::adjustedContentsRect() const

View File

@ -141,7 +141,7 @@ public:
dragEnabled(0), clickCausedFocus(0), hscroll(0), vscroll(0), dragEnabled(0), clickCausedFocus(0), hscroll(0), vscroll(0),
alignment(Qt::AlignLeading | Qt::AlignVCenter), alignment(Qt::AlignLeading | Qt::AlignVCenter),
leftTextMargin(0), topTextMargin(0), rightTextMargin(0), bottomTextMargin(0), leftTextMargin(0), topTextMargin(0), rightTextMargin(0), bottomTextMargin(0),
lastTextSize(0) lastTextSize(0), mouseYThreshold(0)
{ {
} }
@ -155,6 +155,7 @@ public:
QPointer<QAction> selectAllAction; QPointer<QAction> selectAllAction;
#endif #endif
void init(const QString&); void init(const QString&);
void initMouseYThreshold();
QRect adjustedControlRect(const QRect &) const; QRect adjustedControlRect(const QRect &) const;
@ -253,6 +254,7 @@ private:
SideWidgetEntryList leadingSideWidgets; SideWidgetEntryList leadingSideWidgets;
SideWidgetEntryList trailingSideWidgets; SideWidgetEntryList trailingSideWidgets;
int lastTextSize; int lastTextSize;
int mouseYThreshold;
}; };
Q_DECLARE_TYPEINFO(QLineEditPrivate::SideWidgetEntry, Q_PRIMITIVE_TYPE); Q_DECLARE_TYPEINFO(QLineEditPrivate::SideWidgetEntry, Q_PRIMITIVE_TYPE);
Q_DECLARE_TYPEINFO(QLineEditPrivate::SideWidgetLocation, Q_PRIMITIVE_TYPE); Q_DECLARE_TYPEINFO(QLineEditPrivate::SideWidgetLocation, Q_PRIMITIVE_TYPE);

View File

@ -67,6 +67,8 @@
#include "../../../shared/platforminputcontext.h" #include "../../../shared/platforminputcontext.h"
#include <private/qinputmethod_p.h> #include <private/qinputmethod_p.h>
Q_LOGGING_CATEGORY(lcTests, "qt.widgets.tests")
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
class QPainter; class QPainter;
QT_END_NAMESPACE QT_END_NAMESPACE
@ -300,8 +302,8 @@ private slots:
void shortcutOverrideOnReadonlyLineEdit_data(); void shortcutOverrideOnReadonlyLineEdit_data();
void shortcutOverrideOnReadonlyLineEdit(); void shortcutOverrideOnReadonlyLineEdit();
void QTBUG59957_clearButtonLeftmostAction(); void QTBUG59957_clearButtonLeftmostAction();
void QTBUG_60319_setInputMaskCheckImSurroundingText(); void QTBUG_60319_setInputMaskCheckImSurroundingText();
void testQuickSelectionWithMouse();
protected slots: protected slots:
void editingFinished(); void editingFinished();
@ -4699,5 +4701,90 @@ void tst_QLineEdit::QTBUG_60319_setInputMaskCheckImSurroundingText()
QCOMPARE(surroundingText.length(), cursorPosition); 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) QTEST_MAIN(tst_QLineEdit)
#include "tst_qlineedit.moc" #include "tst_qlineedit.moc"