From: Manuel Nickschas Date: Thu, 30 Jul 2009 15:10:55 +0000 (+0200) Subject: Revert "Remove the word boundary cache" X-Git-Tag: 0.5-rc1~100 X-Git-Url: https://git.quassel-irc.org/?p=quassel.git;a=commitdiff_plain;h=00f633d013c9c9b87ae811c62daca40c1f73d1b8 Revert "Remove the word boundary cache" We'll look into those issues again later. For now prevent performance degradation. This reverts commit 94f4e9d385d32eb1bb8fbf1c9736ca3ccc31cdd6. --- diff --git a/src/qtui/chatitem.cpp b/src/qtui/chatitem.cpp index 819877ae..390e428a 100644 --- a/src/qtui/chatitem.cpp +++ b/src/qtui/chatitem.cpp @@ -89,15 +89,11 @@ void ChatItem::initLayoutHelper(QTextLayout *layout, QTextOption::WrapMode wrapM } void ChatItem::doLayout(QTextLayout *layout) const { - qreal h = 0; layout->beginLayout(); - forever { - QTextLine line = layout->createLine(); - if(!line.isValid()) - break; + QTextLine line = layout->createLine(); + if(line.isValid()) { line.setLineWidth(width()); - line.setPosition(QPointF(0, h)); - h += line.height(); + line.setPosition(QPointF(0,0)); } layout->endLayout(); } @@ -398,15 +394,38 @@ qreal ContentsChatItem::setGeometryByWidth(qreal w) { //if(w != width()) { prepareGeometryChange(); setWidth(w); - QTextLayout layout; - initLayout(&layout); - setHeight(layout.boundingRect().height()); + // compute height + int lines = 1; + WrapColumnFinder finder(this); + while(finder.nextWrapColumn() > 0) + lines++; + setHeight(lines * fontMetrics()->lineSpacing()); delete _data; _data = 0; //} return height(); } +void ContentsChatItem::doLayout(QTextLayout *layout) const { + ChatLineModel::WrapList wrapList = data(ChatLineModel::WrapListRole).value(); + if(!wrapList.count()) return; // empty chatitem + + qreal h = 0; + WrapColumnFinder finder(this); + layout->beginLayout(); + forever { + QTextLine line = layout->createLine(); + if(!line.isValid()) + break; + + int col = finder.nextWrapColumn(); + line.setNumColumns(col >= 0 ? col - line.textStart() : layout->text().length()); + line.setPosition(QPointF(0, h)); + h += fontMetrics()->lineSpacing(); + } + layout->endLayout(); +} + // NOTE: This method is not threadsafe and not reentrant! // (RegExps are not constant while matching, and they are static here for efficiency) QList ContentsChatItem::findClickables() const { @@ -669,3 +688,66 @@ void ContentsChatItem::clearWebPreview() { /*************************************************************************************************/ +ContentsChatItem::WrapColumnFinder::WrapColumnFinder(const ChatItem *_item) + : item(_item), + wrapList(item->data(ChatLineModel::WrapListRole).value()), + wordidx(0), + lineCount(0), + choppedTrailing(0) +{ +} + +ContentsChatItem::WrapColumnFinder::~WrapColumnFinder() { +} + +qint16 ContentsChatItem::WrapColumnFinder::nextWrapColumn() { + if(wordidx >= wrapList.count()) + return -1; + + lineCount++; + qreal targetWidth = lineCount * item->width() + choppedTrailing; + + qint16 start = wordidx; + qint16 end = wrapList.count() - 1; + + // check if the whole line fits + if(wrapList.at(end).endX <= targetWidth) // || start == end) + return -1; + + // check if we have a very long word that needs inter word wrap + if(wrapList.at(start).endX > targetWidth) { + if(!line.isValid()) { + item->initLayoutHelper(&layout, QTextOption::NoWrap); + layout.beginLayout(); + line = layout.createLine(); + layout.endLayout(); + } + return line.xToCursor(targetWidth, QTextLine::CursorOnCharacter); + } + + while(true) { + if(start + 1 == end) { + wordidx = end; + const ChatLineModel::Word &lastWord = wrapList.at(start); // the last word we were able to squeeze in + + // both cases should be cought preliminary + Q_ASSERT(lastWord.endX <= targetWidth); // ensure that "start" really fits in + Q_ASSERT(end < wrapList.count()); // ensure that start isn't the last word + + choppedTrailing += lastWord.trailing - (targetWidth - lastWord.endX); + return wrapList.at(wordidx).start; + } + + qint16 pivot = (end + start) / 2; + if(wrapList.at(pivot).endX > targetWidth) { + end = pivot; + } else { + start = pivot; + } + } + Q_ASSERT(false); + return -1; +} + +/*************************************************************************************************/ + diff --git a/src/qtui/chatitem.h b/src/qtui/chatitem.h index cf71ad9b..10c53993 100644 --- a/src/qtui/chatitem.h +++ b/src/qtui/chatitem.h @@ -186,13 +186,15 @@ protected: virtual QVector additionalFormats() const; virtual inline void initLayout(QTextLayout *layout) const { - initLayoutHelper(layout, QTextOption::WrapAtWordBoundaryOrAnywhere); + initLayoutHelper(layout, QTextOption::WrapAnywhere); doLayout(layout); } + virtual void doLayout(QTextLayout *layout) const; private: struct Clickable; class ActionProxy; + class WrapColumnFinder; ContentsChatItemPrivate *_data; ContentsChatItemPrivate *privateData() const; @@ -241,6 +243,23 @@ struct ContentsChatItemPrivate { ContentsChatItemPrivate(const QList &c, ContentsChatItem *parent) : contentsItem(parent), clickables(c) {} }; +class ContentsChatItem::WrapColumnFinder { +public: + WrapColumnFinder(const ChatItem *parent); + ~WrapColumnFinder(); + + qint16 nextWrapColumn(); + +private: + const ChatItem *item; + QTextLayout layout; + QTextLine line; + ChatLineModel::WrapList wrapList; + qint16 wordidx; + qint16 lineCount; + qreal choppedTrailing; +}; + //! Acts as a proxy for Action signals targetted at a ContentsChatItem /** Since a ChatItem is not a QObject, hence cannot receive signals, we use a static ActionProxy * as a receiver instead. This avoids having to handle ChatItem actions (e.g. context menu entries) diff --git a/src/qtui/chatlinemodel.cpp b/src/qtui/chatlinemodel.cpp index a1cbbc34..72af95c1 100644 --- a/src/qtui/chatlinemodel.cpp +++ b/src/qtui/chatlinemodel.cpp @@ -23,7 +23,8 @@ ChatLineModel::ChatLineModel(QObject *parent) : MessageModel(parent) { - + qRegisterMetaType("ChatLineModel::WrapList"); + qRegisterMetaTypeStreamOperators("ChatLineModel::WrapList"); } // MessageModelItem *ChatLineModel::createMessageModelItem(const Message &msg) { @@ -42,3 +43,23 @@ Message ChatLineModel::takeMessageAt(int i) { _messageList.removeAt(i); return msg; } + +QDataStream &operator<<(QDataStream &out, const ChatLineModel::WrapList wplist) { + out << wplist.count(); + ChatLineModel::WrapList::const_iterator it = wplist.begin(); + while(it != wplist.end()) { + out << (*it).start << (*it).width << (*it).trailing; + ++it; + } + return out; +} + +QDataStream &operator>>(QDataStream &in, ChatLineModel::WrapList &wplist) { + quint16 cnt; + in >> cnt; + wplist.resize(cnt); + for(quint16 i = 0; i < cnt; i++) { + in >> wplist[i].start >> wplist[i].width >> wplist[i].trailing; + } + return in; +} diff --git a/src/qtui/chatlinemodel.h b/src/qtui/chatlinemodel.h index c21d1c3b..74f0aceb 100644 --- a/src/qtui/chatlinemodel.h +++ b/src/qtui/chatlinemodel.h @@ -31,12 +31,16 @@ class ChatLineModel : public MessageModel { public: enum ChatLineRole { - MsgLabelRole = MessageModel::UserRole, + WrapListRole = MessageModel::UserRole, + MsgLabelRole, SelectedBackgroundRole }; ChatLineModel(QObject *parent = 0); + typedef ChatLineModelItem::Word Word; + typedef ChatLineModelItem::WrapList WrapList; + protected: // virtual MessageModelItem *createMessageModelItem(const Message &); @@ -58,5 +62,10 @@ private: QList _messageList; }; +QDataStream &operator<<(QDataStream &out, const ChatLineModel::WrapList); +QDataStream &operator>>(QDataStream &in, ChatLineModel::WrapList &); + +Q_DECLARE_METATYPE(ChatLineModel::WrapList) + #endif diff --git a/src/qtui/chatlinemodelitem.cpp b/src/qtui/chatlinemodelitem.cpp index 187ba33a..343fb992 100644 --- a/src/qtui/chatlinemodelitem.cpp +++ b/src/qtui/chatlinemodelitem.cpp @@ -18,11 +18,29 @@ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * ***************************************************************************/ +#include +#include + #include "chatlinemodelitem.h" #include "chatlinemodel.h" #include "qtui.h" #include "qtuistyle.h" +// This Struct is taken from Harfbuzz. We use it only to calc it's size. +// we use a shared memory region so we do not have to malloc a buffer area for every line +typedef struct { + /*HB_LineBreakType*/ unsigned lineBreakType :2; + /*HB_Bool*/ unsigned whiteSpace :1; /* A unicode whitespace character, except NBSP, ZWNBSP */ + /*HB_Bool*/ unsigned charStop :1; /* Valid cursor position (for left/right arrow) */ + /*HB_Bool*/ unsigned wordBoundary :1; + /*HB_Bool*/ unsigned sentenceBoundary :1; + unsigned unused :2; +} HB_CharAttributes_Dummy; + + +unsigned char *ChatLineModelItem::TextBoundaryFinderBuffer = (unsigned char *)malloc(512 * sizeof(HB_CharAttributes_Dummy)); +int ChatLineModelItem::TextBoundaryFinderBufferSize = 512 * (sizeof(HB_CharAttributes_Dummy) / sizeof(unsigned char)); + // **************************************** // the actual ChatLineModelItem // **************************************** @@ -103,6 +121,10 @@ QVariant ChatLineModelItem::contentsData(int role) const { return backgroundBrush(UiStyle::Contents, true); case ChatLineModel::FormatRole: return QVariant::fromValue(_styledMsg.contentsFormatList()); + case ChatLineModel::WrapListRole: + if(_wrapList.isEmpty()) + computeWrapList(); + return QVariant::fromValue(_wrapList); } return QVariant(); } @@ -122,3 +144,78 @@ QVariant ChatLineModelItem::backgroundBrush(UiStyle::FormatType subelement, bool return QVariant::fromValue(fmt.background()); return QVariant(); } + +void ChatLineModelItem::computeWrapList() const { + int length = _styledMsg.plainContents().length(); + if(!length) + return; + + enum Mode { SearchStart, SearchEnd }; + + QList wplist; // use a temp list which we'll later copy into a QVector for efficiency + QTextBoundaryFinder finder(QTextBoundaryFinder::Word, _styledMsg.plainContents().unicode(), length, + TextBoundaryFinderBuffer, TextBoundaryFinderBufferSize); + + int idx; + int oldidx = 0; + bool wordStart = false; + bool wordEnd = false; + Mode mode = SearchEnd; + ChatLineModel::Word word; + word.start = 0; + qreal wordstartx = 0; + + QTextLayout layout(_styledMsg.plainContents()); + QTextOption option; + option.setWrapMode(QTextOption::NoWrap); + layout.setTextOption(option); + + layout.setAdditionalFormats(QtUi::style()->toTextLayoutList(_styledMsg.contentsFormatList(), length, messageLabel())); + layout.beginLayout(); + QTextLine line = layout.createLine(); + line.setNumColumns(length); + layout.endLayout(); + + do { + idx = finder.toNextBoundary(); + if(idx < 0) { + idx = length; + wordStart = false; + wordEnd = false; + mode = SearchStart; + } else { + wordStart = finder.boundaryReasons().testFlag(QTextBoundaryFinder::StartWord); + wordEnd = finder.boundaryReasons().testFlag(QTextBoundaryFinder::EndWord); + } + + //if(flg) qDebug() << idx << mode << wordStart << wordEnd << contents->plainText.left(idx) << contents->plainText.mid(idx); + + if(mode == SearchEnd || (!wordStart && wordEnd)) { + if(wordStart || !wordEnd) continue; + oldidx = idx; + mode = SearchStart; + continue; + } + qreal wordendx = line.cursorToX(oldidx); + qreal trailingendx = line.cursorToX(idx); + word.endX = wordendx; + word.width = wordendx - wordstartx; + word.trailing = trailingendx - wordendx; + wordstartx = trailingendx; + wplist.append(word); + + if(wordStart) { + word.start = idx; + mode = SearchEnd; + } + // the part " || (finder.position() == contents->plainText.length())" shouldn't be necessary + // but in rare and indeterministic cases Qt states that the end of the text is not a boundary o_O + } while(finder.isAtBoundary() || (finder.position() == length)); + + // A QVector needs less space than a QList + _wrapList.resize(wplist.count()); + for(int i = 0; i < wplist.count(); i++) { + _wrapList[i] = wplist.at(i); + } +} + diff --git a/src/qtui/chatlinemodelitem.h b/src/qtui/chatlinemodelitem.h index 9fe4b8bc..b7a3a858 100644 --- a/src/qtui/chatlinemodelitem.h +++ b/src/qtui/chatlinemodelitem.h @@ -39,6 +39,15 @@ public: virtual inline Message::Type msgType() const { return _styledMsg.type(); } virtual inline Message::Flags msgFlags() const { return _styledMsg.flags(); } + /// Used to store information about words to be used for wrapping + struct Word { + quint16 start; + qreal endX; + qreal width; + qreal trailing; + }; + typedef QVector WrapList; + private: QVariant timestampData(int role) const; QVariant senderData(int role) const; @@ -47,7 +56,13 @@ private: QVariant backgroundBrush(UiStyle::FormatType subelement, bool selected = false) const; quint32 messageLabel() const; + void computeWrapList() const; + + mutable WrapList _wrapList; UiStyle::StyledMessage _styledMsg; + + static unsigned char *TextBoundaryFinderBuffer; + static int TextBoundaryFinderBufferSize; }; #endif