Focus the message input when the chat window is activated

This commit is contained in:
evgeny committed 2026-11-01 05:49:06 +01:00
1 parent 4e3c1182db
commit d8db018166
5 files changed
+86

No files matched your search

+20
View File
@@ -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();
+2
View File
@@ -53,6 +53,8 @@ public:
~MainWindow() override;
protected:
// После активации окна передать фокус composer; Qt сначала завершает восстановление фокуса дочерних виджетов.
void changeEvent(QEvent* event) override;
// Сохранить геометрию и скрыть в tray; для выхода служит requestShutdown.
void closeEvent(QCloseEvent *event) override;
+1
View File
@@ -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);
+1
View File
@@ -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.
@@ -17,6 +17,11 @@
#include <QTemporaryDir>
#include <QFile>
#include <QTextEdit>
#include <QLineEdit>
#include <QRadioButton>
#include <QClipboard>
#include <QDialog>
#include <QMenu>
#include <QListView>
#include <QStandardItemModel>
#include <QPainter>
@@ -192,6 +197,63 @@ private slots:
QCOMPARE(outstanding.size(), size_t(0));
#endif
}
void chatWindowReactivationFocus_data() {
QTest::addColumn<bool>("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<MessageList*>(); QVERIFY(history); history->loadChannel("123");
auto* composer = history->findChild<InputBar*>(); QVERIFY(composer); composer->setRecordingContext(directory.path(), "123");
auto* editor = composer->findChild<QTextEdit*>(); 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<QRadioButton*>("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<QListView*>(); 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<QString>("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<MessageList*>(); QVERIFY(history); history->loadChannel("123");
auto* composer = history->findChild<InputBar*>(); QVERIFY(composer);
auto* editor = composer->findChild<QTextEdit*>(); QVERIFY(editor);
auto* view = history->findChild<QListView*>(); 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");