JavaFX stop opening URL in WebView - open in browser instead

前端 未结 9 1229
無奈伤痛
無奈伤痛 2020-12-09 05:12

The embedded WebView browser I am using needs special handling for particular URLs, to open them in the native default browser instead of WebView. The actual browsing part w

相关标签:
9条回答
  • 2020-12-09 05:52

    There is another method for handling this.

    You can add an event listener to the DOM elements and intercept it that way.

    Example:

    NodeList nodeList = document.getElementsByTagName("a");
                for (int i = 0; i < nodeList.getLength(); i++)
                {
                    Node node= nodeList.item(i);
                    EventTarget eventTarget = (EventTarget) node;
                    eventTarget.addEventListener("click", new EventListener()
                    {
                        @Override
                        public void handleEvent(Event evt)
                        {
                            EventTarget target = evt.getCurrentTarget();
                            HTMLAnchorElement anchorElement = (HTMLAnchorElement) target;
                            String href = anchorElement.getHref();
                            //handle opening URL outside JavaFX WebView
                            System.out.println(href);
                            evt.preventDefault();
                        }
                    }, false);
                }
    

    Where document is the DOM document object. Make sure this is done after the document has finished loading.

    0 讨论(0)
  • 2020-12-09 05:55

    I found another solution:

    1. Register a CreatePopupHandler that returns a different WebEngine than the main one
    2. The main WebEngine sends the load-call to the secondary WebEngine
    3. Register a LocationChangeListener on the secondary WebEngine and catch the location change (including the address) and open it in our external browser
    4. Finally clean up the secondary WebEngine: stop loading & unload the URL

    I implemented it so the secondary WebEngine is initialized lazy. It may also be initialized in the constructor. Both has pros and contras.

    Note: This only triggers for Links which open as a popup. This usually is the case when an a-element has a target-attribute that is not "_self" or with JS: window.open(...).

    Here is the Magic ...

    Register it like this:

    engine.setCreatePopupHandler(new BrowserPopupHandler());
    

    The core class:

    public static class BrowserPopupHandler implements Callback<PopupFeatures, WebEngine>
    {
    
        private WebEngine popupHandlerEngine;
    
        public WebEngine call(PopupFeatures popupFeatures)
        {
            // by returning null here the action would be canceled
            // by returning a different WebEngine (than the main one where we register our listener) the load-call will go to that one
            // we return a different WebEngine here and register a location change listener on it (see blow)
            return getPopupHandler();
        }
    
        private WebEngine getPopupHandler()
        {
            if (popupHandlerEngine == null) // lazy init - so we only initialize it when needed ...
            {
                synchronized (this) // double checked synchronization
                {
                    if (popupHandlerEngine == null)
                    {
                        popupHandlerEngine = initEngine();
                    }
                }
            }
            return popupHandlerEngine;
        }
    
        private WebEngine initEngine()
        {
            final WebEngine popupHandlerEngine = new WebEngine();
    
            // this change listener will trigger when our secondary popupHandlerEngine starts to load the url ...
            popupHandlerEngine.locationProperty().addListener(new ChangeListener<String>()
            {
    
                public void changed(ObservableValue<? extends String> observable, String oldValue, String location)
                {
                    if (!location.isEmpty())
                    {
                        Platform.runLater(new Runnable()
                        {
    
                            public void run()
                            {
                                popupHandlerEngine.loadContent(""); // stop loading and unload the url
                                // -> does this internally: popupHandlerEngine.getLoadWorker().cancelAndReset();
                            }
    
                        });
    
                        try
                        {
                            // Open URL in Browser:
                            Desktop desktop = Desktop.getDesktop();
                            if (desktop.isSupported(Desktop.Action.BROWSE))
                            {
                                URI uri = new URI(location);
                                desktop.browse(uri);
                            }
                            else
                            {
                                System.out.println("Could not load URL: " + location);
                            }
                        }
                        catch (Exception e)
                        {
                            e.printStackTrace();
                        }
                    }
                }
    
            });
            return popupHandlerEngine;
        }
    
    }
    
    0 讨论(0)
  • 2020-12-09 05:57

    2017 version - still very hacky but much more concise:

    class AboutDialog extends Dialog {
        private final Controller controller;
        private final String url;
    
        AboutDialog() {
            super();
    
            this.controller = Controller.getInstance();
    
            this.setTitle(controller.getProperty("about_title"));
            this.setHeaderText(null);
    
            this.url = getClass().getResource("/about_dialog.html").toExternalForm();
    
            this.setWebView();
    
            this.getDialogPane().getButtonTypes().add(new ButtonType(controller.getProperty("close"), ButtonBar.ButtonData.CANCEL_CLOSE));
            this.getDialogPane().setPrefWidth(600);
        }
    
        private void setWebView() {
            final WebView webView = new WebView();
            webView.getEngine().load(url);
    
            webView.getEngine().locationProperty().addListener((observable, oldValue, newValue) -> {
                controller.getMainFxApp().getHostServices().showDocument(newValue);
                Platform.runLater(this::setWebView);
            });
    
            this.getDialogPane().setContent(webView);
        }
    }
    
    0 讨论(0)
  • 2020-12-09 05:59

    Sorry for digging out this old thread but I found another solution that I wanted to share with others who struggle with the same problem. I found a library that has a nice wrapper around the entire issue, see its docs at github.

    Edit: Oh, sry for not telling what the project does: The linked library contains a class that actually implemented all of the code discussed in this thread. The user can simply create a new instance of the WebViewHyperlinkListener-interface that gets automatically called when something (mouse enter, mouse quit, mouse click) happens with the link. Once the handler terminates, it returns a boolean: If the handler returns true, the WebView will not navigate to the linked web page. If the handler returns false, it will.

    0 讨论(0)
  • 2020-12-09 06:01

    I finally found a working solution that worked for me:

    import javafx.beans.value.ChangeListener;
    import javafx.beans.value.ObservableValue;
    import javafx.concurrent.Worker;
    import javafx.scene.web.WebView;
    import org.w3c.dom.Document;
    import org.w3c.dom.Node;
    import org.w3c.dom.NodeList;
    import org.w3c.dom.events.Event;
    import org.w3c.dom.events.EventListener;
    import org.w3c.dom.events.EventTarget;
    import org.w3c.dom.html.HTMLAnchorElement;
    
    import java.awt.*;
    import java.net.URI;
    
    public class HyperLinkRedirectListener implements ChangeListener<Worker.State>, EventListener
    {
        private static final String CLICK_EVENT = "click";
        private static final String ANCHOR_TAG = "a";
    
        private final WebView webView;
    
        public HyperLinkRedirectListener(WebView webView)
        {
            this.webView = webView;
        }
    
        @Override
        public void changed(ObservableValue<? extends Worker.State> observable, Worker.State oldValue, Worker.State newValue)
        {
            if (Worker.State.SUCCEEDED.equals(newValue))
            {
                Document document = webView.getEngine().getDocument();
                NodeList anchors = document.getElementsByTagName(ANCHOR_TAG);
                for (int i = 0; i < anchors.getLength(); i++)
                {
                    Node node = anchors.item(i);
                    EventTarget eventTarget = (EventTarget) node;
                    eventTarget.addEventListener(CLICK_EVENT, this, false);
                }
            }
        }
    
        @Override
        public void handleEvent(Event event)
        {
            HTMLAnchorElement anchorElement = (HTMLAnchorElement) event.getCurrentTarget();
            String href = anchorElement.getHref();
    
            if (Desktop.isDesktopSupported())
            {
                openLinkInSystemBrowser(href);
            } else
            {
                // LOGGER.warn("OS does not support desktop operations like browsing. Cannot open link '{}'.", href);
            }
    
            event.preventDefault();
        }
    
        private void openLinkInSystemBrowser(String url)
        {
            // LOGGER.debug("Opening link '{}' in default system browser.", url);
    
            try
            {
                URI uri = new URI(url);
                Desktop.getDesktop().browse(uri);
            } catch (Throwable e)
            {
                // LOGGER.error("Error on opening link '{}' in system browser.", url);
            }
        }
    }
    

    Usage:

    webView.getEngine().getLoadWorker().stateProperty().addListener(new HyperLinkRedirectListener(webView));
    
    0 讨论(0)
  • 2020-12-09 06:01

    When wrapping the d.browse call into a Runnable Object the runtime error never occured again. The strange thing was without that wrapping the ChangeListener was called a second time after some seconds with the same new location and this second call crashed the JVM.

    0 讨论(0)
提交回复
热议问题