From 919b60b4fcc72fdcd5dc0e80e642f922da17cd96 Mon Sep 17 00:00:00 2001 From: Andrew den Exter Date: Fri, 6 Jul 2012 16:05:33 +1000 Subject: [PATCH] Fix restoration of cursor position and selection after undo/redo. If a text selection was deleted, the selection should be restored by an undo, but not if the selection was part of an atomic operation like the DeleteStartOfWord key sequence. Change-Id: Ia37f29c78f6367c60377c539c4e394e014485a49 Reviewed-by: Yann Bodson --- src/quick/items/qquicktextinput.cpp | 133 ++++++++++++++----- src/quick/items/qquicktextinput_p_p.h | 5 + .../quick/qquicktextinput/tst_qquicktextinput.cpp | 146 ++++++++++++++++++++- 3 files changed, 245 insertions(+), 39 deletions(-) diff --git a/src/quick/items/qquicktextinput.cpp b/src/quick/items/qquicktextinput.cpp index bb38e51..4dbd39e 100644 --- a/src/quick/items/qquicktextinput.cpp +++ b/src/quick/items/qquicktextinput.cpp @@ -2815,7 +2815,7 @@ void QQuickTextInputPrivate::cancelPreedit() void QQuickTextInputPrivate::backspace() { int priorState = m_undoState; - if (hasSelectedText()) { + if (separateSelection()) { removeSelectedText(); } else if (m_cursor) { --m_cursor; @@ -2848,7 +2848,7 @@ void QQuickTextInputPrivate::backspace() void QQuickTextInputPrivate::del() { int priorState = m_undoState; - if (hasSelectedText()) { + if (separateSelection()) { removeSelectedText(); } else { int n = m_textLayout.nextCursorPosition(m_cursor) - m_cursor; @@ -2868,7 +2868,8 @@ void QQuickTextInputPrivate::del() void QQuickTextInputPrivate::insert(const QString &newText) { int priorState = m_undoState; - removeSelectedText(); + if (separateSelection()) + removeSelectedText(); internalInsert(newText); finishChange(priorState); } @@ -2881,6 +2882,7 @@ void QQuickTextInputPrivate::insert(const QString &newText) void QQuickTextInputPrivate::clear() { int priorState = m_undoState; + separateSelection(); m_selstart = 0; m_selend = m_text.length(); removeSelectedText(); @@ -3031,6 +3033,7 @@ void QQuickTextInputPrivate::processInputMethodEvent(QInputMethodEvent *event) if (isGettingInput) { // If any text is being input, remove selected text. priorState = m_undoState; + separateSelection(); if (m_echoMode == QQuickTextInput::PasswordEchoOnEdit && !m_passwordEchoEditing) { updatePasswordEchoEditing(true); m_selstart = 0; @@ -3303,8 +3306,7 @@ void QQuickTextInputPrivate::internalInsert(const QString &s) if (delay > 0) m_passwordEchoTimer.start(delay, q); } - if (hasSelectedText()) - addCommand(Command(SetSelection, m_cursor, 0, m_selstart, m_selend)); + Q_ASSERT(!hasSelectedText()); // insert(), processInputMethodEvent() call removeSelectedText() first. if (m_maskData) { QString ms = maskString(m_cursor, s); for (int i = 0; i < (int) ms.length(); ++i) { @@ -3341,8 +3343,7 @@ void QQuickTextInputPrivate::internalDelete(bool wasBackspace) { if (m_cursor < (int) m_text.length()) { cancelPasswordEchoTimer(); - if (hasSelectedText()) - addCommand(Command(SetSelection, m_cursor, 0, m_selstart, m_selend)); + Q_ASSERT(!hasSelectedText()); // del(), backspace() call removeSelectedText() first. addCommand(Command((CommandType)((m_maskData ? 2 : 0) + (wasBackspace ? Remove : Delete)), m_cursor, m_text.at(m_cursor), -1, -1)); if (m_maskData) { @@ -3368,9 +3369,7 @@ void QQuickTextInputPrivate::removeSelectedText() { if (m_selstart < m_selend && m_selend <= (int) m_text.length()) { cancelPasswordEchoTimer(); - separate(); int i ; - addCommand(Command(SetSelection, m_cursor, 0, m_selstart, m_selend)); if (m_selstart <= m_cursor && m_cursor < m_selend) { // cursor is within the selection. Split up the commands // to be able to restore the correct cursor position @@ -3399,6 +3398,25 @@ void QQuickTextInputPrivate::removeSelectedText() /*! \internal + Adds the current selection to the undo history. + + Returns true if there is a current selection and false otherwise. +*/ + +bool QQuickTextInputPrivate::separateSelection() +{ + if (hasSelectedText()) { + separate(); + addCommand(Command(SetSelection, m_cursor, 0, m_selstart, m_selend)); + return true; + } else { + return false; + } +} + +/*! + \internal + Parses the input mask specified by \a maskFields to generate the mask data used to handle input masks. */ @@ -3797,11 +3815,14 @@ void QQuickTextInputPrivate::internalUndo(int until) } if (until < 0 && m_undoState) { Command& next = m_history[m_undoState-1]; - if (next.type != cmd.type && next.type < RemoveSelection - && (cmd.type < RemoveSelection || next.type == Separator)) + if (next.type != cmd.type + && next.type < RemoveSelection + && (cmd.type < RemoveSelection || next.type == Separator)) { break; + } } } + separate(); m_textDirty = true; } @@ -3839,9 +3860,12 @@ void QQuickTextInputPrivate::internalRedo() } if (m_undoState < (int)m_history.size()) { Command& next = m_history[m_undoState]; - if (next.type != cmd.type && cmd.type < RemoveSelection && next.type != Separator - && (next.type < RemoveSelection || cmd.type == Separator)) + if (next.type != cmd.type + && cmd.type < RemoveSelection + && next.type != Separator + && (next.type < RemoveSelection || cmd.type == Separator)) { break; + } } } m_textDirty = true; @@ -3937,16 +3961,12 @@ void QQuickTextInput::timerEvent(QTimerEvent *event) void QQuickTextInputPrivate::processKeyEvent(QKeyEvent* event) { Q_Q(QQuickTextInput); - bool inlineCompletionAccepted = false; if (event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return) { if (hasAcceptableInput(m_text) || fixup()) { emit q->accepted(); } - if (inlineCompletionAccepted) - event->accept(); - else - event->ignore(); + event->ignore(); return; } @@ -3996,11 +4016,8 @@ void QQuickTextInputPrivate::processKeyEvent(QKeyEvent* event) } } else if (event == QKeySequence::DeleteEndOfLine) { - if (!m_readOnly) { - setSelection(m_cursor, end()); - copy(); - del(); - } + if (!m_readOnly) + deleteEndOfLine(); } #endif //QT_NO_CLIPBOARD else if (event == QKeySequence::MoveToStartOfLine || event == QKeySequence::MoveToStartOfBlock) { @@ -4065,16 +4082,12 @@ void QQuickTextInputPrivate::processKeyEvent(QKeyEvent* event) del(); } else if (event == QKeySequence::DeleteEndOfWord) { - if (!m_readOnly) { - cursorWordForward(true); - del(); - } + if (!m_readOnly) + deleteEndOfWord(); } else if (event == QKeySequence::DeleteStartOfWord) { - if (!m_readOnly) { - cursorWordBackward(true); - del(); - } + if (!m_readOnly) + deleteStartOfWord(); } #endif // QT_NO_SHORTCUT else { @@ -4082,10 +4095,8 @@ void QQuickTextInputPrivate::processKeyEvent(QKeyEvent* event) if (event->modifiers() & Qt::ControlModifier) { switch (event->key()) { case Qt::Key_Backspace: - if (!m_readOnly) { - cursorWordBackward(true); - del(); - } + if (!m_readOnly) + deleteStartOfWord(); break; default: if (!handled) @@ -4125,6 +4136,58 @@ void QQuickTextInputPrivate::processKeyEvent(QKeyEvent* event) event->accept(); } +/*! + \internal + + Deletes the portion of the word before the current cursor position. +*/ + +void QQuickTextInputPrivate::deleteStartOfWord() +{ + int priorState = m_undoState; + Command cmd(SetSelection, m_cursor, 0, m_selstart, m_selend); + separate(); + cursorWordBackward(true); + addCommand(cmd); + removeSelectedText(); + finishChange(priorState); +} + +/*! + \internal + + Deletes the portion of the word after the current cursor position. +*/ + +void QQuickTextInputPrivate::deleteEndOfWord() +{ + int priorState = m_undoState; + Command cmd(SetSelection, m_cursor, 0, m_selstart, m_selend); + separate(); + cursorWordForward(true); + // moveCursor (sometimes) calls separate() so we need to add the command after that so the + // cursor position and selection are restored in the same undo operation as the remove. + addCommand(cmd); + removeSelectedText(); + finishChange(priorState); +} + +/*! + \internal + + Deletes all text from the cursor position to the end of the line. +*/ + +void QQuickTextInputPrivate::deleteEndOfLine() +{ + int priorState = m_undoState; + Command cmd(SetSelection, m_cursor, 0, m_selstart, m_selend); + separate(); + setSelection(m_cursor, end()); + addCommand(cmd); + removeSelectedText(); + finishChange(priorState); +} QT_END_NAMESPACE diff --git a/src/quick/items/qquicktextinput_p_p.h b/src/quick/items/qquicktextinput_p_p.h index 0cc0846..e147c17 100644 --- a/src/quick/items/qquicktextinput_p_p.h +++ b/src/quick/items/qquicktextinput_p_p.h @@ -435,6 +435,11 @@ private: inline void separate() { m_separator = true; } + bool separateSelection(); + void deleteStartOfWord(); + void deleteEndOfWord(); + void deleteEndOfLine(); + enum ValidatorState { #ifndef QT_NO_VALIDATOR InvalidInput = QValidator::Invalid, diff --git a/tests/auto/quick/qquicktextinput/tst_qquicktextinput.cpp b/tests/auto/quick/qquicktextinput/tst_qquicktextinput.cpp index a4d1920..7cc9ddb 100644 --- a/tests/auto/quick/qquicktextinput/tst_qquicktextinput.cpp +++ b/tests/auto/quick/qquicktextinput/tst_qquicktextinput.cpp @@ -192,6 +192,8 @@ private slots: void undo_keypressevents_data(); void undo_keypressevents(); + void backspaceSurrogatePairs(); + void QTBUG_19956(); void QTBUG_19956_data(); void QTBUG_19956_regexp(); @@ -236,8 +238,8 @@ Q_DECLARE_METATYPE(KeyList) void tst_qquicktextinput::simulateKeys(QWindow *window, const QList &keys) { for (int i = 0; i < keys.count(); ++i) { - const int key = keys.at(i).first; - const int modifiers = key & Qt::KeyboardModifierMask; + const int key = keys.at(i).first & ~Qt::KeyboardModifierMask; + const int modifiers = keys.at(i).first & Qt::KeyboardModifierMask; const QString text = !keys.at(i).second.isNull() ? QString(keys.at(i).second) : QString(); QKeyEvent press(QEvent::KeyPress, Qt::Key(key), Qt::KeyboardModifiers(modifiers), text); @@ -5138,7 +5140,7 @@ void tst_qquicktextinput::undo_keypressevents_data() << QKeySequence::MoveToStartOfLine // selecting 'AB' << (Qt::Key_Right | Qt::ShiftModifier) << (Qt::Key_Right | Qt::ShiftModifier) - << Qt::Key_Delete + << Qt::Key_Backspace << QKeySequence::Undo << Qt::Key_Right << (Qt::Key_Right | Qt::ShiftModifier) << (Qt::Key_Right | Qt::ShiftModifier) @@ -5213,7 +5215,6 @@ void tst_qquicktextinput::undo_keypressevents_data() << "ABC"; expectedString << "ABC"; - // for versions previous to 3.2 we overwrite needed two undo operations expectedString << "123"; QTest::newRow("Inserts,moving,selection and overwriting") << keys << expectedString; @@ -5230,6 +5231,106 @@ void tst_qquicktextinput::undo_keypressevents_data() expectedString << QString(); QTest::newRow("Insert,undo,redo") << keys << expectedString; + } { + KeyList keys; + QStringList expectedString; + + keys << "hello world" + << (Qt::Key_Backspace | Qt::ControlModifier) + << QKeySequence::Undo + << QKeySequence::Redo + << "hello"; + + expectedString + << "hello hello" + << "hello " + << "hello world" + << QString(); + + QTest::newRow("Insert,delete previous word,undo,redo,insert") << keys << expectedString; + } { + KeyList keys; + QStringList expectedString; + + keys << "hello world" + << QKeySequence::SelectPreviousWord + << (Qt::Key_Backspace) + << QKeySequence::Undo + << "hello"; + + expectedString + << "hello hello" + << "hello world" + << QString(); + + QTest::newRow("Insert,select previous word,remove,undo,insert") << keys << expectedString; + } { + KeyList keys; + QStringList expectedString; + + keys << "hello world" + << QKeySequence::DeleteStartOfWord + << QKeySequence::Undo + << "hello"; + + expectedString + << "hello worldhello" + << "hello world" + << QString(); + + QTest::newRow("Insert,delete previous word,undo,insert") << keys << expectedString; + } { + KeyList keys; + QStringList expectedString; + + keys << "hello world" + << QKeySequence::MoveToPreviousWord + << QKeySequence::DeleteEndOfWord + << QKeySequence::Undo + << "hello"; + + expectedString + << "hello helloworld" + << "hello world" + << QString(); + + QTest::newRow("Insert,move,delete next word,undo,insert") << keys << expectedString; + } + if (!QKeySequence(QKeySequence::DeleteEndOfLine).isEmpty()) { // X11 only. + KeyList keys; + QStringList expectedString; + + keys << "hello world" + << QKeySequence::MoveToStartOfLine + << Qt::Key_Right + << QKeySequence::DeleteEndOfLine + << QKeySequence::Undo + << "hello"; + + expectedString + << "hhelloello world" + << "hello world" + << QString(); + + QTest::newRow("Insert,move,delete end of line,undo,insert") << keys << expectedString; + } { + KeyList keys; + QStringList expectedString; + + keys << "hello world" + << QKeySequence::MoveToPreviousWord + << (Qt::Key_Left | Qt::ShiftModifier) + << (Qt::Key_Left | Qt::ShiftModifier) + << QKeySequence::DeleteEndOfWord + << QKeySequence::Undo + << "hello"; + + expectedString + << "hellhelloworld" + << "hello world" + << QString(); + + QTest::newRow("Insert,move,select,delete next word,undo,insert") << keys << expectedString; } } @@ -5260,6 +5361,43 @@ void tst_qquicktextinput::undo_keypressevents() QVERIFY(textInput->text().isEmpty()); } +void tst_qquicktextinput::backspaceSurrogatePairs() +{ + // Test backspace, and delete remove both characters in a surrogate pair. + static const quint16 textData[] = { 0xd800, 0xdf00, 0xd800, 0xdf01, 0xd800, 0xdf02, 0xd800, 0xdf03, 0xd800, 0xdf04 }; + const QString text = QString::fromUtf16(textData, lengthOf(textData)); + + QString componentStr = "import QtQuick 2.0\nTextInput { focus: true }"; + QQmlComponent textInputComponent(&engine); + textInputComponent.setData(componentStr.toLatin1(), QUrl()); + QQuickTextInput *textInput = qobject_cast(textInputComponent.create()); + QVERIFY(textInput != 0); + textInput->setText(text); + textInput->setCursorPosition(text.length()); + + QQuickWindow window; + textInput->setParentItem(window.contentItem()); + window.show(); + window.requestActivateWindow(); + QTest::qWaitForWindowShown(&window); + QTRY_COMPARE(QGuiApplication::focusWindow(), &window); + + for (int i = text.length(); i >= 0; i -= 2) { + QCOMPARE(textInput->text(), text.mid(0, i)); + QTest::keyClick(&window, Qt::Key_Backspace, Qt::NoModifier); + } + QCOMPARE(textInput->text(), QString()); + + textInput->setText(text); + textInput->setCursorPosition(0); + + for (int i = 0; i < text.length(); i += 2) { + QCOMPARE(textInput->text(), text.mid(i)); + QTest::keyClick(&window, Qt::Key_Delete, Qt::NoModifier); + } + QCOMPARE(textInput->text(), QString()); +} + void tst_qquicktextinput::QTBUG_19956() { QFETCH(QString, url); -- 2.7.4