From 23bd1076fe31fec61bc435d33f2873e81ef8ac69 Mon Sep 17 00:00:00 2001 From: mueller-ma Date: Thu, 21 Dec 2023 15:50:55 +0100 Subject: [PATCH 1/7] Implement buttongrid Closes #3494 Signed-off-by: mueller-ma --- .idea/dictionaries/openhab.xml | 1 + .../openhab/habdroid/model/LabeledValue.kt | 10 +++- .../java/org/openhab/habdroid/model/Widget.kt | 3 +- .../org/openhab/habdroid/ui/ViewExtensions.kt | 1 + .../org/openhab/habdroid/ui/WidgetAdapter.kt | 47 ++++++++++++++++++- .../res/layout/widgetlist_buttongriditem.xml | 18 +++++++ .../org/openhab/habdroid/model/ItemTest.kt | 8 ++-- 7 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 mobile/src/main/res/layout/widgetlist_buttongriditem.xml diff --git a/.idea/dictionaries/openhab.xml b/.idea/dictionaries/openhab.xml index 0dfc1db2e2..be0bf34f64 100644 --- a/.idea/dictionaries/openhab.xml +++ b/.idea/dictionaries/openhab.xml @@ -5,6 +5,7 @@ addr basicui basicuidark + buttongrid colorpicker datasource demomode diff --git a/mobile/src/main/java/org/openhab/habdroid/model/LabeledValue.kt b/mobile/src/main/java/org/openhab/habdroid/model/LabeledValue.kt index b648841ddd..1ae91c9379 100644 --- a/mobile/src/main/java/org/openhab/habdroid/model/LabeledValue.kt +++ b/mobile/src/main/java/org/openhab/habdroid/model/LabeledValue.kt @@ -21,12 +21,18 @@ import org.json.JSONObject import org.openhab.habdroid.util.optStringOrNull @Parcelize -data class LabeledValue internal constructor(val value: String, val label: String, val icon: IconResource?) : Parcelable +data class LabeledValue internal constructor( + val value: String, + val label: String, + val icon: IconResource?, + val row: Int, + val column: Int +) : Parcelable @Throws(JSONException::class) fun JSONObject.toLabeledValue(valueKey: String, labelKey: String): LabeledValue { val value = getString(valueKey) val label = optString(labelKey, value) val icon = optStringOrNull("icon")?.toOH2IconResource() - return LabeledValue(value, label, icon) + return LabeledValue(value, label, icon, optInt("row"), optInt("column")) } diff --git a/mobile/src/main/java/org/openhab/habdroid/model/Widget.kt b/mobile/src/main/java/org/openhab/habdroid/model/Widget.kt index 845ddaca3e..6f524a1dfd 100644 --- a/mobile/src/main/java/org/openhab/habdroid/model/Widget.kt +++ b/mobile/src/main/java/org/openhab/habdroid/model/Widget.kt @@ -116,6 +116,7 @@ data class Widget( Video, Webview, Input, + Buttongrid, Unknown } @@ -303,7 +304,7 @@ fun Node.collectWidgets(parent: Widget?): List { "label" -> mappingLabel = childNode.textContent } } - mappings.add(LabeledValue(mappingCommand, mappingLabel, null)) + mappings.add(LabeledValue(mappingCommand, mappingLabel, null, 0, 0)) } else -> {} } diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/ViewExtensions.kt b/mobile/src/main/java/org/openhab/habdroid/ui/ViewExtensions.kt index 04eb5e276b..fd09116e10 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/ViewExtensions.kt +++ b/mobile/src/main/java/org/openhab/habdroid/ui/ViewExtensions.kt @@ -124,6 +124,7 @@ fun RemoteViews.duplicate(): RemoteViews { } fun MaterialButton.setTextAndIcon(connection: Connection, mapping: LabeledValue) { + contentDescription = mapping.label val iconUrl = mapping.icon?.toUrl(context, true) if (iconUrl == null) { icon = null diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt b/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt index 1ece3b4ca8..3639598ca8 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt +++ b/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt @@ -32,6 +32,7 @@ import android.view.inputmethod.EditorInfo import android.webkit.WebChromeClient import android.webkit.WebView import android.widget.Button +import android.widget.GridLayout import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView @@ -40,9 +41,11 @@ import androidx.annotation.LayoutRes import androidx.annotation.StringRes import androidx.annotation.VisibleForTesting import androidx.core.content.ContextCompat +import androidx.core.view.allViews import androidx.core.view.children import androidx.core.view.get import androidx.core.view.isGone +import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.core.widget.ContentLoadingProgressBar import androidx.core.widget.doAfterTextChanged @@ -221,6 +224,7 @@ class WidgetAdapter( TYPE_LOCATION -> MapViewHelper.createViewHolder(initData) TYPE_INPUT -> InputViewHolder(initData) TYPE_DATETIMEINPUT -> DateTimeInputViewHolder(initData) + TYPE_BUTTONGRID -> ButtongridViewHolder(initData) TYPE_INVISIBLE -> InvisibleWidgetViewHolder(initData) else -> throw IllegalArgumentException("View type $viewType is not known") } @@ -345,6 +349,7 @@ class WidgetAdapter( Widget.Type.Colorpicker -> TYPE_COLOR Widget.Type.Mapview -> TYPE_LOCATION Widget.Type.Input -> if (widget.shouldUseDateTimePickerForInput()) TYPE_DATETIMEINPUT else TYPE_INPUT + Widget.Type.Buttongrid -> TYPE_BUTTONGRID else -> TYPE_GENERICITEM } return toInternalViewType(actualViewType, compactMode) @@ -780,6 +785,45 @@ class WidgetAdapter( } } + class ButtongridViewHolder internal constructor(private val initData: ViewHolderInitData) : + LabeledItemBaseViewHolder(initData, R.layout.widgetlist_buttongriditem), View.OnClickListener { + private val table: GridLayout = itemView.findViewById(R.id.widget_content) + + override fun bind(widget: Widget) { + super.bind(widget) + val mappings = widget.mappings.filter { it.column != 0 && it.row != 0 } + table.rowCount = mappings.maxOfOrNull { it.row } ?: 0 + table.columnCount = mappings.maxOfOrNull { it.column } ?: 0 + (0.. + (0.. + val buttonView = initData.inflater.inflate(R.layout.widgetlist_sectionswitchitem_button, null) as MaterialButton + // Rows and columns start with 1 in Sitemap definition, thus decrement them here + val mapping = mappings.firstOrNull { it.row - 1 == row && it.column - 1 == column } + if (mapping == null) { + buttonView.visibility = View.INVISIBLE + } else { + buttonView.setOnClickListener(this) + buttonView.setTextAndIcon(connection, mapping) + buttonView.tag = mapping.value + buttonView.visibility = View.VISIBLE + } + + table.addView( + buttonView, + GridLayout.LayoutParams( + GridLayout.spec(row, GridLayout.FILL, 1f), + GridLayout.spec(column, GridLayout.FILL, 1f) + ) + ) + } + } + } + + override fun onClick(view: View) { + connection.httpClient.sendItemCommand(boundWidget?.item, view.tag as String) + } + } + class SliderViewHolder internal constructor(initData: ViewHolderInitData) : LabeledItemBaseViewHolder(initData, R.layout.widgetlist_slideritem, R.layout.widgetlist_slideritem_compact), WidgetSlider.UpdateListener { @@ -1555,7 +1599,8 @@ class WidgetAdapter( private const val TYPE_LOCATION = 18 private const val TYPE_INPUT = 19 private const val TYPE_DATETIMEINPUT = 20 - private const val TYPE_INVISIBLE = 21 + private const val TYPE_BUTTONGRID = 21 + private const val TYPE_INVISIBLE = 22 private fun toInternalViewType(viewType: Int, compactMode: Boolean): Int { return viewType or (if (compactMode) 0x100 else 0) diff --git a/mobile/src/main/res/layout/widgetlist_buttongriditem.xml b/mobile/src/main/res/layout/widgetlist_buttongriditem.xml new file mode 100644 index 0000000000..cab703e2a0 --- /dev/null +++ b/mobile/src/main/res/layout/widgetlist_buttongriditem.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/mobile/src/test/java/org/openhab/habdroid/model/ItemTest.kt b/mobile/src/test/java/org/openhab/habdroid/model/ItemTest.kt index 01adf9c0e0..de1b8904f4 100644 --- a/mobile/src/test/java/org/openhab/habdroid/model/ItemTest.kt +++ b/mobile/src/test/java/org/openhab/habdroid/model/ItemTest.kt @@ -107,8 +107,8 @@ class ItemTest { @Test fun getCommandOptions() { val sut = itemWithCommandOptions.toItem() - assertEquals(LabeledValue("1", "One", "switch".toOH2IconResource()), sut.options!!.component1()) - assertEquals(LabeledValue("2", "Two", null), sut.options!!.component2()) + assertEquals(LabeledValue("1", "One", "switch".toOH2IconResource(), 1, 2), sut.options!!.component1()) + assertEquals(LabeledValue("2", "Two", null, 0, 0), sut.options!!.component2()) } @Test @@ -209,7 +209,9 @@ class ItemTest { { 'command': '1', 'label': 'One', - 'icon': 'switch' + 'icon': 'switch', + 'row': 1, + 'column': 2 }, { 'command': '2', From 739b02e82b46ce538659a36a6f197232fc37a236 Mon Sep 17 00:00:00 2001 From: mueller-ma Date: Tue, 26 Dec 2023 19:44:44 +0100 Subject: [PATCH 2/7] Use 'until' Signed-off-by: mueller-ma --- .../src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt b/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt index 3639598ca8..6b0910f738 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt +++ b/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt @@ -794,12 +794,13 @@ class WidgetAdapter( val mappings = widget.mappings.filter { it.column != 0 && it.row != 0 } table.rowCount = mappings.maxOfOrNull { it.row } ?: 0 table.columnCount = mappings.maxOfOrNull { it.column } ?: 0 - (0.. - (0.. + (0 until table.rowCount).forEach { row -> + (0 until table.columnCount).forEach { column -> val buttonView = initData.inflater.inflate(R.layout.widgetlist_sectionswitchitem_button, null) as MaterialButton // Rows and columns start with 1 in Sitemap definition, thus decrement them here val mapping = mappings.firstOrNull { it.row - 1 == row && it.column - 1 == column } if (mapping == null) { + // Create invisible buttons so each cell has an equal size buttonView.visibility = View.INVISIBLE } else { buttonView.setOnClickListener(this) From 385fa2e9585ff8d8fb4f65e5d42223e44af8ef3b Mon Sep 17 00:00:00 2001 From: mueller-ma Date: Tue, 26 Dec 2023 19:58:55 +0100 Subject: [PATCH 3/7] Show diffs for .idea files by default Signed-off-by: mueller-ma --- .gitattributes | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..24683a7b1c --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +.idea/ linguist-generated=false From 9c8cae438003c27927947f51a5965252591d6bc5 Mon Sep 17 00:00:00 2001 From: mueller-ma Date: Wed, 27 Dec 2023 09:55:20 +0100 Subject: [PATCH 4/7] Recycle buttons Signed-off-by: mueller-ma --- .../java/org/openhab/habdroid/ui/WidgetAdapter.kt | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt b/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt index 6b0910f738..20c1178e54 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt +++ b/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt @@ -41,16 +41,13 @@ import androidx.annotation.LayoutRes import androidx.annotation.StringRes import androidx.annotation.VisibleForTesting import androidx.core.content.ContextCompat -import androidx.core.view.allViews import androidx.core.view.children import androidx.core.view.get import androidx.core.view.isGone -import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.core.widget.ContentLoadingProgressBar import androidx.core.widget.doAfterTextChanged import androidx.fragment.app.DialogFragment -import androidx.recyclerview.widget.RecyclerView import androidx.media3.common.MediaItem import androidx.media3.common.PlaybackException import androidx.media3.datasource.DataSource @@ -61,6 +58,7 @@ import androidx.media3.exoplayer.hls.HlsMediaSource import androidx.media3.exoplayer.source.LoadEventInfo import androidx.media3.exoplayer.source.MediaLoadData import androidx.media3.exoplayer.source.ProgressiveMediaSource +import androidx.recyclerview.widget.RecyclerView import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButtonToggleGroup import com.google.android.material.datepicker.MaterialDatePicker @@ -788,15 +786,24 @@ class WidgetAdapter( class ButtongridViewHolder internal constructor(private val initData: ViewHolderInitData) : LabeledItemBaseViewHolder(initData, R.layout.widgetlist_buttongriditem), View.OnClickListener { private val table: GridLayout = itemView.findViewById(R.id.widget_content) + private val spareViews = mutableListOf() override fun bind(widget: Widget) { super.bind(widget) val mappings = widget.mappings.filter { it.column != 0 && it.row != 0 } + spareViews.addAll(table.children.filter { it is MaterialButton }) + table.removeAllViews() table.rowCount = mappings.maxOfOrNull { it.row } ?: 0 table.columnCount = mappings.maxOfOrNull { it.column } ?: 0 (0 until table.rowCount).forEach { row -> (0 until table.columnCount).forEach { column -> - val buttonView = initData.inflater.inflate(R.layout.widgetlist_sectionswitchitem_button, null) as MaterialButton + var buttonView = spareViews.removeFirstOrNull() as MaterialButton? + if (buttonView == null) { + buttonView = initData.inflater.inflate( + R.layout.widgetlist_sectionswitchitem_button, + null + ) as MaterialButton + } // Rows and columns start with 1 in Sitemap definition, thus decrement them here val mapping = mappings.firstOrNull { it.row - 1 == row && it.column - 1 == column } if (mapping == null) { From 0895d946f0f860c620c2753de6fafc2d9ab57905 Mon Sep 17 00:00:00 2001 From: mueller-ma Date: Wed, 27 Dec 2023 10:01:09 +0100 Subject: [PATCH 5/7] Copy show/hide label behavior from BasicUI Signed-off-by: mueller-ma --- .../src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt b/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt index 20c1178e54..089a6972ce 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt +++ b/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt @@ -790,6 +790,12 @@ class WidgetAdapter( override fun bind(widget: Widget) { super.bind(widget) + + val showLabelAndIcon = widget.label.isNotEmpty() + && widget.labelSource == Widget.LabelSource.SitemapDefinition + labelView.isVisible = showLabelAndIcon + iconView.isVisible = showLabelAndIcon + val mappings = widget.mappings.filter { it.column != 0 && it.row != 0 } spareViews.addAll(table.children.filter { it is MaterialButton }) table.removeAllViews() From e4a6cccc12762a6e1471e209f6df6c58a0372726 Mon Sep 17 00:00:00 2001 From: mueller-ma Date: Wed, 27 Dec 2023 10:07:37 +0100 Subject: [PATCH 6/7] Limit columns based on screen size Signed-off-by: mueller-ma --- mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt b/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt index 089a6972ce..c39b9e05bf 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt +++ b/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt @@ -75,6 +75,7 @@ import java.time.format.DateTimeFormatter import java.time.format.FormatStyle import java.time.temporal.ChronoUnit import java.util.Locale +import kotlin.math.max import kotlin.math.min import kotlin.math.roundToInt import kotlinx.coroutines.CoroutineScope @@ -787,6 +788,7 @@ class WidgetAdapter( LabeledItemBaseViewHolder(initData, R.layout.widgetlist_buttongriditem), View.OnClickListener { private val table: GridLayout = itemView.findViewById(R.id.widget_content) private val spareViews = mutableListOf() + private val maxColumns = itemView.resources.getInteger(R.integer.section_switch_max_buttons) override fun bind(widget: Widget) { super.bind(widget) @@ -800,7 +802,7 @@ class WidgetAdapter( spareViews.addAll(table.children.filter { it is MaterialButton }) table.removeAllViews() table.rowCount = mappings.maxOfOrNull { it.row } ?: 0 - table.columnCount = mappings.maxOfOrNull { it.column } ?: 0 + table.columnCount = min(mappings.maxOfOrNull { it.column } ?: 0, maxColumns) (0 until table.rowCount).forEach { row -> (0 until table.columnCount).forEach { column -> var buttonView = spareViews.removeFirstOrNull() as MaterialButton? From c455971a6361188dc05a88726994890728328779 Mon Sep 17 00:00:00 2001 From: mueller-ma Date: Thu, 28 Dec 2023 11:14:03 +0100 Subject: [PATCH 7/7] Apply suggestion Signed-off-by: mueller-ma --- .../org/openhab/habdroid/ui/WidgetAdapter.kt | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt b/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt index c39b9e05bf..d33aa5d132 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt +++ b/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt @@ -44,6 +44,7 @@ import androidx.core.content.ContextCompat import androidx.core.view.children import androidx.core.view.get import androidx.core.view.isGone +import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.core.widget.ContentLoadingProgressBar import androidx.core.widget.doAfterTextChanged @@ -787,7 +788,7 @@ class WidgetAdapter( class ButtongridViewHolder internal constructor(private val initData: ViewHolderInitData) : LabeledItemBaseViewHolder(initData, R.layout.widgetlist_buttongriditem), View.OnClickListener { private val table: GridLayout = itemView.findViewById(R.id.widget_content) - private val spareViews = mutableListOf() + private val spareViews = mutableListOf() private val maxColumns = itemView.resources.getInteger(R.integer.section_switch_max_buttons) override fun bind(widget: Widget) { @@ -799,25 +800,21 @@ class WidgetAdapter( iconView.isVisible = showLabelAndIcon val mappings = widget.mappings.filter { it.column != 0 && it.row != 0 } - spareViews.addAll(table.children.filter { it is MaterialButton }) + spareViews.addAll(table.children.map { it as? MaterialButton }.filterNotNull()) table.removeAllViews() + table.rowCount = mappings.maxOfOrNull { it.row } ?: 0 table.columnCount = min(mappings.maxOfOrNull { it.column } ?: 0, maxColumns) (0 until table.rowCount).forEach { row -> (0 until table.columnCount).forEach { column -> - var buttonView = spareViews.removeFirstOrNull() as MaterialButton? - if (buttonView == null) { - buttonView = initData.inflater.inflate( - R.layout.widgetlist_sectionswitchitem_button, - null - ) as MaterialButton - } + val buttonView = spareViews.removeFirstOrNull() ?: + initData.inflater.inflate(R.layout.widgetlist_sectionswitchitem_button, table, false) + as MaterialButton // Rows and columns start with 1 in Sitemap definition, thus decrement them here val mapping = mappings.firstOrNull { it.row - 1 == row && it.column - 1 == column } - if (mapping == null) { - // Create invisible buttons so each cell has an equal size - buttonView.visibility = View.INVISIBLE - } else { + // Create invisible buttons if there's no mapping so each cell has an equal size + buttonView.isInvisible = mapping == null + if (mapping != null) { buttonView.setOnClickListener(this) buttonView.setTextAndIcon(connection, mapping) buttonView.tag = mapping.value