Skip to content
Merged
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
39 changes: 38 additions & 1 deletion modules/juce_gui_extra/misc/juce_WebBrowserComponent.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -378,10 +378,30 @@ class NativeEventListeners
class WebBrowserComponent::Impl
{
public:
Impl (WebBrowserComponent& ownerIn, const Options& optionsIn)
Impl (WebBrowserComponent& ownerIn, const Options& optionsInRaw)
: owner (ownerIn),
options ([&]
{
// Auto-register a native function so JS can flip the editable-focus
// routing without every consumer wiring up their own bridge plumbing.
// Only registered when native integration is already in use — adding
// it unconditionally would force `withNativeIntegrationEnabled` on for
// every WebBrowserComponent, which JUCE flags as a security risk for
// components loading untrusted content.
const bool nativeIntegrationInUse = optionsInRaw.getNativeIntegrationsEnabled()
|| ! optionsInRaw.getNativeFunctions().empty();

const auto optionsIn = nativeIntegrationInUse
? optionsInRaw.withNativeFunction (
"__juceSetEditableFocusActive",
[this] (const Array<var>& args, const std::function<void (var)>& completion)
{
if (! args.isEmpty())
setEditableFocusActive (static_cast<bool> (args[0]));
completion ({});
})
: optionsInRaw;

makeFunctionsProviderIfNecessary (nativeFunctionsProvider, *this, optionsIn);

if (nativeFunctionsProvider.has_value())
Expand Down Expand Up @@ -483,6 +503,11 @@ class WebBrowserComponent::Impl
platform->focusGainedWithDirection (type, dir);
}

void setEditableFocusActive (bool active)
{
platform->setEditableFocusActive (active);
}

struct Platform;

private:
Expand All @@ -503,6 +528,13 @@ class WebBrowserComponent::Impl

virtual void focusGainedWithDirection (FocusChangeType, FocusChangeDirection) {}
virtual void fallbackPaint (Graphics&) {}

// Default no-op — platforms that support host keyboard pass-through
// (macOS WKWebView, Windows WebView2) override this to flip the
// routing of OS-level key events between the host responder chain
// and the embedded DOM. See WebBrowserComponent::setEditableFocusActive
// for the semantics.
virtual void setEditableFocusActive (bool) {}
};

static void makeFunctionsProviderIfNecessary (std::optional<NativeFunctionsProvider>& provider,
Expand Down Expand Up @@ -724,6 +756,11 @@ void WebBrowserComponent::focusGainedWithDirection (FocusChangeType type,
impl->focusGainedWithDirection (type, direction);
}

void WebBrowserComponent::setEditableFocusActive (bool editableFocusActive)
{
impl->setEditableFocusActive (editableFocusActive);
}

#endif

} // namespace juce
30 changes: 30 additions & 0 deletions modules/juce_gui_extra/misc/juce_WebBrowserComponent.h
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,36 @@ class JUCE_API WebBrowserComponent : public Component
*/
void emitEventIfBrowserIsVisible (const Identifier& eventId, const var& object);

/** Controls whether key events received by the underlying OS web surface should be
forwarded to the host responder chain instead of being consumed by the web view.

When the WebBrowserComponent is hosted inside a plugin, the OS web surface
(WKWebView on macOS, WebView2 on Windows) becomes first responder on click and
swallows keystrokes — preventing host shortcuts (transport, MIDI typing keyboards,
etc.) from reaching the DAW. JUCE's default behaviour is to forward those keys
to the host so the DAW remains controllable. Call this with `true` while an
editable DOM element (e.g. an `<input>`) has focus so character input reaches
the DOM, and `false` again when the editable element loses focus.

Frontend code can drive this through the auto-registered native function
`__juceSetEditableFocusActive`, available via JUCE's `getNativeFunction` helper:

@code
import { getNativeFunction } from 'juce-framework-frontend';
const setEditableFocus = getNativeFunction('__juceSetEditableFocusActive');
document.addEventListener('focusin', e => {
const t = e.target;
const editable =
t instanceof HTMLInputElement ||
t instanceof HTMLTextAreaElement ||
t instanceof HTMLSelectElement ||
(t instanceof HTMLElement && t.isContentEditable);
setEditableFocus(editable);
});
@endcode
*/
void setEditableFocusActive (bool editableFocusActive);

//==============================================================================
/** This callback is called when the browser is about to navigate
to a new location.
Expand Down
78 changes: 78 additions & 0 deletions modules/juce_gui_extra/native/juce_WebBrowserComponent_mac.mm
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,22 @@ static var fromObject (id object)
return getIvar<LastFocusChange*> (instance, lastFocusChangeMemberName);
}

// Per-instance pointer to the WKWebViewImpl's std::atomic<bool> tracking whether
// the JS layer has reported editable DOM focus. Read on every key event; when
// false (the default) the responder forwards keys to the host so the DAW
// receives transport and shortcut keys instead of WKWebView swallowing them.
static const char* editableFocusActiveMemberName = "editableFocusActiveHandle";

[[maybe_unused]] static void setEditableFocusActiveHandle (id instance, std::atomic<bool>* flag)
{
object_setInstanceVariable (instance, editableFocusActiveMemberName, flag);
}

[[maybe_unused]] static std::atomic<bool>* getEditableFocusActiveHandle (id instance)
{
return getIvar<std::atomic<bool>*> (instance, editableFocusActiveMemberName);
}

#if JUCE_MAC
template <class WebViewClass>
struct WebViewKeyEquivalentResponder final : public ObjCClass<WebViewClass>
Expand All @@ -186,10 +202,55 @@ explicit WebViewKeyEquivalentResponder (bool acceptsFirstMouse)
: Base ("WebViewKeyEquivalentResponder_")
{
this->template addIvar<LastFocusChange*> (lastFocusChangeMemberName);
this->template addIvar<std::atomic<bool>*> (editableFocusActiveMemberName);

// When an editable DOM element has focus (signalled by JS via
// __juceSetEditableFocusActive), keys are routed to WKWebView's default
// handlers so character input reaches the DOM. Otherwise we forward them
// to the next responder so the plugin host (DAW) receives shortcuts and
// MIDI-typing keys. Default is "forward to host" — the OS web surface
// would otherwise swallow every keystroke once it became first responder.
//
// The lambdas passed to addMethod must be captureless so JUCE's
// toFnPtr can lower them to plain function pointers, so the
// editable-focus read is inlined into each method rather than living
// in a local helper closure.

this->addMethod (@selector (keyDown:),
[] (id self, SEL selector, NSEvent* event)
{
auto* handle = getEditableFocusActiveHandle (self);

if (handle != nullptr && handle->load())
Base::template sendSuperclassMessage<void> (self, selector, event);
else
[[self nextResponder] keyDown:event];
});

this->addMethod (@selector (keyUp:),
[] (id self, SEL selector, NSEvent* event)
{
auto* handle = getEditableFocusActiveHandle (self);

if (handle != nullptr && handle->load())
Base::template sendSuperclassMessage<void> (self, selector, event);
else
[[self nextResponder] keyUp:event];
});

this->addMethod (@selector (performKeyEquivalent:),
[] (id self, SEL selector, NSEvent* event)
{
// Outside of editable focus, return NO so AppKit walks
// the responder chain past us — that lets host Cmd-shortcuts
// (DAW save, undo, transport variants) reach the plugin
// host instead of being trapped by WKWebView.
auto* handle = getEditableFocusActiveHandle (self);
const bool editableFocusActive = handle != nullptr && handle->load();

if (! editableFocusActive)
return (BOOL) NO;

const auto isCommandDown = [event]
{
const auto modifierFlags = [event modifierFlags];
Expand Down Expand Up @@ -702,6 +763,7 @@ static void displayError (WebBrowserComponent* owner, NSError* error)
groupName: nsEmptyString()]);

setLastFocusChangeHandle (webView.get(), &lastFocusChange);
setEditableFocusActiveHandle (webView.get(), &editableFocusActive);

webView.get().customUserAgent = juceStringToNS (userAgent);

Expand Down Expand Up @@ -809,9 +871,15 @@ void evaluateJavascript (const String&, WebBrowserComponent::EvaluationCallback)
jassertfalse;
}

void setEditableFocusActive (bool active) override
{
editableFocusActive.store (active, std::memory_order_relaxed);
}

private:
WebBrowserComponent& browser;
LastFocusChange lastFocusChange;
std::atomic<bool> editableFocusActive { false };
ObjCObjectHandle<WebView*> webView;
ObjCObjectHandle<id> clickListener;
};
Expand Down Expand Up @@ -903,6 +971,7 @@ void evaluateJavascript (const String&, WebBrowserComponent::EvaluationCallback)
configuration: config.get()]);

setLastFocusChangeHandle (webView.get(), &lastFocusChange);
setEditableFocusActiveHandle (webView.get(), &editableFocusActive);
#else
webView.reset ([[WKWebView alloc] initWithFrame: CGRectMake (0, 0, 100.0f, 100.0f)
configuration: config.get()]);
Expand Down Expand Up @@ -1208,13 +1277,22 @@ void evaluateJavascript (const String& script, WebBrowserComponent::EvaluationCa
}];
}

void setEditableFocusActive (bool active) override
{
editableFocusActive.store (active, std::memory_order_relaxed);
}

private:
static inline auto blankPageUrl = "about:blank";

WebBrowserComponent::Impl& owner;
DelegateConnector delegateConnector;
bool allowAccessToEnclosingDirectory = false;
LastFocusChange lastFocusChange;
// Default false → keyDown forwards to the host responder chain. Flipped
// to true by the JS layer via __juceSetEditableFocusActive when an
// editable DOM element gains focus, so character input reaches the DOM.
std::atomic<bool> editableFocusActive { false };
ObjCObjectHandle<WKWebView*> webView;
ObjCObjectHandle<id> webViewDelegate;
String lastRequestedUrl, lastLoadedUrl;
Expand Down
98 changes: 88 additions & 10 deletions modules/juce_gui_extra/native/juce_WebBrowserComponent_windows.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,11 @@ class WebBrowserComponent::Impl::Platform::WebView2 final : public WebBrowserCom
webViewController->MoveFocus (moveFocusReason);
}

void setEditableFocusActive (bool active) override
{
editableFocusActive.store (active, std::memory_order_relaxed);
}

~WebView2() override
{
if (webView2ConstructionHelper.webView2BeingCreated == this)
Expand Down Expand Up @@ -961,6 +966,27 @@ class WebBrowserComponent::Impl::Platform::WebView2 final : public WebBrowserCom

return S_OK;
}).Get(), &moveFocusRequestedToken);

// AcceleratorKeyPressed fires for accelerator keys (Tab, Esc,
// F-keys, modifier combos) before WebView2 processes them. Marking
// put_Handled(FALSE) yields the key to the host's window proc so
// the plugin host (DAW) receives Cmd/Ctrl shortcuts and transport
// controls — the default behaviour users expect. When the JS layer
// has reported editable DOM focus we set put_Handled(TRUE) so the
// WebView keeps the key for in-DOM editing (e.g. Tab to indent,
// Esc to dismiss a popover). This complements
// ICoreWebView2ControllerOptions2::AllowHostInputProcessing, which
// covers the character keys (spacebar, QWERTY) that
// AcceleratorKeyPressed does not fire for.
webViewController->add_AcceleratorKeyPressed (
Callback<ICoreWebView2AcceleratorKeyPressedEventHandler> (
[this] (ICoreWebView2Controller*, ICoreWebView2AcceleratorKeyPressedEventArgs* args) -> HRESULT
{
if (args != nullptr)
args->put_Handled (editableFocusActive.load (std::memory_order_relaxed) ? TRUE : FALSE);

return S_OK;
}).Get(), &acceleratorKeyPressedToken);
}
}

Expand Down Expand Up @@ -994,6 +1020,9 @@ class WebBrowserComponent::Impl::Platform::WebView2 final : public WebBrowserCom
{
if (moveFocusRequestedToken.value != 0)
webViewController->remove_MoveFocusRequested (moveFocusRequestedToken);

if (acceleratorKeyPressedToken.value != 0)
webViewController->remove_AcceleratorKeyPressed (acceleratorKeyPressedToken);
}
}

Expand Down Expand Up @@ -1057,8 +1086,8 @@ class WebBrowserComponent::Impl::Platform::WebView2 final : public WebBrowserCom
webView2ConstructionHelper.viewsWaitingForCreation.erase (this);
webView2ConstructionHelper.webView2BeingCreated = this;

webViewHandle.environment->CreateCoreWebView2Controller ((HWND) peer->getNativeHandle(),
Callback<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler> (
const auto hostHwnd = (HWND) peer->getNativeHandle();
auto completionHandler = Callback<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler> (
[weakThis = WeakReference<WebView2> { this }] (HRESULT, ICoreWebView2Controller* controller) -> HRESULT
{
if (weakThis != nullptr)
Expand Down Expand Up @@ -1137,7 +1166,49 @@ class WebBrowserComponent::Impl::Platform::WebView2 final : public WebBrowserCom
}

return S_OK;
}).Get());
});

// Prefer CreateCoreWebView2ControllerWithOptions so we can enable
// AllowHostInputProcessing — that routes keyboard/mouse input
// through the host's window proc, letting the plugin host (DAW)
// see character keys (spacebar, QWERTY) that WebView2 would
// otherwise consume entirely. AcceleratorKeyPressed handles the
// accelerator-key subset for runtimes/SDKs that don't expose the
// option. Falling back to the plain Create function keeps the
// backend working when either the environment (older runtime) or
// the controller options (older SDK) interface is unavailable.
const auto createWithHostInputProcessing = [&]() -> HRESULT
{
ComSmartPtr<ICoreWebView2Environment10> environment10;
webViewHandle.environment.QueryInterface (environment10);

if (environment10 == nullptr)
return E_NOINTERFACE;

ComSmartPtr<ICoreWebView2ControllerOptions> controllerOptions;

if (environment10->CreateCoreWebView2ControllerOptions (controllerOptions.resetAndGetPointerAddress()) != S_OK
|| controllerOptions == nullptr)
{
return E_NOINTERFACE;
}

ComSmartPtr<ICoreWebView2ControllerOptions2> controllerOptions2;
controllerOptions.QueryInterface (controllerOptions2);

if (controllerOptions2 == nullptr)
return E_NOINTERFACE;

if (controllerOptions2->put_AllowHostInputProcessing (TRUE) != S_OK)
return E_FAIL;

return environment10->CreateCoreWebView2ControllerWithOptions (hostHwnd,
controllerOptions.get(),
completionHandler.Get());
}();

if (! SUCCEEDED (createWithHostInputProcessing))
webViewHandle.environment->CreateCoreWebView2Controller (hostHwnd, completionHandler.Get());
}
}

Expand Down Expand Up @@ -1206,16 +1277,23 @@ class WebBrowserComponent::Impl::Platform::WebView2 final : public WebBrowserCom
ComSmartPtr<ICoreWebView2Controller> webViewController;
ComSmartPtr<ICoreWebView2> webView;

EventRegistrationToken navigationStartingToken { 0 },
newWindowRequestedToken { 0 },
windowCloseRequestedToken { 0 },
navigationCompletedToken { 0 },
webResourceRequestedToken { 0 },
moveFocusRequestedToken { 0 },
webMessageReceivedToken { 0 };
EventRegistrationToken navigationStartingToken { 0 },
newWindowRequestedToken { 0 },
windowCloseRequestedToken { 0 },
navigationCompletedToken { 0 },
webResourceRequestedToken { 0 },
moveFocusRequestedToken { 0 },
webMessageReceivedToken { 0 },
acceleratorKeyPressedToken { 0 };

bool inMoveFocusRequested = false;

// Default false → AcceleratorKeyPressed releases keys to the host's window
// proc so the DAW receives shortcuts. Flipped to true by the JS layer via
// __juceSetEditableFocusActive whenever an editable DOM element gains
// focus, so the WebView retains keys for in-DOM editing.
std::atomic<bool> editableFocusActive { false };

struct URLRequest
{
String url;
Expand Down
Loading