Browse Source

Fix desktop hover and emoji playback; share Android chat background

master
evgeny 13 hours ago
parent
commit
1e9264735b
  1. 9
      tools/chatgui-android/app/build.gradle.kts
  2. 27
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/ConversationMessages.kt
  3. 6
      tools/chatgui/CMakeLists.txt
  4. 2
      tools/chatgui/src/accountdelegate.cpp
  5. 5
      tools/chatgui/src/accountlist.cpp
  6. 33
      tools/chatgui/src/animtimer.cpp
  7. 8
      tools/chatgui/src/animtimer.h
  8. 47
      tools/chatgui/src/chatview.cpp
  9. 5
      tools/chatgui/src/chatview.h
  10. 24
      tools/chatgui/src/emojipanel.cpp
  11. 21
      tools/chatgui/src/messagelist.cpp
  12. 1
      tools/chatgui/src/messagelist.h
  13. 201
      tools/chatgui/tests/test_emoji_playback.cpp

9
tools/chatgui-android/app/build.gradle.kts

@ -3,6 +3,14 @@ plugins {
id("org.jetbrains.kotlin.android")
}
// Фон берём из desktop-чатгуи; drawable-nodpi сохраняет исходный размер плитки.
val prepareChatBackground by tasks.registering(Copy::class) {
from("../../chatgui/resources/bg.png")
into(layout.buildDirectory.dir("generated/chatBackground/res/drawable-nodpi"))
rename { "chat_background.png" }
}
tasks.named("preBuild") { dependsOn(prepareChatBackground) }
android {
namespace = "com.utun.chat"
compileSdk = 36
@ -40,6 +48,7 @@ android {
// Общие TGS-анимации с desktop-чатгуи, без копирования ресурсов.
sourceSets["main"].assets.srcDir("../../chatgui/resources/animations")
sourceSets["main"].res.srcDir(layout.buildDirectory.dir("generated/chatBackground/res"))
composeOptions {
kotlinCompilerExtensionVersion = "1.5.5"

27
tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/ConversationMessages.kt

@ -3,9 +3,21 @@ package com.utun.chat.ui.components
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.ImageShader
import androidx.compose.ui.graphics.ShaderBrush
import androidx.compose.ui.graphics.TileMode
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.res.imageResource
import androidx.compose.ui.unit.dp
import com.utun.chat.R
import com.utun.chat.data.ConversationTarget
import com.utun.chat.data.LogManager
import com.utun.chat.data.Message
import com.utun.chat.viewmodel.ChatViewModel
@ -16,7 +28,20 @@ fun ConversationMessages(target: ConversationTarget, messages: List<Message>, st
onPlayVoice: (String, Int) -> Unit, onSeekVoice: (String, Int, Int) -> Unit,
onOpenFile: (Message) -> Unit, onPlayVideo: (Message) -> Unit,
onDownloadFile: (Message) -> Unit = {}, onOpenImage: (Message) -> Unit = {}) {
LazyColumn(modifier = modifier, state = state, reverseLayout = true) {
val background = ImageBitmap.imageResource(R.drawable.chat_background)
LaunchedEffect(background) {
LogManager.addLog("DEBUG", "UI", "Chat background ready: desktop bg.png tile=${background.width}x${background.height}")
}
// Плитка закреплена за областью списка; плотность экрана сохраняет размер узора в dp.
val backgroundModifier = Modifier.drawWithCache {
val brush = ShaderBrush(ImageShader(background, TileMode.Repeated, TileMode.Repeated))
onDrawBehind {
scale(density, density, pivot = Offset.Zero) {
drawRect(brush, size = Size(size.width / density, size.height / density))
}
}
}
LazyColumn(modifier = modifier.then(backgroundModifier), state = state, reverseLayout = true) {
item { Spacer(Modifier.height(8.dp)) }
items(messages.reversed(), key = { "${target.nativeId}:${it.isOutgoing}:${it.id}" }) { message ->
MessageBubble(message, playbackState, onPlayVoice, onSeekVoice, onDownloadFile, onOpenFile, onOpenImage, onPlayVideo)

6
tools/chatgui/CMakeLists.txt

@ -213,6 +213,12 @@ endif()
enable_testing()
add_executable(test_emoji_playback tests/test_emoji_playback.cpp src/animtimer.cpp src/lottieicon.cpp
src/emojipanel.cpp src/emojitabbar.cpp src/emoji.cpp src/chatview.cpp)
target_link_libraries(test_emoji_playback PRIVATE ${QT_LIBS} ${QT_TEST_LIB} rlottie::rlottie ZLIB::ZLIB utun)
add_test(NAME test_emoji_playback COMMAND test_emoji_playback)
set_tests_properties(test_emoji_playback PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen" TIMEOUT 30)
add_executable(test_config_updater
tests/test_config_updater.cpp
transport/config_updater.cpp

2
tools/chatgui/src/accountdelegate.cpp

@ -14,7 +14,7 @@ void AccountDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio
if (option.state & QStyle::State_Selected)
painter->fillRect(option.rect, option.palette.highlight());
else if (option.state & QStyle::State_MouseOver)
painter->fillRect(option.rect, option.palette.light());
painter->fillRect(option.rect, option.palette.alternateBase().color().lighter(115));
else
painter->fillRect(option.rect, option.palette.alternateBase());

5
tools/chatgui/src/accountlist.cpp

@ -84,10 +84,15 @@ AccountList::AccountList(DbManager* db, QWidget *parent)
m_listView->setItemDelegate(new AccountDelegate(m_listView));
m_listView->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_listView->setSelectionMode(QAbstractItemView::SingleSelection);
m_listView->setMouseTracking(true);
m_listView->setIconSize(QSize(24, 24));
m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_listView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
m_listView->setContextMenuPolicy(Qt::CustomContextMenu);
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "AccountList: mouse_tracking=%d background=%s hover=%s selected=%s",
m_listView->hasMouseTracking(), qPrintable(m_listView->palette().alternateBase().color().name()),
qPrintable(m_listView->palette().alternateBase().color().lighter(115).name()),
qPrintable(m_listView->palette().highlight().color().name()));
m_detailPanel = new QWidget(this);
auto* detailLayout = new QVBoxLayout(m_detailPanel);
detailLayout->setContentsMargins(0, 0, 0, 0);

33
tools/chatgui/src/animtimer.cpp

@ -5,7 +5,6 @@
AnimTimer::AnimTimer() {
m_timer.setInterval(33);
connect(&m_timer, &QTimer::timeout, this, &AnimTimer::onTimer);
m_timer.start();
}
AnimTimer *AnimTimer::instance() {
@ -15,6 +14,7 @@ AnimTimer *AnimTimer::instance() {
void AnimTimer::shutdown() {
instance()->m_timer.stop();
instance()->m_playback.invalidate();
instance()->m_icons.clear();
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "AnimTimer: shutdown");
}
@ -31,22 +31,29 @@ LottieIcon *AnimTimer::icon(const QString &key) const {
return m_icons.value(key, nullptr);
}
void AnimTimer::activate() {
if (m_activeRef++ == 0) {
for (auto *icon : m_icons)
icon->advanceFrame();
emit ticked();
void AnimTimer::play() {
if (m_icons.isEmpty()) {
GUI_WARN("AnimTimer: playback requested without registered icons");
return;
}
}
void AnimTimer::deactivate() {
if (m_activeRef > 0) m_activeRef--;
GUI_DEBUG("AnimTimer: hover trigger icons=%d running=%d playback_ms=%d", (int)m_icons.size(), m_timer.isActive(), PlaybackMs);
m_playback.start();
m_timer.start();
}
void AnimTimer::onTimer() {
if (m_activeRef <= 0) return;
for (auto *icon : m_icons)
icon->advanceFrame();
const qint64 elapsed = m_playback.elapsed();
const bool finishing = elapsed >= PlaybackMs;
bool finished = true;
for (auto *icon : m_icons) {
if (!finishing || icon->currentFrame() != 0) icon->advanceFrame();
if (icon->currentFrame() != 0) finished = false;
}
if (finishing && finished) {
m_timer.stop();
m_playback.invalidate();
GUI_DEBUG("AnimTimer: playback finished elapsed_ms=%lld icons=%d all_frames=0", (long long)elapsed, (int)m_icons.size());
}
emit ticked();
}

8
tools/chatgui/src/animtimer.h

@ -3,6 +3,7 @@
#include <QObject>
#include <QTimer>
#include <QHash>
#include <QElapsedTimer>
class LottieIcon;
@ -16,8 +17,9 @@ public:
LottieIcon *icon(const QString &key) const;
void logRegisteredIcons() const;
void activate();
void deactivate();
// Наведение запускает минимум две секунды воспроизведения, затем каждый значок завершает свой цикл.
static constexpr int PlaybackMs = 2000;
void play();
static void shutdown();
signals:
@ -30,5 +32,5 @@ private:
AnimTimer();
QTimer m_timer;
QHash<QString, LottieIcon *> m_icons;
int m_activeRef = 0;
QElapsedTimer m_playback;
};

47
tools/chatgui/src/chatview.cpp

@ -1,5 +1,7 @@
#include "chatview.h"
#include "messagedelegate.h"
#include "emoji.h"
#include "debug_ui.h"
#include <QPainter>
#include <QClipboard>
#include <QApplication>
@ -8,6 +10,20 @@
#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)
{
@ -65,6 +81,24 @@ void ChatView::paintEvent(QPaintEvent *event) {
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 {
@ -95,6 +129,13 @@ void ChatView::mousePressEvent(QMouseEvent *event) {
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();
@ -115,7 +156,11 @@ void ChatView::mouseMoveEvent(QMouseEvent *event) {
QModelIndex idx = indexAt(viewport()->mapFrom(this, event->pos()));
if (idx != QModelIndex(m_lastHovered)) {
m_lastHovered = idx;
emit hoveredIndexChanged(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) {

5
tools/chatgui/src/chatview.h

@ -4,6 +4,7 @@
#include <QPersistentModelIndex>
#include <QPoint>
#include <QPushButton>
#include <QSet>
struct ChatTextSelection {
QPersistentModelIndex index;
@ -31,9 +32,10 @@ public:
void setTextSelChars(int start, int end) { m_textSel.startChar = start; m_textSel.endChar = end; }
signals:
void hoveredIndexChanged(const QModelIndex &idx);
void animationTriggered();
protected:
bool viewportEvent(QEvent *event) override;
void paintEvent(QPaintEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
void scrollContentsBy(int dx, int dy) override;
@ -51,6 +53,7 @@ private:
QPersistentModelIndex m_selEnd;
ChatTextSelection m_textSel;
QPersistentModelIndex m_lastHovered;
QSet<QString> m_visibleAnimations;
QPersistentModelIndex m_pressIndex;
QPoint m_pressPos;
bool m_selecting = false;

24
tools/chatgui/src/emojipanel.cpp

@ -3,6 +3,7 @@
#include "emojitabbar.h"
#include "animtimer.h"
#include "lottieicon.h"
#include "debug_ui.h"
#include <QPushButton>
#include <QScrollArea>
#include <QTabWidget>
@ -38,6 +39,12 @@ public:
connect(&m_timer, &QTimer::timeout, this, [this]() {
if (m_icon && m_icon->isValid()) {
m_frame = (m_frame + 1) % m_icon->frameCount();
if (m_playback.elapsed() >= AnimTimer::PlaybackMs && m_frame == 0) {
GUI_DEBUG("EmojiButton: playback finished path=%s elapsed_ms=%lld frame=0",
qPrintable(m_data.animPath), (long long)m_playback.elapsed());
m_timer.stop();
m_playback.invalidate();
}
update();
}
});
@ -53,19 +60,16 @@ protected:
#endif
QPushButton::enterEvent(e);
if (m_icon && m_icon->isValid()) {
m_hovered = true;
m_frame = 0;
GUI_DEBUG("EmojiButton: hover trigger path=%s running=%d frame=%d playback_ms=%d",
qPrintable(m_data.animPath), m_timer.isActive(), m_frame, AnimTimer::PlaybackMs);
if (!m_timer.isActive()) m_frame = 0;
m_playback.start();
m_timer.start();
update();
}
}
void leaveEvent(QEvent *e) override {
QPushButton::leaveEvent(e);
m_hovered = false;
m_timer.stop();
update();
}
void paintEvent(QPaintEvent *e) override {
if (m_hovered && m_icon && m_icon->isValid()) {
if (m_timer.isActive() && m_icon && m_icon->isValid()) {
QPainter p(this);
p.setRenderHint(QPainter::SmoothPixmapTransform);
int sz = qMin(width(), height()) - 4;
@ -82,7 +86,7 @@ private:
LottieIcon *m_icon = nullptr;
QTimer m_timer;
int m_frame = 0;
bool m_hovered = false;
QElapsedTimer m_playback;
};
} // namespace

21
tools/chatgui/src/messagelist.cpp

@ -597,26 +597,7 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
m_view->viewport()->update();
});
connect(m_view, &ChatView::hoveredIndexChanged, this, [this](const QModelIndex &idx) {
bool hasAnim = false;
if (idx.isValid()) {
QString text = idx.data(MsgTextRole).toString();
for (auto it = g_animatedEmojiMap.begin(); it != g_animatedEmojiMap.end(); ++it) {
if (text.contains(it.key())) { hasAnim = true; break; }
}
if (!hasAnim) {
QVariantList reactions = idx.data(MsgReactionsRole).toList();
for (const QVariant &rv : reactions) {
if (g_animatedEmojiMap.contains(rv.toMap()["emoji"].toString()))
{ hasAnim = true; break; }
}
}
}
bool wasActive = m_animHoverActive;
m_animHoverActive = hasAnim;
if (!wasActive && m_animHoverActive) AnimTimer::instance()->activate();
else if (wasActive && !m_animHoverActive) AnimTimer::instance()->deactivate();
});
connect(m_view, &ChatView::animationTriggered, AnimTimer::instance(), &AnimTimer::play);
m_readDebounce = new QTimer(this);
m_readDebounce->setSingleShot(true);

1
tools/chatgui/src/messagelist.h

@ -52,7 +52,6 @@ private:
EmojiPanel *m_emojiPanel;
DbManager *m_db;
QString m_currentChannelId;
bool m_animHoverActive = false;
QHash<quint64, QColor> m_avatarCache;
qint64 m_lastReadMsgId = 0;
QTimer* m_readDebounce;

201
tools/chatgui/tests/test_emoji_playback.cpp

@ -0,0 +1,201 @@
#include "../src/animtimer.h"
#include "../src/chatview.h"
#include "../src/emoji.h"
#include "../src/messagedelegate.h"
#include "../src/emojipanel.h"
#include "../src/lottieicon.h"
#include "../../../lib/debug_config.h"
#include <QtTest>
#include <QEnterEvent>
#include <QPushButton>
#include <QTabWidget>
#include <QStandardItemModel>
#include <zlib.h>
#include <memory>
static QStringList playbackLog;
static void captureLog(int, const char *, const char *message) {
playbackLog.append(QString::fromUtf8(message));
}
// Два коротких цикла разной длины выявляют остановку посреди кадра и ожидание общего кратного.
static QByteArray animationData(int frames) {
QByteArray json = QString(R"({"v":"5.7.4","fr":30,"ip":0,"op":%1,"w":32,"h":32,"layers":[]})").arg(frames).toUtf8();
QByteArray gzip(1024, '\0');
z_stream stream = {};
if (deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 16 + MAX_WBITS, 8, Z_DEFAULT_STRATEGY) != Z_OK) return {};
stream.next_in = reinterpret_cast<Bytef *>(json.data());
stream.avail_in = json.size();
stream.next_out = reinterpret_cast<Bytef *>(gzip.data());
stream.avail_out = gzip.size();
int result = deflate(&stream, Z_FINISH);
gzip.resize(stream.total_out);
deflateEnd(&stream);
return result == Z_STREAM_END ? gzip : QByteArray();
}
class EmojiPlaybackTest : public QObject {
Q_OBJECT
std::unique_ptr<LottieIcon> shortIcon, longIcon;
private slots:
void init() {
debug_config_init();
debug_set_category_level(DEBUG_CATEGORY_GENERAL, DEBUG_LEVEL_DEBUG);
debug_set_log_hook(captureLog);
playbackLog.clear();
shortIcon = std::make_unique<LottieIcon>(animationData(7), "short");
longIcon = std::make_unique<LottieIcon>(animationData(11), "long");
QVERIFY(shortIcon->isValid());
QVERIFY(longIcon->isValid());
AnimTimer::instance()->registerIcon("short", shortIcon.get());
AnimTimer::instance()->registerIcon("long", longIcon.get());
}
void cleanup() {
AnimTimer::shutdown();
debug_set_log_hook(nullptr);
shortIcon.reset();
longIcon.reset();
}
void messagesFinishEachCycle() {
auto *timer = AnimTimer::instance();
QSignalSpy ticks(timer, &AnimTimer::ticked);
timer->play();
QTest::qWait(1850);
QVERIFY(playbackLog.filter("AnimTimer: playback finished").isEmpty());
int count = ticks.count();
QTest::qWait(100);
QVERIFY(ticks.count() > count);
QTRY_COMPARE_WITH_TIMEOUT(playbackLog.filter("AnimTimer: playback finished").size(), 1, 1000);
QCOMPARE(shortIcon->currentFrame(), 0);
QCOMPARE(longIcon->currentFrame(), 0);
count = ticks.count();
QTest::qWait(150);
QCOMPARE(ticks.count(), count);
}
void anotherHoverRestartsDeadline() {
auto *timer = AnimTimer::instance();
QSignalSpy ticks(timer, &AnimTimer::ticked);
timer->play();
QTest::qWait(1400);
timer->play();
QTest::qWait(1000);
QVERIFY(playbackLog.filter("AnimTimer: playback finished").isEmpty());
int count = ticks.count();
QTest::qWait(100);
QVERIFY(ticks.count() > count);
QTRY_COMPARE_WITH_TIMEOUT(playbackLog.filter("AnimTimer: playback finished").size(), 1, 1500);
QCOMPARE(shortIcon->currentFrame(), 0);
QCOMPARE(longIcon->currentFrame(), 0);
}
void pickerFinishesWithOrWithoutHover() {
AnimTimer::instance()->registerIcon(":/animations/robot.tgs", shortIcon.get());
EmojiPanel panel;
panel.resize(320, 240);
panel.findChild<QTabWidget *>()->setCurrentIndex(3);
panel.show();
QPushButton *button = nullptr;
for (auto *candidate : panel.findChildren<QPushButton *>()) {
if (candidate->toolTip() == ":robot:" && candidate->hasMouseTracking()) button = candidate;
}
QVERIFY(button);
QEvent leave(QEvent::Leave);
QApplication::sendEvent(button, &leave);
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QEnterEvent enter(QPointF(1, 1), QPointF(1, 1), QPointF(1, 1));
#else
QEvent enter(QEvent::Enter);
#endif
QApplication::sendEvent(button, &enter);
QTest::qWait(1850);
QVERIFY(playbackLog.filter("EmojiButton: playback finished").isEmpty());
QTRY_COMPARE_WITH_TIMEOUT(playbackLog.filter("EmojiButton: playback finished").size(), 1, 1000);
QTest::qWait(150);
QCOMPARE(playbackLog.filter("EmojiButton: hover trigger").size(), 1);
QApplication::sendEvent(button, &leave);
QApplication::sendEvent(button, &enter);
QTest::qWait(100);
QApplication::sendEvent(button, &leave);
QTest::qWait(1750);
QCOMPARE(playbackLog.filter("EmojiButton: playback finished").size(), 1);
QTRY_COMPARE_WITH_TIMEOUT(playbackLog.filter("EmojiButton: playback finished").size(), 2, 1000);
}
void visibleMessagesTriggerOnce() {
builtinEmojiSet();
QStandardItemModel model;
for (int row = 0; row < 6; ++row) {
auto *item = new QStandardItem("message");
item->setSizeHint(QSize(200, 60));
item->setData(row + 1, MsgIdRole);
item->setData("test-channel", MsgChannelIdRole);
item->setData(row == 0 || row == 5 ? QString::fromUtf8("🤖") : QString("text"), MsgTextRole);
model.appendRow(item);
}
ChatView view;
view.setModel(&model);
view.resize(240, 140);
QSignalSpy triggers(&view, &ChatView::animationTriggered);
connect(&view, &ChatView::animationTriggered, AnimTimer::instance(), &AnimTimer::play);
view.show();
QTRY_COMPARE(triggers.count(), 1);
view.viewport()->update();
QTest::qWait(100);
QCOMPARE(triggers.count(), 1);
view.scrollToBottom();
QTRY_COMPARE(triggers.count(), 2);
view.scrollToTop();
QTRY_COMPARE(triggers.count(), 3);
view.scrollToBottom();
QTRY_COMPARE(triggers.count(), 4);
auto *message = new QStandardItem("new message");
message->setSizeHint(QSize(200, 60));
message->setData(7, MsgIdRole);
message->setData("test-channel", MsgChannelIdRole);
message->setData(QString::fromUtf8("🤖"), MsgTextRole);
model.appendRow(message);
view.scrollToBottom();
QTRY_COMPARE(triggers.count(), 5);
view.viewport()->update();
QTest::qWait(100);
QCOMPARE(triggers.count(), 5);
}
void hoverTriggersOnEntry() {
builtinEmojiSet();
QStandardItemModel model;
auto *message = new QStandardItem("message");
message->setSizeHint(QSize(200, 60));
message->setData(1, MsgIdRole);
message->setData(QString::fromUtf8("🤖"), MsgTextRole);
model.appendRow(message);
ChatView view;
view.setModel(&model);
view.resize(240, 140);
view.show();
QTest::qWait(50);
QSignalSpy triggers(&view, &ChatView::animationTriggered);
const QPointF point = view.visualRect(model.index(0, 0)).center();
QMouseEvent move(QEvent::MouseMove, point, point, Qt::NoButton, Qt::NoButton, Qt::NoModifier);
QApplication::sendEvent(view.viewport(), &move);
QCOMPARE(triggers.count(), 1);
QApplication::sendEvent(view.viewport(), &move);
QCOMPARE(triggers.count(), 1);
QEvent leave(QEvent::Leave);
QApplication::sendEvent(view.viewport(), &leave);
QCOMPARE(triggers.count(), 1);
QApplication::sendEvent(view.viewport(), &move);
QCOMPARE(triggers.count(), 2);
}
};
QTEST_MAIN(EmojiPlaybackTest)
#include "test_emoji_playback.moc"
Loading…
Cancel
Save