X-Git-Url: https://git.quassel-irc.org/?p=quassel.git;a=blobdiff_plain;f=src%2Fqtui%2Fchatitem.cpp;h=499ae7740b580e5c31a48ced0ab20b579034d5ba;hp=2159f3f3ffb641ff91be39dada0a9ae05ae99a89;hb=55579f53f3bd37f2a20d9be7458bdc54b345a052;hpb=5199330f0b249b20c27cd372d995909f97433786 diff --git a/src/qtui/chatitem.cpp b/src/qtui/chatitem.cpp index 2159f3f3..499ae774 100644 --- a/src/qtui/chatitem.cpp +++ b/src/qtui/chatitem.cpp @@ -84,20 +84,20 @@ void ChatItem::initLayoutHelper(QTextLayout *layout, QTextOption::WrapMode wrapM layout->setTextOption(option); QList formatRanges - = QtUi::style()->toTextLayoutList(data(MessageModel::FormatRole).value(), layout->text().length(), data(ChatLineModel::MsgLabelRole).toUInt()); + = QtUi::style()->toTextLayoutList(formatList(), layout->text().length(), data(ChatLineModel::MsgLabelRole).toUInt()); layout->setAdditionalFormats(formatRanges); } +UiStyle::FormatList ChatItem::formatList() const { + return data(MessageModel::FormatRole).value(); +} + 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(); } @@ -122,7 +122,7 @@ void ChatItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QTextLayout layout; initLayout(&layout); - layout.draw(painter, QPointF(0,0), selectionFormats(), boundingRect()); + layout.draw(painter, QPointF(0,0), additionalFormats(), boundingRect()); // layout()->draw(painter, QPointF(0,0), formats, boundingRect()); @@ -154,6 +154,34 @@ void ChatItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, // painter->drawRect(_boundingRect.adjusted(0, 0, -1, -1)); } +void ChatItem::overlayFormat(UiStyle::FormatList &fmtList, int start, int end, quint32 overlayFmt) const { + for(int i = 0; i < fmtList.count(); i++) { + int fmtStart = fmtList.at(i).first; + int fmtEnd = (i < fmtList.count()-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(i, fmtList.at(i)); + fmtList[++i].first = start; + } + if(end < fmtEnd) { + fmtList.insert(i, fmtList.at(i)); + fmtList[i+1].first = end; + } + + fmtList[i].second |= overlayFmt; + } +} + +QVector ChatItem::additionalFormats() const { + return selectionFormats(); +} + QVector ChatItem::selectionFormats() const { if(!hasSelection()) return QVector(); @@ -167,14 +195,14 @@ QVector ChatItem::selectionFormats() const { end = qMax(_selectionStart, _selectionEnd); } - UiStyle::FormatList fmtList = data(MessageModel::FormatRole).value(); + UiStyle::FormatList fmtList = formatList(); - while(fmtList.count() >=2 && fmtList.at(1).first <= start) + while(fmtList.count() > 1 && fmtList.at(1).first <= start) fmtList.removeFirst(); fmtList.first().first = start; - while(fmtList.count() >= 2 && fmtList.last().first >= end) + while(fmtList.count() > 1 && fmtList.last().first >= end) fmtList.removeLast(); return QtUi::style()->toTextLayoutList(fmtList, end, UiStyle::Selected|data(ChatLineModel::MsgLabelRole).toUInt()).toVector(); @@ -335,7 +363,7 @@ void SenderChatItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *op QPixmap pixmap(layout.boundingRect().toRect().size()); pixmap.fill(Qt::transparent); QPainter pixPainter(&pixmap); - layout.draw(&pixPainter, QPointF(qMax(offset, (qreal)0), 0), selectionFormats()); + layout.draw(&pixPainter, QPointF(qMax(offset, (qreal)0), 0), additionalFormats()); pixPainter.end(); // Create alpha channel mask @@ -357,7 +385,7 @@ void SenderChatItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *op pixmap.setAlphaChannel(mask); painter->drawPixmap(0, 0, pixmap); } else { - layout.draw(painter, QPointF(0,0), selectionFormats(), boundingRect()); + layout.draw(painter, QPointF(0,0), additionalFormats(), boundingRect()); } } @@ -375,7 +403,7 @@ ContentsChatItem::ContentsChatItem(const qreal &width, const QPointF &pos, QGrap } QFontMetricsF *ContentsChatItem::fontMetrics() const { - return QtUi::style()->fontMetrics(data(ChatLineModel::FormatRole).value().at(0).second); + return QtUi::style()->fontMetrics(data(ChatLineModel::FormatRole).value().at(0).second, 0); } ContentsChatItem::~ContentsChatItem() { @@ -385,114 +413,79 @@ ContentsChatItem::~ContentsChatItem() { ContentsChatItemPrivate *ContentsChatItem::privateData() const { if(!_data) { ContentsChatItem *that = const_cast(this); - that->_data = new ContentsChatItemPrivate(findClickables(), that); + that->_data = new ContentsChatItemPrivate(ClickableList::fromString(data(ChatLineModel::DisplayRole).toString()), that); } return _data; } qreal ContentsChatItem::setGeometryByWidth(qreal w) { - if(w != width()) { + // We use this for reloading layout info as well, so we can't bail out if the width doesn't change + + // compute height + int lines = 1; + WrapColumnFinder finder(this); + while(finder.nextWrapColumn(w) > 0) + lines++; + qreal h = lines * fontMetrics()->lineSpacing(); + delete _data; + _data = 0; + + if(w != width() || h != height()) { prepareGeometryChange(); - setWidth(w); - QTextLayout layout; - initLayout(&layout); - setHeight(layout.boundingRect().height()); - delete _data; - _data = 0; + setGeometry(w, h); } - return height(); + return h; } -// NOTE: This method is not threadsafe and not reentrant! -// (RegExps are not constant while matching, and they are static here for efficiency) -QList ContentsChatItem::findClickables() const { - // For matching URLs - static QString urlEnd("(?:>|[,.;:\"]*\\s|\\b|$)"); - static QString urlChars("(?:[,.;:]*[\\w\\-~@/?&=+$()!%#*|{}\\[\\]'^])"); - - static QRegExp regExp[] = { - // URL - // QRegExp(QString("((?:https?://|s?ftp://|irc://|mailto:|www\\.)%1+|%1+\\.[a-z]{2,4}(?:?=/%1+|\\b))%2").arg(urlChars, urlEnd)), - QRegExp(QString("((?:(?:mailto:|\\w+://)|www\\.)%1+)%2").arg(urlChars, urlEnd), Qt::CaseInsensitive), - - // Channel name - // We don't match for channel names starting with + or &, because that gives us a lot of false positives. - QRegExp("((?:#|![A-Z0-9]{5})[^,:\\s]+(?::[^,:\\s]+)?)\\b", Qt::CaseInsensitive) - - // TODO: Nicks, we'll need a filtering for only matching known nicknames further down if we do this - }; - - static const int regExpCount = 2; // number of regexps in the array above - - qint16 matches[] = { 0, 0, 0 }; - qint16 matchEnd[] = { 0, 0, 0 }; - - QString str = data(ChatLineModel::DisplayRole).toString(); +void ContentsChatItem::doLayout(QTextLayout *layout) const { + ChatLineModel::WrapList wrapList = data(ChatLineModel::WrapListRole).value(); + if(!wrapList.count()) return; // empty chatitem - QList result; - qint16 idx = 0; - qint16 minidx; - int type = -1; - - do { - type = -1; - minidx = str.length(); - for(int i = 0; i < regExpCount; i++) { - if(matches[i] < 0 || matchEnd[i] > str.length()) continue; - if(idx >= matchEnd[i]) { - matches[i] = regExp[i].indexIn(str, qMax(matchEnd[i], idx)); - if(matches[i] >= 0) matchEnd[i] = matches[i] + regExp[i].cap(1).length(); - } - if(matches[i] >= 0 && matches[i] < minidx) { - minidx = matches[i]; - type = i; - } - } - if(type >= 0) { - idx = matchEnd[type]; - QString match = str.mid(matches[type], matchEnd[type] - matches[type]); - if(type == Clickable::Url && str.at(idx-1) == ')') { // special case: closing paren only matches if we had an open one - if(!match.contains('(')) { - matchEnd[type]--; - match.chop(1); - } - } - if(type == Clickable::Channel) { - // don't make clickable if it could be a #number - if(QRegExp("^#\\d+$").exactMatch(match)) - continue; - } - result.append(Clickable((Clickable::Type)type, matches[type], matchEnd[type] - matches[type])); - } - } while(type >= 0); + qreal h = 0; + WrapColumnFinder finder(this); + layout->beginLayout(); + forever { + QTextLine line = layout->createLine(); + if(!line.isValid()) + break; - /* testing - if(!result.isEmpty()) qDebug() << str; - foreach(Clickable click, result) { - qDebug() << str.mid(click.start, click.length); + int col = finder.nextWrapColumn(width()); + line.setNumColumns(col >= 0 ? col - line.textStart() : layout->text().length()); + line.setPosition(QPointF(0, h)); + h += fontMetrics()->lineSpacing(); } - */ - return result; + layout->endLayout(); } -ContentsChatItem::Clickable ContentsChatItem::clickableAt(const QPointF &pos) const { +Clickable ContentsChatItem::clickableAt(const QPointF &pos) const { qint16 idx = posToCursor(pos); 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(idx >= click.start() && idx < click.start() + click.length()) return click; } return Clickable(); } +UiStyle::FormatList ContentsChatItem::formatList() const { + UiStyle::FormatList fmtList = ChatItem::formatList(); + for(int i = 0; i < privateData()->clickables.count(); i++) { + Clickable click = privateData()->clickables.at(i); + if(click.type() == Clickable::Url) { + overlayFormat(fmtList, click.start(), click.start() + click.length(), UiStyle::Url); + } + } + return fmtList; +} + QVector ContentsChatItem::additionalFormats() const { + QVector fmt = ChatItem::additionalFormats(); // mark a clickable if hovered upon - QVector fmt; if(privateData()->currentClickable.isValid()) { Clickable click = privateData()->currentClickable; QTextLayout::FormatRange f; - f.start = click.start; - f.length = click.length; + f.start = click.start(); + f.length = click.length(); f.format.setFontUnderline(true); fmt.append(f); } @@ -514,8 +507,8 @@ void ContentsChatItem::handleClick(const QPointF &pos, ChatScene::ClickMode clic if(clickMode == ChatScene::SingleClick) { Clickable click = clickableAt(pos); if(click.isValid()) { - QString str = data(ChatLineModel::DisplayRole).toString().mid(click.start, click.length); - switch(click.type) { + QString str = data(ChatLineModel::DisplayRole).toString().mid(click.start(), click.length()); + switch(click.type()) { case Clickable::Url: if(!str.contains("://")) str = "http://" + str; @@ -542,8 +535,8 @@ void ContentsChatItem::handleClick(const QPointF &pos, ChatScene::ClickMode clic setSelectionMode(PartialSelection); Clickable click = clickableAt(pos); if(click.isValid()) { - setSelectionStart(click.start); - setSelectionEnd(click.start + click.length); + setSelectionStart(click.start()); + setSelectionEnd(click.start() + click.length()); } else { // find word boundary QString str = data(ChatLineModel::DisplayRole).toString(); @@ -576,11 +569,11 @@ void ContentsChatItem::hoverMoveEvent(QGraphicsSceneHoverEvent *event) { bool onClickable = false; Clickable click = clickableAt(event->pos()); if(click.isValid()) { - if(click.type == Clickable::Url) { + if(click.type() == Clickable::Url) { onClickable = true; showWebPreview(click); - } else if(click.type == Clickable::Channel) { - QString name = data(ChatLineModel::DisplayRole).toString().mid(click.start, click.length); + } else if(click.type() == Clickable::Channel) { + QString name = data(ChatLineModel::DisplayRole).toString().mid(click.start(), click.length()); // don't make clickable if it's our own name BufferId myId = data(MessageModel::BufferIdRole).value(); if(Client::networkModel()->bufferName(myId) != name) @@ -600,7 +593,7 @@ void ContentsChatItem::hoverMoveEvent(QGraphicsSceneHoverEvent *event) { void ContentsChatItem::addActionsToMenu(QMenu *menu, const QPointF &pos) { if(privateData()->currentClickable.isValid()) { Clickable click = privateData()->currentClickable; - switch(click.type) { + switch(click.type()) { case Clickable::Url: privateData()->activeClickable = click; menu->addAction(SmallIcon("edit-copy"), tr("Copy Link Address"), @@ -610,7 +603,7 @@ void ContentsChatItem::addActionsToMenu(QMenu *menu, const QPointF &pos) { // Hide existing menu actions, they confuse us when right-clicking on a clickable foreach(QAction *action, menu->actions()) action->setVisible(false); - QString name = data(ChatLineModel::DisplayRole).toString().mid(click.start, click.length); + QString name = data(ChatLineModel::DisplayRole).toString().mid(click.start(), click.length()); GraphicalUi::contextMenuActionProvider()->addActions(menu, chatScene()->filter(), data(MessageModel::BufferIdRole).value(), name); break; } @@ -625,8 +618,8 @@ void ContentsChatItem::addActionsToMenu(QMenu *menu, const QPointF &pos) { void ContentsChatItem::copyLinkToClipboard() { Clickable click = privateData()->activeClickable; - if(click.isValid() && click.type == Clickable::Url) { - QString url = data(ChatLineModel::DisplayRole).toString().mid(click.start, click.length); + if(click.isValid() && click.type() == Clickable::Url) { + QString url = data(ChatLineModel::DisplayRole).toString().mid(click.start(), click.length()); if(!url.contains("://")) url = "http://" + url; chatScene()->stringToClipboard(url); @@ -641,16 +634,16 @@ void ContentsChatItem::showWebPreview(const Clickable &click) { #else QTextLayout layout; initLayout(&layout); - QTextLine line = layout.lineForTextPosition(click.start); - qreal x = line.cursorToX(click.start); - qreal width = line.cursorToX(click.start + click.length) - x; + 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(); qreal y = height * line.lineNumber(); QPointF topLeft = scenePos() + QPointF(x, y); QRectF urlRect = QRectF(topLeft.x(), topLeft.y(), width, height); - QString url = data(ChatLineModel::DisplayRole).toString().mid(click.start, click.length); + QString url = data(ChatLineModel::DisplayRole).toString().mid(click.start(), click.length()); if(!url.contains("://")) url = "http://" + url; chatScene()->loadWebPreview(this, url, urlRect); @@ -665,3 +658,66 @@ void ContentsChatItem::clearWebPreview() { /*************************************************************************************************/ +ContentsChatItem::WrapColumnFinder::WrapColumnFinder(const ChatItem *_item) + : item(_item), + wrapList(item->data(ChatLineModel::WrapListRole).value()), + wordidx(0), + lineCount(0), + choppedTrailing(0) +{ +} + +ContentsChatItem::WrapColumnFinder::~WrapColumnFinder() { +} + +qint16 ContentsChatItem::WrapColumnFinder::nextWrapColumn(qreal width) { + if(wordidx >= wrapList.count()) + return -1; + + lineCount++; + qreal targetWidth = lineCount * 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; +} + +/*************************************************************************************************/ +