diff --git a/pom.xml b/pom.xml index d403a63462..17e32eff64 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,6 @@ - diff --git a/webfx-kit/webfx-kit-javafxbase-emul/src/main/java/javafx/event/Event.java b/webfx-kit/webfx-kit-javafxbase-emul/src/main/java/javafx/event/Event.java index 72369865c3..2fb336dd70 100644 --- a/webfx-kit/webfx-kit-javafxbase-emul/src/main/java/javafx/event/Event.java +++ b/webfx-kit/webfx-kit-javafxbase-emul/src/main/java/javafx/event/Event.java @@ -205,8 +205,8 @@ public static void fireEvent(EventTarget eventTarget, Event event) { // back to the browser even if it has been consumed by JavaFX. This is the purpose of the propagateToPeerEvent field. private static Event propagateToPeerEvent; - // This setter can be called by the control (or behaviour) that consumed the event in JavaFX to request WebFX to - // not stop its propagation, but pass it to the peer. + // This setter is called by TextInputControl that consumed an event in JavaFX (to stop its propagation in JavaFX), + // but still requests WebFX to pass the event to the html peer to solve the case explained above. public static void setPropagateToPeerEvent(Event propagateToPeerEvent) { Event.propagateToPeerEvent = propagateToPeerEvent; } diff --git a/webfx-kit/webfx-kit-javafxcontrols-emul/src/main/java/com/sun/javafx/scene/control/behavior/TextInputControlBehavior.java b/webfx-kit/webfx-kit-javafxcontrols-emul/src/main/java/com/sun/javafx/scene/control/behavior/TextInputControlBehavior.java index c75c5107af..46424a8763 100644 --- a/webfx-kit/webfx-kit-javafxcontrols-emul/src/main/java/com/sun/javafx/scene/control/behavior/TextInputControlBehavior.java +++ b/webfx-kit/webfx-kit-javafxcontrols-emul/src/main/java/com/sun/javafx/scene/control/behavior/TextInputControlBehavior.java @@ -1,8 +1,6 @@ package com.sun.javafx.scene.control.behavior; -import javafx.event.Event; import javafx.scene.control.TextInputControl; -import javafx.scene.input.KeyEvent; import java.util.List; @@ -23,22 +21,6 @@ public abstract class TextInputControlBehavior exten */ public TextInputControlBehavior(T textInputControl, List bindings) { super(textInputControl, bindings); - // Although the key events are entirely managed by the peer, we consume them in JavaFX to not propagate these - // events to further JavaFX controls. - textInputControl.addEventHandler(KeyEvent.ANY, e -> { - if (textInputControl.isFocused()) { - // Exception is made for accelerators such as Enter or ESC, as they should be passed beyond this control - switch (e.getCode()) { - case ENTER: - case ESCAPE: - return; - } - // Otherwise, we stop the propagation in JavaFX - e.consume(); - // But we still ask WebFX to propagate them to the peer. - Event.setPropagateToPeerEvent(e); // See WebFX comments on Event class for more explanation. - } - }); } } \ No newline at end of file diff --git a/webfx-kit/webfx-kit-javafxcontrols-emul/src/main/java/javafx/scene/control/TextInputControl.java b/webfx-kit/webfx-kit-javafxcontrols-emul/src/main/java/javafx/scene/control/TextInputControl.java index 600ec2742e..461d34fcfd 100644 --- a/webfx-kit/webfx-kit-javafxcontrols-emul/src/main/java/javafx/scene/control/TextInputControl.java +++ b/webfx-kit/webfx-kit-javafxcontrols-emul/src/main/java/javafx/scene/control/TextInputControl.java @@ -5,6 +5,8 @@ import dev.webfx.kit.mapper.peers.javafxgraphics.markers.HasPromptTextProperty; import dev.webfx.kit.mapper.peers.javafxgraphics.markers.HasTextProperty; import javafx.beans.property.*; +import javafx.event.Event; +import javafx.scene.input.KeyEvent; import javafx.scene.text.Font; /** @@ -98,4 +100,23 @@ public interface SelectableTextInputControlPeer { void selectRange(int anchor, int caretPosition); } + + { + // Although the key events are entirely managed by the html peer, we consume them in JavaFX to not propagate + // these events to further JavaFX controls. + addEventHandler(KeyEvent.ANY, e -> { + if (isFocused()) { + // Exception is made for accelerators such as Enter or ESC, as they should be passed beyond this control + switch (e.getCode()) { + case ENTER: + case ESCAPE: + return; + } + // Otherwise, we stop the propagation in JavaFX + e.consume(); + // But we still ask WebFX to propagate them to the peer. + Event.setPropagateToPeerEvent(e); // See WebFX comments on Event class for more explanation. + } + }); + } } diff --git a/webfx-kit/webfx-kit-javafxgraphics-emul/src/main/java/javafx/scene/Parent.java b/webfx-kit/webfx-kit-javafxgraphics-emul/src/main/java/javafx/scene/Parent.java index 5493496396..26c491b6c6 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-emul/src/main/java/javafx/scene/Parent.java +++ b/webfx-kit/webfx-kit-javafxgraphics-emul/src/main/java/javafx/scene/Parent.java @@ -13,7 +13,6 @@ import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.scene.layout.LayoutFlags; -import javafx.scene.layout.PreferenceResizableNode; import java.util.ArrayList; import java.util.List; @@ -412,14 +411,6 @@ public double getBaselineOffset() { break; } performingLayout = true; - // Temporary webfx code to automatically bind the height to the preferred height - if (bindHeightToPrefHeight) { - PreferenceResizableNode resizableNode = (PreferenceResizableNode) this; - double prefHeight = resizableNode.getPrefHeight(); - if (prefHeight == -1) - prefHeight = resizableNode.prefHeight(resizableNode.getWidth()); - resizableNode.setHeight(prefHeight); - } layoutChildren(); // Intended fall-through case DIRTY_BRANCH: @@ -438,17 +429,6 @@ public double getBaselineOffset() { } } - // Temporary webfx field to automatically bind the height to the preferred height - private boolean bindHeightToPrefHeight; - - public void setBindHeightToPrefHeight(boolean bindHeightToPrefHeight) { - if (this instanceof PreferenceResizableNode) - this.bindHeightToPrefHeight = bindHeightToPrefHeight; - else - throw new IllegalStateException("Parent.setBindHeightToPrefHeight() can be called only if implementing PreferenceResizableNode"); - } - - /** * Invoked during the layout pass to layout the children in this * {@code Parent}. By default it will only set the size of managed, diff --git a/webfx-kit/webfx-kit-javafxgraphics-emul/src/main/java/javafx/scene/Scene.java b/webfx-kit/webfx-kit-javafxgraphics-emul/src/main/java/javafx/scene/Scene.java index 91ccadc09e..f8c069cc0e 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-emul/src/main/java/javafx/scene/Scene.java +++ b/webfx-kit/webfx-kit-javafxgraphics-emul/src/main/java/javafx/scene/Scene.java @@ -2935,39 +2935,97 @@ public String getName() { return onKeyTyped; } + // PENDING_DOC_REVIEW /** - * Sets the handler to use for this event type. There can only be one such - * handler specified at a time. This handler is guaranteed to be called - * first. This is used for registering the user-defined onFoo event - * handlers. + * Registers an event handler to this scene. The handler is called when the + * scene receives an {@code Event} of the specified type during the bubbling + * phase of event delivery. * * @param the specific event class of the handler - * @param eventType the event type to associate with the given eventHandler - * @param eventHandler the handler to register, or null to unregister - * @throws NullPointerException if the event type is null + * @param eventType the type of the events to receive by the handler + * @param eventHandler the handler to register + * @throws NullPointerException if the event type or handler is null */ - protected final void setEventHandler( + public final void addEventHandler( final EventType eventType, final EventHandler eventHandler) { getInternalEventDispatcher().getEventHandlerManager() - .setEventHandler(eventType, eventHandler); + .addEventHandler(eventType, eventHandler); } + // PENDING_DOC_REVIEW /** - * Registers an event handler to this scene. The handler is called when the - * scene receives an {@code Event} of the specified type during the bubbling - * phase of event delivery. + * Unregisters a previously registered event handler from this scene. One + * handler might have been registered for different event types, so the + * caller needs to specify the particular event type from which to + * unregister the handler. * * @param the specific event class of the handler - * @param eventType the type of the events to receive by the handler - * @param eventHandler the handler to register + * @param eventType the event type from which to unregister + * @param eventHandler the handler to unregister * @throws NullPointerException if the event type or handler is null */ - public final void addEventHandler( + public final void removeEventHandler( final EventType eventType, final EventHandler eventHandler) { getInternalEventDispatcher().getEventHandlerManager() - .addEventHandler(eventType, eventHandler); + .removeEventHandler(eventType, + eventHandler); + } + + // PENDING_DOC_REVIEW + /** + * Registers an event filter to this scene. The filter is called when the + * scene receives an {@code Event} of the specified type during the + * capturing phase of event delivery. + * + * @param the specific event class of the filter + * @param eventType the type of the events to receive by the filter + * @param eventFilter the filter to register + * @throws NullPointerException if the event type or filter is null + */ + public final void addEventFilter( + final EventType eventType, + final EventHandler eventFilter) { + getInternalEventDispatcher().getEventHandlerManager() + .addEventFilter(eventType, eventFilter); + } + + // PENDING_DOC_REVIEW + /** + * Unregisters a previously registered event filter from this scene. One + * filter might have been registered for different event types, so the + * caller needs to specify the particular event type from which to + * unregister the filter. + * + * @param the specific event class of the filter + * @param eventType the event type from which to unregister + * @param eventFilter the filter to unregister + * @throws NullPointerException if the event type or filter is null + */ + public final void removeEventFilter( + final EventType eventType, + final EventHandler eventFilter) { + getInternalEventDispatcher().getEventHandlerManager() + .removeEventFilter(eventType, eventFilter); + } + + /** + * Sets the handler to use for this event type. There can only be one such + * handler specified at a time. This handler is guaranteed to be called + * first. This is used for registering the user-defined onFoo event + * handlers. + * + * @param the specific event class of the handler + * @param eventType the event type to associate with the given eventHandler + * @param eventHandler the handler to register, or null to unregister + * @throws NullPointerException if the event type is null + */ + protected final void setEventHandler( + final EventType eventType, + final EventHandler eventHandler) { + getInternalEventDispatcher().getEventHandlerManager() + .setEventHandler(eventType, eventHandler); } /** diff --git a/webfx-kit/webfx-kit-javafxgraphics-fat-j2cl/pom.xml b/webfx-kit/webfx-kit-javafxgraphics-fat-j2cl/pom.xml index 102317fd38..05f1e5affe 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-fat-j2cl/pom.xml +++ b/webfx-kit/webfx-kit-javafxgraphics-fat-j2cl/pom.xml @@ -13,7 +13,6 @@ webfx-kit-javafxgraphics-fat-j2cl - diff --git a/webfx-kit/webfx-kit-javafxgraphics-fat-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css b/webfx-kit/webfx-kit-javafxgraphics-fat-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css index c10bf5d7d6..28553a638b 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-fat-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css +++ b/webfx-kit/webfx-kit-javafxgraphics-fat-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css @@ -15,9 +15,11 @@ body { opacity: 50%; } -/* When fill and stroke are not set by application code, we make them transparent by default like in JavaFX (otherwise - the browser will make them black). */ -fx-svgpath svg { - fill: none; - stroke: none; +/* Applying the default JavaFX behaviour for SVGPath */ +fx-svgpath svg path:not([fill]):not([stroke]) { /* if the application code didn't set neither fill nor stroke */ + fill: black; /* then the fill is black */ +} + +fx-svgpath svg path:not([fill])[stroke] { /* if the application code set the stroke but not the fill */ + fill: transparent; /* then the fill is transparent */ } \ No newline at end of file diff --git a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlBrowserRegionPeer.java b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlBrowserRegionPeer.java new file mode 100644 index 0000000000..ad1bb7aee6 --- /dev/null +++ b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlBrowserRegionPeer.java @@ -0,0 +1,22 @@ +package dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.html; + +import dev.webfx.kit.mapper.peers.javafxgraphics.HasNoChildrenPeers; +import dev.webfx.kit.mapper.peers.javafxgraphics.base.RegionPeerBase; +import dev.webfx.kit.mapper.peers.javafxgraphics.base.RegionPeerMixin; +import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.html.layoutmeasurable.HtmlLayoutMeasurable; +import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.util.HtmlUtil; +import javafx.scene.layout.Region; + +/** + * @author Bruno Salmon + */ +public final class HtmlBrowserRegionPeer + , NM extends RegionPeerMixin> + + extends HtmlRegionPeer implements HtmlLayoutMeasurable, HasNoChildrenPeers { + + public HtmlBrowserRegionPeer(String tagName) { + super((NB) new RegionPeerBase(), HtmlUtil.createElement(tagName)); + } + +} diff --git a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlLayoutPeer.java b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlJavaFXRegionPeer.java similarity index 90% rename from webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlLayoutPeer.java rename to webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlJavaFXRegionPeer.java index a38fff45c2..d6a5919449 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlLayoutPeer.java +++ b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlJavaFXRegionPeer.java @@ -9,13 +9,13 @@ /** * @author Bruno Salmon */ -public final class HtmlLayoutPeer +public final class HtmlJavaFXRegionPeer , NM extends RegionPeerMixin> extends HtmlRegionPeer implements NoWrapWhiteSpacePeer { - public HtmlLayoutPeer(String tag) { + public HtmlJavaFXRegionPeer(String tag) { super((NB) new RegionPeerBase(), HtmlUtil.createElement(tag)); } diff --git a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlRegionPeer.java b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlRegionPeer.java index 86ce39512a..8cf08828fb 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlRegionPeer.java +++ b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlRegionPeer.java @@ -1,5 +1,6 @@ package dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.html; +import dev.webfx.kit.mapper.peers.javafxgraphics.HasNoChildrenPeers; import dev.webfx.kit.mapper.peers.javafxgraphics.base.RegionPeerBase; import dev.webfx.kit.mapper.peers.javafxgraphics.base.RegionPeerMixin; import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.util.DomType; @@ -25,15 +26,22 @@ public abstract class HtmlRegionPeer extends HtmlNodePeer implements RegionPeerMixin { - private final HTMLElement fxBackground = createBehindElement("fx-background"); - private final HTMLElement fxBorder = createBehindElement("fx-border"); + private final HTMLElement fxBackground; + private final HTMLElement fxBorder; protected HtmlRegionPeer(NB base, HTMLElement element) { super(base, element); - fxBorder.style.boxSizing = "border-box"; - HTMLElement fxChildren = createBehindElement("fx-children"); - setChildrenContainer(fxChildren); - HtmlUtil.setChildren(element, fxBackground, fxBorder, fxChildren); + if (this instanceof HasNoChildrenPeers) { + fxBackground = element; + fxBorder = element; + } else { + fxBackground = createBehindElement("fx-background"); + fxBorder = createBehindElement("fx-border"); + fxBorder.style.boxSizing = "border-box"; + HTMLElement fxChildren = createBehindElement("fx-children"); + setChildrenContainer(fxChildren); + HtmlUtil.setChildren(element, fxBackground, fxBorder, fxChildren); + } } private static HTMLElement createBehindElement(String tag) { @@ -92,7 +100,7 @@ protected HTMLElement getBorderElement() { @Override protected HTMLElement getEffectElement() { - return fxBackground; + return getBackgroundElement(); } @Override diff --git a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css index c10bf5d7d6..28553a638b 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css +++ b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css @@ -15,9 +15,11 @@ body { opacity: 50%; } -/* When fill and stroke are not set by application code, we make them transparent by default like in JavaFX (otherwise - the browser will make them black). */ -fx-svgpath svg { - fill: none; - stroke: none; +/* Applying the default JavaFX behaviour for SVGPath */ +fx-svgpath svg path:not([fill]):not([stroke]) { /* if the application code didn't set neither fill nor stroke */ + fill: black; /* then the fill is black */ +} + +fx-svgpath svg path:not([fill])[stroke] { /* if the application code set the stroke but not the fill */ + fill: transparent; /* then the fill is transparent */ } \ No newline at end of file diff --git a/webfx-kit/webfx-kit-javafxgraphics-registry-gwt-j2cl/src/main/java/dev/webfx/kit/registry/javafxgraphics/JavaFxGraphicsRegistry.java b/webfx-kit/webfx-kit-javafxgraphics-registry-gwt-j2cl/src/main/java/dev/webfx/kit/registry/javafxgraphics/JavaFxGraphicsRegistry.java index 9b76bff2a0..d9f52b7c20 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-registry-gwt-j2cl/src/main/java/dev/webfx/kit/registry/javafxgraphics/JavaFxGraphicsRegistry.java +++ b/webfx-kit/webfx-kit-javafxgraphics-registry-gwt-j2cl/src/main/java/dev/webfx/kit/registry/javafxgraphics/JavaFxGraphicsRegistry.java @@ -1,11 +1,11 @@ package dev.webfx.kit.registry.javafxgraphics; +import dev.webfx.kit.mapper.peers.javafxgraphics.NodePeerFactoryRegistry; +import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.html.*; import javafx.scene.canvas.Canvas; import javafx.scene.image.ImageView; import javafx.scene.shape.*; import javafx.scene.text.Text; -import dev.webfx.kit.mapper.peers.javafxgraphics.NodePeerFactoryRegistry; -import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.html.*; import static dev.webfx.kit.mapper.peers.javafxgraphics.NodePeerFactoryRegistry.*; @@ -13,12 +13,12 @@ public class JavaFxGraphicsRegistry { public static void registerGroup() { - NodePeerFactoryRegistry.registerDefaultGroupPeerFactory(node -> { - String tag = requestedCustomTag(node); + NodePeerFactoryRegistry.registerDefaultGroupPeerFactory(group -> { + String tag = requestedCustomTag(group); if (tag == null) { - String classTag = classTag(node); + String classTag = classTag(group); // Hot registration for future cases - registerNodePeerFactory(node.getClass(), () -> new HtmlGroupPeer<>(classTag)); + registerNodePeerFactory(group.getClass(), () -> new HtmlGroupPeer<>(classTag)); tag = classTag; } return new HtmlGroupPeer<>(tag); @@ -26,15 +26,26 @@ public static void registerGroup() { } public static void registerRegion() { - NodePeerFactoryRegistry.registerDefaultRegionPeerFactory(node -> { - String tag = requestedCustomTag(node); - if (tag == null) { - String classTag = classTag(node); - // Hot registration for future cases - registerNodePeerFactory(node.getClass(), () -> new HtmlLayoutPeer<>(classTag)); - tag = classTag; + NodePeerFactoryRegistry.registerDefaultRegionPeerFactory(region -> { + String tag = requestedCustomTag(region); + if (tag != null) { // Ex: HTML "div" + // In this case, we return a HtmlBrowserRegionPeer instance, which is not supposed to have children + // managed by JavaFX, and its size (prefHeight(), getLayoutBounds(), etc...) will actually be computed + // by the browser and not JavaFX. Its main usage should be to act as a container for a third-party + // component (ex: a JS library like Google Map or any other) that will be integrated seamlessly in the + // browser page (without the need of an iFrame, i.e. a WebView in JavaFX). Usually the application code + // should then set an id on it - using setId() - and pass that id to the JS library. + return new HtmlBrowserRegionPeer<>(tag); } - return new HtmlLayoutPeer<>(tag); + // For other cases, we return a HtmlJavaFXRegionPeer instance, which will map all other classes extending + // the JavaFX region class, either from OpenJFX (such as Pane, VBox, etc...) or from the application code + // which can also extends Region or its subclasses. In this case, WebFX will map its children to the DOM. + // The layout of these children will be managed by the class itself (via the layoutChildren() method). + String classTag = classTag(region); + // Hot registration of the extended region class for future cases (will be a bit faster) + registerNodePeerFactory(region.getClass(), () -> new HtmlJavaFXRegionPeer<>(classTag)); + // But for this first call with this specific class, we instantiate the peer now. + return new HtmlJavaFXRegionPeer<>(classTag); }); } diff --git a/webfx-kit/webfx-kit-javafxweb-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxweb/spi/gwt/HtmlWebViewPeer.java b/webfx-kit/webfx-kit-javafxweb-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxweb/spi/gwt/HtmlWebViewPeer.java index 98d1b2a7f8..d7aede8acb 100644 --- a/webfx-kit/webfx-kit-javafxweb-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxweb/spi/gwt/HtmlWebViewPeer.java +++ b/webfx-kit/webfx-kit-javafxweb-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxweb/spi/gwt/HtmlWebViewPeer.java @@ -24,7 +24,11 @@ public class HtmlWebViewPeer extends HtmlNodePeer implements EmulWebViewPeerMixin, HasNoChildrenPeers { + private static final boolean DEBUG = true; + private final HTMLIFrameElement iFrame; + private Scheduled iFrameStateChecker; + private boolean onLoadAlreadyCalled; public HtmlWebViewPeer() { this((NB) new EmulWebViewPeerBase(), HtmlUtil.createElement("fx-webview")); @@ -40,13 +44,6 @@ public HtmlWebViewPeer(NB base, HTMLElement webViewElement) { // Allowing fullscreen and autoplay for videos iFrame.allow = "fullscreen; autoplay"; // new way HtmlUtil.setAttribute(iFrame, "allowfullscreen", "true"); // old way (must be executed second otherwise warning) - // Error management. Actually this listener is never called by the browser for an unknown reason. So if it's - // important for the application code to be aware of errors (ex: network errors), webfx provides an alternative - // iFrame loading mode called prefetch which is able to report such errors (see updateUrl()). - iFrame.onerror = e -> { - reportError(); - return null; - }; // Focus management. // 1) Detecting when the iFrame gained focus DomGlobal.window.addEventListener("blur", e -> { // when iFrame gained focus, the parent window lost focus @@ -89,14 +86,24 @@ public void updateHeight(Number height) { getElement().style.height = CSSProperties.HeightUnionType.of(toPx(height.doubleValue())); } + @Override + public void updatePageFill(Color pageFill) { + HtmlUtil.setStyleAttribute(iFrame, "background", HtmlPaints.toCssColor(pageFill)); + } + + @Override + public void updateLoadContent(String content) { + if (content != null) + iFrame.srcdoc = content; + } + @Override public void updateUrl(String url) { if (url == null) { if (!Strings.isEmpty(iFrame.src)) iFrame.src = ""; } else { - WorkerImpl worker = (WorkerImpl) getNode().getEngine().getLoadWorker(); - worker.setState(Worker.State.SCHEDULED); + resetWebEngineLoadWorkerState(); // WebFX proposes different loading mode for the iFrame: Object webfxLoadingMode = getNode().getProperties().get("webfx-loadingMode"); if ("prefetch".equals(webfxLoadingMode)) { // prefetch mode @@ -105,87 +112,151 @@ public void updateUrl(String url) { iFrame.contentWindow.fetch(url) .then(response -> { response.text().then(text -> { - worker.setState(Worker.State.RUNNING); + setWebEngineLoadWorkerState(Worker.State.RUNNING); updateLoadContent(text); - worker.setState(Worker.State.SUCCEEDED); + setWebEngineLoadWorkerState(Worker.State.SUCCEEDED); return null; }).catch_(error -> { - worker.setState(Worker.State.FAILED); + setWebEngineLoadWorkerState(Worker.State.FAILED); reportError(); return null; }); return null; }).catch_(error -> { - worker.setState(Worker.State.FAILED); + setWebEngineLoadWorkerState(Worker.State.FAILED); reportError(); return null; }); - } else if ("replace".equals(webfxLoadingMode)) { - // Using iframe location replace() instead of setting iFrame.src has the benefit to not interfere with - // the parent window history (see explanation in standard loading mode below). However, it doesn't work - // in all situations (ex: embed YouTube videos are not loading in this mode). - iFrame.contentWindow.location.replace(url); - } else { // Standard loading mode - Scheduled iFrameStateChecker = UiScheduler.schedulePeriodic(100, scheduled -> { - // Note: iFrame.contentDocument can be inaccessible (returns null) with cross-origin - Document contentDocument = iFrame.contentDocument; - if (contentDocument != null) { - String readyState = contentDocument.readyState.toLowerCase(); - //DomGlobal.console.log("iFrame readyState = " + readyState); - switch (readyState) { - case "uninitialized": - worker.setState(Worker.State.READY); - break; - case "loading": - worker.setState(Worker.State.SCHEDULED); - break; - case "loaded": - case "interactive": - worker.setState(Worker.State.RUNNING); - break; - case "complete": - DomGlobal.console.log("iFrame readyState = " + readyState); - worker.setState(Worker.State.SUCCEEDED); - scheduled.cancel(); - break; - } + } else { // Standard or replace mode + if (!"replace".equals(webfxLoadingMode)) { // Standard mode + iFrame.src = url; // Standard way to load an iFrame + // But it has 2 downsides (which is why webfx proposes alternative loading modes): + // 1) it doesn't report any network errors (iFrame.onerror not called). Issue addressed by the webfx + // "prefetch" mode + // 2) it has a side effect on the parent window navigation when the url changes several times. The first + // time has no side effect, but the subsequent times add an unwanted new entry in the parent window + // history, so the user needs to click twice to go back to the previous page, instead of a single click. + // Issue addressed by the webfx "replace" mode. + } else { // replace mode + // Using iframe location replace() instead of setting iFrame.src has the benefit to not interfere with + // the parent window history (see explanation in standard loading mode below). However, it doesn't work + // in all situations (ex: embed YouTube videos are not loading in this mode). + iFrame.contentWindow.location.replace(url); + } + // TODO: extend the following state management to the prefetch mode as well + // We also need to continue updating the web engine load worker state to report how the loading is going + // in case the application code is listening these states. + startIFrameStateChecker(); + iFrame.onload = e -> { // Note: if the iFrame is removed and then reinserted into the DOM, the browser + // will unload and then reload the iFrame. So onLoad may be called several times. + logDebug("iFrame onload is called"); + if (!onLoadAlreadyCalled) { // initial onLoad (not reload) + onLoadAlreadyCalled = true; + updateWebEngineLoadWorkerState(); + } else { // reload + logDebug("Detected iFrame reload (from onload)"); + onReloadDetected(); } - }); - iFrame.onload = e -> { - worker.setState(Worker.State.SUCCEEDED); - iFrameStateChecker.cancel(); }; + // Error management. Note: this listener is not called when network errors occur. If the application + // code wants to detect them, it can try the "prefetch" loading mode can be used instead. iFrame.onerror = e -> { - worker.setState(Worker.State.FAILED); - iFrameStateChecker.cancel(); + logDebug("iFrame onerror is called"); + setWebEngineLoadWorkerState(Worker.State.FAILED); + stopIFrameStateChecker(); + reportError(); return null; }; iFrame.onabort = e -> { - worker.setState(Worker.State.CANCELLED); - iFrameStateChecker.cancel(); + logDebug("iFrame onabort is called"); + setWebEngineLoadWorkerState(Worker.State.CANCELLED); + stopIFrameStateChecker(); return null; }; - iFrame.src = url; // Standard way to load an iFrame - - // But it has 2 downsides (which is why webfx proposes alternative loading modes): - // 1) it doesn't report any network errors (iFrame.onerror not called). Issue addressed by the webfx - // "prefetch" mode - // 2) it has a side effect on the parent window navigation when the url changes several times. The first - // time has no side effect, but the subsequent times add an unwanted new entry in the parent window - // history, so the user needs to click twice to go back to the previous page, instead of a single click. - // Issue addressed by the webfx "replace" mode. } } } - @Override - public void updateLoadContent(String content) { - if (content != null) - iFrame.srcdoc = content; + private WorkerImpl getWebEngineLoadWorker() { + return (WorkerImpl) getNode().getEngine().getLoadWorker(); } - @Override - public void updatePageFill(Color pageFill) { - HtmlUtil.setStyleAttribute(iFrame, "background", HtmlPaints.toCssColor(pageFill)); + private Worker.State getWebEngineLoadWorkerState() { + return getWebEngineLoadWorker().getState(); } + + private void setWebEngineLoadWorkerState(Worker.State state) { + getWebEngineLoadWorker().setState(state); + } + + private void resetWebEngineLoadWorkerState() { + // We move the web engine worker state to SCHEDULED, but to ensure the change of state can be eventually + // detected by the application code (because it may be already in that state), we move it first to READY + setWebEngineLoadWorkerState(Worker.State.READY); // this transition can be used by the application code to detect a reload + setWebEngineLoadWorkerState(Worker.State.SCHEDULED); + } + + private void onReloadDetected() { + resetWebEngineLoadWorkerState(); + startIFrameStateChecker(); // We ensure the state checker is running (will restart it if it's a reload) + } + + private void startIFrameStateChecker() { + if (iFrameStateChecker != null && iFrameStateChecker.isRunning()) + return; + iFrameStateChecker = UiScheduler.schedulePeriodic(100, scheduled -> { + updateWebEngineLoadWorkerState(); + if (getWebEngineLoadWorkerState() == Worker.State.SUCCEEDED) { + scheduled.cancel(); + } + }); + } + + private void stopIFrameStateChecker() { + if (iFrameStateChecker != null) + iFrameStateChecker.cancel(); + iFrameStateChecker = null; + } + + private String getIFrameDocumentReadyState() { + // Note: iFrame.contentDocument can be inaccessible (returns null) with cross-origin + Document contentDocument = iFrame.contentDocument; + return contentDocument == null ? null : contentDocument.readyState.toLowerCase(); + } + + private void updateWebEngineLoadWorkerState() { + String readyState = getIFrameDocumentReadyState(); + logDebug("iFrame readyState = " + readyState); + if (readyState == null) { // Can happen due to cross-origin restrictions, or at the end of iFrame state lifecycle + if (onLoadAlreadyCalled) { // We stop the checker at this point (otherwise it can run indefinitely on null state) + logDebug("Stopping iFrame state checker because readyState is null"); + stopIFrameStateChecker(); + } + } else { + switch (readyState) { + case "loading": + // Reload detection: + // At this stage, the state should be SCHEDULED, if not, this indicates that it's a reload + if (getWebEngineLoadWorkerState() != Worker.State.SCHEDULED) { + logDebug("Detected iFrame reload (from readyState)"); + onReloadDetected(); + } + break; + case "interactive": + setWebEngineLoadWorkerState(Worker.State.RUNNING); + break; + case "complete": + setWebEngineLoadWorkerState(Worker.State.SUCCEEDED); + break; + default: + DomGlobal.console.log("Unknown iFrame readyState: " + readyState); + } + } + } + + private static void logDebug(String message) { + if (DEBUG) + DomGlobal.console.log(message); + } + } diff --git a/webfx-kit/webfx-kit-javafxweb-registry-gwt-j2cl/src/main/java/dev/webfx/kit/registry/javafxweb/GwtJSObject.java b/webfx-kit/webfx-kit-javafxweb-registry-gwt-j2cl/src/main/java/dev/webfx/kit/registry/javafxweb/GwtJSObject.java index 10e64be6c0..8314050b27 100644 --- a/webfx-kit/webfx-kit-javafxweb-registry-gwt-j2cl/src/main/java/dev/webfx/kit/registry/javafxweb/GwtJSObject.java +++ b/webfx-kit/webfx-kit-javafxweb-registry-gwt-j2cl/src/main/java/dev/webfx/kit/registry/javafxweb/GwtJSObject.java @@ -12,8 +12,6 @@ */ final class GwtJSObject extends JSObject { - private static final Function EVAL_FUNCTION = (Function) Js.asPropertyMap(DomGlobal.window).get("eval"); - private final JsPropertyMap jsMap; public GwtJSObject(Object javaScriptObject) { @@ -73,11 +71,16 @@ public static Object call(Object javaScriptObject, String methodName, Object... public static Object call(JsPropertyMap jsMap, String methodName, Object... args) throws JSException { Function f = (Function) jsMap.get(methodName); + if (f == null) { + DomGlobal.console.log("Function '" + methodName + "' not found on following object:"); + DomGlobal.console.log(jsMap); + throw new IllegalArgumentException("Function '" + methodName + "' not found on object '" + jsMap + "'"); + } return callFunctionAndWrapResult(f, jsMap, args); } public static Object eval(Object javaScriptObject, String script) throws JSException { - return callFunctionAndWrapResult(EVAL_FUNCTION, javaScriptObject, script); + return call(Js.asPropertyMap(javaScriptObject), "eval", script); } private static Object callFunctionAndWrapResult(Function f, Object o, Object... args) { diff --git a/webfx-kit/webfx-kit-util/src/main/java/dev/webfx/kit/util/properties/FXProperties.java b/webfx-kit/webfx-kit-util/src/main/java/dev/webfx/kit/util/properties/FXProperties.java index 1af9292da4..626a0d1e3b 100644 --- a/webfx-kit/webfx-kit-util/src/main/java/dev/webfx/kit/util/properties/FXProperties.java +++ b/webfx-kit/webfx-kit-util/src/main/java/dev/webfx/kit/util/properties/FXProperties.java @@ -118,4 +118,39 @@ public void changed(ObservableValue observable, T oldValue, T newVa }); } } + + public static void bindConverted(Property pA, ObservableValue pB, Function baConverter) { + pA.bind(compute(pB, baConverter)); + } + + public static void bindConvertedBidirectional(Property pA, Property pB, Function baConverter, Function abConverter) { + boolean[] converting = { false }; + pB.addListener(new ChangeListener() { + @Override + public void changed(ObservableValue observable, B oldValue, B newValue) { + if (!converting[0]) { + converting[0] = true; + try { + pA.setValue(baConverter.apply(newValue)); + } finally { + converting[0] = false; + } + } + } + }); + pA.addListener(new ChangeListener() { + @Override + public void changed(ObservableValue observable, A oldValue, A newValue) { + if (!converting[0]) { + converting[0] = true; + try { + pB.setValue(abConverter.apply(newValue)); + } finally { + converting[0] = false; + } + } + } + }); + } + } \ No newline at end of file