Fix restoration of cursor position and selection after undo/redo.
authorAndrew den Exter <andrew.den-exter@nokia.com>
Fri, 6 Jul 2012 06:05:33 +0000 (16:05 +1000)
committerQt by Nokia <qt-info@nokia.com>
Tue, 24 Jul 2012 05:53:05 +0000 (07:53 +0200)
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 <yann.bodson@nokia.com>
src/quick/items/qquicktextinput.cpp
src/quick/items/qquicktextinput_p_p.h
tests/auto/quick/qquicktextinput/tst_qquicktextinput.cpp

index bb38e51..4dbd39e 100644 (file)
@@ -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
 
index 0cc0846..e147c17 100644 (file)
@@ -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,
index a4d1920..7cc9ddb 100644 (file)
@@ -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<Key> &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<QQuickTextInput*>(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);