From 7070d3b6e69187c2765ce7f4f0735acf4eb34c6b Mon Sep 17 00:00:00 2001 From: Bilal89 Date: Sat, 20 Jun 2026 14:47:05 +0200 Subject: [PATCH] Fix autocomplete popup focus handling --- .../texteditorautocompletecomponent.cpp | 119 +++++++++++------- .../texteditorautocompletecomponent.h | 2 + edbee-test/CMakeLists.txt | 3 +- edbee-test/edbee-test.pro | 7 +- .../texteditorautocompletecomponenttest.cpp | 51 ++++++++ .../texteditorautocompletecomponenttest.h | 20 +++ 6 files changed, 151 insertions(+), 51 deletions(-) create mode 100644 edbee-test/edbee/views/texteditorautocompletecomponenttest.cpp create mode 100644 edbee-test/edbee/views/texteditorautocompletecomponenttest.h diff --git a/edbee-lib/edbee/views/components/texteditorautocompletecomponent.cpp b/edbee-lib/edbee/views/components/texteditorautocompletecomponent.cpp index 3c23332d..65ae0360 100644 --- a/edbee-lib/edbee/views/components/texteditorautocompletecomponent.cpp +++ b/edbee-lib/edbee/views/components/texteditorautocompletecomponent.cpp @@ -50,10 +50,15 @@ TextEditorAutoCompleteComponent::TextEditorAutoCompleteComponent(TextEditorContr this->setAttribute(Qt::WA_ShowWithoutActivating); menuRef_ = new QMenu(this); + menuRef_->setFocusPolicy(Qt::NoFocus); + menuRef_->setAttribute(Qt::WA_ShowWithoutActivating); menuRef_->setAccessibleName("Autocomplete"); listWidgetRef_ = new QListWidget(menuRef_); + listWidgetRef_->setFocusPolicy(Qt::NoFocus); + listWidgetRef_->setAttribute(Qt::WA_ShowWithoutActivating); + editorComponentRef_->installEventFilter(this); listWidgetRef_->installEventFilter(this); menuRef_->installEventFilter(this); @@ -336,63 +341,83 @@ bool TextEditorAutoCompleteComponent::eventFilter(QObject *obj, QEvent *event) return QObject::eventFilter(obj, event); } + if (obj == editorComponentRef_ && event->type() == QEvent::KeyPress && menuRef_->isVisible()) { + return handleAutoCompleteKeyPress(static_cast(event), true); + } + if(obj == listWidgetRef_ && event->type() == QEvent::KeyPress) { - QKeyEvent* key = static_cast(event); - - // text keys are allowed - if (!key->text().isEmpty()) { - QChar nextChar = key->text().at(0); - if (nextChar.isLetterOrNumber()) { - QApplication::sendEvent(editorComponentRef_, event); - return true; - } - } + return handleAutoCompleteKeyPress(static_cast(event), false); + + } + return QObject::eventFilter(obj, event); +} + + +bool TextEditorAutoCompleteComponent::handleAutoCompleteKeyPress(QKeyEvent* key, bool eventFromEditor) +{ + if (eventBeingFiltered_) { + return false; + } + + switch (key->key()) { + case Qt::Key_Escape: + menuRef_->close(); + canceled_ = true; + return true; - // escape key - switch (key->key()) { - case Qt::Key_Escape: + case Qt::Key_Enter: + case Qt::Key_Return: + case Qt::Key_Tab: + if (listWidgetRef_->currentItem() && currentWord_ == listWidgetRef_->currentItem()->text()) { menuRef_->close(); - canceled_ = true; - return true; // stop event - - case Qt::Key_Enter: - case Qt::Key_Return: - case Qt::Key_Tab: - if (listWidgetRef_->currentItem() && currentWord_ == listWidgetRef_->currentItem()->text()) { // sends normal enter/return/tab if you've typed a full word - menuRef_->close(); - QApplication::sendEvent(editorComponentRef_, event); - return true; - } else if (listWidgetRef_->currentItem()) { - insertCurrentSelectedListItem(); - menuRef_->close(); - updateList(); - return true; + if (eventFromEditor) { + return false; } - break; - - case Qt::Key_Backspace: - QApplication::sendEvent(editorComponentRef_, event); + QApplication::sendEvent(editorComponentRef_, key); return true; - - case Qt::Key_Shift: //ignore shift, don't hide - QApplication::sendEvent(editorComponentRef_, event); + } else if (listWidgetRef_->currentItem()) { + insertCurrentSelectedListItem(); + menuRef_->close(); + updateList(); return true; + } + break; + + case Qt::Key_Up: + case Qt::Key_Down: + case Qt::Key_PageDown: + case Qt::Key_PageUp: + eventBeingFiltered_ = true; + QApplication::sendEvent(listWidgetRef_, key); + eventBeingFiltered_ = false; + return true; + + case Qt::Key_Backspace: + case Qt::Key_Shift: + if (eventFromEditor) { + return false; + } + QApplication::sendEvent(editorComponentRef_, key); + return true; + } - // forward special keys to list - case Qt::Key_Up: - case Qt::Key_Down: - case Qt::Key_PageDown: - case Qt::Key_PageUp: + if (!key->text().isEmpty()) { + const QChar nextChar = key->text().at(0); + if (nextChar.isLetterOrNumber()) { + if (eventFromEditor) { return false; + } + QApplication::sendEvent(editorComponentRef_, key); + return true; } + } - // default operation is to hide and continue the event - menuRef_->close(); - QApplication::sendEvent(editorComponentRef_, event); - return true; - + menuRef_->close(); + if (eventFromEditor) { + return false; } - return QObject::eventFilter(obj, event); + QApplication::sendEvent(editorComponentRef_, key); + return true; } @@ -428,7 +453,7 @@ void TextEditorAutoCompleteComponent::updateList() // fills the autocomplete list with the curent word if (fillAutoCompleteList(doc, range, currentWord_)) { menuRef_->popup(menuRef_->pos()); - listWidgetRef_->setFocus(); + editorComponentRef_->setFocus(); // position the widget showInfoTip(); diff --git a/edbee-lib/edbee/views/components/texteditorautocompletecomponent.h b/edbee-lib/edbee/views/components/texteditorautocompletecomponent.h index c735f2e3..08c41153 100644 --- a/edbee-lib/edbee/views/components/texteditorautocompletecomponent.h +++ b/edbee-lib/edbee/views/components/texteditorautocompletecomponent.h @@ -18,6 +18,7 @@ class QListWidget; class QListWidgetItem; +class QKeyEvent; namespace edbee { @@ -70,6 +71,7 @@ class EDBEE_EXPORT TextEditorAutoCompleteComponent : public QWidget void positionWidgetForCaretOffset(size_t offset); bool eventFilter(QObject* obj, QEvent* event); + bool handleAutoCompleteKeyPress(QKeyEvent* key, bool eventFromEditor); void hideEvent(QHideEvent* event); //void focusOutEvent(QFocusEvent *event); diff --git a/edbee-test/CMakeLists.txt b/edbee-test/CMakeLists.txt index 536b63c7..5927e0cc 100644 --- a/edbee-test/CMakeLists.txt +++ b/edbee-test/CMakeLists.txt @@ -38,6 +38,7 @@ SET(SOURCES edbee/models/dynamicvariablestest.cpp edbee/util/rangelineiteratortest.cpp edbee/views/textthememanagertest.cpp + edbee/views/texteditorautocompletecomponenttest.cpp ) SET(HEADERS @@ -68,6 +69,7 @@ SET(HEADERS edbee/models/dynamicvariablestest.h edbee/util/rangelineiteratortest.h edbee/views/textthememanagertest.h + edbee/views/texteditorautocompletecomponenttest.h ) if (BUILD_WITH_QT5) @@ -85,4 +87,3 @@ ADD_EXECUTABLE(edbee-test TARGET_LINK_LIBRARIES(edbee-test edbee-lib ${QT_LIBS}) set_target_properties(edbee-test PROPERTIES AUTOMOC ON CXX_STANDARD 11) - diff --git a/edbee-test/edbee-test.pro b/edbee-test/edbee-test.pro index e80b4bdd..7a0b3c5b 100644 --- a/edbee-test/edbee-test.pro +++ b/edbee-test/edbee-test.pro @@ -50,7 +50,8 @@ SOURCES += \ edbee/util/rangesetlineiteratortest.cpp \ edbee/models/dynamicvariablestest.cpp \ edbee/util/rangelineiteratortest.cpp \ - edbee/views/textthememanagertest.cpp + edbee/views/textthememanagertest.cpp \ + edbee/views/texteditorautocompletecomponenttest.cpp HEADERS += \ edbee/commands/replaceselectioncommandtest.h \ @@ -79,7 +80,8 @@ HEADERS += \ edbee/util/rangesetlineiteratortest.h \ edbee/models/dynamicvariablestest.h \ edbee/util/rangelineiteratortest.h \ - edbee/views/textthememanagertest.h + edbee/views/textthememanagertest.h \ + edbee/views/texteditorautocompletecomponenttest.h ##OTHER_FILES += ../edbee-data/config/* ##OTHER_FILES += ../edbee-data/keymaps/* @@ -114,4 +116,3 @@ else:LIBNAME=libedbee.a win32:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../edbee-lib/release/$$LIBNAME else:win32:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../edbee-lib/debug/$$LIBNAME else:unix:!symbian: PRE_TARGETDEPS += $$OUT_PWD/../edbee-lib/$$LIBNAME - diff --git a/edbee-test/edbee/views/texteditorautocompletecomponenttest.cpp b/edbee-test/edbee/views/texteditorautocompletecomponenttest.cpp new file mode 100644 index 00000000..4c8cde97 --- /dev/null +++ b/edbee-test/edbee/views/texteditorautocompletecomponenttest.cpp @@ -0,0 +1,51 @@ +// edbee - Copyright (c) 2012-2025 by Rick Blommers and contributors +// SPDX-License-Identifier: MIT + +#include "texteditorautocompletecomponenttest.h" + +#include "edbee/models/textautocompleteprovider.h" +#include "edbee/models/textdocument.h" +#include "edbee/texteditorwidget.h" +#include "edbee/views/components/texteditorautocompletecomponent.h" +#include "edbee/views/components/texteditorcomponent.h" +#include "edbee/views/textselection.h" + +#include +#include +#include + +namespace edbee { + +void TextEditorAutoCompleteComponentTest::keepsEditorFocusWhenPopupIsShown() +{ + TextEditorWidget widget; + widget.resize(400, 200); + + auto* provider = new StringTextAutoCompleteProvider(); + provider->add("print", TextAutoCompleteKind::Function); + provider->add("printf", TextAutoCompleteKind::Function); + widget.textDocument()->autoCompleteProviderList()->giveProvider(provider); + + widget.textDocument()->setText("pri"); + widget.textSelection()->setRange(3, 3); + widget.show(); + widget.textEditorComponent()->setFocus(); + QApplication::processEvents(); + + widget.autoCompleteComponent()->updateList(); + QApplication::processEvents(); + + testTrue(widget.textEditorComponent()->hasFocus(), "autocomplete must not take focus from the editor"); + testFalse(widget.autoCompleteComponent()->listWidget()->hasFocus()); + + QKeyEvent downEvent(QEvent::KeyPress, Qt::Key_Down, Qt::NoModifier); + QApplication::sendEvent(widget.textEditorComponent(), &downEvent); + QKeyEvent enterEvent(QEvent::KeyPress, Qt::Key_Return, Qt::NoModifier); + QApplication::sendEvent(widget.textEditorComponent(), &enterEvent); + QApplication::processEvents(); + + testEqual(widget.textDocument()->text(), "printf"); + testTrue(widget.textEditorComponent()->hasFocus(), "accepting autocomplete should keep editor focus"); +} + +} // edbee diff --git a/edbee-test/edbee/views/texteditorautocompletecomponenttest.h b/edbee-test/edbee/views/texteditorautocompletecomponenttest.h new file mode 100644 index 00000000..7ffd9729 --- /dev/null +++ b/edbee-test/edbee/views/texteditorautocompletecomponenttest.h @@ -0,0 +1,20 @@ +// edbee - Copyright (c) 2012-2025 by Rick Blommers and contributors +// SPDX-License-Identifier: MIT + +#pragma once + +#include "edbee/util/test.h" + +namespace edbee { + +class TextEditorAutoCompleteComponentTest : public edbee::test::TestCase +{ + Q_OBJECT + +private slots: + void keepsEditorFocusWhenPopupIsShown(); +}; + +} // edbee + +DECLARE_TEST(edbee::TextEditorAutoCompleteComponentTest);