List icon Contents

Context menu

This guide describes how to use ShowContextMenuCallback and display a custom context menu.

Overview 

You can programmatically intercept and handle any user attempt to open a context menu in BrowserView. This allows you to implement a custom context-menu UI or select the appropriate menu item directly from code.

Custom context menu in SWT

A custom context menu for BrowserView in SWT.

Use the ShowContextMenuCallback callback to handle the attempt to open a context menu:

Java
Kotlin

browser.set(ShowContextMenuCallback.class, (params, tell) -> {
    tell.close();
    // Or:
    // tell.select(...);
});

browser.register(ShowContextMenuCallback { params, tell ->
    tell.close()
    // Or:
    // tell.select(...)
})

The tell.close() and tell.select() methods instruct Chromium to “close” the context menu, either silently or by selecting a specific menu item. You must call one of these methods when you are ready to “close” the context menu. They may be invoked from any thread and even after the callback has returned.

This callback is not called if the web page disabled the context menu by cancelling the contextmenu JavaScript event.

JxBrowser does not support displaying the native Chromium context menu.

Context menu parameters 

The parameters of ShowContextMenuCallback contain all the information needed to choose the right menu item in code or implement your own context menu using any supported UI toolkit.

Page and frame 

The context menu parameters provide information about the page and the specific frame where the right-click occurred. For example, you can use this information to create a “Copy page URL” menu item:

Java
Kotlin

// The URL of the page.
var pageUrl = params.pageUrl();
// The URL of the frame.
var frameUrl = params.frameUrl();
// The charset used in the frame.
var frameCharset = params.frameCharset();

// 页面 URL。
val pageUrl = params.pageUrl()
// 框架的 URL。
val frameUrl = params.frameUrl()
// 框架所使用的字符集。
val frameCharset = params.frameCharset()

Location 

To show the context menu in the right position, use the location() method that returns the location of the context menu relative to BrowserView:

Java
Kotlin

var location = params.location();
var x = location.x();
var y = location.y();

val location = params.location()
val x = location.x()
val y = location.y()

You can also retrieve the menu’s location relative to the frame:

Java
Kotlin

var locationInFrame = params.locationInFrame();

val locationInFrame = params.locationInFrame()

Finding element under cursor 

You can determine which DOM node the user right-clicked when opening the context menu. To do this, inspect the point where the click occurred:

Java
Kotlin

var locationInFrame = params.locationInFrame();
params.frame().ifPresent(frame -> {
    var inspection = frame.inspect(locationInFrame);
    inspection.node().ifPresent(node -> {
    ...
    });
});

val locationInFrame = params.locationInFrame()
params.frame().ifPresent { frame ->
    val inspection = frame.inspect(
        locationInFrame!!
    )
    inspection.node().ifPresent { node ->
    ...
    }
}

Context menu content 

When the context menu is triggered, Chromium indicates which categories of menu items apply to the part of the page where the click occurred:

Java
Kotlin

var contentTypes = params.contentType();

val contentTypes = params.contentType()

When the context menu is triggered on a media element, Chromium reports the exact media type. The following types are currently available: NONE, IMAGE, VIDEO, AUDIO, CANVAS, FILE, PLUGIN.

Java
Kotlin

var mediaType = params.mediaType();

val mediaType = params.mediaType()

If the user right-clicked a link, Chromium provides the link text and URL

Java
Kotlin

var linkUrl = params.linkUrl();
var linkText = params.linkText();

val linkUrl = params.linkUrl()
val linkText = params.linkText()

For <audio>, <video>, and <img> elements, Chromium provides the value of the src attribute:

Java
Kotlin

var src = params.srcUrl();

val src = params.srcUrl()

If the user right-clicked a piece of selected text, Chromium provides the selected text:

Java
Kotlin

var selectedText = params.selectedText();

val selectedText = params.selectedText()

Spell checking 

When the user right-clicks a misspelled word, the context menu parameters will include spell-checking information that lets you replace the word with a suggestion from Chromium, and add the word to a custom dictionary.

Java
Kotlin

var spellCheckMenu = params.spellCheckMenu();
var word = spellCheckMenu.misspelledWord();
if (!word.isEmpty()) {
    // Get the list of suggested corrections:
    List<String> suggestions = spellCheckMenu.dictionarySuggestions();

    // Replace the misspelled word with a correction or any other string:
    browser.replaceMisspelledWord(theRightSuggestion);

    // Add the misspelled word to the dictionary.
    browser.profile().spellChecker().customDictionary().add(word);
}

