# 文本编辑器工具菜单(TextEditorToolMenu)

TextEditorToolMenu 接口用于在文本编辑器顶部工具栏的「编辑」分组内添加自定义菜单项。通过实现此接口,您可以为文本编辑器添加各种实用工具,如编码转换、格式化、加密解密等功能。

# 快速开始

继承 BaseTextEditorToolMenu 基类创建一个简单的工具菜单:

public class Base64ToolMenu extends BaseTextEditorToolMenu {
    @NonNull
    @Override
    public String name() {
        return "Base64";
    }

    @NonNull
    @Override
    public Drawable icon() {
        // 使用内置的 Material 图标
        return MaterialIcons.get("code");
    }

    @Override
    public boolean checkVisible(@NonNull TextEditor editor) {
        // 始终显示菜单
        return true;
    }

    @Override
    public void onMenuClick(@NonNull PluginUI pluginUI, @NonNull TextEditor editor) {
        // 获取选中的文本
        int start = editor.getSelectionStart();
        int end = editor.getSelectionEnd();
        String selectedText = editor.subText(start, end);

        // 显示处理对话框
        pluginUI.buildDialog()
            .setTitle("Base64 编码/解码")
            .setMessage("选中文本: " + selectedText)
            .setPositiveButton("{ok}", null)
            .show();
    }
}

# 接口概览

# TextEditorToolMenu 接口

TextEditorToolMenu 继承自 TextEditorBaseMenu,具体方法定义如下:

方法 说明
void init(PluginContext) 初始化方法
PluginContext getContext() 获取插件上下文
boolean isEnabled() 判断菜单是否启用
String name() 获取菜单显示名称
Drawable icon() 获取菜单图标
void onPluginButtonClick(PluginUI) 插件设置按钮点击回调
boolean checkVisible(TextEditor) 测试是否应该显示菜单
void onMenuClick(PluginUI, TextEditor) 菜单点击的核心方法

# BaseTextEditorToolMenu 基类

BaseTextEditorToolMenu 抽象基类提供了接口的部分默认实现,简化开发:

默认实现的方法:

  • init(PluginContext) - 自动管理插件上下文的存储
  • getContext() - 返回插件上下文实例
  • isEnabled() - 默认返回 true,菜单始终启用
  • onPluginButtonClick(PluginUI) - 显示插件信息对话框

需要子类实现的抽象方法:

  • String name() - 菜单名称
  • Drawable icon() - 菜单图标
  • boolean checkVisible(TextEditor) - 菜单可见性判断
  • void onMenuClick(PluginUI, TextEditor) - 核心功能逻辑

可重写的钩子方法:

  • void init() - 无参初始化钩子,在上下文设置后调用

推荐:优先继承 BaseTextEditorToolMenu 基类而非直接实现 TextEditorToolMenu 接口,可减少样板代码。

# 生命周期

工具菜单的典型生命周期如下:

  1. 初始化阶段: init(PluginContext) - 菜单实例创建后立即调用,仅调用一次
  2. 启用检查: isEnabled() - 可能被频繁调用,用于判断菜单是否可用
  3. 显示名称: name() - 获取菜单的显示名称
  4. 菜单图标: icon() - 获取菜单的图标
  5. 可见性检查: checkVisible(TextEditor) - 打开「编辑」菜单前调用,判断是否显示该菜单项
  6. 执行操作: onMenuClick(PluginUI, TextEditor) - 用户点击菜单项时调用

# 详细说明

# init

void init(PluginContext context)

初始化方法,在菜单实例创建后立即调用。此方法在每个实例的生命周期中仅调用一次,用于进行必要的初始化工作,如资源加载、配置读取、状态初始化等。

参数:

  • context - MT 插件上下文,提供插件运行所需的环境和资源访问能力

注意事项:

如果继承 BaseTextEditorToolMenu,通常无需重写此方法。可重写无参的 init() 钩子方法进行自定义初始化。


# getContext

PluginContext getContext()

