X-Git-Url: https://git.quassel-irc.org/?p=quassel.git;a=blobdiff_plain;f=src%2Fqtui%2Fchatitem.cpp;h=9b7c220343905b31f3e1d45726a875cc45155416;hp=072ae1b08419da6b13e4800375613d37b3572ae9;hb=e8e50ceb0f0df46a6f281f6d4fd587f00fe532d3;hpb=339ed024e6cf074108e39360e7db58ea0961761b diff --git a/src/qtui/chatitem.cpp b/src/qtui/chatitem.cpp index 072ae1b0..9b7c2203 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-09 by the Quassel Project * * devel@quassel-irc.org * * * * This program is free software; you can redistribute it and/or modify * @@ -26,19 +26,19 @@ #include #include #include -#ifdef HAVE_WEBKIT -#include -#endif -#include +#include +#include "buffermodel.h" +#include "bufferview.h" #include "chatitem.h" #include "chatlinemodel.h" +#include "iconloader.h" +#include "mainwin.h" #include "qtui.h" #include "qtuistyle.h" ChatItem::ChatItem(const qreal &width, const qreal &height, const QPointF &pos, QGraphicsItem *parent) : QGraphicsItem(parent), - _data(0), _boundingRect(0, 0, width, height), _selectionMode(NoSelection), _selectionStart(-1) @@ -48,10 +48,6 @@ ChatItem::ChatItem(const qreal &width, const qreal &height, const QPointF &pos, setPos(pos); } -ChatItem::~ChatItem() { - delete _data; -} - QVariant ChatItem::data(int role) const { QModelIndex index = model()->index(row(), column()); if(!index.isValid()) { @@ -61,8 +57,10 @@ QVariant ChatItem::data(int role) const { return model()->data(index, role); } -QTextLayout *ChatItem::createLayout(QTextOption::WrapMode wrapMode, Qt::Alignment alignment) { - QTextLayout *layout = new QTextLayout(data(MessageModel::DisplayRole).toString()); +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); @@ -72,80 +70,69 @@ QTextLayout *ChatItem::createLayout(QTextOption::WrapMode wrapMode, Qt::Alignmen QList formatRanges = QtUi::style()->toTextLayoutList(data(MessageModel::FormatRole).value(), layout->text().length()); layout->setAdditionalFormats(formatRanges); - return layout; } -void ChatItem::updateLayout() { - if(!privateData()) { - setPrivateData(new ChatItemPrivate(createLayout())); - } - QTextLayout *layout_ = layout(); - layout_->beginLayout(); - QTextLine line = layout_->createLine(); +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(); + layout->endLayout(); } -void ChatItem::clearLayout() { - 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(!hasLayout()) - 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 = additionalFormats(); - 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()); + QTextLayout::FormatRange selectFmt = selectionFormat(); + if(selectFmt.format.isValid()) formats.append(selectFmt); + QTextLayout layout; + initLayout(&layout); + layout.draw(painter, QPointF(0,0), formats, boundingRect()); + + // 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 + // 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); -// 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); + // 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().toInt()); +// QPointF bottomPoint = boundingRect().bottomLeft(); +// bottomPoint.ry() -= 2; +// painter->drawText(bottomPoint, msgIdString); +// } + // 3) draw bounding rect // painter->drawRect(_boundingRect.adjusted(0, 0, -1, -1)); } -qint16 ChatItem::posToCursor(const QPointF &pos) { +qint16 ChatItem::posToCursor(const QPointF &pos) const { if(pos.y() > height()) return data(MessageModel::DisplayRole).toString().length(); if(pos.y() < 0) return 0; - if(!hasLayout()) - updateLayout(); - for(int l = layout()->lineCount() - 1; l >= 0; l--) { - QTextLine line = layout()->lineAt(l); + + 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); } @@ -153,6 +140,30 @@ qint16 ChatItem::posToCursor(const QPointF &pos) { return 0; } +bool ChatItem::hasSelection() const { + if(_selectionMode == NoSelection) + return false; + if(_selectionMode == FullSelection) + return true; + // partial + return _selectionStart != _selectionEnd; +} + +QString ChatItem::selection() const { + if(_selectionMode == FullSelection) + return data(MessageModel::DisplayRole).toString(); + if(_selectionMode == PartialSelection) + return data(MessageModel::DisplayRole).toString().mid(qMin(_selectionStart, _selectionEnd), qAbs(_selectionStart - _selectionEnd)); + return QString(); +} + +void ChatItem::setSelection(SelectionMode mode, qint16 start, qint16 end) { + _selectionMode = mode; + _selectionStart = start; + _selectionEnd = end; + update(); +} + void ChatItem::setFullSelection() { if(_selectionMode != FullSelection) { _selectionMode = FullSelection; @@ -161,8 +172,10 @@ void ChatItem::setFullSelection() { } void ChatItem::clearSelection() { - _selectionMode = NoSelection; - update(); + if(_selectionMode != NoSelection) { + _selectionMode = NoSelection; + update(); + } } void ChatItem::continueSelecting(const QPointF &pos) { @@ -171,6 +184,35 @@ void ChatItem::continueSelecting(const QPointF &pos) { update(); } +bool ChatItem::isPosOverSelection(const QPointF &pos) const { + if(_selectionMode == FullSelection) + return true; + if(_selectionMode == PartialSelection) { + int cursor = posToCursor(pos); + return cursor >= qMin(_selectionStart, _selectionEnd) && cursor <= qMax(_selectionStart, _selectionEnd); + } + return false; +} + +QTextLayout::FormatRange ChatItem::selectionFormat() const { + QTextLayout::FormatRange selectFmt; + if(_selectionMode != NoSelection) { + 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(); + } + } else { + selectFmt.start = -1; + selectFmt.length = 0; + } + return selectFmt; +} + QList ChatItem::findWords(const QString &searchWord, Qt::CaseSensitivity caseSensitive) { QList resultList; const QAbstractItemModel *model_ = model(); @@ -185,12 +227,10 @@ QList ChatItem::findWords(const QString &searchWord, Qt::CaseSensitivity searchIdx = plainText.indexOf(searchWord, searchIdx + 1, caseSensitive); } - bool hadLayout = hasLayout(); - if(!hadLayout) - updateLayout(); - + 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(); @@ -198,20 +238,16 @@ QList ChatItem::findWords(const QString &searchWord, Qt::CaseSensitivity resultList << QRectF(x, y, width, height); } - if(!hadLayout) - clearLayout(); return resultList; } -void ChatItem::mousePressEvent(QGraphicsSceneMouseEvent *event) { - if(event->buttons() == Qt::LeftButton) { +void ChatItem::handleClick(const QPointF &pos, ChatScene::ClickMode clickMode) { + // single clicks are already handled by the scene (for clearing the selection) + if(clickMode == ChatScene::DragStartClick) { chatScene()->setSelectingItem(this); - _selectionStart = _selectionEnd = posToCursor(event->pos()); + _selectionStart = _selectionEnd = posToCursor(pos); _selectionMode = NoSelection; // will be set to PartialSelection by mouseMoveEvent update(); - event->accept(); - } else { - event->ignore(); } } @@ -234,27 +270,87 @@ void ChatItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { } } +void ChatItem::mousePressEvent(QGraphicsSceneMouseEvent *event) { + if(event->buttons() == Qt::LeftButton) + event->accept(); + else + event->ignore(); +} + void ChatItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { - if(_selectionMode != NoSelection && !event->buttons() & Qt::LeftButton) { - _selectionEnd = posToCursor(event->pos()); - QString selection - = data(MessageModel::DisplayRole).toString().mid(qMin(_selectionStart, _selectionEnd), qAbs(_selectionStart - _selectionEnd)); - chatScene()->putToClipboard(selection); + if(_selectionMode != NoSelection && event->button() == Qt::LeftButton) { + chatScene()->selectionToClipboard(QClipboard::Selection); event->accept(); - } else { + } else event->ignore(); - } +} + +void ChatItem::addActionsToMenu(QMenu *menu, const QPointF &pos) { + Q_UNUSED(menu); + Q_UNUSED(pos); + } // ************************************************************ // SenderChatItem // ************************************************************ +void SenderChatItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { + Q_UNUSED(option); Q_UNUSED(widget); + + painter->setClipRect(boundingRect()); // no idea why QGraphicsItem clipping won't work + QTextLayout layout; + initLayout(&layout); + qreal layoutWidth = layout.minimumWidth(); + qreal offset = 0; + if(chatScene()->senderCutoffMode() == ChatScene::CutoffLeft) + offset = qMin(width() - layoutWidth, (qreal)0); + else + offset = qMax(layoutWidth - width(), (qreal)0); + + QTextLayout::FormatRange selectFmt = selectionFormat(); + + 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()); + pixmap.fill(Qt::transparent); + QPainter pixPainter(&pixmap); + layout.draw(&pixPainter, QPointF(qMax(offset, (qreal)0), 0), QVector() << selectFmt); + pixPainter.end(); + + // Create alpha channel mask + QPixmap mask(pixmap.size()); + QPainter maskPainter(&mask); + QLinearGradient gradient; + if(offset < 0) { + gradient.setStart(0, 0); + gradient.setFinalStop(12, 0); + gradient.setColorAt(0, Qt::black); + gradient.setColorAt(1, Qt::white); + } else { + gradient.setStart(width()-10, 0); + gradient.setFinalStop(width(), 0); + gradient.setColorAt(0, Qt::white); + gradient.setColorAt(1, Qt::black); + } + maskPainter.fillRect(boundingRect(), gradient); + pixmap.setAlphaChannel(mask); + painter->drawPixmap(0, 0, pixmap); + } else { + layout.draw(painter, QPointF(0,0), QVector() << selectFmt, boundingRect()); + } +} + // ************************************************************ // ContentsChatItem // ************************************************************ + +ContentsChatItem::ActionProxy ContentsChatItem::_actionProxy; + ContentsChatItem::ContentsChatItem(const qreal &width, const QPointF &pos, QGraphicsItem *parent) - : ChatItem(0, 0, pos, parent) + : ChatItem(0, 0, pos, parent), + _data(0) { const QAbstractItemModel *model_ = model(); QModelIndex index = model_->index(row(), column()); @@ -263,8 +359,21 @@ ContentsChatItem::ContentsChatItem(const qreal &width, const QPointF &pos, QGrap setGeometryByWidth(width); } +ContentsChatItem::~ContentsChatItem() { + delete _data; +} + +ContentsChatItemPrivate *ContentsChatItem::privateData() const { + if(!_data) { + ContentsChatItem *that = const_cast(this); + that->_data = new ContentsChatItemPrivate(findClickables(), that); + } + return _data; +} + qreal ContentsChatItem::setGeometryByWidth(qreal w) { if(w != width()) { + prepareGeometryChange(); setWidth(w); // compute height int lines = 1; @@ -272,51 +381,47 @@ qreal ContentsChatItem::setGeometryByWidth(qreal w) { while(finder.nextWrapColumn() > 0) lines++; setHeight(lines * fontMetrics()->lineSpacing()); + delete _data; + _data = 0; } return height(); } -void ContentsChatItem::updateLayout() { - if(!privateData()) { - ContentsChatItemPrivate *data = new ContentsChatItemPrivate(createLayout(QTextOption::WrapAnywhere), findClickables(), this); - setPrivateData(data); - } - - // Now layout +void ContentsChatItem::doLayout(QTextLayout *layout) const { ChatLineModel::WrapList wrapList = data(ChatLineModel::WrapListRole).value(); if(!wrapList.count()) return; // empty chatitem qreal h = 0; WrapColumnFinder finder(this); - layout()->beginLayout(); + layout->beginLayout(); forever { - QTextLine line = layout()->createLine(); + QTextLine line = layout->createLine(); if(!line.isValid()) break; int col = finder.nextWrapColumn(); - line.setNumColumns(col >= 0 ? col - line.textStart() : layout()->text().length()); + line.setNumColumns(col >= 0 ? col - line.textStart() : layout->text().length()); line.setPosition(QPointF(0, h)); h += fontMetrics()->lineSpacing(); } - layout()->endLayout(); + 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::findClickables() { +QList ContentsChatItem::findClickables() const { // For matching URLs static QString urlEnd("(?:>|[,.;:\"]*\\s|\\b|$)"); - static QString urlChars("(?:[\\w\\-~@/?&=+$()!%#]|[,.;:]\\w)"); + 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("((?:(?:https?://|s?ftp://|irc://|mailto:)|www)%1+)%2").arg(urlChars, urlEnd)), + QRegExp(QString("((?:(?:https?://|s?ftp://|irc://|gopher://|mailto:)|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") + 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 }; @@ -349,8 +454,17 @@ QList ContentsChatItem::findClickables() { } 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(!str.mid(matches[type], matchEnd[type]-matches[type]).contains('(')) matchEnd[type]--; + 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])); } @@ -365,6 +479,16 @@ QList ContentsChatItem::findClickables() { return result; } +ContentsChatItem::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) + return click; + } + return Clickable(); +} + QVector ContentsChatItem::additionalFormats() const { // mark a clickable if hovered upon QVector fmt; @@ -380,53 +504,70 @@ QVector ContentsChatItem::additionalFormats() const { } void ContentsChatItem::endHoverMode() { - if(hasLayout()) { + if(privateData()) { if(privateData()->currentClickable.isValid()) { setCursor(Qt::ArrowCursor); privateData()->currentClickable = Clickable(); } -#ifdef HAVE_WEBKIT clearWebPreview(); -#endif update(); } } -void ContentsChatItem::mousePressEvent(QGraphicsSceneMouseEvent *event) { - privateData()->hasDragged = false; - ChatItem::mousePressEvent(event); -} - -void ContentsChatItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { - if(!event->buttons() && !privateData()->hasDragged) { - // got a click - Clickable click = privateData()->currentClickable; +void ContentsChatItem::handleClick(const QPointF &pos, ChatScene::ClickMode clickMode) { + 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) { case Clickable::Url: - if(!str.contains("://")) - str = "http://" + str; - QDesktopServices::openUrl(str); + if(!str.contains("://")) + str = "http://" + str; + QDesktopServices::openUrl(QUrl::fromEncoded(str.toUtf8(), QUrl::TolerantMode)); break; - case Clickable::Channel: - // TODO join or whatever... + case Clickable::Channel: { + NetworkId networkId = Client::networkModel()->networkId(data(MessageModel::BufferIdRole).value()); + BufferId bufId = Client::networkModel()->bufferId(networkId, str); + if(bufId.isValid()) { + QModelIndex targetIdx = Client::networkModel()->bufferIndex(bufId); + Client::bufferModel()->switchToBuffer(bufId); + if(!targetIdx.data(NetworkModel::ItemActiveRole).toBool()) + Client::userInput(BufferInfo::fakeStatusBuffer(networkId), QString("/JOIN %1").arg(str)); + } else + Client::userInput(BufferInfo::fakeStatusBuffer(networkId), QString("/JOIN %1").arg(str)); break; + } default: break; } } + } else if(clickMode == ChatScene::DoubleClick) { + chatScene()->setSelectingItem(this); + setSelectionMode(PartialSelection); + Clickable click = clickableAt(pos); + if(click.isValid()) { + setSelectionStart(click.start); + setSelectionEnd(click.start + click.length); + } else { + // find word boundary + QString str = data(ChatLineModel::DisplayRole).toString(); + qint16 cursor = posToCursor(pos); + qint16 start = str.lastIndexOf(QRegExp("\\W"), cursor) + 1; + qint16 end = qMin(str.indexOf(QRegExp("\\W"), cursor), str.length()); + if(end < 0) end = str.length(); + setSelectionStart(start); + setSelectionEnd(end); + } + update(); + } else if(clickMode == ChatScene::TripleClick) { + setSelection(PartialSelection, 0, data(ChatLineModel::DisplayRole).toString().length()); } - ChatItem::mouseReleaseEvent(event); + ChatItem::handleClick(pos, clickMode); } 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(hasLayout() && !privateData()->hasDragged && event->buttons() & Qt::LeftButton - && (event->buttonDownScreenPos(Qt::LeftButton) - event->screenPos()).manhattanLength() >= QApplication::startDragDistance()) - privateData()->hasDragged = true; ChatItem::mouseMoveEvent(event); } @@ -437,137 +578,102 @@ void ContentsChatItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event) { void ContentsChatItem::hoverMoveEvent(QGraphicsSceneHoverEvent *event) { bool onClickable = false; - qint16 idx = posToCursor(event->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(click.type == Clickable::Url) { + Clickable click = clickableAt(event->pos()); + if(click.isValid()) { + 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); + // don't make clickable if it's our own name + BufferId myId = data(MessageModel::BufferIdRole).value(); + if(Client::networkModel()->bufferName(myId) != name) onClickable = true; -#ifdef HAVE_WEBKIT - showWebPreview(click); -#endif - } else if(click.type == Clickable::Channel) { - // TODO: don't make clickable if it's our own name - //onClickable = true; //FIXME disabled for now - } - if(onClickable) { - setCursor(Qt::PointingHandCursor); - privateData()->currentClickable = click; - update(); - break; - } + } + if(onClickable) { + setCursor(Qt::PointingHandCursor); + privateData()->currentClickable = click; + update(); + return; } } if(!onClickable) endHoverMode(); event->accept(); } -#ifdef HAVE_WEBKIT -void ContentsChatItem::showWebPreview(const Clickable &click) { - if(!hasLayout()) - updateLayout(); +void ContentsChatItem::addActionsToMenu(QMenu *menu, const QPointF &pos) { + Q_UNUSED(pos); // we assume that the current mouse cursor pos is the point of invocation - 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(); - QRectF urlRect(x, y, width, height); - QString url = data(ChatLineModel::DisplayRole).toString().mid(click.start, click.length); - if(!url.contains("://")) - url = "http://" + url; - privateData()->loadWebPreview(url, urlRect); -} - -void ContentsChatItem::clearWebPreview() { - if(hasLayout()) - privateData()->clearWebPreview(); -} -#endif - -// **************************************** -// ContentsChatItemPrivate -// **************************************** -#ifdef HAVE_WEBKIT -ContentsChatItemPrivate::~ContentsChatItemPrivate() { - clearWebPreview(); -} - -void ContentsChatItemPrivate::loadWebPreview(const QString &url, const QRectF &urlRect) { - if(url != previewUrl) { - previewUrl = url; - // load a new web view and delete the old one (if exists) - if(previewItem) { - contentsItem->scene()->removeItem(previewItem); - delete previewItem; + if(privateData()->currentClickable.isValid()) { + Clickable click = privateData()->currentClickable; + switch(click.type) { + case Clickable::Url: + privateData()->activeClickable = click; + menu->addAction(SmallIcon("edit-copy"), tr("Copy Link Address"), + &_actionProxy, SLOT(copyLinkToClipboard()))->setData(QVariant::fromValue(this)); + break; + case Clickable::Channel: { + // 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); + Client::mainUi()->actionProvider()->addActions(menu, chatScene()->filter(), data(MessageModel::BufferIdRole).value(), name); + break; + } + default: + break; } - QWebView *view = new QWebView; - view->load(url); - previewItem = new ContentsChatItemPrivate::PreviewItem(view); - contentsItem->scene()->addItem(previewItem); - } - if(urlRect != previewUrlRect) { - previewUrlRect = urlRect; - QPointF sPos = contentsItem->scenePos(); - qreal previewY = sPos.y() + urlRect.y() + urlRect.height(); // bottom of url; - qreal previewX = sPos.x() + urlRect.x(); - if(previewY + previewItem->boundingRect().height() > contentsItem->scene()->sceneRect().bottom()) - previewY = sPos.y() + urlRect.y() - previewItem->boundingRect().height(); - - if(previewX + previewItem->boundingRect().width() > contentsItem->scene()->sceneRect().width()) - previewX = contentsItem->scene()->sceneRect().right() - previewItem->boundingRect().width(); - - previewItem->setPos(previewX, previewY); + } else { + + // Buffer-specific actions + Client::mainUi()->actionProvider()->addActions(menu, chatScene()->filter(), data(MessageModel::BufferIdRole).value()); } } -void ContentsChatItemPrivate::clearWebPreview() { - if(previewItem) { - contentsItem->scene()->removeItem(previewItem); - delete previewItem; - previewItem = 0; +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(!url.contains("://")) + url = "http://" + url; + chatScene()->stringToClipboard(url); } - previewUrl = QString(); - previewUrlRect = QRectF(); } -ContentsChatItemPrivate::PreviewItem::PreviewItem(QWebView *webView) - : QGraphicsItem(0), // needs to be a top level item as we otherwise cannot guarantee that it's on top of other chatlines - _boundingRect(0, 0, 400, 300) -{ - qreal frameWidth = 5; - webView->resize(1000, 750); - QGraphicsProxyWidget *proxyItem = new QGraphicsProxyWidget(this); - proxyItem->setWidget(webView); - proxyItem->setAcceptHoverEvents(false); +/******** WEB PREVIEW *****************************************************************************/ - qreal xScale = (_boundingRect.width() - 2 * frameWidth) / webView->width(); - qreal yScale = (_boundingRect.height() - 2 * frameWidth) / webView->height(); - proxyItem->scale(xScale, yScale); - proxyItem->setPos(frameWidth, frameWidth); +void ContentsChatItem::showWebPreview(const Clickable &click) { +#ifndef HAVE_WEBKIT + Q_UNUSED(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; + qreal height = line.height(); + qreal y = height * line.lineNumber(); - setZValue(30); -} + QPointF topLeft = scenePos() + QPointF(x, y); + QRectF urlRect = QRectF(topLeft.x(), topLeft.y(), width, height); -void ContentsChatItemPrivate::PreviewItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { - Q_UNUSED(option); Q_UNUSED(widget); - painter->setClipRect(boundingRect()); - painter->setPen(QPen(Qt::black, 5)); - painter->setBrush(Qt::black); - painter->setRenderHints(QPainter::Antialiasing); - painter->drawRoundedRect(boundingRect(), 10, 10); + QString url = data(ChatLineModel::DisplayRole).toString().mid(click.start, click.length); + if(!url.contains("://")) + url = "http://" + url; + chatScene()->loadWebPreview(this, url, urlRect); +#endif +} - painter->setPen(QPen(Qt::green)); - QString text = QString::number(zValue()); - painter->drawText(_boundingRect.center(), text); +void ContentsChatItem::clearWebPreview() { +#ifdef HAVE_WEBKIT + chatScene()->clearWebPreview(this); +#endif } -#endif // #ifdef HAVE_WEBKIT /*************************************************************************************************/ -ContentsChatItem::WrapColumnFinder::WrapColumnFinder(ChatItem *_item) +ContentsChatItem::WrapColumnFinder::WrapColumnFinder(const ChatItem *_item) : item(_item), - layout(0), wrapList(item->data(ChatLineModel::WrapListRole).value()), wordidx(0), lineCount(0), @@ -576,7 +682,6 @@ ContentsChatItem::WrapColumnFinder::WrapColumnFinder(ChatItem *_item) } ContentsChatItem::WrapColumnFinder::~WrapColumnFinder() { - delete layout; } qint16 ContentsChatItem::WrapColumnFinder::nextWrapColumn() { @@ -596,10 +701,10 @@ qint16 ContentsChatItem::WrapColumnFinder::nextWrapColumn() { // check if we have a very long word that needs inter word wrap if(wrapList.at(start).endX > targetWidth) { if(!line.isValid()) { - layout = item->createLayout(QTextOption::NoWrap); - layout->beginLayout(); - line = layout->createLine(); - layout->endLayout(); + item->initLayoutHelper(&layout, QTextOption::NoWrap); + layout.beginLayout(); + line = layout.createLine(); + layout.endLayout(); } return line.xToCursor(targetWidth, QTextLine::CursorOnCharacter); } @@ -628,3 +733,5 @@ qint16 ContentsChatItem::WrapColumnFinder::nextWrapColumn() { return -1; } +/*************************************************************************************************/ +