Skip to content

Commit ce9d3c5

Browse files
fix(webview): sync Qt/Win32 focus on WebView2 transitions
- Switch focusChanged to inspect `now` instead of `old` so notifyFocusLost is only called when focus actually leaves a WebView, not when it arrives — avoids spurious releases. - Pass `newFocusWidget` to notifyFocusLost so the Win32 impl can SetFocus on the correct top-level HWND rather than blindly calling MoveFocus, which was unreliable across windows. - Guard the SetFocus call with GetFocus() to skip it when the WebView2 controller no longer holds native focus. - Add GotFocusHandler to sync Qt focus state when WebView2 grabs native focus; without this Qt's focus tracking diverges from Win32, causing the next click on a Qt widget to not fire focusChanged. - Set ClickFocus policy on m_hostWidget so Qt registers it as a focusable target, completing the round-trip.
1 parent 382eab4 commit ce9d3c5

3 files changed

Lines changed: 59 additions & 19 deletions

File tree

src/MiniAppManager.cpp

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -100,19 +100,19 @@ MiniAppManager::MiniAppManager(NotepadNextApplication *app,
100100
// Re-tint tab icons on theme change
101101
connect(app, &NotepadNextApplication::effectiveThemeChanged, this, &MiniAppManager::retintAllIcons);
102102

103-
// When Qt focus leaves a WebView2 widget, tell it to release Win32 keyboard
104-
// focus so keystrokes go to the newly focused Qt widget (e.g. AI chat input).
105-
connect(qApp, &QApplication::focusChanged, this, [this](QWidget *old, QWidget * /*now*/) {
106-
if (!old) return;
107-
auto checkAndNotify = [old](WebViewWidget *wv) {
108-
if (!wv) return;
109-
if (old == wv || wv->isAncestorOf(old))
110-
wv->notifyFocusLost();
111-
};
112-
for (MiniAppInstance *inst : m_instances)
113-
checkAndNotify(inst->webViewWidget());
114-
for (const QuickBrowserTab &tab : m_quickBrowserTabs)
115-
checkAndNotify(tab.webView);
103+
// When Qt focus moves to a non-WebView widget, tell all WebViews to release
104+
// Win32 keyboard focus so keystrokes reach the newly focused Qt widget.
105+
connect(qApp, &QApplication::focusChanged, this, [this](QWidget * /*old*/, QWidget *now) {
106+
if (!now) return;
107+
for (MiniAppInstance *inst : m_instances) {
108+
WebViewWidget *wv = inst->webViewWidget();
109+
if (wv && now != wv && !wv->isAncestorOf(now))
110+
wv->notifyFocusLost(now);
111+
}
112+
for (const QuickBrowserTab &tab : m_quickBrowserTabs) {
113+
if (tab.webView && now != tab.webView && !tab.webView->isAncestorOf(now))
114+
tab.webView->notifyFocusLost(now);
115+
}
116116
});
117117
}
118118

src/widgets/WebViewWidget.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,10 @@ class WebViewWidget : public QWidget
4141
virtual void destroy() = 0;
4242
virtual QString debugInfo() const { return QString(); }
4343

44-
// Called when Qt focus leaves this WebView widget. Platform implementations
45-
// use this to release native keyboard focus (e.g. WebView2 on Windows).
46-
virtual void notifyFocusLost() {}
44+
// Called when Qt focus moves to a widget outside this WebView. Platform
45+
// implementations use this to release native keyboard focus back to the
46+
// target widget's top-level window (e.g. WebView2 on Windows).
47+
virtual void notifyFocusLost(QWidget *newFocusWidget) { Q_UNUSED(newFocusWidget); }
4748

4849
virtual void executeScript(const QString &js, std::function<void(const QString &)> callback = nullptr) = 0;
4950
virtual QString nativePostMessage() const = 0;

src/widgets/WebViewWidget_win.cpp

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ class WebViewWidgetWin : public WebViewWidget
9999
m_hostWidget = new QWidget(this);
100100
m_hostWidget->setAttribute(Qt::WA_NativeWindow);
101101
m_hostWidget->setAttribute(Qt::WA_DontCreateNativeAncestors, false);
102+
m_hostWidget->setFocusPolicy(Qt::ClickFocus);
102103
mainLayout()->addWidget(m_hostWidget, 1);
103104
}
104105

@@ -232,10 +233,18 @@ class WebViewWidgetWin : public WebViewWidget
232233
hideCdpUrl();
233234
}
234235

235-
void notifyFocusLost() override
236+
void notifyFocusLost(QWidget *newFocusWidget) override
236237
{
237-
if (m_controller)
238-
m_controller->MoveFocus(COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC);
238+
if (!m_controller) return;
239+
HWND focused = GetFocus();
240+
if (!focused) return;
241+
if (focused == m_hwnd || IsChild(m_hwnd, focused)) {
242+
HWND target = newFocusWidget
243+
? GetAncestor(reinterpret_cast<HWND>(newFocusWidget->effectiveWinId()), GA_ROOT)
244+
: GetAncestor(m_hwnd, GA_ROOT);
245+
if (target)
246+
SetFocus(target);
247+
}
239248
}
240249

241250
protected:
@@ -444,6 +453,13 @@ class WebViewWidgetWin : public WebViewWidget
444453
m_webView->add_WebMessageReceived(
445454
new WebMessageReceivedHandler(this), &msgToken);
446455

456+
// Sync Qt focus state when WebView2 grabs native focus. Without this,
457+
// Qt doesn't know focus left the previously focused widget, so clicking
458+
// back on that widget won't fire focusChanged (Qt thinks it already has
459+
// focus there).
460+
EventRegistrationToken gotFocusToken;
461+
m_controller->add_GotFocus(new GotFocusHandler(this), &gotFocusToken);
462+
447463
// Navigate to initial URL
448464
setLoading(true);
449465
m_dbgNavigateCalled = true;
@@ -656,6 +672,29 @@ class WebViewWidgetWin : public WebViewWidget
656672
}
657673
};
658674

675+
// GotFocusHandler: fires when WebView2 acquires native focus. Sets Qt focus
676+
// to the host widget so Qt's focus tracking stays in sync with Win32.
677+
struct GotFocusHandler : ICoreWebView2FocusChangedEventHandler {
678+
WebViewWidgetWin *owner;
679+
std::shared_ptr<std::atomic<bool>> alive;
680+
ULONG refCount = 1;
681+
GotFocusHandler(WebViewWidgetWin *o) : owner(o), alive(o->m_alive) {}
682+
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppv) override {
683+
if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_ICoreWebView2FocusChangedEventHandler)) {
684+
*ppv = this; AddRef(); return S_OK;
685+
}
686+
*ppv = nullptr; return E_NOINTERFACE;
687+
}
688+
ULONG STDMETHODCALLTYPE AddRef() override { return ++refCount; }
689+
ULONG STDMETHODCALLTYPE Release() override { if (--refCount == 0) { delete this; return 0; } return refCount; }
690+
HRESULT STDMETHODCALLTYPE Invoke(ICoreWebView2Controller *, IUnknown *) override {
691+
if (!alive->load(std::memory_order_acquire)) return S_OK;
692+
if (owner->m_hostWidget)
693+
owner->m_hostWidget->setFocus(Qt::OtherFocusReason);
694+
return S_OK;
695+
}
696+
};
697+
659698
// --- CDP Discovery ---
660699

661700
void startCdpDiscovery()

0 commit comments

Comments
 (0)