Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.chaomixian.vflow.core.types.EnhancedBaseVObject
import com.chaomixian.vflow.core.types.VObject
import com.chaomixian.vflow.core.types.VTypeRegistry
import com.chaomixian.vflow.core.types.basic.VBoolean
import com.chaomixian.vflow.core.types.basic.VList
import com.chaomixian.vflow.core.types.basic.VNull
import com.chaomixian.vflow.core.types.basic.VString
import com.chaomixian.vflow.core.types.properties.PropertyRegistry
Expand Down Expand Up @@ -51,6 +52,7 @@ class VUiComponent(
com.chaomixian.vflow.core.workflow.module.ui.model.UiElementType.BUTTON -> element.label
com.chaomixian.vflow.core.workflow.module.ui.model.UiElementType.INPUT -> currentValue?.toString() ?: element.defaultValue
com.chaomixian.vflow.core.workflow.module.ui.model.UiElementType.SWITCH -> currentValue?.toString() ?: element.defaultValue
com.chaomixian.vflow.core.workflow.module.ui.model.UiElementType.LIST_PICKER -> currentValue?.toString() ?: element.defaultValue
}

override fun asNumber(): Double? = null
Expand Down Expand Up @@ -108,6 +110,13 @@ class VUiComponent(
register("isswitch", returnType = VTypeRegistry.BOOLEAN, displayName = "是否开关", nameStringRes = R.string.vtype_uicomponent_isswitch, getter = { host ->
VBoolean((host as VUiComponent).element.type == com.chaomixian.vflow.core.workflow.module.ui.model.UiElementType.SWITCH)
})
register("ispicker", "ispick", "islistpicker", returnType = VTypeRegistry.BOOLEAN, displayName = "是否列表选择", nameStringRes = R.string.vtype_uicomponent_ispicker, getter = { host ->
VBoolean((host as VUiComponent).element.type == com.chaomixian.vflow.core.workflow.module.ui.model.UiElementType.LIST_PICKER)
})
register("options", "items", returnType = VTypeRegistry.LIST, displayName = "选项", nameStringRes = R.string.vtype_uicomponent_options, getter = { host ->
val options = (host as VUiComponent).element.options
VList(options.map { VString(it) })
})
}

