|
|
|
|
@ -27,18 +27,25 @@
|
|
|
|
|
#include <QRegularExpression> |
|
|
|
|
|
|
|
|
|
/* Один layout для размеров, отрисовки и hit-test длинных invite-ссылок. */ |
|
|
|
|
static qreal layoutInviteText(QTextLayout& layout, int width) { |
|
|
|
|
static qreal layoutInviteText(QTextLayout& layout, int width, const QString& compactLink = {}) { |
|
|
|
|
int linkStart = -1; |
|
|
|
|
if (!compactLink.isEmpty() && layout.text().endsWith(compactLink)) { |
|
|
|
|
linkStart = layout.text().length() - compactLink.length(); |
|
|
|
|
const QString shortened = QFontMetrics(layout.font()).elidedText(compactLink, Qt::ElideRight, qMax(1, width - 1)); |
|
|
|
|
layout.setText(layout.text().left(linkStart) + shortened); |
|
|
|
|
} |
|
|
|
|
QTextOption option; option.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); |
|
|
|
|
layout.setTextOption(option); |
|
|
|
|
static const QRegularExpression pattern(QStringLiteral("utun://[A-Za-z0-9+/]+={0,2}")); |
|
|
|
|
QVector<QTextLayout::FormatRange> formats; |
|
|
|
|
auto matches = pattern.globalMatch(layout.text()); |
|
|
|
|
while (matches.hasNext()) { |
|
|
|
|
auto match = matches.next(); |
|
|
|
|
while (linkStart >= 0 || matches.hasNext()) { |
|
|
|
|
QTextLayout::FormatRange range; |
|
|
|
|
range.start = match.capturedStart(); range.length = match.capturedLength(); |
|
|
|
|
if (linkStart >= 0) { range.start = linkStart; range.length = layout.text().length() - linkStart; } |
|
|
|
|
else { auto match = matches.next(); range.start = match.capturedStart(); range.length = match.capturedLength(); } |
|
|
|
|
range.format.setForeground(QColor(0x33, 0x90, 0xEC)); range.format.setFontUnderline(true); |
|
|
|
|
formats.append(range); |
|
|
|
|
if (linkStart >= 0) break; |
|
|
|
|
} |
|
|
|
|
layout.setFormats(formats); |
|
|
|
|
layout.beginLayout(); |
|
|
|
|
@ -61,14 +68,16 @@ QString MessageDelegate::inviteLinkAt(const QStyleOptionViewItem& option, const
|
|
|
|
|
if (!QRect(QPoint(0, 0), geometry.textRect.size()).contains(local)) return {}; |
|
|
|
|
QString display = text; display.replace('\n', QChar(0x2028)); |
|
|
|
|
QTextLayout layout(display, option.font); |
|
|
|
|
layoutInviteText(layout, geometry.textRect.width()); |
|
|
|
|
const QString compactLink = index.data(MsgInviteLinkRole).toString(); |
|
|
|
|
layoutInviteText(layout, geometry.textRect.width(), compactLink); |
|
|
|
|
for (int i = 0; i < layout.lineCount(); ++i) { |
|
|
|
|
const auto line = layout.lineAt(i); |
|
|
|
|
if (local.y() < line.y() || local.y() >= line.y() + line.height()) continue; |
|
|
|
|
if (local.x() < line.x() || local.x() >= line.x() + line.naturalTextWidth()) return {}; |
|
|
|
|
const int position = line.xToCursor(local.x(), QTextLine::CursorOnCharacter); |
|
|
|
|
for (const auto& range : layout.formats()) { |
|
|
|
|
if (position >= range.start && position < range.start + range.length) return text.mid(range.start, range.length); |
|
|
|
|
if (position >= range.start && position < range.start + range.length) |
|
|
|
|
return compactLink.isEmpty() ? text.mid(range.start, range.length) : compactLink; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return {}; |
|
|
|
|
@ -314,12 +323,12 @@ QSize MessageDelegate::sizeHint(const QStyleOptionViewItem &option,
|
|
|
|
|
return QSize(L.viewWidth, L.totalHeight); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
static int measureTextHeight(const QFont &font, const QString &text, int width) { |
|
|
|
|
static int measureTextHeight(const QFont &font, const QString &text, int width, const QString& compactLink = {}) { |
|
|
|
|
if (text.isEmpty() || width <= 0) return 0; |
|
|
|
|
if (text.contains(QStringLiteral("utun://"))) { |
|
|
|
|
QString display = text; display.replace('\n', QChar(0x2028)); |
|
|
|
|
QTextLayout layout(display, font); |
|
|
|
|
return (int)std::ceil(layoutInviteText(layout, width)); |
|
|
|
|
return (int)std::ceil(layoutInviteText(layout, width, compactLink)); |
|
|
|
|
} |
|
|
|
|
QRect r = QFontMetrics(font).boundingRect(QRect(0, 0, width, 100000), |
|
|
|
|
Qt::AlignLeft | Qt::TextWordWrap, text); |
|
|
|
|
@ -614,7 +623,7 @@ MessageDelegate::Layout MessageDelegate::calcLayout(
|
|
|
|
|
qH = kQuotePadV + qOneLineH + (qText.isEmpty() ? 0 : 2 + qTextH) + kQuotePadV; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
int textH = measureTextHeight(option.font, text, textW); |
|
|
|
|
int textH = measureTextHeight(option.font, text, textW, index.data(MsgInviteLinkRole).toString()); |
|
|
|
|
int authorH = fm.height(); |
|
|
|
|
int bubbleH = L.isOutgoing |
|
|
|
|
? kPadTop + (hasQuote ? qH + kQuoteGap : 0) + textH + kStatusH + kPadBot |
|
|
|
|
@ -1371,11 +1380,14 @@ void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio
|
|
|
|
|
painter->setFont(textFont); |
|
|
|
|
|
|
|
|
|
QString msgText = index.data(MsgTextRole).toString(); |
|
|
|
|
QString overlayText = msgText; |
|
|
|
|
const bool hasInvite = msgText.contains(QStringLiteral("utun://")); |
|
|
|
|
if (hasInvite) { |
|
|
|
|
QString display = msgText; display.replace('\n', QChar(0x2028)); |
|
|
|
|
QTextLayout layout(display, textFont); |
|
|
|
|
layoutInviteText(layout, L.textRect.width()); |
|
|
|
|
const QString compactLink = index.data(MsgInviteLinkRole).toString(); |
|
|
|
|
layoutInviteText(layout, L.textRect.width(), compactLink); |
|
|
|
|
overlayText = layout.text(); overlayText.replace(QChar(0x2028), '\n'); |
|
|
|
|
QVector<QTextLayout::FormatRange> selections; |
|
|
|
|
if (singleTextSel && cv) { |
|
|
|
|
auto cursorAt = [&](const QPoint& point) -> int { |
|
|
|
|
@ -1383,14 +1395,22 @@ void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio
|
|
|
|
|
const auto line = layout.lineAt(i); |
|
|
|
|
if (point.y() < line.y() + line.height()) return line.xToCursor(point.x()); |
|
|
|
|
} |
|
|
|
|
return msgText.length(); |
|
|
|
|
return layout.text().length(); |
|
|
|
|
}; |
|
|
|
|
const auto& selection = cv->textSelection(); |
|
|
|
|
int start = cursorAt(selection.clickPos - L.textRect.topLeft()); |
|
|
|
|
int end = cursorAt(selection.dragPos - L.textRect.topLeft()); |
|
|
|
|
if (start > end) qSwap(start, end); |
|
|
|
|
if (start == end && end < msgText.length()) ++end; |
|
|
|
|
const_cast<ChatView*>(cv)->setTextSelChars(start, end); |
|
|
|
|
if (start == end && end < layout.text().length()) ++end; |
|
|
|
|
int copyStart = start, copyEnd = end; |
|
|
|
|
if (!compactLink.isEmpty()) { |
|
|
|
|
const int linkStart = msgText.length() - compactLink.length(); |
|
|
|
|
if (end > linkStart) { |
|
|
|
|
if (start >= linkStart) { start = linkStart; copyStart = linkStart; } |
|
|
|
|
end = layout.text().length(); copyEnd = msgText.length(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
const_cast<ChatView*>(cv)->setTextSelChars(copyStart, copyEnd); |
|
|
|
|
QTextLayout::FormatRange range; |
|
|
|
|
range.start = start; range.length = end - start; range.format.setBackground(QColor(0x33, 0x90, 0xEC, 60)); |
|
|
|
|
selections.append(range); |
|
|
|
|
@ -1414,7 +1434,7 @@ void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio
|
|
|
|
|
startChar, endChar); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
drawAnimatedEmojiOverlay(painter, textFont, msgText, |
|
|
|
|
drawAnimatedEmojiOverlay(painter, textFont, overlayText, |
|
|
|
|
L.textRect.width(), L.textRect); |
|
|
|
|
|
|
|
|
|
drawStatusBar(painter, L, index, option); |
|
|
|
|
|