~singpolyma/cheogram-android

196453df74d85156aadd7f370fb5e6bd20edb1cf — Stephen Paul Weber 3 months ago 7b59d2b
If there is only one list-single field, render as buttons

If there is only one (non-cancel) action and only one field and it's a
list-single then let the user just tap the option they want to continue instead
of choosing something and then tapping next.
A src/cheogram/res/layout/button_grid_item.xml => src/cheogram/res/layout/button_grid_item.xml +10 -0
@@ 0,0 1,10 @@
<?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">

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textAllCaps="false" />

</layout>

A src/cheogram/res/layout/command_button_grid_field.xml => src/cheogram/res/layout/command_button_grid_field.xml +67 -0
@@ 0,0 1,67 @@
<?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:gravity="center"
            android:textAppearance="@style/TextAppearance.Conversations.Subhead"
            android:textColor="?attr/edit_text_color" />

        <TextView
            android:id="@+id/desc"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:paddingLeft="16dp"
            android:paddingRight="8dp"
            android:gravity="center"
            android:textAppearance="@style/TextAppearance.Conversations.Status"
            android:textColor="?android:textColorSecondary" />

        <Button
            android:id="@+id/default_button"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginRight="40dp"
            android:layout_marginLeft="40dp"
            android:layout_marginBottom="24dp"
            android:layout_marginTop="24dp"
            android:gravity="center"
            android:textAllCaps="false"
            android:minHeight="75dp" />

        <com.cheogram.android.GridView
            android:id="@+id/buttons"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:layout_marginRight="16dp"
            android:paddingLeft="16dp"
            android:horizontalSpacing="0dp"
            android:verticalSpacing="0dp"
            android:gravity="center"
            android:numColumns="auto_fit" />

        <Button
            android:id="@+id/open_button"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginRight="8dp"
            android:layout_marginLeft="8dp"
            android:layout_marginTop="40dp"
            android:gravity="center"
            android:text="other / custom"
            style="@style/Widget.Conversations.Button.Borderless" />

    </LinearLayout>
</layout>

M src/main/java/eu/siacs/conversations/entities/Conversation.java => src/main/java/eu/siacs/conversations/entities/Conversation.java +148 -7
@@ 2,6 2,7 @@ package eu.siacs.conversations.entities;

import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.graphics.Rect;


@@ 21,6 22,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.GridLayout;
import android.widget.ListView;


@@ 37,6 39,8 @@ import android.util.SparseArray;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AlertDialog.Builder;
import androidx.core.content.ContextCompat;
import androidx.databinding.DataBindingUtil;
import androidx.databinding.ViewDataBinding;


@@ 71,24 75,27 @@ import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.OmemoSetting;
import eu.siacs.conversations.crypto.PgpDecryptionService;
import eu.siacs.conversations.databinding.CommandPageBinding;
import eu.siacs.conversations.databinding.CommandNoteBinding;
import eu.siacs.conversations.databinding.CommandResultFieldBinding;
import eu.siacs.conversations.databinding.CommandResultCellBinding;
import eu.siacs.conversations.databinding.CommandItemCardBinding;
import eu.siacs.conversations.databinding.CommandButtonGridFieldBinding;
import eu.siacs.conversations.databinding.CommandCheckboxFieldBinding;
import eu.siacs.conversations.databinding.CommandItemCardBinding;
import eu.siacs.conversations.databinding.CommandNoteBinding;
import eu.siacs.conversations.databinding.CommandPageBinding;
import eu.siacs.conversations.databinding.CommandProgressBarBinding;
import eu.siacs.conversations.databinding.CommandRadioEditFieldBinding;
import eu.siacs.conversations.databinding.CommandResultCellBinding;
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.CommandWebviewBinding;
import eu.siacs.conversations.databinding.DialogQuickeditBinding;
import eu.siacs.conversations.persistance.DatabaseBackend;
import eu.siacs.conversations.services.AvatarService;
import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.text.FixedURLSpan;
import eu.siacs.conversations.ui.util.ShareUtil;
import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
import eu.siacs.conversations.utils.JidHelper;
import eu.siacs.conversations.utils.MessageUtils;
import eu.siacs.conversations.utils.UIHelper;