获取插件上下文实例,返回在 init() 方法中传入的 PluginContext 实例,用于后续操作中访问插件框架提供的各种服务和资源。

返回值: 插件上下文实例,不会为 null


# isEnabled

boolean isEnabled()

判断当前菜单是否启用。此方法的返回值决定菜单的可见性:

  • 返回 false 时:用户即使在设置界面也无法看到此菜单
  • 返回 true 时:菜单正常显示和使用

典型使用场景:

  • 功能开关控制:插件提供设置界面让用户选择启用或禁用某些菜单功能
  • 条件性启用:根据运行环境、权限状态或其他条件动态决定菜单可用性
  • 精简菜单列表:当插件实现多个菜单接口时,避免选项过多造成用户困扰

返回值: true 表示菜单启用,false 表示菜单禁用

注意事项:

此方法可能被频繁调用,建议避免在其中执行耗时操作。

示例:

@Override
public boolean isEnabled() {
    // 从配置中读取是否启用
    SharedPreferences prefs = getContext().getPreferences();
    return prefs.getBoolean("enable_base64_tool", true);
}

# name

String name()

获取菜单的显示名称,该名称将显示在文本编辑器顶部工具栏的「编辑」菜单中。

返回值: 菜单名称,不能为 null 或空字符串

本地化文本支持:

支持 {key} 格式的本地化文本引用。例如返回 {base64_tool} 会自动查找语言包中的对应翻译。详见 LocalString 文档。

示例:

@NonNull
@Override
public String name() {
    return "{base64_tool}";  // 使用本地化文本
}

对应的语言包文件:

assets/strings.mtl:

base64_tool: Base64

assets/strings-zh-CN.mtl:

base64_tool: Base64 编解码

# icon

Drawable icon()

获取菜单的图标,该图标将显示在菜单项的左侧。

返回值: 菜单图标,不能为 null

获取图标的三种方式:

# 1. 使用 Material 图标(推荐)

@NonNull
@Override
public Drawable icon() {
    return MaterialIcons.get("code");
}

访问 https://mt2.cn/icons (opens new window) 查看所有可用的 Material 图标。

# 2. 加载 Vector XML 文件

@NonNull
@Override
public Drawable icon() {
    return VectorDrawableLoader.fromVectorXml(getContext(), "my_icon.xml");
}

# 3. 加载 SVG 文件

@NonNull
@Override
public Drawable icon() {
    return VectorDrawableLoader.fromSvg(getContext(), "my_icon.svg");
}

# onPluginButtonClick

void onPluginButtonClick(PluginUI pluginUI)

插件设置按钮点击时的回调方法。在菜单编辑界面,每个插件功能项的右边有一个插件图标样式的按钮,当该按钮被点击后会调用此方法。

参数:

  • pluginUI - 插件 UI 接口,用于显示对话框、Toast 消息等

典型使用场景:

  • 显示关于对话框,介绍工具的功能和用法
  • 打开设置界面,让用户配置工具的参数
  • 显示帮助信息或使用说明

注意:

BaseTextEditorToolMenu 已提供默认实现,会显示插件信息对话框。如需自定义行为,可以重写此方法。

示例:

@Override
public void onPluginButtonClick(@NonNull PluginUI pluginUI) {
    pluginUI.buildDialog()
        .setTitle("关于 Base64 工具")
        .setMessage("这是一个 Base64 编码和解码工具,支持多种编码选项。")
        .setPositiveButton("{ok}", null)
        .show();
}

# checkVisible

boolean checkVisible(TextEditor editor)

测试是否应该显示菜单。该方法在打开「编辑」菜单前调用,您需要在此方法中对文本编辑器状态进行检查,以决定是否显示菜单。

参数:

  • editor - 文本编辑器实例,可用于检查编辑器状态

返回值: true 表示菜单显示,false 表示菜单隐藏

典型使用场景:

  • 根据是否选中文本决定菜单可见性
  • 根据文本内容类型决定是否显示菜单
  • 根据编辑器状态(只读模式等)决定菜单可见性

