Customized context menu on javafx webview/webengine


Question

How may I have a customized context menu for whole entry of the document in WebEngine javafx?
Something like this

+------------+
|Reload      |
|Save page   |
|Hide Images |
+------------+

I like to invoke and show this context popup for whole document entry(same for every node). Thanks.

1
6
11/20/2014 7:01:31 PM

Accepted Answer

I don't see a way to interact with the default context menu. However, it's not hard to disable it and implement your own.

Disable the default context menu with

webView.setContextMenuEnabled();

Then create your own context menu, and register a mouse listener with the web view to show it on right click:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextField;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.BorderPane;
import javafx.scene.web.WebView;
import javafx.stage.Stage;


public class WebViewContextMenuTest extends Application {

    private final String START_URL = 
            "http://stackoverflow.com/questions/27047447/customized-context-menu-on-javafx-webview-webengine/27047830#27047830";

    @Override
    public void start(Stage primaryStage) {
        TextField locationField = new TextField(START_URL);
        WebView webView = new WebView();
        webView.getEngine().load(START_URL);

        webView.setContextMenuEnabled(false);
        createContextMenu(webView);

        locationField.setOnAction(e -> {
            webView.getEngine().load(getUrl(locationField.getText()));
        });
        BorderPane root = new BorderPane(webView, locationField, null, null, null);
        primaryStage.setScene(new Scene(root, 800, 600));
        primaryStage.show();

    }

    private void createContextMenu(WebView webView) {
        ContextMenu contextMenu = new ContextMenu();
        MenuItem reload = new MenuItem("Reload");
        reload.setOnAction(e -> webView.getEngine().reload());
        MenuItem savePage = new MenuItem("Save Page");
        savePage.setOnAction(e -> System.out.println("Save page..."));
        MenuItem hideImages = new MenuItem("Hide Images");
        hideImages.setOnAction(e -> System.out.println("Hide Images..."));
        contextMenu.getItems().addAll(reload, savePage, hideImages);

        webView.setOnMousePressed(e -> {
            if (e.getButton() == MouseButton.SECONDARY) {
                contextMenu.show(webView, e.getScreenX(), e.getScreenY());
            } else {
                contextMenu.hide();
            }
        });
    }

    private String getUrl(String text) {
        if (text.indexOf("://")==-1) {
            return "http://" + text ;
        } else {
            return text ;
        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}
12
11/20/2014 8:38:41 PM

There's no easy solution for this, since there's no public API, and a request is still unresolved.

The hacky solution uses some private API, so it's not very advisable since it could change without notice.

The ContextMenu shown when the user right clicks on the web page is in another window, so using some lookups we'll try to find it, then access to its content and then modify existing or add more MenuItems.

These are the private classes required:

import com.sun.javafx.scene.control.skin.ContextMenuContent;
import com.sun.javafx.scene.control.skin.ContextMenuContent.MenuItemContainer;

In our application, we listen for a context menu request:

@Override
public void start(Stage primaryStage) {
    WebView webView = new WebView();
    WebEngine webEngine = webView.getEngine();
    Scene scene = new Scene(webView);

    primaryStage.setScene(scene);
    primaryStage.show();

    webView.setOnContextMenuRequested(new EventHandler<ContextMenuEvent>() {

        @Override
        public void handle(ContextMenuEvent e) {
            getPopupWindow();
        }
    });

}

where getPopupWindow() will:

  • Look for the new window being instance of ContextMenu
  • With lookup find the CSS selector context-menu. This is a node having as its only child a ContextMenuContent instance.
  • This object has an VBox as a container for all the items, which are MenuItem in an special container, MenuItemContainer.
  • We can access to any of the existing items, like reload page, go back, ... and customize them, modifying its text or adding a graphic.
  • We can add our custom items to this box, providing our own actions.

Customize the items as you need to:

private PopupWindow getPopupWindow() {
    @SuppressWarnings("deprecation") 
    final Iterator<Window> windows = Window.impl_getWindows();

    while (windows.hasNext()) {
        final Window window = windows.next();

        if (window instanceof ContextMenu) {
            if(window.getScene()!=null && window.getScene().getRoot()!=null){ 
                Parent root = window.getScene().getRoot();

                // access to context menu content
                if(root.getChildrenUnmodifiable().size()>0){
                    Node popup = root.getChildrenUnmodifiable().get(0);
                    if(popup.lookup(".context-menu")!=null){
                        Node bridge = popup.lookup(".context-menu");
                        ContextMenuContent cmc= (ContextMenuContent)((Parent)bridge).getChildrenUnmodifiable().get(0);

                        VBox itemsContainer = cmc.getItemsContainer();
                        for(Node n: itemsContainer.getChildren()){
                            MenuItemContainer item=(MenuItemContainer)n;
                            // customize text:
                            item.getItem().setText("My Custom: "+item.getItem().getText());
                            // customize graphic:
                            item.getItem().setGraphic(new ImageView(new Image(getClass().getResource("unlock24.png").toExternalForm())));
                        }
                        // remove some item:
                        // itemsContainer.getChildren().remove(0);

                        // adding new item:
                        MenuItem menuItem = new MenuItem("Save page");
                        menuItem.setOnAction(new EventHandler<ActionEvent>() {

                            @Override
                            public void handle(ActionEvent e) {
                                System.out.println("Save Page");
                            }
                        });
                        // add new item:
                        cmc.getItemsContainer().getChildren().add(cmc.new MenuItemContainer(menuItem));

                        return (PopupWindow)window;
                    }
                }
            }
            return null;
        }
    }
    return null;
}

This is how it looks like:

Custom ContextMenu


Licensed under: CC-BY-SA with attribution
Not affiliated with: Stack Overflow
Icon