X-Git-Url: https://git.quassel-irc.org/?a=blobdiff_plain;f=src%2Fqtui%2Fchatitem.cpp;h=1f4897a7ea70330c04361738baeae742f97dd403;hb=edc55b1f86cf613a332eeeb5d85537a54120dfa5;hp=f09553b565a9c0447d9675be3ed8faf4b94f8c09;hpb=4256ac4821446383da9a9dcda3532fbfa3eb6beb;p=quassel.git diff --git a/src/qtui/chatitem.cpp b/src/qtui/chatitem.cpp index f09553b5..1f4897a7 100644 --- a/src/qtui/chatitem.cpp +++ b/src/qtui/chatitem.cpp @@ -1,5 +1,5 @@ /*************************************************************************** - * Copyright (C) 2005-08 by the Quassel Project * + * Copyright (C) 2005-2019 by the Quassel Project * * devel@quassel-irc.org * * * * This program is free software; you can redistribute it and/or modify * @@ -15,306 +15,939 @@ * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * - * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ +#include "chatitem.h" + +#include +#include + #include #include #include #include #include +#include #include #include #include -#include "chatitem.h" +#include "action.h" +#include "buffermodel.h" +#include "bufferview.h" +#include "chatline.h" #include "chatlinemodel.h" +#include "chatview.h" +#include "contextmenuactionprovider.h" +#include "icon.h" +#include "mainwin.h" #include "qtui.h" +#include "qtuistyle.h" + +ChatItem::ChatItem(const QRectF& boundingRect, ChatLine* parent) + : _parent(parent) + , _boundingRect(boundingRect) + , _selectionMode(NoSelection) + , _selectionStart(-1) + , _cachedLayout(nullptr) +{} + +ChatItem::~ChatItem() +{ + delete _cachedLayout; +} + +ChatLine* ChatItem::chatLine() const +{ + return _parent; +} + +ChatScene* ChatItem::chatScene() const +{ + return chatLine()->chatScene(); +} + +ChatView* ChatItem::chatView() const +{ + return chatScene()->chatView(); +} + +const QAbstractItemModel* ChatItem::model() const +{ + return chatLine()->model(); +} + +int ChatItem::row() const +{ + return chatLine()->row(); +} + +QPointF ChatItem::mapToLine(const QPointF& p) const +{ + return p + pos(); +} + +QPointF ChatItem::mapFromLine(const QPointF& p) const +{ + return p - pos(); +} + +// relative to the ChatLine +QPointF ChatItem::mapToScene(const QPointF& p) const +{ + return chatLine()->mapToScene(p /* + pos() */); +} + +QPointF ChatItem::mapFromScene(const QPointF& p) const +{ + return chatLine()->mapFromScene(p) /* - pos() */; +} + +QVariant ChatItem::data(int role) const +{ + QModelIndex index = model()->index(row(), column()); + if (!index.isValid()) { + qWarning() << "ChatItem::data(): model index is invalid!" << index; + return QVariant(); + } + return model()->data(index, role); +} + +QTextLayout* ChatItem::layout() const +{ + if (_cachedLayout) + return _cachedLayout; + + _cachedLayout = new QTextLayout; + initLayout(_cachedLayout); + chatView()->setHasCache(chatLine()); + return _cachedLayout; +} + +void ChatItem::clearCache() +{ + delete _cachedLayout; + _cachedLayout = nullptr; +} + +void ChatItem::initLayoutHelper(QTextLayout* layout, QTextOption::WrapMode wrapMode, Qt::Alignment alignment) const +{ + Q_ASSERT(layout); + + layout->setText(data(MessageModel::DisplayRole).toString()); + + QTextOption option; + option.setWrapMode(wrapMode); + option.setAlignment(alignment); + layout->setTextOption(option); + + QList formatRanges = QtUi::style() + ->toTextLayoutList(formatList(), + layout->text().length(), + data(ChatLineModel::MsgLabelRole).value()); + layout->setAdditionalFormats(formatRanges); +} + +void ChatItem::initLayout(QTextLayout* layout) const +{ + initLayoutHelper(layout, QTextOption::NoWrap); + doLayout(layout); +} -ChatItem::ChatItem(const QPersistentModelIndex &index_, QGraphicsItem *parent) : QGraphicsItem(parent), _index(index_) { - _fontMetrics = QtUi::style()->fontMetrics(data(ChatLineModel::FormatRole).value().at(0).second); - _layout = 0; - _lines = 0; - _selectionStart = -1; - _selectionMode = NoSelection; - setAcceptHoverEvents(true); - setZValue(20); -} - -ChatItem::~ChatItem() { - delete _layout; -} - -QVariant ChatItem::data(int role) const { - if(!_index.isValid()) { - qWarning() << "ChatItem::data(): Model index is invalid!" << _index; - return QVariant(); - } - return _index.data(role); -} - -qreal ChatItem::setWidth(qreal w) { - if(w == _boundingRect.width()) return _boundingRect.height(); - prepareGeometryChange(); - _boundingRect.setWidth(w); - qreal h = computeHeight(); - _boundingRect.setHeight(h); - if(haveLayout()) updateLayout(); - return h; -} - -qreal ChatItem::computeHeight() { - if(data(ChatLineModel::ColumnTypeRole).toUInt() != ChatLineModel::ContentsColumn) - return fontMetrics()->lineSpacing(); // only contents can be multi-line - - _lines = 1; - WrapColumnFinder finder(this); - while(finder.nextWrapColumn() > 0) _lines++; - return _lines * fontMetrics()->lineSpacing(); -} - -QTextLayout *ChatItem::createLayout(QTextOption::WrapMode wrapMode, Qt::Alignment alignment) { - QTextLayout *layout = new QTextLayout(data(MessageModel::DisplayRole).toString()); - - QTextOption option; - option.setWrapMode(wrapMode); - option.setAlignment(alignment); - layout->setTextOption(option); - - QList formatRanges - = QtUi::style()->toTextLayoutList(data(MessageModel::FormatRole).value(), layout->text().length()); - layout->setAdditionalFormats(formatRanges); - return layout; -} - -void ChatItem::updateLayout() { - switch(data(ChatLineModel::ColumnTypeRole).toUInt()) { - case ChatLineModel::TimestampColumn: - if(!haveLayout()) _layout = createLayout(QTextOption::WrapAnywhere, Qt::AlignLeft); - // fallthrough - case ChatLineModel::SenderColumn: - if(!haveLayout()) _layout = createLayout(QTextOption::WrapAnywhere, Qt::AlignRight); - _layout->beginLayout(); - { - QTextLine line = _layout->createLine(); - if(line.isValid()) { - line.setLineWidth(width()); - line.setPosition(QPointF(0,0)); +void ChatItem::doLayout(QTextLayout* layout) const +{ + layout->beginLayout(); + QTextLine line = layout->createLine(); + if (line.isValid()) { + line.setLineWidth(width()); + line.setPosition(QPointF(0, 0)); + } + 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); } - _layout->endLayout(); - } - break; - case ChatLineModel::ContentsColumn: { - if(!haveLayout()) _layout = createLayout(QTextOption::WrapAnywhere); - - // Now layout - 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 += line.height() + fontMetrics()->leading(); - } - _layout->endLayout(); } - break; - } + return 0; } -void ChatItem::clearLayout() { - delete _layout; - _layout = 0; +void ChatItem::paintBackground(QPainter* painter) +{ + QVariant bgBrush; + if (_selectionMode == FullSelection) + bgBrush = data(ChatLineModel::SelectedBackgroundRole); + else + bgBrush = data(ChatLineModel::BackgroundRole); + if (bgBrush.isValid()) + painter->fillRect(boundingRect(), bgBrush.value()); } // 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(); - painter->setClipRect(boundingRect()); // no idea why QGraphicsItem clipping won't work - //if(_selectionMode == FullSelection) { - //painter->save(); - //painter->fillRect(boundingRect(), QApplication::palette().brush(QPalette::Highlight)); - //painter->restore(); - //} - QVector formats; - if(_selectionMode != NoSelection) { - QTextLayout::FormatRange selectFmt; - selectFmt.format.setForeground(QApplication::palette().brush(QPalette::HighlightedText)); - selectFmt.format.setBackground(QApplication::palette().brush(QPalette::Highlight)); - if(_selectionMode == PartialSelection) { - selectFmt.start = qMin(_selectionStart, _selectionEnd); - selectFmt.length = qAbs(_selectionStart - _selectionEnd); - } else { // FullSelection - selectFmt.start = 0; - selectFmt.length = data(MessageModel::DisplayRole).toString().length(); - } - formats.append(selectFmt); - } - _layout->draw(painter, QPointF(0,0), formats, boundingRect()); -} - -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(); - 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::setFullSelection() { - if(_selectionMode != FullSelection) { - _selectionMode = FullSelection; - update(); - } -} - -void ChatItem::clearSelection() { - if(_selectionMode != NoSelection) { - _selectionMode = NoSelection; - update(); - } -} - -void ChatItem::continueSelecting(const QPointF &pos) { - _selectionMode = PartialSelection; - _selectionEnd = posToCursor(pos); - update(); -} - -void ChatItem::mousePressEvent(QGraphicsSceneMouseEvent *event) { - if(event->buttons() & Qt::LeftButton) { - if(_selectionMode == NoSelection) { - chatScene()->setSelectingItem(this); // removes earlier selection if exists - _selectionStart = _selectionEnd = posToCursor(event->pos()); - _selectionMode = PartialSelection; - } else { - chatScene()->setSelectingItem(0); - _selectionMode = NoSelection; - update(); +void ChatItem::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) +{ + Q_UNUSED(option); + Q_UNUSED(widget); + painter->save(); + painter->setClipRect(boundingRect()); + paintBackground(painter); + + layout()->draw(painter, pos(), additionalFormats(), boundingRect()); + + // layout()->draw(painter, QPointF(0,0), formats, boundingRect()); + + // Debuging Stuff + // uncomment partially or all of the following stuff: + // + // 0) alternativ painter color for debug stuff + // if(row() % 2) + // painter->setPen(Qt::red); + // else + // painter->setPen(Qt::blue); + // 1) draw wordwrap points in the first line + // if(column() == 2) { + // ChatLineModel::WrapList wrapList = data(ChatLineModel::WrapListRole).value(); + // foreach(ChatLineModel::Word word, wrapList) { + // if(word.endX > width()) + // break; + // painter->drawLine(word.endX, 0, word.endX, height()); + // } + // } + // 2) draw MsgId over the time column + // if(column() == 0) { + // QString msgIdString = QString::number(data(MessageModel::MsgIdRole).value().toLongLong()); + // QPointF bottomPoint = boundingRect().bottomLeft(); + // bottomPoint.ry() -= 2; + // painter->drawText(bottomPoint, msgIdString); + // } + // 3) draw bounding rect + // painter->drawRect(_boundingRect.adjusted(0, 0, -1, -1)); + + painter->restore(); +} + +void ChatItem::overlayFormat(UiStyle::FormatList& fmtList, quint16 start, quint16 end, UiStyle::FormatType overlayFmt) const +{ + for (size_t i = 0; i < fmtList.size(); i++) { + int fmtStart = fmtList.at(i).first; + int fmtEnd = (i < fmtList.size() - 1 ? fmtList.at(i + 1).first : data(MessageModel::DisplayRole).toString().length()); + + if (fmtEnd <= start) + continue; + if (fmtStart >= end) + break; + + // split the format if necessary + if (fmtStart < start) { + fmtList.insert(fmtList.begin() + i, fmtList.at(i)); + fmtList[++i].first = start; + } + if (end < fmtEnd) { + fmtList.insert(fmtList.begin() + i, fmtList.at(i)); + fmtList[i + 1].first = end; + } + + fmtList[i].second.type |= overlayFmt; } - event->accept(); - } else { - event->ignore(); - } -} - -void ChatItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { - if(contains(event->pos())) { - qint16 end = posToCursor(event->pos()); - if(end != _selectionEnd) { - _selectionEnd = end; - update(); - } - } else { - setFullSelection(); - chatScene()->startGlobalSelection(this, event->pos()); - } -} - -void ChatItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { - if(_selectionMode != NoSelection) { - _selectionEnd = posToCursor(event->pos()); - QString selection - = data(MessageModel::DisplayRole).toString().mid(qMin(_selectionStart, _selectionEnd), qAbs(_selectionStart - _selectionEnd)); - QApplication::clipboard()->setText(selection, QClipboard::Clipboard); // TODO configure where selections should go - event->accept(); - } else { - event->ignore(); - } } -void ChatItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) { - // FIXME dirty and fast hack to make http:// urls klickable +QVector ChatItem::additionalFormats() const +{ + // Calculate formats to overlay (only) if there's a selection, and/or a hovered clickable + if (!hasSelection() && !hasActiveClickable()) { + return {}; + } - QRegExp regex("\\b((?:h|f)t{1,2}ps?://[^\\s]+)\\b"); - QString str = data(ChatLineModel::DisplayRole).toString(); - int idx = posToCursor(event->pos()); - int mi = 0; - do { - mi = regex.indexIn(str, mi); - if(mi < 0) break; - if(idx >= mi && idx < mi + regex.matchedLength()) { - QDesktopServices::openUrl(QUrl(regex.capturedTexts()[1])); - break; + using Label = UiStyle::MessageLabel; + using Format = UiStyle::Format; + + auto itemLabel = data(ChatLineModel::MsgLabelRole).value