From 176d22d49934223b9279719ac5d9e7c03e530d40 Mon Sep 17 00:00:00 2001 From: Marcus Eggenberger Date: Sun, 21 Sep 2008 13:13:45 +0200 Subject: [PATCH] Another Speed boost for the new ChatView. This commit is more then due but is still WIP. Heavy changes in ChatScene, ChatLine, ChatItems. Finally making use of the disposable layouts. --- src/qtui/chatitem.cpp | 217 ++++++++++++++++----------------- src/qtui/chatitem.h | 121 ++++++++++-------- src/qtui/chatline.cpp | 71 +++++++++-- src/qtui/chatline.h | 28 +++-- src/qtui/chatlinemodel.h | 1 + src/qtui/chatlinemodelitem.cpp | 1 + src/qtui/chatscene.cpp | 94 +++++++++----- src/qtui/chatscene.h | 10 +- src/qtui/chatview.cpp | 22 ++-- src/qtui/chatview.h | 2 +- 10 files changed, 350 insertions(+), 217 deletions(-) diff --git a/src/qtui/chatitem.cpp b/src/qtui/chatitem.cpp index 28b496ea..495a97ef 100644 --- a/src/qtui/chatitem.cpp +++ b/src/qtui/chatitem.cpp @@ -32,22 +32,24 @@ #include "qtui.h" #include "qtuistyle.h" -ChatItem::ChatItem(ChatLineModel::ColumnType col, QAbstractItemModel *model, QGraphicsItem *parent) +ChatItem::ChatItem(const qreal &width, const qreal &height, const QPointF &pos, ChatLineModel::ColumnType col, QGraphicsItem *parent) : QGraphicsItem(parent), + _data(0), + _boundingRect(0, 0, width, height), _fontMetrics(0), _selectionMode(NoSelection), - _selectionStart(-1), - _layout(0) + _selectionStart(-1) { - Q_ASSERT(model); - QModelIndex index = model->index(row(), col); - _fontMetrics = QtUi::style()->fontMetrics(model->data(index, ChatLineModel::FormatRole).value().at(0).second); + const QAbstractItemModel *model_ = model(); + QModelIndex index = model_->index(row(), col); + _fontMetrics = QtUi::style()->fontMetrics(model_->data(index, ChatLineModel::FormatRole).value().at(0).second); setAcceptHoverEvents(true); setZValue(20); + setPos(pos); } ChatItem::~ChatItem() { - delete _layout; + delete _data; } QVariant ChatItem::data(int role) const { @@ -59,20 +61,6 @@ QVariant ChatItem::data(int role) const { return model()->data(index, role); } -qreal ChatItem::setGeometry(qreal w, qreal h) { - if(w == _boundingRect.width()) return _boundingRect.height(); - prepareGeometryChange(); - _boundingRect.setWidth(w); - if(h < 0) h = computeHeight(); - _boundingRect.setHeight(h); - if(haveLayout()) updateLayout(); - return h; -} - -qreal ChatItem::computeHeight() { - return fontMetrics()->lineSpacing(); // only contents can be multi-line -} - QTextLayout *ChatItem::createLayout(QTextOption::WrapMode wrapMode, Qt::Alignment alignment) { QTextLayout *layout = new QTextLayout(data(MessageModel::DisplayRole).toString()); @@ -88,28 +76,30 @@ QTextLayout *ChatItem::createLayout(QTextOption::WrapMode wrapMode, Qt::Alignmen } void ChatItem::updateLayout() { - if(!haveLayout()) - setLayout(createLayout(QTextOption::WrapAnywhere, Qt::AlignLeft)); - - layout()->beginLayout(); - QTextLine line = layout()->createLine(); + if(!privateData()) { + setPrivateData(new ChatItemPrivate(createLayout())); + } + QTextLayout *layout_ = layout(); + layout_->beginLayout(); + QTextLine line = layout_->createLine(); if(line.isValid()) { line.setLineWidth(width()); line.setPosition(QPointF(0,0)); } - layout()->endLayout(); + layout_->endLayout(); } void ChatItem::clearLayout() { - delete _layout; - _layout = 0; + delete _data; + _data = 0; } // NOTE: This is not the most time-efficient implementation, but it saves space by not caching unnecessary data // This is a deliberate trade-off. (-> selectFmt creation, data() call) void ChatItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { Q_UNUSED(option); Q_UNUSED(widget); - if(!haveLayout()) updateLayout(); + if(!hasLayout()) + updateLayout(); painter->setClipRect(boundingRect()); // no idea why QGraphicsItem clipping won't work //if(_selectionMode == FullSelection) { //painter->save(); @@ -131,12 +121,29 @@ void ChatItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, formats.append(selectFmt); } layout()->draw(painter, QPointF(0,0), formats, boundingRect()); + + // Debuging Stuff + // uncomment the following lines to draw the bounding rect and the row number in alternating colors +// if(row() % 2) +// painter->setPen(Qt::red); +// else +// painter->setPen(Qt::blue); +// QString rowString = QString::number(row()); +// QRect rowRect = painter->fontMetrics().boundingRect(rowString); +// QPointF topPoint = _boundingRect.topLeft(); +// topPoint.ry() += rowRect.height(); +// painter->drawText(topPoint, rowString); +// QPointF bottomPoint = _boundingRect.bottomRight(); +// bottomPoint.rx() -= rowRect.width(); +// painter->drawText(bottomPoint, rowString); +// painter->drawRect(_boundingRect.adjusted(0, 0, -1, -1)); } qint16 ChatItem::posToCursor(const QPointF &pos) { if(pos.y() > height()) return data(MessageModel::DisplayRole).toString().length(); if(pos.y() < 0) return 0; - if(!haveLayout()) updateLayout(); + if(!hasLayout()) + updateLayout(); for(int l = layout()->lineCount() - 1; l >= 0; l--) { QTextLine line = layout()->lineAt(l); if(pos.y() >= line.y()) { @@ -178,7 +185,7 @@ QList ChatItem::findWords(const QString &searchWord, Qt::CaseSensitivity searchIdx = plainText.indexOf(searchWord, searchIdx + 1, caseSensitive); } - if(!haveLayout()) + if(!hasLayout()) updateLayout(); foreach(int idx, indexList) { @@ -235,51 +242,39 @@ void ChatItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { } } -/*************************************************************************************************/ - -/*************************************************************************************************/ - -void SenderChatItem::updateLayout() { - if(!haveLayout()) setLayout(createLayout(QTextOption::WrapAnywhere, Qt::AlignRight)); - ChatItem::updateLayout(); -} - -/*************************************************************************************************/ +// ************************************************************ +// SenderChatItem +// ************************************************************ -ContentsChatItem::ContentsChatItem(QAbstractItemModel *model, QGraphicsItem *parent) : ChatItem(column(), model, parent), - _layoutData(0) +// ************************************************************ +// ContentsChatItem +// ************************************************************ +ContentsChatItem::ContentsChatItem(const qreal &width, const QPointF &pos, QGraphicsItem *parent) + : ChatItem(0, 0, pos, column(), parent) { - -} - -ContentsChatItem::~ContentsChatItem() { - delete _layoutData; -} - -qreal ContentsChatItem::computeHeight() { - int lines = 1; - WrapColumnFinder finder(this); - while(finder.nextWrapColumn() > 0) lines++; - return lines * fontMetrics()->lineSpacing(); -} - -void ContentsChatItem::setLayout(QTextLayout *layout) { - if(!_layoutData) { - _layoutData = new LayoutData; - _layoutData->clickables = findClickables(); - } else { - delete _layoutData->layout; + setGeometryByWidth(width); +} + +qreal ContentsChatItem::setGeometryByWidth(qreal w) { + if(w != width()) { + setWidth(w); + // compute height + int lines = 1; + WrapColumnFinder finder(this); + while(finder.nextWrapColumn() > 0) + lines++; + setHeight(lines * fontMetrics()->lineSpacing()); } - _layoutData->layout = layout; -} - -void ContentsChatItem::clearLayout() { - delete _layoutData; - _layoutData = 0; + return height(); } void ContentsChatItem::updateLayout() { - if(!haveLayout()) setLayout(createLayout(QTextOption::WrapAnywhere)); + if(!privateData()) { + ContentsChatItemPrivate *data = new ContentsChatItemPrivate(createLayout(QTextOption::WrapAnywhere), + findClickables()); + // data->clickables = findClickables(); + setPrivateData(data); + } // Now layout ChatLineModel::WrapList wrapList = data(ChatLineModel::WrapListRole).value(); @@ -367,8 +362,8 @@ QList ContentsChatItem::findClickables() { QVector ContentsChatItem::additionalFormats() const { // mark a clickable if hovered upon QVector fmt; - if(layoutData()->currentClickable.isValid()) { - Clickable click = layoutData()->currentClickable; + if(privateData()->currentClickable.isValid()) { + Clickable click = privateData()->currentClickable; QTextLayout::FormatRange f; f.start = click.start; f.length = click.length; @@ -379,22 +374,22 @@ QVector ContentsChatItem::additionalFormats() const { } void ContentsChatItem::endHoverMode() { - if(layoutData()->currentClickable.isValid()) { + if(privateData()->currentClickable.isValid()) { setCursor(Qt::ArrowCursor); - layoutData()->currentClickable = Clickable(); + privateData()->currentClickable = Clickable(); update(); } } void ContentsChatItem::mousePressEvent(QGraphicsSceneMouseEvent *event) { - layoutData()->hasDragged = false; + privateData()->hasDragged = false; ChatItem::mousePressEvent(event); } void ContentsChatItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { - if(!event->buttons() && !layoutData()->hasDragged) { + if(!event->buttons() && !privateData()->hasDragged) { // got a click - Clickable click = layoutData()->currentClickable; + Clickable click = privateData()->currentClickable; if(click.isValid()) { QString str = data(ChatLineModel::DisplayRole).toString().mid(click.start, click.length); switch(click.type) { @@ -416,9 +411,9 @@ void ContentsChatItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { // mouse move events always mean we're not hovering anymore... endHoverMode(); // also, check if we have dragged the mouse - if(!layoutData()->hasDragged && event->buttons() & Qt::LeftButton + if(!privateData()->hasDragged && event->buttons() & Qt::LeftButton && (event->buttonDownScreenPos(Qt::LeftButton) - event->screenPos()).manhattanLength() >= QApplication::startDragDistance()) - layoutData()->hasDragged = true; + privateData()->hasDragged = true; ChatItem::mouseMoveEvent(event); } @@ -430,8 +425,8 @@ void ContentsChatItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event) { void ContentsChatItem::hoverMoveEvent(QGraphicsSceneHoverEvent *event) { bool onClickable = false; qint16 idx = posToCursor(event->pos()); - for(int i = 0; i < layoutData()->clickables.count(); i++) { - Clickable click = layoutData()->clickables.at(i); + for(int i = 0; i < privateData()->clickables.count(); i++) { + Clickable click = privateData()->clickables.at(i); if(idx >= click.start && idx < click.start + click.length) { if(click.type == Clickable::Url) onClickable = true; @@ -441,7 +436,7 @@ void ContentsChatItem::hoverMoveEvent(QGraphicsSceneHoverEvent *event) { } if(onClickable) { setCursor(Qt::PointingHandCursor); - layoutData()->currentClickable = click; + privateData()->currentClickable = click; update(); break; } @@ -458,9 +453,11 @@ ContentsChatItem::WrapColumnFinder::WrapColumnFinder(ChatItem *_item) layout(0), wrapList(item->data(ChatLineModel::WrapListRole).value()), wordidx(0), + lineCount(0), + choppedTrailing(0), lastwrapcol(0), lastwrappos(0), - w(0) + width(0) { } @@ -469,33 +466,35 @@ ContentsChatItem::WrapColumnFinder::~WrapColumnFinder() { } qint16 ContentsChatItem::WrapColumnFinder::nextWrapColumn() { - while(wordidx < wrapList.count()) { - w += wrapList.at(wordidx).width; - if(w >= item->width()) { - if(lastwrapcol >= wrapList.at(wordidx).start) { - // first word, and it doesn't fit - if(!line.isValid()) { - layout = item->createLayout(QTextOption::NoWrap); - layout->beginLayout(); - line = layout->createLine(); - line.setLineWidth(item->width()); - layout->endLayout(); - } - int idx = line.xToCursor(lastwrappos + item->width(), QTextLine::CursorOnCharacter); - qreal x = line.cursorToX(idx, QTextLine::Trailing); - w = w - wrapList.at(wordidx).width - (x - lastwrappos); - lastwrappos = x; - lastwrapcol = idx; - return idx; + 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; + + while(true) { + if(start == end) { + wordidx = start; + if(wordidx > 0) { + const ChatLineModel::Word &prevWord = wrapList.at(wordidx - 1); + choppedTrailing += prevWord.trailing - (targetWidth - prevWord.endX); } - // not the first word, so just wrap before this - lastwrapcol = wrapList.at(wordidx).start; - lastwrappos = lastwrappos + w - wrapList.at(wordidx).width; - w = 0; - return lastwrapcol; + return wrapList.at(wordidx).start; + } + qint16 pivot = (end + start) / 2; + if(wrapList.at(pivot).endX > targetWidth && wordidx != pivot) { + end = pivot; + } else { + start = pivot + 1; } - w += wrapList.at(wordidx).trailing; - wordidx++; } return -1; } + diff --git a/src/qtui/chatitem.h b/src/qtui/chatitem.h index a6eb531b..587cc609 100644 --- a/src/qtui/chatitem.h +++ b/src/qtui/chatitem.h @@ -30,15 +30,15 @@ #include "qtui.h" class QTextLayout; +struct ChatItemPrivate; class ChatItem : public QGraphicsItem { - protected: - ChatItem(ChatLineModel::ColumnType column, QAbstractItemModel *, QGraphicsItem *parent); + ChatItem(const qreal &width, const qreal &height, const QPointF &pos, ChatLineModel::ColumnType column, QGraphicsItem *parent); virtual ~ChatItem(); public: - inline const QAbstractItemModel *model() const { return chatScene() ? chatScene()->model() : 0; } + inline const QAbstractItemModel *model() const; inline int row() const; virtual ChatLineModel::ColumnType column() const = 0; inline ChatScene *chatScene() const { return qobject_cast(scene()); } @@ -48,16 +48,15 @@ public: inline qreal width() const { return _boundingRect.width(); } inline qreal height() const { return _boundingRect.height(); } - virtual inline bool haveLayout() const { return layout() != 0; } - virtual void clearLayout(); - virtual QTextLayout *createLayout(QTextOption::WrapMode, Qt::Alignment = Qt::AlignLeft); + inline bool hasLayout() const { return (bool)_data; } + QTextLayout *createLayout(QTextOption::WrapMode, Qt::Alignment = Qt::AlignLeft); + virtual inline QTextLayout *createLayout() { return createLayout(QTextOption::WrapAnywhere); } + void clearLayout(); + virtual void updateLayout(); virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); - virtual QVariant data(int role) const; - - // returns height - qreal setGeometry(qreal width, qreal height = -1); + QVariant data(int role) const; // selection stuff, to be called by the scene void clearSelection(); @@ -71,66 +70,85 @@ protected: virtual void mousePressEvent(QGraphicsSceneMouseEvent *event); virtual void mouseReleaseEvent(QGraphicsSceneMouseEvent *event); - virtual inline QTextLayout *layout() const { return _layout; } - virtual inline void setLayout(QTextLayout *l) { _layout = l; } + inline QTextLayout *layout() const; + virtual inline QVector additionalFormats() const { return QVector(); } qint16 posToCursor(const QPointF &pos); - virtual qreal computeHeight(); + inline void ChatItem::setPrivateData(ChatItemPrivate *data) { Q_ASSERT(!_data); _data = data; } + inline ChatItemPrivate *ChatItem::privateData() const; - QRectF _boundingRect; + // WARNING: setGeometry and setHeight should not be used without either: + // a) calling prepareGeometryChange() immediately before setColumns() + // b) calling Chatline::setPos() immediately afterwards + inline void setGeometry(qreal width, qreal height) { + _boundingRect.setWidth(width); + _boundingRect.setHeight(height); + } + inline void setHeight(const qreal &height) { _boundingRect.setHeight(height); } + inline void setWidth(const qreal &width) { _boundingRect.setWidth(width); } private: // internal selection stuff void setSelection(int start, int length); + ChatItemPrivate *_data; + QRectF _boundingRect; QFontMetricsF *_fontMetrics; enum SelectionMode { NoSelection, PartialSelection, FullSelection }; SelectionMode _selectionMode; qint16 _selectionStart, _selectionEnd; - QTextLayout *_layout; + friend class ChatLine; }; -/*************************************************************************************************/ +struct ChatItemPrivate { + QTextLayout *layout; + ChatItemPrivate(QTextLayout *l) : layout(l) {} + ~ChatItemPrivate() { + delete layout; + } +}; + +// inlines of ChatItem +QTextLayout *ChatItem::layout() const { return privateData()->layout; } +ChatItemPrivate *ChatItem::privateData() const { return _data; } + +// ************************************************************ +// TimestampChatItem +// ************************************************************ //! A ChatItem for the timestamp column class TimestampChatItem : public ChatItem { - public: - TimestampChatItem(QAbstractItemModel *model, QGraphicsItem *parent) : ChatItem(column(), model, parent) {} - inline ChatLineModel::ColumnType column() const { return ChatLineModel::TimestampColumn; } - + TimestampChatItem(const qreal &width, const qreal &height, QGraphicsItem *parent) : ChatItem(width, height, QPointF(0, 0), column(), parent) {} + virtual inline ChatLineModel::ColumnType column() const { return ChatLineModel::TimestampColumn; } }; -/*************************************************************************************************/ - +// ************************************************************ +// SenderChatItem +// ************************************************************ //! A ChatItem for the sender column class SenderChatItem : public ChatItem { - public: - SenderChatItem(QAbstractItemModel *model, QGraphicsItem *parent) : ChatItem(column(), model, parent) {} - inline ChatLineModel::ColumnType column() const { return ChatLineModel::SenderColumn; } - - virtual void updateLayout(); + SenderChatItem(const qreal &width, const qreal &height, const QPointF &pos, QGraphicsItem *parent) : ChatItem(width, height, pos, column(), parent) {} + virtual inline ChatLineModel::ColumnType column() const { return ChatLineModel::SenderColumn; } + virtual inline QTextLayout *createLayout() { return ChatItem::createLayout(QTextOption::WrapAnywhere, Qt::AlignRight); } }; -/*************************************************************************************************/ +// ************************************************************ +// ContentsChatItem +// ************************************************************ +struct ContentsChatItemPrivate; //! A ChatItem for the contents column class ContentsChatItem : public ChatItem { - public: - ContentsChatItem(QAbstractItemModel *model, QGraphicsItem *parent); - virtual ~ContentsChatItem(); + ContentsChatItem(const qreal &width, const QPointF &pos, QGraphicsItem *parent); inline ChatLineModel::ColumnType column() const { return ChatLineModel::ContentsColumn; } - virtual void clearLayout(); - virtual void updateLayout(); - virtual inline bool haveLayout() const { return _layoutData != 0 && layout() != 0; } - protected: virtual void mouseMoveEvent(QGraphicsSceneMouseEvent *event); virtual void mousePressEvent(QGraphicsSceneMouseEvent *event); @@ -138,22 +156,25 @@ protected: virtual void hoverLeaveEvent(QGraphicsSceneHoverEvent *event); virtual void hoverMoveEvent(QGraphicsSceneHoverEvent *event); - virtual inline QTextLayout *layout() const; - virtual void setLayout(QTextLayout *l); virtual QVector additionalFormats() const; + virtual void updateLayout(); + private: - struct LayoutData; struct Clickable; class WrapColumnFinder; - inline LayoutData *layoutData() const; + inline ContentsChatItemPrivate *privateData() const; - qreal computeHeight(); QList findClickables(); void endHoverMode(); - LayoutData *_layoutData; + // WARNING: setGeometry and setHeight should not be used without either: + // a) calling prepareGeometryChange() immediately before setColumns() + // b) calling Chatline::setPos() immediately afterwards + qreal setGeometryByWidth(qreal w); + friend class ChatLine; + friend struct ContentsChatItemPrivate; }; struct ContentsChatItem::Clickable { @@ -174,14 +195,12 @@ struct ContentsChatItem::Clickable { inline bool isValid() const { return type != Invalid; } }; -struct ContentsChatItem::LayoutData { - QTextLayout *layout; - QList clickables; - Clickable currentClickable; +struct ContentsChatItemPrivate : ChatItemPrivate { + QList clickables; + ContentsChatItem::Clickable currentClickable; bool hasDragged; - LayoutData() { layout = 0; hasDragged = false; } - ~LayoutData() { delete layout; } + ContentsChatItemPrivate(QTextLayout *l, const QList &c) : ChatItemPrivate(l), clickables(c), hasDragged(false) {} }; class ContentsChatItem::WrapColumnFinder { @@ -197,18 +216,20 @@ private: QTextLine line; ChatLineModel::WrapList wrapList; qint16 wordidx; + qint16 lineCount; + qreal choppedTrailing; qint16 lastwrapcol; qreal lastwrappos; - qreal w; + qreal width; }; /*************************************************************************************************/ // Avoid circular include deps #include "chatline.h" +const QAbstractItemModel *ChatItem::model() const { return static_cast(parentItem())->model(); } int ChatItem::row() const { return static_cast(parentItem())->row(); } -QTextLayout *ContentsChatItem::layout() const { return _layoutData->layout; } -ContentsChatItem::LayoutData *ContentsChatItem::layoutData() const { Q_ASSERT(_layoutData); return _layoutData; } +ContentsChatItemPrivate *ContentsChatItem::privateData() const { return (ContentsChatItemPrivate *)privateData(); } #endif diff --git a/src/qtui/chatline.cpp b/src/qtui/chatline.cpp index 1a8501c7..df218a51 100644 --- a/src/qtui/chatline.cpp +++ b/src/qtui/chatline.cpp @@ -34,14 +34,35 @@ #include "qtuisettings.h" #include "qtuistyle.h" -ChatLine::ChatLine(int row, QAbstractItemModel *model, QGraphicsItem *parent) +// ChatLine::ChatLine(int row, QAbstractItemModel *model, QGraphicsItem *parent) +// : QGraphicsItem(parent), +// _row(row), // needs to be set before the items +// _model(model), +// _contentsItem(this), +// _senderItem(this), +// _timestampItem(this), +// _width(0), +// _height(0), +// _selection(0) +// { +// Q_ASSERT(model); +// QModelIndex index = model->index(row, ChatLineModel::ContentsColumn); +// setHighlighted(model->data(index, MessageModel::FlagsRole).toInt() & Message::Highlight); +// } + +ChatLine::ChatLine(int row, QAbstractItemModel *model, + const qreal &width, + const qreal ×tampWidth, const qreal &senderWidth, const qreal &contentsWidth, + const QPointF &senderPos, const QPointF &contentsPos, + QGraphicsItem *parent) : QGraphicsItem(parent), _row(row), // needs to be set before the items - _timestampItem(model, this), - _senderItem(model, this), - _contentsItem(model, this), - _width(0), - _height(0), + _model(model), + _contentsItem(contentsWidth, contentsPos, this), + _senderItem(senderWidth, _contentsItem.height(), senderPos, this), + _timestampItem(timestampWidth, _contentsItem.height(), this), + _width(width), + _height(_contentsItem.height()), _selection(0) { Q_ASSERT(model); @@ -67,13 +88,47 @@ ChatItem &ChatLine::item(ChatLineModel::ColumnType column) { } } -qreal ChatLine::setGeometry(qreal width) { +// WARNING: setColumns should not be used without either: +// a) calling prepareGeometryChange() immediately before setColumns() +// b) calling Chatline::setPos() immediately afterwards +// +// NOTE: senderPos and contentsPos are in ChatLines coordinate system! +qreal ChatLine::setColumns(const qreal ×tampWidth, const qreal &senderWidth, const qreal &contentsWidth, + const QPointF &senderPos, const QPointF &contentsPos) { + _height = _contentsItem.setGeometryByWidth(contentsWidth); + _senderItem.setGeometry(senderWidth, _height); + _timestampItem.setGeometry(timestampWidth, _height); + + _senderItem.setPos(senderPos); + _contentsItem.setPos(contentsPos); + + _contentsItem.clearLayout(); + _senderItem.clearLayout(); + _timestampItem.clearLayout(); + + return _height; +} + +// WARNING: setGeometryByWidth should not be used without either: +// a) calling prepareGeometryChange() immediately before setColumns() +// b) calling Chatline::setPos() immediately afterwards +qreal ChatLine::setGeometryByWidth(const qreal &width, const qreal &contentsWidth) { + _width = width; + _height = _contentsItem.setGeometryByWidth(contentsWidth); + _timestampItem.setHeight(_height); + _senderItem.setHeight(_height); + _contentsItem.clearLayout(); + return _height; +} + +qreal ChatLine::setGeometryByWidth(qreal width) { if(width != _width) prepareGeometryChange(); ColumnHandleItem *firstColumnHandle = chatScene()->firstColumnHandle(); ColumnHandleItem *secondColumnHandle = chatScene()->secondColumnHandle(); - _height = _contentsItem.setGeometry(width - secondColumnHandle->sceneRight()); + + _height = _contentsItem.setGeometryByWidth(width - secondColumnHandle->sceneRight()); _timestampItem.setGeometry(firstColumnHandle->sceneLeft(), _height); _senderItem.setGeometry(secondColumnHandle->sceneLeft() - firstColumnHandle->sceneRight(), _height); diff --git a/src/qtui/chatline.h b/src/qtui/chatline.h index d69caa5b..13f45cec 100644 --- a/src/qtui/chatline.h +++ b/src/qtui/chatline.h @@ -27,38 +27,50 @@ #include "chatitem.h" class ChatLine : public QGraphicsItem { - public: - ChatLine(int row, QAbstractItemModel *model, QGraphicsItem *parent = 0); +// ChatLine(int row, QAbstractItemModel *model, QGraphicsItem *parent = 0); + ChatLine(int row, QAbstractItemModel *model, + const qreal &width, + const qreal ×tampWidth, const qreal &senderWidth, const qreal &contentsWidth, + const QPointF &senderPos, const QPointF &contentsPos, + QGraphicsItem *parent = 0); virtual QRectF boundingRect () const; inline int row() { return _row; } inline void setRow(int row) { _row = row; } - inline const QAbstractItemModel *model() const { return chatScene() ? chatScene()->model() : 0; } + inline const QAbstractItemModel *model() const { return _model; } inline ChatScene *chatScene() const { return qobject_cast(scene()); } inline qreal width() const { return _width; } inline qreal height() const { return _height; } ChatItem &item(ChatLineModel::ColumnType); + inline ChatItem ×tampItem() { return _timestampItem; } + inline ChatItem &senderItem() { return _senderItem; } + inline ContentsChatItem &contentsItem() { return _contentsItem; } virtual void paint (QPainter * painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); // returns height - qreal setGeometry(qreal width); + qreal setGeometryByWidth(qreal width); void setSelected(bool selected, ChatLineModel::ColumnType minColumn = ChatLineModel::ContentsColumn); void setHighlighted(bool highlighted); -protected: - private: int _row; - TimestampChatItem _timestampItem; - SenderChatItem _senderItem; + QAbstractItemModel *_model; ContentsChatItem _contentsItem; + SenderChatItem _senderItem; + TimestampChatItem _timestampItem; qreal _width, _height; enum { Selected = 0x40, Highlighted = 0x80 }; quint8 _selection; // save space, so we put both the col and the flags into one byte + + // setColumns and setGeometryByWidth both return height + qreal setColumns(const qreal ×tampWidth, const qreal &senderWidth, const qreal &contentsWidth, + const QPointF &senderPos, const QPointF &contentsPos); + qreal setGeometryByWidth(const qreal &width, const qreal &contentsWidth); + friend class ChatScene; }; #endif diff --git a/src/qtui/chatlinemodel.h b/src/qtui/chatlinemodel.h index 4604a9b7..4f64efe1 100644 --- a/src/qtui/chatlinemodel.h +++ b/src/qtui/chatlinemodel.h @@ -36,6 +36,7 @@ public: /// Used to store information about words to be used for wrapping struct Word { quint16 start; + qreal endX; qreal width; qreal trailing; }; diff --git a/src/qtui/chatlinemodelitem.cpp b/src/qtui/chatlinemodelitem.cpp index 24e2273d..1e12cd1a 100644 --- a/src/qtui/chatlinemodelitem.cpp +++ b/src/qtui/chatlinemodelitem.cpp @@ -156,6 +156,7 @@ private: } qreal wordendx = line.cursorToX(oldidx); qreal trailingendx = line.cursorToX(idx); + word.endX = wordendx; word.width = wordendx - wordstartx; word.trailing = trailingendx - wordendx; wordstartx = trailingendx; diff --git a/src/qtui/chatscene.cpp b/src/qtui/chatscene.cpp index c3a07b15..ed8e68d2 100644 --- a/src/qtui/chatscene.cpp +++ b/src/qtui/chatscene.cpp @@ -43,6 +43,7 @@ ChatScene::ChatScene(QAbstractItemModel *model, const QString &idString, qreal w _model(model), _singleBufferScene(false), _sceneRect(0, 0, width, 0), + _viewportHeight(0), _selectingItem(0), _selectionStart(-1), _isSelecting(false), @@ -95,7 +96,6 @@ void ChatScene::rowsInserted(const QModelIndex &index, int start, int end) { bool atTop = true; bool atBottom = false; bool moveTop = false; - bool hasWidth = (width != 0); if(start > 0) { y = _lines.value(start - 1)->y() + _lines.value(start - 1)->height(); @@ -104,19 +104,27 @@ void ChatScene::rowsInserted(const QModelIndex &index, int start, int end) { if(start == _lines.count()) atBottom = true; + qreal contentsWidth = width - secondColumnHandle()->sceneRight(); + qreal senderWidth = secondColumnHandle()->sceneLeft() - firstColumnHandle()->sceneRight(); + qreal timestampWidth = firstColumnHandle()->sceneLeft(); + QPointF contentsPos(secondColumnHandle()->sceneRight(), 0); + QPointF senderPos(firstColumnHandle()->sceneRight(), 0); + + for(int i = end; i >= start; i--) { - ChatLine *line = new ChatLine(i, model()); + ChatLine *line = new ChatLine(i, model(), + width, + timestampWidth, senderWidth, contentsWidth, + senderPos, contentsPos); + if(atTop) { + h -= line->height(); + line->setPos(0, y+h); + } else { + line->setPos(0, y+h); + h += line->height(); + } _lines.insert(start, line); addItem(line); - if(hasWidth) { - if(atTop) { - h -= line->setGeometry(width); - line->setPos(0, y+h); - } else { - line->setPos(0, y+h); - h += line->setGeometry(width); - } - } } // update existing items @@ -159,7 +167,7 @@ void ChatScene::rowsInserted(const QModelIndex &index, int start, int end) { updateSceneRect(_sceneRect.adjusted(0, h, 0, 0)); } else { updateSceneRect(_sceneRect.adjusted(0, 0, 0, h)); - emit sceneHeightChanged(h); + emit lastLineChanged(_lines.last()); } } @@ -230,31 +238,57 @@ void ChatScene::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int e } } +void ChatScene::updateForViewport(qreal width, qreal height) { + _viewportHeight = height; + setWidth(width); +} + +// setWidth is used for 2 things: +// a) updating the scene to fit the width of the corresponding view +// b) to update the positions of the items if a columhandle has changed it's position +// forceReposition is true in the second case +// this method features some codeduplication for the sake of performance void ChatScene::setWidth(qreal width, bool forceReposition) { if(width == _sceneRect.width() && !forceReposition) return; - // clock_t startT = clock(); - qreal oldHeight = _sceneRect.height(); - qreal y = _sceneRect.y(); - qreal linePos = y; - - foreach(ChatLine *line, _lines) { - line->setPos(0, linePos); - linePos += line->setGeometry(width); +// clock_t startT = clock(); + + qreal linePos = _sceneRect.y() + _sceneRect.height(); + qreal yBottom = linePos; + QList::iterator lineIter = _lines.end(); + QList::iterator lineIterBegin = _lines.begin(); + ChatLine *line = 0; + qreal lineHeight = 0; + qreal contentsWidth = width - secondColumnHandle()->sceneRight(); + + if(forceReposition) { + qreal timestampWidth = firstColumnHandle()->sceneLeft(); + qreal senderWidth = secondColumnHandle()->sceneLeft() - firstColumnHandle()->sceneRight(); + QPointF senderPos(firstColumnHandle()->sceneRight(), 0); + QPointF contentsPos(secondColumnHandle()->sceneRight(), 0); + while(lineIter != lineIterBegin) { + lineIter--; + line = *lineIter; + lineHeight = line->setColumns(timestampWidth, senderWidth, contentsWidth, senderPos, contentsPos); + linePos -= lineHeight; + line->setPos(0, linePos); + } + } else { + while(lineIter != lineIterBegin) { + lineIter--; + line = *lineIter; + lineHeight = line->setGeometryByWidth(width, contentsWidth); + linePos -= lineHeight; + line->setPos(0, linePos); + } } - qreal height = linePos - y; - - updateSceneRect(QRectF(0, y, width, height)); + updateSceneRect(QRectF(0, linePos, width, yBottom - linePos)); setHandleXLimits(); - qreal dh = height - oldHeight; - if(dh > 0) - emit sceneHeightChanged(dh); - - // clock_t endT = clock(); - // qDebug() << "resized" << _lines.count() << "in" << (float)(endT - startT) / CLOCKS_PER_SEC << "sec"; +// clock_t endT = clock(); +// qDebug() << "resized" << _lines.count() << "in" << (float)(endT - startT) / CLOCKS_PER_SEC << "sec"; } void ChatScene::handlePositionChanged(qreal xpos) { @@ -416,7 +450,7 @@ QString ChatScene::selectionToString() const { } void ChatScene::requestBacklog() { - static const int REQUEST_COUNT = 100; + static const int REQUEST_COUNT = 500; int backlogSize = model()->rowCount(); if(isSingleBufferScene() && backlogSize != 0 && _lastBacklogSize + REQUEST_COUNT <= backlogSize) { QModelIndex msgIdx = model()->index(0, 0); diff --git a/src/qtui/chatscene.h b/src/qtui/chatscene.h index fa441023..e0aaa500 100644 --- a/src/qtui/chatscene.h +++ b/src/qtui/chatscene.h @@ -54,6 +54,7 @@ public: inline ColumnHandleItem *secondColumnHandle() const { return secondColHandle; } public slots: + void updateForViewport(qreal width, qreal height); void setWidth(qreal, bool forceReposition = false); // these are used by the chatitems to notify the scene and manage selections @@ -65,7 +66,7 @@ public slots: void requestBacklog(); signals: - void sceneHeightChanged(qreal dh); + void lastLineChanged(QGraphicsItem *); protected: virtual void mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent); @@ -93,6 +94,7 @@ private: // we store the size in a member variable. QRectF _sceneRect; void updateSceneRect(const QRectF &rect); + qreal _viewportHeight; ColumnHandleItem *firstColHandle, *secondColHandle; qreal firstColHandlePos, secondColHandlePos; @@ -108,7 +110,11 @@ private: }; bool ChatScene::containsBuffer(const BufferId &id) const { - return qobject_cast(model()) ? qobject_cast(model())->containsBuffer(id) : false; + MessageFilter *filter = qobject_cast(model()); + if(filter) + return filter->containsBuffer(id); + else + return false; } #endif diff --git a/src/qtui/chatview.cpp b/src/qtui/chatview.cpp index 6344c9c2..6b2d1f59 100644 --- a/src/qtui/chatview.cpp +++ b/src/qtui/chatview.cpp @@ -47,29 +47,33 @@ ChatView::ChatView(MessageFilter *filter, QWidget *parent) void ChatView::init(MessageFilter *filter) { setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - setAlignment(Qt::AlignBottom); + // setAlignment(Qt::AlignBottom); setInteractive(true); + setOptimizationFlags(QGraphicsView::DontClipPainter | QGraphicsView::DontAdjustForAntialiasing); + setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate); + setTransformationAnchor(QGraphicsView::NoAnchor); _scene = new ChatScene(filter, filter->idString(), viewport()->width() - 2, this); // see below: resizeEvent() - connect(_scene, SIGNAL(sceneHeightChanged(qreal)), this, SLOT(sceneHeightChanged(qreal))); connect(_scene, SIGNAL(sceneRectChanged(const QRectF &)), this, SLOT(sceneRectChanged(const QRectF &))); + //connect(_scene, SIGNAL(lastLineChanged(QGraphicsItem *)), this, SLOT(lastLineChanged(QGraphicsItem *))); setScene(_scene); - _lastScrollbarPos = verticalScrollBar()->maximum(); connect(verticalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(verticalScrollbarChanged(int))); } void ChatView::resizeEvent(QResizeEvent *event) { QGraphicsView::resizeEvent(event); - scene()->setWidth(viewport()->width() - 2); // FIXME figure out why we have to hardcode the -2 here -> Qt-Bug most probably + + // FIXME: without the hardcoded -2 Qt reserves space for a horizontal scrollbar even though it's disabled permanently. + // this does only occur on QtX11 (at least not on Qt for Mac OS). Seems like a Qt Bug. + scene()->updateForViewport(viewport()->width() - 2, viewport()->height()); + _lastScrollbarPos = verticalScrollBar()->maximum(); verticalScrollBar()->setValue(verticalScrollBar()->maximum()); } -void ChatView::sceneHeightChanged(qreal dh) { - QAbstractSlider *vbar = verticalScrollBar(); - Q_ASSERT(vbar); - if(vbar->maximum() - vbar->value() <= dh + 5) // in case we had scrolled only about half a line to the bottom we allow a grace of 5 - vbar->setValue(vbar->maximum()); +void ChatView::lastLineChanged(QGraphicsItem *chatLine) { + // FIXME: if vbar offset < chatline height -> centerOn(chatLine); + centerOn(chatLine); } void ChatView::verticalScrollbarChanged(int newPos) { diff --git a/src/qtui/chatview.h b/src/qtui/chatview.h index 5341109f..eab096b9 100644 --- a/src/qtui/chatview.h +++ b/src/qtui/chatview.h @@ -48,7 +48,7 @@ protected: virtual void resizeEvent(QResizeEvent *event); protected slots: - virtual void sceneHeightChanged(qreal dh); + void lastLineChanged(QGraphicsItem *chatLine); virtual inline void sceneRectChanged(const QRectF &rect) { setSceneRect(rect); } virtual void verticalScrollbarChanged(int); -- 2.20.1