#include "chatview.h" #include "messagedelegate.h" #include "emoji.h" #include "debug_ui.h" #include #include #include #include #include #include #include // Текст и реакции сообщения используют общий набор анимированных значков. static QSet animatedEmojiPaths(const QModelIndex &index) { QSet 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 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(); } }