# 文本编辑器浮动菜单(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 接口,可减少样板代码。

# 生命周期

浮动菜单的典型生命周期如下:

  1. 初始化阶段: init(PluginContext) - 菜单实例创建后立即调用,仅调用一次
  2. 启用检查: isEnabled() - 可能被频繁调用,用于判断菜单是否可用
  3. 显示名称: name() - 获取菜单的显示名称
  4. 菜单图标: icon() - 获取菜单的图标
  5. 可见性检查: checkVisible(TextEditor) - 弹出浮动菜单前调用,判断是否显示该菜单项
  6. 执行操作: 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

图标获取方式:

  1. 使用内置的 Material 图标(推荐):

    return MaterialIcons.get("swap_vert");
    

    更多图标请访问:https://mt2.cn/icons

  2. 加载 Vector XML 文件:

    return VectorDrawableLoader.fromVectorXml(getContext(), "icon.xml");
    
  3. 加载 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 表示菜单隐藏

注意事项:

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

典型使用场景:

  1. 仅在选中文本时显示:

    @Override
    public boolean checkVisible(@NonNull TextEditor editor) {
        return editor.hasTextSelected();
    }
    
  2. 根据选中文本内容判断:

    @Override
    public boolean checkVisible(@NonNull TextEditor editor) {
        if (!editor.hasTextSelected()) {
            return false;
        }
        String selected = editor.getSelectedText();
        // 仅在选中的是数字时显示
        return selected.matches("\\d+");
    }
    
  3. 根据编辑器状态判断:

    @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 管理器将无法识别该接口

# 相关接口