]> git.ktnx.net Git - mobile-ledger.git/blobdiff - app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateDetailsAdapter.java
whitespace
[mobile-ledger.git] / app / src / main / java / net / ktnx / mobileledger / ui / templates / TemplateDetailsAdapter.java
index 23260e008f515d8a55f0105b196c485c72c96d3c..9152fdfd691a858f4bdc6cb63123a82f70d8c736 100644 (file)
 
 package net.ktnx.mobileledger.ui.templates;
 
+import android.annotation.SuppressLint;
+import android.content.res.Resources;
 import android.text.Editable;
 import android.text.TextWatcher;
 import android.view.LayoutInflater;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.TextView;
@@ -28,13 +31,14 @@ import androidx.annotation.NonNull;
 import androidx.appcompat.app.AppCompatActivity;
 import androidx.recyclerview.widget.AsyncListDiffer;
 import androidx.recyclerview.widget.DiffUtil;
+import androidx.recyclerview.widget.ItemTouchHelper;
 import androidx.recyclerview.widget.RecyclerView;
 
+import net.ktnx.mobileledger.BuildConfig;
 import net.ktnx.mobileledger.R;
 import net.ktnx.mobileledger.databinding.TemplateDetailsAccountBinding;
 import net.ktnx.mobileledger.databinding.TemplateDetailsHeaderBinding;
 import net.ktnx.mobileledger.db.AccountAutocompleteAdapter;
-import net.ktnx.mobileledger.db.TemplateBase;
 import net.ktnx.mobileledger.model.Data;
 import net.ktnx.mobileledger.model.TemplateDetailsItem;
 import net.ktnx.mobileledger.ui.QRScanCapableFragment;
@@ -45,7 +49,6 @@ import net.ktnx.mobileledger.utils.Misc;
 import org.jetbrains.annotations.NotNull;
 
 import java.text.ParseException;
-import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
 import java.util.regex.Matcher;
@@ -54,8 +57,11 @@ import java.util.regex.Pattern;
 class TemplateDetailsAdapter extends RecyclerView.Adapter<TemplateDetailsAdapter.ViewHolder> {
     private static final String D_TEMPLATE_UI = "template-ui";
     private final AsyncListDiffer<TemplateDetailsItem> differ;
-    public TemplateDetailsAdapter() {
+    private final TemplateDetailsViewModel mModel;
+    private final ItemTouchHelper itemTouchHelper;
+    public TemplateDetailsAdapter(TemplateDetailsViewModel model) {
         super();
+        mModel = model;
         setHasStableIds(true);
         differ = new AsyncListDiffer<>(this, new DiffUtil.ItemCallback<TemplateDetailsItem>() {
             @Override
@@ -90,12 +96,113 @@ class TemplateDetailsAdapter extends RecyclerView.Adapter<TemplateDetailsAdapter
                 }
             }
         });
+        itemTouchHelper = new ItemTouchHelper(new ItemTouchHelper.Callback() {
+            @Override
+            public float getMoveThreshold(@NonNull RecyclerView.ViewHolder viewHolder) {
+                return 0.5f;
+            }
+            @Override
+            public boolean isLongPressDragEnabled() {
+                return false;
+            }
+            @Override
+            public RecyclerView.ViewHolder chooseDropTarget(
+                    @NonNull RecyclerView.ViewHolder selected,
+                    @NonNull List<RecyclerView.ViewHolder> dropTargets, int curX, int curY) {
+                RecyclerView.ViewHolder best = null;
+                int bestDistance = 0;
+                for (RecyclerView.ViewHolder v : dropTargets) {
+                    if (v == selected)
+                        continue;
+
+                    final int viewTop = v.itemView.getTop();
+                    int distance = Math.abs(viewTop - curY);
+                    if (best == null) {
+                        best = v;
+                        bestDistance = distance;
+                    }
+                    else {
+                        if (distance < bestDistance) {
+                            bestDistance = distance;
+                            best = v;
+                        }
+                    }
+                }
+
+                Logger.debug("dnd", "Best target is " + best);
+                return best;
+            }
+            @Override
+            public boolean canDropOver(@NonNull RecyclerView recyclerView,
+                                       @NonNull RecyclerView.ViewHolder current,
+                                       @NonNull RecyclerView.ViewHolder target) {
+                final int adapterPosition = target.getAdapterPosition();
+
+                // first item is immovable
+                if (adapterPosition == 0)
+                    return false;
+
+                return super.canDropOver(recyclerView, current, target);
+            }
+            @Override
+            public int getMovementFlags(@NonNull RecyclerView recyclerView,
+                                        @NonNull RecyclerView.ViewHolder viewHolder) {
+                int flags = 0;
+                // the top item (transaction params) is always there
+                final int adapterPosition = viewHolder.getAdapterPosition();
+                if (adapterPosition > 0)
+                    flags |= makeFlag(ItemTouchHelper.ACTION_STATE_DRAG,
+                            ItemTouchHelper.UP | ItemTouchHelper.DOWN) |
+                             makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE,
+                                     ItemTouchHelper.START | ItemTouchHelper.END);
+
+                return flags;
+            }
+            @Override
+            public boolean onMove(@NonNull RecyclerView recyclerView,
+                                  @NonNull RecyclerView.ViewHolder viewHolder,
+                                  @NonNull RecyclerView.ViewHolder target) {
+
+                final int fromPosition = viewHolder.getAdapterPosition();
+                final int toPosition = target.getAdapterPosition();
+                if (fromPosition == toPosition) {
+                    Logger.debug("drag", String.format(Locale.US,
+                            "Ignoring request to move an account from position %d to %d",
+                            fromPosition, toPosition));
+                    return false;
+                }
+
+                Logger.debug("drag",
+                        String.format(Locale.US, "Moving account from %d to %d", fromPosition,
+                                toPosition));
+                mModel.moveItem(fromPosition, toPosition);
+
+                return true;
+            }
+            @Override
+            public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
+                int pos = viewHolder.getAdapterPosition();
+                mModel.removeItem(pos);
+            }
+        });
+    }
+    @Override
+    public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
+        super.onAttachedToRecyclerView(recyclerView);
+
+        itemTouchHelper.attachToRecyclerView(recyclerView);
+    }
+    @Override
+    public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
+        super.onDetachedFromRecyclerView(recyclerView);
+
+        itemTouchHelper.attachToRecyclerView(null);
     }
     @Override
     public long getItemId(int position) {
         // header item is always first and IDs id may duplicate some of the account IDs
         if (position == 0)
-            return -1;
+            return 0;
         TemplateDetailsItem.AccountRow accRow = differ.getCurrentList()
                                                       .get(position)
                                                       .asAccountRowItem();
@@ -135,15 +242,16 @@ class TemplateDetailsAdapter extends RecyclerView.Adapter<TemplateDetailsAdapter
         return differ.getCurrentList()
                      .size();
     }