注意事项:

此方法可能被频繁调用,建议避免在其中执行耗时操作。

示例:

@Override
public boolean checkVisible(@NonNull TextEditor editor) {
    // 始终显示菜单
    return true;
}
@Override
public boolean checkVisible(@NonNull TextEditor editor) {
    // 仅在非只读模式下显示
    return !editor.isReadOnly();
}

# onMenuClick

void onMenuClick(PluginUI pluginUI, TextEditor editor)

菜单功能的核心方法。当用户点击菜单项时调用此方法,实现具体的功能逻辑,可以通过 editor 参数操作文本内容。

参数:

  • pluginUI - 插件 UI 接口,用于创建对话框、显示消息等
  • editor - 文本编辑器实例,提供文本操作接口

注意事项:

获取选中文本时,推荐先调用 getSelectionStart()getSelectionEnd() 获取位置,再调用 subText(start, end) 截取文本。

示例:

@Override
public void onMenuClick(@NonNull PluginUI pluginUI, @NonNull TextEditor editor) {
    // 获取选中的文本
    int start = editor.getSelectionStart();
    int end = editor.getSelectionEnd();
    String selectedText = editor.subText(start, end);

    // 执行处理逻辑
    String result = processText(selectedText);

    // 替换文本
    if (!selectedText.isEmpty()) {
        editor.replaceText(start, end, result);
    } else {
        pluginUI.showToast("请先选中文本");
    }
}

# 完整示例

# 示例一:简单的文本转换工具

实现一个将文本转换为大写的工具菜单:

public class UppercaseToolMenu extends BaseTextEditorToolMenu {
    @NonNull
    @Override
    public String name() {
        return "转大写";
    }

    @NonNull
    @Override
    public Drawable icon() {
        return MaterialIcons.get("format_size");
    }

    @Override
    public boolean checkVisible(@NonNull TextEditor editor) {
        // 仅在选中文本时显示
        return editor.hasTextSelected();
    }

    @Override
    public void onMenuClick(@NonNull PluginUI pluginUI, @NonNull TextEditor editor) {
        int start = editor.getSelectionStart();
        int end = editor.getSelectionEnd();
        String selectedText = editor.subText(start, end);

        // 转换为大写
        String upperCase = selectedText.toUpperCase();
        editor.replaceText(start, end, upperCase);

        pluginUI.showToast("已转换为大写");
    }
}

# 示例二:Base64 编码解码工具

实现一个完整的 Base64 编码解码工具,支持多种选项配置:

public class Base64ToolMenu extends BaseTextEditorToolMenu {
    public static final String KEY_BASE64_FLAGS = "base64Flags";
    public static final String KEY_CHARSET = "charset";

    @NonNull
    @Override
    public String name() {
        return "Base64";
    }

    @NonNull
    @Override
    public Drawable icon() {
        return MaterialIcons.get("code");
    }

    @Override
    public boolean checkVisible(@NonNull TextEditor editor) {
        return true;
    }

