Slider field

Stephen Paul Weber created

Change summary

src/cheogram/res/layout/command_slider_field.xml                | 42 +
src/cheogram/res/values/themes.xml                              |  4 
src/main/java/eu/siacs/conversations/entities/Conversation.java | 77 ++
3 files changed, 122 insertions(+), 1 deletion(-)

Detailed changes

src/cheogram/res/layout/command_slider_field.xml 🔗

@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <LinearLayout
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:paddingBottom="16dp"
+        android:orientation="vertical">
+
+        <TextView
+            android:id="@+id/label"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:paddingLeft="13dp"
+            android:paddingRight="8dp"
+            android:paddingBottom="8dp"
+            android:textAppearance="@style/TextAppearance.Conversations.Subhead"
+            android:textColor="?attr/edit_text_color" />
+
+        <com.google.android.material.slider.Slider
+            android:id="@+id/slider"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:paddingLeft="8dp"
+            android:paddingRight="8dp"
+            android:paddingBottom="8dp"
+            app:trackColorInactive="?color_background_primary"
+            app:trackColorActive="?colorAccent" />
+
+        <TextView
+            android:id="@+id/desc"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:paddingLeft="16dp"
+            android:paddingRight="8dp"
+            android:textAppearance="@style/TextAppearance.Conversations.Status"
+            android:textColor="?android:textColorSecondary" />
+
+    </LinearLayout>
+
+</layout>

src/cheogram/res/values/themes.xml 🔗

@@ -7,6 +7,10 @@
         <item name="colorAccent">@color/black_perpy</item>
         <item name="popupOverlayStyle">@style/ThemeOverlay.AppCompat.Light</item>
 
+        <item name="colorOnPrimary">?edit_text_color</item>
+        <item name="colorOnBackground">?colorPrimaryDark</item>
+        <item name="colorSurface">?colorPrimaryDark</item>
+
         <item name="message_bubble_received_bg">@color/perpy_desaturated_light</item>
         <item name="message_bubble_sent_bg">?color_background_primary</item>
         <item name="message_bubble_shadow_light">#00CCCCCC</item>

src/main/java/eu/siacs/conversations/entities/Conversation.java 🔗

@@ -88,6 +88,7 @@ import java.time.ZonedDateTime;
 import java.time.format.DateTimeParseException;
 import java.time.format.DateTimeFormatter;
 import java.time.format.FormatStyle;
+import java.text.DecimalFormat;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -120,6 +121,7 @@ import eu.siacs.conversations.databinding.CommandResultFieldBinding;
 import eu.siacs.conversations.databinding.CommandSearchListFieldBinding;
 import eu.siacs.conversations.databinding.CommandSpinnerFieldBinding;
 import eu.siacs.conversations.databinding.CommandTextFieldBinding;
+import eu.siacs.conversations.databinding.CommandSliderFieldBinding;
 import eu.siacs.conversations.databinding.CommandWebviewBinding;
 import eu.siacs.conversations.databinding.DialogQuickeditBinding;
 import eu.siacs.conversations.http.HttpConnectionManager;
@@ -2385,6 +2387,64 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                 public void onTextChanged(CharSequence s, int start, int count, int after) { }
             }
 
