Draw Copy controls for code blocks and copy complete block on click
This commit is contained in:
1 parent
f7e82c04a7
commit
61aafac32d
3 files changed
+50
-2
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in new issue
Block a user