@@ 1840,6 1847,102 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                }
            }

            class ButtonGridFieldViewHolder extends ViewHolder<CommandButtonGridFieldBinding> {
                public ButtonGridFieldViewHolder(CommandButtonGridFieldBinding binding) {
                    super(binding);
                    options = new ArrayAdapter<Option>(binding.getRoot().getContext(), R.layout.button_grid_item) {
                        @Override
                        public View getView(int position, View convertView, ViewGroup parent) {
                            Button v = (Button) super.getView(position, convertView, parent);
                            v.setOnClickListener((view) -> {
                                mValue.setContent(getItem(position).getValue());
                                execute();
                            });
                            return v;
                        }
                    };
                }
                protected Element mValue = null;
                protected ArrayAdapter<Option> options;
                protected Option defaultOption = null;

                @Override
                public void bind(Item item) {
                    Field field = (Field) item;
                    setTextOrHide(binding.label, field.getLabel());
                    setTextOrHide(binding.desc, field.getDesc());

                    if (field.error != null) {
                        binding.desc.setVisibility(View.VISIBLE);
                        binding.desc.setText(field.error);
                        binding.desc.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Design_Error);
                    } else {
                        binding.desc.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Status);
                    }

                    mValue = field.getValue();

                    Element validate = field.el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
                    binding.openButton.setVisibility((validate != null && validate.findChild("open", "http://jabber.org/protocol/xdata-validate") != null) ? View.VISIBLE : View.GONE);
                    binding.openButton.setOnClickListener((view) -> {
                        AlertDialog.Builder builder = new AlertDialog.Builder(binding.getRoot().getContext());
                        DialogQuickeditBinding dialogBinding = DataBindingUtil.inflate(LayoutInflater.from(binding.getRoot().getContext()), R.layout.dialog_quickedit, null, false);
                        builder.setPositiveButton(R.string.action_execute, null);
                        if (field.getDesc().isPresent()) {
                            dialogBinding.inputLayout.setHint(field.getDesc().get());
                        }
                        dialogBinding.inputEditText.requestFocus();
                        dialogBinding.inputEditText.getText().append(mValue.getContent());
                        builder.setView(dialogBinding.getRoot());
                        builder.setNegativeButton(R.string.cancel, null);
                        final AlertDialog dialog = builder.create();
                        dialog.setOnShowListener(d -> SoftKeyboardUtils.showKeyboard(dialogBinding.inputEditText));
                        dialog.show();
                        View.OnClickListener clickListener = v -> {
                            String value = dialogBinding.inputEditText.getText().toString();
                            mValue.setContent(value);
                            SoftKeyboardUtils.hideSoftKeyboard(dialogBinding.inputEditText);
                            dialog.dismiss();
                            execute();
                        };
                        dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(clickListener);
                        dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((v -> {
                            SoftKeyboardUtils.hideSoftKeyboard(dialogBinding.inputEditText);
                            dialog.dismiss();
                        }));
                        dialog.setCanceledOnTouchOutside(false);
                        dialog.setOnDismissListener(dialog1 -> {
                            SoftKeyboardUtils.hideSoftKeyboard(dialogBinding.inputEditText);
                        });
                    });

                    options.clear();
                    List<Option> theOptions = field.getOptions();

                    defaultOption = null;
                    for (Option option : theOptions) {
                        if (option.getValue().equals(mValue.getContent())) {
                            defaultOption = option;
                            break;
                        }
                    }
                    if (defaultOption == null) {
                        binding.defaultButton.setVisibility(View.GONE);
                    } else {
                        theOptions.remove(defaultOption);
                        binding.defaultButton.setVisibility(View.VISIBLE);
                        binding.defaultButton.setText(defaultOption.toString());
                        binding.defaultButton.setOnClickListener((view) -> {
                            mValue.setContent(defaultOption.getValue());
                            execute();
                        });
                    }

                    options.addAll(theOptions);
                    binding.buttons.setAdapter(options);
                }
            }

            class TextFieldViewHolder extends ViewHolder<CommandTextFieldBinding> implements TextWatcher {
                public TextFieldViewHolder(CommandTextFieldBinding binding) {
                    super(binding);


@@ 2027,6 2130,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                            Element validate = el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
                            if (Option.forField(el).size() > 9) {
                                viewType = TYPE_SEARCH_LIST_FIELD;
                            } else if (fillableFieldCount == 1 && actionsAdapter.countExceptCancel() < 1) {
                                viewType = TYPE_BUTTON_GRID_FIELD;
                            } else if (el.findChild("value", "jabber:x:data") == null || (validate != null && validate.findChild("open", "http://jabber.org/protocol/xdata-validate") != null)) {
                                viewType = TYPE_RADIO_EDIT_FIELD;
                            } else {


@@ 2096,6 2201,23 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                    }
                    return -1;
                }

                public int countExceptCancel() {
                    int count = 0;
                    for(int i = 0; i < getCount(); i++) {
                        if (!getItem(i).first.equals("cancel")) count++;
                    }
                    return count;
                }

                public void clearExceptCancel() {
                    Pair<String,String> cancelItem = null;
                    for(int i = 0; i < getCount(); i++) {
                        if (getItem(i).first.equals("cancel")) cancelItem = getItem(i);
                    }
                    clear();
                    if (cancelItem != null) add(cancelItem);
                }
            }

            final int TYPE_ERROR = 1;


