Support visual cursor movement for BIDI text
authorJiang Jiang <jiang.jiang@nokia.com>
Wed, 15 Dec 2010 14:11:45 +0000 (15:11 +0100)
committerJiang Jiang <jiang.jiang@nokia.com>
Fri, 29 Apr 2011 09:02:23 +0000 (11:02 +0200)
Bidi input can in some contexts be more intuitive if the cursor
works in visual way: pressing left arrow key always make cursor
move one character to the left regardless the language of text,
pressing right arrow key always make cursor move to the right.
It is also the behavior of Mac OS X. Based on the above reason
and requests from Symbian we implemented this support for visual
movement in BIDI text. 3 public properties are added to
QTextDocument, QTextLayout and QLineEdit respectively:

- QTextDocument::defaultCursorMoveStyle can be used to control
  the cursor behavior in all widgets based on QTextDocument,
  like QTextEdit, QPlainTextEdit, etc. When set to QTextCursor::
  Visual, it will enable visual movement for all the cursors in
  the corresponding text edit. Default is QTextCursor::Logical.

- QTextLayout::cursorMoveStyle is used for low-level cursor
  manipulation. When set to Visual, it will enable visual movement
  behavior for all the cursor related methods, including cursorToX,
  xToCursor and drawCursor. Default is Logical.

- QLineEdit::cursorMoveStyle is used to control cursor movement
  behavior in QLineEdit. Default is Logical.:

Task-number: QTBUG-13859
Reviewed-by: Eskil
(cherry picked from commit c480dd641f5d22d1ee72cb27bf39e24c6df65658)

17 files changed:
src/gui/text/qtextcursor.cpp
src/gui/text/qtextcursor.h
src/gui/text/qtextdocument.cpp
src/gui/text/qtextdocument.h
src/gui/text/qtextdocument_p.cpp
src/gui/text/qtextdocument_p.h
src/gui/text/qtextengine.cpp
src/gui/text/qtextengine_p.h
src/gui/text/qtextlayout.cpp
src/gui/text/qtextlayout.h
src/gui/widgets/qlinecontrol.cpp
src/gui/widgets/qlinecontrol_p.h
src/gui/widgets/qlineedit.cpp
src/gui/widgets/qlineedit.h
tests/auto/qcomplextext/tst_qcomplextext.cpp
tests/auto/qlineedit/tst_qlineedit.cpp
tests/auto/qtextedit/tst_qtextedit.cpp

