# 文本编辑器快捷功能(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 接口,可减少样板代码。

# 生命周期

快捷功能的典型生命周期如下:

  1. 初始化阶段: init(PluginContext) - 功能实例创建后立即调用,仅调用一次
  2. 启用检查: isEnabled() - 可能被频繁调用,用于判断功能是否可用
  3. 显示名称: name() - 获取功能的显示名称
  4. 配置界面 (可选):
    • 用户点击添加按钮 → buildOptionsView(PluginUI, JSONObject) - 创建配置界面
    • 用户确认配置 → getOptionsData(PluginUI, PluginView) - 获取并验证配置数据
  5. 执行功能: 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 管理器将无法识别该接口

# 相关接口