M8Test Help

编辑器插件

18

开发编辑器插件的步骤如下:

  1. build.gradle.kts 文件中添加 m8test sdk 依赖。为减小插件 APK 的体积,如果依赖项已包含在 M8Test Version Catalog 中,建议使用 compileOnly 方式引入依赖。

import com.m8test.util.VersionUtils plugins { alias(m8test.plugins.android.application) alias(m8test.plugins.kotlin.android) alias(m8test.plugins.kotlin.compose) } android { namespace = "com.m8test.editor.language" compileSdk = m8test.versions.compileSdk.get().toInt() defaultConfig { minSdk = m8test.versions.minSdk.get().toInt() targetSdk = m8test.versions.targetSdk.get().toInt() versionName = libs.versions.versionName.get() versionCode = VersionUtils.getCode(versionName!!) testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } compileOptions { sourceCompatibility = JavaVersion.toVersion(m8test.versions.sourceCompatibility.get()) targetCompatibility = JavaVersion.toVersion(m8test.versions.targetCompatibility.get()) } kotlinOptions { jvmTarget = m8test.versions.jvmTarget.get() } buildFeatures { compose = true } } dependencies { compileOnly(platform(m8test.androidx.compose.bom)) compileOnly(m8test.bundles.compose) compileOnly(m8test.m8test.sdk) compileOnly(m8test.gson) implementation(libs.m8test.compose.widget) implementation(libs.bundles.editor.kit) }
  1. 编写插件类,继承 AbstractComposableEditorPlugin 并重写 Content 方法

