目录

上下文菜单

本指南介绍如何使用 ShowContextMenuCallback 并显示自定义上下文菜单。

概述 

您可以以编程方式拦截并处理用户在 BrowserView 可以通过编程方式拦截并处理用户在 BrowserView 中打开上下文菜单的任何尝试。这允许您实现自定义的上下文菜单 UI,或者直接在代码中选择合适的菜单项。

SWT 中的自定义上下文菜单

SWT 中 BrowserView 的自定义上下文菜单。

使用 ShowContextMenuCallback 回调来处理打开上下文菜单的操作:

Java
Kotlin

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

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

tell.close()tell.select() 方法用于指示 Chromium “关闭”上下文菜单,可以是静默关闭,也可以是选择某个菜单项。你必须在准备“关闭”上下文菜单时调用其中之一。它们可以从任何线程调用,即使是在回调返回之后。

如果网页通过取消 contextmenu JavaScript 事件禁用了上下文菜单,则不会触发该回调。

JxBrowser 不支持显示 Chromium 的原生上下文菜单。

上下文菜单参数 

ShowContextMenuCallback参数包含了在代码中选择正确菜单项或实现自定义上下文菜单所需的全部信息。

页面与 frame 

上下文菜单参数提供了页面以及发生右键点击的具体 frame 的信息。例如,你可以用这些信息创建“Copy page URL(复制页面 URL)”菜单项:

Java
Kotlin

// 页面 URL。
var pageUrl = params.pageUrl();
// frame 的 URL。
var frameUrl = params.frameUrl();
// frame 使用的字符集。
var frameCharset = params.frameCharset();

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

位置 

要在正确的位置显示上下文菜单,请使用 location() 方法,该方法返回相对于 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()

您还可以获取相对于 frame 的位置:

Java
Kotlin

var locationInFrame = params.locationInFrame();

val locationInFrame = params.locationInFrame()

查找光标下的元素 

您可以确定用户右键点击的是哪个 DOM 节点,通过检查点击位置:

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 ->
    ...
    }
}

上下文菜单内容 

当触发上下文菜单时,Chromium 会指示当前点击位置适用的菜单类别:

Java
Kotlin

var contentTypes = params.contentType();

val contentTypes = params.contentType()

当在媒体元素上触发上下文菜单时,Chromium 会报告确切的媒体类型。当前可用的类型有:NONEIMAGEVIDEOAUDIOCANVASFILEPLUGIN

Java
Kotlin

var mediaType = params.mediaType();

val mediaType = params.mediaType()

如果用户右键单击了链接,Chromium 会提供链接文本和 URL:

Java
Kotlin

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

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

对于 <audio><video><img> 元素,Chromium 提供 src 属性的值:

Java
Kotlin

var src = params.srcUrl();

val src = params.srcUrl()

如果用户右键单击了一段选中的文本,Chromium 会提供选中的文本:

Java
Kotlin

var selectedText = params.selectedText();

val selectedText = params.selectedText()

拼写检查 

当用户右键单击拼写错误的单词时,上下文菜单参数将包含拼写检查信息,使您能够用 Chromium 的建议替换该单词,并将该单词添加到自定义词典中。

Java
Kotlin

var spellCheckMenu = params.spellCheckMenu();
var word = spellCheckMenu.misspelledWord();
if (!word.isEmpty()) {
    // 获取建议的更正列表:
    List<String> suggestions = spellCheckMenu.dictionarySuggestions();

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

    // 将拼写错误的单词添加到词典。
    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)
}

此外,Chromium 还提供了一个本地化的“添加到词典(Add to dictionary)”标签,您可以在应用程序中使用它:

Java
Kotlin

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

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

扩展菜单项 

如果安装了 Chrome 扩展,库可能会提供这些扩展创建的额外上下文菜单项。使用 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 -> {
            // 针对标准项的逻辑。
        }
        case CHECKABLE_ITEM -> {
            var checked = item.isChecked();
            // 针对复选框项的逻辑。
        }
        case SUB_MENU -> {
            var children = item.items();
            // 针对子菜单及其子项的逻辑。
        }
        case 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 -> {}
    }
}

使用 tell.select() 告诉 Chromium 选择其中一个扩展项:

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()
});

实现示例 

在本节中,我们为 Swing、JavaFX 和 SWT 提供 ShowContextMenuCallback 的简单实现。

Swing 

这是一个简单的 ShowContextMenuCallback 实现,它在 Swing 中显示上下文菜单。

HARDWARE_ACCELERATED 模式下,显示 JPopupMenu 之前请调用 JPopupMenu.setDefaultLightWeightPopupEnabled(false)。否则,菜单将不可见。有关更多详细信息,请阅读关于硬件加速模式的限制的内容。

Swing 中的自定义上下文菜单

Swing 中 BrowserView 的自定义上下文菜单。

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 

这是一个简单的 ShowContextMenuCallback实现,它在 SWT 中显示上下文菜单。由于 SWT 小部件的原生特性,上下文菜单在两种渲染模式下均可见。

SWT 中的自定义上下文菜单

SWT 中 BrowserView 的自定义上下文菜单。

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) {
        // 默认情况下关闭菜单。
        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 -> {
                // 当菜单隐藏时调用此事件:无论是用户选择了菜单项,还是点击外部取消菜单。
                //
                // 由于此事件总是在 SWT.Selection 之前发生,我们还不知道用户选择了哪个菜单项。
                // 因此,我们延迟执行,让 SWT.Selection 处理器先执行。
                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 

这是一个简单的 ShowContextMenuCallback 实现,它在 JavaFX 中显示上下文菜单。

HARDWARE_ACCELERATED 模式下,菜单将不可见。有关详细信息,请阅读关于硬件加速模式的限制的内容。

JavaFX 中的自定义上下文菜单

JavaFX 中 BrowserView 的自定义上下文菜单。

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;
    }
}