diff --git a/tools/chatgui/src/chatview.cpp b/tools/chatgui/src/chatview.cpp index 9c0427890..774b60448 100644 --- a/tools/chatgui/src/chatview.cpp +++ b/tools/chatgui/src/chatview.cpp @@ -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(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); diff --git a/tools/chatgui/src/messagedelegate.cpp b/tools/chatgui/src/messagedelegate.cpp index e0e77d2c0..4c1fafd59 100644 --- a/tools/chatgui/src/messagedelegate.cpp +++ b/tools/chatgui/src/messagedelegate.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -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(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); diff --git a/tools/chatgui/src/messagedelegate.h b/tools/chatgui/src/messagedelegate.h index e7141f757..60e2086d1 100644 --- a/tools/chatgui/src/messagedelegate.h +++ b/tools/chatgui/src/messagedelegate.h @@ -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 m_downloadCb;