package com.m8test.editor.language import android.content.res.Resources import androidx.activity.compose.LocalOnBackPressedDispatcherOwner import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.unit.dp import com.blacksquircle.ui.language.css.CssLanguage import com.blacksquircle.ui.language.groovy.GroovyLanguage import com.blacksquircle.ui.language.html.HtmlLanguage import com.blacksquircle.ui.language.ini.IniLanguage import com.blacksquircle.ui.language.java.JavaLanguage import com.blacksquircle.ui.language.javascript.JavaScriptLanguage import com.blacksquircle.ui.language.json.JsonLanguage import com.blacksquircle.ui.language.kotlin.KotlinLanguage import com.blacksquircle.ui.language.lua.LuaLanguage import com.blacksquircle.ui.language.markdown.MarkdownLanguage import com.blacksquircle.ui.language.php.PhpLanguage import com.blacksquircle.ui.language.python.PythonLanguage import com.blacksquircle.ui.language.ruby.RubyLanguage import com.blacksquircle.ui.language.toml.TomlLanguage import com.blacksquircle.ui.language.typescript.TypeScriptLanguage import com.blacksquircle.ui.language.xml.XmlLanguage import com.blacksquircle.ui.language.yaml.YamlLanguage import com.google.accompanist.imageloading.rememberDrawablePainter import com.google.gson.Gson import com.google.gson.reflect.TypeToken import com.m8test.compose.widget.AutoResizeSurfaceDialog import com.m8test.compose.widget.BackTopBarScaffold import com.m8test.editor.plugin.AbstractComposableEditorPlugin import com.m8test.editor.ui.Editor import com.m8test.plugin.api.ApkPluginProvider import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch /** * Description TODO * * @date 2024/12/18 15:43:44 * @author M8Test, [email protected], https://m8test.com */ class LanguageEditorPlugin(apkPluginProvider: ApkPluginProvider) : AbstractComposableEditorPlugin(apkPluginProvider = apkPluginProvider) { private val gson = Gson() private val defaultLanguages = listOf( LanguageWrapper(language = "groovy", extensions = mutableSetOf("groovy")), LanguageWrapper(language = "java", extensions = mutableSetOf("java", "javas")), LanguageWrapper(language = "javascript", extensions = mutableSetOf("js")), LanguageWrapper(language = "kotlin", extensions = mutableSetOf("kt", "kts")), LanguageWrapper(language = "lua", extensions = mutableSetOf("lua")), LanguageWrapper(language = "php", extensions = mutableSetOf("php")), LanguageWrapper(language = "python", extensions = mutableSetOf("py")), LanguageWrapper(language = "ruby", extensions = mutableSetOf("rb")), LanguageWrapper(language = "yaml", extensions = mutableSetOf("yaml", "yml")), LanguageWrapper(language = "json", extensions = mutableSetOf("json")), LanguageWrapper(language = "xml", extensions = mutableSetOf("xml")), LanguageWrapper(language = "typescript", extensions = mutableSetOf("ts")), LanguageWrapper(language = "markdown", extensions = mutableSetOf("md")), LanguageWrapper(language = "css", extensions = mutableSetOf("css")), LanguageWrapper(language = "html", extensions = mutableSetOf("html")), LanguageWrapper(language = "ini", extensions = mutableSetOf("ini")), LanguageWrapper(language = "toml", extensions = mutableSetOf("toml")) ) private val languages = listOf( GroovyLanguage(), JavaLanguage(), JavaScriptLanguage(), KotlinLanguage(), LuaLanguage(), PhpLanguage(), PythonLanguage(), RubyLanguage(), YamlLanguage(), JsonLanguage(), XmlLanguage(), TypeScriptLanguage(), MarkdownLanguage(), CssLanguage(), HtmlLanguage(), IniLanguage(), TomlLanguage(), ) @Composable private fun RestoreDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) { AutoResizeSurfaceDialog( onDismissRequest = onDismiss, shape = RoundedCornerShape(20.dp), content = { Column( modifier = Modifier.padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally ) { val r = [email protected]() Text( text = r.getString(R.string.text_warn), style = MaterialTheme.typography.titleLarge ) Text(text = r.getString(R.string.text_restore_language_warning)) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly ) { TextButton(onClick = onDismiss) { Text(text = r.getString(R.string.text_cancel)) } TextButton(onClick = { onDismiss() onConfirm() }) { Text(text = r.getString(R.string.text_confirm)) } } } }) } @Composable override fun Content() { val languages = remember { mutableStateListOf<LanguageWrapper>().apply { addAll(getLanguagesFromSetting()) } } val onBackPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher var canSave by remember { mutableStateOf(false) } var showRestoreDialog by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() if (showRestoreDialog) { RestoreDialog(onConfirm = { getSettings().remove("languages") scope.launch(Dispatchers.IO) { languages.clear() delay(100) languages.addAll(getLanguagesFromSetting()) } }) { showRestoreDialog = false } } BackTopBarScaffold( onBackPress = { onBackPressedDispatcher?.onBackPressed() }, titleText = [email protected]() .getString(R.string.text_editor_language_setting), actions = { IconButton(enabled = canSave, onClick = { getSettings().set("languages", gson.toJson(languages)) canSave = false }) { Icon( painter = painterResource( [email protected](), R.drawable.ic_baseline_save_24 ), contentDescription = null ) } IconButton(onClick = { showRestoreDialog = true }) { Icon( painter = painterResource( resources = [email protected](), R.drawable.ic_baseline_restore_24 ), contentDescription = null ) } }) { paddingValues -> LazyColumn( modifier = Modifier .fillMaxSize() .padding(paddingValues) ) { items(languages) { language -> LanguageItem( language = language, onValueChange = { canSave = true }, modifier = Modifier.fillMaxWidth() ) } } } } private fun getLanguagesFromSetting(): List<LanguageWrapper> { return getSettings().get("languages") ?.let { gson.fromJson(it, object : TypeToken<List<LanguageWrapper>>() {}.type) } ?: defaultLanguages.map { LanguageWrapper( language = it.language, extensions = mutableSetOf<String>().apply { addAll(it.extensions) } ) } } @Composable private fun LanguageItem( language: LanguageWrapper, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, ) { Row(modifier = modifier) { var value by remember { mutableStateOf(language.extensions.joinToString()) } OutlinedTextField( modifier = Modifier.fillMaxWidth(), label = { Text(text = language.language) }, placeholder = { val r = [email protected]() Text(text = r.getString(R.string.text_extension_placeholder)) }, value = value, onValueChange = { value = it language.extensions.clear() language.extensions.addAll(value.split(",").map { it.trim() } .filter { it.isNotBlank() }) onValueChange(it) }) } } override fun onAttached(editor: Editor) { super.onAttached(editor) val settingLanguages = getLanguagesFromSetting() editor.language = editor.file.extension.let { extension -> languages.firstOrNull { l -> settingLanguages.firstOrNull { it.language == l.languageName } ?.extensions?.contains(extension) == true } } ?: editor.scriptLanguage?.getName()?.lowercase() ?.let { name -> languages.firstOrNull { it.languageName == name } } } } @Composable private fun painterResource(resources: Resources, id: Int): Painter { return rememberDrawablePainter(drawable = resources.getDrawable(id, null)) }
  1. AndroidManifest.xml 中配置插件信息

<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"> <application android:icon="@drawable/ic_launcher"> <meta-data android:name="com.m8test.plugin.description" android:value="本插件实现了为脚本编辑器添加编程语言语法高亮显示的功能." /> <meta-data android:name="com.m8test.plugin.url" android:value="https://github.com/m8test/Plugins" /> <meta-data android:name="com.m8test.plugin.type" android:value="editor" /> <meta-data android:name="com.m8test.plugin.name" android:value="language-editor" /> <meta-data android:name="com.m8test.plugin.className" android:value="com.m8test.editor.language.LanguageEditorPlugin" /> </application> </manifest>
  • com.m8test.plugin.type: 插件类型,此处为 common

  • com.m8test.plugin.name: 插件名称,可为任意字符串

  • com.m8test.plugin.className: 插件实现类的全限定类名

Last modified: 12 June 2025