Skip to content

fix: keep Lua autocomplete popup from stealing editor focus - #10780

Merged
vadi2 merged 5 commits into
Mudlet:developmentfrom
Furox-Art:fix/autocomplete-focus
Oct 3, 2026
Merged

vadi2 merged 5 commits into
Mudlet:developmentfrom
Furox-Art:fix/autocomplete-focus

Conversation

@Furox-Art

Copy link
Copy Markdown
Contributor

Brief overview of PR changes/additions

Keeps the keyboard focus with the source editor while the Lua autocomplete popup is open, instead of letting the popup take it. dlgSourceEditorArea (the widget that owns and configures the edbee editor) now applies the same approach Qt's own QCompleter uses for completion popups:

  • the popup (QMenu) and its QListWidget are made non-focusable (Qt::NoFocus) and non-activating (WA_ShowWithoutActivating);
  • the list's focus proxy is pointed at the editor's text component, which turns edbee's internal listWidget()->setFocus() call into a no-op;
  • a small event filter routes key events while the popup is open: the completion keys (Up/Down/PageUp/PageDown navigate, Enter/Return/Tab accept, Escape cancels) go to the popup list, everything else (normal typing, Backspace, IME input) goes to the editor, and keys that edbee's popup design closes on (e.g. Space, punctuation) close the popup and reach the editor.

Motivation for adding to Mudlet

Fixes #5310. Users intermittently lose the ability to type in the Lua editor while the autocomplete popup is showing; pressing Esc removes the popup, but it reappears on the next letter and typing stays blocked.

Root cause analysis (Qt event level)

In the bundled edbee-lib, TextEditorAutoCompleteComponent::updateList() runs after every character typed/backspaced and does:

menuRef_->popup(menuRef_->pos());
listWidgetRef_->setFocus();

menuRef_ is a QMenu, i.e. a Qt::Popup window. Two Qt behaviours then conspire against the editor:

  1. QWidget::isActiveWindow() unconditionally reports a visible Qt::Popup window as active, so listWidgetRef_->setFocus() really does move QApplication::focusWidget() from the editor's text component to the list (QWidget::setFocus() does not consult the focus policy, so setting Qt::NoFocus alone does not prevent this).
  2. While any popup is open, QWidgetWindow::handleKeyEvent() delivers all keyboard events to popup->focusWidget() (or the popup itself) — never to the editor. The editor only receives text because edbee's event filter forwards letters back to it one by one, and every forwarded character re-enters updateList() → popup() + setFocus() again. This focus ping-pong (plus the popup window activation churn that WA_ShowWithoutActivating is missing) loses keys and IME input on Windows, leaving the editor unusable until the popup is dismissed — matching the video in Autocomplete steals window focus, prevents further typing #5310.

