# 文本编辑器浮动菜单(TextEditorFloatingMenu)
TextEditorFloatingMenu 接口用于为文本编辑器的浮动菜单添加自定义菜单项。当用户在文本编辑器中选中文本时,会弹出浮动菜单,通过实现此接口,您可以在菜单中添加自定义的操作选项,如文本转换、复制处理、内容分析等。
# 快速开始
继承 BaseTextEditorFloatingMenu 基类创建一个简单的浮动菜单:
public class CaseInversionMenu extends BaseTextEditorFloatingMenu {
@NonNull
@Override
public String name() {
return "大小写反转";
}
@NonNull
@Override
public Drawable icon() {
// 使用内置的 Material 图标
return MaterialIcons.get("swap_vert");
}
@Override
public boolean checkVisible(@NonNull TextEditor editor) {
// 仅在选中文本时显示菜单
return editor.hasTextSelected();
}
@Override
public void onMenuClick(@NonNull PluginUI pluginUI, @NonNull TextEditor editor) {
// 获取选中的文本
int from = editor.getSelectionStart();
int to = editor.getSelectionEnd();
String selected = editor.subText(from, to);
// 反转大小写
char[] charArray = selected.toCharArray();
for (int i = 0; i < charArray.length; i++) {
char c = charArray[i];
if (Character.isLowerCase(c)) {
charArray[i] = Character.toUpperCase(c);
} else {
charArray[i] = Character.toLowerCase(c);
}
}
// 替换文本
editor.replaceText(from, to, new String(charArray));
}
}
# 接口概览
# TextEditorFloatingMenu 接口
TextEditorFloatingMenu 继承自 TextEditorBaseMenu,具体方法定义如下:
| 方法 | 说明 |
|---|---|
void init(PluginContext) | 初始化方法 |
PluginContext getContext() | 获取插件上下文 |
boolean isEnabled() | 判断菜单是否启用 |
String name() | 获取菜单显示名称 |
Drawable icon() | 获取菜单图标 |
void onPluginButtonClick(PluginUI) | 插件设置按钮点击回调 |
boolean checkVisible(TextEditor) | 测试是否应该显示菜单 |
void onMenuClick(PluginUI, TextEditor) | 菜单点击的核心方法 |
# BaseTextEditorFloatingMenu 基类
BaseTextEditorFloatingMenu 抽象基类提供了接口的部分默认实现,简化开发:
默认实现的方法:
init(PluginContext)- 自动管理插件上下文的存储getContext()- 返回插件上下文实例isEnabled()- 默认返回true,菜单始终启用onPluginButtonClick(PluginUI)- 显示插件信息对话框
需要子类实现的抽象方法:
String name()- 菜单名称Drawable icon()- 菜单图标boolean checkVisible(TextEditor)- 菜单可见性判断void onMenuClick(PluginUI, TextEditor)- 核心功能逻辑
可重写的钩子方法:
void init()- 无参初始化钩子,在上下文设置后调用
推荐:优先继承
BaseTextEditorFloatingMenu基类而非直接实现TextEditorFloatingMenu接口,可减少样板代码。
# 生命周期
浮动菜单的典型生命周期如下:
- 初始化阶段:
init(PluginContext)- 菜单实例创建后立即调用,仅调用一次 - 启用检查:
isEnabled()- 可能被频繁调用,用于判断菜单是否可用 - 显示名称:
name()- 获取菜单的显示名称 - 菜单图标:
icon()- 获取菜单的图标 - 可见性检查:
checkVisible(TextEditor)- 弹出浮动菜单前调用,判断是否显示该菜单项 - 执行操作:
onMenuClick(PluginUI, TextEditor)- 用户点击菜单项时调用
# 详细说明
# init
void init(PluginContext context)
初始化方法,在菜单实例创建后立即调用。此方法在每个实例的生命周期中仅调用一次,用于进行必要的初始化工作,如资源加载、配置读取、状态初始化等。
参数:
context- MT 插件上下文,提供插件运行所需的环境和资源访问能力
注意事项:
如果继承
BaseTextEditorFloatingMenu,通常无需重写此方法。可重写无参的init()钩子方法进行自定义初始化。
# getContext
PluginContext getContext()
获取插件上下文实例,返回在 init() 方法中传入的 PluginContext 实例,用于后续操作中访问插件框架提供的各种服务和资源。
返回值: 插件上下文实例,不会为 null
# isEnabled
boolean isEnabled()
判断当前菜单是否启用。此方法的返回值决定菜单的可见性:
- 返回
false时:用户即使在设置界面也无法看到此菜单 - 返回
true时:菜单正常显示和使用
典型使用场景:
- 功能开关控制:插件提供设置界面让用户选择启用或禁用某些菜单功能
- 条件性启用:根据运行环境、权限状态或其他条件动态决定菜单可用性
- 精简菜单列表:当插件实现多个菜单接口时,避免选项过多造成用户困扰
返回值: true 表示菜单启用,false 表示菜单禁用
注意事项:
此方法可能被频繁调用,建议避免在其中执行耗时操作。
示例:
@Override
public boolean isEnabled() {
// 根据用户设置决定是否启用
return getContext().getPreferences().getBoolean("enable_case_menu", true);
}
# name
String name()
获取菜单的显示名称,该名称将显示在浮动菜单中。菜单命名请尽可能简短,以适应屏幕的有限空间。
返回值: 菜单名称,不能为 null 或空字符串
本地化文本支持:
支持
{key}格式的本地化文本引用。如果返回的名称为{key}格式,将尝试转化为本地化文本。
示例:
@Override
public String name() {
return "{case_inversion}"; // 使用本地化文本
}
对应的语言包文件:
assets/strings.mtl:
case_inversion: Case Inversion
assets/strings-zh-CN.mtl:
case_inversion: 大小写反转
# icon
Drawable icon()
获取菜单的图标,图标将显示在浮动菜单的菜单项中。
返回值: 菜单图标,不能为 null
图标获取方式:
使用内置的 Material 图标(推荐):
return MaterialIcons.get("swap_vert");更多图标请访问:https://mt2.cn/icons
加载 Vector XML 文件:
return VectorDrawableLoader.fromVectorXml(getContext(), "icon.xml");加载 SVG 文件:
return VectorDrawableLoader.fromSvg(getContext(), "icon.svg");
示例:
@Override
public Drawable icon() {
// 使用内置的 Material 图标
return MaterialIcons.get("swap_vert");
}
# onPluginButtonClick
void onPluginButtonClick(@NonNull PluginUI pluginUI)
插件按钮点击时的回调方法。在菜单编辑界面,每个插件功能项的右边有一个插件图标样式的按钮,当该按钮被点击后会调用此方法。
参数:
pluginUI- 插件 UI 接口
默认实现:
BaseTextEditorFloatingMenu 提供的默认实现会显示一个对话框,包含插件名称、插件 ID 和接口类名。如果需要自定义行为(如显示帮助信息、打开设置等),可以重写此方法。
典型使用场景:
- 显示功能使用说明
- 打开相关设置界面
- 显示插件信息或版本
示例:
@Override
public void onPluginButtonClick(@NonNull PluginUI pluginUI) {
pluginUI.buildDialog()
.setTitle("大小写反转")
.setMessage("将选中文本的大小写进行反转:\n• 小写字母转大写\n• 大写字母转小写")
.setPositiveButton("{ok}", null)
.show();
}
# checkVisible
boolean checkVisible(@NonNull TextEditor editor)
测试是否应该显示菜单。该方法在弹出浮动菜单前调用,您需要在此方法中对文本编辑器状态进行检查,以决定是否显示该菜单项。
参数:
editor- 文本编辑器实例
返回值: true 表示菜单显示,false 表示菜单隐藏
注意事项:
此方法可能被频繁调用,建议避免在其中执行耗时操作。
典型使用场景:
仅在选中文本时显示:
@Override public boolean checkVisible(@NonNull TextEditor editor) { return editor.hasTextSelected(); }根据选中文本内容判断:
@Override public boolean checkVisible(@NonNull TextEditor editor) { if (!editor.hasTextSelected()) { return false; } String selected = editor.getSelectedText(); // 仅在选中的是数字时显示 return selected.matches("\\d+"); }根据编辑器状态判断:
@Override public boolean checkVisible(@NonNull TextEditor editor) { // 仅在非只读模式下显示 return !editor.isReadOnly(); }
# onMenuClick
void onMenuClick(@NonNull PluginUI pluginUI, @NonNull TextEditor editor)
菜单功能的核心方法。当用户点击菜单项时调用此方法,实现具体的功能逻辑。可以通过 editor 参数操作文本内容。
参数:
pluginUI- 插件 UI 接口editor- 文本编辑器实例,提供文本操作接口
常用编辑器操作:
// 获取选中位置
int start = editor.getSelectionStart();
int end = editor.getSelectionEnd();
// 获取选中的文本
String selectedText = editor.subText(start, end);
// 替换文本
editor.replaceText(start, end, "新文本");
// 插入文本
editor.insertText(position, "插入内容");
// 删除文本
editor.deleteText(start, end);
// 设置选中位置
editor.setSelection(newStart, newEnd);
// 检查是否有选中文本
boolean hasSelection = editor.hasTextSelected();
示例:
@Override
public void onMenuClick(@NonNull PluginUI pluginUI, @NonNull TextEditor editor) {
int from = editor.getSelectionStart();
int to = editor.getSelectionEnd();
String selected = editor.subText(from, to);
if (selected.isEmpty()) {
pluginUI.showToast("请先选中文本");
return;
}
// 处理文本
String result = processText(selected);
// 替换选中的文本
editor.replaceText(from, to, result);
// 显示提示
pluginUI.showToast("操作完成");
}
# 完整示例
# 示例一:大小写反转菜单
创建一个将选中文本的大小写进行反转的浮动菜单:
public class CaseInversionMenu extends BaseTextEditorFloatingMenu {
@NonNull
@Override
public String name() {
return "{case_inversion}";
}
@NonNull
@Override
public Drawable icon() {
// 直接获取内置的Material图标:https://mt2.cn/icons
return MaterialIcons.get("swap_vert");
}
@Override
public boolean checkVisible(@NonNull TextEditor editor) {
// 仅在选中文本时显示菜单
return editor.hasTextSelected();
}
@Override
public void onMenuClick(@NonNull PluginUI pluginUI, @NonNull TextEditor editor) {
int from = editor.getSelectionStart();
int to = editor.getSelectionEnd();
char[] charArray = editor.subText(from, to).toCharArray();
// 反转大小写
for (int i = 0; i < charArray.length; i++) {
char c = charArray[i];
if (Character.isLowerCase(c)) {
charArray[i] = Character.toUpperCase(c);
} else {
charArray[i] = Character.toLowerCase(c);
}
}
editor.replaceText(from, to, new String(charArray));
}
}
对应的语言包文件:
assets/strings-zh-CN.mtl:
case_inversion: 大小写反转
# 示例二:URL 编码/解码菜单
创建一个对选中文本进行 URL 编码或解码的浮动菜单:
public class URLEncodeMenu extends BaseTextEditorFloatingMenu {
@NonNull
@Override
public String name() {
return "URL编码";
}
@NonNull
@Override
public Drawable icon() {
return MaterialIcons.get("link");
}
@Override
public boolean checkVisible(@NonNull TextEditor editor) {
return editor.hasTextSelected();
}
@Override
public void onMenuClick(@NonNull PluginUI pluginUI, @NonNull TextEditor editor) {
int from = editor.getSelectionStart();
int to = editor.getSelectionEnd();
String selected = editor.subText(from, to);
if (selected.isEmpty()) {
pluginUI.showToast("请先选中文本");
return;
}
// 创建选择对话框
pluginUI.buildDialog()
.setTitle("URL编码")
.setItems(new String[]{"编码", "解码"}, (dialog, which) -> {
try {
String result;
if (which == 0) {
// URL 编码
result = java.net.URLEncoder.encode(selected, "UTF-8");
} else {
// URL 解码
result = java.net.URLDecoder.decode(selected, "UTF-8");
}
editor.replaceText(from, to, result);
pluginUI.showToast("操作完成");
} catch (Exception e) {
pluginUI.showErrorMessage(e);
}
})
.show();
}
}
# 示例三:Base64 编码菜单
创建一个对选中文本进行 Base64 编码或解码的浮动菜单:
public class Base64Menu extends BaseTextEditorFloatingMenu {
@NonNull
@Override
public String name() {
return "Base64";
}
@NonNull
@Override
public Drawable icon() {
return MaterialIcons.get("code");
}
@Override
public boolean checkVisible(@NonNull TextEditor editor) {
return editor.hasTextSelected();
}
@Override
public void onMenuClick(@NonNull PluginUI pluginUI, @NonNull TextEditor editor) {
int from = editor.getSelectionStart();
int to = editor.getSelectionEnd();
String selected = editor.subText(from, to);
if (selected.isEmpty()) {
pluginUI.showToast("请先选中文本");
return;
}
// 创建选择对话框
pluginUI.buildDialog()
.setTitle("Base64")
.setItems(new String[]{"编码", "解码"}, (dialog, which) -> {
try {
if (which == 0) {
// Base64 编码
String result = android.util.Base64.encodeToString(
selected.getBytes("UTF-8"),
android.util.Base64.NO_WRAP
);
editor.replaceText(from, to, result);
} else {
// Base64 解码
byte[] decoded = android.util.Base64.decode(
selected,
android.util.Base64.NO_WRAP
);
String result = new String(decoded, "UTF-8");
editor.replaceText(from, to, result);
}
pluginUI.showToast("操作完成");
} catch (Exception e) {
pluginUI.showErrorMessage(e);
}
})
.show();
}
}
# 接口配置
重要:所有实现的插件接口都必须在模块的 build.gradle 中的 mtPlugin {} 配置块中注册。
在 app/build.gradle 文件中:
mtPlugin {
pluginID = "com.example.myplugin"
versionCode = 1
versionName = "v1.0"
name = "插件名称"
// 所有对外接口(必须包含所有实现的接口)
interfaces = [
"com.example.myplugin.CaseInversionMenu",
"com.example.myplugin.URLEncodeMenu",
"com.example.myplugin.Base64Menu",
// ... 其他接口
]
}
# 注意事项
- 接口类型会自动识别,无需手动指定
interfaces列表中的类路径必须是完整的(包名 + 类名)- 如果接口未在此配置,MT 管理器将无法识别该接口
# 相关接口
- PluginContext - 插件上下文
- PluginUI - 插件 UI 核心
- TextEditor - 文本编辑器操作接口
- TextEditorFunction - 文本编辑器快捷功能
- TextEditorToolMenu - 文本编辑器工具菜单