package org.openautonomousconnection.luascript.fx; import org.openautonomousconnection.luascript.events.UiEventRegistry; import org.openautonomousconnection.luascript.hosts.EventHost; import org.openautonomousconnection.luascript.runtime.LuaEventRouter; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.events.EventListener; import org.w3c.dom.events.EventTarget; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * EventHost implementation for JavaFX WebView using W3C DOM EventTarget listeners. * *

No JavaScript required. Events are forwarded to Lua via {@link LuaEventRouter}.

*/ public final class FxEventHost implements EventHost { private static final String GLOBAL_TARGET_ID = "__global__"; private final FxDomHost dom; private volatile LuaEventRouter router; private final ConcurrentHashMap elementListeners = new ConcurrentHashMap<>(); private final ConcurrentHashMap globalListeners = new ConcurrentHashMap<>(); private final ExecutorService dispatchExecutor = Executors.newSingleThreadExecutor(r -> { Thread t = new Thread(r, "oac-fx-event-dispatch"); t.setDaemon(true); return t; }); /** * Creates a new FxEventHost. * * @param dom fx dom host */ public FxEventHost(FxDomHost dom) { this.dom = Objects.requireNonNull(dom, "dom"); } /** * Sets the active Lua router for this host. * * @param router lua router */ public void setRouter(LuaEventRouter router) { this.router = Objects.requireNonNull(router, "router"); } @Override public void addListener(String elementId, String eventName) { Objects.requireNonNull(elementId, "elementId"); Objects.requireNonNull(eventName, "eventName"); String evt = UiEventRegistry.normalize(eventName); String key = elementId + "\n" + evt; elementListeners.computeIfAbsent(key, k -> { EventListener listener = ev -> { Map payload = FxEventPayloadExtractor.extract(ev); dispatchExecutor.execute(() -> emitSafely(elementId, evt, payload)); }; FxThreadBridge.runAndWait(() -> { Element el = dom.byId(elementId); ((EventTarget) el).addEventListener(evt, listener, false); }); return listener; }); } @Override public void removeListener(String elementId, String eventName) { Objects.requireNonNull(elementId, "elementId"); Objects.requireNonNull(eventName, "eventName"); String evt = UiEventRegistry.normalize(eventName); String key = elementId + "\n" + evt; EventListener listener = elementListeners.remove(key); if (listener == null) return; FxThreadBridge.runAndWait(() -> { Element el = dom.byId(elementId); ((EventTarget) el).removeEventListener(evt, listener, false); }); } @Override public void addGlobalListener(String eventName) { Objects.requireNonNull(eventName, "eventName"); String evt = UiEventRegistry.normalize(eventName); globalListeners.computeIfAbsent(evt, k -> { EventListener listener = ev -> { Map payload = FxEventPayloadExtractor.extract(ev); dispatchExecutor.execute(() -> emitSafely(GLOBAL_TARGET_ID, evt, payload)); }; FxThreadBridge.runAndWait(() -> { Document doc = dom.requireDocument(); ((EventTarget) doc).addEventListener(evt, listener, false); }); return listener; }); } @Override public void removeGlobalListener(String eventName) { Objects.requireNonNull(eventName, "eventName"); String evt = UiEventRegistry.normalize(eventName); EventListener listener = globalListeners.remove(evt); if (listener == null) return; FxThreadBridge.runAndWait(() -> { Document doc = dom.requireDocument(); ((EventTarget) doc).removeEventListener(evt, listener, false); }); } private void emitSafely(String elementId, String eventName, Map payload) { try { LuaEventRouter activeRouter = router; if (activeRouter == null) { System.err.println("[oac.event] Dropping '" + eventName + "' for '" + elementId + "': router not initialized"); return; } activeRouter.emit(elementId, eventName, payload); } catch (RuntimeException ex) { System.err.println("[oac.event] Failed to dispatch '" + eventName + "' for '" + elementId + "': " + ex.getMessage()); ex.printStackTrace(System.err); } } }