# 文本编辑器工具菜单(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接口,可减少样板代码。
# 生命周期
工具菜单的典型生命周期如下:
- 初始化阶段:
init(PluginContext)- 菜单实例创建后立即调用,仅调用一次 - 启用检查:
isEnabled()- 可能被频繁调用,用于判断菜单是否可用 - 显示名称:
name()- 获取菜单的显示名称 - 菜单图标:
icon()- 获取菜单的图标 - 可见性检查:
checkVisible(TextEditor)- 打开「编辑」菜单前调用,判断是否显示该菜单项 - 执行操作:
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.gradle 的 mtPlugin {} 配置块中注册:
mtPlugin {
pluginID = "com.example.myplugin"
versionCode = 1
versionName = "1.0"
name = "文本工具"
interfaces = [
"com.example.myplugin.Base64ToolMenu",
"com.example.myplugin.JsonFormatterToolMenu",
// ... 其他接口
]
}
注意:必须使用完整的类路径(包名 + 类名),接口类型会自动识别。
# 相关接口
- PluginContext - 插件上下文
- PluginUI - 插件 UI 核心
- TextEditor - 文本编辑器操作接口
- TextEditorFunction - 文本编辑器快捷功能
- TextEditorFloatingMenu - 文本编辑器浮动菜单