@@ 2110,6 2232,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
            final int TYPE_PROGRESSBAR = 10;
            final int TYPE_SEARCH_LIST_FIELD = 11;
            final int TYPE_ITEM_CARD = 12;
            final int TYPE_BUTTON_GRID_FIELD = 13;

            protected boolean loading = false;
            protected Timer loadingTimer = new Timer();


@@ 2124,6 2247,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
            protected ActionsAdapter actionsAdapter;
            protected GridLayoutManager layoutManager;
            protected WebView actionToWebview = null;
            protected int fillableFieldCount = 0;

            CommandSession(String title, String node, XmppConnectionService xmppConnectionService) {
                loading();


@@ 2154,6 2278,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                this.loadingTimer = new Timer();
                this.loading = false;
                this.responseElement = null;
                this.fillableFieldCount = 0;
                this.reported = null;
                this.response = iq;
                this.items.clear();


@@ 2197,6 2322,18 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                                    actionsAdapter.add(Pair.create(action.getValue(), action.toString()));
                                }
                            }

                            String fillableFieldType = null;
                            for (eu.siacs.conversations.xmpp.forms.Field field : form.getFields()) {
                                if (field.getType() != null && !field.getType().equals("hidden") && !field.getType().equals("fixed") && !field.getFieldName().equals("http://jabber.org/protocol/commands#actions")) {
                                    fillableFieldType = field.getType();
                                    fillableFieldCount++;
                                }
                            }

                            if (fillableFieldCount == 1 && actionsAdapter.countExceptCancel() < 2 && fillableFieldType != null && fillableFieldType.equals("list-single")) {
                                actionsAdapter.clearExceptCancel();
                            }
                            break;
                        }
                        if (el.getName().equals("x") && el.getNamespace().equals("jabber:x:oob")) {


@@ 2220,13 2357,13 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                        return;
                    }

                    if (command.getAttribute("status").equals("executing") && actionsAdapter.getCount() < 1) {
                    if (command.getAttribute("status").equals("executing") && actionsAdapter.countExceptCancel() < 1 && fillableFieldCount > 1) {
                        // No actions have been given, but we are not done?
                        // This is probably a spec violation, but we should do *something*
                        actionsAdapter.add(Pair.create("execute", "execute"));
                    }

                    if (!actionsAdapter.isEmpty()) {
                    if (!actionsAdapter.isEmpty() || fillableFieldCount > 0) {
                        if (command.getAttribute("status").equals("completed") || command.getAttribute("status").equals("canceled")) {
                            actionsAdapter.add(Pair.create("close", "close"));
                        } else if (actionsAdapter.getPosition("cancel") < 0) {


@@ 2401,6 2538,10 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                        CommandSpinnerFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_spinner_field, container, false);
                        return new SpinnerFieldViewHolder(binding);
                    }
                    case TYPE_BUTTON_GRID_FIELD: {
                        CommandButtonGridFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_button_grid_field, container, false);
                        return new ButtonGridFieldViewHolder(binding);
                    }
                    case TYPE_TEXT_FIELD: {
                        CommandTextFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_text_field, container, false);
                        return new TextFieldViewHolder(binding);