From: Manuel Nickschas Date: Tue, 11 May 2010 20:56:02 +0000 (+0200) Subject: Cache ChatLine layout data for visible chatlines X-Git-Tag: 0.7-beta1~68 X-Git-Url: https://git.quassel-irc.org/?p=quassel.git;a=commitdiff_plain;h=77176474b628e801ba94d9d50d0d961a12aa01c4 Cache ChatLine layout data for visible chatlines Up to now, we've computed e.g. the text layout for a chatline on the fly. This happens surprisingly often, for example whenever the mouse is moved. As a result, moving the mouse (among other things) caused high CPU load. We now cache such data within the chatitems until the chatline goes out of view, in which case the cache is cleared in order to not waste space needlessly. Fixes #954. --- diff --git a/src/qtui/chatitem.cpp b/src/qtui/chatitem.cpp index 05f09443..bec1eb78 100644 --- a/src/qtui/chatitem.cpp +++ b/src/qtui/chatitem.cpp @@ -33,6 +33,7 @@ #include "chatitem.h" #include "chatline.h" #include "chatlinemodel.h" +#include "chatview.h" #include "contextmenuactionprovider.h" #include "iconloader.h" #include "mainwin.h" @@ -43,11 +44,16 @@ ChatItem::ChatItem(const QRectF &boundingRect, ChatLine *parent) : _parent(parent), _boundingRect(boundingRect), _selectionMode(NoSelection), - _selectionStart(-1) + _selectionStart(-1), + _cachedLayout(0) { } +ChatItem::~ChatItem() { + delete _cachedLayout; +} + ChatLine *ChatItem::chatLine() const { return _parent; } @@ -56,6 +62,10 @@ ChatScene *ChatItem::chatScene() const { return chatLine()->chatScene(); } +ChatView *ChatItem::chatView() const { + return chatScene()->chatView(); +} + const QAbstractItemModel *ChatItem::model() const { return chatLine()->model(); } @@ -90,20 +100,24 @@ QVariant ChatItem::data(int role) const { return model()->data(index, role); } -qint16 ChatItem::posToCursor(const QPointF &posInLine) const { - QPointF pos = mapFromLine(posInLine); - if(pos.y() > height()) return data(MessageModel::DisplayRole).toString().length(); - if(pos.y() < 0) return 0; +QTextLayout *ChatItem::layout() const { + if(_cachedLayout) + return _cachedLayout; - QTextLayout layout; - initLayout(&layout); - for(int l = layout.lineCount() - 1; l >= 0; l--) { - QTextLine line = layout.lineAt(l); - if(pos.y() >= line.y()) { - return line.xToCursor(pos.x(), QTextLine::CursorOnCharacter); - } - } - return 0; + _cachedLayout = new QTextLayout; + initLayout(_cachedLayout); + chatView()->setHasCache(chatLine()); + return _cachedLayout; +} + +void ChatItem::clearCache() { + delete _cachedLayout; + _cachedLayout = 0; +} + +void ChatItem::initLayout(QTextLayout *layout, QTextOption::WrapMode mode, Qt::Alignment alignment) const { + initLayoutHelper(layout, mode, alignment); + doLayout(layout); } void ChatItem::initLayoutHelper(QTextLayout *layout, QTextOption::WrapMode wrapMode, Qt::Alignment alignment) const { @@ -121,10 +135,6 @@ void ChatItem::initLayoutHelper(QTextLayout *layout, QTextOption::WrapMode wrapM layout->setAdditionalFormats(formatRanges); } -UiStyle::FormatList ChatItem::formatList() const { - return data(MessageModel::FormatRole).value(); -} - void ChatItem::doLayout(QTextLayout *layout) const { layout->beginLayout(); QTextLine line = layout->createLine(); @@ -135,6 +145,26 @@ void ChatItem::doLayout(QTextLayout *layout) const { layout->endLayout(); } +UiStyle::FormatList ChatItem::formatList() const { + return data(MessageModel::FormatRole).value(); +} + +qint16 ChatItem::posToCursor(const QPointF &posInLine) const { + QPointF pos = mapFromLine(posInLine); + if(pos.y() > height()) + return data(MessageModel::DisplayRole).toString().length(); + if(pos.y() < 0) + return 0; + + for(int l = layout()->lineCount() - 1; l >= 0; l--) { + QTextLine line = layout()->lineAt(l); + if(pos.y() >= line.y()) { + return line.xToCursor(pos.x(), QTextLine::CursorOnCharacter); + } + } + return 0; +} + void ChatItem::paintBackground(QPainter *painter) { QVariant bgBrush; if(_selectionMode == FullSelection) @@ -153,9 +183,7 @@ void ChatItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, painter->setClipRect(boundingRect()); paintBackground(painter); - QTextLayout layout; - initLayout(&layout); - layout.draw(painter, pos(), additionalFormats(), boundingRect()); + layout()->draw(painter, pos(), additionalFormats(), boundingRect()); // layout()->draw(painter, QPointF(0,0), formats, boundingRect()); @@ -311,10 +339,8 @@ QList ChatItem::findWords(const QString &searchWord, Qt::CaseSensitivity searchIdx = plainText.indexOf(searchWord, searchIdx + 1, caseSensitive); } - QTextLayout layout; - initLayout(&layout); foreach(int idx, indexList) { - QTextLine line = layout.lineForTextPosition(idx); + QTextLine line = layout()->lineForTextPosition(idx); qreal x = line.cursorToX(idx); qreal width = line.cursorToX(idx + searchWord.count()) - x; qreal height = line.height(); @@ -385,9 +411,7 @@ void SenderChatItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *op painter->setClipRect(boundingRect()); paintBackground(painter); - QTextLayout layout; - initLayout(&layout); - qreal layoutWidth = layout.minimumWidth(); + qreal layoutWidth = layout()->minimumWidth(); qreal offset = 0; if(chatScene()->senderCutoffMode() == ChatScene::CutoffLeft) offset = qMin(width() - layoutWidth, (qreal)0); @@ -397,10 +421,10 @@ void SenderChatItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *op if(layoutWidth > width()) { // Draw a nice gradient for longer items // Qt's text drawing with a gradient brush sucks, so we use an alpha-channeled pixmap instead - QPixmap pixmap(layout.boundingRect().toRect().size()); + QPixmap pixmap(layout()->boundingRect().toRect().size()); pixmap.fill(Qt::transparent); QPainter pixPainter(&pixmap); - layout.draw(&pixPainter, QPointF(qMax(offset, (qreal)0), 0), additionalFormats()); + layout()->draw(&pixPainter, QPointF(qMax(offset, (qreal)0), 0), additionalFormats()); pixPainter.end(); // Create alpha channel mask @@ -422,7 +446,7 @@ void SenderChatItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *op pixmap.setAlphaChannel(mask); painter->drawPixmap(pos(), pixmap); } else { - layout.draw(painter, pos(), additionalFormats(), boundingRect()); + layout()->draw(painter, pos(), additionalFormats(), boundingRect()); } painter->restore(); } @@ -461,6 +485,12 @@ ContentsChatItem::~ContentsChatItem() { delete _data; } +void ContentsChatItem::clearCache() { + delete _data; + _data = 0; + ChatItem::clearCache(); +} + ContentsChatItemPrivate *ContentsChatItem::privateData() const { if(!_data) { ContentsChatItem *that = const_cast(this); @@ -489,7 +519,6 @@ qreal ContentsChatItem::setGeometryByWidth(qreal w) { } void ContentsChatItem::doLayout(QTextLayout *layout) const { - // QString t = data(Qt::DisplayRole).toString(); bool d = t.contains("protien"); ChatLineModel::WrapList wrapList = data(ChatLineModel::WrapListRole).value(); if(!wrapList.count()) return; // empty chatitem @@ -677,9 +706,7 @@ void ContentsChatItem::showWebPreview(const Clickable &click) { #ifndef HAVE_WEBKIT Q_UNUSED(click); #else - QTextLayout layout; - initLayout(&layout); - QTextLine line = layout.lineForTextPosition(click.start()); + QTextLine line = layout()->lineForTextPosition(click.start()); qreal x = line.cursorToX(click.start()); qreal width = line.cursorToX(click.start() + click.length()) - x; qreal height = line.height(); diff --git a/src/qtui/chatitem.h b/src/qtui/chatitem.h index b6f4cf41..436f3d09 100644 --- a/src/qtui/chatitem.h +++ b/src/qtui/chatitem.h @@ -33,6 +33,7 @@ #include class ChatLine; +class ChatView; /* All external positions are relative to the parent ChatLine */ /* Yes, that's also true for the boundingRect() and related things */ @@ -41,12 +42,13 @@ class ChatItem { protected: // boundingRect is relative to the parent ChatLine ChatItem(const QRectF &boundingRect, ChatLine *parent); - virtual ~ChatItem() {} + virtual ~ChatItem(); public: const QAbstractItemModel *model() const; ChatLine *chatLine() const; ChatScene *chatScene() const; + ChatView *chatView() const; int row() const; virtual ChatLineModel::ColumnType column() const = 0; @@ -63,14 +65,6 @@ public: QPointF mapToScene(const QPointF &) const; QPointF mapFromScene(const QPointF &) const; - void initLayoutHelper(QTextLayout *layout, QTextOption::WrapMode, Qt::Alignment = Qt::AlignLeft) const; - virtual inline void initLayout(QTextLayout *layout) const { - initLayoutHelper(layout, QTextOption::NoWrap); - doLayout(layout); - } - virtual void doLayout(QTextLayout *) const; - virtual UiStyle::FormatList formatList() const; - virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); virtual inline int type() const { return ChatScene::ChatItemType; } @@ -89,6 +83,13 @@ public: virtual void addActionsToMenu(QMenu *menu, const QPointF &itemPos); virtual void handleClick(const QPointF &pos, ChatScene::ClickMode); + void initLayoutHelper(QTextLayout *layout, QTextOption::WrapMode, Qt::Alignment = Qt::AlignLeft) const; + + //! Remove internally cached data + /** This removes e.g. the cached QTextLayout to avoid wasting space for nonvisible ChatLines + */ + virtual void clearCache(); + protected: enum SelectionMode { NoSelection, @@ -103,6 +104,12 @@ protected: virtual void hoverLeaveEvent(QGraphicsSceneHoverEvent *) {}; virtual void hoverMoveEvent(QGraphicsSceneHoverEvent *) {}; + QTextLayout *layout() const; + virtual inline void initLayout(QTextLayout *layout) const { initLayout(layout, QTextOption::NoWrap); } + virtual void initLayout(QTextLayout *layout, QTextOption::WrapMode, Qt::Alignment alignment = Qt::AlignLeft) const; + virtual void doLayout(QTextLayout *) const; + virtual UiStyle::FormatList formatList() const; + void paintBackground(QPainter *); QVector selectionFormats() const; virtual QVector additionalFormats() const; @@ -130,6 +137,8 @@ private: SelectionMode _selectionMode; qint16 _selectionStart, _selectionEnd; + mutable QTextLayout *_cachedLayout; + // internal selection stuff void setSelection(int start, int length); @@ -161,10 +170,7 @@ public: protected: virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); virtual inline int type() const { return ChatScene::SenderChatItemType; } - virtual inline void initLayout(QTextLayout *layout) const { - initLayoutHelper(layout, QTextOption::ManualWrap, Qt::AlignRight); - doLayout(layout); - } + virtual inline void initLayout(QTextLayout *layout) const { ChatItem::initLayout(layout, QTextOption::ManualWrap, Qt::AlignRight); } }; // ************************************************************ @@ -185,6 +191,8 @@ public: inline ChatLineModel::ColumnType column() const { return ChatLineModel::ContentsColumn; } QFontMetricsF *fontMetrics() const; + virtual void clearCache(); + protected: virtual void mouseMoveEvent(QGraphicsSceneMouseEvent *event); virtual void hoverLeaveEvent(QGraphicsSceneHoverEvent *event); @@ -196,10 +204,7 @@ protected: virtual QVector additionalFormats() const; - virtual inline void initLayout(QTextLayout *layout) const { - initLayoutHelper(layout, QTextOption::WrapAtWordBoundaryOrAnywhere); - doLayout(layout); - } + virtual inline void initLayout(QTextLayout *layout) const { ChatItem::initLayout(layout, QTextOption::WrapAtWordBoundaryOrAnywhere); } virtual void doLayout(QTextLayout *layout) const; virtual UiStyle::FormatList formatList() const; @@ -207,7 +212,7 @@ private: class ActionProxy; class WrapColumnFinder; - ContentsChatItemPrivate *_data; + mutable ContentsChatItemPrivate *_data; ContentsChatItemPrivate *privateData() const; Clickable clickableAt(const QPointF &pos) const; @@ -217,13 +222,14 @@ private: void clearWebPreview(); qreal setGeometryByWidth(qreal w); - friend class ChatLine; - friend struct ContentsChatItemPrivate; QFontMetricsF *_fontMetrics; // we need a receiver for Action signals static ActionProxy _actionProxy; + + friend class ChatLine; + friend struct ContentsChatItemPrivate; }; struct ContentsChatItemPrivate { diff --git a/src/qtui/chatline.cpp b/src/qtui/chatline.cpp index 08d089ec..99e09829 100644 --- a/src/qtui/chatline.cpp +++ b/src/qtui/chatline.cpp @@ -59,6 +59,11 @@ ChatLine::ChatLine(int row, QAbstractItemModel *model, setHighlighted(index.data(MessageModel::FlagsRole).toInt() & Message::Highlight); } +ChatLine::~ChatLine() { + if(chatView()) + chatView()->setHasCache(this, false); +} + ChatItem *ChatLine::item(ChatLineModel::ColumnType column) { switch(column) { case ChatLineModel::TimestampColumn: @@ -82,6 +87,12 @@ ChatItem *ChatLine::itemAt(const QPointF &pos) { return 0; } +void ChatLine::clearCache() { + _timestampItem.clearCache(); + _senderItem.clearCache(); + _contentsItem.clearCache(); +} + void ChatLine::setMouseGrabberItem(ChatItem *item) { _mouseGrabberItem = item; } diff --git a/src/qtui/chatline.h b/src/qtui/chatline.h index a7c68e87..45365208 100644 --- a/src/qtui/chatline.h +++ b/src/qtui/chatline.h @@ -35,6 +35,8 @@ public: const QPointF &senderPos, const QPointF &contentsPos, QGraphicsItem *parent = 0); + virtual ~ChatLine(); + virtual inline QRectF boundingRect () const { return QRectF(0, 0, _width, _height); } inline QModelIndex index() const { return model()->index(row(), 0); } @@ -44,7 +46,7 @@ public: inline const QAbstractItemModel *model() const { return _model; } inline ChatScene *chatScene() const { return qobject_cast(scene()); } - inline ChatView *chatView() const { return chatScene()->chatView(); } + inline ChatView *chatView() const { return chatScene() ? chatScene()->chatView() : 0; } inline qreal width() const { return _width; } inline qreal height() const { return _height; } @@ -69,6 +71,8 @@ public: void setSelected(bool selected, ChatLineModel::ColumnType minColumn = ChatLineModel::ContentsColumn); void setHighlighted(bool highlighted); + void clearCache(); + protected: virtual bool sceneEvent(QEvent *event); diff --git a/src/qtui/chatview.cpp b/src/qtui/chatview.cpp index ddec8fbe..e61dc9e8 100644 --- a/src/qtui/chatview.cpp +++ b/src/qtui/chatview.cpp @@ -123,6 +123,8 @@ bool ChatView::event(QEvent *event) { void ChatView::resizeEvent(QResizeEvent *event) { QGraphicsView::resizeEvent(event); + // FIXME: do we really need to scroll down on resize? + // we can reduce viewport updates if we scroll to the bottom allready at the beginning verticalScrollBar()->setValue(verticalScrollBar()->maximum()); scene()->updateForViewport(viewport()->width(), viewport()->height()); @@ -130,6 +132,8 @@ void ChatView::resizeEvent(QResizeEvent *event) { _lastScrollbarPos = verticalScrollBar()->maximum(); verticalScrollBar()->setValue(verticalScrollBar()->maximum()); + + checkChatLineCaches(); } void ChatView::adjustSceneRect() { @@ -222,6 +226,7 @@ bool chatLinePtrLessThan(ChatLine *one, ChatLine *other) { return one->row() < other->row(); } +// TODO: figure out if it's cheaper to use a cached list (that we'd need to keep updated) QSet ChatView::visibleChatLines(Qt::ItemSelectionMode mode) const { QSet result; foreach(QGraphicsItem *item, items(viewport()->rect().adjusted(-1, -1, 1, 1), mode)) { @@ -338,3 +343,29 @@ void ChatView::invalidateFilter() { _invalidateFilter = true; } } + +void ChatView::scrollContentsBy(int dx, int dy) { + QGraphicsView::scrollContentsBy(dx, dy); + checkChatLineCaches(); +} + +void ChatView::setHasCache(ChatLine *line, bool hasCache) { + if(hasCache) + _linesWithCache.insert(line); + else + _linesWithCache.remove(line); +} + +void ChatView::checkChatLineCaches() { + qreal top = mapToScene(viewport()->rect().topLeft()).y() - 10; // some grace area to avoid premature cleaning + qreal bottom = mapToScene(viewport()->rect().bottomRight()).y() + 10; + QSet::iterator iter = _linesWithCache.begin(); + while(iter != _linesWithCache.end()) { + ChatLine *line = *iter; + if(line->pos().y() + line->height() < top || line->pos().y() > bottom) { + line->clearCache(); + iter = _linesWithCache.erase(iter); + } else + ++iter; + } +} diff --git a/src/qtui/chatview.h b/src/qtui/chatview.h index 3f9e29c9..02655675 100644 --- a/src/qtui/chatview.h +++ b/src/qtui/chatview.h @@ -69,11 +69,18 @@ public: virtual void addActionsToMenu(QMenu *, const QPointF &pos); - virtual bool event(QEvent *event); - inline bool isMarkerLineVisible() const { return _markerLineVisible; } inline ChatLine *markedLine() const { return _markedLine; } + //! Tell the view that this ChatLine has cached data + /** ChatLines cache some layout data that should be cleared as soon as it's no + * longer visible. A ChatLine caching data registers itself with this method to + * tell the view about it. The view will call ChatLine::clearCache() when + * appropriate. + * \param line The ChatLine having cached data + */ + void setHasCache(ChatLine *line, bool hasCache = true); + public slots: inline virtual void clear() {} void zoomIn(); @@ -84,7 +91,9 @@ public slots: void setMarkedLine(ChatLine *line); protected: + virtual bool event(QEvent *event); virtual void resizeEvent(QResizeEvent *event); + virtual void scrollContentsBy(int dx, int dy); protected slots: virtual void verticalScrollbarChanged(int); @@ -92,6 +101,7 @@ protected slots: private slots: void lastLineChanged(QGraphicsItem *chatLine, qreal offset); void adjustSceneRect(); + void checkChatLineCaches(); void mouseMoveWhileSelecting(const QPointF &scenePos); void scrollTimerTimeout(); void invalidateFilter(); @@ -109,6 +119,7 @@ private: bool _invalidateFilter; bool _markerLineVisible; ChatLine *_markedLine; + QSet _linesWithCache; };