+            class SliderFieldViewHolder extends ViewHolder<CommandSliderFieldBinding> {
+                public SliderFieldViewHolder(CommandSliderFieldBinding binding) { super(binding); }
+                protected Field field = null;
+
+                @Override
+                public void bind(Item item) {
+                    field = (Field) item;
+                    setTextOrHide(binding.label, field.getLabel());
+                    setTextOrHide(binding.desc, field.getDesc());
+                    final Element validate = field.el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
+                    final String datatype = validate == null ? null : validate.getAttribute("datatype");
+                    final Element range = validate == null ? null : validate.findChild("range", "http://jabber.org/protocol/xdata-validate");
+                    final boolean open = validate != null && validate.findChild("open", "http://jabber.org/protocol/xdata-validate") != null;
+                    Float min = null;
+                    try { min = range.getAttribute("min") == null ? null : Float.valueOf(range.getAttribute("min")); } catch (NumberFormatException e) { }
+                    Float max = null;
+                    try { max = range.getAttribute("max") == null ? null : Float.valueOf(range.getAttribute("max"));  } catch (NumberFormatException e) { }
+
+                    List<Float> options = field.getOptions().stream().map(o -> Float.valueOf(o.getValue())).collect(Collectors.toList());
+                    Collections.sort(options);
+                    if (!open && options.size() > 0) {
+                        min = options.get(0);
+                        max = options.get(options.size()-1);
+                    }
+
+                    if (field.getValues().size() > 0) binding.slider.setValue(Float.valueOf(field.getValue().getContent()));
+                    binding.slider.setValueFrom(min == null ? Float.MIN_VALUE : min);
+                    binding.slider.setValueTo(max == null ? Float.MAX_VALUE : max);
+                    if ("xs:integer".equals(datatype) || "xs:int".equals(datatype) || "xs:long".equals(datatype) || "xs:short".equals(datatype) || "xs:byte".equals(datatype)) {
+                        binding.slider.setStepSize(1);
+                    } else {
+                        binding.slider.setStepSize(0);
+                    }
+
+                    if (!open && options.size() > 0) {
+                        float step = -1;
+                        Float prev = null;
+                        for (final Float option : options) {
+                            if (prev != null) {
+                                float nextStep = option - prev;
+                                if (step > 0 && step != nextStep) {
+                                    step = -1;
+                                    break;
+                                }
+                                step = nextStep;
+                            }
+                            prev = option;
+                        }
+                        if (step > 0) binding.slider.setStepSize(step);
+                        // NOTE: if step == -1 and !open then the widget will allow illegal values
+                    }
+
+                    binding.slider.addOnChangeListener((slider, value, fromUser) -> {
+                        field.setValues(List.of(new DecimalFormat().format(value)));
+                    });
+                }
+            }
+
             class WebViewHolder extends ViewHolder<CommandWebviewBinding> {
                 public WebViewHolder(CommandWebviewBinding binding) { super(binding); }
                 protected String boundUrl = "";
@@ -2551,14 +2611,24 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                     if (formType.equals("result") || fieldType.equals("fixed")) {
                         viewType = TYPE_RESULT_FIELD;
                     } else if (formType.equals("form")) {
+                        final Element validate = el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
+                        final String datatype = validate == null ? null : validate.getAttribute("datatype");
+                        final Element range = validate == null ? null : validate.findChild("range", "http://jabber.org/protocol/xdata-validate");
                         if (fieldType.equals("boolean")) {
                             if (fillableFieldCount == 1 && actionsAdapter.countExceptCancel() < 1) {
                                 viewType = TYPE_BUTTON_GRID_FIELD;
                             } else {
                                 viewType = TYPE_CHECKBOX_FIELD;
                             }
+                        } else if (
+                            range != null && (
+                                "xs:integer".equals(datatype) || "xs:int".equals(datatype) || "xs:long".equals(datatype) || "xs:short".equals(datatype) || "xs:byte".equals(datatype) ||
+                                "xs:decimal".equals(datatype) || "xs:double".equals(datatype)
+                            )
+                        ) {
+                            // has a range and is numeric, use a slider
+                            viewType = TYPE_SLIDER_FIELD;
                         } else if (fieldType.equals("list-single")) {
-                            Element validate = el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
                             if (fillableFieldCount == 1 && actionsAdapter.countExceptCancel() < 1) {
                                 viewType = TYPE_BUTTON_GRID_FIELD;
                             } else if (Option.forField(el).size() > 9) {
@@ -2666,6 +2736,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
             final int TYPE_SEARCH_LIST_FIELD = 11;
             final int TYPE_ITEM_CARD = 12;
             final int TYPE_BUTTON_GRID_FIELD = 13;
+            final int TYPE_SLIDER_FIELD = 14;
 
             protected boolean executing = false;
             protected boolean loading = false;
@@ -3054,6 +3125,10 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                         CommandTextFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_text_field, container, false);
                         return new TextFieldViewHolder(binding);
                     }
+                    case TYPE_SLIDER_FIELD: {
+                        CommandSliderFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_slider_field, container, false);
+                        return new SliderFieldViewHolder(binding);
+                    }
                     case TYPE_PROGRESSBAR: {
                         CommandProgressBarBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_progress_bar, container, false);
                         return new ProgressBarViewHolder(binding);