Elide Qt invitation links with consistent layout and full-address selection
This commit is contained in:
1 parent
12e09cb0ed
commit
6ad89a4f6a
2 files changed
+43
-14
No files matched your search
@@ -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);
|
||||
|
||||
@@ -1151,6 +1151,14 @@ void MessageList::onDmMessages(const uint8_t* data, int len) {
|
||||
qint64 ts = (qint64)message.value("ts").toDouble();
|
||||
quint64 author = message.value("author").toString().toULongLong();
|
||||
QString text = message.value("data").toString();
|
||||
const bool invite = message.value("ct").toString() == QStringLiteral("invite");
|
||||
const QString inviteLink = invite ? text : QString();
|
||||
if (invite) {
|
||||
const QString groupName = message.value("tags").toObject().value("group_name").toString();
|
||||
text = tr("Приглашение в группу «%1»").arg(groupName) + '\n' + inviteLink;
|
||||
GUI_DEBUG("invite PM: rendered conv=%s seq=%lld group_name_bytes=%d link_bytes=%d", qPrintable(m_currentChannelId),
|
||||
(long long)seq, (int)groupName.toUtf8().size(), (int)inviteLink.size());
|
||||
}
|
||||
bool file = message.value("ct").toString() == "file";
|
||||
QString state = message.value("media_status").toString();
|
||||
QString status;
|
||||
@@ -1181,6 +1189,7 @@ void MessageList::onDmMessages(const uint8_t* data, int len) {
|
||||
item->setData(m_currentChannelId, MsgChannelIdRole);
|
||||
QString type = message.value("content_type").toString(QStringLiteral("text/plain"));
|
||||
item->setData(type, MsgContentTypeRole);
|
||||
if (invite) item->setData(inviteLink, MsgInviteLinkRole);
|
||||
if (file) {
|
||||
QString path = message.value("media_path").toString();
|
||||
item->setData(path, MsgVoiceFileRole);
|
||||
|
||||
Reference in new issue
Block a user