-    public void setTemplateItems(List<TemplateBase> items) {
-        ArrayList<TemplateDetailsItem> list = new ArrayList<>();
-        for (TemplateBase p : items) {
-            TemplateDetailsItem item = TemplateDetailsItem.fromRoomObject(p);
-            list.add(item);
-        }
-        setItems(list);
-    }
     public void setItems(List<TemplateDetailsItem> items) {
+        if (BuildConfig.DEBUG) {
+            Logger.debug("tmpl", "Got new list");
+            for (int i = 1; i < items.size(); i++) {
+                final TemplateDetailsItem item = items.get(i);
+                Logger.debug("tmpl",
+                        String.format(Locale.US, "  %d: id %d, pos %d", i, item.getId(),
+                                item.getPosition()));
+            }
+        }
         differ.submitList(items);
     }
     public String getMatchGroupText(int groupNumber) {
@@ -176,8 +284,22 @@ class TemplateDetailsAdapter extends RecyclerView.Adapter<TemplateDetailsAdapter
         abstract void bind(TemplateDetailsItem item);
     }
 
-    public class Header extends ViewHolder {
+    private abstract static class BaseItem extends ViewHolder {
+        boolean updatePropagationDisabled = false;
+        BaseItem(@NonNull View itemView) {
+            super(itemView);
+        }
+        void disableUpdatePropagation() {
+            updatePropagationDisabled = true;
+        }
+        void enableUpdatePropagation() {
+            updatePropagationDisabled = false;
+        }
+    }
+
+    public class Header extends BaseItem {
         private final TemplateDetailsHeaderBinding b;
+        boolean updatePropagationDisabled = false;
         public Header(@NonNull TemplateDetailsHeaderBinding binding) {
             super(binding.getRoot());
             b = binding;
@@ -189,6 +311,9 @@ class TemplateDetailsAdapter extends RecyclerView.Adapter<TemplateDetailsAdapter
                 public void onTextChanged(CharSequence s, int start, int before, int count) {}
                 @Override
                 public void afterTextChanged(Editable s) {
+                    if (updatePropagationDisabled)
+                        return;
+
                     final TemplateDetailsItem.Header header = getItem();
                     Logger.debug(D_TEMPLATE_UI,
                             "Storing changed template name " + s + "; header=" + header);
@@ -204,6 +329,9 @@ class TemplateDetailsAdapter extends RecyclerView.Adapter<TemplateDetailsAdapter
                 public void onTextChanged(CharSequence s, int start, int before, int count) {}
                 @Override
                 public void afterTextChanged(Editable s) {
+                    if (updatePropagationDisabled)
+                        return;
+
                     final TemplateDetailsItem.Header header = getItem();
                     Logger.debug(D_TEMPLATE_UI,
                             "Storing changed pattern " + s + "; header=" + header);
@@ -221,6 +349,9 @@ class TemplateDetailsAdapter extends RecyclerView.Adapter<TemplateDetailsAdapter
                 public void onTextChanged(CharSequence s, int start, int before, int count) {}
                 @Override
                 public void afterTextChanged(Editable s) {
+                    if (updatePropagationDisabled)
+                        return;
+
                     final TemplateDetailsItem.Header header = getItem();
                     Logger.debug(D_TEMPLATE_UI,
                             "Storing changed test text " + s + "; header=" + header);
@@ -237,10 +368,12 @@ class TemplateDetailsAdapter extends RecyclerView.Adapter<TemplateDetailsAdapter
                 }
                 @Override
                 public void onTextChanged(CharSequence s, int start, int before, int count) {
-
                 }
                 @Override
                 public void afterTextChanged(Editable s) {
+                    if (updatePropagationDisabled)
+                        return;
+
                     final TemplateDetailsItem.Header header = getItem();
                     Logger.debug(D_TEMPLATE_UI,
                             "Storing changed transaction description " + s + "; header=" + header);
@@ -251,14 +384,15 @@ class TemplateDetailsAdapter extends RecyclerView.Adapter<TemplateDetailsAdapter
             TextWatcher transactionCommentWatcher = new TextWatcher() {
                 @Override
                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
-
                 }
                 @Override
                 public void onTextChanged(CharSequence s, int start, int before, int count) {
-
                 }
                 @Override
                 public void afterTextChanged(Editable s) {
+                    if (updatePropagationDisabled)
+                        return;
+
                     final TemplateDetailsItem.Header header = getItem();
                     Logger.debug(D_TEMPLATE_UI,
                             "Storing changed transaction description " + s + "; header=" + header);
@@ -266,6 +400,19 @@ class TemplateDetailsAdapter extends RecyclerView.Adapter<TemplateDetailsAdapter
                 }
             };
             b.transactionComment.addTextChangedListener(transactionCommentWatcher);
+
+            b.templateIsFallbackSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
+                if (updatePropagationDisabled)
+                    return;
+
+                getItem().setFallback(isChecked);
+                b.templateIsFallbackText.setText(isChecked ? R.string.template_is_fallback_yes
+                                                           : R.string.template_is_fallback_no);
+            });
+            final View.OnClickListener fallbackLabelClickListener =
+                    (view) -> b.templateIsFallbackSwitch.toggle();
+            b.templateIsFallbackLabel.setOnClickListener(fallbackLabelClickListener);
+            b.templateIsFallbackText.setOnClickListener(fallbackLabelClickListener);
         }
         @NotNull
         private TemplateDetailsItem.Header getItem() {
@@ -334,107 +481,119 @@ class TemplateDetailsAdapter extends RecyclerView.Adapter<TemplateDetailsAdapter
             TemplateDetailsItem.Header header = item.asHeaderItem();
             Logger.debug(D_TEMPLATE_UI, "Binding to header " + header);
 
-            String groupNoText = b.getRoot()
-                                  .getResources()
-                                  .getString(R.string.template_item_match_group_source);
-
-            b.templateName.setText(header.getName());
-            b.pattern.setText(header.getPattern());
-            b.testText.setText(header.getTestText());
+            disableUpdatePropagation();
+            try {
+                String groupNoText = b.getRoot()
+                                      .getResources()
+                                      .getString(R.string.template_item_match_group_source);
+
+                b.templateName.setText(header.getName());
+                b.pattern.setText(header.getPattern());
+                b.testText.setText(header.getTestText());
+
+                if (header.hasLiteralDateYear()) {
+                    b.templateDetailsYearSource.setText(R.string.template_details_source_literal);
+                    final Integer dateYear = header.getDateYear();
+                    b.templateDetailsDateYear.setText(
+                            (dateYear == null) ? null : String.valueOf(dateYear));
+                    b.templateDetailsDateYearLayout.setVisibility(View.VISIBLE);
+                }
+                else {
+                    b.templateDetailsDateYearLayout.setVisibility(View.GONE);
+                    b.templateDetailsYearSource.setText(
+                            String.format(Locale.US, groupNoText, header.getDateYearMatchGroup(),
+                                    getMatchGroupText(header.getDateYearMatchGroup())));
+                }
+                b.templateDetailsYearSourceLabel.setOnClickListener(
+                        v -> selectHeaderDetailSource(v, HeaderDetail.DATE_YEAR));
+                b.templateDetailsYearSource.setOnClickListener(
+                        v -> selectHeaderDetailSource(v, HeaderDetail.DATE_YEAR));
+
+                if (header.hasLiteralDateMonth()) {
+                    b.templateDetailsMonthSource.setText(R.string.template_details_source_literal);
+                    final Integer dateMonth = header.getDateMonth();
+                    b.templateDetailsDateMonth.setText(
+                            (dateMonth == null) ? null : String.valueOf(dateMonth));
+                    b.templateDetailsDateMonthLayout.setVisibility(View.VISIBLE);
+                }
+                else {
+                    b.templateDetailsDateMonthLayout.setVisibility(View.GONE);
+                    b.templateDetailsMonthSource.setText(
+                            String.format(Locale.US, groupNoText, header.getDateMonthMatchGroup(),
+                                    getMatchGroupText(header.getDateMonthMatchGroup())));
+                }
+                b.templateDetailsMonthSourceLabel.setOnClickListener(
+                        v -> selectHeaderDetailSource(v, HeaderDetail.DATE_MONTH));
+                b.templateDetailsMonthSource.setOnClickListener(
+                        v -> selectHeaderDetailSource(v, HeaderDetail.DATE_MONTH));
+
+                if (header.hasLiteralDateDay()) {
+                    b.templateDetailsDaySource.setText(R.string.template_details_source_literal);
+                    final Integer dateDay = header.getDateDay();
+                    b.templateDetailsDateDay.setText(
+                            (dateDay == null) ? null : String.valueOf(dateDay));
+                    b.templateDetailsDateDayLayout.setVisibility(View.VISIBLE);
+                }
+                else {
+                    b.templateDetailsDateDayLayout.setVisibility(View.GONE);
+                    b.templateDetailsDaySource.setText(
+                            String.format(Locale.US, groupNoText, header.getDateDayMatchGroup(),
+                                    getMatchGroupText(header.getDateDayMatchGroup())));
+                }
+                b.templateDetailsDaySourceLabel.setOnClickListener(
+                        v -> selectHeaderDetailSource(v, HeaderDetail.DATE_DAY));
+                b.templateDetailsDaySource.setOnClickListener(
+                        v -> selectHeaderDetailSource(v, HeaderDetail.DATE_DAY));
+
+                if (header.hasLiteralTransactionDescription()) {
+                    b.templateTransactionDescriptionSource.setText(
+                            R.string.template_details_source_literal);
+                    b.transactionDescription.setText(header.getTransactionDescription());
+                    b.transactionDescriptionLayout.setVisibility(View.VISIBLE);
+                }
+                else {
+                    b.transactionDescriptionLayout.setVisibility(View.GONE);
+                    b.templateTransactionDescriptionSource.setText(
+                            String.format(Locale.US, groupNoText,
+                                    header.getTransactionDescriptionMatchGroup(), getMatchGroupText(
+                                            header.getTransactionDescriptionMatchGroup())));
 
-            if (header.hasLiteralDateYear()) {
-                b.templateDetailsYearSource.setText(R.string.template_details_source_literal);
-                final Integer dateYear = header.getDateYear();
-                b.templateDetailsDateYear.setText(
-                        (dateYear == null) ? null : String.valueOf(dateYear));
-                b.templateDetailsDateYearLayout.setVisibility(View.VISIBLE);
-            }
-            else {
-                b.templateDetailsDateYearLayout.setVisibility(View.GONE);
-                b.templateDetailsYearSource.setText(
-                        String.format(Locale.US, groupNoText, header.getDateYearMatchGroup(),
-                                getMatchGroupText(header.getDateYearMatchGroup())));
-            }
-            b.templateDetailsYearSourceLabel.setOnClickListener(
-                    v -> selectHeaderDetailSource(v, HeaderDetail.DATE_YEAR));
-            b.templateDetailsYearSource.setOnClickListener(
-                    v -> selectHeaderDetailSource(v, HeaderDetail.DATE_YEAR));
-
-            if (header.hasLiteralDateMonth()) {
-                b.templateDetailsMonthSource.setText(R.string.template_details_source_literal);
-                final Integer dateMonth = header.getDateMonth();
-                b.templateDetailsDateMonth.setText(
-                        (dateMonth == null) ? null : String.valueOf(dateMonth));
-                b.templateDetailsDateMonthLayout.setVisibility(View.VISIBLE);
-            }
-            else {
-                b.templateDetailsDateMonthLayout.setVisibility(View.GONE);
-                b.templateDetailsMonthSource.setText(
-                        String.format(Locale.US, groupNoText, header.getDateMonthMatchGroup(),
-                                getMatchGroupText(header.getDateMonthMatchGroup())));
-            }
-            b.templateDetailsMonthSourceLabel.setOnClickListener(
-                    v -> selectHeaderDetailSource(v, HeaderDetail.DATE_MONTH));
-            b.templateDetailsMonthSource.setOnClickListener(
-                    v -> selectHeaderDetailSource(v, HeaderDetail.DATE_MONTH));
-
-            if (header.hasLiteralDateDay()) {
-                b.templateDetailsDaySource.setText(R.string.template_details_source_literal);
-                final Integer dateDay = header.getDateDay();
-                b.templateDetailsDateDay.setText(
-                        (dateDay == null) ? null : String.valueOf(dateDay));
-                b.templateDetailsDateDayLayout.setVisibility(View.VISIBLE);
-            }
-            else {
-                b.templateDetailsDateDayLayout.setVisibility(View.GONE);
-                b.templateDetailsDaySource.setText(
-                        String.format(Locale.US, groupNoText, header.getDateDayMatchGroup(),
-                                getMatchGroupText(header.getDateDayMatchGroup())));
-            }
-            b.templateDetailsDaySourceLabel.setOnClickListener(
-                    v -> selectHeaderDetailSource(v, HeaderDetail.DATE_DAY));
-            b.templateDetailsDaySource.setOnClickListener(
-                    v -> selectHeaderDetailSource(v, HeaderDetail.DATE_DAY));
-
-            if (header.hasLiteralTransactionDescription()) {
-                b.templateTransactionDescriptionSource.setText(
-                        R.string.template_details_source_literal);
-                b.transactionDescription.setText(header.getTransactionDescription());
-                b.transactionDescriptionLayout.setVisibility(View.VISIBLE);
-            }
-            else {
-                b.transactionDescriptionLayout.setVisibility(View.GONE);
-                b.templateTransactionDescriptionSource.setText(String.format(Locale.US, groupNoText,
-                        header.getTransactionDescriptionMatchGroup(),
-                        getMatchGroupText(header.getTransactionDescriptionMatchGroup())));
+                }
+                b.templateTransactionDescriptionSourceLabel.setOnClickListener(
+                        v -> selectHeaderDetailSource(v, HeaderDetail.DESCRIPTION));
+                b.templateTransactionDescriptionSource.setOnClickListener(
+                        v -> selectHeaderDetailSource(v, HeaderDetail.DESCRIPTION));
+
+                if (header.hasLiteralTransactionComment()) {
+                    b.templateTransactionCommentSource.setText(
+                            R.string.template_details_source_literal);
+                    b.transactionComment.setText(header.getTransactionComment());
+                    b.transactionCommentLayout.setVisibility(View.VISIBLE);
+                }
+                else {
+                    b.transactionCommentLayout.setVisibility(View.GONE);
+                    b.templateTransactionCommentSource.setText(String.format(Locale.US, groupNoText,
+                            header.getTransactionCommentMatchGroup(),
+                            getMatchGroupText(header.getTransactionCommentMatchGroup())));
 
-            }
-            b.templateTransactionDescriptionSourceLabel.setOnClickListener(
-                    v -> selectHeaderDetailSource(v, HeaderDetail.DESCRIPTION));
-            b.templateTransactionDescriptionSource.setOnClickListener(
-                    v -> selectHeaderDetailSource(v, HeaderDetail.DESCRIPTION));
-
-            if (header.hasLiteralTransactionComment()) {
-                b.templateTransactionCommentSource.setText(
-                        R.string.template_details_source_literal);
-                b.transactionComment.setText(header.getTransactionComment());
-                b.transactionCommentLayout.setVisibility(View.VISIBLE);
-            }
-            else {
-                b.transactionCommentLayout.setVisibility(View.GONE);
-                b.templateTransactionCommentSource.setText(String.format(Locale.US, groupNoText,
-                        header.getTransactionCommentMatchGroup(),
-                        getMatchGroupText(header.getTransactionCommentMatchGroup())));
+                }
+                b.templateTransactionCommentSourceLabel.setOnClickListener(
+                        v -> selectHeaderDetailSource(v, HeaderDetail.COMMENT));
+                b.templateTransactionCommentSource.setOnClickListener(
+                        v -> selectHeaderDetailSource(v, HeaderDetail.COMMENT));
 
-            }
-            b.templateTransactionCommentSourceLabel.setOnClickListener(
-                    v -> selectHeaderDetailSource(v, HeaderDetail.COMMENT));
-            b.templateTransactionCommentSource.setOnClickListener(
-                    v -> selectHeaderDetailSource(v, HeaderDetail.COMMENT));
+                b.templateDetailsHeadScanQrButton.setOnClickListener(this::scanTestQR);
 
-            b.templateDetailsHeadScanQrButton.setOnClickListener(this::scanTestQR);
+                b.templateIsFallbackSwitch.setChecked(header.isFallback());
+                b.templateIsFallbackText.setText(
+                        header.isFallback() ? R.string.template_is_fallback_yes
+                                            : R.string.template_is_fallback_no);
 
-            checkPatternError(header);
+                checkPatternError(header);
+            }
+            finally {
+                enableUpdatePropagation();
+            }
         }
         private void checkPatternError(TemplateDetailsItem.Header item) {
             if (item.getPatternError() != null) {
@@ -462,7 +621,7 @@ class TemplateDetailsAdapter extends RecyclerView.Adapter<TemplateDetailsAdapter
         }
     }
 
-    public class AccountRow extends ViewHolder {
+    public class AccountRow extends BaseItem {
         private final TemplateDetailsAccountBinding b;
         public AccountRow(@NonNull TemplateDetailsAccountBinding binding) {
             super(binding.getRoot());
@@ -475,10 +634,15 @@ class TemplateDetailsAdapter extends RecyclerView.Adapter<TemplateDetailsAdapter
                 public void onTextChanged(CharSequence s, int start, int before, int count) {}
                 @Override
                 public void afterTextChanged(Editable s) {
+                    if (updatePropagationDisabled)
+                        return;
+
                     TemplateDetailsItem.AccountRow accRow = getItem();
                     Logger.debug(D_TEMPLATE_UI,
                             "Storing changed account name " + s + "; accRow=" + accRow);
                     accRow.setAccountName(String.valueOf(s));
+
+                    mModel.applyList(null);
                 }
             };
             b.templateDetailsAccountName.addTextChangedListener(accountNameWatcher);
@@ -494,10 +658,15 @@ class TemplateDetailsAdapter extends RecyclerView.Adapter<TemplateDetailsAdapter
                 public void onTextChanged(CharSequence s, int start, int before, int count) {}
                 @Override
                 public void afterTextChanged(Editable s) {
+                    if (updatePropagationDisabled)
+                        return;
+
                     TemplateDetailsItem.AccountRow accRow = getItem();
                     Logger.debug(D_TEMPLATE_UI,
                             "Storing changed account comment " + s + "; accRow=" + accRow);
                     accRow.setAccountComment(String.valueOf(s));
+
+                    mModel.applyList(null);
                 }
             };
             b.templateDetailsAccountComment.addTextChangedListener(accountCommentWatcher);
@@ -505,14 +674,15 @@ class TemplateDetailsAdapter extends RecyclerView.Adapter<TemplateDetailsAdapter
             b.templateDetailsAccountAmount.addTextChangedListener(new TextWatcher() {
                 @Override
                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
-
                 }
                 @Override
                 public void onTextChanged(CharSequence s, int start, int before, int count) {
-
                 }
                 @Override
                 public void afterTextChanged(Editable s) {
+                    if (updatePropagationDisabled)
+                        return;
+
                     TemplateDetailsItem.AccountRow accRow = getItem();
 
                     String str = String.valueOf(s);
@@ -533,6 +703,8 @@ class TemplateDetailsAdapter extends RecyclerView.Adapter<TemplateDetailsAdapter
                             b.templateDetailsAccountAmountLayout.setError("!");
                         }
                     }
+
+                    mModel.applyList(null);
                 }
             });
             b.templateDetailsAccountAmount.setOnFocusChangeListener((v, hasFocus) -> {
@@ -550,83 +722,111 @@ class TemplateDetailsAdapter extends RecyclerView.Adapter<TemplateDetailsAdapter
             });
 
             b.negateAmountSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
+                if (updatePropagationDisabled)
+                    return;
+
                 getItem().setNegateAmount(isChecked);
                 b.templateDetailsNegateAmountText.setText(
                         isChecked ? R.string.template_account_change_amount_sign
                                   : R.string.template_account_keep_amount_sign);
             });
-            final View.OnClickListener negLabelClickListener = (view) -> {
-                b.negateAmountSwitch.toggle();
-            };
+            final View.OnClickListener negLabelClickListener =
+                    (view) -> b.negateAmountSwitch.toggle();
             b.templateDetailsNegateAmountLabel.setOnClickListener(negLabelClickListener);
             b.templateDetailsNegateAmountText.setOnClickListener(negLabelClickListener);
+            manageAccountLabelDrag();
+        }
+        @SuppressLint("ClickableViewAccessibility")
+        public void manageAccountLabelDrag() {
+            b.patternAccountLabel.setOnTouchListener((v, event) -> {
+                if (event.getAction() == MotionEvent.ACTION_DOWN) {
+                    itemTouchHelper.startDrag(this);
+                }
+                return false;
+            });
         }
         @Override
         void bind(TemplateDetailsItem item) {
-            String groupNoText = b.getRoot()
-                                  .getResources()
-                                  .getString(R.string.template_item_match_group_source);
-
-            TemplateDetailsItem.AccountRow accRow = item.asAccountRowItem();
-            if (accRow.hasLiteralAccountName()) {
-                b.templateDetailsAccountNameLayout.setVisibility(View.VISIBLE);
-                b.templateDetailsAccountName.setText(accRow.getAccountName());
-                b.templateDetailsAccountNameSource.setText(
-                        R.string.template_details_source_literal);
-            }
-            else {
-                b.templateDetailsAccountNameLayout.setVisibility(View.GONE);
-                b.templateDetailsAccountNameSource.setText(
-                        String.format(Locale.US, groupNoText, accRow.getAccountNameMatchGroup(),
-                                getMatchGroupText(accRow.getAccountNameMatchGroup())));
-            }
+            disableUpdatePropagation();
+            try {
+                final Resources resources = b.getRoot()
+                                             .getResources();
+                String groupNoText = resources.getString(R.string.template_item_match_group_source);
+
+                Logger.debug("drag", String.format(Locale.US, "Binding account id %d, pos %d at %d",
+                        item.getId(), item.getPosition(), getAdapterPosition()));
+                TemplateDetailsItem.AccountRow accRow = item.asAccountRowItem();
+                b.patternAccountLabel.setText(String.format(Locale.US,
+                        resources.getString(R.string.template_details_account_row_label),
+                        accRow.getPosition()));
+                if (accRow.hasLiteralAccountName()) {
+                    b.templateDetailsAccountNameLayout.setVisibility(View.VISIBLE);
+                    b.templateDetailsAccountName.setText(accRow.getAccountName());
+                    b.templateDetailsAccountNameSource.setText(
+                            R.string.template_details_source_literal);
+                }
+                else {
+                    b.templateDetailsAccountNameLayout.setVisibility(View.GONE);
+                    b.templateDetailsAccountNameSource.setText(
+                            String.format(Locale.US, groupNoText, accRow.getAccountNameMatchGroup(),
+                                    getMatchGroupText(accRow.getAccountNameMatchGroup())));
+                }
 
-            if (accRow.hasLiteralAccountComment()) {
-                b.templateDetailsAccountCommentLayout.setVisibility(View.VISIBLE);
-                b.templateDetailsAccountComment.setText(accRow.getAccountComment());
-                b.templateDetailsAccountCommentSource.setText(
-                        R.string.template_details_source_literal);
-            }
-            else {
-                b.templateDetailsAccountCommentLayout.setVisibility(View.GONE);
-                b.templateDetailsAccountCommentSource.setText(
-                        String.format(Locale.US, groupNoText, accRow.getAccountCommentMatchGroup(),
-                                getMatchGroupText(accRow.getAccountCommentMatchGroup())));
-            }
+                if (accRow.hasLiteralAccountComment()) {
+                    b.templateDetailsAccountCommentLayout.setVisibility(View.VISIBLE);
+                    b.templateDetailsAccountComment.setText(accRow.getAccountComment());
+                    b.templateDetailsAccountCommentSource.setText(
+                            R.string.template_details_source_literal);
+                }
+                else {
+                    b.templateDetailsAccountCommentLayout.setVisibility(View.GONE);
+                    b.templateDetailsAccountCommentSource.setText(
+                            String.format(Locale.US, groupNoText,
+                                    accRow.getAccountCommentMatchGroup(),
+                                    getMatchGroupText(accRow.getAccountCommentMatchGroup())));
+                }
 
-            if (accRow.hasLiteralAmount()) {
-                b.templateDetailsAccountAmountSource.setText(
-                        R.string.template_details_source_literal);
-                b.templateDetailsAccountAmount.setVisibility(View.VISIBLE);
-                Float amt = accRow.getAmount();
-                b.templateDetailsAccountAmount.setText((amt == null) ? null : String.format(
-                        Data.locale.getValue(), "%,4.2f", (accRow.getAmount())));
-                b.negateAmountSwitch.setVisibility(View.GONE);
+                if (accRow.hasLiteralAmount()) {
+                    b.templateDetailsAccountAmountSource.setText(
+                            R.string.template_details_source_literal);
+                    b.templateDetailsAccountAmount.setVisibility(View.VISIBLE);
+                    Float amt = accRow.getAmount();
+                    b.templateDetailsAccountAmount.setText((amt == null) ? null : String.format(
+                            Data.locale.getValue(), "%,4.2f", (accRow.getAmount())));
+                    b.negateAmountSwitch.setVisibility(View.GONE);
+                    b.templateDetailsNegateAmountLabel.setVisibility(View.GONE);
+                    b.templateDetailsNegateAmountText.setVisibility(View.GONE);
+                }
+                else {
+                    b.templateDetailsAccountAmountSource.setText(
+                            String.format(Locale.US, groupNoText, accRow.getAmountMatchGroup(),
+                                    getMatchGroupText(accRow.getAmountMatchGroup())));
+                    b.templateDetailsAccountAmountLayout.setVisibility(View.GONE);
+                    b.negateAmountSwitch.setVisibility(View.VISIBLE);
+                    b.negateAmountSwitch.setChecked(accRow.isNegateAmount());
+                    b.templateDetailsNegateAmountText.setText(
+                            accRow.isNegateAmount() ? R.string.template_account_change_amount_sign
+                                                    : R.string.template_account_keep_amount_sign);
+                    b.templateDetailsNegateAmountLabel.setVisibility(View.VISIBLE);
+                    b.templateDetailsNegateAmountText.setVisibility(View.VISIBLE);
+                }
+
+                b.templateAccountNameSourceLabel.setOnClickListener(
+                        v -> selectAccountRowDetailSource(v, AccDetail.ACCOUNT));
+                b.templateDetailsAccountNameSource.setOnClickListener(
+                        v -> selectAccountRowDetailSource(v, AccDetail.ACCOUNT));
+                b.templateAccountCommentSourceLabel.setOnClickListener(
+                        v -> selectAccountRowDetailSource(v, AccDetail.COMMENT));
+                b.templateDetailsAccountCommentSource.setOnClickListener(
+                        v -> selectAccountRowDetailSource(v, AccDetail.COMMENT));
+                b.templateAccountAmountSourceLabel.setOnClickListener(
+                        v -> selectAccountRowDetailSource(v, AccDetail.AMOUNT));
+                b.templateDetailsAccountAmountSource.setOnClickListener(
+                        v -> selectAccountRowDetailSource(v, AccDetail.AMOUNT));
             }
-            else {
-                b.templateDetailsAccountAmountSource.setText(
-                        String.format(Locale.US, groupNoText, accRow.getAmountMatchGroup(),
-                                getMatchGroupText(accRow.getAmountMatchGroup())));
-                b.templateDetailsAccountAmountLayout.setVisibility(View.GONE);
-                b.negateAmountSwitch.setVisibility(View.VISIBLE);
-                b.negateAmountSwitch.setChecked(accRow.isNegateAmount());
-                b.templateDetailsNegateAmountText.setText(
-                        accRow.isNegateAmount() ? R.string.template_account_change_amount_sign
-                                                : R.string.template_account_keep_amount_sign);
+            finally {
+                enableUpdatePropagation();
             }
-
-            b.templateAccountNameSourceLabel.setOnClickListener(
-                    v -> selectAccountRowDetailSource(v, AccDetail.ACCOUNT));
-            b.templateDetailsAccountNameSource.setOnClickListener(
-                    v -> selectAccountRowDetailSource(v, AccDetail.ACCOUNT));
-            b.templateAccountCommentSourceLabel.setOnClickListener(
-                    v -> selectAccountRowDetailSource(v, AccDetail.COMMENT));
-            b.templateDetailsAccountCommentSource.setOnClickListener(
-                    v -> selectAccountRowDetailSource(v, AccDetail.COMMENT));
-            b.templateAccountAmountSourceLabel.setOnClickListener(
-                    v -> selectAccountRowDetailSource(v, AccDetail.AMOUNT));
-            b.templateDetailsAccountAmountSource.setOnClickListener(
-                    v -> selectAccountRowDetailSource(v, AccDetail.AMOUNT));
         }
         private @NotNull TemplateDetailsItem.AccountRow getItem() {
             return differ.getCurrentList()