From 4f6808156f6c200e62a5bf8debbd1e33c53697a8 Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 21 Oct 2026 13:05:31 +0200 Subject: [PATCH] Add chat history keyboard scrolling and remove unused PTT setting --- tools/chatgui/CMakeLists.txt | 2 +- tools/chatgui/src/channelsettingsdialog.cpp | 8 ---- tools/chatgui/src/channelsettingsdialog.h | 3 +- tools/chatgui/src/chatview.cpp | 25 +++++++++++ tools/chatgui/src/chatview.h | 4 ++ tools/chatgui/src/inputbar.cpp | 1 + tools/chatgui/src/messagelist.cpp | 2 + tools/chatgui/tests/test_file_drop.cpp | 47 +++++++++++++++++++++ tools/chatgui/translations/chatgui_en.ts | 10 ----- tools/chatgui/translations/chatgui_ru.ts | 10 ----- 10 files changed, 81 insertions(+), 31 deletions(-) diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index 10ebc9ff..00b60a4b 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -492,7 +492,7 @@ if(TARGET X11::X11) target_link_libraries(test_message_notification_x11 PRIVATE chatgui_notifications ${QT_TEST_LIB} X11::X11 pthread ${CMAKE_DL_LIBS}) endif() -add_executable(test_file_drop tests/test_file_drop.cpp src/inputbar.cpp src/audiorecorder.cpp src/audio_device.cpp src/sound_manager.cpp +add_executable(test_file_drop tests/test_file_drop.cpp src/inputbar.cpp src/chatview.cpp src/audiorecorder.cpp src/audio_device.cpp src/sound_manager.cpp src/voicemessageencoder.cpp src/emojipanel.cpp src/emojitabbar.cpp src/emoji.cpp src/animtimer.cpp src/lottieicon.cpp src/voice_audio_io.cpp transport/miniaudio_impl.c transport/audio_diagnostics.cpp) target_include_directories(test_file_drop PRIVATE ${UTUN_INCLUDE_DIRS}) diff --git a/tools/chatgui/src/channelsettingsdialog.cpp b/tools/chatgui/src/channelsettingsdialog.cpp index bf1c18b8..b0999fe3 100644 --- a/tools/chatgui/src/channelsettingsdialog.cpp +++ b/tools/chatgui/src/channelsettingsdialog.cpp @@ -78,11 +78,6 @@ ChannelSettingsDialog::ChannelSettingsDialog(const QString& channelId, DbManager m_soundOnMessage->setToolTip(QCoreApplication::translate("ChatGui", "Play a notification sound when a message arrives in this channel.")); layout->addWidget(m_soundOnMessage); - m_pttMode = new QCheckBox(QCoreApplication::translate("ChatGui", "Push-To-Talk (PTT) mode"), this); - m_pttMode->setToolTip(QCoreApplication::translate("ChatGui", "Record voice messages with push-to-talk. (Coming soon)")); - m_pttMode->setEnabled(false); - layout->addWidget(m_pttMode); - layout->addStretch(); auto* btnLayout = new QHBoxLayout(); @@ -116,8 +111,6 @@ ChannelSettingsDialog::ChannelSettingsDialog(const QString& channelId, DbManager m_autoPlayVoice->setChecked(m_db->getUiStateInt(autoPlayKey, 0) != 0); QString soundKey = QStringLiteral("ch_%1_sound_on_message").arg(channelId); m_soundOnMessage->setChecked(m_db->getUiStateInt(soundKey, 1) != 0); - QString pttKey = QStringLiteral("ch_%1_ptt_mode").arg(channelId); - m_pttMode->setChecked(m_db->getUiStateInt(pttKey, 0) != 0); } } @@ -138,7 +131,6 @@ void ChannelSettingsDialog::onSave() { } }; saveKey("auto_play_voice", m_autoPlayVoice->isChecked() ? 1 : 0); - saveKey("ptt_mode", m_pttMode->isChecked() ? 1 : 0); /* sound_on_message: сохранить + разослать пирам (троттлинг db_sync на сервере) */ { diff --git a/tools/chatgui/src/channelsettingsdialog.h b/tools/chatgui/src/channelsettingsdialog.h index df09fe10..17a00e21 100644 --- a/tools/chatgui/src/channelsettingsdialog.h +++ b/tools/chatgui/src/channelsettingsdialog.h @@ -1,4 +1,4 @@ -/* Локальные настройки выбранного канала: autoplay голосовых, звук сообщений и режим PTT. */ +/* Локальные настройки выбранного канала: autoplay голосовых и звук сообщений. */ #pragma once #include @@ -20,5 +20,4 @@ private: DbManager* m_db; QCheckBox* m_autoPlayVoice; QCheckBox* m_soundOnMessage; - QCheckBox* m_pttMode; }; diff --git a/tools/chatgui/src/chatview.cpp b/tools/chatgui/src/chatview.cpp index 4db89063..c4e6aa07 100644 --- a/tools/chatgui/src/chatview.cpp +++ b/tools/chatgui/src/chatview.cpp @@ -208,7 +208,32 @@ void ChatView::mouseReleaseEvent(QMouseEvent *event) { QListView::mouseReleaseEvent(event); } +bool ChatView::scrollWithKey(QKeyEvent *event) { + if (event->modifiers() != Qt::NoModifier) return false; + QAbstractSlider::SliderAction action; + switch (event->key()) { + case Qt::Key_Up: action = QAbstractSlider::SliderSingleStepSub; break; + case Qt::Key_Down: action = QAbstractSlider::SliderSingleStepAdd; break; + case Qt::Key_PageUp: action = QAbstractSlider::SliderPageStepSub; break; + case Qt::Key_PageDown: action = QAbstractSlider::SliderPageStepAdd; break; + default: return false; + } + auto* bar = verticalScrollBar(); + const int before = bar->value(); + bar->triggerAction(action); + GUI_DEBUG("history keyboard scroll: key=%d action=%d position=%d->%d range=%d..%d", + event->key(), int(action), before, bar->value(), bar->minimum(), bar->maximum()); + event->accept(); + return true; +} + +bool ChatView::eventFilter(QObject *object, QEvent *event) { + if (event->type() == QEvent::KeyPress && isVisible() && isEnabled() && scrollWithKey(static_cast(event))) return true; + return QListView::eventFilter(object, event); +} + void ChatView::keyPressEvent(QKeyEvent *event) { + if (scrollWithKey(event)) return; if (event == QKeySequence::Copy) { copySelection(); return; diff --git a/tools/chatgui/src/chatview.h b/tools/chatgui/src/chatview.h index 3801ba7e..12a36d1c 100644 --- a/tools/chatgui/src/chatview.h +++ b/tools/chatgui/src/chatview.h @@ -42,6 +42,8 @@ signals: void latestRequested(); protected: + /* Up/Down/PgUp/PgDown от поля ввода и окна, установивших этот вид как eventFilter. */ + bool eventFilter(QObject *object, QEvent *event) override; bool viewportEvent(QEvent *event) override; void paintEvent(QPaintEvent *event) override; void resizeEvent(QResizeEvent *event) override; @@ -52,6 +54,8 @@ protected: void keyPressEvent(QKeyEvent *event) override; private: + /* Прокрутить историю по клавише без модификаторов; true, если событие принято. Фокус сохраняется. */ + bool scrollWithKey(QKeyEvent *event); void copySelection(); void updateScrollDownBtn(); diff --git a/tools/chatgui/src/inputbar.cpp b/tools/chatgui/src/inputbar.cpp index 47cc637a..59098b9f 100644 --- a/tools/chatgui/src/inputbar.cpp +++ b/tools/chatgui/src/inputbar.cpp @@ -148,6 +148,7 @@ InputBar::InputBar(EmojiPanel *emojiPanel, AudioRecorder *recorder, QWidget *par m_input->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_input->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_input->setTabChangesFocus(true); + setFocusProxy(m_input); m_input->setStyleSheet( "QTextEdit { border: 1px solid palette(mid); border-radius: 6px;" " padding: 4px 8px; background: palette(base); }" diff --git a/tools/chatgui/src/messagelist.cpp b/tools/chatgui/src/messagelist.cpp index 181b5429..fcee1796 100644 --- a/tools/chatgui/src/messagelist.cpp +++ b/tools/chatgui/src/messagelist.cpp @@ -680,6 +680,8 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent m_inputBar = new InputBar(m_emojiPanel, recorder, this); layout->addWidget(m_inputBar); m_inputBar->installFileDropTarget(m_view->viewport()); + m_inputBar->focusProxy()->installEventFilter(m_view); + window()->installEventFilter(m_view); connect(m_inputBar, &InputBar::sendMessage, this, [this](const QString &text, const QByteArray& reply, quint64 request) { if (!gui_bridge_get_inst() || !gui_bridge_is_uasync_ready()) { diff --git a/tools/chatgui/tests/test_file_drop.cpp b/tools/chatgui/tests/test_file_drop.cpp index 1b19b33e..2b4b70d2 100644 --- a/tools/chatgui/tests/test_file_drop.cpp +++ b/tools/chatgui/tests/test_file_drop.cpp @@ -1,5 +1,6 @@ /* Реальные Qt-события на viewport: drop только готовит вложение, отправку инициирует пользователь. */ #include "../src/inputbar.h" +#include "../src/chatview.h" #include "../../../lib/debug_config.h" extern "C" { #include "media_async/attachment_send.h" @@ -23,6 +24,10 @@ extern "C" { #include #include #include +#include +#include +#include +#include static QString makeFile(const QTemporaryDir& dir, const QString& name, const QByteArray& contents = "test") { const QString path = dir.filePath(name); @@ -48,6 +53,48 @@ static bool dropUrls(QWidget* target, const QList& urls, bool process = tr class FileDropTest : public QObject { Q_OBJECT private slots: + void historyKeyboardNavigation_data() { + QTest::addColumn("key"); + QTest::addColumn("focus"); + for (int key : {Qt::Key_Up, Qt::Key_Down, Qt::Key_PageUp, Qt::Key_PageDown}) + for (int focus = 0; focus < 3; focus++) + QTest::newRow(qPrintable(QStringLiteral("key-%1-focus-%2").arg(key).arg(focus))) << key << focus; + } + void historyKeyboardNavigation() { + QFETCH(int, key); QFETCH(int, focus); + QMainWindow window; + auto* body = new QWidget(&window); auto* layout = new QVBoxLayout(body); + auto* view = new ChatView(body); auto* input = new InputBar(nullptr, nullptr, body); + layout->addWidget(view, 1); layout->addWidget(input); window.setCentralWidget(body); + QStringList rows; for (int i = 0; i < 100; i++) rows.append(QString::number(i)); + QStringListModel model(rows); view->setModel(&model); + view->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + auto* editor = input->findChild(); QVERIFY(editor); QCOMPARE(input->focusProxy(), editor); + editor->installEventFilter(view); window.installEventFilter(view); + const QString draft = QStringLiteral("first line\nsecond line\nthird line"); editor->setPlainText(draft); + auto cursor = editor->textCursor(); cursor.setPosition(5); editor->setTextCursor(cursor); + window.resize(600, 400); window.show(); window.activateWindow(); + QWidget* target = focus == 0 ? static_cast(editor) : focus == 1 ? static_cast(&window) : view; + if (focus == 0) input->setFocus(); else target->setFocus(); + QTRY_COMPARE(QApplication::focusWidget(), target); + auto* bar = view->verticalScrollBar(); QTRY_VERIFY(bar->maximum() > bar->pageStep() * 2); + bar->setValue(bar->maximum() / 2); + const int before = bar->value(); + const bool up = key == Qt::Key_Up || key == Qt::Key_PageUp; + const int step = key == Qt::Key_Up || key == Qt::Key_Down ? bar->singleStep() : bar->pageStep(); + QTest::keyClick(target, static_cast(key)); + QCOMPARE(bar->value(), before + (up ? -step : step)); + QCOMPARE(QApplication::focusWidget(), target); QCOMPARE(editor->textCursor().position(), 5); QCOMPARE(editor->toPlainText(), draft); + bar->setValue(up ? bar->minimum() : bar->maximum()); + const int boundary = bar->value(); QTest::keyClick(target, static_cast(key)); + QCOMPARE(bar->value(), boundary); QCOMPARE(editor->textCursor().position(), 5); + if (focus == 0) { + bar->setValue(before); + QTest::keyClick(editor, Qt::Key_Down, Qt::ShiftModifier); + QVERIFY(editor->textCursor().hasSelection()); QCOMPARE(bar->value(), before); + QCOMPARE(editor->toPlainText(), draft); QCOMPARE(QApplication::focusWidget(), editor); + } + } void encryptedAttachmentCapturesTargetAndResetsChoice_data() { QTest::addColumn("picker"); QTest::newRow("drop") << false; QTest::newRow("file-picker") << true; diff --git a/tools/chatgui/translations/chatgui_en.ts b/tools/chatgui/translations/chatgui_en.ts index 2fd8363d..b156bc1f 100644 --- a/tools/chatgui/translations/chatgui_en.ts +++ b/tools/chatgui/translations/chatgui_en.ts @@ -1728,11 +1728,6 @@ with per-link BBR statistics Public Public - - - Push-To-Talk (PTT) mode - Push-To-Talk (PTT) mode - Query error @@ -1805,11 +1800,6 @@ with per-link BBR statistics Recipient: %1 Recipient: %1 - - - Record voice messages with push-to-talk. (Coming soon) - Record voice messages with push-to-talk. (Coming soon) - Recording device: diff --git a/tools/chatgui/translations/chatgui_ru.ts b/tools/chatgui/translations/chatgui_ru.ts index 75f66ce9..5201ceeb 100644 --- a/tools/chatgui/translations/chatgui_ru.ts +++ b/tools/chatgui/translations/chatgui_ru.ts @@ -1731,11 +1731,6 @@ with per-link BBR statistics Public Публичный - - - Push-To-Talk (PTT) mode - Режим «нажми и говори» (PTT) - Query error @@ -1808,11 +1803,6 @@ with per-link BBR statistics Recipient: %1 Получатель: %1 - - - Record voice messages with push-to-talk. (Coming soon) - Запись голосовых сообщений в режиме «нажми и говори». (Скоро) - Recording device: