diff --git a/app/src/main/java/com/chaomixian/vflow/core/execution/WorkflowExecutor.kt b/app/src/main/java/com/chaomixian/vflow/core/execution/WorkflowExecutor.kt index 42f9f4d0..75504fef 100644 --- a/app/src/main/java/com/chaomixian/vflow/core/execution/WorkflowExecutor.kt +++ b/app/src/main/java/com/chaomixian/vflow/core/execution/WorkflowExecutor.kt @@ -22,6 +22,7 @@ import com.chaomixian.vflow.core.types.basic.VDictionary import com.chaomixian.vflow.core.types.complex.VImage import com.chaomixian.vflow.core.utils.StorageManager import com.chaomixian.vflow.core.workflow.model.ActionStep +import com.chaomixian.vflow.core.workflow.model.ActionStepExecutionSettings import com.chaomixian.vflow.core.workflow.model.Workflow import com.chaomixian.vflow.core.workflow.model.WorkflowReentryBehavior import com.chaomixian.vflow.core.workflow.module.logic.* @@ -29,7 +30,6 @@ import com.chaomixian.vflow.services.ExecutionNotificationManager import com.chaomixian.vflow.services.ExecutionNotificationState import com.chaomixian.vflow.services.ExecutionUIService import com.chaomixian.vflow.services.ServiceStateBus -import com.chaomixian.vflow.ui.workflow_editor.ActionEditorSheet import kotlinx.coroutines.* import kotlinx.coroutines.TimeoutCancellationException import java.io.File @@ -538,14 +538,15 @@ object WorkflowExecutor { DebugLogger.d("WorkflowExecutor", "[${workflow.name}][#${pc + 1}] -> 执行: ${module.metadata.name}") // --- 错误处理与重试逻辑 --- - val errorPolicy = step.parameters[ActionEditorSheet.KEY_ERROR_POLICY] as? String ?: ActionEditorSheet.POLICY_STOP - val retryCount = (step.parameters[ActionEditorSheet.KEY_RETRY_COUNT] as? Number)?.toInt() ?: 3 - val retryInterval = (step.parameters[ActionEditorSheet.KEY_RETRY_INTERVAL] as? Number)?.toLong() ?: 1000L + val executionSettings = ActionStepExecutionSettings.fromParameters(step.parameters) + val errorPolicy = executionSettings.policy + val retryCount = executionSettings.retryCount + val retryInterval = executionSettings.retryIntervalMillis var attempt = 0 var finalResult: ExecutionResult? = null - while (attempt <= retryCount || errorPolicy != ActionEditorSheet.POLICY_RETRY) { + while (attempt <= retryCount || errorPolicy != ActionStepExecutionSettings.POLICY_RETRY) { if (attempt > 0) { DebugLogger.w("WorkflowExecutor", "步骤执行失败,正在进行第 $attempt 次重试 (等待 ${retryInterval}ms)...") // 在模块内部进度更新时,也刷新通知 @@ -563,7 +564,7 @@ object WorkflowExecutor { } // 只有策略是 RETRY 且是 Failure 时才继续循环 - if (errorPolicy == ActionEditorSheet.POLICY_RETRY && finalResult is ExecutionResult.Failure) { + if (errorPolicy == ActionStepExecutionSettings.POLICY_RETRY && finalResult is ExecutionResult.Failure) { attempt++ if (attempt > retryCount) break } else { @@ -582,7 +583,7 @@ object WorkflowExecutor { is ExecutionResult.Failure -> { DebugLogger.e("WorkflowExecutor", "模块执行失败: ${result.errorTitle} - ${result.errorMessage}") - if (errorPolicy == ActionEditorSheet.POLICY_SKIP) { + if (errorPolicy == ActionStepExecutionSettings.POLICY_SKIP) { DebugLogger.w("WorkflowExecutor", "根据策略,跳过错误继续执行。") // 1. 如果模块提供了 partialOutputs,使用它们作为基础 diff --git a/app/src/main/java/com/chaomixian/vflow/core/module/definitions.kt b/app/src/main/java/com/chaomixian/vflow/core/module/definitions.kt index e39843c5..76e77711 100644 --- a/app/src/main/java/com/chaomixian/vflow/core/module/definitions.kt +++ b/app/src/main/java/com/chaomixian/vflow/core/module/definitions.kt @@ -72,12 +72,12 @@ data class EditorAction( */ abstract class CustomEditorViewHolder(val view: View) - /** * 模块用户界面提供者接口。 * 将模块的 Android View 相关逻辑从核心模块逻辑中分离出来,实现解耦。 */ interface ModuleUIProvider { + /** * 创建在工作流步骤卡片中显示的自定义预览视图。 * 如果返回 null,则通常会回退到使用模块的 getSummary() 方法。 @@ -94,7 +94,7 @@ interface ModuleUIProvider { step: ActionStep, allSteps: List, onStartActivityForResult: ((Intent, (resultCode: Int, data: Intent?) -> Unit) -> Unit)? = null - ): View? + ): View? = null /** * 创建用于编辑模块参数的自定义用户界面。 @@ -115,21 +115,27 @@ interface ModuleUIProvider { onMagicVariableRequested: ((inputId: String) -> Unit)? = null, allSteps: List? = null, onStartActivityForResult: ((Intent, (resultCode: Int, data: Intent?) -> Unit) -> Unit)? = null - ): CustomEditorViewHolder + ): CustomEditorViewHolder = object : CustomEditorViewHolder(View(context)) {} /** * 从自定义编辑器界面中读取用户输入的参数值。 * @param holder 包含编辑器视图的 CustomEditorViewHolder 实例。 * @return 一个包含参数ID和对应值的 Map。 */ - fun readFromEditor(holder: CustomEditorViewHolder): Map + fun readFromEditor(holder: CustomEditorViewHolder): Map = emptyMap() /** * 声明此 UI 提供者具体处理了哪些输入参数的界面渲染。 * 对于在此返回的输入ID,通用的参数编辑界面将不会为它们创建默认的UI控件。 * @return 一个包含已处理的输入参数ID的集合。 */ - fun getHandledInputIds(): Set + fun getHandledInputIds(): Set = emptySet() + + /** + * 是否提供独立的自定义编辑器。 + * 默认视为存在自定义编辑器;getHandledInputIds 只负责声明哪些输入由自定义 UI 接管。 + */ + fun hasCustomEditor(): Boolean = true } /** diff --git a/app/src/main/java/com/chaomixian/vflow/core/pill/PillBuilder.kt b/app/src/main/java/com/chaomixian/vflow/core/pill/PillBuilder.kt deleted file mode 100644 index 478c9259..00000000 --- a/app/src/main/java/com/chaomixian/vflow/core/pill/PillBuilder.kt +++ /dev/null @@ -1,38 +0,0 @@ -// 文件: main/java/com/chaomixian/vflow/core/pill/PillBuilder.kt -// 描述: Pill构建器接口,平台无关的抽象 -package com.chaomixian.vflow.core.pill - -/** - * Pill构建器接口(平台无关) - * - * 这个接口定义了构建包含Pill的文本结构的方法。 - * 不同的平台可以实现这个接口来提供平台特定的文本对象。 - * - * 例如: - * - Android平台可以实现返回 android.text.Spannable - * - iOS平台可以实现返回 NSAttributedString - * - Desktop平台可以实现返回富文本对象 - * - * 核心模块只依赖这个接口,不依赖任何平台特定的实现。 - */ -interface PillBuilder { - /** - * 构建包含Pill的文本结构 - * - * 此方法接受混合的文本片段和Pill对象,返回平台特定的文本对象。 - * - * @param parts 文本片段和Pill的混合序列 - * - String: 普通文本 - * - Pill: Pill对象(包含text, parameterId等) - * @return 平台特定的文本对象(如Android的Spannable) - * - * 示例: - * ``` - * val pill = Pill("{{step1.text}}", "input1") - * val result = builder.build("如果 ", pill, " 等于 ", "test") - * // Android平台返回 Spannable - * // iOS平台返回 NSAttributedString - * ``` - */ - fun build(vararg parts: Any): Any -} diff --git a/app/src/main/java/com/chaomixian/vflow/core/workflow/model/ActionStepExecutionSettings.kt b/app/src/main/java/com/chaomixian/vflow/core/workflow/model/ActionStepExecutionSettings.kt new file mode 100644 index 00000000..49686c99 --- /dev/null +++ b/app/src/main/java/com/chaomixian/vflow/core/workflow/model/ActionStepExecutionSettings.kt @@ -0,0 +1,29 @@ +package com.chaomixian.vflow.core.workflow.model + +data class ActionStepExecutionSettings( + val policy: String = POLICY_STOP, + val retryCount: Int = DEFAULT_RETRY_COUNT, + val retryIntervalMillis: Long = DEFAULT_RETRY_INTERVAL_MS +) { + companion object { + const val KEY_ERROR_POLICY = "__error_policy" + const val KEY_RETRY_COUNT = "__retry_count" + const val KEY_RETRY_INTERVAL = "__retry_interval" + + const val POLICY_STOP = "STOP" + const val POLICY_SKIP = "SKIP" + const val POLICY_RETRY = "RETRY" + + const val DEFAULT_RETRY_COUNT = 3 + const val DEFAULT_RETRY_INTERVAL_MS = 1000L + + fun fromParameters(parameters: Map): ActionStepExecutionSettings { + return ActionStepExecutionSettings( + policy = parameters[KEY_ERROR_POLICY] as? String ?: POLICY_STOP, + retryCount = (parameters[KEY_RETRY_COUNT] as? Number)?.toInt() ?: DEFAULT_RETRY_COUNT, + retryIntervalMillis = (parameters[KEY_RETRY_INTERVAL] as? Number)?.toLong() + ?: DEFAULT_RETRY_INTERVAL_MS + ) + } + } +} diff --git a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/core/CoreScreenOperationModuleUIProvider.kt b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/core/CoreScreenOperationModuleUIProvider.kt index 0dbe13f9..feb26592 100644 --- a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/core/CoreScreenOperationModuleUIProvider.kt +++ b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/core/CoreScreenOperationModuleUIProvider.kt @@ -191,7 +191,7 @@ class CoreScreenOperationModuleUIProvider : ModuleUIProvider { if (valStr.isMagicVariable() || valStr.isNamedVariable()) { // 变量药丸(支持魔法变量和命名变量) val pill = LayoutInflater.from(context).inflate(R.layout.magic_variable_pill, valueContainer, false) - val displayName = PillRenderer.getDisplayNameForVariableReference(valStr!!, allSteps ?: emptyList(), context) + val displayName = PillRenderer.resolveDisplayName(context, valStr!!, allSteps ?: emptyList()) pill.findViewById(R.id.pill_text).text = displayName pill.setOnClickListener { onMagicReq?.invoke(inputDef.id) } valueContainer.addView(pill) diff --git a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/data/CommentModule.kt b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/data/CommentModule.kt index ff4f9cd9..86fc8218 100644 --- a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/data/CommentModule.kt +++ b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/data/CommentModule.kt @@ -1,16 +1,11 @@ package com.chaomixian.vflow.core.workflow.module.data import android.content.Context -import android.content.Intent -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView import com.chaomixian.vflow.R import com.chaomixian.vflow.core.execution.ExecutionContext import com.chaomixian.vflow.core.module.* import com.chaomixian.vflow.core.workflow.model.ActionStep -import com.chaomixian.vflow.ui.workflow_editor.PillRenderer +import com.chaomixian.vflow.ui.workflow_editor.PillUtil class CommentModule : BaseModule() { override val id = "vflow.data.comment" @@ -24,8 +19,6 @@ class CommentModule : BaseModule() { categoryId = "data" ) - override val uiProvider: ModuleUIProvider? = CommentUIProvider() - override fun getInputs(): List = listOf( InputDefinition( id = "content", @@ -42,7 +35,14 @@ class CommentModule : BaseModule() { override fun getOutputs(step: ActionStep?): List = emptyList() override fun getSummary(context: Context, step: ActionStep): CharSequence { - return context.getString(R.string.module_vflow_data_comment_name) + return PillUtil.buildSpannable( + context, + context.getString(R.string.module_vflow_data_comment_name), + PillUtil.richTextPreview( + rawText = step.parameters["content"]?.toString(), + onlyWhenComplex = false + ) + ) } override suspend fun execute( @@ -53,44 +53,3 @@ class CommentModule : BaseModule() { return ExecutionResult.Success(emptyMap()) } } - -/** - * 注释模块的UI提供器,使用默认的富文本输入框 - * 同时重写createPreview以始终显示预览(无论内容是否复杂) - */ -class CommentUIProvider : ModuleUIProvider { - - private val innerProvider = com.chaomixian.vflow.ui.workflow_editor.RichTextUIProvider("content") - - override fun getHandledInputIds(): Set = emptySet() - - override fun createEditor( - context: Context, parent: ViewGroup, currentParameters: Map, - onParametersChanged: () -> Unit, onMagicVariableRequested: ((String) -> Unit)?, - allSteps: List?, onStartActivityForResult: ((Intent, (Int, Intent?) -> Unit) -> Unit)? - ): CustomEditorViewHolder { - // 返回一个空的 ViewHolder,让系统使用默认的富文本输入框 - return object : CustomEditorViewHolder(View(context)) {} - } - - override fun readFromEditor(holder: CustomEditorViewHolder): Map = emptyMap() - - /** - * 始终创建预览视图,无论内容是否复杂 - */ - override fun createPreview( - context: Context, parent: ViewGroup, step: ActionStep, allSteps: List, - onStartActivityForResult: ((Intent, (resultCode: Int, data: Intent?) -> Unit) -> Unit)? - ): View? { - val rawText = step.parameters["content"]?.toString() ?: "" - - val inflater = LayoutInflater.from(context) - val previewView = inflater.inflate(R.layout.partial_rich_text_preview, parent, false) - val textView = previewView.findViewById(R.id.rich_text_preview_content) - - val spannable = PillRenderer.renderRichTextToSpannable(context, rawText, allSteps) - textView.text = spannable - - return previewView - } -} diff --git a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/data/CreateVariableModule.kt b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/data/CreateVariableModule.kt index 4c3d2c5e..7d7b95a5 100644 --- a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/data/CreateVariableModule.kt +++ b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/data/CreateVariableModule.kt @@ -156,18 +156,27 @@ class CreateVariableModule : BaseModule() { // 优先检查是否为"复杂内容"。 // 如果是复杂的(包含多个变量或混合文本),则只返回简单标题。 - // RichTextUIProvider 会负责渲染预览视图。 + // 复杂字符串值会由预览层补充展示。 if (type == TYPE_STRING && VariableResolver.isComplex(rawText)) { return if (name.isNullOrBlank()) { - context.getString(R.string.summary_vflow_data_create_anon, typeLabel, "") + PillUtil.buildSpannable( + context, + context.getString(R.string.summary_vflow_data_create_anon, typeLabel, ""), + PillUtil.richTextPreview(rawText) + ) } else { val namePill = PillUtil.Pill("[[$name]]", "variableName") - PillUtil.buildSpannable(context, context.getString(R.string.summary_vflow_data_create_variable, "", typeLabel), namePill) + PillUtil.buildSpannable( + context, + context.getString(R.string.summary_vflow_data_create_variable, "", typeLabel), + namePill, + PillUtil.richTextPreview(rawText) + ) } } // 如果是字典、列表或坐标,且不是单纯的变量引用 - // 此时 VariableValueUIProvider 会显示详细预览列表,摘要中隐藏 value pill 以防重复 + // 此时内部的列表/字典预览会显示详细内容,摘要中隐藏 value pill 以防重复 if ((type == TYPE_DICTIONARY || type == TYPE_LIST || type == TYPE_COORDINATE) && !rawText.isMagicVariable() && !rawText.isNamedVariable()) { return buildSimpleSummary(context, name, type) } diff --git a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/data/ModifyVariableModule.kt b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/data/ModifyVariableModule.kt index 9feffc4a..e5605936 100644 --- a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/data/ModifyVariableModule.kt +++ b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/data/ModifyVariableModule.kt @@ -10,7 +10,6 @@ import com.chaomixian.vflow.core.types.VTypeRegistry import com.chaomixian.vflow.core.types.basic.VNull import com.chaomixian.vflow.core.workflow.model.ActionStep import com.chaomixian.vflow.ui.workflow_editor.PillUtil -import com.chaomixian.vflow.ui.workflow_editor.RichTextUIProvider class ModifyVariableModule : BaseModule() { override val id = "vflow.variable.modify" @@ -35,8 +34,6 @@ class ModifyVariableModule : BaseModule() { requiredInputIds = setOf("variable", "newValue") ) - override val uiProvider: ModuleUIProvider? = RichTextUIProvider("newValue") - override fun getInputs(): List = listOf( InputDefinition( id = "variable", // ID 从 "variableName" 改为 "variable" @@ -67,12 +64,12 @@ class ModifyVariableModule : BaseModule() { getInputs().find { it.id == "variable" } ) - // 如果新值是复杂内容,RichTextUIProvider 会显示预览,这里只显示变量名 + // 如果新值是复杂内容,预览层会显示富文本内容,这里只显示变量名 val rawValue = step.parameters["newValue"]?.toString() ?: "" if (VariableResolver.isComplex(rawValue)) { val prefix = context.getString(R.string.summary_vflow_variable_modify_prefix) val middle = context.getString(R.string.summary_vflow_variable_modify_middle) - return PillUtil.buildSpannable(context, prefix, namePill, middle) + return PillUtil.buildSpannable(context, prefix, namePill, middle, PillUtil.richTextPreview(rawValue)) } val valuePill = PillUtil.createPillFromParam( diff --git a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/data/ParseJsonModule.kt b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/data/ParseJsonModule.kt index db73239d..f3c5c9e7 100644 --- a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/data/ParseJsonModule.kt +++ b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/data/ParseJsonModule.kt @@ -12,7 +12,6 @@ import com.chaomixian.vflow.core.types.basic.VList import com.chaomixian.vflow.core.types.basic.VNull import com.chaomixian.vflow.core.workflow.model.ActionStep import com.chaomixian.vflow.ui.workflow_editor.PillUtil -import com.chaomixian.vflow.ui.workflow_editor.RichTextUIProvider import org.json.JSONArray import org.json.JSONObject @@ -66,8 +65,6 @@ class ParseJsonModule : BaseModule() { ) ) - override val uiProvider: ModuleUIProvider? = RichTextUIProvider("json") - override fun getOutputs(step: ActionStep?): List = listOf( OutputDefinition( id = "first_value", @@ -99,7 +96,8 @@ class ParseJsonModule : BaseModule() { context, "使用", pathPill, - "解析 JSON" + "解析 JSON", + PillUtil.richTextPreview(rawJson) ) } diff --git a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/data/ParseXmlModule.kt b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/data/ParseXmlModule.kt index d2b76de9..0e8fb080 100644 --- a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/data/ParseXmlModule.kt +++ b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/data/ParseXmlModule.kt @@ -19,7 +19,6 @@ import com.chaomixian.vflow.core.types.VTypeRegistry import com.chaomixian.vflow.core.types.basic.VList import com.chaomixian.vflow.core.workflow.model.ActionStep import com.chaomixian.vflow.ui.workflow_editor.PillUtil -import com.chaomixian.vflow.ui.workflow_editor.RichTextUIProvider import org.w3c.dom.Node import org.w3c.dom.NodeList import org.xml.sax.InputSource @@ -53,8 +52,6 @@ class ParseXmlModule : BaseModule() { requiredInputIds = setOf("xml", "xpath") ) - override val uiProvider: ModuleUIProvider? = RichTextUIProvider("xml") - override fun getInputs(): List = listOf( InputDefinition( id = "xml", @@ -101,7 +98,7 @@ class ParseXmlModule : BaseModule() { ) if (VariableResolver.isComplex(rawXml)) { - return PillUtil.buildSpannable(context, "使用", xpathPill, "解析 XML") + return PillUtil.buildSpannable(context, "使用", xpathPill, "解析 XML", PillUtil.richTextPreview(rawXml)) } val xmlPill = PillUtil.createPillFromParam( diff --git a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/data/VariableModuleUIProvider.kt b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/data/VariableModuleUIProvider.kt index b4410107..d6441181 100644 --- a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/data/VariableModuleUIProvider.kt +++ b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/data/VariableModuleUIProvider.kt @@ -23,7 +23,6 @@ import com.chaomixian.vflow.ui.workflow_editor.ListItemAdapter import com.chaomixian.vflow.ui.workflow_editor.PillUtil import com.chaomixian.vflow.ui.workflow_editor.RichTextView import com.chaomixian.vflow.ui.workflow_editor.PillRenderer -import com.chaomixian.vflow.ui.workflow_editor.RichTextUIProvider import com.chaomixian.vflow.ui.workflow_editor.StandardControlFactory import com.chaomixian.vflow.ui.workflow_editor.VariableValueUIProvider import com.google.android.material.materialswitch.MaterialSwitch @@ -52,8 +51,7 @@ class VariableModuleUIProvider( ) : ModuleUIProvider { // 实例化内部需要用到的 UI 提供者 - private val richTextUIProvider = RichTextUIProvider("value") - private val variableValueUIProvider = VariableValueUIProvider() + private val variableValueUIProvider = VariableValueUIProvider private fun getTypeLabels(context: Context): List { return typeOptions.map { CreateVariableModule.getTypeLabel(context, it) } @@ -78,13 +76,12 @@ class VariableModuleUIProvider( ?: CreateVariableModule.TYPE_STRING // 根据变量类型,委托给相应的 UIProvider 来创建预览 return when (type) { - CreateVariableModule.TYPE_STRING -> richTextUIProvider.createPreview(context, parent, step, allSteps, onStartActivityForResult) + CreateVariableModule.TYPE_STRING -> null CreateVariableModule.TYPE_DICTIONARY, CreateVariableModule.TYPE_LIST -> variableValueUIProvider.createPreview(context, parent, step, allSteps, onStartActivityForResult) else -> null // 其他类型(如数字、布尔)不需要自定义预览,返回null即可 } } - override fun createEditor( context: Context, parent: ViewGroup, @@ -248,7 +245,7 @@ class VariableModuleUIProvider( if (type != CreateVariableModule.TYPE_STRING && currentValue is String && (currentValue.isMagicVariable() || currentValue.isNamedVariable())) { val pill = LayoutInflater.from(context).inflate(R.layout.magic_variable_pill, holder.valueContainer, false) val pillText = pill.findViewById(R.id.pill_text) - pillText.text = PillRenderer.getDisplayNameForVariableReference(currentValue, holder.allSteps ?: emptyList(), context) + pillText.text = PillRenderer.resolveDisplayName(context, currentValue, holder.allSteps ?: emptyList()) // 允许点击药丸重新选择(包括“清除”以恢复列表编辑) pill.setOnClickListener { @@ -381,7 +378,7 @@ class VariableModuleUIProvider( // 显示变量药丸 val pill = LayoutInflater.from(context).inflate(R.layout.magic_variable_pill, xValueContainer, false) val pillText = pill.findViewById(R.id.pill_text) - pillText.text = PillRenderer.getDisplayNameForVariableReference(currentX, holder.allSteps ?: emptyList(), context) + pillText.text = PillRenderer.resolveDisplayName(context, currentX, holder.allSteps ?: emptyList()) pill.setOnClickListener { holder.onMagicVariableRequested?.invoke("value.x") } @@ -422,7 +419,7 @@ class VariableModuleUIProvider( // 显示变量药丸 val pill = LayoutInflater.from(context).inflate(R.layout.magic_variable_pill, yValueContainer, false) val pillText = pill.findViewById(R.id.pill_text) - pillText.text = PillRenderer.getDisplayNameForVariableReference(currentY, holder.allSteps ?: emptyList(), context) + pillText.text = PillRenderer.resolveDisplayName(context, currentY, holder.allSteps ?: emptyList()) pill.setOnClickListener { holder.onMagicVariableRequested?.invoke("value.y") } diff --git a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/interaction/AgentModule.kt b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/interaction/AgentModule.kt index b6dda7a5..468076f9 100644 --- a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/interaction/AgentModule.kt +++ b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/interaction/AgentModule.kt @@ -169,7 +169,11 @@ class AgentModule : BaseModule() { // 如果指令复杂,只显示标题 if (VariableResolver.isComplex(rawInstruction)) { - return prefix + metadata.getLocalizedName(context) + return PillUtil.buildSpannable( + context, + prefix + metadata.getLocalizedName(context), + PillUtil.richTextPreview(rawInstruction) + ) } val instructionPill = PillUtil.createPillFromParam(step.parameters["instruction"], getInputs().find { it.id == "instruction" }) diff --git a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/interaction/AgentModuleUIProvider.kt b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/interaction/AgentModuleUIProvider.kt index 1d81b8a9..bae8df49 100644 --- a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/interaction/AgentModuleUIProvider.kt +++ b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/interaction/AgentModuleUIProvider.kt @@ -16,9 +16,6 @@ import com.chaomixian.vflow.R import com.chaomixian.vflow.core.module.CustomEditorViewHolder import com.chaomixian.vflow.core.module.ModuleUIProvider import com.chaomixian.vflow.core.workflow.model.ActionStep -import com.chaomixian.vflow.ui.workflow_editor.PillRenderer -import com.chaomixian.vflow.ui.workflow_editor.PillUtil -import com.chaomixian.vflow.ui.workflow_editor.RichTextUIProvider import com.chaomixian.vflow.ui.workflow_editor.RichTextView import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipGroup @@ -32,8 +29,6 @@ class AgentModuleUIProvider : ModuleUIProvider { private const val PROVIDER_CUSTOM = "custom" } - private val richTextUIProvider = RichTextUIProvider("instruction") - class ViewHolder(view: View) : CustomEditorViewHolder(view) { val providerGroup: ChipGroup = view.findViewById(R.id.cg_provider) val chipBigModel: Chip = view.findViewById(R.id.chip_bigmodel) @@ -57,13 +52,6 @@ class AgentModuleUIProvider : ModuleUIProvider { "provider", "base_url", "model", "api_key", "instruction", "max_steps" ) - override fun createPreview( - context: Context, parent: ViewGroup, step: ActionStep, allSteps: List, - onStartActivityForResult: ((Intent, (resultCode: Int, data: Intent?) -> Unit) -> Unit)? - ): View? { - return richTextUIProvider.createPreview(context, parent, step, allSteps, onStartActivityForResult) - } - override fun createEditor( context: Context, parent: ViewGroup, diff --git a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/interaction/AutoGLMModule.kt b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/interaction/AutoGLMModule.kt index 871497f6..0451c4bc 100644 --- a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/interaction/AutoGLMModule.kt +++ b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/interaction/AutoGLMModule.kt @@ -133,7 +133,11 @@ class AutoGLMModule : BaseModule() { // 如果指令复杂,只显示标题,避免与预览框重复 if (VariableResolver.isComplex(rawInstruction)) { - return prefix + metadata.getLocalizedName(context) + return PillUtil.buildSpannable( + context, + prefix + metadata.getLocalizedName(context), + PillUtil.richTextPreview(rawInstruction) + ) } val instructionPill = PillUtil.createPillFromParam(step.parameters["instruction"], getInputs().find { it.id == "instruction" }) diff --git a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/interaction/AutoGLMModuleUIProvider.kt b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/interaction/AutoGLMModuleUIProvider.kt index bc16644e..312c3237 100644 --- a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/interaction/AutoGLMModuleUIProvider.kt +++ b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/interaction/AutoGLMModuleUIProvider.kt @@ -15,9 +15,6 @@ import com.chaomixian.vflow.R import com.chaomixian.vflow.core.module.CustomEditorViewHolder import com.chaomixian.vflow.core.module.ModuleUIProvider import com.chaomixian.vflow.core.workflow.model.ActionStep -import com.chaomixian.vflow.ui.workflow_editor.PillRenderer -import com.chaomixian.vflow.ui.workflow_editor.PillUtil -import com.chaomixian.vflow.ui.workflow_editor.RichTextUIProvider import com.chaomixian.vflow.ui.workflow_editor.RichTextView import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipGroup @@ -33,8 +30,6 @@ class AutoGLMModuleUIProvider : ModuleUIProvider { private const val PROVIDER_CUSTOM = "custom" } - private val richTextUIProvider = RichTextUIProvider("instruction") - class ViewHolder(view: View) : CustomEditorViewHolder(view) { val providerGroup: ChipGroup = view.findViewById(R.id.cg_provider) val chipBigModel: Chip = view.findViewById(R.id.chip_bigmodel) // 智谱 @@ -58,13 +53,6 @@ class AutoGLMModuleUIProvider : ModuleUIProvider { "provider", "base_url", "model", "api_key", "instruction", "max_steps" ) - override fun createPreview( - context: Context, parent: ViewGroup, step: ActionStep, allSteps: List, - onStartActivityForResult: ((Intent, (resultCode: Int, data: Intent?) -> Unit) -> Unit)? - ): View? { - return richTextUIProvider.createPreview(context, parent, step, allSteps, onStartActivityForResult) - } - override fun createEditor( context: Context, parent: ViewGroup, diff --git a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/interaction/InputTextModule.kt b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/interaction/InputTextModule.kt index f698aa70..9ed01a4a 100644 --- a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/interaction/InputTextModule.kt +++ b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/interaction/InputTextModule.kt @@ -159,11 +159,23 @@ class InputTextModule : BaseModule() { // 如果内容复杂(包含变量或长文本),只返回简单文本标题 if (VariableResolver.isComplex(rawText)) { return if (mode == MODE_AUTO) { - context.getString(R.string.summary_vflow_interaction_input_text) + PillUtil.buildSpannable( + context, + context.getString(R.string.summary_vflow_interaction_input_text), + PillUtil.richTextPreview(rawText) + ) } else { - context.getString(R.string.summary_vflow_interaction_input_text_with_mode_prefix) + - modeInput.getLocalizedOptions(context)[modeInput.options.indexOf(mode).coerceAtLeast(0)] + - context.getString(R.string.summary_vflow_interaction_input_text_with_mode_middle) + val modePill = PillUtil.createPillFromParam(rawMode, modeInput) + val prefix = context.getString(R.string.summary_vflow_interaction_input_text_with_mode_prefix) + val middle = context.getString(R.string.summary_vflow_interaction_input_text_with_mode_middle) + PillUtil.buildSpannable( + context, + prefix, + modePill, + " ", + middle, + PillUtil.richTextPreview(rawText) + ) } } diff --git a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/interaction/InputTextModuleUIProvider.kt b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/interaction/InputTextModuleUIProvider.kt index 34fad5e7..a0096564 100644 --- a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/interaction/InputTextModuleUIProvider.kt +++ b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/interaction/InputTextModuleUIProvider.kt @@ -15,15 +15,12 @@ import com.chaomixian.vflow.R import com.chaomixian.vflow.core.module.CustomEditorViewHolder import com.chaomixian.vflow.core.module.ModuleUIProvider import com.chaomixian.vflow.core.workflow.model.ActionStep -import com.chaomixian.vflow.ui.workflow_editor.RichTextUIProvider import com.chaomixian.vflow.ui.workflow_editor.RichTextView import com.chaomixian.vflow.ui.workflow_editor.StandardControlFactory import com.google.android.material.textfield.TextInputLayout class InputTextModuleUIProvider : ModuleUIProvider { - private val richTextUIProvider = RichTextUIProvider("text") - class ViewHolder(view: View) : CustomEditorViewHolder(view) { val textContainer: ViewGroup = view.findViewById(R.id.container_text_input) val advancedHeader: LinearLayout = view.findViewById(R.id.layout_advanced_header) @@ -38,13 +35,6 @@ class InputTextModuleUIProvider : ModuleUIProvider { override fun getHandledInputIds(): Set = setOf("text", "mode", "action_after", "show_advanced") - override fun createPreview( - context: Context, parent: ViewGroup, step: ActionStep, allSteps: List, - onStartActivityForResult: ((Intent, (resultCode: Int, data: Intent?) -> Unit) -> Unit)? - ): View? { - return richTextUIProvider.createPreview(context, parent, step, allSteps, onStartActivityForResult) - } - override fun createEditor( context: Context, parent: ViewGroup, diff --git a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/interaction/OperitModule.kt b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/interaction/OperitModule.kt index 92f69330..3b6351fd 100644 --- a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/interaction/OperitModule.kt +++ b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/interaction/OperitModule.kt @@ -12,7 +12,6 @@ import com.chaomixian.vflow.core.types.VTypeRegistry import com.chaomixian.vflow.core.types.basic.* import com.chaomixian.vflow.core.workflow.model.ActionStep import com.chaomixian.vflow.ui.workflow_editor.PillUtil -import com.chaomixian.vflow.ui.workflow_editor.RichTextUIProvider import kotlinx.coroutines.suspendCancellableCoroutine import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException @@ -51,8 +50,6 @@ class OperitModule : BaseModule() { private val modes = listOf(MODE_CHAT, MODE_WORKFLOW) - override val uiProvider: ModuleUIProvider? = RichTextUIProvider("message") - override fun getInputs(): List = listOf( // 通用参数 InputDefinition( @@ -195,15 +192,19 @@ class OperitModule : BaseModule() { OutputDefinition("ai_response", "AI回复", VTypeRegistry.STRING.id, nameStringRes = R.string.output_vflow_interaction_operit_ai_response_name), OutputDefinition("chat_id", "对话ID", VTypeRegistry.STRING.id, nameStringRes = R.string.output_vflow_interaction_operit_chat_id_name), OutputDefinition("error", "错误信息", VTypeRegistry.STRING.id, nameStringRes = R.string.output_vflow_interaction_operit_error_name) - ) + ) override fun getSummary(context: Context, step: ActionStep): CharSequence { val mode = step.parameters["mode"] as? String ?: MODE_CHAT - // 使用 RichTextUIProvider 时,getSummary 只返回简单标题 + // 复杂消息由预览层单独展示,这里只返回简单标题 // 富文本预览会自动显示在下方 return when (mode) { - MODE_CHAT -> context.getString(R.string.summary_vflow_interaction_operit_send) + MODE_CHAT -> PillUtil.buildSpannable( + context, + context.getString(R.string.summary_vflow_interaction_operit_send), + PillUtil.richTextPreview(step.parameters["message"]?.toString()) + ) MODE_WORKFLOW -> { val action = step.parameters["workflow_action"] as? String ?: ACTION_TRIGGER_WORKFLOW val actionPill = PillUtil.createPillFromParam(action, getInputs().find { it.id == "workflow_action" }) diff --git a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/interaction/ScreenOperationModuleUIProvider.kt b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/interaction/ScreenOperationModuleUIProvider.kt index b7a12905..4ee5855d 100644 --- a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/interaction/ScreenOperationModuleUIProvider.kt +++ b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/interaction/ScreenOperationModuleUIProvider.kt @@ -221,7 +221,7 @@ class ScreenOperationModuleUIProvider : ModuleUIProvider { if (valStr.isMagicVariable() || valStr.isNamedVariable()) { // 变量药丸(支持魔法变量和命名变量) val pill = LayoutInflater.from(context).inflate(R.layout.magic_variable_pill, valueContainer, false) - val displayName = PillRenderer.getDisplayNameForVariableReference(valStr!!, allSteps ?: emptyList(), context) + val displayName = PillRenderer.resolveDisplayName(context, valStr!!, allSteps ?: emptyList()) pill.findViewById(R.id.pill_text).text = displayName pill.setOnClickListener { onMagicReq?.invoke(inputDef.id) } valueContainer.addView(pill) diff --git a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/network/AIModule.kt b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/network/AIModule.kt index 5d7c875c..b77633a4 100644 --- a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/network/AIModule.kt +++ b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/network/AIModule.kt @@ -90,12 +90,11 @@ class AIModule : BaseModule() { val providerDisplay = getProviderDisplayName(context, provider) // 如果内容是"复杂"的(多变量或长文本),VariableResolver.isComplex 会返回 true - // 此时我们由 createPreview 显示大预览框,摘要只显示简单标题 if (VariableResolver.isComplex(promptText)) { val providerPill = PillUtil.createPillFromParam(step.parameters["provider"], PROVIDER_INPUT_DEFINITION, isModuleOption = true) val prefix = context.getString(R.string.summary_vflow_network_ai_prefix) val suffix = context.getString(R.string.summary_vflow_network_ai_middle) - return PillUtil.buildSpannable(context, prefix, providerPill, suffix) + return PillUtil.buildSpannable(context, prefix, providerPill, suffix, PillUtil.richTextPreview(promptText)) } else { // 如果内容简单(单变量或短文本),显示完整的摘要(包含 Prompt 药丸) val promptPill = PillUtil.createPillFromParam(step.parameters["prompt"], getInputs().find { it.id == "prompt" }) diff --git a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/network/AIModuleUIProvider.kt b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/network/AIModuleUIProvider.kt index ff26d8b0..8e843d19 100644 --- a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/network/AIModuleUIProvider.kt +++ b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/network/AIModuleUIProvider.kt @@ -18,9 +18,6 @@ import com.chaomixian.vflow.R import com.chaomixian.vflow.core.module.CustomEditorViewHolder import com.chaomixian.vflow.core.module.ModuleUIProvider import com.chaomixian.vflow.core.workflow.model.ActionStep -import com.chaomixian.vflow.ui.workflow_editor.PillRenderer -import com.chaomixian.vflow.ui.workflow_editor.PillUtil -import com.chaomixian.vflow.ui.workflow_editor.RichTextUIProvider import com.chaomixian.vflow.ui.workflow_editor.RichTextView import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipGroup @@ -29,9 +26,6 @@ import com.google.android.material.textfield.TextInputEditText import java.util.Locale class AIModuleUIProvider : ModuleUIProvider { - // 实例化 RichTextUIProvider,指定要预览的字段为 "prompt" - private val richTextUIProvider = RichTextUIProvider("prompt") - class ViewHolder(view: View) : CustomEditorViewHolder(view) { val providerGroup: ChipGroup = view.findViewById(R.id.cg_provider) val chipOpenAI: Chip = view.findViewById(R.id.chip_openai) @@ -60,14 +54,6 @@ class AIModuleUIProvider : ModuleUIProvider { "provider", "api_key", "base_url", "model", "prompt", "system_prompt", "temperature", "show_advanced" ) - // 实现 createPreview,委托给 richTextUIProvider - override fun createPreview( - context: Context, parent: ViewGroup, step: ActionStep, allSteps: List, - onStartActivityForResult: ((Intent, (resultCode: Int, data: Intent?) -> Unit) -> Unit)? - ): View? { - return richTextUIProvider.createPreview(context, parent, step, allSteps, onStartActivityForResult) - } - override fun createEditor( context: Context, parent: ViewGroup, diff --git a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/network/HttpRequestModule.kt b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/network/HttpRequestModule.kt index 7eb2220f..9528815f 100644 --- a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/network/HttpRequestModule.kt +++ b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/network/HttpRequestModule.kt @@ -136,8 +136,13 @@ class HttpRequestModule : BaseModule() { // 检查 URL 是否为复杂内容(包含变量或其他文本的组合) if (VariableResolver.isComplex(rawUrl)) { - // 复杂内容:只显示方法和模块名称,详细内容由 UIProvider 在预览中显示 - return PillUtil.buildSpannable(context, method, " ", metadata.name) + return PillUtil.buildSpannable( + context, + method, + " ", + metadata.name, + PillUtil.richTextPreview(rawUrl) + ) } // 简单内容:显示完整的摘要(带药丸) diff --git a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/network/HttpRequestModuleUIProvider.kt b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/network/HttpRequestModuleUIProvider.kt index a04f75cd..c22846a5 100644 --- a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/network/HttpRequestModuleUIProvider.kt +++ b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/network/HttpRequestModuleUIProvider.kt @@ -16,11 +16,8 @@ import com.chaomixian.vflow.core.module.CustomEditorViewHolder import com.chaomixian.vflow.core.module.InputDefinition import com.chaomixian.vflow.core.module.ModuleUIProvider import com.chaomixian.vflow.core.module.ParameterType -import com.chaomixian.vflow.core.types.VTypeRegistry import com.chaomixian.vflow.core.workflow.model.ActionStep import com.chaomixian.vflow.ui.workflow_editor.DictionaryKVAdapter -import com.chaomixian.vflow.ui.workflow_editor.PillRenderer -import com.chaomixian.vflow.ui.workflow_editor.PillUtil import com.chaomixian.vflow.ui.workflow_editor.RichTextView import com.chaomixian.vflow.ui.workflow_editor.StandardControlFactory import com.google.android.material.textfield.TextInputLayout @@ -74,28 +71,6 @@ class HttpRequestModuleUIProvider : ModuleUIProvider { // URL 不在这里处理,让它使用标准控件 override fun getHandledInputIds(): Set = setOf("method", "headers", "query_params", "body_type", "body", "show_advanced") - override fun createPreview( - context: Context, - parent: ViewGroup, - step: ActionStep, - allSteps: List, - onStartActivityForResult: ((Intent, (resultCode: Int, data: Intent?) -> Unit) -> Unit)? - ): View? { - // 为复杂的 URL 创建富文本预览框 - val rawUrl = step.parameters["url"]?.toString() ?: "" - if (VariableResolver.isComplex(rawUrl)) { - val inflater = LayoutInflater.from(context) - val previewView = inflater.inflate(R.layout.partial_rich_text_preview, parent, false) - val textView = previewView.findViewById(R.id.rich_text_preview_content) - - val spannable = PillRenderer.renderRichTextToSpannable(context, rawUrl, allSteps) - textView.text = spannable - - return previewView - } - return null - } - override fun createEditor( context: Context, parent: ViewGroup, @@ -252,11 +227,6 @@ class HttpRequestModuleUIProvider : ModuleUIProvider { richTextView.setRichText(currentValue?.toString() ?: "", holder.allSteps ?: emptyList()) - // 为"文件"类型添加类型过滤,只允许图片类型变量 - if (bodyType == BODY_TYPE_FILE) { - richTextView.setVariableTypeFilter(setOf(VTypeRegistry.IMAGE.id)) - } - richTextView.tag = "body" holder.rawBodyRichTextView = richTextView diff --git a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/system/QuickViewModule.kt b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/system/QuickViewModule.kt index 486cfc20..af5258ea 100644 --- a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/system/QuickViewModule.kt +++ b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/system/QuickViewModule.kt @@ -21,7 +21,6 @@ import com.chaomixian.vflow.core.workflow.model.ActionStep import com.chaomixian.vflow.permissions.PermissionManager import com.chaomixian.vflow.services.ExecutionUIService import com.chaomixian.vflow.ui.workflow_editor.PillUtil -import com.chaomixian.vflow.ui.workflow_editor.RichTextUIProvider /** * “快速查看”模块。 @@ -53,9 +52,6 @@ class QuickViewModule : BaseModule() { // 此模块需要悬浮窗权限才能显示UI override val requiredPermissions = listOf(PermissionManager.OVERLAY) - // 使用 RichTextUIProvider 提供富文本预览 - override val uiProvider: ModuleUIProvider? = RichTextUIProvider("content") - /** * 定义模块的输入参数。 */ @@ -91,8 +87,11 @@ class QuickViewModule : BaseModule() { // 检查内容是否为复杂内容(包含变量或其他文本的组合) if (VariableResolver.isComplex(rawContent)) { - // 复杂内容:只显示模块名称,详细内容由 UIProvider 在预览中显示 - return metadata.getLocalizedName(context) + return PillUtil.buildSpannable( + context, + metadata.getLocalizedName(context), + PillUtil.richTextPreview(rawContent) + ) } // 简单内容:显示完整的摘要(带药丸) diff --git a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/system/ToastModule.kt b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/system/ToastModule.kt index 61a5c8b5..9e28a19b 100644 --- a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/system/ToastModule.kt +++ b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/system/ToastModule.kt @@ -13,7 +13,6 @@ import com.chaomixian.vflow.core.types.basic.VString import com.chaomixian.vflow.core.workflow.model.ActionStep import com.chaomixian.vflow.permissions.PermissionManager import com.chaomixian.vflow.ui.workflow_editor.PillUtil -import com.chaomixian.vflow.ui.workflow_editor.RichTextUIProvider import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -42,7 +41,6 @@ class ToastModule : BaseModule() { requiredInputIds = setOf("message") ) - override val uiProvider: ModuleUIProvider? = RichTextUIProvider("message") override val requiredPermissions = listOf(PermissionManager.NOTIFICATIONS) override fun getInputs(): List = listOf( @@ -73,7 +71,11 @@ class ToastModule : BaseModule() { // [修改] 直接使用 VariableResolver 的通用判断逻辑,删除本地冗余代码 if (VariableResolver.isComplex(rawText)) { - return metadata.getLocalizedName(context) + return PillUtil.buildSpannable( + context, + metadata.getLocalizedName(context), + PillUtil.richTextPreview(rawText) + ) } else { // 内容简单,显示完整的摘要 val messagePill = PillUtil.createPillFromParam( diff --git a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/ui/components/UiTextModule.kt b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/ui/components/UiTextModule.kt index a9bb3959..1b253126 100644 --- a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/ui/components/UiTextModule.kt +++ b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/ui/components/UiTextModule.kt @@ -10,7 +10,6 @@ import com.chaomixian.vflow.core.workflow.model.ActionStep import com.chaomixian.vflow.core.workflow.module.ui.model.UiElement import com.chaomixian.vflow.core.workflow.module.ui.model.UiElementType import com.chaomixian.vflow.ui.workflow_editor.PillUtil -import com.chaomixian.vflow.ui.workflow_editor.RichTextUIProvider /** * 文本展示组件 @@ -36,8 +35,6 @@ class UiTextModule : BaseUiComponentModule() { category = "UI 组件", categoryId = "ui" ) - override val uiProvider: ModuleUIProvider? = RichTextUIProvider("content") - override fun getInputs() = listOf( InputDefinition("content", "内容", ParameterType.STRING, acceptsMagicVariable = true, supportsRichText = true, nameStringRes = R.string.param_vflow_ui_component_text_content_name), InputDefinition("key", "ID (可选)", ParameterType.STRING, "", isHidden = true, nameStringRes = R.string.param_vflow_ui_component_text_key_name) @@ -46,9 +43,13 @@ class UiTextModule : BaseUiComponentModule() { override fun getSummary(context: Context, step: ActionStep): CharSequence { val rawText = step.parameters["content"]?.toString() ?: "" - // 如果内容复杂(包含变量),只显示模块名称,让 RichTextUIProvider 显示富文本预览卡片 + // 如果内容复杂(包含变量),只显示模块名称,让预览层显示富文本卡片 if (VariableResolver.isComplex(rawText)) { - return metadata.getLocalizedName(context) + return PillUtil.buildSpannable( + context, + metadata.getLocalizedName(context), + PillUtil.richTextPreview(rawText) + ) } // 内容简单,显示完整的摘要 diff --git a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorExecutionSettings.kt b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorExecutionSettings.kt new file mode 100644 index 00000000..67c2681a --- /dev/null +++ b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorExecutionSettings.kt @@ -0,0 +1,19 @@ +package com.chaomixian.vflow.ui.workflow_editor + +import com.chaomixian.vflow.core.workflow.model.ActionStepExecutionSettings + +internal fun ActionEditorSessionState.getExecutionSettings(): ActionStepExecutionSettings { + return ActionStepExecutionSettings.fromParameters(asMap()) +} + +internal fun ActionEditorSessionState.setExecutionSettings(settings: ActionStepExecutionSettings) { + this[ActionStepExecutionSettings.KEY_ERROR_POLICY] = settings.policy + + if (settings.policy == ActionStepExecutionSettings.POLICY_RETRY) { + this[ActionStepExecutionSettings.KEY_RETRY_COUNT] = settings.retryCount + this[ActionStepExecutionSettings.KEY_RETRY_INTERVAL] = settings.retryIntervalMillis + } else { + remove(ActionStepExecutionSettings.KEY_RETRY_COUNT) + remove(ActionStepExecutionSettings.KEY_RETRY_INTERVAL) + } +} diff --git a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorRichTextLocator.kt b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorRichTextLocator.kt new file mode 100644 index 00000000..bb01d19e --- /dev/null +++ b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorRichTextLocator.kt @@ -0,0 +1,29 @@ +package com.chaomixian.vflow.ui.workflow_editor + +import android.view.ViewGroup +import com.chaomixian.vflow.R +import com.chaomixian.vflow.core.module.CustomEditorViewHolder +import com.chaomixian.vflow.core.module.InputDefinition + +internal object ActionEditorRichTextLocator { + fun findRichTextView( + inputId: String, + inputDefinition: InputDefinition?, + inputViews: Map, + customEditorHolder: CustomEditorViewHolder? + ): RichTextView? { + if (inputDefinition?.supportsRichText == true) { + val genericInputView = inputViews[inputId] + val genericRichTextView = (genericInputView + ?.findViewById(R.id.input_value_container) + ?.getChildAt(0) as? ViewGroup) + ?.findViewById(R.id.rich_text_view) + if (genericRichTextView != null) { + return genericRichTextView + } + } + + val customRootView = customEditorHolder?.view ?: return null + return customRootView.findViewWithTag(inputId) + } +} diff --git a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorSessionState.kt b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorSessionState.kt new file mode 100644 index 00000000..32eac895 --- /dev/null +++ b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorSessionState.kt @@ -0,0 +1,54 @@ +package com.chaomixian.vflow.ui.workflow_editor + +import com.chaomixian.vflow.core.module.InputDefinition +import com.chaomixian.vflow.core.workflow.model.ActionStep + +internal class ActionEditorSessionState { + private val parameters = mutableMapOf() + private val parameterTreeEditor = ParameterTreeEditor(parameters) + + operator fun get(key: String): Any? = parameters[key] + + operator fun set(key: String, value: Any?) { + parameters[key] = value + } + + fun remove(key: String) { + parameters.remove(key) + } + + fun clear() { + parameters.clear() + } + + fun putAll(values: Map) { + parameters.putAll(values) + } + + fun replaceAll(values: Map) { + parameters.clear() + parameters.putAll(values) + } + + fun initializeDefaults(inputs: List) { + inputs.forEach { def -> + def.defaultValue?.let { parameters[def.id] = it } + } + } + + fun setPath(path: ParamPath, value: Any?) { + parameterTreeEditor.set(path, value) + } + + fun clearPath(path: ParamPath, topLevelDefaultValue: Any?) { + parameterTreeEditor.clear(path, topLevelDefaultValue) + } + + fun asMap(): Map = parameters + + fun snapshot(): MutableMap = parameters.toMutableMap() + + fun toActionStep(moduleId: String, stepId: String = ""): ActionStep { + return ActionStep(moduleId = moduleId, parameters = snapshot(), id = stepId) + } +} diff --git a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorSheet.kt b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorSheet.kt index b1f0e55c..893e4323 100644 --- a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorSheet.kt +++ b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorSheet.kt @@ -12,15 +12,14 @@ import android.widget.* import androidx.core.view.isVisible import com.chaomixian.vflow.R import com.chaomixian.vflow.core.module.* +import com.chaomixian.vflow.core.workflow.model.ActionStepExecutionSettings import com.chaomixian.vflow.core.workflow.model.ActionStep import com.chaomixian.vflow.ui.common.ModuleDetailDialog -import com.google.android.material.materialswitch.MaterialSwitch import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.button.MaterialButton import com.google.android.material.card.MaterialCardView +import com.google.android.material.materialswitch.MaterialSwitch import com.google.android.material.slider.Slider -import com.google.android.material.textfield.TextInputEditText -import com.google.android.material.textfield.TextInputLayout /** * 模块参数编辑器底部表单。 @@ -31,19 +30,14 @@ class ActionEditorSheet : BottomSheetDialogFragment() { private lateinit var module: ActionModule private var existingStep: ActionStep? = null private var focusedInputId: String? = null - - /** - * 获取当前编辑的模块 - */ - fun getModule(): ActionModule? = if (::module.isInitialized) module else null private var allSteps: ArrayList? = null - private var availableNamedVariables: List? = null var onSave: ((ActionStep) -> Unit)? = null var onMagicVariableRequested: ((inputId: String, currentParameters: Map) -> Unit)? = null var onStartActivityForResult: ((Intent, (resultCode: Int, data: Intent?) -> Unit) -> Unit)? = null private val inputViews = mutableMapOf() private var customEditorHolder: CustomEditorViewHolder? = null - private val currentParameters = mutableMapOf() + private val currentParameters = ActionEditorSessionState() + private var onPickerRequested: ((inputDef: InputDefinition) -> Unit)? = null // 引用容器视图 private var customUiCard: MaterialCardView? = null @@ -56,7 +50,6 @@ class ActionEditorSheet : BottomSheetDialogFragment() { // 异常处理 UI 组件 private var errorSettingsContent: LinearLayout? = null private var errorPolicyGroup: RadioGroup? = null - private var retryOptionsContainer: LinearLayout? = null private var retryCountSlider: Slider? = null private var retryIntervalSlider: Slider? = null @@ -64,22 +57,12 @@ class ActionEditorSheet : BottomSheetDialogFragment() { // 单次编辑会话的展开状态缓存 private val expandedSections = mutableMapOf() - // 异常处理策略相关的常量 Key - const val KEY_ERROR_POLICY = "__error_policy" - const val KEY_RETRY_COUNT = "__retry_count" - const val KEY_RETRY_INTERVAL = "__retry_interval" - - const val POLICY_STOP = "STOP" - const val POLICY_SKIP = "SKIP" - const val POLICY_RETRY = "RETRY" - /** 创建 ActionEditorSheet 实例。 */ fun newInstance( module: ActionModule, existingStep: ActionStep?, focusedInputId: String?, - allSteps: List? = null, - availableNamedVariables: List? = null + allSteps: List? = null ): ActionEditorSheet { return ActionEditorSheet().apply { arguments = Bundle().apply { @@ -87,7 +70,6 @@ class ActionEditorSheet : BottomSheetDialogFragment() { putParcelable("existingStep", existingStep) putString("focusedInputId", focusedInputId) allSteps?.let { putParcelableArrayList("allSteps", ArrayList(it)) } - availableNamedVariables?.let { putStringArrayList("namedVariables", ArrayList(it)) } } } } @@ -98,19 +80,24 @@ class ActionEditorSheet : BottomSheetDialogFragment() { super.onCreate(savedInstanceState) val moduleId = arguments?.getString("moduleId") module = moduleId?.let { ModuleRegistry.getModule(it) } ?: return dismiss() - existingStep = arguments?.getParcelable("existingStep") + existingStep = arguments?.getParcelableCompat("existingStep") focusedInputId = arguments?.getString("focusedInputId") - allSteps = arguments?.getParcelableArrayList("allSteps") - availableNamedVariables = arguments?.getStringArrayList("namedVariables") + allSteps = arguments?.getParcelableArrayListCompat("allSteps") // 初始化参数,首先使用模块定义的默认值 - module.getInputs().forEach { def -> - def.defaultValue?.let { currentParameters[def.id] = it } - } + currentParameters.initializeDefaults(module.getInputs()) // 然后用步骤已有的参数覆盖默认值 existingStep?.parameters?.let { currentParameters.putAll(it) } } + fun setOnPickerRequestedListener(listener: (InputDefinition) -> Unit) { + onPickerRequested = listener + } + + private fun dispatchPickerRequest(inputDefinition: InputDefinition) { + onPickerRequested?.invoke(inputDefinition) + } + /** 创建视图并构建UI。 */ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(R.layout.sheet_action_editor, container, false) @@ -155,15 +142,15 @@ class ActionEditorSheet : BottomSheetDialogFragment() { saveButton.setOnClickListener { readParametersFromUi() - readErrorSettingsFromUi() // 读取错误处理配置 + syncExecutionSettingsFromUi() val finalParams = existingStep?.parameters?.toMutableMap() ?: mutableMapOf() - finalParams.putAll(currentParameters) + finalParams.putAll(currentParameters.snapshot()) val stepForValidation = ActionStep(moduleId = module.id, parameters = finalParams, id = existingStep?.id ?: "") // 调用 validate 方法进行验证 val validationResult = module.validate(stepForValidation, allSteps ?: emptyList()) if (validationResult.isValid) { - onSave?.invoke(ActionStep(module.id, currentParameters)) + onSave?.invoke(currentParameters.toActionStep(module.id)) dismiss() } else { Toast.makeText(context, validationResult.errorMessage, Toast.LENGTH_LONG).show() @@ -181,9 +168,18 @@ class ActionEditorSheet : BottomSheetDialogFragment() { errorSettingsContent?.removeAllViews() val radioGroup = RadioGroup(context).apply { orientation = RadioGroup.VERTICAL } - val rbStop = RadioButton(context).apply { text = getString(R.string.editor_error_policy_stop); tag = POLICY_STOP } - val rbSkip = RadioButton(context).apply { text = getString(R.string.editor_error_policy_skip); tag = POLICY_SKIP } - val rbRetry = RadioButton(context).apply { text = getString(R.string.editor_error_policy_retry); tag = POLICY_RETRY } + val rbStop = RadioButton(context).apply { + text = getString(R.string.editor_error_policy_stop) + tag = ActionStepExecutionSettings.POLICY_STOP + } + val rbSkip = RadioButton(context).apply { + text = getString(R.string.editor_error_policy_skip) + tag = ActionStepExecutionSettings.POLICY_SKIP + } + val rbRetry = RadioButton(context).apply { + text = getString(R.string.editor_error_policy_retry) + tag = ActionStepExecutionSettings.POLICY_RETRY + } radioGroup.addView(rbStop) radioGroup.addView(rbSkip) @@ -196,9 +192,10 @@ class ActionEditorSheet : BottomSheetDialogFragment() { } // 恢复状态 - val currentPolicy = currentParameters[KEY_ERROR_POLICY] as? String ?: POLICY_STOP - val currentRetryCount = (currentParameters[KEY_RETRY_COUNT] as? Number)?.toFloat() ?: 3f - val currentRetryInterval = (currentParameters[KEY_RETRY_INTERVAL] as? Number)?.toFloat() ?: 1000f + val executionSettings = currentParameters.getExecutionSettings() + val currentPolicy = executionSettings.policy + val currentRetryCount = executionSettings.retryCount.toFloat() + val currentRetryInterval = executionSettings.retryIntervalMillis.toFloat() // --- 重试次数 --- val sliderRetryCount = StandardControlFactory.createSliderWithLabel( @@ -228,11 +225,11 @@ class ActionEditorSheet : BottomSheetDialogFragment() { retryContainer.addView(sliderRetryInterval) when (currentPolicy) { - POLICY_SKIP -> rbSkip.isChecked = true - POLICY_RETRY -> rbRetry.isChecked = true + ActionStepExecutionSettings.POLICY_SKIP -> rbSkip.isChecked = true + ActionStepExecutionSettings.POLICY_RETRY -> rbRetry.isChecked = true else -> rbStop.isChecked = true } - retryContainer.isVisible = (currentPolicy == POLICY_RETRY) + retryContainer.isVisible = (currentPolicy == ActionStepExecutionSettings.POLICY_RETRY) // 获取滑块引用 val sliderRetryCountView = sliderRetryCount.getChildAt(1) as Slider @@ -245,7 +242,6 @@ class ActionEditorSheet : BottomSheetDialogFragment() { // 保存引用 this.errorPolicyGroup = radioGroup - this.retryOptionsContainer = retryContainer this.retryCountSlider = sliderRetryCountView this.retryIntervalSlider = sliderRetryIntervalView @@ -253,117 +249,131 @@ class ActionEditorSheet : BottomSheetDialogFragment() { errorSettingsContent?.addView(retryContainer) } - /** - * 读取异常处理配置到 currentParameters。 - */ - private fun readErrorSettingsFromUi() { + private fun syncExecutionSettingsFromUi() { + currentParameters.setExecutionSettings(readExecutionSettingsFromUi()) + } + + private fun readExecutionSettingsFromUi(): ActionStepExecutionSettings { val selectedId = errorPolicyGroup?.checkedRadioButtonId val view = errorPolicyGroup?.findViewById(selectedId ?: -1) - val policy = view?.tag as? String ?: POLICY_STOP + val policy = view?.tag as? String ?: ActionStepExecutionSettings.POLICY_STOP - currentParameters[KEY_ERROR_POLICY] = policy + return ActionStepExecutionSettings( + policy = policy, + retryCount = retryCountSlider?.value?.toInt() ?: ActionStepExecutionSettings.DEFAULT_RETRY_COUNT, + retryIntervalMillis = retryIntervalSlider?.value?.toLong() + ?: ActionStepExecutionSettings.DEFAULT_RETRY_INTERVAL_MS + ) + } - if (policy == POLICY_RETRY) { - currentParameters[KEY_RETRY_COUNT] = retryCountSlider?.value?.toInt() ?: 3 - currentParameters[KEY_RETRY_INTERVAL] = retryIntervalSlider?.value?.toLong() ?: 1000L - } else { - // 清理无用参数 - currentParameters.remove(KEY_RETRY_COUNT) - currentParameters.remove(KEY_RETRY_INTERVAL) + private fun requestMagicVariableSelection(inputId: String) { + readParametersFromUi() + onMagicVariableRequested?.invoke(inputId, currentParameters.asMap()) + } + + private fun mutateSession( + readUiFirst: Boolean = false, + rebuildMode: RebuildMode = RebuildMode.IMMEDIATE, + mutation: ActionEditorSessionState.() -> Unit + ) { + if (readUiFirst) { + readParametersFromUi() + } + mutation(currentParameters) + when (rebuildMode) { + RebuildMode.NONE -> Unit + RebuildMode.IMMEDIATE -> buildUi() + RebuildMode.POST -> postBuildUi() + } + } + + private fun applyParameterUpdate( + inputId: String, + updatedValue: Any?, + readUiFirst: Boolean, + rebuildMode: RebuildMode + ) { + mutateSession(readUiFirst = readUiFirst, rebuildMode = rebuildMode) { + val updatedStep = toActionStep(module.id, existingStep?.id ?: "").copy( + parameters = snapshot().apply { + this[inputId] = updatedValue + } + ) + replaceAll(module.onParameterUpdated(updatedStep, inputId, updatedValue)) + } + } + + private fun postBuildUi() { + android.os.Handler(android.os.Looper.getMainLooper()).post { + try { + buildUi() + } catch (_: Exception) { + // 忽略视图已销毁的情况 + } } } + private enum class RebuildMode { + NONE, + IMMEDIATE, + POST + } + /** * 构建UI的逻辑。 */ private fun buildUi() { - // 清空所有容器 + val uiModel = computeUiModel() + applyUiModelCorrections(uiModel) + renderUiModel(uiModel) + } + + private fun computeUiModel(): EditorUiModel { + return ActionEditorUiModelBuilder.build( + module = module, + sessionState = currentParameters, + allSteps = allSteps + ) + } + + private fun applyUiModelCorrections(uiModel: EditorUiModel) { + val correctedParameters = uiModel.effectiveParameters + if (currentParameters.asMap() != correctedParameters) { + currentParameters.replaceAll(correctedParameters) + } + } + + private fun renderUiModel(uiModel: EditorUiModel) { customUiContainer?.removeAllViews() genericInputsContainer?.removeAllViews() editorActionsContainer?.removeAllViews() inputViews.clear() customEditorHolder = null - val stepForUi = ActionStep(module.id, currentParameters) - val inputsToShow = module.getDynamicInputs(stepForUi, allSteps) - - // 校正无效的枚举参数值,防止因模块更新导致崩溃 - inputsToShow.forEach { inputDef -> - if (inputDef.staticType == ParameterType.ENUM) { - val currentValue = currentParameters[inputDef.id] as? String - if (currentValue != null && !inputDef.options.contains(currentValue)) { - // 尝试使用向后兼容映射 - val mappedValue = inputDef.normalizeEnumValueOrNull(currentValue) - if (mappedValue != null && inputDef.options.contains(mappedValue)) { - currentParameters[inputDef.id] = mappedValue - } else { - // 映射失败,使用默认值 - currentParameters[inputDef.id] = inputDef.defaultValue - } - } - } - } - - val uiProvider = module.uiProvider - val editorActions = module.getEditorActions(stepForUi, allSteps) - val handledInputIds = uiProvider?.getHandledInputIds() ?: emptySet() - - // 构建自定义 UI - if (uiProvider != null && uiProvider !is RichTextUIProvider) { + if (uiModel.showCustomUi) { + val uiProvider = uiModel.uiProvider ?: return customEditorHolder = uiProvider.createEditor( context = requireContext(), parent = customUiContainer!!, - currentParameters = currentParameters, + currentParameters = uiModel.effectiveParameters, onParametersChanged = { readParametersFromUi() }, - onMagicVariableRequested = { inputId -> - readParametersFromUi() - this.onMagicVariableRequested?.invoke(inputId, currentParameters) - }, + onMagicVariableRequested = ::requestMagicVariableSelection, allSteps = allSteps, onStartActivityForResult = onStartActivityForResult ) customUiContainer?.addView(customEditorHolder!!.view) } - customUiCard?.isVisible = uiProvider != null && uiProvider !is RichTextUIProvider - /** - * 检查输入参数是否应该显示。 - * 优先使用新的 visibility 条件系统,同时保持对 isHidden 的向后兼容。 - */ - fun isInputVisible(inputDef: InputDefinition, params: Map): Boolean { - // 如果设置了 visibility 条件,使用新系统 - inputDef.visibility?.let { visibility -> - return visibility.isVisible(params) - } - // 否则使用传统的 isHidden 标志 - return !inputDef.isHidden - } - - // 构建通用参数列表 (分离普通参数和折叠参数) - val normalInputs = mutableListOf() - val foldedInputs = mutableListOf() + customUiCard?.isVisible = uiModel.showCustomUi - inputsToShow.forEach { inputDef -> - if (!handledInputIds.contains(inputDef.id) && isInputVisible(inputDef, currentParameters)) { - if (inputDef.isFolded) { - foldedInputs.add(inputDef) - } else { - normalInputs.add(inputDef) - } - } - } - - // 添加普通参数 - normalInputs.forEach { inputDef -> - val inputView = createViewForInputDefinition(inputDef, genericInputsContainer!!) + uiModel.genericInputsSection.fields.forEach { fieldModel -> + val inputView = createViewForInputDefinition(fieldModel) genericInputsContainer?.addView(inputView) - inputViews[inputDef.id] = inputView + inputViews[fieldModel.inputDefinition.id] = inputView } - // 如果有折叠参数,创建“更多设置”区域 - if (foldedInputs.isNotEmpty()) { - // 创建分隔线 - if (normalInputs.isNotEmpty()) { + if (uiModel.advancedInputsSection.isVisible) { + if (uiModel.shouldShowAdvancedDivider) { val divider = View(context).apply { layoutParams = LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, @@ -377,20 +387,16 @@ class ActionEditorSheet : BottomSheetDialogFragment() { genericInputsContainer?.addView(divider) } - // 创建折叠容器结构 - val advancedSection = createAdvancedSection(foldedInputs) + val advancedSection = createAdvancedSection(uiModel.advancedInputsSection.fields) genericInputsContainer?.addView(advancedSection) } - // 只有当有通用参数(普通或折叠)时才显示卡片 - genericInputsCard?.isVisible = normalInputs.isNotEmpty() || foldedInputs.isNotEmpty() + genericInputsCard?.isVisible = uiModel.showsAnyGenericInputs - if (editorActions.isNotEmpty()) { - editorActionsContainer?.addView(createEditorActionsSection(editorActions)) - editorActionsCard?.isVisible = true - } else { - editorActionsCard?.isVisible = false + if (uiModel.editorActionsSection.isVisible) { + editorActionsContainer?.addView(createEditorActionsSection(uiModel.editorActionsSection.actions)) } + editorActionsCard?.isVisible = uiModel.editorActionsSection.isVisible } private fun createEditorActionsSection(actions: List): View { @@ -428,7 +434,7 @@ class ActionEditorSheet : BottomSheetDialogFragment() { /** * 动态创建"更多设置"折叠区域。 */ - private fun createAdvancedSection(inputs: List): View { + private fun createAdvancedSection(fields: List): View { val context = requireContext() val density = resources.displayMetrics.density @@ -508,11 +514,11 @@ class ActionEditorSheet : BottomSheetDialogFragment() { } // 填充参数到内容容器 - inputs.forEach { inputDef -> - val inputView = createViewForInputDefinition(inputDef, contentLayout) + fields.forEach { fieldModel -> + val inputView = createViewForInputDefinition(fieldModel) contentLayout.addView(inputView) // 注册到 inputViews,这样 readParametersFromUi 就能自动读取它们 - inputViews[inputDef.id] = inputView + inputViews[fieldModel.inputDefinition.id] = inputView } // 设置点击事件 @@ -542,7 +548,10 @@ class ActionEditorSheet : BottomSheetDialogFragment() { * 对于 PICKER 类型,使用带选择器图标的输入框。 * 对于其他风格,使用完整的参数输入行布局。 */ - private fun createViewForInputDefinition(inputDef: InputDefinition, parent: ViewGroup): View { + private fun createViewForInputDefinition(fieldModel: EditorFieldModel): View { + val inputDef = fieldModel.inputDefinition + val currentValue = fieldModel.currentValue + // CHIP_GROUP 风格不需要魔法变量按钮,使用简化布局 if (inputDef.inputStyle == InputStyle.CHIP_GROUP) { val row = LayoutInflater.from(requireContext()).inflate(R.layout.row_editor_input, null, false) @@ -555,18 +564,15 @@ class ActionEditorSheet : BottomSheetDialogFragment() { val chipGroupView = StandardControlFactory.createChipGroup( context = requireContext(), options = inputDef.options, - currentValue = currentParameters[inputDef.id] as? String, + currentValue = currentValue as? String, onSelectionChanged = { selectedItem -> if (currentParameters[inputDef.id] != selectedItem) { - readParametersFromUi() - currentParameters[inputDef.id] = selectedItem - android.os.Handler(android.os.Looper.getMainLooper()).post { - try { - buildUi() - } catch (e: Exception) { - // 忽略视图已销毁的情况 - } - } + applyParameterUpdate( + inputId = inputDef.id, + updatedValue = selectedItem, + readUiFirst = true, + rebuildMode = RebuildMode.POST + ) } }, optionsStringRes = if (inputDef.optionsStringRes.isNotEmpty()) inputDef.optionsStringRes else null @@ -587,11 +593,11 @@ class ActionEditorSheet : BottomSheetDialogFragment() { val pickerInputView = StandardControlFactory.createPickerInput( context = requireContext(), - currentValue = currentParameters[inputDef.id], + currentValue = currentValue, pickerType = inputDef.pickerType, hint = inputDef.getLocalizedHint(requireContext()), onPickerClicked = { - onPickerRequested?.invoke(inputDef) + dispatchPickerRequest(inputDef) } ) valueContainer.addView(pickerInputView) @@ -603,102 +609,93 @@ class ActionEditorSheet : BottomSheetDialogFragment() { return StandardControlFactory.createParameterInputRow( context = requireContext(), inputDef = inputDef, - currentValue = currentParameters[inputDef.id], + currentValue = currentValue, allSteps = allSteps, - onMagicVariableRequested = { inputId -> - readParametersFromUi() - this.onMagicVariableRequested?.invoke(inputId, currentParameters) - }, + onMagicVariableRequested = ::requestMagicVariableSelection, onEnumItemSelected = { selectedItem -> // 防止重复触发:只在值真正改变时才处理 if (currentParameters[inputDef.id] != selectedItem) { - // 先读取当前UI中的所有参数值,防止切换条件时丢失其他输入框的值 - readParametersFromUi() - currentParameters[inputDef.id] = selectedItem - // 使用 Handler 延迟重建UI以避免在事件处理期间冻结 - android.os.Handler(android.os.Looper.getMainLooper()).post { - try { - buildUi() - } catch (e: Exception) { - // 忽略视图已销毁的情况 - } - } + applyParameterUpdate( + inputId = inputDef.id, + updatedValue = selectedItem, + readUiFirst = true, + rebuildMode = RebuildMode.POST + ) } } - ) + ).also { row -> + bindImmediateDynamicInputUpdates(row, inputDef) + } } - /** - * picker 被点击时的回调,由外部设置(Activity/Fragment)。 - * 格式:inputDef 为被点击的输入定义,result 为选择的结果 - */ - var onPickerResult: ((inputDef: InputDefinition, result: Any?) -> Unit)? = null - - /** - * 获取当前参数值,供 PickerHandler 使用 - */ - fun getCurrentParameter(inputDef: InputDefinition): Any? { - return currentParameters[inputDef.id] + private fun bindImmediateDynamicInputUpdates(row: View, inputDef: InputDefinition) { + if (inputDef.staticType != ParameterType.BOOLEAN && inputDef.inputStyle != InputStyle.SWITCH) { + return + } + val toggle = findDescendantMaterialSwitch(row) ?: return + toggle.setOnCheckedChangeListener { _, isChecked -> + if (currentParameters[inputDef.id] == isChecked) { + return@setOnCheckedChangeListener + } + applyParameterUpdate( + inputId = inputDef.id, + updatedValue = isChecked, + readUiFirst = true, + rebuildMode = RebuildMode.POST + ) + } } - /** - * 内部触发 picker 请求的回调。 - */ - private var onPickerRequested: ((inputDef: InputDefinition) -> Unit)? = { inputDef -> - // 默认实现:交给外部处理 - // 子类可以重写此方法来实际显示选择器 + private fun findDescendantMaterialSwitch(root: View): MaterialSwitch? { + if (root is MaterialSwitch) { + return root + } + if (root !is ViewGroup) { + return null + } + for (index in 0 until root.childCount) { + val match = findDescendantMaterialSwitch(root.getChildAt(index)) + if (match != null) { + return match + } + } + return null } - fun setOnPickerRequestedListener(listener: (InputDefinition) -> Unit) { - onPickerRequested = listener + fun updateParametersAndRebuildUi(newParameters: Map) { + mutateSession { + putAll(newParameters) + } } - fun updateParametersAndRebuildUi(newParameters: Map) { - currentParameters.putAll(newParameters) - buildUi() + private fun currentStepForUi(): ActionStep { + return currentParameters.toActionStep(module.id) } - private fun isVariableReference(value: Any?): Boolean { - return StandardControlFactory.isVariableReference(value) + private fun findDynamicInputDefinition(inputId: String): InputDefinition? { + return module.getDynamicInputs(currentStepForUi(), allSteps).find { it.id == inputId } } - private fun readParametersFromUi() { + private fun mergeCustomEditorParametersFromUi() { val uiProvider = module.uiProvider - if (uiProvider != null && customEditorHolder != null && uiProvider !is RichTextUIProvider) { + if (uiProvider != null && customEditorHolder != null && uiProvider.hasCustomEditor()) { currentParameters.putAll(uiProvider.readFromEditor(customEditorHolder!!)) } + } - inputViews.forEach { (id, view) -> - val stepForUi = ActionStep(module.id, currentParameters) - val inputDef = module.getDynamicInputs(stepForUi, allSteps).find { it.id == id } - - if (inputDef?.supportsRichText == false && isVariableReference(currentParameters[id])) { - return@forEach - } - - val valueContainer = view.findViewById(R.id.input_value_container) ?: return@forEach - if (valueContainer.childCount == 0) return@forEach - - val staticView = valueContainer.getChildAt(0) - - val value: Any? = if (inputDef != null) { - StandardControlFactory.readValueFromInputRow(view, inputDef) - } else { - null - } + private fun readParametersFromUi() { + mergeCustomEditorParametersFromUi() - if (value != null) { - val convertedValue: Any? = when (inputDef?.staticType) { - ParameterType.NUMBER -> { - if (inputDef.supportsRichText) value else { - val strVal = value.toString() - strVal.toLongOrNull() ?: strVal.toDoubleOrNull() - } - } - else -> value - } - currentParameters[id] = convertedValue - } + inputViews.forEach { (id, view) -> + val inputDef = findDynamicInputDefinition(id) + val resolvedInputDef = inputDef ?: return@forEach + val readResult = ActionEditorViewStateReader.readParameterValue( + view = view, + inputDefinition = resolvedInputDef, + currentValue = currentParameters[id] + ) + if (!readResult.shouldUpdate) return@forEach + currentParameters[id] = readResult.value } } @@ -706,140 +703,33 @@ class ActionEditorSheet : BottomSheetDialogFragment() { * 更新输入框的变量。 */ fun updateInputWithVariable(inputId: String, variableReference: String) { - val stepForUi = ActionStep(module.id, currentParameters) - val inputDef = module.getDynamicInputs(stepForUi, allSteps).find { it.id == inputId } - - var richTextView: RichTextView? = null - - // 尝试从通用输入视图中查找 - if (inputDef?.supportsRichText == true) { - val view = inputViews[inputId] - richTextView = (view?.findViewById(R.id.input_value_container)?.getChildAt(0) as? ViewGroup) - ?.findViewById(R.id.rich_text_view) - } - - // 如果在通用视图中找不到,则尝试在自定义编辑器视图中查找 - if (richTextView == null && customEditorHolder != null) { - // 首先尝试用 inputId 作为 tag 查找(标准控件使用的方式) - richTextView = customEditorHolder?.view?.findViewWithTag(inputId) - // 如果找不到,尝试旧的方式 - if (richTextView == null) { - richTextView = customEditorHolder?.view?.findViewWithTag("rich_text_view_value") - } - } + val inputDef = findDynamicInputDefinition(inputId) + val path = ParamPath.parse(inputId) + val richTextView = ActionEditorRichTextLocator.findRichTextView( + inputId = inputId, + inputDefinition = inputDef, + inputViews = inputViews, + customEditorHolder = customEditorHolder + ) if (richTextView != null) { - // 使用新的 API:直接传递变量引用,内部使用 PillVariableResolver 解析 richTextView.insertVariablePill(variableReference) - return // 直接操作视图后返回,避免重建UI + return } - if (inputId.contains('.')) { - val parts = inputId.split('.', limit = 2) - val mainInputId = parts[0] - val subKey = parts[1] - - // 获取当前参数值 - val currentValue = currentParameters[mainInputId] - - if (currentValue is List<*>) { - // 如果当前是列表,则按索引更新列表 - val mutableList = currentValue.toMutableList() - val index = subKey.toIntOrNull() - // 确保索引有效 - if (index != null && index >= 0 && index < mutableList.size) { - mutableList[index] = variableReference // 更新指定位置 - currentParameters[mainInputId] = mutableList - } - } else { - // 如果是字典或默认情况,按 Map 更新 - val dict = (currentValue as? Map<*, *>)?.toMutableMap() ?: mutableMapOf() - dict[subKey] = variableReference - currentParameters[mainInputId] = dict - } - } else { - currentParameters[inputId] = variableReference + mutateSession { + setPath(path, variableReference) } - buildUi() } /** * 清除输入框的变量。 */ fun clearInputVariable(inputId: String) { - if (inputId.contains('.')) { - val parts = inputId.split('.', limit = 2) - val mainInputId = parts[0] - val subKey = parts[1] - - val currentValue = currentParameters[mainInputId] - - if (currentValue is List<*>) { - // 列表:清除指定索引的内容(置为空字符串) - val mutableList = currentValue.toMutableList() - val index = subKey.toIntOrNull() - if (index != null && index >= 0 && index < mutableList.size) { - mutableList[index] = "" - currentParameters[mainInputId] = mutableList - } - } else { - // 字典:清除指定 Key 的内容 - val dict = (currentValue as? Map<*, *>)?.toMutableMap() ?: return - dict[subKey] = "" - currentParameters[mainInputId] = dict - } - } else { - val inputDef = module.getInputs().find { it.id == inputId } ?: return - currentParameters[inputId] = inputDef.defaultValue - } - // 清除后重建UI - buildUi() - } - - /** - * 当一个参数值在UI上发生变化时调用此方法。 - * @param updatedId 被更新的参数的ID。 - * @param updatedValue 新的参数值。 - */ - private fun parameterUpdated(updatedId: String, updatedValue: Any?) { - // 基于当前参数状态,应用刚刚发生变化的那个值 - val parametersBeforeModuleUpdate = currentParameters.toMutableMap() - parametersBeforeModuleUpdate[updatedId] = updatedValue - - // 创建一个临时的ActionStep实例,用于传递给模块 - val stepForUpdate = ActionStep(module.id, parametersBeforeModuleUpdate) - - // 调用模块的onParameterUpdated方法,获取模块处理后的全新参数集 - val newParametersFromServer = module.onParameterUpdated(stepForUpdate, updatedId, updatedValue) - - // 使用模块返回的参数集,完全更新编辑器内部的当前参数状态 - currentParameters.clear() - currentParameters.putAll(newParametersFromServer) - // 使用新的参数状态,重建整个UI - buildUi() - } - - private fun createBaseViewForInputType(inputDef: InputDefinition, currentValue: Any?): View { - val view = StandardControlFactory.createBaseViewForInputType( - requireContext(), - inputDef, - currentValue, - onItemSelectedCallback = { selectedValue -> - if (currentParameters[inputDef.id] != selectedValue) { - parameterUpdated(inputDef.id, selectedValue) - } - } - ) - - // 添加参数变更监听器 - when (view) { - is MaterialSwitch -> { - view.setOnCheckedChangeListener { _, isChecked -> - parameterUpdated(inputDef.id, isChecked) - } - } + val path = ParamPath.parse(inputId) + val topLevelDefaultValue = module.getInputs().find { it.id == path.rootId }?.defaultValue + mutateSession { + clearPath(path, topLevelDefaultValue) } - - return view } } diff --git a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorUiModel.kt b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorUiModel.kt new file mode 100644 index 00000000..c33bea63 --- /dev/null +++ b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorUiModel.kt @@ -0,0 +1,111 @@ +package com.chaomixian.vflow.ui.workflow_editor + +import com.chaomixian.vflow.core.module.ActionModule +import com.chaomixian.vflow.core.module.EditorAction +import com.chaomixian.vflow.core.module.InputDefinition +import com.chaomixian.vflow.core.module.ModuleUIProvider +import com.chaomixian.vflow.core.module.ParameterType +import com.chaomixian.vflow.core.workflow.model.ActionStep + +internal data class EditorFieldModel( + val inputDefinition: InputDefinition, + val currentValue: Any? +) + +internal data class EditorSectionModel( + val fields: List = emptyList(), + val actions: List = emptyList(), + val isVisible: Boolean = false +) + +internal data class EditorUiModel( + val uiProvider: ModuleUIProvider?, + val effectiveParameters: Map, + val showCustomUi: Boolean, + val genericInputsSection: EditorSectionModel, + val advancedInputsSection: EditorSectionModel, + val editorActionsSection: EditorSectionModel +) { + val showsAnyGenericInputs: Boolean + get() = genericInputsSection.isVisible || advancedInputsSection.isVisible + + val shouldShowAdvancedDivider: Boolean + get() = genericInputsSection.isVisible && advancedInputsSection.isVisible +} + +internal object ActionEditorUiModelBuilder { + fun build( + module: ActionModule, + sessionState: ActionEditorSessionState, + allSteps: List? + ): EditorUiModel { + val baseStep = sessionState.toActionStep(module.id) + val baseInputs = module.getDynamicInputs(baseStep, allSteps) + val correctedParameterValues = computeEnumCorrections(baseInputs, sessionState.asMap()) + val effectiveParameters = sessionState.snapshot().apply { + putAll(correctedParameterValues) + } + val effectiveStep = ActionStep(module.id, effectiveParameters) + val inputsToShow = module.getDynamicInputs(effectiveStep, allSteps) + val uiProvider = module.uiProvider + val handledInputIds = uiProvider?.getHandledInputIds() ?: emptySet() + val visibleFields = inputsToShow + .asSequence() + .filterNot { handledInputIds.contains(it.id) } + .filter { it.isVisibleForEditor(effectiveParameters) } + .map { inputDef -> + EditorFieldModel( + inputDefinition = inputDef, + currentValue = effectiveParameters[inputDef.id] + ) + } + .toList() + val genericFields = visibleFields.filterNot { it.inputDefinition.isFolded } + val advancedFields = visibleFields.filter { it.inputDefinition.isFolded } + val editorActions = module.getEditorActions(effectiveStep, allSteps) + + return EditorUiModel( + uiProvider = uiProvider, + effectiveParameters = effectiveParameters, + showCustomUi = uiProvider?.hasCustomEditor() == true, + genericInputsSection = EditorSectionModel( + fields = genericFields, + isVisible = genericFields.isNotEmpty() + ), + advancedInputsSection = EditorSectionModel( + fields = advancedFields, + isVisible = advancedFields.isNotEmpty() + ), + editorActionsSection = EditorSectionModel( + actions = editorActions, + isVisible = editorActions.isNotEmpty() + ) + ) + } + + private fun computeEnumCorrections( + inputsToShow: List, + currentParameters: Map + ): Map { + val correctedValues = linkedMapOf() + + inputsToShow.forEach { inputDef -> + if (inputDef.staticType != ParameterType.ENUM) return@forEach + + val currentValue = currentParameters[inputDef.id] as? String ?: return@forEach + if (inputDef.options.contains(currentValue)) return@forEach + + val correctedValue = inputDef.normalizeEnumValueOrNull(currentValue) + ?.takeIf { inputDef.options.contains(it) } + ?: inputDef.defaultValue + correctedValues[inputDef.id] = correctedValue + } + + return correctedValues + } +} + +private fun InputDefinition.isVisibleForEditor(currentParameters: Map): Boolean { + visibility?.let { return it.isVisible(currentParameters) } + return !isHidden +} diff --git a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorViewStateReader.kt b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorViewStateReader.kt new file mode 100644 index 00000000..051cc078 --- /dev/null +++ b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorViewStateReader.kt @@ -0,0 +1,59 @@ +package com.chaomixian.vflow.ui.workflow_editor + +import android.view.View +import android.view.ViewGroup +import com.chaomixian.vflow.R +import com.chaomixian.vflow.core.module.InputDefinition +import com.chaomixian.vflow.core.module.ParameterType + +internal object ActionEditorViewStateReader { + data class ReadResult( + val shouldUpdate: Boolean, + val value: Any? + ) + + fun readParameterValue( + view: View, + inputDefinition: InputDefinition, + currentValue: Any? + ): ReadResult { + if (inputDefinition.supportsRichText == false && StandardControlFactory.isVariableReference(currentValue)) { + return ReadResult(shouldUpdate = true, value = currentValue) + } + + val valueContainer = view.findViewById(R.id.input_value_container) + ?: return ReadResult(shouldUpdate = false, value = null) + if (valueContainer.childCount == 0) { + return ReadResult(shouldUpdate = false, value = null) + } + + val rawValue = StandardControlFactory.readValueFromInputRow(view, inputDefinition) + if (rawValue == null) { + return when (inputDefinition.staticType) { + ParameterType.NUMBER -> ReadResult(shouldUpdate = true, value = null) + else -> ReadResult(shouldUpdate = false, value = null) + } + } + return ReadResult( + shouldUpdate = true, + value = convertValue(rawValue, inputDefinition) + ) + } + + private fun convertValue( + rawValue: Any, + inputDefinition: InputDefinition + ): Any? { + return when (inputDefinition.staticType) { + ParameterType.NUMBER -> { + if (inputDefinition.supportsRichText) { + rawValue + } else { + val stringValue = rawValue.toString() + stringValue.toLongOrNull() ?: stringValue.toDoubleOrNull() + } + } + else -> rawValue + } + } +} diff --git a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionStepAdapter.kt b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionStepAdapter.kt index 516e8975..27fad1b8 100644 --- a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionStepAdapter.kt +++ b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionStepAdapter.kt @@ -126,7 +126,8 @@ class ActionStepAdapter( cardView: View, step: ActionStep, actualPosition: Int, - title: CharSequence, + rawSummary: CharSequence?, + prefixText: String?, allSteps: List, indentLevel: Int, isDeletable: Boolean, @@ -153,14 +154,50 @@ class ActionStepAdapter( categoryColorBar.background = drawable contentContainer.removeAllViews() - val headerView = createHeaderRow( + val summarySegments = buildSummarySegments( context = context, - summary = title, - clickTarget = cardView, - onParameterPillClick = onParameterPillClick, - onFallbackClick = onClick - ) - contentContainer.addView(headerView) + rawSummary = rawSummary, + allSteps = allSteps + ).ifEmpty { + listOf(module.metadata.getLocalizedName(context) as Any) + } + + var prefixApplied = false + summarySegments.forEach { segment -> + when (segment) { + is CharSequence -> { + val summary = if (!prefixApplied) { + prefixApplied = true + buildStepHeader(context, segment, prefixText) + } else { + segment + } + val headerView = createHeaderRow( + context = context, + summary = summary, + clickTarget = cardView, + onParameterPillClick = onParameterPillClick, + onFallbackClick = onClick + ) + contentContainer.addView(headerView) + } + is PillUtil.RichTextPill -> { + if (segment.onlyWhenComplex && + !com.chaomixian.vflow.core.execution.VariableResolver.isComplex(segment.rawText) + ) { + return@forEach + } + val previewView = PillRenderer.createPreviewTextView( + context = context, + parent = contentContainer, + content = segment.rawText, + allSteps = allSteps, + style = PillRenderer.DisplayStyle.RICH_TEXT + ) + contentContainer.addView(previewView) + } + } + } val customPreview = module.uiProvider?.createPreview(context, contentContainer, step, allSteps) { intent, callback -> onStartActivityForResult(actualPosition, intent, callback) @@ -185,6 +222,25 @@ class ActionStepAdapter( } } + private fun buildSummarySegments( + context: Context, + rawSummary: CharSequence?, + allSteps: List + ): List { + return PillUtil.splitSummaryContent(rawSummary).mapNotNull { part -> + when (part) { + is CharSequence -> PillRenderer.renderDisplayText( + context = context, + content = part, + allSteps = allSteps, + style = PillRenderer.DisplayStyle.SUMMARY + ) + is PillUtil.RichTextPill -> part + else -> null + } + } + } + private fun createHeaderRow( context: Context, summary: CharSequence, @@ -242,15 +298,13 @@ class ActionStepAdapter( triggerSteps.forEachIndexed { index, step -> val module = ModuleRegistry.getModule(step.moduleId) ?: return@forEachIndexed val rawSummary = module.getSummary(itemView.context, step) - val summary = PillRenderer.renderPills(itemView.context, rawSummary, allSteps, step) - ?: module.metadata.getLocalizedName(itemView.context) - val cardTitle = buildStepHeader(itemView.context, summary, null) val embeddedCard = inflater.inflate(R.layout.item_action_step, triggerContainer, false) bindEmbeddedStepCard( cardView = embeddedCard, step = step, actualPosition = index, - title = cardTitle, + rawSummary = rawSummary, + prefixText = null, allSteps = allSteps, indentLevel = 0, isDeletable = triggerSteps.size > 1, @@ -275,15 +329,13 @@ class ActionStepAdapter( fun bind(step: ActionStep, actualPosition: Int, displayIndex: Int, allSteps: List) { val module = ModuleRegistry.getModule(step.moduleId) ?: return val rawSummary = module.getSummary(context, step) - val summary = PillRenderer.renderPills(context, rawSummary, allSteps, step) - ?: module.metadata.getLocalizedName(context) - val title = buildStepHeader(context, summary, "#$displayIndex ") bindEmbeddedStepCard( cardView = itemView, step = step, actualPosition = actualPosition, - title = title, + rawSummary = rawSummary, + prefixText = "#$displayIndex ", allSteps = allSteps, indentLevel = step.indentationLevel, isDeletable = module.blockBehavior.isIndividuallyDeletable || diff --git a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/BundleCompat.kt b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/BundleCompat.kt new file mode 100644 index 00000000..e1d4d272 --- /dev/null +++ b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/BundleCompat.kt @@ -0,0 +1,23 @@ +package com.chaomixian.vflow.ui.workflow_editor + +import android.os.Build +import android.os.Bundle +import android.os.Parcelable + +internal inline fun Bundle.getParcelableCompat(key: String): T? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getParcelable(key, T::class.java) + } else { + @Suppress("DEPRECATION") + getParcelable(key) + } +} + +internal inline fun Bundle.getParcelableArrayListCompat(key: String): ArrayList? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getParcelableArrayList(key, T::class.java) + } else { + @Suppress("DEPRECATION") + getParcelableArrayList(key) + } +} diff --git a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/DictionaryKVAdapter.kt b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/DictionaryKVAdapter.kt index cf5a040e..eb1ee282 100644 --- a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/DictionaryKVAdapter.kt +++ b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/DictionaryKVAdapter.kt @@ -93,24 +93,11 @@ class DictionaryKVAdapter( val textView = pillView.findViewById(R.id.pill_text) // 尝试使用 PillVariableResolver 解析变量以获取友好的显示名称 - val displayName = if (allSteps != null) { - PillVariableResolver.resolveVariable(holder.itemView.context, item.second, allSteps)?.displayName - } else { - null - } ?: run { - // Fallback: 提取变量名作为显示文本 - when { - item.second.isMagicVariable() -> { - val content = item.second.removeSurrounding("{{", "}}") - val parts = content.split('.') - if (parts.size >= 2) "${parts[0]}.${parts[1]}" else content - } - item.second.isNamedVariable() -> { - item.second.removeSurrounding("[[", "]]") - } - else -> item.second - } - } + val displayName = PillRenderer.resolveDisplayName( + context = holder.itemView.context, + variableReference = item.second, + allSteps = allSteps ?: emptyList() + ) textView.text = displayName holder.valueContainer.addView(pillView) @@ -166,7 +153,7 @@ class DictionaryKVAdapter( if (position != RecyclerView.NO_POSITION) { val currentKey = holder.keyEditText.text.toString() if (currentKey.isNotBlank()) { - onMagicClick?.invoke(currentKey) + onMagicClick?.invoke(ParamPath.encodeSegment(currentKey)) } else { Toast.makeText(holder.itemView.context, R.string.dictionary_key_required, Toast.LENGTH_SHORT).show() } diff --git a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ListItemAdapter.kt b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ListItemAdapter.kt index 47a58e45..1908990a 100644 --- a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ListItemAdapter.kt +++ b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ListItemAdapter.kt @@ -95,24 +95,11 @@ class ListItemAdapter( val textView = pillView.findViewById(R.id.pill_text) // 尝试使用 PillVariableResolver 解析变量以获取友好的显示名称 - val displayName = if (allSteps != null) { - PillVariableResolver.resolveVariable(holder.itemView.context, itemValue, allSteps)?.displayName - } else { - null - } ?: run { - // Fallback: 提取变量名作为显示文本 - when { - itemValue.isMagicVariable() -> { - val content = itemValue.removeSurrounding("{{", "}}") - val parts = content.split('.') - if (parts.size >= 2) "${parts[0]}.${parts[1]}" else content - } - itemValue.isNamedVariable() -> { - itemValue.removeSurrounding("[[", "]]") - } - else -> itemValue - } - } + val displayName = PillRenderer.resolveDisplayName( + context = holder.itemView.context, + variableReference = itemValue, + allSteps = allSteps ?: emptyList() + ) textView.text = displayName holder.valueContainer.addView(pillView) diff --git a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ParamPath.kt b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ParamPath.kt new file mode 100644 index 00000000..8c503b1e --- /dev/null +++ b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ParamPath.kt @@ -0,0 +1,60 @@ +package com.chaomixian.vflow.ui.workflow_editor + +import java.nio.charset.StandardCharsets + +internal data class ParamPath( + val rootId: String, + val segments: List +) { + val isTopLevel: Boolean + get() = segments.isEmpty() + + sealed interface Segment { + data class Key(val value: String) : Segment + data class Index(val value: Int) : Segment + } + + companion object { + private const val ENCODED_SEGMENT_PREFIX = "%" + + fun parse(rawPath: String): ParamPath { + val parts = splitPath(rawPath) + require(parts.isNotEmpty() && parts.first().isNotBlank()) { + "Parameter path must start with a root id" + } + return ParamPath( + rootId = parts.first(), + segments = parts.drop(1).map { rawSegment -> + decodeSegmentOrNull(rawSegment)?.let(Segment::Key) + ?: rawSegment.toIntOrNull()?.let(Segment::Index) + ?: Segment.Key(rawSegment) + } + ) + } + + fun encodeSegment(rawSegment: String): String { + val bytes = rawSegment.toByteArray(StandardCharsets.UTF_8) + val hex = bytes.joinToString(separator = "") { byte -> + "%02x".format(byte.toInt() and 0xff) + } + return ENCODED_SEGMENT_PREFIX + hex + } + + private fun splitPath(rawPath: String): List { + return rawPath.split('.') + } + + private fun decodeSegmentOrNull(rawSegment: String): String? { + if (!rawSegment.startsWith(ENCODED_SEGMENT_PREFIX)) return null + val hex = rawSegment.removePrefix(ENCODED_SEGMENT_PREFIX) + if (hex.length % 2 != 0) return rawSegment + val bytes = ByteArray(hex.length / 2) + for (index in bytes.indices) { + val start = index * 2 + val value = hex.substring(start, start + 2).toIntOrNull(16) ?: return rawSegment + bytes[index] = value.toByte() + } + return String(bytes, StandardCharsets.UTF_8) + } + } +} diff --git a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ParameterTreeEditor.kt b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ParameterTreeEditor.kt new file mode 100644 index 00000000..1e592601 --- /dev/null +++ b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ParameterTreeEditor.kt @@ -0,0 +1,96 @@ +package com.chaomixian.vflow.ui.workflow_editor + +internal class ParameterTreeEditor( + private val parameters: MutableMap +) { + fun set(path: ParamPath, value: Any?) { + if (path.isTopLevel) { + parameters[path.rootId] = value + return + } + + val updatedRoot = setNested(parameters[path.rootId], path.segments, value) ?: return + parameters[path.rootId] = updatedRoot + } + + fun clear(path: ParamPath, topLevelDefaultValue: Any?) { + if (path.isTopLevel) { + parameters[path.rootId] = topLevelDefaultValue + return + } + + val currentRoot = parameters[path.rootId] ?: return + val updatedRoot = clearNested(currentRoot, path.segments) ?: return + parameters[path.rootId] = updatedRoot + } + + private fun setNested( + currentValue: Any?, + segments: List, + newValue: Any? + ): Any? { + val segment = segments.first() + val remaining = segments.drop(1) + return when (segment) { + is ParamPath.Segment.Key -> { + val map = currentValue.toEditableMap() ?: mutableMapOf() + if (remaining.isEmpty()) { + map[segment.value] = newValue + } else { + val updatedChild = setNested(map[segment.value], remaining, newValue) ?: return null + map[segment.value] = updatedChild + } + map + } + + is ParamPath.Segment.Index -> { + val list = (currentValue as? List<*>)?.toMutableList() ?: return null + if (segment.value !in list.indices) return null + if (remaining.isEmpty()) { + list[segment.value] = newValue + } else { + val updatedChild = setNested(list[segment.value], remaining, newValue) ?: return null + list[segment.value] = updatedChild + } + list + } + } + } + + private fun clearNested( + currentValue: Any?, + segments: List + ): Any? { + val segment = segments.first() + val remaining = segments.drop(1) + return when (segment) { + is ParamPath.Segment.Key -> { + val map = currentValue.toEditableMap() ?: return null + if (remaining.isEmpty()) { + map[segment.value] = "" + } else { + val updatedChild = clearNested(map[segment.value], remaining) ?: return null + map[segment.value] = updatedChild + } + map + } + + is ParamPath.Segment.Index -> { + val list = (currentValue as? List<*>)?.toMutableList() ?: return null + if (segment.value !in list.indices) return null + if (remaining.isEmpty()) { + list[segment.value] = "" + } else { + val updatedChild = clearNested(list[segment.value], remaining) ?: return null + list[segment.value] = updatedChild + } + list + } + } + } + + private fun Any?.toEditableMap(): MutableMap? { + val map = this as? Map<*, *> ?: return null + return map.entries.associate { it.key to it.value }.toMutableMap() + } +} diff --git a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/PillRenderer.kt b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/PillRenderer.kt index 0095c175..f1227630 100644 --- a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/PillRenderer.kt +++ b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/PillRenderer.kt @@ -4,12 +4,19 @@ package com.chaomixian.vflow.ui.workflow_editor import android.content.Context import android.graphics.* +import android.text.Spanned import android.text.SpannableStringBuilder import android.text.style.ReplacementSpan +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView import com.chaomixian.vflow.R +import com.chaomixian.vflow.core.execution.VariableResolver import com.chaomixian.vflow.core.module.isMagicVariable import com.chaomixian.vflow.core.module.isNamedVariable import com.chaomixian.vflow.core.workflow.model.ActionStep +import com.chaomixian.vflow.ui.workflow_editor.pill.ParameterPillSpan import com.chaomixian.vflow.ui.workflow_editor.pill.PillTheme import com.chaomixian.vflow.ui.workflow_editor.pill.PillVariableResolver import kotlin.math.roundToInt @@ -28,6 +35,18 @@ import kotlin.math.roundToInt */ object PillRenderer { + /** + * 显示样式。 + * AUTO 会根据内容自动判断当前更适合的 pill 渲染方式。 + */ + enum class DisplayStyle { + AUTO, + PLAIN, + SUMMARY, + RICH_TEXT, + EDITOR + } + /** * 渲染模式枚举 * EDIT: 编辑模式,用于 EditText,保留原始变量引用 @@ -35,6 +54,87 @@ object PillRenderer { */ enum class RenderMode { EDIT, PREVIEW } + fun resolveDisplayName( + context: Context, + variableReference: String, + allSteps: List + ): String { + return PillVariableResolver.resolveVariable(context, variableReference, allSteps)?.displayName + ?: fallbackDisplayName(variableReference) + } + + /** + * 统一的显示入口。 + * + * 它会根据内容类型和显式样式决定采用: + * - 结构化摘要 pill 渲染 + * - 富文本变量 pill 渲染 + * - 编辑器渲染 + * - 原样文本 + */ + fun renderDisplayText( + context: Context, + content: CharSequence?, + allSteps: List, + style: DisplayStyle = DisplayStyle.AUTO + ): CharSequence? { + content ?: return null + return when (resolveDisplayStyle(content, style)) { + DisplayStyle.PLAIN -> content + DisplayStyle.SUMMARY -> renderStructuredSummary(context, content, allSteps) + DisplayStyle.RICH_TEXT -> renderToSpannable(content.toString(), RenderMode.PREVIEW, allSteps, context) + DisplayStyle.EDITOR -> renderToSpannable(content.toString(), RenderMode.EDIT, allSteps, context) + DisplayStyle.AUTO -> content + } + } + + fun createPreviewTextView( + context: Context, + parent: ViewGroup, + content: CharSequence, + allSteps: List, + style: DisplayStyle + ): View { + val inflater = LayoutInflater.from(context) + val view = inflater.inflate(R.layout.partial_rich_text_preview, parent, false).apply { + findViewById(R.id.rich_text_preview_content).text = renderDisplayText( + context = context, + content = content, + allSteps = allSteps, + style = style + ) + } + + view.layoutParams = ViewGroup.MarginLayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ).apply { + topMargin = (8 * context.resources.displayMetrics.density).toInt() + } + return view + } + + private fun resolveDisplayStyle( + content: CharSequence, + requestedStyle: DisplayStyle + ): DisplayStyle { + if (requestedStyle != DisplayStyle.AUTO) { + return requestedStyle + } + + if (content is Spanned && + content.getSpans(0, content.length, ParameterPillSpan::class.java).isNotEmpty() + ) { + return DisplayStyle.SUMMARY + } + + return if (VariableResolver.hasVariableReference(content.toString())) { + DisplayStyle.RICH_TEXT + } else { + DisplayStyle.PLAIN + } + } + // ========== 兼容层(已废弃,保留向后兼容) ========== /** @@ -44,33 +144,10 @@ object PillRenderer { */ @Deprecated("Use PillVariableResolver.resolveVariable() for complete information") fun getDisplayNameForVariableReference(variableReference: String, allSteps: List, context: Context): String { - val varInfo = com.chaomixian.vflow.core.execution.VariableInfo.fromReference(variableReference, allSteps) - if (varInfo != null) { - val propName = when { - variableReference.startsWith("[[") -> { - val content = variableReference.removeSurrounding("[[", "]]") - val parts = content.split('.') - if (parts.size > 1) parts[1] else null - } - variableReference.startsWith("{{") -> { - val content = variableReference.removeSurrounding("{{", "}}") - val parts = content.split('.') - if (parts.size > 2) parts[2] else null - } - else -> null - } + return resolveDisplayName(context, variableReference, allSteps) + } - return if (propName != null) { - val propDisplay = varInfo.getPropertyDisplayName(context, propName) - context.getString( - R.string.magic_variable_property_name, - varInfo.getLocalizedSourceName(context), - propDisplay - ) - } else { - varInfo.getLocalizedSourceName(context) - } - } + private fun fallbackDisplayName(variableReference: String): String { return variableReference } @@ -78,22 +155,15 @@ object PillRenderer { return if (text.length > maxLength) text.take(maxLength) + "..." else text } - /** - * 渲染 Pills(兼容层,用于 ActionStepAdapter) - * - * @deprecated 此方法仅供旧代码使用,新代码应使用 renderToSpannable() - */ - @Deprecated("Use renderToSpannable() for new code") - fun renderPills( + private fun renderStructuredSummary( context: Context, - summary: CharSequence?, - allSteps: List, - currentStep: ActionStep - ): CharSequence? { + summary: CharSequence, + allSteps: List + ): CharSequence { if (summary !is android.text.Spanned) return summary val spannable = SpannableStringBuilder(summary) - spannable.getSpans(0, spannable.length, com.chaomixian.vflow.ui.workflow_editor.pill.ParameterPillSpan::class.java) + spannable.getSpans(0, spannable.length, ParameterPillSpan::class.java) .reversed().forEach { span -> val start = spannable.getSpanStart(span) val end = spannable.getSpanEnd(span) @@ -111,8 +181,11 @@ object PillRenderer { // 纯变量引用:替换为显示名称 isPureVariable -> { val resolvedInfo = PillVariableResolver.resolveVariable(context, reference, allSteps) - val displayName = resolvedInfo?.displayName ?: "变量" - pillText = truncate(displayName) + pillText = if (resolvedInfo != null) { + truncate(resolvedInfo.displayName) + } else { + reference + } color = resolvedInfo?.color ?: PillTheme.getColor(context, R.color.variable_pill_color) } // 包含变量引用的混合文本:保持原样,让 RoundedBackgroundSpan 渲染嵌套Pill @@ -131,7 +204,7 @@ object PillRenderer { val newEnd = start + pillText.length spannable.setSpan(RoundedBackgroundSpan(context, color, null, allSteps), start, newEnd, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - spannable.setSpan(com.chaomixian.vflow.ui.workflow_editor.pill.ParameterPillSpan(span.parameterId), start, newEnd, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + spannable.setSpan(ParameterPillSpan(span.parameterId), start, newEnd, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } return spannable } @@ -265,15 +338,6 @@ object PillRenderer { return spannable } - /** - * 在渲染富文本时,为每个药丸渲染 pill - * @deprecated 使用 renderToSpannable() 替代 - */ - fun renderRichTextToSpannable(context: Context, rawText: String, allSteps: List): SpannableStringBuilder { - // 直接委托给新的统一方法,使用 PREVIEW 模式 - return renderToSpannable(rawText, RenderMode.PREVIEW, allSteps, context) - } - /** * Pill 背景 Span(圆角矩形背景) * diff --git a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/PillUtil.kt b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/PillUtil.kt index d7030f20..00179c1c 100644 --- a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/PillUtil.kt +++ b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/PillUtil.kt @@ -1,21 +1,15 @@ -// 文件: main/java/com/chaomixian/vflow/ui/workflow_editor/PillUtil.kt -// 描述: Pill工具类(兼容层)- 委托给新的架构 -// -// @deprecated 此类为兼容层保留,新代码应使用: -// - com.chaomixian.vflow.core.pill.PillFormatter (创建Pill) -// - com.chaomixian.vflow.ui.workflow_editor.pill.AndroidPillBuilder (构建Spannable) -// - com.chaomixian.vflow.ui.workflow_editor.pill.PillTheme (颜色管理) -// - com.chaomixian.vflow.ui.workflow_editor.pill.ParameterPillSpan (点击标记) - package com.chaomixian.vflow.ui.workflow_editor import android.content.Context import android.graphics.Canvas import android.graphics.drawable.Drawable import android.text.Spannable +import android.text.SpannableStringBuilder import android.view.LayoutInflater import android.view.View import android.widget.TextView +import androidx.core.graphics.createBitmap +import androidx.core.graphics.drawable.toDrawable import com.chaomixian.vflow.R import com.chaomixian.vflow.core.module.InputDefinition import com.chaomixian.vflow.core.module.ParameterType @@ -24,55 +18,16 @@ import com.chaomixian.vflow.core.logging.LogManager import com.chaomixian.vflow.core.pill.Pill as CorePill import com.chaomixian.vflow.core.pill.PillFormatter import com.chaomixian.vflow.core.pill.PillType -import com.chaomixian.vflow.ui.workflow_editor.pill.AndroidPillBuilder import com.chaomixian.vflow.ui.workflow_editor.pill.ParameterPillSpan import com.chaomixian.vflow.ui.workflow_editor.pill.PillTheme -import androidx.core.graphics.createBitmap -import androidx.core.graphics.drawable.toDrawable -/** - * 可点击参数药丸的 Span(兼容别名) - * - * @deprecated 使用 com.chaomixian.vflow.ui.workflow_editor.pill.ParameterPillSpan 替代 - */ -@Deprecated( - "Use ParameterPillSpan from pill package instead", - ReplaceWith("ParameterPillSpan", "com.chaomixian.vflow.ui.workflow_editor.pill.ParameterPillSpan") -) -typealias ParameterPillSpan = com.chaomixian.vflow.ui.workflow_editor.pill.ParameterPillSpan - -/** - * 参数药丸 (Pill) UI 工具类(兼容层) - * - * @deprecated 此类保留用于向后兼容。新代码应使用: - * - PillFormatter.createPillFromParam() 替代 createPillFromParam() - * - AndroidPillBuilder().build() 替代 buildSpannable() - * - PillTheme.getCategoryColor() 替代 getCategoryColor() - * - ParameterPillSpan 替代 PillUtil.ParameterPillSpan - */ -@Deprecated( - "Use PillFormatter from core.pill package instead", - ReplaceWith("PillFormatter", "com.chaomixian.vflow.core.pill.PillFormatter") -) object PillUtil { - /** - * 参数药丸的数据模型(兼容层) - * - * @deprecated 使用 com.chaomixian.vflow.core.pill.Pill 替代 - */ - @Deprecated( - "Use Pill from core.pill package instead", - ReplaceWith("CorePill", "com.chaomixian.vflow.core.pill.Pill") - ) data class Pill( val text: String, val parameterId: String, val isModuleOption: Boolean = false, ) { - /** - * 转换为新的核心Pill对象 - */ fun toCorePill(): CorePill { return CorePill( text = text, @@ -82,9 +37,6 @@ object PillUtil { } companion object { - /** - * 从核心Pill对象转换为旧的Pill对象 - */ fun fromCorePill(corePill: CorePill): Pill { return Pill( text = corePill.text, @@ -95,15 +47,21 @@ object PillUtil { } } - /** - * 从模块参数创建 Pill(兼容方法) - * - * @deprecated 使用 PillFormatter.createPillFromParam() 替代 - */ - @Deprecated( - "Use PillFormatter.createPillFromParam() instead", - ReplaceWith("PillFormatter.createPillFromParam(paramValue, inputDef, if (isModuleOption) PillType.MODULE_OPTION else PillType.PARAMETER)") + data class RichTextPill( + val rawText: String, + val onlyWhenComplex: Boolean = true ) + + internal sealed interface SummaryPart { + data class Inline(val content: CharSequence) : SummaryPart + data class Rich(val pill: RichTextPill) : SummaryPart + } + + class SummaryContent internal constructor( + internal val parts: List, + private val plainText: CharSequence + ) : CharSequence by plainText + fun createPillFromParam( paramValue: Any?, inputDef: InputDefinition?, @@ -117,6 +75,83 @@ object PillUtil { return Pill.fromCorePill(corePill) } + fun richTextPreview( + rawText: String?, + onlyWhenComplex: Boolean = true + ): RichTextPill? { + val normalized = rawText?.takeIf { it.isNotEmpty() } ?: return null + return RichTextPill( + rawText = normalized, + onlyWhenComplex = onlyWhenComplex + ) + } + + fun buildSpannable(context: Context, vararg parts: Any?): CharSequence { + val summaryParts = mutableListOf() + val currentInline = SpannableStringBuilder() + + fun flushInline() { + if (currentInline.isNotEmpty()) { + summaryParts += SummaryPart.Inline(SpannableStringBuilder(currentInline)) + currentInline.clear() + } + } + + parts.forEach { part -> + when (part) { + null -> Unit + is String -> currentInline.append(part) + is Pill -> appendPill(currentInline, part) + is CorePill -> appendPill(currentInline, Pill.fromCorePill(part)) + is RichTextPill -> { + flushInline() + summaryParts += SummaryPart.Rich(part) + } + is SummaryContent -> { + flushInline() + summaryParts += part.parts + } + is CharSequence -> currentInline.append(part) + else -> Unit + } + } + flushInline() + + if (summaryParts.none { it is SummaryPart.Rich }) { + return summaryParts + .filterIsInstance() + .firstOrNull() + ?.content + ?: SpannableStringBuilder() + } + + val plainText = SpannableStringBuilder().apply { + summaryParts.forEach { part -> + when (part) { + is SummaryPart.Inline -> append(part.content) + is SummaryPart.Rich -> { + if (isNotEmpty()) append('\n') + append(part.pill.rawText) + } + } + } + } + return SummaryContent(summaryParts, plainText) + } + + internal fun splitSummaryContent(content: CharSequence?): List { + return when (content) { + null -> emptyList() + is SummaryContent -> content.parts.map { + when (it) { + is SummaryPart.Inline -> it.content + is SummaryPart.Rich -> it.pill + } + } + else -> listOf(content) + } + } + private fun localizeEnumValue(paramValue: Any?, inputDef: InputDefinition?): Any? { if (inputDef?.staticType != ParameterType.ENUM) return paramValue @@ -134,33 +169,6 @@ object PillUtil { return localizedOptions.getOrNull(optionIndex) ?: normalizedValue } - /** - * 构建包含药丸"结构"的 Spannable 文本(兼容方法) - * - * @deprecated 使用 AndroidPillBuilder(context).build() 替代 - */ - @Deprecated( - "Use AndroidPillBuilder(context).build() instead", - ReplaceWith("AndroidPillBuilder(context).build(*parts)") - ) - fun buildSpannable(context: Context, vararg parts: Any): CharSequence { - // 将旧的Pill对象转换为核心Pill对象 - val convertedParts = parts.map { part -> - when (part) { - is Pill -> part.toCorePill() - else -> part - } - }.toTypedArray() - - @Suppress("UNCHECKED_CAST") - return AndroidPillBuilder(context).build(*convertedParts) as CharSequence - } - - /** - * 创建一个用于在 EditText 中显示的"药丸"Drawable。 - * - * 注意:此方法暂时保留,未来可能会移到专门的UI工具类中。 - */ fun createPillDrawable(context: Context, text: String): Drawable { val pillView = LayoutInflater.from(context).inflate(R.layout.magic_variable_pill, null) val textView = pillView.findViewById(R.id.pill_text) @@ -181,16 +189,14 @@ object PillUtil { } } - /** - * 根据模块分类获取颜色资源ID(兼容方法) - * - * @deprecated 使用 PillTheme.getCategoryColor() 替代 - */ - @Deprecated( - "Use PillTheme.getCategoryColor() instead", - ReplaceWith("PillTheme.getCategoryColor(category)") - ) fun getCategoryColor(category: String): Int { return PillTheme.getCategoryColor(category) } + + private fun appendPill(builder: SpannableStringBuilder, pill: Pill) { + val start = builder.length + builder.append(" ${pill.text} ") + val end = builder.length + builder.setSpan(ParameterPillSpan(pill.parameterId), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } } diff --git a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/RichTextUIProvider.kt b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/RichTextUIProvider.kt deleted file mode 100644 index 7afa58dc..00000000 --- a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/RichTextUIProvider.kt +++ /dev/null @@ -1,56 +0,0 @@ -// 文件: main/java/com/chaomixian/vflow/ui/workflow_editor/RichTextUIProvider.kt -package com.chaomixian.vflow.ui.workflow_editor - -import android.content.Context -import android.content.Intent -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import com.chaomixian.vflow.R -import com.chaomixian.vflow.core.execution.VariableResolver // 引入 -import com.chaomixian.vflow.core.module.CustomEditorViewHolder -import com.chaomixian.vflow.core.module.ModuleUIProvider -import com.chaomixian.vflow.core.workflow.model.ActionStep - -class RichTextUIProvider(private val richTextInputId: String) : ModuleUIProvider { - - override fun getHandledInputIds(): Set = emptySet() - - override fun createEditor( - context: Context, parent: ViewGroup, currentParameters: Map, - onParametersChanged: () -> Unit, onMagicVariableRequested: ((String) -> Unit)?, - allSteps: List?, onStartActivityForResult: ((Intent, (Int, Intent?) -> Unit) -> Unit)? - ): CustomEditorViewHolder { - throw NotImplementedError("RichTextUIProvider does not create a custom editor.") - } - - override fun readFromEditor(holder: CustomEditorViewHolder): Map { - throw NotImplementedError("RichTextUIProvider does not read from a custom editor.") - } - - /** - * 创建预览视图。 - * 使用 VariableResolver.isComplex 统一判断标准。 - */ - override fun createPreview( - context: Context, parent: ViewGroup, step: ActionStep, allSteps: List, - onStartActivityForResult: ((Intent, (resultCode: Int, data: Intent?) -> Unit) -> Unit)? - ): View? { - val rawText = step.parameters[richTextInputId]?.toString() ?: "" - - // 如果内容不复杂,则不创建自定义预览,让 ActionStepAdapter 回退到 getSummary - if (!VariableResolver.isComplex(rawText)) { - return null - } - - val inflater = LayoutInflater.from(context) - val previewView = inflater.inflate(R.layout.partial_rich_text_preview, parent, false) - val textView = previewView.findViewById(R.id.rich_text_preview_content) - - val spannable = PillRenderer.renderRichTextToSpannable(context, rawText, allSteps) - textView.text = spannable - - return previewView - } -} \ No newline at end of file diff --git a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/RichTextView.kt b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/RichTextView.kt index 3198c82c..0b29b2ed 100644 --- a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/RichTextView.kt +++ b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/RichTextView.kt @@ -28,9 +28,6 @@ class RichTextView @JvmOverloads constructor( // 保存 allSteps 引用,用于 insertVariable 方法 private var currentAllSteps: List = emptyList() - // 可选的变量类型过滤器(例如只允许图片类型) - private var variableTypeFilter: Set? = null - /** * 设置富文本内容 * @@ -80,19 +77,4 @@ class RichTextView @JvmOverloads constructor( // 将光标移动到插入内容的末尾 setSelection(start + pillSpannable.length) } - - /** - * 设置变量类型过滤器 - * 设置后,只有符合类型的变量才能被插入(用于变量选择器过滤) - * - * @param types 允许的变量类型集合(例如 setOf("vflow.type.image")) - */ - fun setVariableTypeFilter(types: Set?) { - variableTypeFilter = types - } - - /** - * 获取变量类型过滤器 - */ - fun getVariableTypeFilter(): Set? = variableTypeFilter } diff --git a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/StandardControlFactory.kt b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/StandardControlFactory.kt index 72347cf7..e0763f09 100644 --- a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/StandardControlFactory.kt +++ b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/StandardControlFactory.kt @@ -488,10 +488,10 @@ object StandardControlFactory { ): View { val pill = LayoutInflater.from(context).inflate(R.layout.magic_variable_pill, parent, false) val pillText = pill.findViewById(R.id.pill_text) - pillText.text = PillRenderer.getDisplayNameForVariableReference( - variableReference, - allSteps ?: emptyList(), - context + pillText.text = PillRenderer.resolveDisplayName( + context = context, + variableReference = variableReference, + allSteps = allSteps ?: emptyList() ) onClick?.let { pill.setOnClickListener { it() } } return pill diff --git a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/VariableValueUIProvider.kt b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/VariableValueUIProvider.kt index 3d4714ad..a9cfbb8c 100644 --- a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/VariableValueUIProvider.kt +++ b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/VariableValueUIProvider.kt @@ -11,25 +11,26 @@ import android.view.ViewGroup import android.widget.LinearLayout import android.widget.TextView import com.chaomixian.vflow.R -import com.chaomixian.vflow.core.module.CustomEditorViewHolder -import com.chaomixian.vflow.core.module.ModuleUIProvider import com.chaomixian.vflow.core.module.isMagicVariable import com.chaomixian.vflow.core.module.isNamedVariable import com.chaomixian.vflow.core.workflow.module.data.CreateVariableModule import com.chaomixian.vflow.core.workflow.model.ActionStep /** - * 一个可复用的 ModuleUIProvider,专门用于在步骤卡片中 + * 一个可复用的预览 helper,专门用于在步骤卡片中 * 详细地以内联方式显示字典和列表的内容。 */ -class VariableValueUIProvider : ModuleUIProvider { +object VariableValueUIProvider { - override fun getHandledInputIds(): Set = setOf("value") + private fun buildSummaryLine(context: Context, prefix: String, value: Any?): CharSequence { + return PillUtil.buildSpannable( + context, + prefix, + PillUtil.Pill(value?.toString() ?: "", "value") + ) + } - /** - * 现在会先加载 CardView,再找到内部的 LinearLayout 来填充内容。 - */ - override fun createPreview( + fun createPreview( context: Context, parent: ViewGroup, step: ActionStep, @@ -45,12 +46,9 @@ class VariableValueUIProvider : ModuleUIProvider { } val inflater = LayoutInflater.from(context) - // 加载整个卡片视图 val cardView = inflater.inflate(R.layout.partial_variable_preview, parent, false) - // 从卡片中找到用于填充内容的容器 val previewContainer = cardView.findViewById(R.id.variable_preview_container) - when (type) { CreateVariableModule.TYPE_DICTIONARY -> { @Suppress("UNCHECKED_CAST") @@ -58,10 +56,14 @@ class VariableValueUIProvider : ModuleUIProvider { if (map.isEmpty()) return null map.forEach { (key, mapValue) -> - val summaryLine = PillUtil.buildSpannable(context, "$key: ", PillUtil.Pill(mapValue.toString(), "value")) - val renderedLine = PillRenderer.renderPills(context, summaryLine, allSteps, step) - val textView = createTextView(context, renderedLine ?: "") - previewContainer.addView(textView) + val summaryLine = buildSummaryLine(context, "$key: ", mapValue) + val renderedLine = PillRenderer.renderDisplayText( + context = context, + content = summaryLine, + allSteps = allSteps, + style = PillRenderer.DisplayStyle.SUMMARY + ) + previewContainer.addView(createTextView(context, renderedLine ?: "")) } } CreateVariableModule.TYPE_LIST -> { @@ -70,16 +72,19 @@ class VariableValueUIProvider : ModuleUIProvider { if (list.isEmpty()) return null list.forEachIndexed { index, item -> - val summaryLine = PillUtil.buildSpannable(context, "${index + 1}. ", PillUtil.Pill(item.toString(), "value")) - val renderedLine = PillRenderer.renderPills(context, summaryLine, allSteps, step) - val textView = createTextView(context, renderedLine ?: "") - previewContainer.addView(textView) + val summaryLine = buildSummaryLine(context, "${index + 1}. ", item) + val renderedLine = PillRenderer.renderDisplayText( + context = context, + content = summaryLine, + allSteps = allSteps, + style = PillRenderer.DisplayStyle.SUMMARY + ) + previewContainer.addView(createTextView(context, renderedLine ?: "")) } } else -> return null } - // 只有当容器内确实有内容时,才返回整个卡片视图 return if (previewContainer.childCount > 0) cardView else null } @@ -92,15 +97,4 @@ class VariableValueUIProvider : ModuleUIProvider { } } - override fun createEditor( - context: Context, parent: ViewGroup, currentParameters: Map, - onParametersChanged: () -> Unit, onMagicVariableRequested: ((String) -> Unit)?, - allSteps: List?, onStartActivityForResult: ((Intent, (Int, Intent?) -> Unit) -> Unit)? - ): CustomEditorViewHolder { - throw NotImplementedError("VariableValueUIProvider does not create a custom editor.") - } - - override fun readFromEditor(holder: CustomEditorViewHolder): Map { - throw NotImplementedError("VariableValueUIProvider does not read from a custom editor.") - } } diff --git a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/WorkflowEditorActivity.kt b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/WorkflowEditorActivity.kt index 68d49c67..b8a048ad 100644 --- a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/WorkflowEditorActivity.kt +++ b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/WorkflowEditorActivity.kt @@ -34,7 +34,6 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearSmoothScroller import androidx.recyclerview.widget.RecyclerView import com.chaomixian.vflow.R -import com.chaomixian.vflow.core.execution.ExecutionState import com.chaomixian.vflow.core.execution.ExecutionStateBus import com.chaomixian.vflow.core.execution.WorkflowExecutor import com.chaomixian.vflow.core.module.* @@ -66,8 +65,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.gson.Gson -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch import java.util.* /** @@ -96,10 +93,8 @@ class WorkflowEditorActivity : BaseActivity() { private var initialWorkflowJson: String? = null private var pendingExecutionWorkflow: Workflow? = null - - // 跟踪当前正在执行的工作流 ID(包括未保存的工作流) - private var currentlyExecutingWorkflowId: String? = null - private var currentlyExecutingExecutionInstanceId: String? = null + private lateinit var executionTracker: WorkflowEditorExecutionTracker + private lateinit var magicVariableCatalogBuilder: WorkflowEditorMagicVariableCatalogBuilder private var listBeforeDrag: List? = null private var dragStartPosition: Int = -1 @@ -129,20 +124,14 @@ class WorkflowEditorActivity : BaseActivity() { ) { result -> if (result.resultCode == Activity.RESULT_OK) { pendingExecutionWorkflow?.let { - // 记录正在执行的工作流 ID,以便正确跟踪状态 - currentlyExecutingWorkflowId = it.id - currentlyExecutingExecutionInstanceId = null + executionTracker.beginExecutionTracking(it.id) toast(getString(R.string.editor_toast_execution_start, it.name)) val executionInstanceId = WorkflowExecutor.execute( workflow = it, context = this, triggerStepId = it.manualTrigger()?.id ) - if (executionInstanceId.isBlank()) { - currentlyExecutingWorkflowId = null - } else { - currentlyExecutingExecutionInstanceId = executionInstanceId - } + executionTracker.finishExecutionLaunch(executionInstanceId) } } pendingExecutionWorkflow = null @@ -206,14 +195,42 @@ class WorkflowEditorActivity : BaseActivity() { applyWindowInsets() workflowManager = WorkflowManager(this) + magicVariableCatalogBuilder = WorkflowEditorMagicVariableCatalogBuilder(this, workflowManager) + bindViews() + setupExecutionTracker() + setupNavigation() + setupRecyclerView() + setupInspectorInsertController() + setupRecyclerFocusDismiss() + setupPickerHandler() + restoreOrLoadEditorState(savedInstanceState) + setupDragAndDrop() + setupUndoControls() + setupPrimaryActions() + observeExecutionState() + } + + private fun bindViews() { nameEditText = findViewById(R.id.edit_text_workflow_name) editorMoreButton = findViewById(R.id.btn_editor_more) undoButton = findViewById(R.id.button_undo_edit) executeButton = findViewById(R.id.button_execute_workflow) recyclerView = findViewById(R.id.recycler_view_action_steps) configureExecuteButtonShadow() + } + private fun setupExecutionTracker() { + executionTracker = WorkflowEditorExecutionTracker( + isWorkflowRunning = WorkflowExecutor::isRunning, + stopWorkflowExecution = WorkflowExecutor::stopExecution, + updateExecuteButton = ::updateExecuteButton, + highlightStep = ::highlightStep, + highlightStepAsFailed = ::highlightStepAsFailed, + clearHighlight = ::clearHighlight + ) + } + private fun setupNavigation() { val toolbar = findViewById(R.id.toolbar_editor) toolbar.setNavigationOnClickListener { handleExitRequest() } onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { @@ -221,8 +238,9 @@ class WorkflowEditorActivity : BaseActivity() { handleExitRequest() } }) + } - setupRecyclerView() + private fun setupInspectorInsertController() { inspectorInsertController = WorkflowInspectorInsertController( activity = this, actionSteps = actionSteps, @@ -231,16 +249,18 @@ class WorkflowEditorActivity : BaseActivity() { onStepsChanged = { recalculateAndNotify() } ) inspectorInsertController.register() + } - // 点击内容区域时清除标题输入框焦点并关闭键盘 + private fun setupRecyclerFocusDismiss() { recyclerView.setOnTouchListener { _, _ -> val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager imm.hideSoftInputFromWindow(nameEditText.windowToken, 0) nameEditText.clearFocus() false } + } - // 初始化 PickerHandler + private fun setupPickerHandler() { pickerHandler = PickerHandler( activity = this, appPickerLauncher = generalIntentLauncher, @@ -252,108 +272,70 @@ class WorkflowEditorActivity : BaseActivity() { currentEditorSheet?.updateParametersAndRebuildUi(params) } ) + } - // 检查是否有已保存的状态,如果有则恢复,否则才从数据库加载 + private fun restoreOrLoadEditorState(savedInstanceState: Bundle?) { if (savedInstanceState != null) { - val savedTriggerSteps = savedInstanceState.getParcelableArrayList(STATE_TRIGGER_STEPS) - val savedSteps = savedInstanceState.getParcelableArrayList(STATE_ACTION_STEPS) - if (savedTriggerSteps != null) { - triggerSteps.clear() - triggerSteps.addAll(savedTriggerSteps) - } - if (savedSteps != null) { - actionSteps.clear() - actionSteps.addAll(savedSteps) - } - nameEditText.setText(savedInstanceState.getString(STATE_WORKFLOW_NAME)) - - // 恢复 currentWorkflow 对象以正确显示执行按钮状态 - val workflowId = intent.getStringExtra(EXTRA_WORKFLOW_ID) - if (workflowId != null) { - currentWorkflow = workflowManager.getWorkflow(workflowId) - currentWorkflow?.let { - val isRunning = WorkflowExecutor.isRunning(it.id) - updateExecuteButton(isRunning) - if (isRunning) { - currentlyExecutingWorkflowId = it.id - currentlyExecutingExecutionInstanceId = null - } - initialWorkflowJson = gson.toJson(getCurrentWorkflowState()) - } - } - recalculateAndNotify() // 通知适配器数据已恢复 - } else { - // 首次创建时,从数据库加载数据 - loadWorkflowData() + restoreEditorState(savedInstanceState) + return } + loadWorkflowData() + } - setupDragAndDrop() - setupUndoControls() + private fun restoreEditorState(savedInstanceState: Bundle) { + restoreSavedSteps(savedInstanceState) + nameEditText.setText(savedInstanceState.getString(STATE_WORKFLOW_NAME)) + restoreCurrentWorkflow() + currentWorkflow?.let { initialWorkflowJson = gson.toJson(getCurrentWorkflowState()) } + recalculateAndNotify() + } - // “添加动作”按钮现在只负责添加普通动作,不再处理触发器 - findViewById