index 6ddfdb0..4f6857a 100644 (file)
@@ -362,20 +362,23 @@ bool QTextCursorPrivate::movePosition(QTextCursor::MoveOperation op, QTextCursor
     currentCharFormat = -1;
     bool adjustX = true;
     QTextBlock blockIt = block();
+    bool visualMovement = priv->defaultCursorMoveStyle == QTextCursor::Visual;
 
     if (!blockIt.isValid())
         return false;
 
-    if (op >= QTextCursor::Left && op <= QTextCursor::WordRight
-        && blockIt.textDirection() == Qt::RightToLeft) {
-        if (op == QTextCursor::Left)
-            op = QTextCursor::NextCharacter;
-        else if (op == QTextCursor::Right)
-            op = QTextCursor::PreviousCharacter;
-        else if (op == QTextCursor::WordLeft)
+    if (blockIt.textDirection() == Qt::RightToLeft) {
+        if (op == QTextCursor::WordLeft)
             op = QTextCursor::NextWord;
         else if (op == QTextCursor::WordRight)
             op = QTextCursor::PreviousWord;
+
+        if (!visualMovement) {
+            if (op == QTextCursor::Left)
+                op = QTextCursor::NextCharacter;
+            else if (op == QTextCursor::Right)
+                op = QTextCursor::PreviousCharacter;
+        }
     }
 
     const QTextLayout *layout = blockLayout(blockIt);
@@ -418,9 +421,12 @@ bool QTextCursorPrivate::movePosition(QTextCursor::MoveOperation op, QTextCursor
         break;
     }
     case QTextCursor::PreviousCharacter:
-    case QTextCursor::Left:
         newPosition = priv->previousCursorPosition(position, QTextLayout::SkipCharacters);
         break;
+    case QTextCursor::Left:
+        newPosition = visualMovement ? priv->leftCursorPosition(position)
+                                     : priv->previousCursorPosition(position, QTextLayout::SkipCharacters);
+        break;
     case QTextCursor::StartOfWord: {
         if (relativePos == 0)
             break;
@@ -529,9 +535,12 @@ bool QTextCursorPrivate::movePosition(QTextCursor::MoveOperation op, QTextCursor
         break;
     }
     case QTextCursor::NextCharacter:
-    case QTextCursor::Right:
         newPosition = priv->nextCursorPosition(position, QTextLayout::SkipCharacters);
         break;
+    case QTextCursor::Right:
+        newPosition = visualMovement ? priv->rightCursorPosition(position)
+                                     : priv->nextCursorPosition(position, QTextLayout::SkipCharacters);
+        break;
     case QTextCursor::NextWord:
     case QTextCursor::WordRight:
         newPosition = priv->nextCursorPosition(position, QTextLayout::SkipWords);
@@ -2558,4 +2567,19 @@ QTextDocument *QTextCursor::document() const
     return 0; // document went away
 }
 
+/*!
+    \enum QTextCursor::MoveStyle
+
+    This enum describes the movement style available to QTextCursor. The options
+    are:
+
+    \value Logical Within a left-to-right text block, increase cursor position
+    when pressing left arrow key, decrease cursor position when pressing the
+    right arrow key. If the text block is right-to-left, the opposite behavior
+    applies.
+    \value Visual Pressing the left arrow key will always cause the cursor to move
+    left, regardless of the text's writing direction. The same behavior applies to
+    right arrow key.
+*/
+
 QT_END_NAMESPACE
index 4eaeb41..30f8272 100644 (file)
@@ -86,6 +86,10 @@ public:
         MoveAnchor,
         KeepAnchor
     };
+    enum MoveStyle {
+        Logical,
+        Visual,
+    };
 
     void setPosition(int pos, MoveMode mode = MoveAnchor);
     int position() const;
index 6dbd755..36f3c6c 100644 (file)
@@ -586,6 +586,29 @@ void QTextDocument::setDefaultTextOption(const QTextOption &option)
 }
 
 /*!
+    \since 4.8
+
+    The default cursor movement style is used by all QTextCursor objects
+    created from the document. The default is QTextCursor::Logical.
+*/
+QTextCursor::MoveStyle QTextDocument::defaultCursorMoveStyle() const
+{
+    Q_D(const QTextDocument);
+    return d->defaultCursorMoveStyle;
+}
+
+/*!
+    \since 4.8
+
+    Set the default cursor movement style.
+*/
+void QTextDocument::setDefaultCursorMoveStyle(QTextCursor::MoveStyle style)
+{
+    Q_D(QTextDocument);
+    d->defaultCursorMoveStyle = style;
+}
+
+/*!
     \fn void QTextDocument::markContentsDirty(int position, int length)
 
     Marks the contents specified by the given \a position and \a length
index f87ccc9..e515b36 100644 (file)
@@ -46,6 +46,7 @@
 #include <QtCore/qsize.h>
 #include <QtCore/qrect.h>
 #include <QtGui/qfont.h>
+#include <QtGui/qtextcursor.h>
 
 QT_BEGIN_HEADER
 
@@ -60,7 +61,6 @@ class QPainter;
 class QPrinter;
 class QAbstractTextDocumentLayout;
 class QPoint;
-class QTextCursor;
 class QTextObject;
 class QTextFormat;
 class QTextFrame;
@@ -269,6 +269,9 @@ public:
     QTextOption defaultTextOption() const;
     void setDefaultTextOption(const QTextOption &option);
 
+    QTextCursor::MoveStyle defaultCursorMoveStyle() const;
+    void setDefaultCursorMoveStyle(QTextCursor::MoveStyle style);
+
 Q_SIGNALS:
     void contentsChange(int from, int charsRemoves, int charsAdded);
     void contentsChanged();
index a997720..779b1ff 100644 (file)
@@ -209,6 +209,7 @@ QTextDocumentPrivate::QTextDocumentPrivate()
 
     defaultTextOption.setTabStop(80); // same as in qtextengine.cpp
     defaultTextOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
+    defaultCursorMoveStyle = QTextCursor::Logical;
 
     indentWidth = 40;
     documentMargin = 4;
@@ -1382,6 +1383,20 @@ int QTextDocumentPrivate::previousCursorPosition(int position, QTextLayout::Curs
     return it.layout()->previousCursorPosition(position-start, mode) + start;
 }
 
+int QTextDocumentPrivate::leftCursorPosition(int position) const
+{
+    QTextBlock it = blocksFind(position);
+    int start = it.position();
+    return it.layout()->leftCursorPosition(position-start) + start;
+}
+
+int QTextDocumentPrivate::rightCursorPosition(int position) const
+{
+    QTextBlock it = blocksFind(position);
+    int start = it.position();
+    return it.layout()->rightCursorPosition(position-start) + start;
+}
+
 void QTextDocumentPrivate::changeObjectFormat(QTextObject *obj, int format)
 {
     beginEditBlock();
index b464f2e..6563920 100644 (file)
@@ -64,6 +64,7 @@
 #include "private/qtextformat_p.h"
 #include "QtGui/qtextdocument.h"
 #include "QtGui/qtextobject.h"
+#include "QtGui/qtextcursor.h"
 #include "QtCore/qmap.h"
 #include "QtCore/qvariant.h"
 #include "QtCore/qurl.h"
@@ -244,6 +245,8 @@ public:
 
     int nextCursorPosition(int position, QTextLayout::CursorMode mode) const;
     int previousCursorPosition(int position, QTextLayout::CursorMode mode) const;
+    int leftCursorPosition(int position) const;
+    int rightCursorPosition(int position) const;
 
     void changeObjectFormat(QTextObject *group, int format);
 
@@ -339,6 +342,7 @@ private:
 
 public:
     QTextOption defaultTextOption;
+    QTextCursor::MoveStyle defaultCursorMoveStyle;
 #ifndef QT_NO_CSSPARSER
     QCss::StyleSheet parsedDefaultStyleSheet;
 #endif
index 08d0eca..0e649f0 100644 (file)
@@ -1304,6 +1304,7 @@ static void init(QTextEngine *e)
     e->ignoreBidi = false;
     e->cacheGlyphs = false;
     e->forceJustification = false;
+    e->visualMovement = false;
 
     e->layoutData = 0;
 
@@ -2737,6 +2738,180 @@ QFixed QTextEngine::leadingSpaceWidth(const QScriptLine &line)
     return width(line.from + pos, line.length - pos);
 }
 
+QFixed QTextEngine::alignLine(const QScriptLine &line)
+{
+    QFixed x = 0;
+    justify(line);
+    // if width is QFIXED_MAX that means we used setNumColumns() and that implicitly makes this line left aligned.
+    if (!line.justified && line.width != QFIXED_MAX) {
+        int align = option.alignment();
+        if (align & Qt::AlignJustify && isRightToLeft())
+            align = Qt::AlignRight;
+        if (align & Qt::AlignRight)
+            x = line.width - (line.textAdvance + leadingSpaceWidth(line));
+        else if (align & Qt::AlignHCenter)
+            x = (line.width - line.textAdvance)/2;
+    }
+    return x;
+}
+
+QFixed QTextEngine::offsetInLigature(const QScriptItem *si, int pos, int max, int glyph_pos)
+{
+    unsigned short *logClusters = this->logClusters(si);
+    const QGlyphLayout &glyphs = shapedGlyphs(si);
+
+    int offsetInCluster = 0;
+    for (int i = pos - 1; i >= 0; i--) {
+        if (logClusters[i] == glyph_pos)
+            offsetInCluster++;
+        else
+            break;
+    }
+
+    // in the case that the offset is inside a (multi-character) glyph,
+    // interpolate the position.
+    if (offsetInCluster > 0) {
+        int clusterLength = 0;
+        for (int i = pos - offsetInCluster; i < max; i++) {
+            if (logClusters[i] == glyph_pos)
+                clusterLength++;
+            else
+                break;
+        }
+        if (clusterLength)
+            return glyphs.advances_x[glyph_pos] * offsetInCluster / clusterLength;
+    }
+
+    return 0;
+}
+
+int QTextEngine::previousLogicalPosition(int oldPos) const
+{
+    const HB_CharAttributes *attrs = attributes();
+    if (!attrs || oldPos < 0)
+        return oldPos;
+
+    if (oldPos <= 0)
+        return 0;
+    oldPos--;
+    while (oldPos && !attrs[oldPos].charStop)
+        oldPos--;
+    return oldPos;
+}
+
+int QTextEngine::nextLogicalPosition(int oldPos) const
+{
+    const HB_CharAttributes *attrs = attributes();
+    int len = block.isValid() ? block.length() - 1
+                              : layoutData->string.length();
+    Q_ASSERT(len <= layoutData->string.length());
+    if (!attrs || oldPos < 0 || oldPos >= len)
+        return oldPos;
+
+    oldPos++;
+    while (oldPos < len && !attrs[oldPos].charStop)
+        oldPos++;
+    return oldPos;
+}
+
+int QTextEngine::lineNumberForTextPosition(int pos)
+{
+    if (!layoutData)
+        itemize();
+    if (pos == layoutData->string.length() && lines.size())
+        return lines.size() - 1;
+    for (int i = 0; i < lines.size(); ++i) {
+        const QScriptLine& line = lines[i];
+        if (line.from + line.length > pos)
+            return i;
+    }
+    return -1;
+}
+
+void QTextEngine::insertionPointsForLine(int lineNum, QVector<int> &insertionPoints)
+{
+    QTextLineItemIterator iterator(this, lineNum);
+    bool rtl = isRightToLeft();
+    bool lastLine = lineNum >= lines.size() - 1;
+
+    while (!iterator.atEnd()) {
+        iterator.next();
+        const QScriptItem *si = &layoutData->items[iterator.item];
+        if (si->analysis.bidiLevel % 2) {
+            int i = iterator.itemEnd - 1, min = iterator.itemStart;
+            if (lastLine && (rtl ? iterator.atBeginning() : iterator.atEnd()))
+                i++;
+            for (; i >= min; i--)
+                insertionPoints.push_back(i);
+        } else {
+            int i = iterator.itemStart, max = iterator.itemEnd;
+            if (lastLine && (rtl ? iterator.atBeginning() : iterator.atEnd()))
+                max++;
+            for (; i < max; i++)
+                insertionPoints.push_back(i);
+        }
+    }
+}
+
+int QTextEngine::endOfLine(int lineNum)
+{
+    QVector<int> insertionPoints;
+    insertionPointsForLine(lineNum, insertionPoints);
+
+    if (insertionPoints.size() > 0)
+        return insertionPoints.last();
+    return 0;
+}
+
+int QTextEngine::beginningOfLine(int lineNum)
+{
+    QVector<int> insertionPoints;
+    insertionPointsForLine(lineNum, insertionPoints);
+
+    if (insertionPoints.size() > 0)
+        return insertionPoints.first();
+    return 0;
+}
+
+int QTextEngine::positionAfterVisualMovement(int pos, QTextCursor::MoveOperation op)
+{
+    if (!layoutData)
+        itemize();
+
+    bool moveRight = (op == QTextCursor::Right);
+    bool alignRight = isRightToLeft();
+    if (!layoutData->hasBidi)
+        return moveRight ^ alignRight ? nextLogicalPosition(pos) : previousLogicalPosition(pos);
+
+    int lineNum = lineNumberForTextPosition(pos);
+    Q_ASSERT(lineNum >= 0);
+
+    QVector<int> insertionPoints;
+    insertionPointsForLine(lineNum, insertionPoints);
+    int i, max = insertionPoints.size();
+    for (i = 0; i < max; i++)
+        if (pos == insertionPoints[i]) {
+            if (moveRight) {
+                if (i + 1 < max)
+                    return insertionPoints[i + 1];
+            } else {
+                if (i > 0)
+                    return insertionPoints[i - 1];
+            }
+
+            if (moveRight ^ alignRight) {
+                if (lineNum + 1 < lines.size())
+                    return alignRight ? endOfLine(lineNum + 1) : beginningOfLine(lineNum + 1);
+            }
+            else {
+                if (lineNum > 0)
+                    return alignRight ? beginningOfLine(lineNum - 1) : endOfLine(lineNum - 1);
+            }
+        }
+
+    return pos;
+}
+
 QStackTextEngine::QStackTextEngine(const QString &string, const QFont &f)
     : QTextEngine(string, f),
       _layoutData(string, _memory, MemSize)
@@ -2841,5 +3016,127 @@ glyph_metrics_t glyph_metrics_t::transformed(const QTransform &matrix) const
     return m;
 }
 
+QTextLineItemIterator::QTextLineItemIterator(QTextEngine *_eng, int _lineNum, const QPointF &pos,
+                                             const QTextLayout::FormatRange *_selection)
+    : eng(_eng),
+      line(eng->lines[_lineNum]),
+      si(0),
+      lineNum(_lineNum),
+      lineEnd(line.from + line.length),
+      firstItem(eng->findItem(line.from)),
+      lastItem(eng->findItem(lineEnd - 1)),
+      nItems((firstItem >= 0 && lastItem >= firstItem)? (lastItem-firstItem+1) : 0),
+      logicalItem(-1),
+      item(-1),
+      visualOrder(nItems),
+      levels(nItems),
+      selection(_selection)
+{
+    pos_x = x = QFixed::fromReal(pos.x());
+
+    x += line.x;
+
+    x += eng->alignLine(line);
+
+    for (int i = 0; i < nItems; ++i)
+        levels[i] = eng->layoutData->items[i+firstItem].analysis.bidiLevel;
+    QTextEngine::bidiReorder(nItems, levels.data(), visualOrder.data());
+
+    eng->shapeLine(line);
+}
+
+QScriptItem &QTextLineItemIterator::next()
+{
+    x += itemWidth;
+
+    ++logicalItem;
+    item = visualOrder[logicalItem] + firstItem;
+    itemLength = eng->length(item);
+    si = &eng->layoutData->items[item];
+    if (!si->num_glyphs)
+        eng->shape(item);
+
+    if (si->analysis.flags >= QScriptAnalysis::TabOrObject) {
+        itemWidth = si->width;
+        return *si;
+    }
+
+    unsigned short *logClusters = eng->logClusters(si);
+    QGlyphLayout glyphs = eng->shapedGlyphs(si);
+
+    itemStart = qMax(line.from, si->position);
+    glyphsStart = logClusters[itemStart - si->position];
+    if (lineEnd < si->position + itemLength) {
+        itemEnd = lineEnd;
+        glyphsEnd = logClusters[itemEnd-si->position];
+    } else {
+        itemEnd = si->position + itemLength;
+        glyphsEnd = si->num_glyphs;
+    }
+    // show soft-hyphen at line-break
+    if (si->position + itemLength >= lineEnd
+        && eng->layoutData->string.at(lineEnd - 1) == 0x00ad)
+        glyphs.attributes[glyphsEnd - 1].dontPrint = false;
+
+    itemWidth = 0;
+    for (int g = glyphsStart; g < glyphsEnd; ++g)
+        itemWidth += glyphs.effectiveAdvance(g);
+
+    return *si;
+}
+
+bool QTextLineItemIterator::getSelectionBounds(QFixed *selectionX, QFixed *selectionWidth) const
+{
+    *selectionX = *selectionWidth = 0;
+
+    if (!selection)
+        return false;
+
+    if (si->analysis.flags >= QScriptAnalysis::TabOrObject) {
+        if (si->position >= selection->start + selection->length
+            || si->position + itemLength <= selection->start)
+            return false;
+
+        *selectionX = x;
+        *selectionWidth = itemWidth;
+    } else {
+        unsigned short *logClusters = eng->logClusters(si);
+        QGlyphLayout glyphs = eng->shapedGlyphs(si);
+
+        int from = qMax(itemStart, selection->start) - si->position;
+        int to = qMin(itemEnd, selection->start + selection->length) - si->position;
+        if (from >= to)
+            return false;
+
+        int start_glyph = logClusters[from];
+        int end_glyph = (to == eng->length(item)) ? si->num_glyphs : logClusters[to];
+        QFixed soff;
+        QFixed swidth;
+        if (si->analysis.bidiLevel %2) {
+            for (int g = glyphsEnd - 1; g >= end_glyph; --g)
+                soff += glyphs.effectiveAdvance(g);
+            for (int g = end_glyph - 1; g >= start_glyph; --g)
+                swidth += glyphs.effectiveAdvance(g);
+        } else {
+            for (int g = glyphsStart; g < start_glyph; ++g)
+                soff += glyphs.effectiveAdvance(g);
+            for (int g = start_glyph; g < end_glyph; ++g)
+                swidth += glyphs.effectiveAdvance(g);
+        }
+
+        // If the starting character is in the middle of a ligature,
+        // selection should only contain the right part of that ligature
+        // glyph, so we need to get the width of the left part here and
+        // add it to *selectionX
+        QFixed leftOffsetInLigature = eng->offsetInLigature(si, from, to, start_glyph);
+        *selectionX = x + soff + leftOffsetInLigature;
+        *selectionWidth = swidth - leftOffsetInLigature;
+        // If the ending character is also part of a ligature, swidth does
+        // not contain that part yet, we also need to find out the width of
+        // that left part
+        *selectionWidth += eng->offsetInLigature(si, to, eng->length(item), end_glyph);
+    }
+    return true;
+}
 
 QT_END_NAMESPACE
index 366c5c3..c476485 100644 (file)
@@ -64,6 +64,7 @@
 #include "QtGui/qpaintengine.h"
 #include "QtGui/qtextobject.h"
 #include "QtGui/qtextoption.h"
+#include "QtGui/qtextcursor.h"
 #include "QtCore/qset.h"
 #include "QtCore/qdebug.h"
 #ifndef QT_BUILD_COMPAT_LIB
@@ -471,6 +472,7 @@ public:
     void shape(int item) const;
 
     void justify(const QScriptLine &si);
+    QFixed alignLine(const QScriptLine &line);
 
     QFixed width(int charFrom, int numChars) const;
     glyph_metrics_t boundingBox(int from,  int len) const;
@@ -586,12 +588,18 @@ public:
     uint cacheGlyphs : 1;
     uint stackEngine : 1;
     uint forceJustification : 1;
+    uint visualMovement : 1;
 
     int *underlinePositions;
 
     mutable LayoutData *layoutData;
 
     inline bool hasFormats() const { return (block.docHandle() || specialData); }
+    inline bool visualCursorMovement() const
+    {
+        return (visualMovement ||
+                (block.docHandle() ? block.docHandle()->defaultCursorMoveStyle == QTextCursor::Visual : false));
+    }
 
     struct SpecialData {
         int preeditPosition;
@@ -611,6 +619,13 @@ public:
     void shapeLine(const QScriptLine &line);
     QFixed leadingSpaceWidth(const QScriptLine &line);
 
+    QFixed offsetInLigature(const QScriptItem *si, int pos, int max, int glyph_pos);
+    int previousLogicalPosition(int oldPos) const;
+    int nextLogicalPosition(int oldPos) const;
+    int lineNumberForTextPosition(int pos);
+    int positionAfterVisualMovement(int oldPos, QTextCursor::MoveOperation op);
+    void insertionPointsForLine(int lineNum, QVector<int> &insertionPoints);
+
 private:
     void setBoundary(int strPos) const;
     void addRequiredBoundaries() const;
@@ -625,6 +640,8 @@ private:
     void splitItem(int item, int pos) const;
 
     void resolveAdditionalFormats() const;
+    int endOfLine(int lineNum);
+    int beginningOfLine(int lineNum);
 };
 
 class QStackTextEngine : public QTextEngine {
@@ -635,6 +652,49 @@ public:
     void *_memory[MemSize];
 };
 
+struct QTextLineItemIterator
+{
+    QTextLineItemIterator(QTextEngine *eng, int lineNum, const QPointF &pos = QPointF(),
+                          const QTextLayout::FormatRange *_selection = 0);
+
+    inline bool atEnd() const { return logicalItem >= nItems - 1; }
+    inline bool atBeginning() const { return logicalItem <= 0; }
+    QScriptItem &next();
+
+    bool getSelectionBounds(QFixed *selectionX, QFixed *selectionWidth) const;
+    inline bool isOutsideSelection() const {
+        QFixed tmp1, tmp2;
+        return !getSelectionBounds(&tmp1, &tmp2);
+    }
+
+    QTextEngine *eng;
+
+    QFixed x;
+    QFixed pos_x;
+    const QScriptLine &line;
+    QScriptItem *si;
+
+    int lineNum;
+    int lineEnd;
+    int firstItem;
+    int lastItem;
+    int nItems;
+    int logicalItem;
+    int item;
+    int itemLength;
+
+    int glyphsStart;
+    int glyphsEnd;
+    int itemStart;
+    int itemEnd;
+
+    QFixed itemWidth;
+
+    QVarLengthArray<int> visualOrder;
+    QVarLengthArray<uchar> levels;
+
+    const QTextLayout::FormatRange *selection;
+};
 
 Q_DECLARE_OPERATORS_FOR_FLAGS(QTextEngine::ShaperFlags)
 
index fe0c53a..9fc4e7f 100644 (file)
@@ -72,23 +72,6 @@ QT_BEGIN_NAMESPACE
 #define SuppressText 0x5012
 #define SuppressBackground 0x513
 
-static QFixed alignLine(QTextEngine *eng, const QScriptLine &line)
-{
-    QFixed x = 0;
-    eng->justify(line);
-    // if width is QFIXED_MAX that means we used setNumColumns() and that implicitly makes this line left aligned.
-    if (!line.justified && line.width != QFIXED_MAX) {
-        int align = eng->option.alignment();
-        if (align & Qt::AlignJustify && eng->isRightToLeft())
-            align = Qt::AlignRight;
-        if (align & Qt::AlignRight)
-            x = line.width - (line.textAdvance + eng->leadingSpaceWidth(line));
-        else if (align & Qt::AlignHCenter)
-            x = (line.width - line.textAdvance)/2;
-    }
-    return x;
-}
-
 /*!
     \class QTextLayout::FormatRange
     \reentrant
@@ -596,6 +579,30 @@ bool QTextLayout::cacheEnabled() const
 }
 
 /*!
+    Set the visual cursor movement style. If the QTextLayout is backed by
+    a document, you can ignore this and use the option in QTextDocument,
+    this option is for widgets like QLineEdit or custom widgets without
+    a QTextDocument. Default value is QTextCursor::Logical.
+
+    \sa setCursorMoveStyle()
+*/
+void QTextLayout::setCursorMoveStyle(QTextCursor::MoveStyle style)
+{
+    d->visualMovement = style == QTextCursor::Visual ? true : false;
+}
+
+/*!
+    The cursor movement style of this QTextLayout. The default is
+    QTextCursor::Logical.
+
+    \sa setCursorMoveStyle()
+*/
+QTextCursor::MoveStyle QTextLayout::cursorMoveStyle() const
+{
+    return d->visualMovement ? QTextCursor::Visual : QTextCursor::Logical;
+}
+
+/*!
     Begins the layout process.
 
     \sa endLayout()
@@ -718,6 +725,34 @@ int QTextLayout::previousCursorPosition(int oldPos, CursorMode mode) const
 }
 
 /*!
+    Returns the cursor position to the right of \a oldPos, next to it.
+    It's dependent on the visual position of characters, after bi-directional
+    reordering.
+
+    \sa leftCursorPosition(), nextCursorPosition()
+*/
+int QTextLayout::rightCursorPosition(int oldPos) const
+{
+    int newPos = d->positionAfterVisualMovement(oldPos, QTextCursor::Right);
+//    qDebug("%d -> %d", oldPos, newPos);
+    return newPos;
+}
+
+/*!
+    Returns the cursor position to the left of \a oldPos, next to it.
+    It's dependent on the visual position of characters, after bi-directional
+    reordering.
+
+    \sa rightCursorPosition(), previousCursorPosition()
+*/
+int QTextLayout::leftCursorPosition(int oldPos) const
+{
+    int newPos = d->positionAfterVisualMovement(oldPos, QTextCursor::Left);
+//    qDebug("%d -> %d", oldPos, newPos);
+    return newPos;
+}
+
+/*!/
     Returns true if position \a pos is a valid cursor position.
 
     In a Unicode context some positions in the text are not valid
@@ -815,16 +850,8 @@ QTextLine QTextLayout::lineAt(int i) const
 */
 QTextLine QTextLayout::lineForTextPosition(int pos) const
 {
-    for (int i = 0; i < d->lines.size(); ++i) {
-        const QScriptLine& line = d->lines[i];
-        if (line.from + (int)line.length > pos)
-            return QTextLine(i, d);
-    }
-    if (!d->layoutData)
-        d->itemize();
-    if (pos == d->layoutData->string.length() && d->lines.size())
-        return QTextLine(d->lines.size()-1, d);
-    return QTextLine();
+    int lineNum = d->lineNumberForTextPosition(pos);
+    return lineNum >= 0 ? lineAt(lineNum) : QTextLine();
 }
 
 /*!
@@ -919,201 +946,6 @@ void QTextLayout::setFlags(int flags)
     }
 }
 
-struct QTextLineItemIterator
-{
-    QTextLineItemIterator(QTextEngine *eng, int lineNum, const QPointF &pos = QPointF(),
-                          const QTextLayout::FormatRange *_selection = 0);
-
-    inline bool atEnd() const { return logicalItem >= nItems - 1; }
-    QScriptItem &next();
-
-    bool getSelectionBounds(QFixed *selectionX, QFixed *selectionWidth) const;
-    inline bool isOutsideSelection() const {
-        QFixed tmp1, tmp2;
-        return !getSelectionBounds(&tmp1, &tmp2);
-    }
-
-    QTextEngine *eng;
-
-    QFixed x;
-    QFixed pos_x;
-    const QScriptLine &line;
-    QScriptItem *si;
-
-    int lineEnd;
-    int firstItem;
-    int lastItem;
-    int nItems;
-    int logicalItem;
-    int item;
-    int itemLength;
-
-    int glyphsStart;
-    int glyphsEnd;
-    int itemStart;
-    int itemEnd;
-
-    QFixed itemWidth;
-
-    QVarLengthArray<int> visualOrder;
-    QVarLengthArray<uchar> levels;
-
-    const QTextLayout::FormatRange *selection;
-};
-
-QTextLineItemIterator::QTextLineItemIterator(QTextEngine *_eng, int lineNum, const QPointF &pos,
-                                             const QTextLayout::FormatRange *_selection)
-    : eng(_eng),
-      line(eng->lines[lineNum]),
-      si(0),
-      lineEnd(line.from + line.length),
-      firstItem(eng->findItem(line.from)),
-      lastItem(eng->findItem(lineEnd - 1)),
-      nItems((firstItem >= 0 && lastItem >= firstItem)? (lastItem-firstItem+1) : 0),
-      logicalItem(-1),
-      item(-1),
-      visualOrder(nItems),
-      levels(nItems),
-      selection(_selection)
-{
-    pos_x = x = QFixed::fromReal(pos.x());
-
-    x += line.x;
-
-    x += alignLine(eng, line);
-
-    for (int i = 0; i < nItems; ++i)
-        levels[i] = eng->layoutData->items[i+firstItem].analysis.bidiLevel;
-    QTextEngine::bidiReorder(nItems, levels.data(), visualOrder.data());
-
-    eng->shapeLine(line);
-}
-
-QScriptItem &QTextLineItemIterator::next()
-{
-    x += itemWidth;
-
-    ++logicalItem;
-    item = visualOrder[logicalItem] + firstItem;
-    itemLength = eng->length(item);
-    si = &eng->layoutData->items[item];
-    if (!si->num_glyphs)
-        eng->shape(item);
-
-    if (si->analysis.flags >= QScriptAnalysis::TabOrObject) {
-        itemWidth = si->width;
-        return *si;
-    }
-
-    unsigned short *logClusters = eng->logClusters(si);
-    QGlyphLayout glyphs = eng->shapedGlyphs(si);
-
-    itemStart = qMax(line.from, si->position);
-    glyphsStart = logClusters[itemStart - si->position];
-    if (lineEnd < si->position + itemLength) {
-        itemEnd = lineEnd;
-        glyphsEnd = logClusters[itemEnd-si->position];
-    } else {
-        itemEnd = si->position + itemLength;
-        glyphsEnd = si->num_glyphs;
-    }
-    // show soft-hyphen at line-break
-    if (si->position + itemLength >= lineEnd
-        && eng->layoutData->string.at(lineEnd - 1) == 0x00ad)
-        glyphs.attributes[glyphsEnd - 1].dontPrint = false;
-
-    itemWidth = 0;
-    for (int g = glyphsStart; g < glyphsEnd; ++g)
-        itemWidth += glyphs.effectiveAdvance(g);
-
-    return *si;
-}
-
-static QFixed offsetInLigature(const unsigned short *logClusters,
-                               const QGlyphLayout &glyphs,
-                               int pos, int max, int glyph_pos)
-{
-    int offsetInCluster = 0;
-    for (int i = pos - 1; i >= 0; i--) {
-        if (logClusters[i] == glyph_pos)
-            offsetInCluster++;
-        else
-            break;
-    }
-
-    // in the case that the offset is inside a (multi-character) glyph,
-    // interpolate the position.
-    if (offsetInCluster > 0) {
-        int clusterLength = 0;
-        for (int i = pos - offsetInCluster; i < max; i++) {
-            if (logClusters[i] == glyph_pos)
-                clusterLength++;
-            else
-                break;
-        }
-        if (clusterLength)
-            return glyphs.advances_x[glyph_pos] * offsetInCluster / clusterLength;
-    }
-
-    return 0;
-}
-
-bool QTextLineItemIterator::getSelectionBounds(QFixed *selectionX, QFixed *selectionWidth) const
-{
-    *selectionX = *selectionWidth = 0;
-
-    if (!selection)
-        return false;
-
-    if (si->analysis.flags >= QScriptAnalysis::TabOrObject) {
-        if (si->position >= selection->start + selection->length
-            || si->position + itemLength <= selection->start)
-            return false;
-
-        *selectionX = x;
-        *selectionWidth = itemWidth;
-    } else {
-        unsigned short *logClusters = eng->logClusters(si);
-        QGlyphLayout glyphs = eng->shapedGlyphs(si);
-
-        int from = qMax(itemStart, selection->start) - si->position;
-        int to = qMin(itemEnd, selection->start + selection->length) - si->position;
-        if (from >= to)
-            return false;
-
-        int start_glyph = logClusters[from];
-        int end_glyph = (to == eng->length(item)) ? si->num_glyphs : logClusters[to];
-        QFixed soff;
-        QFixed swidth;
-        if (si->analysis.bidiLevel %2) {
-            for (int g = glyphsEnd - 1; g >= end_glyph; --g)
-                soff += glyphs.effectiveAdvance(g);
-            for (int g = end_glyph - 1; g >= start_glyph; --g)
-                swidth += glyphs.effectiveAdvance(g);
-        } else {
-            for (int g = glyphsStart; g < start_glyph; ++g)
-                soff += glyphs.effectiveAdvance(g);
-            for (int g = start_glyph; g < end_glyph; ++g)
-                swidth += glyphs.effectiveAdvance(g);
-        }
-
-        // If the starting character is in the middle of a ligature,
-        // selection should only contain the right part of that ligature
-        // glyph, so we need to get the width of the left part here and
-        // add it to *selectionX
-        QFixed leftOffsetInLigature = offsetInLigature(logClusters, glyphs, from,
-                                                       to, start_glyph);
-        *selectionX = x + soff + leftOffsetInLigature;
-        *selectionWidth = swidth - leftOffsetInLigature;
-        // If the ending character is also part of a ligature, swidth does
-        // not contain that part yet, we also need to find out the width of
-        // that left part
-        *selectionWidth += offsetInLigature(logClusters, glyphs, to,
-                                            eng->length(item), end_glyph);
-    }
-    return true;
-}
-
 static void addSelectedRegionsToPath(QTextEngine *eng, int lineNumber, const QPointF &pos, QTextLayout::FormatRange *selection,
                                      QPainterPath *region, QRectF boundingRect)
 {
@@ -1382,18 +1214,9 @@ void QTextLayout::drawCursor(QPainter *p, const QPointF &pos, int cursorPosition
     QFixed pos_y = QFixed::fromReal(position.y());
 
     cursorPosition = qBound(0, cursorPosition, d->layoutData->string.length());
-    int line = 0;
-    if (cursorPosition == d->layoutData->string.length()) {
-        line = d->lines.size() - 1;
-    } else {
-        // ### binary search
-        for (line = 0; line < d->lines.size(); line++) {
-            const QScriptLine &sl = d->lines[line];
-            if (sl.from <= cursorPosition && sl.from + (int)sl.length > cursorPosition)
-                break;
-        }
-    }
-
+    int line = d->lineNumberForTextPosition(cursorPosition);
+    if (line < 0)
+        line = 0;
     if (line >= d->lines.size())
         return;
 
@@ -1402,7 +1225,15 @@ void QTextLayout::drawCursor(QPainter *p, const QPointF &pos, int cursorPosition
 
     qreal x = position.x() + l.cursorToX(cursorPosition);
 
-    int itm = d->findItem(cursorPosition - 1);
+    int itm;
+
+    if (d->visualCursorMovement()) {
+        if (cursorPosition == sl.from + sl.length)
+            cursorPosition--;
+        itm = d->findItem(cursorPosition);
+    } else
+        itm = d->findItem(cursorPosition - 1);
+
     QFixed base = sl.base();
     QFixed descent = sl.descent;
     bool rightToLeft = d->isRightToLeft();
@@ -1512,7 +1343,7 @@ QRectF QTextLine::rect() const
 QRectF QTextLine::naturalTextRect() const
 {
     const QScriptLine& sl = eng->lines[i];
-    QFixed x = sl.x + alignLine(eng, sl);
+    QFixed x = sl.x + eng->alignLine(sl);
 
     QFixed width = sl.textWidth;
     if (sl.justified)
@@ -2635,9 +2466,10 @@ qreal QTextLine::cursorToX(int *cursorPos, Edge edge) const
         eng->itemize();
 
     const QScriptLine &line = eng->lines[i];
+    bool lastLine = i >= eng->lines.size() - 1;
 
     QFixed x = line.x;
-    x += alignLine(eng, line);
+    x += eng->alignLine(line);
 
     if (!i && !eng->layoutData->items.size()) {
         *cursorPos = 0;
@@ -2723,21 +2555,29 @@ qreal QTextLine::cursorToX(int *cursorPos, Edge edge) const
     logClusters = eng->logClusters(si);
     glyphs = eng->shapedGlyphs(si);
     if (si->analysis.flags >= QScriptAnalysis::TabOrObject) {
-        if(pos == l)
+        if (pos == (reverse ? 0 : l))
             x += si->width;
     } else {
+        bool rtl = eng->isRightToLeft();
+        bool visual = eng->visualCursorMovement();
         if (reverse) {
             int end = qMin(lineEnd, si->position + l) - si->position;
             int glyph_end = end == l ? si->num_glyphs : logClusters[end];
-            for (int i = glyph_end - 1; i >= glyph_pos; i--)
+            int glyph_start = glyph_pos;
+            if (visual && !rtl && !(lastLine && itm == (visualOrder[nItems - 1] + firstItem)))
+                glyph_start++;
+            for (int i = glyph_end - 1; i >= glyph_start; i--)
                 x += glyphs.effectiveAdvance(i);
         } else {
             int start = qMax(line.from - si->position, 0);
             int glyph_start = logClusters[start];
-            for (int i = glyph_start; i < glyph_pos; i++)
+            int glyph_end = glyph_pos;
+            if (!visual || !rtl || (lastLine && itm == visualOrder[0] + firstItem))
+                glyph_end--;
+            for (int i = glyph_start; i <= glyph_end; i++)
                 x += glyphs.effectiveAdvance(i);
         }
-        x += offsetInLigature(logClusters, glyphs, pos, line.length, glyph_pos);
+        x += eng->offsetInLigature(si, pos, line.length, glyph_pos);
     }
 
     *cursorPos = pos + si->position;
@@ -2756,6 +2596,8 @@ int QTextLine::xToCursor(qreal _x, CursorPosition cpos) const
 {
     QFixed x = QFixed::fromReal(_x);
     const QScriptLine &line = eng->lines[i];
+    bool lastLine = i >= eng->lines.size() - 1;
+    int lineNum = i;
 
     if (!eng->layoutData)
         eng->itemize();
@@ -2773,7 +2615,7 @@ int QTextLine::xToCursor(qreal _x, CursorPosition cpos) const
         return 0;
 
     x -= line.x;
-    x -= alignLine(eng, line);
+    x -= eng->alignLine(line);
 //     qDebug("xToCursor: x=%f, cpos=%d", x.toReal(), cpos);
 
     QVarLengthArray<int> visualOrder(nItems);
@@ -2782,6 +2624,7 @@ int QTextLine::xToCursor(qreal _x, CursorPosition cpos) const
         levels[i] = eng->layoutData->items[i+firstItem].analysis.bidiLevel;
     QTextEngine::bidiReorder(nItems, levels.data(), visualOrder.data());
 
+    bool visual = eng->visualCursorMovement();
     if (x <= 0) {
         // left of first item
         int item = visualOrder[0]+firstItem;
@@ -2798,8 +2641,13 @@ int QTextLine::xToCursor(qreal _x, CursorPosition cpos) const
                || (line.justified && x < line.width)) {
         // has to be in one of the runs
         QFixed pos;
+        bool rtl = eng->isRightToLeft();
 
         eng->shapeLine(line);
+        QVector<int> insertionPoints;
+        if (visual && rtl)
+            eng->insertionPointsForLine(lineNum, insertionPoints);
+        int nchars = 0;
         for (int i = 0; i < nItems; ++i) {
             int item = visualOrder[i]+firstItem;
             QScriptItem &si = eng->layoutData->items[item];
@@ -2829,6 +2677,7 @@ int QTextLine::xToCursor(qreal _x, CursorPosition cpos) const
 
             if (pos + item_width < x) {
                 pos += item_width;
+                nchars += end;
                 continue;
             }
 //             qDebug("      inside run");
@@ -2873,27 +2722,60 @@ int QTextLine::xToCursor(qreal _x, CursorPosition cpos) const
             } else {
                 QFixed dist = INT_MAX/256;
                 if (si.analysis.bidiLevel % 2) {
-                    pos += item_width;
-                    while (gs <= ge) {
-                        if (glyphs.attributes[gs].clusterStart && qAbs(x-pos) < dist) {
-                            glyph_pos = gs;
-                            dist = qAbs(x-pos);
+                    if (!visual || rtl || (lastLine && i == nItems - 1)) {
+                        pos += item_width;
+                        while (gs <= ge) {
+                            if (glyphs.attributes[gs].clusterStart && qAbs(x-pos) < dist) {
+                                glyph_pos = gs;
+                                dist = qAbs(x-pos);
+                            }
+                            pos -= glyphs.effectiveAdvance(gs);
+                            ++gs;
+                        }
+                    } else {
+                        while (ge >= gs) {
+                            if (glyphs.attributes[ge].clusterStart && qAbs(x-pos) < dist) {
+                                glyph_pos = ge;
+                                dist = qAbs(x-pos);
+                            }
+                            pos += glyphs.effectiveAdvance(ge);
+                            --ge;
                         }
-                        pos -= glyphs.effectiveAdvance(gs);
-                        ++gs;
                     }
                 } else {
-                    while (gs <= ge) {
-                        if (glyphs.attributes[gs].clusterStart && qAbs(x-pos) < dist) {
-                            glyph_pos = gs;
-                            dist = qAbs(x-pos);
+                    if (!visual || !rtl || (lastLine && i == 0)) {
+                        while (gs <= ge) {
+                            if (glyphs.attributes[gs].clusterStart && qAbs(x-pos) < dist) {
+                                glyph_pos = gs;
+                                dist = qAbs(x-pos);
+                            }
+                            pos += glyphs.effectiveAdvance(gs);
+                            ++gs;
                         }
-                        pos += glyphs.effectiveAdvance(gs);
-                        ++gs;
+                    } else {
+                        QFixed oldPos = pos;
+                        while (gs <= ge) {
+                            pos += glyphs.effectiveAdvance(gs);
+                            if (glyphs.attributes[gs].clusterStart && qAbs(x-pos) < dist) {
+                                glyph_pos = gs;
+                                dist = qAbs(x-pos);
+                            }
+                            ++gs;
+                        }
+                        pos = oldPos;
                     }
                 }
-                if (qAbs(x-pos) < dist)
+                if (qAbs(x-pos) < dist) {
+                    if (visual) {
+                        if (!rtl && i < nItems - 1) {
+                            nchars += end;
+                            continue;
+                        }
+                        if (rtl && nchars > 0)
+                            return insertionPoints[lastLine ? nchars : nchars - 1];
+                    }
                     return si.position + end;
+                }
             }
             Q_ASSERT(glyph_pos != -1);
             int j;
index 9dd8ebd..6aa81f9 100644 (file)
@@ -50,6 +50,7 @@
 #include <QtGui/qevent.h>
 #include <QtGui/qtextformat.h>
 #include <QtGui/qglyphs.h>
+#include <QtGui/qtextcursor.h>
 
 QT_BEGIN_HEADER
 
@@ -136,6 +137,9 @@ public:
     void setCacheEnabled(bool enable);
     bool cacheEnabled() const;
 
+    void setCursorMoveStyle(QTextCursor::MoveStyle style);
+    QTextCursor::MoveStyle cursorMoveStyle() const;
+
     void beginLayout();
     void endLayout();
     void clearLayout();
@@ -153,6 +157,8 @@ public:
     bool isValidCursorPosition(int pos) const;
     int nextCursorPosition(int oldPos, CursorMode mode = SkipCharacters) const;
     int previousCursorPosition(int oldPos, CursorMode mode = SkipCharacters) const;
+    int leftCursorPosition(int oldPos) const;
+    int rightCursorPosition(int oldPos) const;
 
     void draw(QPainter *p, const QPointF &pos, const QVector<FormatRange> &selections = QVector<FormatRange>(),
               const QRectF &clip = QRectF()) const;
index 289faa9..eb4e142 100644 (file)
@@ -1585,6 +1585,7 @@ void QLineControl::processKeyEvent(QKeyEvent* event)
     }
 
     bool unknown = false;
+    bool visual = cursorMoveStyle() == QTextCursor::Visual;
 
     if (false) {
     }
@@ -1649,11 +1650,11 @@ void QLineControl::processKeyEvent(QKeyEvent* event)
 #endif
             moveCursor(selectionEnd(), false);
         } else {
-            cursorForward(0, layoutDirection() == Qt::LeftToRight ? 1 : -1);
+            cursorForward(0, visual ? 1 : (layoutDirection() == Qt::LeftToRight ? 1 : -1));
         }
     }
     else if (event == QKeySequence::SelectNextChar) {
-        cursorForward(1, layoutDirection() == Qt::LeftToRight ? 1 : -1);
+        cursorForward(1, visual ? 1 : (layoutDirection() == Qt::LeftToRight ? 1 : -1));
     }
     else if (event == QKeySequence::MoveToPreviousChar) {
 #if !defined(Q_WS_WIN) || defined(QT_NO_COMPLETER)
@@ -1664,11 +1665,11 @@ void QLineControl::processKeyEvent(QKeyEvent* event)
 #endif
             moveCursor(selectionStart(), false);
         } else {
-            cursorForward(0, layoutDirection() == Qt::LeftToRight ? -1 : 1);
+            cursorForward(0, visual ? -1 : (layoutDirection() == Qt::LeftToRight ? -1 : 1));
         }
     }
     else if (event == QKeySequence::SelectPreviousChar) {
-        cursorForward(1, layoutDirection() == Qt::LeftToRight ? -1 : 1);
+        cursorForward(1, visual ? -1 : (layoutDirection() == Qt::LeftToRight ? -1 : 1));
     }
     else if (event == QKeySequence::MoveToNextWord) {
         if (echoMode() == QLineEdit::Normal)
index 3c505c8..0042f17 100644 (file)
@@ -160,6 +160,8 @@ public:
     int cursorWidth() const { return m_cursorWidth; }
     void setCursorWidth(int value) { m_cursorWidth = value; }
 
+    QTextCursor::MoveStyle cursorMoveStyle() const { return m_textLayout.cursorMoveStyle(); }
+    void setCursorMoveStyle(QTextCursor::MoveStyle style) { m_textLayout.setCursorMoveStyle(style); }
 
     void moveCursor(int pos, bool mark = false);
     void cursorForward(bool mark, int steps)
@@ -167,10 +169,12 @@ public:
         int c = m_cursor;
         if (steps > 0) {
             while (steps--)
-                c = m_textLayout.nextCursorPosition(c);
+                c = cursorMoveStyle() == QTextCursor::Visual ? m_textLayout.rightCursorPosition(c)
+                                                             : m_textLayout.nextCursorPosition(c);
         } else if (steps < 0) {
             while (steps++)
-                c = m_textLayout.previousCursorPosition(c);
+                c = cursorMoveStyle() == QTextCursor::Visual ? m_textLayout.leftCursorPosition(c)
+                                                             : m_textLayout.previousCursorPosition(c);
         }
         moveCursor(c, mark);
     }
index 07bd273..43c3f52 100644 (file)
@@ -1112,6 +1112,34 @@ void QLineEdit::setDragEnabled(bool b)
 
 
 /*!
+  \property QLineEdit::cursorMoveStyle
+  \brief the movement style of cursor in this line edit
+  \since 4.8
+
+  When this property is set to QTextCursor::Visual, the line edit will use visual
+  movement style. Pressing the left arrow key will always cause the cursor to move
+  left, regardless of the text's writing direction. The same behavior applies to
+  right arrow key.
+
+  When the property is QTextCursor::Logical (the default), within a LTR text block,
+  increase cursor position when pressing left arrow key, decrease cursor position
+  when pressing the right arrow key. If the text block is right to left, the opposite
+  behavior applies.
+*/
+
+QTextCursor::MoveStyle QLineEdit::cursorMoveStyle() const
+{
+    Q_D(const QLineEdit);
+    return d->control->cursorMoveStyle();
+}
+
+void QLineEdit::setCursorMoveStyle(QTextCursor::MoveStyle style)
+{
+    Q_D(QLineEdit);
+    d->control->setCursorMoveStyle(style);
+}
+
+/*!
     \property QLineEdit::acceptableInput
     \brief whether the input satisfies the inputMask and the
     validator.
index 636cee7..73a736c 100644 (file)
@@ -43,6 +43,7 @@
 #define QLINEEDIT_H
 
 #include <QtGui/qframe.h>
+#include <QtGui/qtextcursor.h>
 #include <QtCore/qstring.h>
 #include <QtCore/qmargins.h>
 
@@ -158,6 +159,9 @@ public:
     void setDragEnabled(bool b);
     bool dragEnabled() const;
 
+    void setCursorMoveStyle(QTextCursor::MoveStyle style);
+    QTextCursor::MoveStyle cursorMoveStyle() const;
+
     QString inputMask() const;
     void setInputMask(const QString &inputMask);
     bool hasAcceptableInput() const;
index 3d8e290..04943c5 100644 (file)
@@ -71,6 +71,10 @@ private slots:
     void bidiReorderString();
     void bidiCursor_qtbug2795();
     void bidiCursor_PDF();
+    void bidiCursorMovement_data();
+    void bidiCursorMovement();
+    void bidiCursorLogicalMovement_data();
+    void bidiCursorLogicalMovement();
 };
 
 tst_QComplexText::tst_QComplexText()
@@ -185,6 +189,89 @@ void tst_QComplexText::bidiCursor_qtbug2795()
     QVERIFY(x1 == x2);
 }
 
+void tst_QComplexText::bidiCursorMovement_data()
+{
+    QTest::addColumn<QString>("logical");
+    QTest::addColumn<int>("basicDir");
+
+    const LV *data = logical_visual;
+    while ( data->name ) {
+        //next we fill it with data
+        QTest::newRow( data->name )
+            << QString::fromUtf8( data->logical )
+            << (int) data->basicDir;
+        data++;
+    }
+}
+
+void tst_QComplexText::bidiCursorMovement()
+{
+    QFETCH(QString, logical);
+    QFETCH(int,  basicDir);
+
+    QTextLayout layout(logical);
+
+    QTextOption option = layout.textOption();
+    option.setTextDirection(basicDir == QChar::DirL ? Qt::LeftToRight : Qt::RightToLeft);
+    layout.setTextOption(option);
+    bool moved;
+    int oldPos, newPos = 0;
+    qreal x, newX;
+
+    layout.beginLayout();
+    QTextLine line = layout.createLine();
+    layout.endLayout();
+
+    newX = line.cursorToX(0);
+    do {
+        oldPos = newPos;
+        x = newX;
+        newX = line.cursorToX(oldPos);
+        if (basicDir == QChar::DirL) {
+            QVERIFY(newX >= x);
+            newPos = layout.rightCursorPosition(oldPos);
+        } else
+        {
+            QVERIFY(newX <= x);
+            newPos = layout.leftCursorPosition(oldPos);
+        }
+        moved = (oldPos != newPos);
+    } while (moved);
+}
+
+void tst_QComplexText::bidiCursorLogicalMovement_data()
+{
+    bidiCursorMovement_data();
+}
+
+void tst_QComplexText::bidiCursorLogicalMovement()
+{
+    QFETCH(QString, logical);
+    QFETCH(int,  basicDir);
+
+    QTextLayout layout(logical);
+
+    QTextOption option = layout.textOption();
+    option.setTextDirection(basicDir == QChar::DirL ? Qt::LeftToRight : Qt::RightToLeft);
+    layout.setTextOption(option);
+    bool moved;
+    int oldPos, newPos = 0;
+
+    do {
+        oldPos = newPos;
+        newPos = layout.nextCursorPosition(oldPos);
+        QVERIFY(newPos >= oldPos);
+        moved = (oldPos != newPos);
+    } while (moved);
+
+    do {
+        oldPos = newPos;
+        newPos = layout.previousCursorPosition(oldPos);
+        QVERIFY(newPos <= oldPos);
+        moved = (oldPos != newPos);
+    } while (moved);
+}
+
 void tst_QComplexText::bidiCursor_PDF()
 {
     QString str = QString::fromUtf8("\342\200\252hello\342\200\254");
index 9176174..f45481c 100644 (file)
@@ -282,6 +282,12 @@ private slots:
     void validateAndSet();
 #endif
 
+    void bidiVisualMovement_data();
+    void bidiVisualMovement();
+
+    void bidiLogicalMovement_data();
+    void bidiLogicalMovement();
+
 protected slots:
 #ifdef QT3_SUPPORT
     void lostFocus();
@@ -3760,5 +3766,135 @@ void tst_QLineEdit::QTBUG13520_textNotVisible()
 }
 
 
+void tst_QLineEdit::bidiVisualMovement_data()
+{
+    QTest::addColumn<QString>("logical");
+    QTest::addColumn<int>("basicDir");
+    QTest::addColumn<IntList>("positionList");
+
+    QTest::newRow("Latin text")
+        << QString::fromUtf8("abc")
+        << (int) QChar::DirL
+        << (IntList() << 0 << 1 << 2 << 3);
+    QTest::newRow("Hebrew text, one item")
+        << QString::fromUtf8("\327\220\327\221\327\222")
+        << (int) QChar::DirR
+        << (QList<int>() << 0 << 1 << 2 << 3);
+    QTest::newRow("Hebrew text after Latin text")
+        << QString::fromUtf8("abc\327\220\327\221\327\222")
+        << (int) QChar::DirL
+        << (QList<int>() << 0 << 1 << 2 << 6 << 5 << 4 << 3);
+    QTest::newRow("Latin text after Hebrew text")
+        << QString::fromUtf8("\327\220\327\221\327\222abc")
+        << (int) QChar::DirR
+        << (QList<int>() << 0 << 1 << 2 << 6 << 5 << 4 << 3);
+    QTest::newRow("LTR, 3 items")
+        << QString::fromUtf8("abc\327\220\327\221\327\222abc")
+        << (int) QChar::DirL
+        << (QList<int>() << 0 << 1 << 2 << 5 << 4 << 3 << 6 << 7 << 8 << 9);
+    QTest::newRow("RTL, 3 items")
+        << QString::fromUtf8("\327\220\327\221\327\222abc\327\220\327\221\327\222")
+        << (int) QChar::DirR
+        << (QList<int>() << 0 << 1 << 2 << 5 << 4 << 3 << 6 << 7 << 8 << 9);
+    QTest::newRow("LTR, 4 items")
+        << QString::fromUtf8("abc\327\220\327\221\327\222abc\327\220\327\221\327\222")
+        << (int) QChar::DirL
+        << (QList<int>() << 0 << 1 << 2 << 5 << 4 << 3 << 6 << 7 << 8 << 12 << 11 << 10 << 9);
+    QTest::newRow("RTL, 4 items")
+        << QString::fromUtf8("\327\220\327\221\327\222abc\327\220\327\221\327\222abc")
+        << (int) QChar::DirR
+        << (QList<int>() << 0 << 1 << 2 << 5 << 4 << 3 << 6 << 7 << 8 << 12 << 11 << 10 << 9);
+}
+
+void tst_QLineEdit::bidiVisualMovement()
+{
+    QFETCH(QString, logical);
+    QFETCH(int,     basicDir);
+    QFETCH(IntList, positionList);
+
+    QLineEdit le;
+    le.setText(logical);
+
+    le.setCursorMoveStyle(QTextCursor::Visual);
+    le.setCursorPosition(0);
+
+    bool moved;
+    int i = 0, oldPos, newPos = 0;
+
+    do {
+        oldPos = newPos;
+        QVERIFY(oldPos == positionList[i]);
+        if (basicDir == QChar::DirL) {
+            QTest::keyClick(&le, Qt::Key_Right);
+        } else
+            QTest::keyClick(&le, Qt::Key_Left);
+        newPos = le.cursorPosition();
+        moved = (oldPos != newPos);
+        i++;
+    } while (moved);
+
+    QVERIFY(i == positionList.size());
+
+    do {
+        i--;
+        oldPos = newPos;
+        QVERIFY(oldPos == positionList[i]);
+        if (basicDir == QChar::DirL) {
+            QTest::keyClick(&le, Qt::Key_Left);
+        } else
+        {
+            QTest::keyClick(&le, Qt::Key_Right);
+        }
+        newPos = le.cursorPosition();
+        moved = (oldPos != newPos);
+    } while (moved && i >= 0);
+}
+
+void tst_QLineEdit::bidiLogicalMovement_data()
+{
+    bidiVisualMovement_data();
+}
+
+void tst_QLineEdit::bidiLogicalMovement()
+{
+    QFETCH(QString, logical);
+    QFETCH(int,     basicDir);
+
+    QLineEdit le;
+    le.setText(logical);
+
+    le.setCursorMoveStyle(QTextCursor::Logical);
+    le.setCursorPosition(0);
+
+    bool moved;
+    int i = 0, oldPos, newPos = 0;
+
+    do {
+        oldPos = newPos;
+        QVERIFY(oldPos == i);
+        if (basicDir == QChar::DirL) {
+            QTest::keyClick(&le, Qt::Key_Right);
+        } else
+            QTest::keyClick(&le, Qt::Key_Left);
+        newPos = le.cursorPosition();
+        moved = (oldPos != newPos);
+        i++;
+    } while (moved);
+
+    do {
+        i--;
+        oldPos = newPos;
+        QVERIFY(oldPos == i);
+        if (basicDir == QChar::DirL) {
+            QTest::keyClick(&le, Qt::Key_Left);
+        } else
+        {
+            QTest::keyClick(&le, Qt::Key_Right);
+        }
+        newPos = le.cursorPosition();
+        moved = (oldPos != newPos);
+    } while (moved && i >= 0);
+}
+
 QTEST_MAIN(tst_QLineEdit)
 #include "tst_qlineedit.moc"
index 9ca17b9..5a64593 100644 (file)
@@ -42,7 +42,6 @@
 
 #include <QtTest/QtTest>
 
-
 #include <qtextedit.h>
 #include <qtextcursor.h>
 #include <qtextlist.h>
@@ -69,6 +68,7 @@ typedef QList<keyPairType> pairListType;
 Q_DECLARE_METATYPE(pairListType);
 Q_DECLARE_METATYPE(keyPairType);
 Q_DECLARE_METATYPE(QList<bool>);
+Q_DECLARE_METATYPE(QList<int>);
 
 #ifdef Q_WS_MAC
 #include <Carbon/Carbon.h>
@@ -205,6 +205,11 @@ private slots:
 #ifndef QT_NO_CONTEXTMENU
     void taskQTBUG_7902_contextMenuCrash();
 #endif
+    void bidiVisualMovement_data();
+    void bidiVisualMovement();
+
+    void bidiLogicalMovement_data();
+    void bidiLogicalMovement();
 
 private:
     void createSelection();
@@ -2235,5 +2240,147 @@ void tst_QTextEdit::taskQTBUG_7902_contextMenuCrash()
 }
 #endif
 
+void tst_QTextEdit::bidiVisualMovement_data()
+{
+    QTest::addColumn<QString>("logical");
+    QTest::addColumn<int>("basicDir");
+    QTest::addColumn<QList<int> >("positionList");
+
+    QTest::newRow("Latin text")
+        << QString::fromUtf8("abc")
+        << (int) QChar::DirL
+        << (QList<int>() << 0 << 1 << 2 << 3);
+    QTest::newRow("Hebrew text, one item")
+        << QString::fromUtf8("\327\220\327\221\327\222")
+        << (int) QChar::DirR
+        << (QList<int>() << 0 << 1 << 2 << 3);
+    QTest::newRow("Hebrew text after Latin text")
+        << QString::fromUtf8("abc\327\220\327\221\327\222")
+        << (int) QChar::DirL
+        << (QList<int>() << 0 << 1 << 2 << 6 << 5 << 4 << 3);
+    QTest::newRow("Latin text after Hebrew text")
+        << QString::fromUtf8("\327\220\327\221\327\222abc")
+        << (int) QChar::DirR
+        << (QList<int>() << 0 << 1 << 2 << 6 << 5 << 4 << 3);
+    QTest::newRow("LTR, 3 items")
+        << QString::fromUtf8("abc\327\220\327\221\327\222abc")
+        << (int) QChar::DirL
+        << (QList<int>() << 0 << 1 << 2 << 5 << 4 << 3 << 6 << 7 << 8 << 9);
+    QTest::newRow("RTL, 3 items")
+        << QString::fromUtf8("\327\220\327\221\327\222abc\327\220\327\221\327\222")
+        << (int) QChar::DirR
+        << (QList<int>() << 0 << 1 << 2 << 5 << 4 << 3 << 6 << 7 << 8 << 9);
+    QTest::newRow("LTR, 4 items")
+        << QString::fromUtf8("abc\327\220\327\221\327\222abc\327\220\327\221\327\222")
+        << (int) QChar::DirL
+        << (QList<int>() << 0 << 1 << 2 << 5 << 4 << 3 << 6 << 7 << 8 << 12 << 11 << 10 << 9);
+    QTest::newRow("RTL, 4 items")
+        << QString::fromUtf8("\327\220\327\221\327\222abc\327\220\327\221\327\222abc")
+        << (int) QChar::DirR
+        << (QList<int>() << 0 << 1 << 2 << 5 << 4 << 3 << 6 << 7 << 8 << 12 << 11 << 10 << 9);
+}
+
+void tst_QTextEdit::bidiVisualMovement()
+{
+    QFETCH(QString,      logical);
+    QFETCH(int,          basicDir);
+    QFETCH(QList<int>,   positionList);
+
+    ed->setText(logical);
+
+    QTextOption option = ed->document()->defaultTextOption();
+    option.setTextDirection(basicDir == QChar::DirL ? Qt::LeftToRight : Qt::RightToLeft);
+    ed->document()->setDefaultTextOption(option);
+
+    ed->document()->setDefaultCursorMoveStyle(QTextCursor::Visual);
+    ed->moveCursor(QTextCursor::Start);
+    ed->show();
+
+    bool moved;
+    int i = 0, oldPos, newPos = 0;
+
+    do {
+        oldPos = newPos;
+        QVERIFY(oldPos == positionList[i]);
+        if (basicDir == QChar::DirL) {
+            ed->moveCursor(QTextCursor::Right);
+        } else
+        {
+            ed->moveCursor(QTextCursor::Left);
+        }
+        newPos = ed->textCursor().position();
+        moved = (oldPos != newPos);
+        i++;
+    } while (moved);
+
+    QVERIFY(i == positionList.size());
+
+    do {
+        i--;
+        oldPos = newPos;
+        QVERIFY(oldPos == positionList[i]);
+        if (basicDir == QChar::DirL) {
+            ed->moveCursor(QTextCursor::Left);
+        } else
+        {
+            ed->moveCursor(QTextCursor::Right);
+        }
+        newPos = ed->textCursor().position();
+        moved = (oldPos != newPos);
+    } while (moved && i >= 0);
+}
+
+void tst_QTextEdit::bidiLogicalMovement_data()
+{
+    bidiVisualMovement_data();
+}
+
+void tst_QTextEdit::bidiLogicalMovement()
+{
+    QFETCH(QString,      logical);
+    QFETCH(int,          basicDir);
+
+    ed->setText(logical);
+
+    QTextOption option = ed->document()->defaultTextOption();
+    option.setTextDirection(basicDir == QChar::DirL ? Qt::LeftToRight : Qt::RightToLeft);
+    ed->document()->setDefaultTextOption(option);
+
+    ed->document()->setDefaultCursorMoveStyle(QTextCursor::Logical);
+    ed->moveCursor(QTextCursor::Start);
+    ed->show();
+
+    bool moved;
+    int i = 0, oldPos, newPos = 0;
+
+    do {
+        oldPos = newPos;
+        QVERIFY(oldPos == i);
+        if (basicDir == QChar::DirL) {
+            ed->moveCursor(QTextCursor::Right);
+        } else
+        {
+            ed->moveCursor(QTextCursor::Left);
+        }
+        newPos = ed->textCursor().position();
+        moved = (oldPos != newPos);
+        i++;
+    } while (moved);
+
+    do {
+        i--;
+        oldPos = newPos;
+        QVERIFY(oldPos == i);
+        if (basicDir == QChar::DirL) {
+            ed->moveCursor(QTextCursor::Left);
+        } else
+        {
+            ed->moveCursor(QTextCursor::Right);
+        }
+        newPos = ed->textCursor().position();
+        moved = (oldPos != newPos);
+    } while (moved && i >= 0);
+}
+
 QTEST_MAIN(tst_QTextEdit)
 #include "tst_qtextedit.moc"