val spellCheckMenu = params.spellCheckMenu()
val word = spellCheckMenu.misspelledWord()
if (word.isNotEmpty()) {
    // 获取建议的更正列表:
    val suggestions = spellCheckMenu.dictionarySuggestions()

    // 将拼写错误的单词替换为某个更正项或任意其他字符串:
    browser.replaceMisspelledWord(theRightSuggestion)

    // 将拼写错误的单词添加到词典。
    browser.profile().spellChecker().customDictionary().add(word)
}

Additionally, Chromium provides a localized “Add to dictionary” label that you can use in your application:

Java
Kotlin

var label = params.spellCheckMenu().addToDictionaryMenuItemText();

val label = params.spellCheckMenu().addToDictionaryMenuItemText()

Extension menu items 

If Chrome extensions are installed, the library may provide additional context menu items created by those extensions. Access them using extensionMenuItems():

Java
Kotlin

List<ContextMenuItem> items = params.extensionMenuItems();

for (var item : params.extensionMenuItems()) {
    var text = item.text();
    var enabled = item.isEnabled();
    switch (item.type()) {
        case ITEM -> {
            // Your logic for a standard item.
        }
        case CHECKABLE_ITEM -> {
            var checked = item.isChecked();
            // Your logic for a check-box item.
        }
        case SUB_MENU -> {
            var children = item.items();
            // Your logic for a sub-menu and its children.
        }
        case SEPARATOR -> {
            // A separator.
        }
    }
}

for (item in params.extensionMenuItems()) {
    val text = item.text()
    val enabled = item.isEnabled
    when (item.type()) {
        ContextMenuItemType.ITEM -> {
            // 针对标准项的逻辑。
        }

        ContextMenuItemType.CHECKABLE_ITEM -> {
            // 针对复选框项的逻辑。
            val checked = item.isChecked
        }

        ContextMenuItemType.SUB_MENU -> {
            // 针对子菜单及其子项的逻辑。
            val children = item.items()
        }

        ContextMenuItemType.SEPARATOR -> {
            // 分隔符。
        }

        else -> {}
    }
}

Use tell.select() to tell Chromium to select one of the extension items:

Java
Kotlin

browser.set(ShowContextMenuCallback.class, (params, tell) -> {
    for (var item : params.extensionMenuItems()) {
        if (item.text().equals("Some action")) {
            tell.select(item);
            return;
        }
    }
    tell.close();
});

browser.register(ShowContextMenuCallback { params, tell ->
    for (item in params.extensionMenuItems()) {
        if (item.text() == "Some action") {
            tell.select(item)
            return@ShowContextMenuCallback
        }
    }
    tell.close()
});

Implementation examples 

In this section, we provide simple implementations of ShowContextMenuCallback for Swing, JavaFX, and SWT.

Swing 

This is a simple implementation of ShowContextMenuCallback, that displays the context menu in Swing.

In the HARDWARE_ACCELERATED mode, call JPopupMenu.setDefaultLightWeightPopupEnabled(false) before displaying JPopupMenu. Otherwise, the menu will not be visible. For more details, read about the limitations of the hardware-accelerated mode.

Custom context menu in Swing

A custom context menu for BrowserView in Swing.

import com.teamdev.jxbrowser.browser.callback.ShowContextMenuCallback;
import com.teamdev.jxbrowser.menu.ContextMenuItem;
import com.teamdev.jxbrowser.view.swing.BrowserView;