    @Override
    public void onMenuClick(@NonNull PluginUI pluginUI, @NonNull TextEditor editor) {
        int selStart = editor.getSelectionStart();
        int selEnd = editor.getSelectionEnd();
        String selectedText = editor.subText(selStart, selEnd);

        // 创建自定义样式
        PluginEditTextBuilder builder = pluginUI
                .defaultStyle(new PluginUI.StyleWrapper() {
                    @Override
                    protected void handleEditText(PluginUI pluginUI, PluginEditTextBuilder builder) {
                        super.handleEditText(pluginUI, builder);
                        builder.minLines(5).maxLines(10).textSize(12)
                               .softWrap(PluginEditText.SOFT_WRAP_KEEP_WORD);
                    }

                    @Override
                    protected void handleButton(PluginUI pluginUI, PluginButtonBuilder builder) {
                        super.handleButton(pluginUI, builder);
                        builder.style(PluginButton.Style.FILLED);
                        builder.text("{base64:" + builder.getId() + "}");
                    }
                })
                .buildVerticalLayout()
                .paddingTop(pluginUI.dialogPaddingVertical() / 2)
                // 添加输入框1
                .addEditBox("input1").text(selectedText)
                // 添加编码解码按钮
                .addHorizontalLayout().gravity(Gravity.CENTER_VERTICAL).children(layout -> layout
                        .addButton("encode").width(0).layoutWeight(1)
                        .addButton("decode").width(0).layoutWeight(1)
                )
                // 添加输入框2
                .addEditBox("input2");

        if (!selectedText.isEmpty()) {
            // 添加替换原文按钮
            builder.addButton("replace").widthMatchParent().enable(false);
        }

        PluginView view = builder.build();
        PluginDialog dialog = pluginUI.buildDialog()
                .setTitle(name())
                .setView(view)
                .setPositiveButton("{close}", null)
                .setNegativeButton("{base64:exchange}", null)
                .setNeutralButton("{base64:options}", null)
                .show();

        PluginEditText input1 = view.requireViewById("input1");
        PluginEditText input2 = view.requireViewById("input2");
        SharedPreferences preferences = pluginUI.getContext().getPreferences();

        // 编码
        view.requireViewById("encode").setOnClickListener(button -> {
            String text = input1.getText().toString();
            int flags = preferences.getInt(KEY_BASE64_FLAGS, 0);
            Charset charset = Charset.forName(preferences.getString(KEY_CHARSET, "UTF-8"));
            input2.setText(Base64.encodeToString(text.getBytes(charset), flags));
        });

        // 解码
        view.requireViewById("decode").setOnClickListener(button -> {
            String text = input1.getText().toString();
            int flags = preferences.getInt(KEY_BASE64_FLAGS, 0);
            Charset charset = Charset.forName(preferences.getString(KEY_CHARSET, "UTF-8"));
            try {
                input2.setText(new String(Base64.decode(text.getBytes(charset), flags), charset));
            } catch (Exception e) {
                pluginUI.showToast(e.toString());
            }
        });

        // 交换
        dialog.getNegativeButton().setOnClickListener(button -> {
            String text = input1.getText().toString();
            input1.setText(input2.getText());
            input2.setText(text);
        });

        // 选项菜单
        dialog.getNeutralButton().setOnClickListener(button -> {
            showOptionsMenu(pluginUI, button, preferences);
        });

        // 替换原文
        if (!selectedText.isEmpty()) {
            PluginView replaceButton = view.requireViewById("replace");
            input2.addTextChangedListener(new PluginEditTextWatcher.Simple() {
                @Override
                public void afterTextChanged(PluginEditText editText, Editable s) {
                    replaceButton.setEnabled(!TextUtils.isEmpty(s));
                }
            });
            replaceButton.setOnClickListener(button -> {
                editor.replaceText(selStart, selEnd, input2.getText());
                dialog.dismiss();
            });
        }
    }

