@@ -2,18 +2,20 @@ package org.appdevforall.codeonthego.computervision.domain.xml
22
33import org.appdevforall.codeonthego.computervision.domain.model.LayoutItem
44import org.appdevforall.codeonthego.computervision.domain.model.ScaledBox
5+ import org.appdevforall.codeonthego.computervision.domain.model.DetectionLabels
56import org.appdevforall.codeonthego.computervision.domain.parser.AttributeKey
67import org.appdevforall.codeonthego.computervision.domain.parser.FuzzyAttributeParser
8+ import org.appdevforall.codeonthego.computervision.domain.parser.patterns.IdPatterns
9+ import org.appdevforall.codeonthego.computervision.domain.parser.patterns.ResourceNamePatterns
710
811class WidgetFactory (
912 private val context : XmlContext ,
1013 private val annotations : Map <ScaledBox , String >,
1114 private val selectedImageOverrides : Map <ScaledBox , String > = emptyMap()
1215) {
13- private val checkboxGroupIdPattern = Regex (" ^cb_group_\\ d+$" )
1416 private val radioChildGroupIdPatterns = listOf (
15- Regex ( " ^rb_group(?:_ \\ d+)?(?:_|$).* " ) ,
16- Regex ( " ^radio_group(?:_ \\ d+)?(?:_|$).* " )
17+ IdPatterns . RADIO_BUTTON_GROUP_CHILD_ID ,
18+ IdPatterns . RADIO_GROUP_CHILD_ID
1719 )
1820
1921 fun createWidgets (item : LayoutItem ): List <AndroidWidget > = when (item) {
@@ -40,11 +42,11 @@ class WidgetFactory(
4042 val widgets = mutableListOf<AndroidWidget >()
4143
4244 val dropdownTitle = box.text
43- .takeIf { box.label == " dropdown " }
45+ .takeIf { box.label == DetectionLabels . DROPDOWN }
4446 ?.let (::extractDropdownTitle)
4547
4648 if (dropdownTitle != null ) {
47- val titleBox = box.copy(label = " text " , text = dropdownTitle)
49+ val titleBox = box.copy(label = DetectionLabels . TEXT , text = dropdownTitle)
4850
4951 val titleAttrs = mapOf (
5052 AttributeKey .WIDTH .xmlName to AndroidConstants .WRAP_CONTENT ,
@@ -71,11 +73,16 @@ class WidgetFactory(
7173
7274 private fun createRadioGroup (item : LayoutItem .RadioGroup ): List <AndroidWidget > {
7375 val groupAnnotation = item.boxes.firstNotNullOfOrNull { annotations[it] }
74- val fullGroupAttrs = FuzzyAttributeParser .parse(groupAnnotation, " RadioGroup " )
76+ val fullGroupAttrs = FuzzyAttributeParser .parse(groupAnnotation, AndroidWidgetTags . RADIO_GROUP )
7577
76- val groupId = resolveRadioGroupId(fullGroupAttrs[" android:id " ]?.substringAfterLast(' /' ))
78+ val groupId = resolveRadioGroupId(fullGroupAttrs[AttributeKey . ID .xmlName ]?.substringAfterLast(' /' ))
7779
78- val groupStructuralAttrs = setOf (" android:id" , " android:layout_width" , " android:layout_height" , " android:orientation" )
80+ val groupStructuralAttrs = setOf (
81+ AttributeKey .ID .xmlName,
82+ AttributeKey .WIDTH .xmlName,
83+ AttributeKey .HEIGHT .xmlName,
84+ AttributeKey .ORIENTATION .xmlName
85+ )
7986 // Group metadata can contain style attributes; visible option labels must stay with each RadioButton's OCR text.
8087 val sharedAttrs = fullGroupAttrs.filterKeys {
8188 it !in groupStructuralAttrs && it != AttributeKey .TEXT .xmlName
@@ -84,63 +91,68 @@ class WidgetFactory(
8491 var checkedId: String? = null
8592
8693 val children = item.boxes.mapIndexed { index, box ->
87- val parsedAttrs = (sharedAttrs + FuzzyAttributeParser .parse(annotations[box], " RadioButton" )).toMutableMap()
94+ val childAnnotation = annotations[box]
95+ val childAttrs = FuzzyAttributeParser .parse(childAnnotation, AndroidWidgetTags .RADIO_BUTTON )
96+ .filterKeys { key -> childAnnotation != groupAnnotation || key !in groupStructuralAttrs }
97+ val parsedAttrs = (sharedAttrs + childAttrs).toMutableMap()
8898 if (box.text.isNotBlank()) {
8999 parsedAttrs.remove(AttributeKey .TEXT .xmlName)
90100 }
91101
92- if (parsedAttrs[" android:id " ] == fullGroupAttrs[" android:id " ]) {
93- parsedAttrs.remove(" android:id " )
102+ if (parsedAttrs[AttributeKey . ID .xmlName ] == fullGroupAttrs[AttributeKey . ID .xmlName ]) {
103+ parsedAttrs.remove(AttributeKey . ID .xmlName )
94104 }
95105
96- val requestedId = parsedAttrs[" android:id " ]?.substringAfterLast(' /' )
106+ val requestedId = parsedAttrs[AttributeKey . ID .xmlName ]?.substringAfterLast(' /' )
97107 val childId = if (requestedId != null && radioChildGroupIdPatterns.any { it.matches(requestedId) }) {
98- context.nextId(" radio_button " )
108+ context.nextId(AndroidIdLabels . RADIO_BUTTON )
99109 } else {
100- context.resolveId(requestedId, " radio_button " )
110+ context.resolveId(requestedId, AndroidIdLabels . RADIO_BUTTON )
101111 }
102112
103- val isChecked = box.label == " radio_button_checked" || parsedAttrs[" android:checked" ]?.equals(" true" , ignoreCase = true ) == true
113+ val isChecked = box.label == DetectionLabels .RADIO_BUTTON_CHECKED ||
114+ parsedAttrs[AttributeKey .CHECKED .xmlName]?.equals(AndroidConstants .TRUE , ignoreCase = true ) == true
104115 if (isChecked) {
105116 checkedId = childId
106- parsedAttrs[" android:checked " ] = " true "
117+ parsedAttrs[AttributeKey . CHECKED .xmlName ] = AndroidConstants . TRUE
107118 } else {
108- parsedAttrs[" android:checked " ] = " false "
119+ parsedAttrs[AttributeKey . CHECKED .xmlName ] = AndroidConstants . FALSE
109120 }
110121
111- val extraAttrs = if (item.orientation == " horizontal " ) {
122+ val extraAttrs = if (item.orientation == AndroidConstants . ORIENTATION_HORIZONTAL ) {
112123 getMarginEndForHorizontalGap(item.boxes, index)
113124 } else emptyMap()
114125
115126 createSimpleWidget(box, parsedAttrsOverride = parsedAttrs, idOverride = childId, extraAttrs = extraAttrs)
116127 }
117128
118- val textStyleAttrs = setOf (" android:textColor" , " android:textSize" , " android:textStyle" , " android:fontFamily" )
129+ val textStyleAttrs = setOf (
130+ AttributeKey .TEXT_COLOR .xmlName,
131+ AttributeKey .TEXT_SIZE .xmlName,
132+ AttributeKey .TEXT_STYLE .xmlName,
133+ AttributeKey .FONT_FAMILY .xmlName
134+ )
119135 val groupFinalAttrs = fullGroupAttrs.filterKeys { it !in textStyleAttrs }.toMutableMap()
120- groupFinalAttrs[" android:id " ] = groupId
136+ groupFinalAttrs[AttributeKey . ID .xmlName ] = groupId
121137
122138 return listOf (RadioGroupWidget (groupFinalAttrs, children, item.orientation, checkedId))
123139 }
124140
125141 private fun createCheckboxGroup (item : LayoutItem .CheckboxGroup ): List <AndroidWidget > {
126142 val groupAnnotation = item.boxes.firstNotNullOfOrNull { annotations[it] }
127- val parsedAttrs = FuzzyAttributeParser .parse(groupAnnotation, " CheckBox " )
143+ val parsedAttrs = FuzzyAttributeParser .parse(groupAnnotation, AndroidWidgetTags . CHECK_BOX )
128144
129- val requestedId = parsedAttrs[" android:id" ]?.substringAfterLast(' /' )
130- val baseId = if (requestedId != null && checkboxGroupIdPattern.matches(requestedId)) {
131- context.resolveId(requestedId, " cb_group" )
132- } else {
133- context.nextId(" cb_group" , initialIndex = 1 )
134- }
145+ val requestedId = parsedAttrs[AttributeKey .ID .xmlName]?.substringAfterLast(' /' )
146+ val baseId = resolveCheckboxGroupId(requestedId)
135147
136148 return item.boxes.mapIndexed { index, box ->
137149 val suffix = (' a' + index).toString()
138150 val childId = " ${baseId} _$suffix "
139151
140152 val safeAttrs = parsedAttrs.toMutableMap()
141- safeAttrs.remove(" android:id " )
153+ safeAttrs.remove(AttributeKey . ID .xmlName )
142154
143- val extraAttrs = if (item.orientation == " horizontal " ) {
155+ val extraAttrs = if (item.orientation == AndroidConstants . ORIENTATION_HORIZONTAL ) {
144156 getMarginEndForHorizontalGap(item.boxes, index)
145157 } else emptyMap()
146158
@@ -160,7 +172,7 @@ class WidgetFactory(
160172 ? : FuzzyAttributeParser .parse(rawAnnotation, tag).toMutableMap()
161173
162174 selectedImageOverrides[box]?.let { drawableReference ->
163- parsedAttrs[" android:src " ] = drawableReference
175+ parsedAttrs[AttributeKey . SRC .xmlName ] = drawableReference
164176 }
165177
166178 return AndroidWidget .create(box, parsedAttrs).apply {
@@ -175,7 +187,7 @@ class WidgetFactory(
175187 .replace(Regex (" ^[vV]\\ s+" ), " " )
176188 .trim()
177189
178- return cleaned.takeIf { it.isNotBlank() && ! it.equals(" dropdown " , ignoreCase = true ) }
190+ return cleaned.takeIf { it.isNotBlank() && ! it.equals(DetectionLabels . DROPDOWN , ignoreCase = true ) }
179191 }
180192
181193 /* * Calculates the non-negative horizontal gap to the next box as an end margin. */
@@ -184,18 +196,36 @@ class WidgetFactory(
184196 val currentBox = boxes[currentIndex]
185197 val nextBox = boxes[currentIndex + 1 ]
186198 val gap = maxOf(0 , nextBox.x - (currentBox.x + currentBox.w))
187- return mapOf (" android:layout_marginEnd " to " ${gap} dp" )
199+ return mapOf (AttributeKey . LAYOUT_MARGIN_END .xmlName to " ${gap} dp" )
188200 }
189201
190202 private fun resolveRadioGroupId (requestedId : String? ): String {
191203 var cleanId = requestedId
192204 if (requestedId != null ) {
193205 val normalizedId = requestedId.lowercase()
194206 when {
195- normalizedId.startsWith(" radio_grou" ) -> cleanId = " radio_group"
196- normalizedId.startsWith(" rb_grou" ) || normalizedId.startsWith(" rb_group" ) -> cleanId = " rb_group"
207+ normalizedId.startsWith(" radio_grou" ) -> cleanId = AndroidIdLabels .RADIO_GROUP
208+ normalizedId.startsWith(" rb_grou" ) || normalizedId.startsWith(AndroidIdLabels .RADIO_BUTTON_GROUP ) ->
209+ cleanId = AndroidIdLabels .RADIO_BUTTON_GROUP
197210 }
198211 }
199- return context.resolveId(cleanId, " radio_group" )
212+ return context.resolveId(cleanId, AndroidIdLabels .RADIO_GROUP )
213+ }
214+
215+ private fun resolveCheckboxGroupId (requestedId : String? ): String {
216+ if (requestedId == null ) return context.nextId(AndroidIdLabels .CHECKBOX_GROUP , initialIndex = 1 )
217+ if (IdPatterns .CHECKBOX_GROUP_ID .matches(requestedId)) {
218+ return context.resolveId(requestedId, AndroidIdLabels .CHECKBOX_GROUP )
219+ }
220+
221+ val compactId = requestedId.lowercase().replace(ResourceNamePatterns .NON_ALPHANUMERIC_LOWER , " " )
222+ val recoveredSuffix = IdPatterns .COMPACT_CHECKBOX_GROUP_ID .matchEntire(compactId)?.groupValues?.getOrNull(1 )
223+ val cleanId = recoveredSuffix?.let { " ${AndroidIdLabels .CHECKBOX_GROUP } _$it " }
224+
225+ return if (cleanId != null ) {
226+ context.resolveId(cleanId, AndroidIdLabels .CHECKBOX_GROUP )
227+ } else {
228+ context.nextId(AndroidIdLabels .CHECKBOX_GROUP , initialIndex = 1 )
229+ }
200230 }
201231}
0 commit comments