import javax.swing.JCheckBoxMenuItem;
import javax.swing.JComponent;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JSeparator;
import javax.swing.SwingUtilities;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class ContextMenuCallback implements ShowContextMenuCallback {

    private final BrowserView browserView;

    public ContextMenuCallback(BrowserView browserView) {
        this.browserView = browserView;
    }

    @Override
    public void on(Params params, Action tell) {
        SwingUtilities.invokeLater(() -> {
            var copyPageUrl = createCopyUrlItem(params, tell);
            var spellCheckMenu = createSpellCheckMenu(params, tell);
            var extensionMenu = addExtensionMenu(params.extensionMenuItems(), tell);

            var contextMenu = new JPopupMenu();

            contextMenu.addPopupMenuListener(new PopupMenuListener() {
                @Override
                public void popupMenuCanceled(PopupMenuEvent e) {
                    tell.close();
                }

                @Override
                public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
                }

                @Override
                public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
                }
            });

            contextMenu.add(copyPageUrl);
            contextMenu.addSeparator();
            spellCheckMenu.forEach(contextMenu::add);
            contextMenu.addSeparator();
            extensionMenu.forEach(contextMenu::add);

            var location = params.location();
            contextMenu.show(browserView, location.x(), location.y());
        });
    }

    private List<JMenuItem> createSpellCheckMenu(Params params, Action tell) {
        var result = new ArrayList<JMenuItem>();
        var spellCheck = params.spellCheckMenu();
        var word = spellCheck.misspelledWord();
        if (word.isEmpty()) {
            return Collections.emptyList();
        }

        var addToDictionary = new JMenuItem(spellCheck.addToDictionaryMenuItemText());
        addToDictionary.addActionListener(e -> {
            var dictionary = params.browser().profile().spellChecker().customDictionary();
            dictionary.add(word);
            tell.close();
        });
        result.add(addToDictionary);

        var suggestionsMenu = new JMenu("Suggestions");
        spellCheck.dictionarySuggestions().forEach(suggestion -> {
            var item = new JMenuItem(suggestion);
            item.addActionListener(e -> {
                params.browser().replaceMisspelledWord(suggestion);
                tell.close();
            });
            suggestionsMenu.add(item);
        });
        result.add(suggestionsMenu);

        return result;
    }

    private JMenuItem createCopyUrlItem(Params params, Action tell) {
        var copyItem = new JMenuItem("Copy page URL");
        copyItem.addActionListener(e -> {
            var pageUrl = params.pageUrl();
            var clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
            clipboard.setContents(new StringSelection(pageUrl), null);
            tell.close();
        });
        return copyItem;
    }

    private List<JComponent> addExtensionMenu(List<ContextMenuItem> items, Action tell) {
        var result = new ArrayList<JComponent>();

        items.forEach(item -> {
            switch (item.type()) {
                case ITEM -> {
                    var menuItem = new JMenuItem(item.text());
                    menuItem.setEnabled(item.isEnabled());
                    menuItem.addActionListener(e -> {
                        tell.select(item);
                    });
                    result.add(menuItem);
                }
                case CHECKABLE_ITEM -> {
                    var menuItem = new JCheckBoxMenuItem(item.text());
                    menuItem.setEnabled(item.isEnabled());
                    menuItem.setSelected(item.isChecked());
                    menuItem.addActionListener(e -> {
                        tell.select(item);
                    });
                    result.add(menuItem);
                }
                case SUB_MENU -> {
                    var subMenu = new JMenu(item.text());
                    subMenu.setEnabled(item.isEnabled());
                    var subMenuItems = addExtensionMenu(item.items(), tell);
                    subMenuItems.forEach(subMenu::add);
                    result.add(subMenu);
                }
                case SEPARATOR -> {
                    result.add(new JSeparator());
                }
            }
        });

        return result;
    }
}

SWT 

This is a simple implementation of ShowContextMenuCallback, that displays the context menu in SWT. Due to the native nature of SWT widgets, the context menu is visible in both rendering modes.

Custom context menu in SWT

A custom context menu for BrowserView in SWT.

import com.teamdev.jxbrowser.browser.callback.ShowContextMenuCallback;
import com.teamdev.jxbrowser.menu.ContextMenuItem;
import com.teamdev.jxbrowser.view.swt.BrowserView;
import org.eclipse.swt.SWT;
import org.eclipse.swt.dnd.Clipboard;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;

import java.util.List;
import java.util.function.Consumer;

public class ContextMenuCallback implements ShowContextMenuCallback {

    private final BrowserView browserView;
    private Consumer<Action> menuAction;

    public ContextMenuCallback(BrowserView browserView) {
        this.browserView = browserView;
    }