    private void showOptionsMenu(PluginUI pluginUI, PluginView anchor, SharedPreferences preferences) {
        PluginPopupMenu popupMenu = pluginUI.createPopupMenu(anchor);
        PluginMenu menu = popupMenu.getMenu();

        int flags = preferences.getInt(KEY_BASE64_FLAGS, 0);
        String[] charsets = new String[]{"UTF-8", "UTF-16", "GBK", "Big5"};
        String currentCharset = preferences.getString(KEY_CHARSET, "UTF-8");

        // 添加 Base64 Flags 选项组
        menu.add("0", "{base64:flag_no_padding}")
            .setCheckable(true)
            .setChecked((flags & Base64.NO_PADDING) != 0);
        menu.add("1", "{base64:flag_no_wrap}")
            .setCheckable(true)
            .setChecked((flags & Base64.NO_WRAP) != 0);
        menu.add("2", "{base64:flag_url_safe}")
            .setCheckable(true)
            .setChecked((flags & Base64.URL_SAFE) != 0);

        // 添加文本编码选项组
        PluginSubMenu charsetGroup = menu.addSubMenu("charsets", "{base64:charset}");
        for (String charset : charsets) {
            charsetGroup.add(charset, charset, "group")
                       .setChecked(currentCharset.equals(charset));
        }
        charsetGroup.setGroupCheckable("group", true, true);

        // 设置菜单点击事件
        popupMenu.setOnMenuItemClickListener(item -> {
            String itemId = item.getItemId();
            if (itemId.equals("charsets")) {
                return true;
            }
            // 设置文本编码
            if (itemId.length() > 1) {
                item.setChecked(true);
                preferences.edit().putString(KEY_CHARSET, item.getItemId()).apply();
                return true;
            }
            // 设置 Base64 Flags
            item.setChecked(!item.isChecked());
            int selectedFlags = switch (itemId) {
                case "0" -> Base64.NO_PADDING;
                case "1" -> Base64.NO_WRAP;
                case "2" -> Base64.URL_SAFE;
                default -> throw new RuntimeException();
            };
            int newFlags = preferences.getInt(KEY_BASE64_FLAGS, 0);
            if (item.isChecked()) {
                newFlags |= selectedFlags;
            } else {
                newFlags &= ~selectedFlags;
            }
            preferences.edit().putInt(KEY_BASE64_FLAGS, newFlags).apply();
            return true;
        });
        popupMenu.show();
    }
}

对应的语言包文件:

assets/base64-zh-CN.mtl:

encode: 编码
decode: 解码
exchange: 交换
options: 选项
replace: 替换原文
flag_no_padding: 无填充
flag_no_wrap: 无换行
flag_url_safe: URL安全
charset: 字符编码

# 示例三:JSON 格式化工具

实现一个 JSON 格式化和压缩工具:

public class JsonFormatterToolMenu extends BaseTextEditorToolMenu {
    @NonNull
    @Override
    public String name() {
        return "JSON";
    }

    @NonNull
    @Override
    public Drawable icon() {
        return MaterialIcons.get("data_object");
    }

    @Override
    public boolean checkVisible(@NonNull TextEditor editor) {
        // 检查文件类型是否为 JSON
        String syntaxName = editor.getSyntaxName();
        return syntaxName != null && syntaxName.equalsIgnoreCase("json");
    }

    @Override
    public void onMenuClick(@NonNull PluginUI pluginUI, @NonNull TextEditor editor) {
        int start = editor.getSelectionStart();
        int end = editor.getSelectionEnd();

        // 如果没有选中文本,处理全文
        if (start == end) {
            start = 0;
            end = editor.length();
        }

        String text = editor.subText(start, end);

        // 显示操作选择对话框
        pluginUI.buildDialog()
                .setTitle("JSON 格式化")
                .setItems(new String[]{"格式化", "压缩"}, (dialog, which) -> {
                    try {
                        String result;
                        if (which == 0) {
                            // 格式化
                            result = formatJson(text);
                        } else {
                            // 压缩
                            result = compressJson(text);
                        }
                        editor.replaceText(start, end, result);
                        pluginUI.showToast("处理成功");
                    } catch (Exception e) {
                        pluginUI.showErrorMessage(e);
                    }
                })
                .show();
    }

    private String formatJson(String json) throws Exception {
        // JSON 格式化实现
        JSONObject obj = new JSONObject(json);
        return obj.toString(4);  // 4个空格缩进
    }

    private String compressJson(String json) throws Exception {
        // JSON 压缩实现
        JSONObject obj = new JSONObject(json);
        return obj.toString();
    }
}

# 接口配置

实现工具菜单后,必须在 build.gradlemtPlugin {} 配置块中注册:

mtPlugin {
    pluginID = "com.example.myplugin"
    versionCode = 1
    versionName = "1.0"
    name = "文本工具"

    interfaces = [
        "com.example.myplugin.Base64ToolMenu",
        "com.example.myplugin.JsonFormatterToolMenu",
        // ... 其他接口
    ]
}

注意:必须使用完整的类路径(包名 + 类名),接口类型会自动识别。

# 相关接口