From d8db018166a340ee7fa9a559bc7c0f85fbd51e94 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sun, 1 Nov 2026 05:49:06 +0100 Subject: [PATCH] Focus the message input when the chat window is activated --- tools/chatgui/src/mainwindow.cpp | 20 ++++++ tools/chatgui/src/mainwindow.h | 2 + tools/chatgui/src/messagelist.cpp | 1 + tools/chatgui/src/messagelist.h | 1 + tools/chatgui/tests/test_desktop_commands.cpp | 62 +++++++++++++++++++ 5 files changed, 86 insertions(+) diff --git a/tools/chatgui/src/mainwindow.cpp b/tools/chatgui/src/mainwindow.cpp index 9c8cc326..28be6ff1 100644 --- a/tools/chatgui/src/mainwindow.cpp +++ b/tools/chatgui/src/mainwindow.cpp @@ -1361,6 +1361,26 @@ void MainWindow::showIncomingMessageNotification(const QString& target, quint64 m_messageNotification->showMessage(target, m_db->getDisplayName(author), channel, m_trayIcon->geometry(), screen()); } +void MainWindow::changeEvent(QEvent* event) { + QMainWindow::changeEvent(event); + if (event->type() != QEvent::ActivationChange || !isActiveWindow()) return; + QTimer::singleShot(0, this, [this] { + auto* input = m_messageList ? m_messageList->focusProxy() : nullptr; + const bool ready = input && input->isVisible() && input->isEnabled(); + if (QApplication::activeWindow() != this || !isVisible() || isMinimized() || + QApplication::activeModalWidget() || QApplication::activePopupWidget() || !ready) { + GUI_DEBUG("window activation: input focus skipped active=%d visible=%d minimized=%d modal=%d popup=%d input_ready=%d", + QApplication::activeWindow() == this, isVisible(), isMinimized(), bool(QApplication::activeModalWidget()), + bool(QApplication::activePopupWidget()), ready); + return; + } + m_messageList->setFocus(Qt::ActiveWindowFocusReason); + auto* focused = QApplication::focusWidget(); + GUI_DEBUG("window activation: message input focused target=%s focus_widget=%s", qPrintable(m_currentChannelId), + focused ? focused->metaObject()->className() : "none"); + }); +} + void MainWindow::closeEvent(QCloseEvent *event) { DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "MainWindow: closeEvent — hiding to tray"); saveWindowGeometry(); diff --git a/tools/chatgui/src/mainwindow.h b/tools/chatgui/src/mainwindow.h index e54f5016..c856c0fa 100644 --- a/tools/chatgui/src/mainwindow.h +++ b/tools/chatgui/src/mainwindow.h @@ -53,6 +53,8 @@ public: ~MainWindow() override; protected: + // После активации окна передать фокус composer; Qt сначала завершает восстановление фокуса дочерних виджетов. + void changeEvent(QEvent* event) override; // Сохранить геометрию и скрыть в tray; для выхода служит requestShutdown. void closeEvent(QCloseEvent *event) override; diff --git a/tools/chatgui/src/messagelist.cpp b/tools/chatgui/src/messagelist.cpp index ff5cccf5..bdfe9c8e 100644 --- a/tools/chatgui/src/messagelist.cpp +++ b/tools/chatgui/src/messagelist.cpp @@ -658,6 +658,7 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent layout->addWidget(m_emojiPanel); m_inputBar = new InputBar(m_emojiPanel, recorder, this); + setFocusProxy(m_inputBar); layout->addWidget(m_inputBar); m_inputBar->installFileDropTarget(m_view->viewport()); m_inputBar->focusProxy()->installEventFilter(m_view); diff --git a/tools/chatgui/src/messagelist.h b/tools/chatgui/src/messagelist.h index db976934..93e1be27 100644 --- a/tools/chatgui/src/messagelist.h +++ b/tools/chatgui/src/messagelist.h @@ -1,4 +1,5 @@ /* История и composer выбранного канала/PM, read/scroll, загрузки, feedback, рация и каталог streams. + * setFocus передаёт фокус полю сообщения через focusProxy composer. * Все методы/callbacks — GUI. Сетевые операции передаются мосту с владением запросом и привязкой instance. * Модель держит самостоятельные Qt-значения; DbManager/AudioRecorder заимствованы. * PM снимки проверяются по conv/generation, канальные строки — по message_id/version. diff --git a/tools/chatgui/tests/test_desktop_commands.cpp b/tools/chatgui/tests/test_desktop_commands.cpp index 484e57ca..51c7d0f5 100644 --- a/tools/chatgui/tests/test_desktop_commands.cpp +++ b/tools/chatgui/tests/test_desktop_commands.cpp @@ -17,6 +17,11 @@ #include #include #include +#include +#include +#include +#include +#include #include #include #include @@ -192,6 +197,63 @@ private slots: QCOMPARE(outstanding.size(), size_t(0)); #endif } + void chatWindowReactivationFocus_data() { + QTest::addColumn("imageChoice"); + QTest::newRow("history") << false; QTest::newRow("image-format") << true; + } + void chatWindowReactivationFocus() { + QFETCH(bool, imageChoice); QTemporaryDir directory; QVERIFY(directory.isValid()); + MainWindow window(nullptr, nullptr, directory.filePath("focus.cfg")); + auto* history = window.findChild(); QVERIFY(history); history->loadChannel("123"); + auto* composer = history->findChild(); QVERIFY(composer); composer->setRecordingContext(directory.path(), "123"); + auto* editor = composer->findChild(); QVERIFY(editor); QCOMPARE(history->focusProxy(), composer); + window.show(); window.activateWindow(); QTRY_VERIFY(window.isActiveWindow()); QTRY_VERIFY(editor->hasFocus()); + const QString draft = "first line\nsecond line"; editor->setPlainText(draft); + auto cursor = editor->textCursor(); cursor.setPosition(2); cursor.setPosition(7, QTextCursor::KeepAnchor); editor->setTextCursor(cursor); + QRadioButton* jpeg = nullptr; + const auto clearClipboard = qScopeGuard([&] { if (imageChoice) QApplication::clipboard()->clear(); }); + if (imageChoice) { + QImage image(32, 24, QImage::Format_RGB32); image.fill(Qt::green); QApplication::clipboard()->setImage(image); editor->paste(); + jpeg = composer->findChild("clipboardJpegChoice"); QVERIFY(jpeg); + QTRY_VERIFY(jpeg->isVisible()); QTest::mouseClick(jpeg, Qt::LeftButton); QVERIFY(jpeg->isChecked()); jpeg->setFocus(); + QTRY_VERIFY(jpeg->hasFocus()); + } else { + auto* view = history->findChild(); QVERIFY(view); view->setFocus(); QTRY_VERIFY(view->hasFocus()); + } + QLineEdit other; other.show(); other.activateWindow(); other.setFocus(); + QTRY_VERIFY(other.isActiveWindow()); QTRY_VERIFY(other.hasFocus()); + QTest::keyClicks(&other, "other window"); QCOMPARE(other.text(), QString("other window")); + window.activateWindow(); QTRY_VERIFY(window.isActiveWindow()); QTRY_VERIFY(editor->hasFocus()); + QCOMPARE(editor->toPlainText(), draft); QCOMPARE(editor->textCursor().position(), 7); QCOMPARE(editor->textCursor().anchor(), 2); + if (imageChoice) QVERIFY(jpeg->isChecked()); + QTest::keyClicks(QApplication::focusWidget(), "x"); QCOMPARE(editor->toPlainText(), QString("fixine\nsecond line")); + } + void chatActivationRespectsCurrentWindow_data() { + QTest::addColumn("state"); + for (const char* state : {"other-window", "disabled", "hidden", "dialog", "popup"}) QTest::newRow(state) << QString(state); + } + void chatActivationRespectsCurrentWindow() { + QFETCH(QString, state); QTemporaryDir directory; QVERIFY(directory.isValid()); + MainWindow window(nullptr, nullptr, directory.filePath("focus.cfg")); + auto* history = window.findChild(); QVERIFY(history); history->loadChannel("123"); + auto* composer = history->findChild(); QVERIFY(composer); + auto* editor = composer->findChild(); QVERIFY(editor); + auto* view = history->findChild(); QVERIFY(view); + window.show(); window.activateWindow(); QTRY_VERIFY(window.isActiveWindow()); QTRY_VERIFY(editor->hasFocus()); + view->setFocus(); QTRY_VERIFY(view->hasFocus()); + QEvent activation(QEvent::ActivationChange); QApplication::sendEvent(&window, &activation); + QLineEdit other; QDialog dialog(&window); QLineEdit dialogInput(&dialog); QMenu popup(&window); popup.addAction("Menu item"); + if (state == "other-window") { other.show(); other.activateWindow(); other.setFocus(); QTRY_VERIFY(other.isActiveWindow()); } + if (state == "disabled") composer->setEnabled(false); + if (state == "hidden") window.hide(); + if (state == "dialog") { dialog.setModal(true); dialog.show(); dialog.activateWindow(); dialogInput.setFocus(); } + if (state == "popup") popup.popup(window.mapToGlobal(QPoint(100, 100))); + QCoreApplication::processEvents(); QVERIFY(!editor->hasFocus()); + if (state == "other-window") { QVERIFY(other.isActiveWindow()); QVERIFY(other.hasFocus()); } + if (state == "dialog") { QCOMPARE(QApplication::activeModalWidget(), &dialog); QTRY_VERIFY(dialogInput.hasFocus()); } + if (state == "popup") QCOMPARE(QApplication::activePopupWidget(), &popup); + } + void imagePreviewPreparedOutsidePaint() { QTemporaryDir directory; QVERIFY(directory.isValid()); const QString path = directory.filePath("image.png");