Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -327,6 +332,21 @@ void TextEditorAutoCompleteComponent::hideEvent(QHideEvent* event)
}


void TextEditorAutoCompleteComponent::sendKeyEventTo(QWidget* target, QKeyEvent* sourceEvent)
{
QKeyEvent event(sourceEvent->type(),
sourceEvent->key(),
sourceEvent->modifiers(),
sourceEvent->text(),
sourceEvent->isAutoRepeat(),
sourceEvent->count());

eventBeingFiltered_ = true;
QApplication::sendEvent(target, &event);
eventBeingFiltered_ = false;
}


/// we need to intercept keypresses if the widget is visible
bool TextEditorAutoCompleteComponent::eventFilter(QObject *obj, QEvent *event)
{
Expand All @@ -336,15 +356,23 @@ bool TextEditorAutoCompleteComponent::eventFilter(QObject *obj, QEvent *event)
return QObject::eventFilter(obj, event);
}

if(obj == listWidgetRef_ && event->type() == QEvent::KeyPress) {
if (eventBeingFiltered_) {
return QObject::eventFilter(obj, event);
}

if ((obj == editorComponentRef_ || obj == listWidgetRef_) && event->type() == QEvent::KeyPress && menuRef_->isVisible()) {
QKeyEvent* key = static_cast<QKeyEvent*>(event);
const bool editorHasEvent = obj == editorComponentRef_;

// text keys are allowed
if (!key->text().isEmpty()) {
QChar nextChar = key->text().at(0);
if (nextChar.isLetterOrNumber()) {
QApplication::sendEvent(editorComponentRef_, event);
return true;
if (editorHasEvent) {
return false;
}
sendKeyEventTo(editorComponentRef_, key);
return true;
}
}

Expand All @@ -360,7 +388,10 @@ bool TextEditorAutoCompleteComponent::eventFilter(QObject *obj, QEvent *event)
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);
if (editorHasEvent) {
return false;
}
sendKeyEventTo(editorComponentRef_, key);
return true;
} else if (listWidgetRef_->currentItem()) {
insertCurrentSelectedListItem();
Expand All @@ -371,24 +402,37 @@ bool TextEditorAutoCompleteComponent::eventFilter(QObject *obj, QEvent *event)
break;

case Qt::Key_Backspace:
QApplication::sendEvent(editorComponentRef_, event);
if (editorHasEvent) {
return false;
}
sendKeyEventTo(editorComponentRef_, key);
return true;

case Qt::Key_Shift: //ignore shift, don't hide
QApplication::sendEvent(editorComponentRef_, event);
if (editorHasEvent) {
return false;
}
sendKeyEventTo(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 (editorHasEvent) {
sendKeyEventTo(listWidgetRef_, key);
return true;
}
return false;
}

// default operation is to hide and continue the event
menuRef_->close();
QApplication::sendEvent(editorComponentRef_, event);
if (editorHasEvent) {
return false;
}
sendKeyEventTo(editorComponentRef_, key);
return true;

}
Expand Down Expand Up @@ -428,7 +472,6 @@ void TextEditorAutoCompleteComponent::updateList()
// fills the autocomplete list with the curent word
if (fillAutoCompleteList(doc, range, currentWord_)) {
menuRef_->popup(menuRef_->pos());
listWidgetRef_->setFocus();

// position the widget
showInfoTip();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

class QListWidget;
class QListWidgetItem;
class QKeyEvent;

namespace edbee {

Expand Down Expand Up @@ -69,6 +70,7 @@ class EDBEE_EXPORT TextEditorAutoCompleteComponent : public QWidget
bool fillAutoCompleteList(TextDocument *document, const TextRange &range, const QString& word );

void positionWidgetForCaretOffset(size_t offset);
void sendKeyEventTo(QWidget* target, QKeyEvent* sourceEvent);
bool eventFilter(QObject* obj, QEvent* event);

void hideEvent(QHideEvent* event);
Expand Down
2 changes: 2 additions & 0 deletions edbee-test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ SET(SOURCES
edbee/util/rangesetlineiteratortest.cpp
edbee/models/dynamicvariablestest.cpp
edbee/util/rangelineiteratortest.cpp
edbee/views/texteditorautocompletecomponenttest.cpp
edbee/views/textthememanagertest.cpp
)

Expand Down Expand Up @@ -67,6 +68,7 @@ SET(HEADERS
edbee/util/rangesetlineiteratortest.h
edbee/models/dynamicvariablestest.h
edbee/util/rangelineiteratortest.h
edbee/views/texteditorautocompletecomponenttest.h
edbee/views/textthememanagertest.h
)

Expand Down
2 changes: 2 additions & 0 deletions edbee-test/edbee-test.pro
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ SOURCES += \
edbee/util/rangesetlineiteratortest.cpp \
edbee/models/dynamicvariablestest.cpp \
edbee/util/rangelineiteratortest.cpp \
edbee/views/texteditorautocompletecomponenttest.cpp \
edbee/views/textthememanagertest.cpp

HEADERS += \
Expand Down Expand Up @@ -79,6 +80,7 @@ HEADERS += \
edbee/util/rangesetlineiteratortest.h \
edbee/models/dynamicvariablestest.h \
edbee/util/rangelineiteratortest.h \
edbee/views/texteditorautocompletecomponenttest.h \
edbee/views/textthememanagertest.h

##OTHER_FILES += ../edbee-data/config/*
Expand Down
224 changes: 224 additions & 0 deletions edbee-test/edbee/views/texteditorautocompletecomponenttest.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
// edbee - Copyright (c) 2012-2025 by Rick Blommers and contributors
// SPDX-License-Identifier: MIT

#include "texteditorautocompletecomponenttest.h"

#include <QApplication>
#include <QKeyEvent>
#include <QLineEdit>
#include <QListWidget>
#include <QVBoxLayout>
#include <QWidget>

#include "edbee/edbee.h"
#include "edbee/models/textautocompleteprovider.h"
#include "edbee/models/textdocument.h"
#include "edbee/texteditorcontroller.h"
#include "edbee/texteditorwidget.h"
#include "edbee/views/components/texteditorautocompletecomponent.h"
#include "edbee/views/components/texteditorcomponent.h"

namespace edbee {

namespace {

class AutoCompleteFixture
{
public:
AutoCompleteFixture()
: widget()
, provider(new StringTextAutoCompleteProvider())
, editor(widget.textEditorComponent())
, autocomplete(widget.autoCompleteComponent())
, list(autocomplete->listWidget())
{
widget.resize(640, 320);
widget.show();

provider->add("compare");
provider->add("complete");
provider->add("compose");
widget.textDocument()->autoCompleteProviderList()->giveProvider(provider);

QApplication::processEvents();
editor->setFocus();
QApplication::processEvents();
}

void typePrefix(const QString& prefix)
{
for (const QChar ch : prefix) {
sendEditorKey(ch.toLatin1(), QString(ch));
}
}

void sendEditorKey(int key, const QString& text = QString())
{
QKeyEvent keyEvent(QEvent::KeyPress, key, Qt::NoModifier, text);
QApplication::sendEvent(editor, &keyEvent);
QApplication::processEvents();
}

void sendFocusedWidgetKey(int key, const QString& text = QString())
{
QWidget* target = QApplication::focusWidget();
Q_ASSERT(target != nullptr);

QKeyEvent keyEvent(QEvent::KeyPress, key, Qt::NoModifier, text);
QApplication::sendEvent(target, &keyEvent);
QApplication::processEvents();
}

void clearDocument()
{
while (!widget.textDocument()->text().isEmpty()) {
sendEditorKey(Qt::Key_Backspace);
}
}

TextEditorWidget widget;
StringTextAutoCompleteProvider* provider;
TextEditorComponent* editor;
TextEditorAutoCompleteComponent* autocomplete;
QListWidget* list;
};

} // namespace

void TextEditorAutoCompleteComponentTest::openingAutocompleteKeepsEditorFocused()
{
AutoCompleteFixture fixture;
fixture.typePrefix("com");

testEqual(fixture.widget.textDocument()->text(), "com");
testEqual(fixture.list->count(), 3);
testTrue(fixture.list->isVisible());
testEqual(fixture.editor->focusPolicy(), Qt::WheelFocus);
testEqual(fixture.list->focusPolicy(), Qt::NoFocus);
testTrue(fixture.list->testAttribute(Qt::WA_ShowWithoutActivating));
testTrue(fixture.editor->hasFocus());
testFalse(fixture.list->hasFocus());
}


void TextEditorAutoCompleteComponentTest::typingContinuesThroughEditorWhenAutocompleteIsVisible()
{
AutoCompleteFixture fixture;
fixture.typePrefix("com");

fixture.sendEditorKey(Qt::Key_P, "p");

testEqual(fixture.widget.textDocument()->text(), "comp");
testTrue(fixture.editor->hasFocus());
testFalse(fixture.list->hasFocus());
}


void TextEditorAutoCompleteComponentTest::typingStillRoutesThroughEditorWhenListGetsFocus()
{
AutoCompleteFixture fixture;
fixture.typePrefix("com");

fixture.list->setFocusPolicy(Qt::StrongFocus);
fixture.list->setFocus();
QApplication::processEvents();

testTrue(fixture.list->hasFocus());
testTrue(QApplication::focusWidget() == fixture.list);

fixture.sendFocusedWidgetKey(Qt::Key_P, "p");

testEqual(fixture.widget.textDocument()->text(), "comp");
testTrue(fixture.list->hasFocus());
}


void TextEditorAutoCompleteComponentTest::navigationKeysMoveSelectionWithoutListFocus()
{
AutoCompleteFixture fixture;
fixture.typePrefix("com");

testEqual(fixture.list->currentRow(), 0);

fixture.sendEditorKey(Qt::Key_Down);

testEqual(fixture.list->currentRow(), 1);
testEqual(fixture.widget.textDocument()->text(), "com");
testTrue(fixture.editor->hasFocus());
testFalse(fixture.list->hasFocus());
}


void TextEditorAutoCompleteComponentTest::escapeCancelsAutocompleteUntilWordIsCleared()
{
AutoCompleteFixture fixture;
fixture.typePrefix("com");

fixture.sendEditorKey(Qt::Key_Escape);

testFalse(fixture.list->isVisible());
testTrue(fixture.editor->hasFocus());

fixture.sendEditorKey(Qt::Key_P, "p");
testEqual(fixture.widget.textDocument()->text(), "comp");
testFalse(fixture.list->isVisible());

fixture.clearDocument();
testEqual(fixture.widget.textDocument()->text(), "");

fixture.typePrefix("com");
testEqual(fixture.widget.textDocument()->text(), "com");
testTrue(fixture.list->isVisible());
}


void TextEditorAutoCompleteComponentTest::hidingAutocompleteDoesNotStealFocusFromSiblingWidget()
{
QWidget container;
QVBoxLayout layout(&container);
TextEditorWidget* widget = new TextEditorWidget(&container);
QLineEdit* sibling = new QLineEdit(&container);

layout.addWidget(widget);
layout.addWidget(sibling);
container.resize(640, 360);
container.show();
QApplication::processEvents();

StringTextAutoCompleteProvider* provider = new StringTextAutoCompleteProvider();
provider->add("compare");
widget->textDocument()->autoCompleteProviderList()->giveProvider(provider);

TextEditorComponent* editor = widget->textEditorComponent();
QListWidget* list = widget->autoCompleteComponent()->listWidget();

editor->setFocus();
QApplication::processEvents();

for (const QChar ch : QStringLiteral("com")) {
QKeyEvent keyEvent(QEvent::KeyPress, ch.toLatin1(), Qt::NoModifier, QString(ch));
QApplication::sendEvent(editor, &keyEvent);
}
QApplication::processEvents();

testTrue(list->isVisible());
testTrue(editor->hasFocus());

sibling->setFocus();
QApplication::processEvents();
testTrue(sibling->hasFocus());
testTrue(QApplication::focusWidget() == sibling);

QKeyEvent escapeKey(QEvent::KeyPress, Qt::Key_Escape, Qt::NoModifier);
QWidget* focusWidget = QApplication::focusWidget();
Q_ASSERT(focusWidget != nullptr);
QApplication::sendEvent(focusWidget, &escapeKey);
QApplication::processEvents();

testTrue(sibling->hasFocus());
testFalse(editor->hasFocus());
testFalse(list->hasFocus());
testFalse(list->isVisible());
}

} // namespace edbee
Loading