Since Qt delivers popup-mode keys to the popup widget, the fix routes them from there — the same place QCompleter routes them from (QCompleter::setPopup() sets Qt::Popup + Qt::NoFocus + setFocusProxy(widget) on its popup, and QCompleter::eventFilter() forwards unhandled keys to the source widget):

  • list->setFocusProxy(editorComponent) makes edbee's setFocus() provably do nothing: QWidget::setFocus() resolves it to the deepest focus proxy (the editor component), sees QApplication::focusWidget() already equals it, and returns early — no FocusOut/FocusIn, no state change.
  • WA_ShowWithoutActivating on the popup window stops the window-activation steal (notably on Windows), so the main window stays active on every popup refresh.
  • The event filter installed on the popup menu mirrors QCompleter::eventFilter()'s routing, preserving edbee's existing key semantics exactly (letters/digits/Backspace keep the popup open, Up/Down/PageUp/PageDown navigate the list through edbee's own list filter, Enter/Tab accept, Escape sets canceled_ so the popup stays dismissed until a new word is typed, other keys close the popup and reach the editor).
  • The filter also swallows the synthetic FocusOut that QApplicationPrivate::openPopup() sends to the current focus widget when the popup first shows — edbee's TextEditorComponent::focusOutEvent() resets undo coalescing, which would otherwise fragment the undo stack on every popup appearance (this is the same reason QCompleter has its eatFocusOut flag).

How to reproduce/test

Reproduce (on development, Windows):

  1. Enable Lua editor autocomplete (Preferences → Editor → "Show an automatic completion list").
  2. Open the trigger/script editor and type a Lua prefix with many matches, e.g. ce (createButton, createGauge, ...).
  3. Keep typing/deleting: on development focus can end up in the popup and typing stops working (keys are swallowed or lost); the user must press Esc and the popup reappears on the next letter.

With this PR:

  1. Same setup — type ce: the popup appears and the editor keeps the caret/focus.
  2. Continue typing createButton: text appears normally while the popup tracks the word; typing is never blocked.
  3. Up/Down/PageUp/PageDown move the selection in the popup; Enter or Tab inserts the selected suggestion; Esc dismisses it and it stays dismissed while editing that word.
  4. Backspace keeps the popup updated; pressing Space, punctuation or moving the caret with Left/Right closes the popup (same behaviour as before).
  5. Mouse selection: hovering/clicking/double-clicking an entry still selects/inserts it.
  6. The editor's undo stack still coalesces a typed word into a single undo step while the popup is open.

No local build was possible for this PR, so it is verified by code inspection against the Qt event flow described above; CI build + reviewer manual test are much appreciated.

Other info (issues closed, discussion etc)

Fixes #5310.

/claim #5310

Disclosure: This PR was authored by @Furox-Art (AI-assisted). Happy to adjust anything.

@add-deployment-links

add-deployment-links Bot commented Sep 12, 2026 •

Copy link
Copy Markdown

@greptile-apps

greptile-apps Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Too many files changed for review (1487 files, 100 file limit).

@github-actions github-actions Bot added this to the 5.1.0 next release milestone Sep 12, 2026
@Furox-Art
Furox-Art changed the base branch from main to development September 13, 2026 13:26
@Furox-Art
Furox-Art requested a review from a team as a code owner September 13, 2026 13:26
Furox-Art and others added 3 commits September 18, 2026 21:03
The header lives at edbee/views/components/texteditorcomponent.h, not
edbee/texteditorcomponent.h, which broke the compile on every platform.
@Furox-Art
Furox-Art force-pushed the fix/autocomplete-focus branch from 824f4e9 to bfbdbdf Compare September 18, 2026 18:04
@Furox-Art

Copy link
Copy Markdown
Contributor Author

Small update: rebased onto current development — the branch had drifted 73 commits behind, which is why the review tool was showing "1487 files changed". The actual change is still just these 2 files, and that banner should clear now. All checks are re-running on the fresh head.

Root cause in one line: edbee's autocomplete list calls setFocus() on itself every time the popup shows, stealing the keyboard focus from the editor (the #5310 bug). We point the list's focus proxy at the editor component, so that call becomes a no-op, the editor keeps focus while the popup is open, and completion keys are routed to the popup via the existing event filter.

Happy to tweak anything after review — /attempt #5310 was claimed on Sep 12.

vadi2 commented Sep 23, 2026

Copy link
Copy Markdown
Member

Thanks for working on this. Of the three open attempts at #5310, this is the one whose approach holds up. It works much like QCompleter: the popup gets no focus widget, a filter on the menu sends each key to either the list or the editor, and the focus proxy soaks up edbee's setFocus(). We tested it against a real edbee build, sending keys through Qt's actual popup routing rather than sendEvent. Typing, Backspace, Escape and mid-list navigation all work, and undo even improves: one Ctrl+Z now removes a whole typed word.

A quick note on expectations: #5310 has no bounty attached, so there is nothing to claim here. We'd still be glad to merge the fix.

Blocking: stack overflow on arrow keys at the ends of the list

Pressing Up on the first suggestion crashes Mudlet. edbee selects row 0 every time it fills the list, so typing pri and pressing Up is enough. So do:

  • Down on the last suggestion;
  • any arrow key when there is a single suggestion;
  • PageUp on row 0.

The cause:

  1. routeAutoCompleteKeyPress() sends the key to the list (dlgSourceEditorArea.cpp:135).
  2. The list can't move, so QAbstractItemView::keyPressEvent ignores the event.
  3. QApplication::notify then passes the ignored event up to the list's parent, which is the popup QMenu.
  4. This PR's filter on that menu catches it and sends it back to the list, and the cycle repeats with no end.

Reproduced with the harness (exit code 139). gdb shows about 59k frames of routeAutoCompleteKeyPress → notify(list) → QMenu → eventFilter → routeAutoCompleteKeyPress.

A re-entrancy guard in the KeyPress branch fixes it. In the same harness, all four edge cases stopped crashing and normal navigation was unaffected:

// a key the list ignored comes back up to its parent, this menu - drop it
if (mRoutingAutoCompleteKey) {
    return true;
}
QScopedValueRollback<bool> guard(mRoutingAutoCompleteKey, true);
routeAutoCompleteKeyPress(static_cast<QKeyEvent*>(pEvent));

Forwarding every key straight to the list still needs this guard, because the loop comes from the list's parent being the filtered menu.

Smaller items

  • FocusOut filter is broader than its comment (dlgSourceEditorArea.cpp:184-191). It swallows every FocusOut while the popup is visible. Please also require static_cast<QFocusEvent*>(pEvent)->reason() == Qt::PopupFocusReason. The "IME state" part of the comment doesn't hold: edbee's focusOutEvent only resets undo coalescing.

  • WA_ShowWithoutActivating on the list (line 109) does nothing, because the list isn't a top-level window.

  • Preferences preview editor. edbeePreviewWidget (dlgProfilePreferences.cpp) also has autocomplete enabled and still steals focus.

  • No test. Please add a functional test (the EDITOR_GROUP_TEST_SOURCES group fits) that:

    • types a prefix, then delivers keys through the popup's QWindow (QTest::keyClick(QApplication::activePopupWidget()->windowHandle(), ...));
    • covers Up on row 0 at minimum;
    • checks that focus stays on the editor.

    QApplication::sendEvent to a widget skips popup routing, so a test built on it passes regardless.

The original symptom doesn't reproduce on Linux offscreen, so a manual check on Windows would still be good before merge.


Generated by Claude Code

Pressing Up on the first suggestion, Down on the last, or any arrow key
when there is a single suggestion, crashed Mudlet with a stack overflow.

dlgSourceEditorArea routes a key press received by the autocomplete popup
either to the suggestion list or to the editor. edbee's own filter lets
Up/Down/PageUp/PageDown through to the list so QAbstractItemView can move
the selection, and a key that cannot move it - which is every one of those
four at the end of the list - is left unhandled. QApplication::notify()
then passes an ignored key on to the next widget up the parent chain, and
edbee parents the list to the popup QMenu, which is the very widget the
routing filter is installed on. The key therefore came back up to the
filter already routed once, and routing it to the list again repeated
until the stack ran out.

Drop such a key instead, guarded by a flag that is only set while a key
is being routed. Keys the list can act on are unaffected, so navigation
and acceptance still work.

Adds EditorAutoCompleteFocusTest, which delivers keys through the popup's
own QWindow so Qt's popup routing is what is exercised - a test built on
QApplication::sendEvent() to a widget would skip that path and pass either
way. It covers the four end-of-list cases, checks that the keyboard focus
stays on the editor, and keeps a case that navigation still moves the
selection so a filter that dropped every key could not pass.

Without this commit the new test ends the process with SIGSEGV.

Assisted-by: opencode:opencode/space-bunny-free
Addresses the rest of the review on #10780.

The autocomplete key routing moves out of dlgSourceEditorArea into
EditorAutoCompleteFocusHandler, because two editors need it and the
preferences dialog's preview was left stealing the focus as it stood. The
script editor and the preview now each get the same treatment from the
same class, so the contract cannot drift between them.

- The FocusOut filter consumed every FocusOut while the popup was up, not
  just the synthetic one Qt sends with Qt::PopupFocusReason when a popup
  opens. Any other reason is a real focus change, and hiding it from the
  editor left it believing it still had the focus. Only PopupFocusReason
  is swallowed now.

- Qt::WA_ShowWithoutActivating is only consulted for a top-level window -
  QWidget checks it in show() under isWindow() - and edbee parents the
  suggestion list to the menu, so setting it on the list had never done
  anything. It stays on the menu, where Qt does look at it, and is off the
  list so it no longer reads as if it were load-bearing there. The list
  keeps the focus proxy and Qt::NoFocus, which are what actually hold the
  keyboard in the editor.

- edbeePreviewWidget gets a handler too, installed in the constructor so
  it holds even when loadEditorTab() returns early for want of a profile.

- EditorAutoCompleteFocusTest gains cases for each of the three, and keeps
  the four end-of-list navigation cases driving keys through the popup's
  own QWindow. The FocusOut case works from a probe event filter installed
  before the handler, so it only sees the FocusOuts the handler let
  through: the popup's own is swallowed, OtherFocusReason and
  MouseFocusReason are not.

- The test also stops including MudletPaths.h, which it never used. That
  header no longer exists on development (removed in 2ea5d46, #10472), and
  it was the only file in the merge tree still reaching for it, so it was
  breaking the build on every platform.

Assisted-by: opencode:opencode/space-bunny-free
@Furox-Art

Copy link
Copy Markdown
Contributor Author

Hi! The CI workflows for this PR are waiting for approval (action_required). Could you please approve the workflow runs so the checks can execute? The PR is ready for review. Thanks!

@Furox-Art

Copy link
Copy Markdown
Contributor Author

Hi @vadi2 — following up: the blocking stack-overflow issue and the smaller items you listed were addressed in d868e810 (re-entrancy guard on the arrow-key loop) and d8bf2365 (FocusOut narrowed to Qt::PopupFocusReason, preferences preview editor covered). Could you take another look when you have a moment? CI workflows are also waiting on approval (action_required) if a maintainer can enable them. Thanks!

@vadi2 vadi2 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@vadi2
vadi2 merged commit 1afc06d into Mudlet:development Oct 3, 2026
13 checks passed
vadi2 added a commit that referenced this pull request Oct 5, 2026
…again (#11449)

#### Brief overview of PR changes/additions
Clicking into the Lua code area of the trigger, alias, script, timer,
key or button editor gives it the keyboard again, and Tab reaches it.
Before this fix, what you typed went to the Name field (silently
renaming the item) or to the item tree.

The autocomplete focus handler from #10780 made the editor the
completion list's focus proxy and then set the list to `Qt::NoFocus`.
`QWidget::setFocusPolicy()` also applies the policy to a widget's focus
proxy, so the editor itself became `Qt::NoFocus`. The two calls are now
swapped, so the list is non-focusable and the editor keeps
`Qt::WheelFocus`. The same handler sits on the editor preview in
Settings > Editor, which is fixed too.

`EditorAutoCompleteFocusTest` gains a case that clicks into an editor
next to a line edit holding the focus, and checks that the editor takes
it. It also checks the focus policy on both the script editor and the
preview. The existing cases used `setFocus()`, which works whatever the
policy is, so they could not catch this. The new case fails without the
fix and passes with it.

#### Motivation for adding to Mudlet
Users could not type Lua into an item at all, which blocks the core
scripting workflow. This is a regression since 5.0.1 and is not in any
release yet.

#### Other info (issues closed, discussion etc)
Closes #11445

Assisted-by: Claude Code

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01VgAGrxnNUmXfYH2ZsDdbCq

---
_Generated by [Claude
Code](https://claude.ai/code/session_01VgAGrxnNUmXfYH2ZsDdbCq)_

---------

Co-authored-by: Claude <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Autocomplete steals window focus, prevents further typing

2 participants