Revert "Remove the word boundary cache"
authorManuel Nickschas <sputnick@quassel-irc.org>
Thu, 30 Jul 2009 15:10:55 +0000 (17:10 +0200)
committerManuel Nickschas <sputnick@quassel-irc.org>
Thu, 6 Aug 2009 18:25:58 +0000 (20:25 +0200)
We'll look into those issues again later. For now prevent performance degradation.

This reverts commit 94f4e9d385d32eb1bb8fbf1c9736ca3ccc31cdd6.

src/qtui/chatitem.cpp
src/qtui/chatitem.h
src/qtui/chatlinemodel.cpp
src/qtui/chatlinemodel.h
src/qtui/chatlinemodelitem.cpp
src/qtui/chatlinemodelitem.h

index 819877a..390e428 100644 (file)
@@ -89,15 +89,11 @@ void ChatItem::initLayoutHelper(QTextLayout *layout, QTextOption::WrapMode wrapM
 }
 
 void ChatItem::doLayout(QTextLayout *layout) const {
 }
 
 void ChatItem::doLayout(QTextLayout *layout) const {
-  qreal h = 0;
   layout->beginLayout();
   layout->beginLayout();
-  forever {
-    QTextLine line = layout->createLine();
-    if(!line.isValid())
-      break;
+  QTextLine line = layout->createLine();
+  if(line.isValid()) {
     line.setLineWidth(width());
     line.setLineWidth(width());
-    line.setPosition(QPointF(0, h));
-    h += line.height();
+    line.setPosition(QPointF(0,0));
   }
   layout->endLayout();
 }
   }
   layout->endLayout();
 }
@@ -398,15 +394,38 @@ qreal ContentsChatItem::setGeometryByWidth(qreal w) {
   //if(w != width()) {
     prepareGeometryChange();
     setWidth(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();
 }
 
     delete _data;
     _data = 0;
   //}
   return height();
 }
 
