Solved Highlighting bidirectional text segments in QTreeView
-
Hello.
My goal is to highlight text segments in QTreeView. You can find sample code (just one single file) at the end of this message.
First option is to use QTextDocument::drawContents but it seems to be slow so I'm trying to avoid using it. And this is where things are getting complicated...
What is the problem ? Try to run sample code with "#define HIGHLIGHT_OPTION 1". As you can see the text entered by user is different from text shown by table. This is because entered text is bidirectional text - it contains text in both text directionalities, both right-to-left (RTL) and left-to-right (LTR). In our example text is "cat قط". So the char sequence is 99, 97, 116, 32, 1602, 1591. But it will drawn as 1602, 1591, 32, 99, 97, 116 because of bidirectional drawing algorithm.
And this is the reason why sample code with "#define HIGHLIGHT_OPTION 2" will fail. Let's say I neet to hihglight text "at". Seems like easy thing to do. Just use QFontMetrics::width to calculate width of "c" and use it as left position of highlight rectangle. Then calculate width of highlighting text ("at") and use it as width of highlight rectangle. It will work for one-directional text (you can try to enter text "cat" or "water" in sample program - program should highlight letter 't' as expected). But bidirictional text will be drawn like this - first "قط" then "cat". So QFontMetrics will not work for me because I can't predict exact position of text segment on the screen.
Next option is more complicated. You can use "#define HIGHLIGHT_OPTION 3" in sample code to see my attempt to use QTextLayout and QTextLine. It looks like in this case I can get actual screen position of any drawn charcter but...- Can I safely use glyph index as unicode char identifier?
- Is there any method to map text position from source string to drawn string?
About mapping. Let's say I have bidirectional text which looks like this: "AA BB AA BB AA BB". Let's say I have to highlight second "AA". But how can I know which of "AA"s is my second "AA" in drawn text?
So that's it. Or maybe you know about better approach to achieve my goal.
And this is sample code:
// main.cpp // You have to run qmake for successful compilation ! // Use HIGHLIGHT_OPTION macro to setup the code. // Possible values: // 1 - do not use highlight, paint text as usual // 2 - highlight by QTextMetric // 3 - highlight by QTextLayout // 4 - highlight by QTextDocument with html-text // 5 - highlight by QTextDocument with html-text and zwj-symbol // 6 - highlight by QTextDocument purely #define HIGHLIGHT_OPTION 4 #include <QApplication> #include <QWidget> #include <QHBoxLayout> #include <QVBoxLayout> #include <QLineEdit> #include <QTableWidget> #include <QStyledItemDelegate> #include <QPainter> #include <QTextLayout> #include <QTextDocument> #include <QLabel> #include <QTextFrame> class Delegate: public QStyledItemDelegate { public: Delegate(): QStyledItemDelegate() {} void setHighlightText(const QString& text) { hlText = text; } void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { #if HIGHLIGHT_OPTION == 1 QStyledItemDelegate::paint(painter, option, index); #elif HIGHLIGHT_OPTION == 2 QString cellText = index.model()->data(index).toString(); const QRect r = option.rect.adjusted(1,0,-1,0); // with indent int flags = Qt::AlignVCenter; painter->drawText(r, flags, cellText); // Highlight code start here. // Important to known - text is right aligned. // Note: this is demo algorithm. It doesn't work flawless. int hlPos = cellText.indexOf(hlText); if (hlPos == -1) return; QString textAfter = cellText.mid(hlPos+hlText.size()); int widthAfter = option.fontMetrics.width(textAfter); int widthHl = option.fontMetrics.width(hlText); QRect hlRect( option.rect.right()-widthAfter-widthHl, option.rect.top(), widthHl, option.rect.bottom() ); painter->save(); painter->setCompositionMode(QPainter::RasterOp_NotSourceAndDestination); // Keep text visible painter->fillRect(hlRect,Qt::red); painter->restore(); #elif HIGHLIGHT_OPTION == 3 QString cellText = index.model()->data(index).toString(); int hlPos = cellText.indexOf(hlText); QTextLayout layout(cellText, option.font); QTextOption textOption = layout.textOption(); textOption.setAlignment(Qt::AlignRight); textOption.setTextDirection(Qt::RightToLeft); layout.setTextOption(textOption); layout.beginLayout(); QTextLine line = layout.createLine(); if (line.isValid()) { line.setLineWidth(option.rect.width()); QPointF pos(0, (option.rect.height() - option.fontMetrics.height())/2); line.setPosition(pos); if (hlPos != -1) { // Highlight code must be here // But I don't know how to get actual text position from hlPos and line QList<QGlyphRun> runs = line.glyphRuns(); for (QList<QGlyphRun>::const_iterator iter = runs.cbegin(); iter != runs.cend(); iter++) { const QGlyphRun& run = *iter; // Maybe I can use these? QVector<quint32> glyphIndexes = run.glyphIndexes(); QVector<QPointF> positions = run.positions(); } } } layout.endLayout(); line = layout.lineAt(0); if (line.isValid()) { QPointF pos(option.rect.left(), option.rect.top()); line.draw(painter, pos); } #elif HIGHLIGHT_OPTION == 4 || HIGHLIGHT_OPTION == 5 QTextDocument doc; doc.setUndoRedoEnabled(false); QTextFrameFormat frameFormat = doc.rootFrame()->frameFormat(); frameFormat.setLeftMargin(0); frameFormat.setRightMargin(0); doc.rootFrame()->setFrameFormat(frameFormat); QString html = "<style>span.blue{background-color:lightblue}</style><div dir='rtl'>"; QString cellText = index.model()->data(index).toString(); int hlPos = cellText.indexOf(hlText); if (hlPos == -1) html += hlText; else html += QString("%1%2<span class='blue'>%3</span>%4") .arg(cellText.mid(0, hlPos)) // Text before highlight #if HIGHLIGHT_OPTION == 4 .arg("") #elif HIGHLIGHT_OPTION == 5 .arg("‍") // I tried to put zwj into different positions but it never works perfectly // For example: // "%1%2<span class='blue'>%3</span>%4" // "%1<span class='blue'>%2%3</span>%4" // etc #endif .arg(cellText.mid(hlPos, hlText.size())) // Text to highlight .arg(cellText.mid(hlPos+ hlText.size())); // Text after highlight html += "</div>"; doc.setHtml(html); painter->translate(option.rect.topLeft()); QRect r = option.rect; r.moveTo(0, 0); doc.drawContents(painter, r); #elif HIGHLIGHT_OPTION == 6 QTextDocument doc; doc.setUndoRedoEnabled(false); QTextFrameFormat frameFormat = doc.rootFrame()->frameFormat(); frameFormat.setLeftMargin(0); frameFormat.setRightMargin(0); doc.rootFrame()->setFrameFormat(frameFormat); QString cellText = index.model()->data(index).toString(); doc.setPlainText(cellText); int hlPos = cellText.indexOf(hlText); if (hlPos != -1) { QTextCursor oCur(&doc); oCur.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, hlPos); oCur.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, hlText.size()); QTextCharFormat fmt = oCur.charFormat(); fmt.setBackground(QBrush(QColor(0xAAAAFF))); oCur.setCharFormat(fmt); } painter->translate(option.rect.topLeft()); QRect r = option.rect; r.moveTo(0, 0); doc.drawContents(painter, r); #endif } private: QString hlText; }; class Widget: public QWidget { Q_OBJECT public: Widget(): QWidget() { setLayout(&layout); layout.addLayout(&layoutCellText); layoutCellText.addWidget(&editCellText); editCellText.setAlignment(Qt::AlignRight); connect(&editCellText, &QLineEdit::textChanged, this, &Widget::onCellTextChanged); layoutCellText.addWidget(&labelCellText); labelCellText.setText("Text in table"); layout.addLayout(&layoutHlText); layoutHlText.addWidget(&editHlText); editHlText.setAlignment(Qt::AlignRight); connect(&editHlText, &QLineEdit::textChanged, this, &Widget::onHlTextChanged); layoutHlText.addWidget(&labelHlText); labelHlText.setText("Text to highlight "); layout.addWidget(&table); table.setRowCount(1); table.setColumnCount(1); table.setItemDelegate(&delegate); table.setItem(0,0, &item); editCellText.setText("cat قط"); editHlText.setText("ق"); } private slots: void onCellTextChanged(const QString& text) { item.setText(text); } void onHlTextChanged(const QString& text) { delegate.setHighlightText(text); table.viewport()->update(); } private: QVBoxLayout layout; QHBoxLayout layoutCellText; QLabel labelCellText; QLineEdit editCellText; QHBoxLayout layoutHlText; QLabel labelHlText; QLineEdit editHlText; QTableWidget table; Delegate delegate; QTableWidgetItem item; }; #include "main.moc" int main(int argc, char *argv[]) { QApplication a(argc, argv); QGuiApplication::setLayoutDirection(Qt::RightToLeft); Widget w; w.show(); return a.exec(); }
-
Having no better idea I tried to use QTextDocument::drawContents. It doesn't work perfect because of word breaking issue described here.
Do I miss something? I can't believe Qt doesn't have fast and reliable solution for this particular issue. -
the QTextLayout/QTextLine idea was a great one but falls short in your particular case.
QTextDocument
seems like the way to go (even if rendering is way slower). did you try using just html tags to format your text in QTextDocument?I can't believe Qt doesn't have fast and reliable solution for this particular issue.
Mixed LTR and RTL text is not really the abc of situations but, I agree, it's not great
-
@VRonin
I updated sample code to show you how exactIy I tried to use QTextDocument::drawContents. New options:- 4 - use QTextDocument with html formatted text
- 5 - same as 4 but also with ZWJ-symbol
- 6 - use QTextDocument "purely" (document, cursor and format)
I don't know what to do now. Maybe digging into Qt sources will give me some insights.
-
Mixed LTR and RTL text is not really the abc of situations
I Agree. This is not something usual. So lack of abilities to do it is not something unbelievable.
-
Finally I got it. Didn't notice QTextLayout::draw function before.
Sometimes it is hard to find working solution even if it is right in front of your eyes.