Draw Copy controls for code blocks and copy complete block on click

This commit is contained in:
evgeny committed 2026-11-02 23:50:42 +01:00
1 parent f7e82c04a7
commit 61aafac32d
3 files changed
+50 -2

No files matched your search

+6 -1
View File
@@ -187,7 +187,12 @@ void ChatView::mouseMoveEvent(QMouseEvent *event) {
if (!m_selecting) {
QModelIndex idx = indexAt(viewport()->mapFrom(this, event->pos()));
const bool link = m_linkHitTest && !m_linkHitTest(idx, event->pos()).isEmpty();
viewport()->setCursor(link ? Qt::PointingHandCursor : Qt::ArrowCursor);
bool copyCode = false;
if (auto* delegate = qobject_cast<MessageDelegate*>(itemDelegate())) {
QStyleOptionViewItem option; option.initFrom(this); option.widget = this; option.rect = visualRect(idx);
copyCode = delegate->codeCopyAt(option, idx, event->pos()) >= 0;
}
viewport()->setCursor(link || copyCode ? Qt::PointingHandCursor : Qt::ArrowCursor);
if (idx != QModelIndex(m_lastHovered)) {
m_lastHovered = idx;
const auto paths = animatedEmojiPaths(idx);
+38 -1
View File
@@ -13,6 +13,7 @@
#include <QPainter>
#include <QPainterPath>
#include <QApplication>
#include <QClipboard>
#include <QAbstractItemView>
#include <QFontMetrics>
#include <QTextLayout>
@@ -92,6 +93,19 @@ QString MessageDelegate::linkAt(const QStyleOptionViewItem& option, const QModel
return {};
}
int MessageDelegate::codeCopyAt(const QStyleOptionViewItem& option, const QModelIndex& index, const QPoint& point) const {
if (!index.isValid()) return -1;
const auto message = formattedMessageText(index);
if (!message || message->code.isEmpty()) return -1;
const auto body = calcLayout(option, index);
QTextLayout layout; layout.setFont(option.font);
layoutFormattedMessage(layout, *message, body.textRect.width(), false);
const QPoint local = point - option.rect.topLeft() - body.textRect.topLeft();
for (int i = 0; i < message->code.size(); ++i)
if (messageCodeCopyRect(layout, message->code[i], body.textRect.width()).contains(local)) return i;
return -1;
}
MessageDelegate::MessageDelegate(QObject *parent)
: QStyledItemDelegate(parent) {
GUI_DEBUG("MessageDelegate: avatar size=%dpx", kAvatar);
@@ -147,6 +161,7 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
const auto* mouse = static_cast<QMouseEvent*>(event);
if (mouse->button() != Qt::LeftButton) return false;
const QPoint point = mouse->pos() - option.rect.topLeft();
const int codeBlock = codeCopyAt(option, index, mouse->pos());
const bool onQuote = L.quoteRect.contains(point);
int reaction = L.reactionHeart.contains(point) ? -1 : -2;
for (const auto& chip : L.reactionChips) if (chip.rect.contains(point)) { reaction = chip.id; break; }
@@ -158,12 +173,23 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
&& (!L.reactionHeart.isValid() || point.y() < L.reactionHeart.top());
const bool onMedia = (L.isVoice || L.isFile || L.isVideo || L.isImage) && L.bubbleRect.contains(point);
if (event->type()==QEvent::MouseButtonPress) {
m_codePress = codeBlock >= 0 ? index : QModelIndex(); m_codePressPosition = mouse->pos(); m_codePressBlock = codeBlock;
m_codePressSource = codeBlock >= 0 ? index.data(MsgTextRole).toString() : QString();
m_mediaPress = onMedia ? index : QModelIndex(); m_mediaPressPosition = mouse->pos();
m_quotePress = onQuote ? index : QModelIndex(); m_quotePressPosition = mouse->pos();
m_reactionPress = onReaction ? index : QModelIndex(); m_pressedReaction = reaction; m_reactionPressPosition = mouse->pos();
m_avatarPress=onAvatar ? index : QModelIndex(); m_avatarPressPosition=mouse->pos();
m_pollPress = onPoll ? index : QModelIndex(); m_pollPressPosition = mouse->pos();
return onQuote || onAvatar || onReaction || onPoll;
return codeBlock >= 0 || onQuote || onAvatar || onReaction || onPoll;
}
const bool copyCode = codeBlock >= 0 && m_codePress == index && m_codePressBlock == codeBlock
&& (mouse->pos() - m_codePressPosition).manhattanLength() < 5 && m_codePressSource == index.data(MsgTextRole).toString();
m_codePress = QPersistentModelIndex(); m_codePressBlock = -1; m_codePressSource.clear();
if (copyCode) {
const auto& block = L.formattedText->code[codeBlock];
QApplication::clipboard()->setText(L.formattedText->text.mid(block.start, block.length), QClipboard::Clipboard);
GUI_INFO("message code copied: row=%d block=%d chars=%d", index.row(), codeBlock, block.length);
return true;
}
const bool activateMedia = onMedia && m_mediaPress == index && (mouse->pos() - m_mediaPressPosition).manhattanLength() < 5;
m_mediaPress = QPersistentModelIndex();
@@ -1587,6 +1613,17 @@ void MessageDelegate::paintBody(QPainter* painter, const QStyleOptionViewItem& o
selections.append(range);
}
layout.draw(painter, L.textRect.topLeft(), selections);
if (L.formattedText) {
for (const auto& block : L.formattedText->code) {
const auto button = messageCodeCopyRect(layout, block, L.textRect.width()).translated(L.textRect.topLeft());
painter->fillRect(QRectF(L.textRect.x(), button.top() - 2, L.textRect.width(), button.height() + 4),
option.palette.alternateBase());
painter->setPen(option.palette.mid().color()); painter->setBrush(option.palette.button());
painter->drawRoundedRect(button.adjusted(0.5, 0.5, -0.5, -0.5), 4, 4);
painter->setPen(option.palette.buttonText().color());
painter->drawText(button, Qt::AlignCenter, QCoreApplication::translate("ChatGui", "Copy"));
}
}
if (L.formattedText)
drawAnimatedEmojiOverlay(painter, textFont, L.formattedText->text, L.textRect.width(), L.textRect, &layout, L.formattedText.data());
} else painter->drawText(L.textRect, Qt::AlignLeft | Qt::TextWordWrap, msgText);
+6
View File
@@ -79,6 +79,8 @@ public:
const QModelIndex &index) override;
// Полная ссылка под point в viewport (включая сокращённый invite), либо пусто вне текста/ссылки.
QString linkAt(const QStyleOptionViewItem& option, const QModelIndex& index, const QPoint& point) const;
// Номер блока под кнопкой Copy в viewport, -1 вне кнопок; тот же layout, что при рисовании.
int codeCopyAt(const QStyleOptionViewItem& option, const QModelIndex& index, const QPoint& point) const;
// Привязать положительную длительность и живой PCM id к filePath после успешного VoicePlayer callback.
static void startAutoPlay(const QString& filePath, float actualDurationSec, uint64_t playbackId);
@@ -157,6 +159,10 @@ private:
QPersistentModelIndex m_quotePress;
QPoint m_quotePressPosition;
QPersistentModelIndex m_mediaPress; // Строка press; инвалидируется при удалении/сбросе модели.
QPersistentModelIndex m_codePress; // Строка нажатой Copy; копируем лишь тот же неизменённый блок при click без drag.
QPoint m_codePressPosition;
int m_codePressBlock = -1;
QString m_codePressSource;
QPoint m_mediaPressPosition; // Порог клика <5px, как для цитат/реакций.
std::function<bool(const QModelIndex&)> m_downloadCb;