@@ -9,6 +9,7 @@ import androidx.fragment.app.FragmentActivity
99import com.omarea.common.model.SelectItem
1010import com.tool.tree.R
1111import com.omarea.krscript.model.ActionParamInfo
12+ import com.omarea.krscript.executor.ScriptEnvironmen
1213import androidx.core.graphics.toColorInt
1314
1415class ActionParamsLayoutRender (private var linearLayout : LinearLayout , activity : FragmentActivity ) {
@@ -176,17 +177,22 @@ class ActionParamsLayoutRender(private var linearLayout: LinearLayout, activity:
176177 for (info in currentParamInfos) {
177178 val name = info.name ? : continue
178179 val initialState = info.dependInitialState.trim().lowercase()
179-
180- val initialVisibility = when {
181- initialState == " auto" -> {
182- // Xác định tự động dựa trên dependDefault
180+
181+ val initialVisibility = when (initialState) {
182+ " hide" -> false
183+ " show" -> true
184+ else -> {
185+ // "auto": xác định tự động dựa trên dependDefault
183186 info.dependDefault.trim().lowercase() != " hide"
184187 }
185- initialState == " hide" -> false
186- else -> true // "show"
187188 }
188-
189- visibilityState[name] = initialVisibility
189+
190+ // CHỈ set View.visibility tức thời để tránh nhấp nháy lúc mở dialog.
191+ // KHÔNG ghi vào visibilityState ở đây: evaluateDependencies() được gọi ngay sau
192+ // initializeDependencyStates() sẽ tính trạng thái thật (có cascade) và mới là nơi
193+ // ghi visibilityState + quyết định gọi depend-onchange. Nếu ghi ở đây, lần đánh giá
194+ // đầu tiên sẽ hiểu nhầm đây là "thay đổi trạng thái" và gọi nhầm callback ngay khi
195+ // dialog vừa mở.
190196 rowViews[name]?.visibility = if (initialVisibility) View .VISIBLE else View .GONE
191197 }
192198 }
@@ -268,146 +274,203 @@ class ActionParamsLayoutRender(private var linearLayout: LinearLayout, activity:
268274 return identifiers
269275 }
270276
271- // ========== TÍNH NĂNG MỚI: ĐÁNH GIÁ PHỤ THUỘC NÂNG CẤP ==========
277+ // ========== TÍNH NĂNG MỚI: ĐÁNH GIÁ PHỤ THUỘC NÂNG CẤP (có depend-cascade) ==========
272278 private fun evaluateDependencies () {
273- for (info in currentParamInfos) {
274- val dependOnRaw = info.dependOn?.trim()
275- val name = info.name
276- if (name.isNullOrEmpty()) continue
277-
278- // Nếu không có dependOn -> hiển thị mặc định
279- if (dependOnRaw.isNullOrEmpty()) {
280- val shouldShow = info.dependDefault.trim().lowercase() != " hide"
281- updateVisibility(name, shouldShow)
282- continue
279+ // Trạng thái TRƯỚC lượt đánh giá này - dùng để phát hiện thay đổi thật sự và quyết
280+ // định có gọi depend-onchange hay không (so với trạng thái đã áp dụng lần trước, KHÔNG
281+ // so giữa các lượt lặp nội bộ bên dưới).
282+ val previousState = HashMap (visibilityState)
283+
284+ // Bộ nhớ TẠM cho lượt đánh giá này. Lặp nhiều lượt để "cha ẩn thì con ẩn theo"
285+ // (depend-cascade) lan truyền đúng qua các chuỗi phụ thuộc nhiều cấp, bất kể param cha
286+ // được khai báo trước hay sau param con trong file XML. Dừng sớm khi không còn gì
287+ // thay đổi giữa 2 lượt liên tiếp (hầu hết trường hợp chỉ cần 1-2 lượt).
288+ val working = HashMap <String , Boolean >()
289+ val maxPasses = currentParamInfos.size.coerceAtLeast(1 ).coerceAtMost(20 )
290+
291+ for (pass in 0 until maxPasses) {
292+ var changedThisPass = false
293+
294+ for (info in currentParamInfos) {
295+ val name = info.name ? : continue
296+ val shouldShow = computeShouldShow(info, working)
297+
298+ if (working[name] != shouldShow) {
299+ working[name] = shouldShow
300+ changedThisPass = true
301+ }
283302 }
284303
285- val dependOnList = dependOnRaw.split(" |" ).map { it.trim() }.filter { it.isNotEmpty() }
286- if (dependOnList.isEmpty()) {
287- updateVisibility(name, info.dependDefault.trim().lowercase() != " hide" )
288- continue
289- }
304+ if (! changedThisPass) break
305+ }
306+
307+ // Áp dụng kết quả cuối cùng lên UI, ghi lại visibilityState chính thức, và gọi
308+ // depend-onchange đúng 1 lần cho mỗi param có trạng thái thực sự thay đổi so với
309+ // trước khi vào hàm evaluateDependencies() này.
310+ for (info in currentParamInfos) {
311+ val name = info.name ? : continue
312+ val shouldShow = working[name] ? : continue
313+ applyVisibility(name, shouldShow, previousState[name])
314+ }
315+ }
290316
291- val dependValueList = (info.dependValue ? : " " ).split(" |" )
292- val dependModeList = info.dependMode.split(" |" )
317+ // Tính shouldShow cho 1 param dựa trên trạng thái ẩn/hiện của các param cha TRONG LƯỢT
318+ // ĐÁNH GIÁ HIỆN TẠI (map "working"), không phải trạng thái đã chốt từ lần render trước.
319+ private fun computeShouldShow (info : ActionParamInfo , working : HashMap <String , Boolean >): Boolean {
320+ val dependOnRaw = info.dependOn?.trim()
293321
294- fun evalCondition (i : Int ): Pair <Boolean , Boolean >? {
295- val parentName = dependOnList[i]
296- val controllerInfo = currentParamInfos.find { it.name == parentName }
297- val reader = valueReaders[parentName]
298- if (controllerInfo == null || reader == null ) return null
322+ if (dependOnRaw.isNullOrEmpty()) {
323+ return info.dependDefault.trim().lowercase() != " hide"
324+ }
299325
300- val currentValues = reader().split(controllerInfo.separator)
301- .map { it.trim() }.filter { it.isNotEmpty() }
302- val parentOptions = controllerInfo.optionsFromShell ? : controllerInfo.options
326+ val dependOnList = dependOnRaw.split(" |" ).map { it.trim() }.filter { it.isNotEmpty() }
327+ if (dependOnList.isEmpty()) {
328+ return info.dependDefault.trim().lowercase() != " hide"
329+ }
303330
304- val currentIdentifiers = HashSet <String >()
305- for (v in currentValues) {
306- currentIdentifiers.addAll(buildValueIdentifiers(v, parentOptions))
307- }
331+ // ========== TÍNH NĂNG MỚI: CHA ẨN THÌ CON ẨN THEO (depend-cascade) ==========
332+ // Nếu BẤT KỲ param cha nào trong depend-on đang bị ẩn (đã tính ở lượt trước/pass hiện
333+ // tại), param này ẩn theo luôn, không cần xét tiếp depend-value/depend-logic.
334+ if (info.dependCascade && dependOnList.any { working[it] == false }) {
335+ return false
336+ }
308337
309- val wantedRaw = dependValueList.getOrNull(i) ? : dependValueList.lastOrNull() ? : " "
310- val wanted = wantedRaw.split(" ," ).map { it.trim() }.filter { it.isNotEmpty() }
311- val matched = wanted.isEmpty() || wanted.any { currentIdentifiers.contains(it) }
338+ val dependValueList = (info.dependValue ? : " " ).split(" |" )
339+ val dependModeList = info.dependMode.split(" |" )
312340
313- val mode = (dependModeList.getOrNull(i) ? : dependModeList.lastOrNull() ? : " show" ).trim()
314- val wantShow = if (mode == " hide" ) ! matched else matched
315- return Pair (matched, wantShow)
341+ fun evalCondition (i : Int ): Pair <Boolean , Boolean >? {
342+ val parentName = dependOnList[i]
343+ val controllerInfo = currentParamInfos.find { it.name == parentName }
344+ val reader = valueReaders[parentName]
345+ if (controllerInfo == null || reader == null ) return null
346+
347+ val currentValues = reader().split(controllerInfo.separator)
348+ .map { it.trim() }.filter { it.isNotEmpty() }
349+ val parentOptions = controllerInfo.optionsFromShell ? : controllerInfo.options
350+
351+ val currentIdentifiers = HashSet <String >()
352+ for (v in currentValues) {
353+ currentIdentifiers.addAll(buildValueIdentifiers(v, parentOptions))
316354 }
317355
318- val logic = info.dependLogic.trim().lowercase()
319- val shouldShow: Boolean = when (logic) {
320- " priority" , " or" , " priority-ltr" , " or-ltr" -> {
321- // Ưu tiên trái -> phải
322- var result = info.dependDefault.trim().lowercase() != " hide"
323- for (i in dependOnList.indices) {
324- val (matched, wantShow) = evalCondition(i) ? : continue
325- if (matched) {
326- result = wantShow
327- break
328- }
356+ val wantedRaw = dependValueList.getOrNull(i) ? : dependValueList.lastOrNull() ? : " "
357+ val wanted = wantedRaw.split(" ," ).map { it.trim() }.filter { it.isNotEmpty() }
358+ val matched = wanted.isEmpty() || wanted.any { currentIdentifiers.contains(it) }
359+
360+ val mode = (dependModeList.getOrNull(i) ? : dependModeList.lastOrNull() ? : " show" ).trim()
361+ val wantShow = if (mode == " hide" ) ! matched else matched
362+ return Pair (matched, wantShow)
363+ }
364+
365+ val logic = info.dependLogic.trim().lowercase()
366+ return when (logic) {
367+ " priority" , " or" , " priority-ltr" , " or-ltr" -> {
368+ // Ưu tiên trái -> phải
369+ var result = info.dependDefault.trim().lowercase() != " hide"
370+ for (i in dependOnList.indices) {
371+ val (matched, wantShow) = evalCondition(i) ? : continue
372+ if (matched) {
373+ result = wantShow
374+ break
329375 }
330- result
331376 }
332- " priority-rtl" , " or-rtl" -> {
333- // Ưu tiên phải -> trái
334- var result = info.dependDefault.trim().lowercase() != " hide"
335- for (i in dependOnList.indices.reversed()) {
336- val (matched, wantShow) = evalCondition(i) ? : continue
337- if (matched) {
338- result = wantShow
339- break
340- }
377+ result
378+ }
379+ " priority-rtl" , " or-rtl" -> {
380+ // Ưu tiên phải -> trái
381+ var result = info.dependDefault.trim().lowercase() != " hide"
382+ for (i in dependOnList.indices.reversed()) {
383+ val (matched, wantShow) = evalCondition(i) ? : continue
384+ if (matched) {
385+ result = wantShow
386+ break
341387 }
342- result
343388 }
344- " xor " -> {
345- // Chỉ ĐÚNG MỘT điều kiện được thỏa
346- var matchCount = 0
347- for (i in dependOnList.indices) {
348- val (matched, _) = evalCondition(i) ? : continue
349- if (matched) matchCount ++
350- }
351- (matchCount == 1 ) != info.dependNegate
389+ result
390+ }
391+ " xor " -> {
392+ // Chỉ ĐÚNG MỘT điều kiện được thỏa
393+ var matchCount = 0
394+ for (i in dependOnList.indices) {
395+ val (matched, _) = evalCondition(i) ? : continue
396+ if (matched) matchCount ++
352397 }
353- " nand" -> {
354- // Phủ định của AND (KHÔNG phải tất cả điều kiện đều thỏa)
355- var result = true
356- for (i in dependOnList.indices) {
357- val (_, wantShow) = evalCondition(i) ? : continue
358- if (! wantShow) {
359- result = false
360- break
361- }
398+ (matchCount == 1 ) != info.dependNegate
399+ }
400+ " nand" -> {
401+ // Phủ định của AND (KHÔNG phải tất cả điều kiện đều thỏa)
402+ var result = true
403+ for (i in dependOnList.indices) {
404+ val (_, wantShow) = evalCondition(i) ? : continue
405+ if (! wantShow) {
406+ result = false
407+ break
362408 }
363- ! result != info.dependNegate
364409 }
365- else -> {
366- // "and" (mặc định)
367- var satisfiedCount = 0
368- var totalCount = 0
369-
370- for (i in dependOnList.indices) {
371- val (_, wantShow) = evalCondition(i) ? : continue
372- totalCount++
373- if (wantShow) satisfiedCount++
374- }
375-
376- val threshold = if (info.dependThreshold < 0 ) {
377- // Mặc định: 100% (tất cả phải thỏa)
378- totalCount
379- } else {
380- // Tính toán % ngưỡng
381- (totalCount * info.dependThreshold / 100 ).coerceAtLeast(1 )
382- }
410+ ! result != info.dependNegate
411+ }
412+ else -> {
413+ // "and" (mặc định)
414+ var satisfiedCount = 0
415+ var totalCount = 0
416+
417+ for (i in dependOnList.indices) {
418+ val (_, wantShow) = evalCondition(i) ? : continue
419+ totalCount++
420+ if (wantShow) satisfiedCount++
421+ }
383422
384- val result = satisfiedCount >= threshold
385- result != info.dependNegate
423+ val threshold = if (info.dependThreshold < 0 ) {
424+ // Mặc định: 100% (tất cả phải thỏa)
425+ totalCount
426+ } else {
427+ // Tính toán % ngưỡng
428+ (totalCount * info.dependThreshold / 100 ).coerceAtLeast(1 )
386429 }
387- }
388430
389- updateVisibility(name, shouldShow)
431+ val result = satisfiedCount >= threshold
432+ result != info.dependNegate
433+ }
390434 }
391435 }
392436
393- // ========== TÍNH NĂNG MỚI: CẬP NHẬT TRẠNG THÁI VÀ GỌI CALLBACK ==========
394- private fun updateVisibility (name : String , shouldShow : Boolean ) {
395- val oldState = visibilityState[name]
437+ // ========== TÍNH NĂNG MỚI: ÁP DỤNG TRẠNG THÁI LÊN UI + GỌI CALLBACK ==========
438+ private fun applyVisibility (name : String , shouldShow : Boolean , oldState : Boolean? ) {
396439 visibilityState[name] = shouldShow
397-
440+
398441 val view = rowViews[name]
399442 view?.visibility = if (shouldShow) View .VISIBLE else View .GONE
400-
401- // Gọi callback nếu trạng thái thay đổi
443+
444+ // Chỉ gọi callback khi ĐÃ TỪNG có trạng thái trước đó (oldState != null) VÀ trạng thái
445+ // thực sự đổi - tránh gọi callback ngay khi dialog vừa mở (lần đầu tiên oldState luôn
446+ // null vì initializeDependencyStates() không ghi vào visibilityState).
402447 if (oldState != null && oldState != shouldShow) {
403448 val info = currentParamInfos.find { it.name == name }
404- if (info != null && ! info.dependOnChangeCallback.isNullOrEmpty()) {
405- // TODO: Implement callback execution (shell script or custom logic)
406- // Example: executeCallback(info.dependOnChangeCallback, name, shouldShow)
449+ val script = info? .dependOnChangeCallback
450+ if ( ! script.isNullOrEmpty()) {
451+ executeDependOnChangeCallback( name, shouldShow, script )
407452 }
408453 }
409454 }
410455
456+ // ========== TÍNH NĂNG MỚI: THỰC THI depend-onchange ==========
457+ // Chạy script/callback trên luồng nền (KHÔNG được chạy trực tiếp trên UI thread vì lệnh
458+ // shell root có thể mất nhiều thời gian và sẽ làm treo giao diện). Truyền tên param vừa
459+ // đổi trạng thái + trạng thái mới (1 = đang hiện, 0 = đang ẩn) làm biến môi trường để
460+ // script có thể tự xử lý theo ngữ cảnh.
461+ private fun executeDependOnChangeCallback (paramName : String , visible : Boolean , script : String ) {
462+ Thread {
463+ try {
464+ val extraParams = HashMap <String , String >()
465+ extraParams[" PARAM_NAME" ] = paramName
466+ extraParams[" PARAM_VISIBLE" ] = if (visible) " 1" else " 0"
467+ ScriptEnvironmen .executeResultRoot(context, script, null , extraParams)
468+ } catch (ex: Exception ) {
469+ // Lỗi khi chạy callback không được làm crash hay ảnh hưởng tới UI chính
470+ }
471+ }.start()
472+ }
473+
411474 private val hideLabelTypes = arrayOf(" bool" , " checkbox" , " switch" )
412475 private fun addToLayout (inputView : View , actionParamInfo : ActionParamInfo ) {
413476 val layout = LayoutInflater .from(context).inflate(R .layout.kr_param_row, null )
0 commit comments