/***************************************************************************
- * Copyright (C) 2005-2014 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 *
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *
***************************************************************************/
+#include "chatitem.h"
+
+#include <algorithm>
+#include <iterator>
+
#include <QApplication>
#include <QClipboard>
#include <QDesktopServices>
#include "buffermodel.h"
#include "bufferview.h"
-#include "chatitem.h"
#include "chatline.h"
#include "chatlinemodel.h"
#include "chatview.h"
#include "contextmenuactionprovider.h"
-#include "iconloader.h"
+#include "icon.h"
#include "mainwin.h"
#include "qtui.h"
#include "qtuistyle.h"
layout->setTextOption(option);
QList<QTextLayout::FormatRange> formatRanges
- = QtUi::style()->toTextLayoutList(formatList(), layout->text().length(), data(ChatLineModel::MsgLabelRole).toUInt());
+ = QtUi::style()->toTextLayoutList(formatList(), layout->text().length(), data(ChatLineModel::MsgLabelRole).value<UiStyle::MessageLabel>());
layout->setAdditionalFormats(formatRanges);
}
// }
// 2) draw MsgId over the time column
// if(column() == 0) {
-// QString msgIdString = QString::number(data(MessageModel::MsgIdRole).value<MsgId>().toInt());
+// QString msgIdString = QString::number(data(MessageModel::MsgIdRole).value<MsgId>().toLongLong());
// QPointF bottomPoint = boundingRect().bottomLeft();
// bottomPoint.ry() -= 2;
// painter->drawText(bottomPoint, msgIdString);
}
-void ChatItem::overlayFormat(UiStyle::FormatList &fmtList, int start, int end, quint32 overlayFmt) const
+void ChatItem::overlayFormat(UiStyle::FormatList &fmtList, quint16 start, quint16 end, UiStyle::FormatType overlayFmt) const
{
- for (int i = 0; i < fmtList.count(); i++) {
+ for (size_t i = 0; i < fmtList.size(); i++) {
int fmtStart = fmtList.at(i).first;
- int fmtEnd = (i < fmtList.count()-1 ? fmtList.at(i+1).first : data(MessageModel::DisplayRole).toString().length());
+ int fmtEnd = (i < fmtList.size()-1 ? fmtList.at(i+1).first : data(MessageModel::DisplayRole).toString().length());
if (fmtEnd <= start)
continue;
// split the format if necessary
if (fmtStart < start) {
- fmtList.insert(i, fmtList.at(i));
+ fmtList.insert(fmtList.begin() + i, fmtList.at(i));
fmtList[++i].first = start;
}
if (end < fmtEnd) {
- fmtList.insert(i, fmtList.at(i));
+ fmtList.insert(fmtList.begin() + i, fmtList.at(i));
fmtList[i+1].first = end;
}
- fmtList[i].second |= overlayFmt;
+ fmtList[i].second.type |= overlayFmt;
}
}
QVector<QTextLayout::FormatRange> ChatItem::additionalFormats() const
{
- return selectionFormats();
-}
+ // Calculate formats to overlay (only) if there's a selection, and/or a hovered clickable
+ if (!hasSelection() && !hasActiveClickable()) {
+ return {};
+ }
+ using Label = UiStyle::MessageLabel;
+ using Format = UiStyle::Format;
-QVector<QTextLayout::FormatRange> ChatItem::selectionFormats() const
-{
- if (!hasSelection())
- return QVector<QTextLayout::FormatRange>();
+ Label itemLabel = data(ChatLineModel::MsgLabelRole).value<Label>();
+ const auto &fmtList = formatList();
- int start, end;
- if (_selectionMode == FullSelection) {
- start = 0;
- end = data(MessageModel::DisplayRole).toString().length();
- }
- else {
- start = qMin(_selectionStart, _selectionEnd);
- end = qMax(_selectionStart, _selectionEnd);
- }
+ struct LabelFormat {
+ quint16 offset;
+ Format format;
+ Label label;
+ };
- UiStyle::FormatList fmtList = formatList();
+ // Transform formatList() into an extended list of LabelFormats
+ std::vector<LabelFormat> labelFmtList;
+ std::transform(fmtList.cbegin(), fmtList.cend(), std::back_inserter(labelFmtList), [itemLabel](const std::pair<quint16, Format> &f) {
+ return LabelFormat{f.first, f.second, itemLabel};
+ });
+ // Append dummy element to avoid special-casing handling the last real format
+ labelFmtList.push_back(LabelFormat{quint16(data(MessageModel::DisplayRole).toString().length()), Format(), itemLabel});
- while (fmtList.count() > 1 && fmtList.at(1).first <= start)
- fmtList.removeFirst();
+ // Apply the given label to the given range in the format list, splitting formats as necessary
+ auto applyLabel = [&labelFmtList](quint16 start, quint16 end, Label label) {
+ size_t i = 0;
- fmtList.first().first = start;
+ // Skip unaffected formats
+ for (; i < labelFmtList.size() - 1; ++i) {
+ if (labelFmtList[i+1].offset > start)
+ break;
+ }
+ // Range start doesn't align; split affected format and let the index point to the newly inserted copy
+ if (labelFmtList[i].offset < start) {
+ labelFmtList.insert(labelFmtList.begin() + i, labelFmtList[i]);
+ labelFmtList[++i].offset = start;
+ }
- while (fmtList.count() > 1 && fmtList.last().first >= end)
- fmtList.removeLast();
+ // Apply label to formats fully affected
+ for (; i < labelFmtList.size() - 1; ++i) {
+ if (labelFmtList[i+1].offset <= end) {
+ labelFmtList[i].label |= label;
+ continue;
+ }
+ // Last affected format, split if end of range doesn't align
+ if (labelFmtList[i+1].offset > end) {
+ labelFmtList.insert(labelFmtList.begin() + i, labelFmtList[i]);
+ labelFmtList[i].label |= label;
+ labelFmtList[i+1].offset = end;
+ }
+ break;
+ }
+ };
+
+ // Apply selection label
+ if (hasSelection()) {
+ quint16 start, end;
+ if (_selectionMode == FullSelection) {
+ start = 0;
+ end = data(MessageModel::DisplayRole).toString().length();
+ }
+ else {
+ start = qMin(_selectionStart, _selectionEnd);
+ end = qMax(_selectionStart, _selectionEnd);
+ }
+ applyLabel(start, end, Label::Selected);
+ }
+
+ // Apply hovered label
+ if (hasActiveClickable()) {
+ applyLabel(activeClickableRange().first, activeClickableRange().second, Label::Hovered);
+ }
+
+ // Add all formats that have an extra label to the additionalFormats list
+ QList<QTextLayout::FormatRange> additionalFormats;
+ for (size_t i = 0; i < labelFmtList.size() - 1; ++i) {
+ if (labelFmtList[i].label != itemLabel) {
+ additionalFormats << QtUi::style()->toTextLayoutList({std::make_pair(labelFmtList[i].offset, labelFmtList[i].format)},
+ labelFmtList[i+1].offset,
+ labelFmtList[i].label);
+ }
+ }
- return QtUi::style()->toTextLayoutList(fmtList, end, UiStyle::Selected|data(ChatLineModel::MsgLabelRole).toUInt()).toVector();
+ return additionalFormats.toVector();
}
}
+bool ChatItem::hasActiveClickable() const
+{
+ return false;
+}
+
+
+std::pair<quint16, quint16> ChatItem::activeClickableRange() const
+{
+ return {};
+}
+
+
QList<QRectF> ChatItem::findWords(const QString &searchWord, Qt::CaseSensitivity caseSensitive)
{
QList<QRectF> resultList;
QFontMetricsF *ContentsChatItem::fontMetrics() const
{
- return QtUi::style()->fontMetrics(data(ChatLineModel::FormatRole).value<UiStyle::FormatList>().at(0).second, 0);
+ return QtUi::style()->fontMetrics(data(ChatLineModel::FormatRole).value<UiStyle::FormatList>().at(0).second.type, UiStyle::MessageLabel::None);
}
}
+bool ContentsChatItem::hasActiveClickable() const
+{
+ return privateData()->currentClickable.isValid();
+}
+
+
+std::pair<quint16, quint16> ContentsChatItem::activeClickableRange() const
+{
+ const auto &clickable = privateData()->currentClickable;
+ if (clickable.isValid()) {
+ return {clickable.start(), clickable.start() + clickable.length()};
+ }
+ return {};
+}
+
+
Clickable ContentsChatItem::clickableAt(const QPointF &pos) const
{
return privateData()->clickables.atCursorPos(posToCursor(pos));
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);
+ overlayFormat(fmtList, click.start(), click.start() + click.length(), UiStyle::FormatType::Url);
}
}
return fmtList;
}
-QVector<QTextLayout::FormatRange> ContentsChatItem::additionalFormats() const
-{
- QVector<QTextLayout::FormatRange> fmt = ChatItem::additionalFormats();
- // mark a clickable if hovered upon
- if (privateData()->currentClickable.isValid()) {
- Clickable click = privateData()->currentClickable;
- QTextLayout::FormatRange f;
- f.start = click.start();
- f.length = click.length();
- f.format.setFontUnderline(true);
- fmt.append(f);
- }
- return fmt;
-}
-
-
void ContentsChatItem::endHoverMode()
{
if (privateData()) {
switch (click.type()) {
case Clickable::Url:
privateData()->activeClickable = click;
- menu->addAction(QIcon::fromTheme("edit-copy"), tr("Copy Link Address"),
+ menu->addAction(icon::get("edit-copy"), tr("Copy Link Address"),
&_actionProxy, SLOT(copyLinkToClipboard()))->setData(QVariant::fromValue<void *>(this));
break;
case Clickable::Channel:
void ContentsChatItem::showWebPreview(const Clickable &click)
{
-#ifndef HAVE_WEBKIT
+#if !defined HAVE_WEBKIT && !defined HAVE_WEBENGINE
Q_UNUSED(click);
#else
QTextLine line = layout()->lineForTextPosition(click.start());
void ContentsChatItem::clearWebPreview()
{
-#ifdef HAVE_WEBKIT
+#if defined HAVE_WEBKIT || defined HAVE_WEBENGINE
chatScene()->clearWebPreview(this);
#endif
}