A src/cheogram/java/com/cheogram/android/GridView.java => src/cheogram/java/com/cheogram/android/GridView.java +38 -0
@@ 0,0 1,38 @@
+package com.cheogram.android;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+// https://blog.jayway.com/2012/10/04/how-to-make-the-height-of-a-gridview-wrap-its-content/
+public class GridView extends android.widget.GridView {
+ public GridView(Context context) {
+ super(context);
+ }
+
+ public GridView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public GridView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int heightSpec;
+
+ if (getLayoutParams().height == LayoutParams.WRAP_CONTENT) {
+ // The great Android "hackatlon", the love, the magic.
+ // The two leftmost bits in the height measure spec have
+ // a special meaning, hence we can't use them to describe height.
+ heightSpec = MeasureSpec.makeMeasureSpec(
+ Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
+ }
+ else {
+ // Any other height should be respected as is.
+ heightSpec = heightMeasureSpec;
+ }
+
+ super.onMeasure(widthMeasureSpec, heightSpec);
+ }
+}
A src/cheogram/java/eu/siacs/conversations/xmpp/Option.java => src/cheogram/java/eu/siacs/conversations/xmpp/Option.java +41 -0
@@ 0,0 1,41 @@
+package eu.siacs.conversations.xmpp;
+
+import java.util.ArrayList;
+import java.util.List;
+import eu.siacs.conversations.xml.Element;
+
+public class Option {
+ protected final String value;
+ protected final String label;
+
+ public static List<Option> forField(Element field) {
+ List<Option> options = new ArrayList<>();
+ for (Element el : field.getChildren()) {
+ if (!el.getNamespace().equals("jabber:x:data")) continue;
+ if (!el.getName().equals("option")) continue;
+ options.add(new Option(el));
+ }
+ return options;
+ }
+
+ public Option(final Element option) {
+ this(option.findChildContent("value", "jabber:x:data"), option.getAttribute("label"));
+ }
+
+ public Option(final String value, final String label) {
+ this.value = value;
+ this.label = label == null ? value : label;
+ }
+
+ public boolean equals(Object o) {
+ if (!(o instanceof Option)) return false;
+
+ if (value == ((Option) o).value) return true;
+ if (value == null || ((Option) o).value == null) return false;
+ return value.equals(((Option) o).value);
+ }
+
+ public String toString() { return label; }
+
+ public String getValue() { return value; }
+}
A src/cheogram/res/layout/command_radio_edit_field.xml => src/cheogram/res/layout/command_radio_edit_field.xml +41 -0
@@ 0,0 1,41 @@
+<?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.cheogram.android.GridView
+ android:id="@+id/radios"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="8dp"
+ android:paddingLeft="8dp"
+ android:paddingBottom="8dp"
+ android:horizontalSpacing="0dp"
+ android:verticalSpacing="0dp"
+ android:numColumns="auto_fit" />
+
+ <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="?attr/edit_text_color" />
+
+ </LinearLayout>
+</layout>
A src/cheogram/res/layout/radio_grid_item.xml => src/cheogram/res/layout/radio_grid_item.xml +9 -0
@@ 0,0 1,9 @@
+<?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">
+
+ <RadioButton
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+</layout>
M src/main/java/eu/siacs/conversations/entities/Conversation.java => src/main/java/eu/siacs/conversations/entities/Conversation.java +115 -34
@@ 6,6 6,8 @@ import android.database.DataSetObserver;
import android.net.Uri;
import android.text.Editable;
import android.text.InputType;
+import android.text.StaticLayout;
+import android.text.TextPaint;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.LayoutInflater;
@@ 19,6 21,7 @@ import android.widget.TextView;
import android.widget.Spinner;
import android.webkit.WebView;
import android.webkit.WebViewClient;
+import android.util.SparseArray;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ 53,6 56,7 @@ import eu.siacs.conversations.databinding.CommandPageBinding;
import eu.siacs.conversations.databinding.CommandNoteBinding;
import eu.siacs.conversations.databinding.CommandResultFieldBinding;
import eu.siacs.conversations.databinding.CommandCheckboxFieldBinding;
+import eu.siacs.conversations.databinding.CommandRadioEditFieldBinding;
import eu.siacs.conversations.databinding.CommandSpinnerFieldBinding;
import eu.siacs.conversations.databinding.CommandTextFieldBinding;
import eu.siacs.conversations.databinding.CommandWebviewBinding;
@@ 66,6 70,7 @@ import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.Option;
import eu.siacs.conversations.xmpp.chatstate.ChatState;
import eu.siacs.conversations.xmpp.mam.MamReference;
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
@@ 1421,6 1426,75 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
}
}
+ class RadioEditFieldViewHolder extends ViewHolder<CommandRadioEditFieldBinding> implements CompoundButton.OnCheckedChangeListener {
+ public RadioEditFieldViewHolder(CommandRadioEditFieldBinding binding) {
+ super(binding);
+ options = new ArrayAdapter<Option>(binding.getRoot().getContext(), R.layout.radio_grid_item) {
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ CompoundButton v = (CompoundButton) super.getView(position, convertView, parent);
+ v.setId(position);
+ v.setChecked(getItem(position).getValue().equals(mValue.getContent()));
+ v.setOnCheckedChangeListener(RadioEditFieldViewHolder.this);
+ return v;
+ }
+ };
+ }
+ protected Element mValue = null;
+ protected ArrayAdapter<Option> options;
+
+ @Override
+ public void bind(Element field) {
+ String label = field.getAttribute("label");
+ if (label == null) label = field.getAttribute("var");
+ if (label == null) {
+ binding.label.setVisibility(View.GONE);
+ } else {
+ binding.label.setVisibility(View.VISIBLE);
+ binding.label.setText(label);
+ }
+
+ String desc = field.findChildContent("desc", "jabber:x:data");
+ if (desc == null) {
+ binding.desc.setVisibility(View.GONE);
+ } else {
+ binding.desc.setVisibility(View.VISIBLE);
+ binding.desc.setText(desc);
+ }
+
+ mValue = field.findChild("value", "jabber:x:data");
+ if (mValue == null) {
+ mValue = field.addChild("value", "jabber:x:data");
+ }
+
+ options.clear();
+ List<Option> theOptions = Option.forField(field);
+ options.addAll(theOptions);
+
+ float screenWidth = binding.getRoot().getContext().getResources().getDisplayMetrics().widthPixels;
+ TextPaint paint = ((TextView) LayoutInflater.from(binding.getRoot().getContext()).inflate(R.layout.radio_grid_item, null)).getPaint();
+ float maxColumnWidth = theOptions.stream().map((x) ->
+ StaticLayout.getDesiredWidth(x.toString(), paint)
+ ).max(Float::compare).orElse(new Float(0.0));
+ if (maxColumnWidth * theOptions.size() < 0.90 * screenWidth) {
+ binding.radios.setNumColumns(theOptions.size());
+ } else if (maxColumnWidth * (theOptions.size() / 2) < 0.90 * screenWidth) {
+ binding.radios.setNumColumns(theOptions.size() / 2);
+ } else {
+ binding.radios.setNumColumns(1);
+ }
+ binding.radios.setAdapter(options);
+ }
+
+ @Override
+ public void onCheckedChanged(CompoundButton radio, boolean isChecked) {
+ if (mValue == null) return;
+
+ if (isChecked) mValue.setContent(options.getItem(radio.getId()).getValue());
+ options.notifyDataSetChanged();
+ }
+ }
+
class SpinnerFieldViewHolder extends ViewHolder<CommandSpinnerFieldBinding> implements AdapterView.OnItemSelectedListener {
public SpinnerFieldViewHolder(CommandSpinnerFieldBinding binding) {
super(binding);
@@ 1455,11 1529,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
ArrayAdapter<Option> options = new ArrayAdapter<Option>(binding.getRoot().getContext(), android.R.layout.simple_spinner_item);
options.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
- for (Element el : field.getChildren()) {
- if (!el.getNamespace().equals("jabber:x:data")) continue;
- if (!el.getName().equals("option")) continue;
- options.add(new Option(el));
- }
+ options.addAll(Option.forField(field));
+
binding.spinner.setAdapter(options);
binding.spinner.setSelection(options.getPosition(new Option(mValue.getContent(), null)));
}
@@ 1469,35 1540,13 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
Option o = (Option) parent.getItemAtPosition(pos);
if (mValue == null) return;
- mValue.setContent(o == null ? "" : o.value);
+ mValue.setContent(o == null ? "" : o.getValue());
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
mValue.setContent("");
}
-
- class Option {
- protected final String value;
- protected final String label;
-
- public Option(final Element option) {
- this(option.findChildContent("value", "jabber:x:data"), option.getAttribute("label"));
- }
-
- public Option(final String value, final String label) {
- this.value = value;
- this.label = label == null ? value : label;
- }
-
- public boolean equals(Object o) {
- if (!(o instanceof Option)) return false;
-
- return value.equals(((Option) o).value);
- }
-
- public String toString() { return label; }
- }
}
class TextFieldViewHolder extends ViewHolder<CommandTextFieldBinding> implements TextWatcher {
@@ 1597,11 1646,13 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
final int TYPE_TEXT_FIELD = 5;
final int TYPE_CHECKBOX_FIELD = 6;
final int TYPE_SPINNER_FIELD = 7;
+ final int TYPE_RADIO_EDIT_FIELD = 8;
protected String mTitle;
protected CommandPageBinding mBinding = null;
protected IqPacket response = null;
protected Element responseElement = null;
+ protected SparseArray<Integer> viewTypes = new SparseArray<>();
protected XmppConnectionService xmppConnectionService;
protected ArrayAdapter<String> actionsAdapter;
@@ 1641,6 1692,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
public void updateWithResponse(IqPacket iq) {
this.responseElement = null;
this.response = iq;
+ this.viewTypes.clear();
this.actionsAdapter.clear();
Element command = iq.findChild("command", "http://jabber.org/protocol/commands");
@@ 1743,13 1795,23 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
@Override
public int getItemViewType(int position) {
+ if (viewTypes.get(position) != null) return viewTypes.get(position);
if (response == null) return -1;
if (response.getType() == IqPacket.TYPE.RESULT) {
Element item = getItem(position);
- if (item.getName().equals("note")) return TYPE_NOTE;
- if (item.getNamespace().equals("jabber:x:oob")) return TYPE_WEB;
- if (item.getName().equals("instructions") && item.getNamespace().equals("jabber:x:data")) return TYPE_NOTE;
+ if (item.getName().equals("note")) {
+ viewTypes.put(position, TYPE_NOTE);
+ return TYPE_NOTE;
+ }
+ if (item.getNamespace().equals("jabber:x:oob")) {
+ viewTypes.put(position, TYPE_WEB);
+ return TYPE_WEB;
+ }
+ if (item.getName().equals("instructions") && item.getNamespace().equals("jabber:x:data")) {
+ viewTypes.put(position, TYPE_NOTE);
+ return TYPE_NOTE;
+ }
if (item.getName().equals("field") && item.getNamespace().equals("jabber:x:data")) {
String formType = responseElement.getAttribute("type");
if (formType == null) return -1;
@@ 1757,11 1819,26 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
String fieldType = item.getAttribute("type");
if (fieldType == null) fieldType = "text-single";
- if (formType.equals("result") || fieldType.equals("fixed")) return TYPE_RESULT_FIELD;
+ if (formType.equals("result") || fieldType.equals("fixed")) {
+ viewTypes.put(position, TYPE_RESULT_FIELD);
+ return TYPE_RESULT_FIELD;
+ }
if (formType.equals("form")) {
- if (fieldType.equals("boolean")) return TYPE_CHECKBOX_FIELD;
- if (fieldType.equals("list-single")) return TYPE_SPINNER_FIELD;
+ viewTypes.put(position, TYPE_CHECKBOX_FIELD);
+ if (fieldType.equals("boolean")) {
+ return TYPE_CHECKBOX_FIELD;
+ }
+ if (fieldType.equals("list-single")) {
+ if (item.findChild("value", "jabber:x:data") == null) {
+ viewTypes.put(position, TYPE_RADIO_EDIT_FIELD);
+ return TYPE_RADIO_EDIT_FIELD;
+ }
+ viewTypes.put(position, TYPE_SPINNER_FIELD);
+ return TYPE_SPINNER_FIELD;
+ }
+
+ viewTypes.put(position, TYPE_TEXT_FIELD);
return TYPE_TEXT_FIELD;
}
}
@@ 1794,6 1871,10 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
CommandCheckboxFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_checkbox_field, container, false);
return new CheckboxFieldViewHolder(binding);
}
+ case TYPE_RADIO_EDIT_FIELD: {
+ CommandRadioEditFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_radio_edit_field, container, false);
+ return new RadioEditFieldViewHolder(binding);
+ }
case TYPE_SPINNER_FIELD: {
CommandSpinnerFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_spinner_field, container, false);
return new SpinnerFieldViewHolder(binding);