You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

249 lines
8.6 KiB

#include "chatview.h"
#include "messagedelegate.h"
#include "emoji.h"
#include "debug_ui.h"
#include <QPainter>
#include <QClipboard>
#include <QApplication>
#include <QDebug>
#include <QMouseEvent>
#include <QKeyEvent>
#include <QScrollBar>
// Текст и реакции сообщения используют общий набор анимированных значков.
static QSet<QString> animatedEmojiPaths(const QModelIndex &index) {
QSet<QString> paths;
const QString text = index.data(MsgTextRole).toString();
for (auto it = g_animatedEmojiMap.constBegin(); it != g_animatedEmojiMap.constEnd(); ++it) {
if (text.contains(it.key())) paths.insert(it.value());
}
for (const auto &reaction : index.data(MsgReactionsRole).toList()) {
const QString path = g_animatedEmojiMap.value(reaction.toMap().value("emoji").toString());
if (!path.isEmpty()) paths.insert(path);
}
return paths;
}
ChatView::ChatView(QWidget *parent)
: QListView(parent)
{
setMouseTracking(true);
m_scrollDownBtn = new QPushButton(QStringLiteral("\u25BC"), this);
m_scrollDownBtn->setFixedSize(36, 36);
m_scrollDownBtn->setCursor(Qt::PointingHandCursor);
m_scrollDownBtn->setStyleSheet(
"QPushButton {"
" background: rgba(60,60,60,180);"
" color: #ffffff;"
" border: none;"
" border-radius: 18px;"
" font-size: 16px;"
"}"
"QPushButton:hover {"
" background: rgba(90,90,90,220);"
"}"
);
m_scrollDownBtn->setToolTip(tr("Scroll to bottom"));
m_scrollDownBtn->setVisible(false);
connect(m_scrollDownBtn, &QPushButton::clicked, this, [this]() {
scrollToBottom();
});
connect(verticalScrollBar(), &QScrollBar::valueChanged, this, &ChatView::updateScrollDownBtn);
connect(verticalScrollBar(), &QScrollBar::rangeChanged, this, &ChatView::updateScrollDownBtn);
m_scrollDownBtn->raise();
}
void ChatView::setBackgroundImage(const QString &path) {
if (!m_background.load(path)) {
qWarning() << "ChatView: failed to load background" << path;
return;
}
viewport()->update();
}
void ChatView::resizeEvent(QResizeEvent *event) {
QListView::resizeEvent(event);
scheduleDelayedItemsLayout();
updateScrollDownBtn();
}
void ChatView::scrollContentsBy(int dx, int dy) {
QListView::scrollContentsBy(dx, dy);
viewport()->update();
}
void ChatView::paintEvent(QPaintEvent *event) {
if (!m_background.isNull()) {
QPainter p(viewport());
p.drawTiledPixmap(viewport()->rect(), m_background);
}
QListView::paintEvent(event);
QSet<QString> visible;
if (model()) {
const QRect area = viewport()->rect();
for (int row = 0; row < model()->rowCount(); ++row) {
const QModelIndex index = model()->index(row, 0);
if (!visualRect(index).intersects(area)) continue;
const QString message = QStringLiteral("%1/%2/%3/")
.arg(index.data(MsgChannelIdRole).toString()).arg(index.data(MsgIdRole).toLongLong())
.arg(index.data(MsgIsOutgoingRole).toInt());
for (const auto &path : animatedEmojiPaths(index)) visible.insert(message + path);
}
}
const int appeared = (visible - m_visibleAnimations).size();
m_visibleAnimations = visible;
if (appeared > 0) {
GUI_DEBUG("ChatView: visible animation trigger appeared=%d visible=%d", appeared, (int)visible.size());
emit animationTriggered();
}
}
bool ChatView::isRowSelected(const QModelIndex &idx) const {
if (!m_selStart.isValid() || !m_selEnd.isValid() || !idx.isValid())
return false;
int r = idx.row();
int a = m_selStart.row();
int b = m_selEnd.row();
if (a > b) qSwap(a, b);
return r >= a && r <= b;
}
void ChatView::mousePressEvent(QMouseEvent *event) {
if (event->button() != Qt::LeftButton) {
QListView::mousePressEvent(event);
return;
}
if (hasSelection()) {
m_selStart = QPersistentModelIndex();
m_selEnd = QPersistentModelIndex();
m_textSel = ChatTextSelection{};
m_selecting = false;
viewport()->update();
}
QModelIndex idx = indexAt(viewport()->mapFrom(this, event->pos()));
m_pressIndex = QPersistentModelIndex(idx);
m_pressPos = event->pos();
QListView::mousePressEvent(event);
}
bool ChatView::viewportEvent(QEvent *event) {
if (event->type() == QEvent::Leave && m_lastHovered.isValid()) {
m_lastHovered = QPersistentModelIndex();
}
return QListView::viewportEvent(event);
}
void ChatView::mouseMoveEvent(QMouseEvent *event) {
if (!m_selecting && m_pressIndex.isValid()) {
int dx = event->pos().x() - m_pressPos.x();
int dy = event->pos().y() - m_pressPos.y();
if (dx * dx + dy * dy >= 25) {
m_selStart = m_pressIndex;
m_selEnd = m_pressIndex;
m_selecting = true;
QRect r = visualRect(QModelIndex(m_pressIndex));
m_textSel.index = m_pressIndex;
m_textSel.clickPos = m_pressPos - r.topLeft();
m_textSel.dragPos = m_textSel.clickPos;
m_textSel.active = true;
viewport()->update();
}
}
if (!m_selecting) {
QModelIndex idx = indexAt(viewport()->mapFrom(this, event->pos()));
const bool link = m_inviteHitTest && !m_inviteHitTest(idx, event->pos()).isEmpty();
viewport()->setCursor(link ? Qt::PointingHandCursor : Qt::ArrowCursor);
if (idx != QModelIndex(m_lastHovered)) {
m_lastHovered = idx;
const auto paths = animatedEmojiPaths(idx);
if (!paths.isEmpty()) {
GUI_DEBUG("ChatView: hover animation trigger row=%d icons=%d", idx.row(), (int)paths.size());
emit animationTriggered();
}
}
}
if (!m_selecting) {
QListView::mouseMoveEvent(event);
return;
}
QModelIndex idx = indexAt(viewport()->mapFrom(this, event->pos()));
if (idx.isValid()) {
m_selEnd = idx;
}
if (m_textSel.active) {
QRect r = visualRect(QModelIndex(m_textSel.index));
m_textSel.dragPos = event->pos() - r.topLeft();
}
viewport()->update();
event->accept();
}
void ChatView::mouseReleaseEvent(QMouseEvent *event) {
const QModelIndex pressed(m_pressIndex);
if (event->button() == Qt::LeftButton && !m_selecting && pressed.isValid() &&
(event->pos() - m_pressPos).manhattanLength() < 5 && indexAt(event->pos()) == pressed) {
const QString link = m_inviteHitTest ? m_inviteHitTest(pressed, event->pos()) : QString();
if (!link.isEmpty()) {
m_pressIndex = QPersistentModelIndex();
GUI_INFO("invite link: activated row=%d bytes=%d", pressed.row(), link.size());
emit inviteLinkActivated(link);
event->accept(); return;
}
}
if (event->button() == Qt::LeftButton && m_selecting) {
m_selecting = false;
viewport()->update();
}
m_pressIndex = QPersistentModelIndex();
QListView::mouseReleaseEvent(event);
}
void ChatView::keyPressEvent(QKeyEvent *event) {
if (event == QKeySequence::Copy) {
copySelection();
return;
}
QListView::keyPressEvent(event);
}
void ChatView::copySelection() {
if (!hasSelection()) return;
auto *m = model();
if (!m) return;
if (selStart() == selEnd()) {
QModelIndex idx(selStart());
QString text = idx.data(MsgTextRole).toString();
if (m_textSel.startChar >= 0 && m_textSel.endChar > m_textSel.startChar)
text = text.mid(m_textSel.startChar, m_textSel.endChar - m_textSel.startChar);
QApplication::clipboard()->clear(QClipboard::Clipboard);
QApplication::clipboard()->setText(text);
} else {
QString r;
int a = selStart().row(), b = selEnd().row();
if (a > b) qSwap(a, b);
for (int i = a; i <= b; ++i) {
QModelIndex idx = m->index(i, 0);
if (!r.isEmpty()) r += '\n';
r += '[' + idx.data(MsgTimeRole).toString() + "] ";
r += idx.data(MsgAuthorRole).toString() + ": ";
r += idx.data(MsgTextRole).toString();
}
QApplication::clipboard()->clear(QClipboard::Clipboard);
QApplication::clipboard()->setText(r);
}
}
void ChatView::updateScrollDownBtn() {
QScrollBar* sb = verticalScrollBar();
bool atBottom = sb && (sb->value() >= sb->maximum());
m_scrollDownBtn->setVisible(!atBottom);
if (!atBottom) {
QRect vr = viewport()->geometry();
m_scrollDownBtn->move(vr.right() - 44, vr.bottom() - 44);
m_scrollDownBtn->raise();
}
}