# 文本编辑器快捷功能(TextEditorFunction)
TextEditorFunction 接口用于为文本编辑器底部工具栏添加自定义快捷功能按钮。通过实现此接口,您可以为用户提供各种文本处理功能,如文本转换、插入模板、格式化代码等。
# 快速开始
继承 BaseTextEditorFunction 基类创建一个简单的快捷功能:
public class UppercaseFunction extends BaseTextEditorFunction {
@NonNull
@Override
public String name() {
return "转大写";
}
@Override
public boolean supportEditTextView() {
return true; // 支持在搜索/替换输入框中使用
}
@Override
public boolean supportRepeat() {
return false; // 不支持长按重复执行
}
@Override
public void doFunction(@NonNull PluginUI pluginUI, @NonNull TextEditor editor, @Nullable JSONObject data) {
// 获取选中的位置
int start = editor.getSelectionStart();
int end = editor.getSelectionEnd();
// 获取选中的文本
String selected = editor.subText(start, end);
if (selected.isEmpty()) {
pluginUI.showToast("请先选中文本");
return;
}
// 转换为大写
String upperCase = selected.toUpperCase();
// 替换选中的文本
editor.replaceText(start, end, upperCase);
}
}
# 接口概览
# TextEditorFunction 接口
| 方法 | 说明 |
|---|---|
void init(PluginContext) | 初始化方法 |
PluginContext getContext() | 获取插件上下文 |
boolean isEnabled() | 判断功能是否启用 |
String name() | 获取功能显示名称 |
boolean supportEditTextView() | 是否支持在搜索/替换输入框中使用 |
boolean supportRepeat() | 是否支持长按重复执行 |
PluginView buildOptionsView(PluginUI, JSONObject) | 创建设置选项视图 |
JSONObject getOptionsData(PluginUI, PluginView) | 获取用户配置的选项数据 |
void doFunction(PluginUI, TextEditor, JSONObject) | 执行快捷功能 |
# BaseTextEditorFunction 基类
BaseTextEditorFunction 抽象基类提供了接口的部分默认实现,简化开发:
默认实现的方法:
init(PluginContext)- 自动管理插件上下文的存储getContext()- 返回插件上下文实例isEnabled()- 默认返回true,功能始终启用buildOptionsView(PluginUI, JSONObject)- 默认返回null,无配置界面getOptionsData(PluginUI, PluginView)- 默认返回null,无配置数据
需要子类实现的抽象方法:
String name()- 功能名称boolean supportEditTextView()- 是否支持用于输入框boolean supportRepeat()- 是否支持重复执行void doFunction(PluginUI, TextEditor, JSONObject)- 核心功能逻辑
可重写的钩子方法:
void init()- 无参初始化钩子,在上下文设置后调用
推荐:优先继承
BaseTextEditorFunction基类而非直接实现TextEditorFunction接口,可减少样板代码。
# 生命周期
快捷功能的典型生命周期如下:
- 初始化阶段:
init(PluginContext)- 功能实例创建后立即调用,仅调用一次 - 启用检查:
isEnabled()- 可能被频繁调用,用于判断功能是否可用 - 显示名称:
name()- 获取功能的显示名称 - 配置界面 (可选):
- 用户点击添加按钮 →
buildOptionsView(PluginUI, JSONObject)- 创建配置界面 - 用户确认配置 →
getOptionsData(PluginUI, PluginView)- 获取并验证配置数据
- 用户点击添加按钮 →
- 执行功能:
doFunction(PluginUI, TextEditor, JSONObject)- 用户点击快捷按钮时调用
# 详细说明
# init
void init(PluginContext context)
初始化方法,在功能实例创建后立即调用。此方法在每个实例的生命周期中仅调用一次,用于进行必要的初始化工作,如资源加载、配置读取、状态初始化等。
参数:
context- MT 插件上下文,提供插件运行所需的环境和资源访问能力
注意事项:
如果继承
BaseTextEditorFunction,通常无需重写此方法。可重写无参的init()钩子方法进行自定义初始化。
# getContext
PluginContext getContext()
获取插件上下文实例,返回在 init() 方法中传入的 PluginContext 实例,用于后续操作中访问插件框架提供的各种服务和资源。
返回值: 插件上下文实例,不会为 null
# isEnabled
boolean isEnabled()
判断当前快捷功能是否启用。此方法的返回值决定功能的可见性和可用性:
- 返回
false时:- 用户在添加快捷功能时将无法看到此功能选项
- 已添加的此快捷功能按钮将变为不可用状态
- 返回
true时:功能正常显示和使用
典型使用场景:
- 功能开关控制:插件提供设置界面让用户选择启用或禁用某些功能
- 条件性启用:根据运行环境、权限状态或其他条件动态决定功能可用性
- 精简功能列表:当插件实现多个功能接口时,避免选项过多造成用户困扰
返回值: true 表示功能启用,false 表示功能禁用
注意事项:
此方法可能被频繁调用,建议避免在其中执行耗时操作。
示例:
@Override
public boolean isEnabled() {
// 根据用户设置决定是否启用
return getContext().getPreferences().getBoolean("enable_my_function", true);
}
# name
String name()
获取快捷功能的显示名称,该名称将显示在添加功能列表中。
返回值: 功能名称,不能为 null 或空字符串
本地化文本支持:
支持
{key}格式的本地化文本引用。如果返回的名称为{key}格式,将尝试转化为本地化文本。
示例:
@Override
public String name() {
return "{my_function_name}"; // 使用本地化文本
}
# supportEditTextView
boolean supportEditTextView()
是否支持在搜索/替换输入框中使用。文本编辑器除了主编辑区域外,还包含搜索和替换功能的输入框。此方法用于控制功能的作用范围:
- 返回
true:当焦点在搜索/替换输入框内时,doFunction将处理该焦点所在输入框的内容 - 返回
false:无论焦点在何处,doFunction始终处理主编辑器的内容
返回值: true 支持在搜索/替换输入框中使用;false 仅在主编辑器中可用
示例场景:
- 光标移动、插入文本等操作通常适合在输入框中使用,应返回
true - 代码格式化、语法检查等操作则仅适用于主编辑器,应返回
false
# supportRepeat
boolean supportRepeat()
是否支持长按重复执行。快捷功能支持设置长按时重复执行短按操作。由于并非所有功能都适合重复执行,需要根据功能特性进行判断:
- 不适合重复执行:如复制文本功能,多次复制与单次复制效果相同,应返回
false - 适合重复执行:如删除或插入文本功能,长按可实现连续删除或插入,应返回
true
返回值: true 支持长按重复执行;false 仅支持单次点击执行
# buildOptionsView
PluginView buildOptionsView(@NonNull PluginUI pluginUI, @Nullable JSONObject data)
创建设置选项视图。当快捷功能需要用户配置参数时(例如:插入模板的内容、格式化选项等),通过此方法创建自定义的配置选项视图。用户完成配置并确认保存后,系统将调用 getOptionsData() 方法获取用户输入的数据。
参数:
pluginUI- 插件 UI 接口data- 上次保存的配置数据,首次调用时为 null,可用于回显历史配置
返回值: 配置选项视图;如果该功能不需要用户参数配置,则返回 null
示例:
@Override
public PluginView buildOptionsView(@NonNull PluginUI pluginUI, @Nullable JSONObject data) {
return pluginUI.buildVerticalLayout()
.addTextView().text("插入内容:")
.addEditText("content").text(data).hint("请输入要插入的文本").singleLine(true)
.addSwitchButton("uppercase").text("转大写").checked(data)
.build();
}
# getOptionsData
JSONObject getOptionsData(@NonNull PluginUI pluginUI, @NonNull PluginView pluginView)
获取用户配置的选项数据。如果 buildOptionsView() 方法返回非 null 视图,在用户填写完参数并点击确定后会调用此方法。您需要从传入的设置选项视图中获取用户输入的配置数据,转换为 JSONObject 格式返回,插件系统会保存这些数据,并在执行 doFunction() 或下次编辑时作为参数传入。
参数:
pluginUI- 插件 UI 接口pluginView- 用户配置界面视图,由buildOptionsView()创建
返回值:
- 用户配置数据的 JSON 对象
- 验证失败时返回
TextEditorFunction.VALIDATION_FAILED - 不需要保存配置时返回 null
输入验证:
如果输入内容验证失败,例如某个选项输入格式错误,您可以为输入框获取焦点并弹出 Toast 告知用户错误原因,然后返回 VALIDATION_FAILED,这样配置对话框将会继续停留而不是直接消失。
示例:
@Override
public JSONObject getOptionsData(@NonNull PluginUI pluginUI, @NonNull PluginView pluginView) {
PluginEditText contentEdit = pluginView.requireViewById("content");
PluginSwitchButton uppercaseSwitch = pluginView.requireViewById("uppercase");
// 验证输入
if (contentEdit.length() == 0) {
contentEdit.requestFocus();
pluginUI.showToast("请输入内容");
return VALIDATION_FAILED;
}
// 保存数据
JSONObject data = new JSONObject();
data.putText(contentEdit);
data.putChecked(uppercaseSwitch);
return data;
}
# doFunction
void doFunction(@NonNull PluginUI pluginUI, @NonNull TextEditor editor, @Nullable JSONObject data)
快捷功能的核心方法。当用户点击快捷功能按钮时调用此方法,实现具体的功能逻辑。可以通过 editor 参数操作文本内容,通过 data 参数获取用户配置的选项数据。
参数:
pluginUI- 插件 UI 接口editor- 文本编辑器实例,提供文本操作接口data- 用户配置的选项数据,来自getOptionsData()方法的返回值,可能为 null
常用编辑器操作:
// 获取选中位置
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);
// 确保选中位置可见
editor.ensureSelectionVisible();
示例:
@Override
public void doFunction(@NonNull PluginUI pluginUI, @NonNull TextEditor editor, @Nullable JSONObject data) {
// 获取选中的位置
int start = editor.getSelectionStart();
int end = editor.getSelectionEnd();
// 获取选中的文本
String selected = editor.subText(start, end);
if (selected.isEmpty()) {
pluginUI.showToast("请先选中文本");
return;
}
// 转换文本
String upperCase = selected.toUpperCase();
// 替换选中的文本
editor.replaceText(start, end, upperCase);
// 显示提示
pluginUI.showToast("转换完成");
}
# 完整示例
# 示例一:简单的文本转换功能
创建一个将选中文本转换为大写的快捷功能:
public class UppercaseFunction extends BaseTextEditorFunction {
@NonNull
@Override
public String name() {
return "转大写";
}
@Override
public boolean supportEditTextView() {
return true;
}
@Override
public boolean supportRepeat() {
return false;
}
@Override
public void doFunction(@NonNull PluginUI pluginUI, @NonNull TextEditor editor, @Nullable JSONObject data) {
// 获取选中的位置
int start = editor.getSelectionStart();
int end = editor.getSelectionEnd();
// 获取选中的文本
String selected = editor.subText(start, end);
if (selected.isEmpty()) {
pluginUI.showToast("请先选中文本");
return;
}
// 转换为大写
String upperCase = selected.toUpperCase();
// 替换选中的文本
editor.replaceText(start, end, upperCase);
}
}
# 示例二:带配置界面的查找替换功能
创建一个完整的查找替换功能,支持正则表达式、区分大小写等选项:
public class FindReplaceFunction extends BaseTextEditorFunction {
@NonNull
@Override
public String name() {
return "{editor:find_and_replace}";
}
@Override
public boolean supportEditTextView() {
return false;
}
@Override
public boolean supportRepeat() {
return false;
}
@Override
public PluginView buildOptionsView(@NonNull PluginUI pluginUI, @Nullable JSONObject data) {
// 获取文本范围列表
List<String> itemList = pluginUI.getContext().getStringList(
"{editor:selected_text}",
"{editor:current_line_text}",
"{editor:full_text}"
);
return pluginUI.buildVerticalLayout()
// 查找内容
.addTextView().text("{editor:find_content}")
.addEditText("find").text(data).singleLine(true).requestFocus()
// 替换内容
.addTextView().text("{editor:replace_content}").marginTopDp(10)
.addEditText("replace").text(data).singleLine(true)
// 区分大小写
.addSwitchButton("matchCase").text("{editor:match_case}")
.checked(data).widthMatchParent().marginTopDp(8)
// 正则表达式
.addSwitchButton("regex").text("{editor:regex}")
.checked(data).widthMatchParent().marginTopDp(8)
.onCheckedChange((buttonView, isChecked) -> {
PluginViewGroup rootView = buttonView.getRootView();
PluginEditText find = rootView.requireViewById("find");
PluginEditText replace = rootView.requireViewById("replace");
// 设置正则语法高亮
find.setSyntaxHighlight(isChecked ? PluginEditText.SYNTAX_REGEX : null);
replace.setSyntaxHighlight(isChecked ? PluginEditText.SYNTAX_REGEX_REPLACEMENT : null);
})
// 文本范围
.addHorizontalLayout().children(builder -> builder
.addTextView("label").text("{editor:text_range}")
.addSpinner("textRange").items(itemList).selection(data)
.widthMatchParent().marginLeftDp(4)
)
.build();
}
@Nullable
@Override
public JSONObject getOptionsData(@NonNull PluginUI pluginUI, @NonNull PluginView pluginView) {
PluginEditText findEdit = pluginView.requireViewById("find");
PluginEditText replaceEdit = pluginView.requireViewById("replace");
PluginSwitchButton matchCaseSwitch = pluginView.requireViewById("matchCase");
PluginSwitchButton regexSwitch = pluginView.requireViewById("regex");
// 检查输入内容
if (findEdit.length() == 0) {
findEdit.requestFocus();
getContext().showToast("{editor:enter_content}");
return VALIDATION_FAILED;
}
if (regexSwitch.isChecked()) {
Pattern pattern;
try {
// 检查正则表达式
pattern = Regex.compile(findEdit.getText().toString());
} catch (Exception ex) {
pluginUI.showErrorMessage(ex);
findEdit.selectAll();
findEdit.requestFocus();
return VALIDATION_FAILED;
}
try {
// 检查正则替换模板
Regex.checkReplacementTemplate(pattern, replaceEdit.getText().toString());
} catch (Exception ex) {
pluginUI.showErrorMessage(ex);
replaceEdit.selectAll();
replaceEdit.requestFocus();
return VALIDATION_FAILED;
}
}
// 保存数据
JSONObject data = new JSONObject();
data.putText(findEdit);
data.putText(replaceEdit);
data.putChecked(matchCaseSwitch);
data.putChecked(regexSwitch);
data.putSelection(pluginView.requireViewById("textRange"));
return data;
}
@Override
public void doFunction(@NonNull PluginUI pluginUI, @NonNull TextEditor editor, @Nullable JSONObject data) {
Objects.requireNonNull(data);
String find = data.getString("find");
String replace = data.getString("replace");
boolean matchCase = data.getBoolean("matchCase");
boolean regex = data.getBoolean("regex");
int textRange = data.getInt("textRange");
// 构建正则表达式
int flags = regex ? Pattern.MULTILINE : Pattern.LITERAL;
if (matchCase) {
flags |= Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE;
}
Pattern pattern;
try {
pattern = Regex.compile(find, flags);
if (regex) {
Regex.checkReplacementTemplate(pattern, replace);
}
} catch (Exception e) {
pluginUI.showToast(e.toString());
return;
}
// 获取文本和选中范围
BufferedText text = editor.getBufferedText();
int[] selection = {editor.getSelectionStart(), editor.getSelectionEnd()};
// 根据文本范围调整选中区域
if (textRange == 0) { // 选中的文本
if (selection[0] == selection[1]) {
pluginUI.showToast("{editor:no_text_selected}");
return;
}
} else if (textRange == 1) { // 当前行文本
selection[0] = TextUtils.lastIndexOf(text, '\n', selection[0] - 1) + 1;
selection[1] = TextUtils.indexOf(text, '\n', selection[1]);
if (selection[1] == -1) {
selection[1] = text.length();
}
} else { // 全部文本
selection = new int[]{0, text.length()};
}
// 执行查找替换
Matcher matcher = text.matcher(pattern);
matcher.region(selection[0], selection[1]);
ArrayList<MatcherSnapshot> snapshots = new ArrayList<>();
while (matcher.find()) {
snapshots.add(matcher.toSnapshot());
}
if (!snapshots.isEmpty()) {
// 准备替换
if (regex) {
for (MatcherSnapshot snapshot : snapshots) {
snapshot.prepareReplacement(replace);
}
}
// 批量替换文本
editor.startLargeBatchEditingMode();
try {
for (int i = snapshots.size() - 1; i >= 0; i--) {
MatcherSnapshot snapshot = snapshots.get(i);
String replacement = regex ? snapshot.getComputedReplacement() : replace;
editor.replaceText(snapshot.start(), snapshot.end(), replacement);
}
} finally {
editor.finishLargeBatchEditingMode();
}
pluginUI.showToast("{editor:replace_result}", snapshots.size());
} else {
pluginUI.showToast("{editor:text_not_found}");
}
}
}
# 接口配置
重要:所有实现的插件接口都必须在模块的 build.gradle 中的 mtPlugin {} 配置块中注册。
在 app/build.gradle 文件中:
mtPlugin {
pluginID = "com.example.myplugin"
versionCode = 1
versionName = "v1.0"
name = "插件名称"
// 所有对外接口(必须包含所有实现的接口)
interfaces = [
"com.example.myplugin.UppercaseFunction",
"com.example.myplugin.FindReplaceFunction",
// ... 其他接口
]
}
# 注意事项
- 接口类型会自动识别,无需手动指定
interfaces列表中的类路径必须是完整的(包名 + 类名)- 如果接口未在此配置,MT 管理器将无法识别该接口
# 相关接口
- PluginContext - 插件上下文
- PluginUI - 插件 UI 核心
- PluginView - 插件视图
- TextEditor - 文本编辑器操作接口
- Regex - 正则表达式工具
- AsyncTask - 异步任务