    @Override
    public void on(Params params, Action tell) {
        // Close the menu by default.
        menuAction = Action::close;

        Display.getDefault().asyncExec(() -> {
            var contextMenu = new Menu(browserView);

            createCopyUrlItem(contextMenu, params, tell);
            new MenuItem(contextMenu, SWT.SEPARATOR);
            createSpellCheckMenu(contextMenu, params, tell);
            new MenuItem(contextMenu, SWT.SEPARATOR);
            addExtensionMenu(contextMenu, params.extensionMenuItems(), tell);

            contextMenu.addListener(SWT.Hide, e -> {
                // This event is called when the menu is hidden: both when a user selected
                // a menu item, or clicked outside to cancel the menu.
                //
                // Since this event always comes before SWT.Selection, we don't know yet
                // what menu item the user has selected. Therefore, we postpone the execution to
                // let SWT.Selection handlers execute first.
                e.display.asyncExec(() -> {
                    menuAction.accept(tell);
                });
            });

            var location = params.location();
            var point = browserView.toDisplay(location.x(), location.y());
            contextMenu.setLocation(point.x, point.y);
            contextMenu.setVisible(true);
        });
    }

    private void createSpellCheckMenu(Menu parent, Params params, Action tell) {
        var spellCheck = params.spellCheckMenu();
        var word = spellCheck.misspelledWord();
        if (word.isEmpty()) {
            return;
        }

        var addToDictionary = new MenuItem(parent, SWT.PUSH);
        addToDictionary.setText(spellCheck.addToDictionaryMenuItemText());
        addToDictionary.addListener(SWT.Selection, e -> {
            var dictionary = params.browser().profile().spellChecker().customDictionary();
            dictionary.add(word);
        });

        var suggestionsMenu = new Menu(parent);
        var suggestionsCascade = new MenuItem(parent, SWT.CASCADE);
        suggestionsCascade.setText("Suggestions");
        suggestionsCascade.setMenu(suggestionsMenu);

        spellCheck.dictionarySuggestions().forEach(suggestion -> {
            var item = new MenuItem(suggestionsMenu, SWT.PUSH);
            item.setText(suggestion);
            item.addListener(SWT.Selection, e -> {
                params.browser().replaceMisspelledWord(suggestion);
            });
        });
    }

    private void createCopyUrlItem(Menu parent, Params params, Action tell) {
        var copyItem = new MenuItem(parent, SWT.PUSH);
        copyItem.setText("Copy page URL");

        copyItem.addListener(SWT.Selection, e -> {
            var pageUrl = params.pageUrl();
            var clipboard = new Clipboard(Display.getDefault());
            var transfer = TextTransfer.getInstance();
            clipboard.setContents(new Object[]{pageUrl}, new Transfer[]{transfer});
            clipboard.dispose();
        });
    }

    private void addExtensionMenu(Menu parent, List<ContextMenuItem> items, Action tell) {
        items.forEach(item -> {
            switch (item.type()) {
                case ITEM -> {
                    var menuItem = new MenuItem(parent, SWT.PUSH);
                    menuItem.setText(item.text());
                    menuItem.setEnabled(item.isEnabled());
                    menuItem.addListener(SWT.Selection, e -> {
                        menuAction = action -> action.select(item);
                    });
                }
                case CHECKABLE_ITEM -> {
                    var menuItem = new MenuItem(parent, SWT.CHECK);
                    menuItem.setText(item.text());
                    menuItem.setEnabled(item.isEnabled());
                    menuItem.setSelection(item.isChecked());
                    menuItem.addListener(SWT.Selection, e -> {
                        menuAction = action -> action.select(item);
                    });
                }
                case SUB_MENU -> {
                    var subMenu = new Menu(parent);
                    var cascade = new MenuItem(parent, SWT.CASCADE);
                    cascade.setMenu(subMenu);
                    cascade.setText(item.text());
                    cascade.setEnabled(item.isEnabled());
                    addExtensionMenu(subMenu, item.items(), tell);
                }
                case SEPARATOR -> new MenuItem(parent, SWT.SEPARATOR);
            }
        });
    }
}

JavaFX 

This is a simple implementation of ShowContextMenuCallback, that displays the context menu in JavaFX.

The menu will not be visible in the HARDWARE_ACCELERATED mode. For details, read about the limitations of the hardware-accelerated mode.

Custom context menu in JavaFX

A custom context menu for BrowserView in JavaFX.

import com.teamdev.jxbrowser.browser.callback.ShowContextMenuCallback;
import com.teamdev.jxbrowser.menu.ContextMenuItem;
import com.teamdev.jxbrowser.view.javafx.BrowserView;
import javafx.application.Application;
import javafx.scene.control.CheckMenuItem;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.input.ClipboardContent;