+void ContentsChatItem::doLayout(QTextLayout *layout) const {
+  ChatLineModel::WrapList wrapList = data(ChatLineModel::WrapListRole).value<ChatLineModel::WrapList>();
+  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::Clickable> ContentsChatItem::findClickables() const {
 // NOTE: This method is not threadsafe and not reentrant!
 //       (RegExps are not constant while matching, and they are static here for efficiency)
 QList<ContentsChatItem::Clickable> ContentsChatItem::findClickables() const {
@@ -669,3 +688,66 @@ void ContentsChatItem::clearWebPreview() {
 
 /*************************************************************************************************/
 
 
 /*************************************************************************************************/
 
+ContentsChatItem::WrapColumnFinder::WrapColumnFinder(const ChatItem *_item)
+  : item(_item),
+    wrapList(item->data(ChatLineModel::WrapListRole).value<ChatLineModel::WrapList>()),
+    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;
+}
+
+/*************************************************************************************************/
+
index cf71ad9..10c5399 100644 (file)
@@ -186,13 +186,15 @@ protected:
   virtual QVector<QTextLayout::FormatRange> additionalFormats() const;
 
   virtual inline void initLayout(QTextLayout *layout) const {
   virtual QVector<QTextLayout::FormatRange> additionalFormats() const;
 
   virtual inline void initLayout(QTextLayout *layout) const {
-    initLayoutHelper(layout, QTextOption::WrapAtWordBoundaryOrAnywhere);
+    initLayoutHelper(layout, QTextOption::WrapAnywhere);
     doLayout(layout);
   }
     doLayout(layout);
   }
+  virtual void doLayout(QTextLayout *layout) const;
 
 private:
   struct Clickable;
   class ActionProxy;
 
 private:
   struct Clickable;
   class ActionProxy;
+  class WrapColumnFinder;
 
   ContentsChatItemPrivate *_data;
   ContentsChatItemPrivate *privateData() const;
 
   ContentsChatItemPrivate *_data;
   ContentsChatItemPrivate *privateData() const;
@@ -241,6 +243,23 @@ struct ContentsChatItemPrivate {
   ContentsChatItemPrivate(const QList<ContentsChatItem::Clickable> &c, ContentsChatItem *parent) : contentsItem(parent), clickables(c) {}
 };
 
   ContentsChatItemPrivate(const QList<ContentsChatItem::Clickable> &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)
 //! 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)
index a1cbbc3..72af95c 100644 (file)
@@ -23,7 +23,8 @@
 ChatLineModel::ChatLineModel(QObject *parent)
   : MessageModel(parent)
 {
 ChatLineModel::ChatLineModel(QObject *parent)
   : MessageModel(parent)
 {
-
+  qRegisterMetaType<WrapList>("ChatLineModel::WrapList");
+  qRegisterMetaTypeStreamOperators<WrapList>("ChatLineModel::WrapList");
 }
 
 // MessageModelItem *ChatLineModel::createMessageModelItem(const Message &msg) {
 }
 
 // MessageModelItem *ChatLineModel::createMessageModelItem(const Message &msg) {
@@ -42,3 +43,23 @@ Message ChatLineModel::takeMessageAt(int i) {
   _messageList.removeAt(i);
   return msg;
 }
   _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;
+}
index c21d1c3..74f0ace 100644 (file)
@@ -31,12 +31,16 @@ class ChatLineModel : public MessageModel {
 
 public:
   enum ChatLineRole {
 
 public:
   enum ChatLineRole {
-    MsgLabelRole = MessageModel::UserRole,
+    WrapListRole = MessageModel::UserRole,
+    MsgLabelRole,
     SelectedBackgroundRole
   };
 
   ChatLineModel(QObject *parent = 0);
 
     SelectedBackgroundRole
   };
 
   ChatLineModel(QObject *parent = 0);
 
+  typedef ChatLineModelItem::Word Word;
+  typedef ChatLineModelItem::WrapList WrapList;
+
 protected:
 //   virtual MessageModelItem *createMessageModelItem(const Message &);
 
 protected:
 //   virtual MessageModelItem *createMessageModelItem(const Message &);
 
@@ -58,5 +62,10 @@ private:
   QList<ChatLineModelItem> _messageList;
 };
 
   QList<ChatLineModelItem> _messageList;
 };
 
+QDataStream &operator<<(QDataStream &out, const ChatLineModel::WrapList);
+QDataStream &operator>>(QDataStream &in, ChatLineModel::WrapList &);
+
+Q_DECLARE_METATYPE(ChatLineModel::WrapList)
+
 #endif
 
 #endif
 
index 187ba33..343fb99 100644 (file)
  *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
  ***************************************************************************/
 
  *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
  ***************************************************************************/
 
+#include <QFontMetrics>
+#include <QTextBoundaryFinder>
+
 #include "chatlinemodelitem.h"
 #include "chatlinemodel.h"
 #include "qtui.h"
 #include "qtuistyle.h"
 
 #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
 // ****************************************
 // ****************************************
 // the actual ChatLineModelItem
 // ****************************************
@@ -103,6 +121,10 @@ QVariant ChatLineModelItem::contentsData(int role) const {
     return backgroundBrush(UiStyle::Contents, true);
   case ChatLineModel::FormatRole:
     return QVariant::fromValue<UiStyle::FormatList>(_styledMsg.contentsFormatList());
     return backgroundBrush(UiStyle::Contents, true);
   case ChatLineModel::FormatRole:
     return QVariant::fromValue<UiStyle::FormatList>(_styledMsg.contentsFormatList());
+  case ChatLineModel::WrapListRole:
+    if(_wrapList.isEmpty())
+      computeWrapList();
+    return QVariant::fromValue<ChatLineModel::WrapList>(_wrapList);
   }
   return QVariant();
 }
   }
   return QVariant();
 }
@@ -122,3 +144,78 @@ QVariant ChatLineModelItem::backgroundBrush(UiStyle::FormatType subelement, bool
     return QVariant::fromValue<QBrush>(fmt.background());
   return QVariant();
 }
     return QVariant::fromValue<QBrush>(fmt.background());
   return QVariant();
 }
+
+void ChatLineModelItem::computeWrapList() const {
+  int length = _styledMsg.plainContents().length();
+  if(!length)
+    return;
+
+  enum Mode { SearchStart, SearchEnd };
+
+  QList<ChatLineModel::Word> 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);
+  }
+}
+
index 9fe4b8b..b7a3a85 100644 (file)
@@ -39,6 +39,15 @@ public:
   virtual inline Message::Type msgType() const { return _styledMsg.type(); }
   virtual inline Message::Flags msgFlags() const { return _styledMsg.flags(); }
 
   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<Word> WrapList;
+
 private:
   QVariant timestampData(int role) const;
   QVariant senderData(int role) const;
 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;
 
   QVariant backgroundBrush(UiStyle::FormatType subelement, bool selected = false) const;
   quint32 messageLabel() const;
 
+  void computeWrapList() const;
+
+  mutable WrapList _wrapList;
   UiStyle::StyledMessage _styledMsg;
   UiStyle::StyledMessage _styledMsg;
+
+  static unsigned char *TextBoundaryFinderBuffer;
+  static int TextBoundaryFinderBufferSize;
 };
 
 #endif
 };
 
 #endif