}
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();
}
//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<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 {
/*************************************************************************************************/
+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;
+}
+
+/*************************************************************************************************/
+
virtual QVector<QTextLayout::FormatRange> 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;
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)
ChatLineModel::ChatLineModel(QObject *parent)
: MessageModel(parent)
{
-
+ qRegisterMetaType<WrapList>("ChatLineModel::WrapList");
+ qRegisterMetaTypeStreamOperators<WrapList>("ChatLineModel::WrapList");
}
// MessageModelItem *ChatLineModel::createMessageModelItem(const Message &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;
+}
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 &);
QList<ChatLineModelItem> _messageList;
};
+QDataStream &operator<<(QDataStream &out, const ChatLineModel::WrapList);
+QDataStream &operator>>(QDataStream &in, ChatLineModel::WrapList &);
+
+Q_DECLARE_METATYPE(ChatLineModel::WrapList)
+
#endif
* 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"
+// 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
// ****************************************
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::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);
+ }
+}
+
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;
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