import java.util.ArrayList;
import java.util.List;

import static java.util.Collections.emptyList;
import static javafx.application.Platform.runLater;
import static javafx.scene.input.Clipboard.getSystemClipboard;

public class ContextMenuCallback implements ShowContextMenuCallback {

    private final BrowserView browserView;
    private boolean menuAlreadyClosed;

    public ContextMenuCallback(BrowserView browserView) {
        this.browserView = browserView;
    }

    @Override
    public void on(Params params, Action tell) {
        menuAlreadyClosed = false;

        runLater(() -> {
            var copyPageUrl = createCopyUrlItem(params, tell);
            var spellCheckMenu = createSpellCheckMenu(params, tell);
            var extensionMenu = addExtensionMenu(params.extensionMenuItems(), tell);

            var contextMenu = new ContextMenu();
            contextMenu.setAutoHide(true);
            contextMenu.setOnHiding(e -> {
                if (!menuAlreadyClosed) {
                    tell.close();
                }
            });

            var menuItems = contextMenu.getItems();
            menuItems.add(copyPageUrl);
            menuItems.add(new SeparatorMenuItem());
            menuItems.addAll(spellCheckMenu);
            menuItems.add(new SeparatorMenuItem());
            menuItems.addAll(extensionMenu);

            var location = params.location();
            var screenPoint = browserView.localToScreen(location.x(), location.y());
            contextMenu.show(browserView, screenPoint.getX(), screenPoint.getY());
        });
    }

    private List<MenuItem> createSpellCheckMenu(Params params, Action tell) {
        var result = new ArrayList<MenuItem>();
        var spellCheck = params.spellCheckMenu();
        var word = spellCheck.misspelledWord();
        if (word.isEmpty()) {
            return emptyList();
        }

        var addToDictionary = new MenuItem(spellCheck.addToDictionaryMenuItemText());
        addToDictionary.setOnAction(e -> {
            var dictionary = params.browser().profile().spellChecker().customDictionary();
            dictionary.add(word);
            menuAlreadyClosed = true;
            tell.close();
        });
        result.add(addToDictionary);

        var suggestionsMenu = new Menu("Suggestions");
        spellCheck.dictionarySuggestions().forEach(suggestion -> {
            var item = new MenuItem(suggestion);
            item.setOnAction(e -> {
                params.browser().replaceMisspelledWord(suggestion);
                menuAlreadyClosed = true;
                tell.close();
            });
            suggestionsMenu.getItems().add(item);
        });
        result.add(suggestionsMenu);
        return result;
    }

    private MenuItem createCopyUrlItem(Params params, Action tell) {
        var copyItem = new MenuItem("Copy page URL");
        copyItem.setOnAction(e -> {
            var pageUrl = params.pageUrl();
            var content = new ClipboardContent();
            content.putString(pageUrl);
            getSystemClipboard().setContent(content);
            menuAlreadyClosed = true;
            tell.close();
        });
        return copyItem;
    }

    private List<MenuItem> addExtensionMenu(List<ContextMenuItem> items, Action tell) {
        var result = new ArrayList<MenuItem>();
        items.forEach(item -> {
            switch (item.type()) {
                case ITEM -> {
                    var menuItem = new MenuItem(item.text());
                    menuItem.setDisable(!item.isEnabled());
                    menuItem.setOnAction(e -> {
                        menuAlreadyClosed = true;
                        tell.select(item);
                    });
                    result.add(menuItem);
                }
                case CHECKABLE_ITEM -> {
                    var menuItem = new CheckMenuItem(item.text());
                    menuItem.setDisable(!item.isEnabled());
                    menuItem.setSelected(item.isChecked());
                    menuItem.setOnAction(e -> {
                        menuAlreadyClosed = true;
                        tell.select(item);
                    });
                    result.add(menuItem);
                }
                case SUB_MENU -> {
                    var subMenu = new Menu(item.text());
                    subMenu.setDisable(!item.isEnabled());
                    var subMenuItems = addExtensionMenu(item.items(), tell);
                    subMenu.getItems().addAll(subMenuItems);
                    result.add(subMenu);
                }
                case SEPARATOR -> result.add(new SeparatorMenuItem());
            }
        });
        return result;
    }
}