+ return fmt;
+}
+
+void ContentsChatItem::endHoverMode() {
+ if(privateData()) {
+ if(privateData()->currentClickable.isValid()) {
+ setCursor(Qt::ArrowCursor);
+ privateData()->currentClickable = Clickable();
+ }
+ clearWebPreview();
+ update();
+ }
+}
+
+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(QUrl::fromEncoded(str.toUtf8(), QUrl::TolerantMode));
+ break;
+ case Clickable::Channel: {
+ NetworkId networkId = Client::networkModel()->networkId(data(MessageModel::BufferIdRole).value<BufferId>());
+ 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::handleClick(pos, clickMode);
+}
+
+void ContentsChatItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) {
+ // mouse move events always mean we're not hovering anymore...
+ endHoverMode();
+ ChatItem::mouseMoveEvent(event);
+}
+
+void ContentsChatItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event) {
+ endHoverMode();
+ event->accept();
+}
+
+void ContentsChatItem::hoverMoveEvent(QGraphicsSceneHoverEvent *event) {
+ bool onClickable = false;
+ 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<BufferId>();
+ if(Client::networkModel()->bufferName(myId) != name)
+ onClickable = true;
+ }
+ if(onClickable) {
+ setCursor(Qt::PointingHandCursor);
+ privateData()->currentClickable = click;
+ update();
+ return;
+ }
+ }
+ if(!onClickable) endHoverMode();
+ event->accept();
+}
+
+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<void *>(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<BufferId>(), 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
+ 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);
+ if(!url.contains("://"))
+ url = "http://" + url;
+ chatScene()->loadWebPreview(this, url, urlRect);
+#endif
+}
+
+void ContentsChatItem::clearWebPreview() {
+#ifdef HAVE_WEBKIT
+ chatScene()->clearWebPreview(this);
+#endif
+}
+
+/*************************************************************************************************/
+
+ContentsChatItem::WrapColumnFinder::WrapColumnFinder(const ChatItem *_item)
+ : item(_item),
+ wrapList(item->data(ChatLineModel::WrapListRole).value<ChatLineModel::WrapList>()),
+ wordidx(0),
+ lineCount(0),
+ choppedTrailing(0)
+{
+}
+
+ContentsChatItem::WrapColumnFinder::~WrapColumnFinder() {
+}
+
+qint16 ContentsChatItem::WrapColumnFinder::nextWrapColumn() {
+ if(wordidx >= wrapList.count())
+ return -1;
+
+ lineCount++;
+ qreal targetWidth = lineCount * item->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);