X-Git-Url: https://git.quassel-irc.org/?p=quassel.git;a=blobdiff_plain;f=src%2Fqtui%2Fchatitem.cpp;h=499ae7740b580e5c31a48ced0ab20b579034d5ba;hp=3d2e065ebe33fd817833a3764896e1abac58b6fa;hb=55579f53f3bd37f2a20d9be7458bdc54b345a052;hpb=1d145922198e7e5d727c9bca8a92f95cf40d02ef diff --git a/src/qtui/chatitem.cpp b/src/qtui/chatitem.cpp index 3d2e065e..499ae774 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 * @@ -28,15 +28,18 @@ #include #include - +#include "buffermodel.h" +#include "bufferview.h" #include "chatitem.h" #include "chatlinemodel.h" +#include "contextmenuactionprovider.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) @@ -46,10 +49,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()) { @@ -59,8 +58,25 @@ QVariant ChatItem::data(int role) const { return model()->data(index, role); } -QTextLayout *ChatItem::createLayout(QTextOption::WrapMode wrapMode, Qt::Alignment alignment) const { - QTextLayout *layout = new QTextLayout(data(MessageModel::DisplayRole).toString()); +qint16 ChatItem::posToCursor(const QPointF &pos) const { + if(pos.y() > height()) return data(MessageModel::DisplayRole).toString().length(); + if(pos.y() < 0) return 0; + + 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; +} + +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); @@ -68,45 +84,47 @@ QTextLayout *ChatItem::createLayout(QTextOption::WrapMode wrapMode, Qt::Alignmen layout->setTextOption(option); QList formatRanges - = QtUi::style()->toTextLayoutList(data(MessageModel::FormatRole).value(), layout->text().length()); + = QtUi::style()->toTextLayoutList(formatList(), layout->text().length(), data(ChatLineModel::MsgLabelRole).toUInt()); layout->setAdditionalFormats(formatRanges); - return layout; } -void ChatItem::doLayout() { - QTextLayout *layout_ = layout(); - layout_->beginLayout(); - QTextLine line = layout_->createLine(); +UiStyle::FormatList ChatItem::formatList() const { + return data(MessageModel::FormatRole).value(); +} + +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; -} +void ChatItem::paintBackground(QPainter *painter) { + painter->setClipRect(boundingRect()); // no idea why QGraphicsItem clipping won't work -ChatItemPrivate *ChatItem::privateData() const { - if(!_data) { - ChatItem *that = const_cast(this); - that->_data = that->newPrivateData(); - that->doLayout(); - } - return _data; + 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); - painter->setClipRect(boundingRect()); // no idea why QGraphicsItem clipping won't work - QVector formats = additionalFormats(); - QTextLayout::FormatRange selectFmt = selectionFormat(); - if(selectFmt.format.isValid()) formats.append(selectFmt); - layout()->draw(painter, QPointF(0,0), formats, boundingRect()); + paintBackground(painter); + + QTextLayout layout; + initLayout(&layout); + layout.draw(painter, QPointF(0,0), additionalFormats(), boundingRect()); + + // layout()->draw(painter, QPointF(0,0), formats, boundingRect()); // Debuging Stuff // uncomment partially or all of the following stuff: @@ -136,16 +154,82 @@ void ChatItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, // 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; - 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); +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; } - return 0; +} + +QVector ChatItem::additionalFormats() const { + return selectionFormats(); +} + +QVector ChatItem::selectionFormats() const { + if(!hasSelection()) + return QVector(); + + int start, end; + if(_selectionMode == FullSelection) { + start = 0; + end = data(MessageModel::DisplayRole).toString().length(); + } else { + start = qMin(_selectionStart, _selectionEnd); + end = qMax(_selectionStart, _selectionEnd); + } + + UiStyle::FormatList fmtList = formatList(); + + while(fmtList.count() > 1 && fmtList.at(1).first <= start) + fmtList.removeFirst(); + + fmtList.first().first = start; + + while(fmtList.count() > 1 && fmtList.last().first >= end) + fmtList.removeLast(); + + return QtUi::style()->toTextLayoutList(fmtList, end, UiStyle::Selected|data(ChatLineModel::MsgLabelRole).toUInt()).toVector(); +} + +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() { @@ -156,8 +240,10 @@ void ChatItem::setFullSelection() { } void ChatItem::clearSelection() { - _selectionMode = NoSelection; - update(); + if(_selectionMode != NoSelection) { + _selectionMode = NoSelection; + update(); + } } void ChatItem::continueSelecting(const QPointF &pos) { @@ -166,23 +252,14 @@ void ChatItem::continueSelecting(const QPointF &pos) { update(); } -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; +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 selectFmt; + return false; } QList ChatItem::findWords(const QString &searchWord, Qt::CaseSensitivity caseSensitive) { @@ -199,10 +276,10 @@ QList ChatItem::findWords(const QString &searchWord, Qt::CaseSensitivity searchIdx = plainText.indexOf(searchWord, searchIdx + 1, caseSensitive); } - bool hadPrivateData = hasPrivateData(); - + 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(); @@ -210,20 +287,16 @@ QList ChatItem::findWords(const QString &searchWord, Qt::CaseSensitivity resultList << QRectF(x, y, width, height); } - if(!hadPrivateData) - 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(); } } @@ -246,16 +319,25 @@ 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(pos); + + GraphicalUi::contextMenuActionProvider()->addActions(menu, chatScene()->filter(), data(MessageModel::BufferIdRole).value()); } // ************************************************************ @@ -264,24 +346,24 @@ void ChatItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { void SenderChatItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { Q_UNUSED(option); Q_UNUSED(widget); + paintBackground(painter); - painter->setClipRect(boundingRect()); // no idea why QGraphicsItem clipping won't work - qreal layoutWidth = layout()->minimumWidth(); + 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()); + QPixmap pixmap(layout.boundingRect().toRect().size()); pixmap.fill(Qt::transparent); QPainter pixPainter(&pixmap); - layout()->draw(&pixPainter, QPointF(qMax(offset, (qreal)0), 0), QVector() << selectFmt); + layout.draw(&pixPainter, QPointF(qMax(offset, (qreal)0), 0), additionalFormats()); pixPainter.end(); // Create alpha channel mask @@ -303,128 +385,107 @@ void SenderChatItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *op pixmap.setAlphaChannel(mask); painter->drawPixmap(0, 0, pixmap); } else { - layout()->draw(painter, QPointF(0,0), QVector() << selectFmt, boundingRect()); + layout.draw(painter, QPointF(0,0), additionalFormats(), 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()); - _fontMetrics = QtUi::style()->fontMetrics(model_->data(index, ChatLineModel::FormatRole).value().at(0).second); - setGeometryByWidth(width); } +QFontMetricsF *ContentsChatItem::fontMetrics() const { + return QtUi::style()->fontMetrics(data(ChatLineModel::FormatRole).value().at(0).second, 0); +} + +ContentsChatItem::~ContentsChatItem() { + delete _data; +} + +ContentsChatItemPrivate *ContentsChatItem::privateData() const { + if(!_data) { + ContentsChatItem *that = const_cast(this); + 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); - // compute height - int lines = 1; - WrapColumnFinder finder(this); - while(finder.nextWrapColumn() > 0) - lines++; - setHeight(lines * fontMetrics()->lineSpacing()); + setGeometry(w, h); } - return height(); + return h; } -void ContentsChatItem::doLayout() { +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()); + int col = finder.nextWrapColumn(width()); + 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() 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("((?:(?:https?://|s?ftp://|irc://|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", 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(); - - QList result; - qint16 idx = 0; - qint16 minidx; - int type = -1; +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(); +} - 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] = str.indexOf(regExp[i], 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]; - 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]--; - } - result.append(Clickable((Clickable::Type)type, matches[type], matchEnd[type] - matches[type])); +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); } - } while(type >= 0); - - /* testing - if(!result.isEmpty()) qDebug() << str; - foreach(Clickable click, result) { - qDebug() << str.mid(click.start, click.length); } - */ - return result; + 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); } @@ -432,7 +493,7 @@ QVector ContentsChatItem::additionalFormats() const { } void ContentsChatItem::endHoverMode() { - if(hasPrivateData()) { + if(privateData()) { if(privateData()->currentClickable.isValid()) { setCursor(Qt::ArrowCursor); privateData()->currentClickable = Clickable(); @@ -442,41 +503,60 @@ void ContentsChatItem::endHoverMode() { } } -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) { + 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(QUrl::fromEncoded(str.toAscii())); + 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(hasPrivateData() && !privateData()->hasDragged && event->buttons() & Qt::LeftButton - && (event->buttonDownScreenPos(Qt::LeftButton) - event->screenPos()).manhattanLength() >= QApplication::startDragDistance()) - privateData()->hasDragged = true; ChatItem::mouseMoveEvent(event); } @@ -487,66 +567,83 @@ 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; - showWebPreview(click); - } 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(); } -void ContentsChatItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) { - 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) { - QMenu menu; - QAction *copyToClipboard = menu.addAction(QObject::tr("Copy to Clipboard")); - QAction *selected = menu.exec(event->screenPos()); - if(selected == copyToClipboard) { - QString url = data(ChatLineModel::DisplayRole).toString().mid(click.start, click.length); -# ifdef Q_WS_X11 - QApplication::clipboard()->setText(url, QClipboard::Selection); -# endif -//# else - QApplication::clipboard()->setText(url); -//# endif - } +void ContentsChatItem::addActionsToMenu(QMenu *menu, const QPointF &pos) { + 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()); + GraphicalUi::contextMenuActionProvider()->addActions(menu, chatScene()->filter(), data(MessageModel::BufferIdRole).value(), name); + break; } + default: + break; } + } else { + // Buffer-specific actions + ChatItem::addActionsToMenu(menu, 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(!url.contains("://")) + url = "http://" + url; + chatScene()->stringToClipboard(url); + } +} + +/******** WEB PREVIEW *****************************************************************************/ + void ContentsChatItem::showWebPreview(const Clickable &click) { #ifndef HAVE_WEBKIT Q_UNUSED(click); #else - QTextLine line = layout()->lineForTextPosition(click.start); - qreal x = line.cursorToX(click.start); - qreal width = line.cursorToX(click.start + click.length) - x; + 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(); 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); @@ -561,9 +658,8 @@ void ContentsChatItem::clearWebPreview() { /*************************************************************************************************/ -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), @@ -572,15 +668,14 @@ ContentsChatItem::WrapColumnFinder::WrapColumnFinder(ChatItem *_item) } ContentsChatItem::WrapColumnFinder::~WrapColumnFinder() { - delete layout; } -qint16 ContentsChatItem::WrapColumnFinder::nextWrapColumn() { +qint16 ContentsChatItem::WrapColumnFinder::nextWrapColumn(qreal width) { if(wordidx >= wrapList.count()) return -1; lineCount++; - qreal targetWidth = lineCount * item->width() + choppedTrailing; + qreal targetWidth = lineCount * width + choppedTrailing; qint16 start = wordidx; qint16 end = wrapList.count() - 1; @@ -592,10 +687,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); } @@ -624,3 +719,5 @@ qint16 ContentsChatItem::WrapColumnFinder::nextWrapColumn() { return -1; } +/*************************************************************************************************/ +