fun propertyDefs(): List<com.chaomixian.vflow.core.types.VPropertyDef> = registry.toPropertyDefs()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,11 +266,12 @@ object ModuleRegistry {
register(EndFloatWindowModule(), context)


// UI 组件 (文本 / 输入 / 按钮 / 开关)
// UI 组件 (文本 / 输入 / 按钮 / 开关 / 列表选择)
register(UiTextModule(), context)
register(UiInputModule(), context)
register(UiButtonModule(), context)
register(UiSwitchModule(), context)
register(UiListPickerModule(), context)
register(ScreenFlashModule(), context)

// 交互逻辑 (事件监听 / 更新 / 关闭 / 获取值)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// 文件: java/com/chaomixian/vflow/core/workflow/module/ui/components/UiListPickerModule.kt
package com.chaomixian.vflow.core.workflow.module.ui.components

import android.content.Context
import com.chaomixian.vflow.R
import com.chaomixian.vflow.core.execution.ExecutionContext
import com.chaomixian.vflow.core.execution.VariableResolver
import com.chaomixian.vflow.core.module.*
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

/**
* 列表选择(单选)组件
*
* 让用户从一个候选列表里挑一个值。底层使用 Material 3 下拉菜单
* (MaterialAutoCompleteTextView)。
*
* 参数:
* - key: 组件 ID,也是变量名(必填)
* - label: 选择框上方的提示文案
* - options: 候选项,按换行符分隔(每行一项)
* - default_value: 默认选中的项(按内容匹配),留空则选中第一项
* - trigger_event: 选中变化时是否触发事件
*
* 输出:
* - id: 组件的唯一标识符
*
* 选中的值是一个字符串(候选项原文),可通过组件的 .value 访问。
* 候选项可通过 .options 访问。
*
* 使用场景:
* - 让用户在 Activity / 悬浮窗表单里从一组固定值中选一个
* - 与 "当组件被操作" 模块配合实现选中即触发的逻辑
*/
class UiListPickerModule : BaseUiComponentModule() {
override val id = "vflow.ui.component.listpicker"
override val metadata = ActionMetadata(
name = "列表选择", // Fallback
nameStringRes = R.string.module_vflow_ui_component_listpicker_name,
description = "让用户从一组选项中挑选一个值。", // Fallback
descriptionStringRes = R.string.module_vflow_ui_component_listpicker_desc,
iconRes = R.drawable.rounded_arrow_drop_down_24,
category = "UI 组件",
categoryId = "ui"
)

override fun getInputs() = listOf(
InputDefinition(
"key", "ID (变量名)",
ParameterType.STRING,
"picker1",
acceptsMagicVariable = false,
nameStringRes = R.string.param_vflow_ui_component_listpicker_key_name
),
InputDefinition(
"label", "标签",
ParameterType.STRING,
"请选择",
acceptsMagicVariable = true,
nameStringRes = R.string.param_vflow_ui_component_listpicker_label_name
),
InputDefinition(
"options", "选项 (每行一项)",
ParameterType.STRING,
"选项 1\n选项 2\n选项 3",
acceptsMagicVariable = true,
nameStringRes = R.string.param_vflow_ui_component_listpicker_options_name
),
InputDefinition(
"default_value", "默认选中",
ParameterType.STRING,
"",
acceptsMagicVariable = true,
nameStringRes = R.string.param_vflow_ui_component_listpicker_default_value_name
),
InputDefinition(
"trigger_event", "选中时触发事件",
ParameterType.BOOLEAN,
true,
nameStringRes = R.string.param_vflow_ui_component_listpicker_trigger_event_name
)
)

override fun getSummary(context: Context, step: ActionStep): CharSequence =
PillUtil.buildSpannable(
context,
context.getString(R.string.summary_prefix_listpicker),
PillUtil.createPillFromParam(step.parameters["key"], getInputs()[0])
)

override fun createUiElement(context: ExecutionContext, step: ActionStep): UiElement {
val label = VariableResolver.resolve(step.parameters["label"]?.toString() ?: "", context)
val optionsRaw = VariableResolver.resolve(step.parameters["options"]?.toString() ?: "", context)
val defaultRaw = VariableResolver.resolve(step.parameters["default_value"]?.toString() ?: "", context)
val key = step.parameters["key"]?.toString()?.takeIf { it.isNotEmpty() }
?: "picker_${System.currentTimeMillis()}"
val trigger = step.parameters["trigger_event"] as? Boolean ?: true

val options = parseOptions(optionsRaw)
// 默认值:匹配用户显式给定的内容,否则取第一项;都没有则空字符串
val resolvedDefault = when {
defaultRaw.isNotEmpty() && options.contains(defaultRaw) -> defaultRaw
options.isNotEmpty() -> options.first()
else -> ""
}

return UiElement(
id = key,
type = UiElementType.LIST_PICKER,
label = label,
defaultValue = resolvedDefault,
placeholder = "",
isRequired = false,
triggerEvent = trigger,
options = options
)
}

companion object {
/**
* 把多行文本解析成选项列表。
* - 同时支持 "\n" 和 "\\n" 两种换行形式,兼容手输和序列化。
* - 自动忽略空行和纯空白行。
* - 去除每项首尾空白。
*/
fun parseOptions(raw: String): List<String> {
if (raw.isBlank()) return emptyList()
return raw.split('\n')
.map { it.trim().replace("\\n", "\n") }
.filter { it.isNotEmpty() }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,28 @@ import kotlinx.parcelize.Parcelize
* UI 元素的类型枚举
*/
enum class UiElementType {
TEXT, // 纯文本展示
INPUT, // 输入框
BUTTON, // 按钮
SWITCH, // 开关
// 后续可扩展 LIST, CHECKBOX 等
TEXT, // 纯文本展示
INPUT, // 输入框
BUTTON, // 按钮
SWITCH, // 开关
LIST_PICKER // 下拉列表选择(单选)
}

/**
* UI 元素的数据定义,用于在 Intent 中传递
*
* 新增的 [options] 字段用于承载 LIST_PICKER 等组件的额外配置。
* 字段位于构造器末尾并带有默认值,向后兼容旧 Parcel 序列化数据时,
* 如果 parcel 中缺少该字段,会被解析为 null 并落回 emptyList()。
*/
@Parcelize
data class UiElement(
val id: String, // 对应的变量名 (key)
val type: UiElementType, // 组件类型
val label: String, // 显示的标签或标题
val defaultValue: String,// 默认值
val defaultValue: String,// 默认值(LIST_PICKER 下为默认选中项;其他类型保持原语义)
val placeholder: String, // 提示词
val isRequired: Boolean, // 是否必填
val triggerEvent: Boolean = true
) : Parcelable
val triggerEvent: Boolean = true,
val options: List<String> = emptyList() // LIST_PICKER 的候选项;其他类型忽略
) : Parcelable
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.chaomixian.vflow.core.workflow.module.ui.model.UiElement
import com.chaomixian.vflow.core.workflow.module.ui.model.UiElementType
import com.google.android.material.button.MaterialButton
import com.google.android.material.materialswitch.MaterialSwitch
import com.google.android.material.textfield.MaterialAutoCompleteTextView
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import kotlinx.coroutines.CoroutineScope
Expand Down Expand Up @@ -57,6 +58,7 @@ object DynamicUiRenderer {
viewsMap.forEach { (id, view) ->
when (view) {
is TextInputEditText -> result[id] = view.text.toString()
is MaterialAutoCompleteTextView -> result[id] = view.text.toString()
is MaterialSwitch -> result[id] = view.isChecked
}
}
Expand Down Expand Up @@ -157,6 +159,47 @@ object DynamicUiRenderer {
viewToRegister = switch
switch
}
UiElementType.LIST_PICKER -> {
val layout = TextInputLayout(context).apply {
hint = element.label
boxBackgroundMode = TextInputLayout.BOX_BACKGROUND_OUTLINE
endIconMode = TextInputLayout.END_ICON_DROPDOWN_MENU
}
val picker = MaterialAutoCompleteTextView(context).apply {
// 关闭输入校验,避免用户输入的内容与下拉项不匹配时报错
threshold = 1
val items = element.options
val adapter = android.widget.ArrayAdapter(
context,
android.R.layout.simple_list_item_1,
items
)
setAdapter(adapter)

// 设置默认值
val defaultIdx = items.indexOf(element.defaultValue)
if (defaultIdx >= 0) {
setText(element.defaultValue, false)
} else if (items.isNotEmpty()) {
setText(items.first(), false)
}

// 选中变化时同步 currentValue 到事件总线
if (element.triggerEvent && lifecycleScope != null && sessionId != null) {
setOnItemClickListener { _, _, position, _ ->
val picked = items.getOrNull(position) ?: ""
setText(picked, false)
val allValues = collectAllValues()
lifecycleScope.launch {
UiSessionBus.emitEvent(UiEvent(sessionId, element.id, "change", picked, allValues))
}
}
}
}
layout.addView(picker)
viewToRegister = picker
layout
}
UiElementType.BUTTON -> {
val button = MaterialButton(context).apply {
text = element.label
Expand Down Expand Up @@ -197,6 +240,7 @@ object DynamicUiRenderer {
viewsMap.forEach { (id, view) ->
when (view) {
is TextInputEditText -> result[id] = view.text.toString()
is MaterialAutoCompleteTextView -> result[id] = view.text.toString()
is MaterialSwitch -> result[id] = view.isChecked
}
}
Expand Down
17 changes: 15 additions & 2 deletions app/src/main/res/values-en/strings_module.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2293,8 +2293,19 @@
<string name="module_vflow_ui_component_text_name">Text Display</string>
<string name="param_vflow_ui_component_text_content_name">Content</string>
<string name="param_vflow_ui_component_text_key_name">ID (Optional)</string>
<string name="output_vflow_ui_component_text_component_name">VUiComponent object (contains all component information)</string>
<string name="output_vflow_ui_component_text_id_name">Component ID (for backward compatibility)</string>
<string name="output_vflow_ui_component_text_component_name">VUiComponent object (contains all component info)</string>
<string name="output_vflow_ui_component_text_id_name">Component ID (backward compatibility)</string>

<!-- vflow.ui.component_listpicker -->
<string name="module_vflow_ui_component_listpicker_desc">Let the user pick one value from a list of options.</string>
<string name="module_vflow_ui_component_listpicker_name">List Picker</string>
<string name="param_vflow_ui_component_listpicker_key_name">ID (Variable Name)</string>
<string name="param_vflow_ui_component_listpicker_label_name">Label</string>
<string name="param_vflow_ui_component_listpicker_options_name">Options (one per line)</string>
<string name="param_vflow_ui_component_listpicker_default_value_name">Default Selection</string>
<string name="param_vflow_ui_component_listpicker_trigger_event_name">Trigger Event on Selection</string>
<string name="output_vflow_ui_component_listpicker_component_name">VUiComponent object (contains all component info)</string>
<string name="output_vflow_ui_component_listpicker_id_name">Unique identifier of the component</string>

<!-- vflow.ui.float_end -->
<string name="module_vflow_ui_float_end_desc">Float window lifecycle end.</string>
Expand Down Expand Up @@ -3054,6 +3065,8 @@
<string name="summary_prefix_input_suffix">Input</string>
<string name="summary_prefix_switch">Switch: </string>
<string name="summary_prefix_switch_suffix">Switch</string>
<string name="summary_prefix_listpicker">List Picker: </string>
<string name="summary_prefix_listpicker_suffix">List Picker</string>
<string name="summary_prefix_display_text">Display text</string>
<string name="button_select">Select</string>
<string name="text_yes">Yes</string>
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values-en/vtype_properties.xml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@
<string name="vtype_uicomponent_isbutton">Is button</string>
<string name="vtype_uicomponent_isinput">Is input</string>
<string name="vtype_uicomponent_isswitch">Is switch</string>
<string name="vtype_uicomponent_ispicker">Is list picker</string>
<string name="vtype_uicomponent_options">Options</string>

<!-- Coordinate Properties -->
<string name="vtype_coordinate_x">X coordinate</string>
Expand Down
11 changes: 11 additions & 0 deletions app/src/main/res/values-ja/strings_module.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2041,6 +2041,15 @@
<string name="param_vflow_ui_component_text_key_name">ID (任意)</string>
<string name="output_vflow_ui_component_text_component_name">VUiComponent オブジェクト(すべてのコンポーネント情報を含む)</string>
<string name="output_vflow_ui_component_text_id_name">コンポーネント ID(後方互換)</string>
<string name="module_vflow_ui_component_listpicker_desc">ユーザーにオプションのリストから1つを選ばせます。</string>
<string name="module_vflow_ui_component_listpicker_name">リスト選択</string>
<string name="param_vflow_ui_component_listpicker_key_name">ID (変数名)</string>
<string name="param_vflow_ui_component_listpicker_label_name">ラベル</string>
<string name="param_vflow_ui_component_listpicker_options_name">オプション(1 行に 1 つ)</string>
<string name="param_vflow_ui_component_listpicker_default_value_name">デフォルト選択</string>
<string name="param_vflow_ui_component_listpicker_trigger_event_name">選択時にイベントを発火</string>
<string name="output_vflow_ui_component_listpicker_component_name">VUiComponent オブジェクト(すべてのコンポーネント情報を含む)</string>
<string name="output_vflow_ui_component_listpicker_id_name">コンポーネントの一意識別子</string>
<string name="module_vflow_ui_float_end_desc">フローティングウィンドウのライフサイクルが終了します。</string>
<string name="module_vflow_ui_float_end_name">フローティング終了</string>
<string name="summary_vflow_ui_float_end">フローティング終了</string>
Expand Down Expand Up @@ -2702,6 +2711,8 @@
<string name="summary_prefix_input_suffix">入力欄</string>
<string name="summary_prefix_switch">スイッチ: </string>
<string name="summary_prefix_switch_suffix">スイッチ</string>
<string name="summary_prefix_listpicker">リスト選択: </string>
<string name="summary_prefix_listpicker_suffix">リスト選択</string>
<string name="summary_prefix_display_text">表示テキスト</string>
<string name="button_select">選択</string>
<string name="text_yes">はい</string>
Expand Down
Loading