]> git.ktnx.net Git - mobile-ledger.git/blobdiff - app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternDetailsAdapter.java
somewhat working pattern list/editor
[mobile-ledger.git] / app / src / main / java / net / ktnx / mobileledger / ui / patterns / PatternDetailsAdapter.java
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternDetailsAdapter.java b/app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternDetailsAdapter.java
new file mode 100644 (file)
index 0000000..5b6ab81
--- /dev/null
@@ -0,0 +1,562 @@
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.patterns;
+
+import android.annotation.SuppressLint;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.recyclerview.widget.AsyncListDiffer;
+import androidx.recyclerview.widget.DiffUtil;
+import androidx.recyclerview.widget.RecyclerView;
+
+import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.databinding.PatternDetailsAccountBinding;
+import net.ktnx.mobileledger.databinding.PatternDetailsHeaderBinding;
+import net.ktnx.mobileledger.db.PatternBase;
+import net.ktnx.mobileledger.model.Data;
+import net.ktnx.mobileledger.model.PatternDetailsItem;
+import net.ktnx.mobileledger.ui.PatternDetailSourceSelectorFragment;
+import net.ktnx.mobileledger.ui.QRScanAbleFragment;
+import net.ktnx.mobileledger.utils.Logger;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+class PatternDetailsAdapter extends RecyclerView.Adapter<PatternDetailsAdapter.ViewHolder> {
+    private static final String D_PATTERN_UI = "pattern-ui";
+    private final AsyncListDiffer<PatternDetailsItem> differ;
+    public PatternDetailsAdapter() {
+        super();
+        setHasStableIds(true);
+        differ = new AsyncListDiffer<>(this, new DiffUtil.ItemCallback<PatternDetailsItem>() {
+            @Override
+            public boolean areItemsTheSame(@NonNull PatternDetailsItem oldItem,
+                                           @NonNull PatternDetailsItem newItem) {
+                if (oldItem.getType() != newItem.getType())
+                    return false;
+                if (oldItem.getType() == PatternDetailsItem.Type.HEADER)
+                    return true;    // only one header item, ever
+                // the rest is comparing two account row items
+                return oldItem.asAccountRowItem()
+                              .getId() == newItem.asAccountRowItem()
+                                                 .getId();
+            }
+            @SuppressLint("DiffUtilEquals")
+            @Override
+            public boolean areContentsTheSame(@NonNull PatternDetailsItem oldItem,
+                                              @NonNull PatternDetailsItem newItem) {
+                if (oldItem.getType() == PatternDetailsItem.Type.HEADER) {
+                    PatternDetailsItem.Header oldHeader = oldItem.asHeaderItem();
+                    PatternDetailsItem.Header newHeader = newItem.asHeaderItem();
+
+                    return oldHeader.equalContents(newHeader);
+                }
+                else {
+                    PatternDetailsItem.AccountRow oldAcc = oldItem.asAccountRowItem();
+                    PatternDetailsItem.AccountRow newAcc = newItem.asAccountRowItem();
+
+                    return oldAcc.equalContents(newAcc);
+                }
+            }
+        });
+    }
+    @Override
+    public long getItemId(int position) {
+        if (position == 0)
+            return -1;
+        PatternDetailsItem.AccountRow accRow = differ.getCurrentList()
+                                                     .get(position)
+                                                     .asAccountRowItem();
+        return accRow.getId();
+    }
+    @Override
+    public int getItemViewType(int position) {
+
+        return differ.getCurrentList()
+                     .get(position)
+                     .getType()
+                     .toInt();
+    }
+    @NonNull
+    @Override
+    public PatternDetailsAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
+                                                               int viewType) {
+        final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+        switch (viewType) {
+            case PatternDetailsItem.TYPE.header:
+                return new Header(PatternDetailsHeaderBinding.inflate(inflater, parent, false));
+            case PatternDetailsItem.TYPE.accountItem:
+                return new AccountRow(
+                        PatternDetailsAccountBinding.inflate(inflater, parent, false));
+            default:
+                throw new IllegalStateException("Unsupported view type " + viewType);
+        }
+    }
+    @Override
+    public void onBindViewHolder(@NonNull PatternDetailsAdapter.ViewHolder holder, int position) {
+        PatternDetailsItem item = differ.getCurrentList()
+                                        .get(position);
+        holder.bind(item);
+    }
+    @Override
+    public int getItemCount() {
+        return differ.getCurrentList()
+                     .size();
+    }
+    public void setPatternItems(List<PatternBase> items) {
+        ArrayList<PatternDetailsItem> list = new ArrayList<>();
+        for (PatternBase p : items) {
+            PatternDetailsItem item = PatternDetailsItem.fromRoomObject(p);
+            list.add(item);
+        }
+        setItems(list);
+    }
+    public void setItems(List<PatternDetailsItem> items) {
+        differ.submitList(items);
+    }
+    public String getMatchGroupText(int groupNumber) {
+        PatternDetailsItem.Header header = getHeader();
+        Pattern p = header.getCompiledPattern();
+        if (p == null) return null;
+
+        Matcher m = p.matcher(header.getTestText());
+        if (m.matches() && m.groupCount() >= groupNumber)
+            return m.group(groupNumber);
+        else
+            return null;
+    }
+    protected PatternDetailsItem.Header getHeader() {
+        return differ.getCurrentList()
+                     .get(0)
+                     .asHeaderItem();
+    }
+
+    private enum HeaderDetail {DESCRIPTION, COMMENT, DATE_YEAR, DATE_MONTH, DATE_DAY}
+
+    private enum AccDetail {ACCOUNT, COMMENT, AMOUNT}
+
+    public abstract static class ViewHolder extends RecyclerView.ViewHolder {
+        protected int updateInProgress = 0;
+        ViewHolder(@NonNull View itemView) {
+            super(itemView);
+        }
+        protected void startUpdate() {
+            updateInProgress++;
+        }
+        protected void finishUpdate() {
+            if (updateInProgress <= 0)
+                throw new IllegalStateException(
+                        "Unexpected updateInProgress value " + updateInProgress);
+
+            updateInProgress--;
+        }
+        abstract void bind(PatternDetailsItem item);
+    }
+
+    public class Header extends ViewHolder {
+        private final PatternDetailsHeaderBinding b;
+        private final TextWatcher patternNameWatcher = 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) {
+                Object tag = b.patternDetailsItemHead.getTag();
+                if (tag != null) {
+                    final PatternDetailsItem.Header header =
+                            ((PatternDetailsItem) tag).asHeaderItem();
+                    Logger.debug(D_PATTERN_UI,
+                            "Storing changed pattern name " + s + "; header=" + header);
+                    header.setName(String.valueOf(s));
+                }
+            }
+        };
+        private final TextWatcher patternWatcher = 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) {
+                Object tag = b.patternDetailsItemHead.getTag();
+                if (tag != null) {
+                    final PatternDetailsItem.Header header =
+                            ((PatternDetailsItem) tag).asHeaderItem();
+                    Logger.debug(D_PATTERN_UI,
+                            "Storing changed pattern " + s + "; header=" + header);
+                    header.setPattern(String.valueOf(s));
+                }
+            }
+        };
+        private final TextWatcher testTextWatcher = 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) {
+                Object tag = b.patternDetailsItemHead.getTag();
+                if (tag != null) {
+                    final PatternDetailsItem.Header header =
+                            ((PatternDetailsItem) tag).asHeaderItem();
+                    Logger.debug(D_PATTERN_UI,
+                            "Storing changed test text " + s + "; header=" + header);
+                    header.setTestText(String.valueOf(s));
+                }
+            }
+        };
+        private final TextWatcher transactionDescriptionWatcher = 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) {
+                PatternDetailsItem.Header header = ((PatternDetailsItem) Objects.requireNonNull(
+                        b.patternDetailsItemHead.getTag())).asHeaderItem();
+                Logger.debug(D_PATTERN_UI,
+                        "Storing changed transaction description " + s + "; header=" + header);
+                header.setTransactionDescription(String.valueOf(s));
+            }
+        };
+        private final 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) {
+                PatternDetailsItem.Header header = ((PatternDetailsItem) Objects.requireNonNull(
+                        b.patternDetailsItemHead.getTag())).asHeaderItem();
+                Logger.debug(D_PATTERN_UI,
+                        "Storing changed transaction description " + s + "; header=" + header);
+                header.setTransactionComment(String.valueOf(s));
+            }
+        };
+        public Header(@NonNull PatternDetailsHeaderBinding binding) {
+            super(binding.getRoot());
+            b = binding;
+        }
+        Header(@NonNull View itemView) {
+            super(itemView);
+            throw new IllegalStateException("Should not be used");
+        }
+        private void selectHeaderDetailSource(View v, PatternDetailsItem.Header header,
+                                              HeaderDetail detail) {
+            Logger.debug(D_PATTERN_UI, "header is " + header);
+            PatternDetailSourceSelectorFragment sel =
+                    PatternDetailSourceSelectorFragment.newInstance(1, header.getPattern(),
+                            header.getTestText());
+            sel.setOnSourceSelectedListener((literal, group) -> {
+                if (literal) {
+                    switch (detail) {
+                        case DESCRIPTION:
+                            header.switchToLiteralTransactionDescription();
+                            break;
+                        case COMMENT:
+                            header.switchToLiteralTransactionComment();
+                            break;
+                        case DATE_YEAR:
+                            header.switchToLiteralDateYear();
+                            break;
+                        case DATE_MONTH:
+                            header.switchToLiteralDateMonth();
+                            break;
+                        case DATE_DAY:
+                            header.switchToLiteralDateDay();
+                            break;
+                        default:
+                            throw new IllegalStateException("Unexpected detail " + detail);
+                    }
+                }
+                else {
+                    switch (detail) {
+                        case DESCRIPTION:
+                            header.setTransactionDescriptionMatchGroup(group);
+                            break;
+                        case COMMENT:
+                            header.setTransactionCommentMatchGroup(group);
+                            break;
+                        case DATE_YEAR:
+                            header.setDateYearMatchGroup(group);
+                            break;
+                        case DATE_MONTH:
+                            header.setDateMonthMatchGroup(group);
+                            break;
+                        case DATE_DAY:
+                            header.setDateDayMatchGroup(group);
+                            break;
+                        default:
+                            throw new IllegalStateException("Unexpected detail " + detail);
+                    }
+                }
+
+                notifyItemChanged(getAdapterPosition());
+            });
+            final AppCompatActivity activity = (AppCompatActivity) v.getContext();
+            sel.show(activity.getSupportFragmentManager(), "pattern-details-source-selector");
+        }
+        @Override
+        void bind(PatternDetailsItem item) {
+            PatternDetailsItem.Header header = item.asHeaderItem();
+            startUpdate();
+            try {
+                Logger.debug(D_PATTERN_UI, "Binding to header " + header);
+                b.patternName.setText(header.getName());
+                b.pattern.setText(header.getPattern());
+                b.testText.setText(header.getTestText());
+
+                if (header.hasLiteralDateYear()) {
+                    b.patternDetailsYearSource.setText(R.string.pattern_details_source_literal);
+                    b.patternDetailsDateYear.setText(String.valueOf(header.getDateYear()));
+                    b.patternDetailsDateYearLayout.setVisibility(View.VISIBLE);
+                }
+                else {
+                    b.patternDetailsDateYearLayout.setVisibility(View.GONE);
+                    b.patternDetailsYearSource.setText(String.format(Locale.US, "Group %d (%s)",
+                            header.getDateYearMatchGroup(), getMatchGroupText(
+                                    header.getDateYearMatchGroup())));
+                }
+                b.patternDetailsYearSourceLabel.setOnClickListener(
+                        v -> selectHeaderDetailSource(v, header, HeaderDetail.DATE_YEAR));
+                b.patternDetailsYearSource.setOnClickListener(
+                        v -> selectHeaderDetailSource(v, header, HeaderDetail.DATE_YEAR));
+
+                if (header.hasLiteralDateMonth()) {
+                    b.patternDetailsMonthSource.setText(R.string.pattern_details_source_literal);
+                    b.patternDetailsDateMonth.setText(String.valueOf(header.getDateMonth()));
+                    b.patternDetailsDateMonthLayout.setVisibility(View.VISIBLE);
+                }
+                else {
+                    b.patternDetailsDateMonthLayout.setVisibility(View.GONE);
+                    b.patternDetailsMonthSource.setText(String.format(Locale.US, "Group %d (%s)",
+                            header.getDateMonthMatchGroup(), getMatchGroupText(
+                                    header.getDateMonthMatchGroup())));
+                }
+                b.patternDetailsMonthSourceLabel.setOnClickListener(
+                        v -> selectHeaderDetailSource(v, header, HeaderDetail.DATE_MONTH));
+                b.patternDetailsMonthSource.setOnClickListener(
+                        v -> selectHeaderDetailSource(v, header, HeaderDetail.DATE_MONTH));
+
+                if (header.hasLiteralDateDay()) {
+                    b.patternDetailsDaySource.setText(R.string.pattern_details_source_literal);
+                    b.patternDetailsDateDay.setText(String.valueOf(header.getDateDay()));
+                    b.patternDetailsDateDayLayout.setVisibility(View.VISIBLE);
+                }
+                else {
+                    b.patternDetailsDateDayLayout.setVisibility(View.GONE);
+                    b.patternDetailsDaySource.setText(String.format(Locale.US, "Group %d (%s)",
+                            header.getDateDayMatchGroup(), getMatchGroupText(
+                                    header.getDateDayMatchGroup())));
+                }
+                b.patternDetailsDaySourceLabel.setOnClickListener(
+                        v -> selectHeaderDetailSource(v, header, HeaderDetail.DATE_DAY));
+                b.patternDetailsDaySource.setOnClickListener(
+                        v -> selectHeaderDetailSource(v, header, HeaderDetail.DATE_DAY));
+
+                if (header.hasLiteralTransactionDescription()) {
+                    b.patternTransactionDescriptionSource.setText(
+                            R.string.pattern_details_source_literal);
+                    b.transactionDescription.setText(header.getTransactionDescription());
+                    b.transactionDescriptionLayout.setVisibility(View.VISIBLE);
+                }
+                else {
+                    b.transactionDescriptionLayout.setVisibility(View.GONE);
+                    b.patternTransactionDescriptionSource.setText(
+                            String.format(Locale.US, "Group %d (%s)",
+                                    header.getTransactionDescriptionMatchGroup(), getMatchGroupText(
+                                            header.getTransactionDescriptionMatchGroup())));
+
+                }
+                b.patternTransactionDescriptionSourceLabel.setOnClickListener(
+                        v -> selectHeaderDetailSource(v, header, HeaderDetail.DESCRIPTION));
+                b.patternTransactionDescriptionSource.setOnClickListener(
+                        v -> selectHeaderDetailSource(v, header, HeaderDetail.DESCRIPTION));
+
+                if (header.hasLiteralTransactionComment()) {
+                    b.patternTransactionCommentSource.setText(
+                            R.string.pattern_details_source_literal);
+                    b.transactionComment.setText(header.getTransactionComment());
+                    b.transactionCommentLayout.setVisibility(View.VISIBLE);
+                }
+                else {
+                    b.transactionCommentLayout.setVisibility(View.GONE);
+                    b.patternTransactionCommentSource.setText(
+                            String.format(Locale.US, "Group %d (%s)",
+                                    header.getTransactionCommentMatchGroup(),
+                                    getMatchGroupText(header.getTransactionCommentMatchGroup())));
+
+                }
+                b.patternTransactionCommentSourceLabel.setOnClickListener(
+                        v -> selectHeaderDetailSource(v, header, HeaderDetail.COMMENT));
+                b.patternTransactionCommentSource.setOnClickListener(
+                        v -> selectHeaderDetailSource(v, header, HeaderDetail.COMMENT));
+
+                b.patternDetailsHeadScanQrButton.setOnClickListener(this::scanTestQR);
+
+                final Object prevTag = b.patternDetailsItemHead.getTag();
+                if (!(prevTag instanceof PatternDetailsItem)) {
+                    Logger.debug(D_PATTERN_UI, "Hooked text change listeners");
+
+                    b.patternName.addTextChangedListener(patternNameWatcher);
+                    b.pattern.addTextChangedListener(patternWatcher);
+                    b.testText.addTextChangedListener(testTextWatcher);
+                    b.transactionDescription.addTextChangedListener(transactionDescriptionWatcher);
+                    b.transactionComment.addTextChangedListener(transactionCommentWatcher);
+                }
+
+                b.patternDetailsItemHead.setTag(item);
+            }
+            finally {
+                finishUpdate();
+            }
+        }
+        private void scanTestQR(View view) {
+            QRScanAbleFragment.triggerQRScan();
+        }
+    }
+
+    public class AccountRow extends ViewHolder {
+        private final PatternDetailsAccountBinding b;
+        public AccountRow(@NonNull PatternDetailsAccountBinding binding) {
+            super(binding.getRoot());
+            b = binding;
+        }
+        AccountRow(@NonNull View itemView) {
+            super(itemView);
+            throw new IllegalStateException("Should not be used");
+        }
+        @Override
+        void bind(PatternDetailsItem item) {
+            PatternDetailsItem.AccountRow accRow = item.asAccountRowItem();
+            if (accRow.hasLiteralAccountName()) {
+                b.patternDetailsAccountNameLayout.setVisibility(View.VISIBLE);
+                b.patternDetailsAccountName.setText(accRow.getAccountName());
+                b.patternDetailsAccountNameSource.setText(R.string.pattern_details_source_literal);
+            }
+            else {
+                b.patternDetailsAccountNameLayout.setVisibility(View.GONE);
+                b.patternDetailsAccountNameSource.setText(
+                        String.format(Locale.US, "Group %d (%s)", accRow.getAccountNameMatchGroup(),
+                                getMatchGroupText(accRow.getAccountNameMatchGroup())));
+            }
+
+            if (accRow.hasLiteralAccountComment()) {
+                b.patternDetailsAccountCommentLayout.setVisibility(View.VISIBLE);
+                b.patternDetailsAccountComment.setText(accRow.getAccountComment());
+                b.patternDetailsAccountCommentSource.setText(
+                        R.string.pattern_details_source_literal);
+            }
+            else {
+                b.patternDetailsAccountCommentLayout.setVisibility(View.GONE);
+                b.patternDetailsAccountCommentSource.setText(
+                        String.format(Locale.US, "Group %d (%s)",
+                                accRow.getAccountCommentMatchGroup(),
+                                getMatchGroupText(accRow.getAccountCommentMatchGroup())));
+            }
+
+            if (accRow.hasLiteralAmount()) {
+                b.patternDetailsAccountAmountSource.setText(
+                        R.string.pattern_details_source_literal);
+                b.patternDetailsAccountAmount.setVisibility(View.VISIBLE);
+                b.patternDetailsAccountAmount.setText(Data.formatNumber(accRow.getAmount()));
+            }
+            else {
+                b.patternDetailsAccountAmountSource.setText(
+                        String.format(Locale.US, "Group %d (%s)", accRow.getAmountMatchGroup(),
+                                getMatchGroupText(accRow.getAmountMatchGroup())));
+                b.patternDetailsAccountAmountLayout.setVisibility(View.GONE);
+            }
+
+            b.patternAccountNameSourceLabel.setOnClickListener(
+                    v -> selectAccountRowDetailSource(v, accRow, AccDetail.ACCOUNT));
+            b.patternDetailsAccountNameSource.setOnClickListener(
+                    v -> selectAccountRowDetailSource(v, accRow, AccDetail.ACCOUNT));
+            b.patternAccountCommentSourceLabel.setOnClickListener(
+                    v -> selectAccountRowDetailSource(v, accRow, AccDetail.COMMENT));
+            b.patternDetailsAccountCommentSource.setOnClickListener(
+                    v -> selectAccountRowDetailSource(v, accRow, AccDetail.COMMENT));
+            b.patternAccountAmountSourceLabel.setOnClickListener(
+                    v -> selectAccountRowDetailSource(v, accRow, AccDetail.AMOUNT));
+            b.patternDetailsAccountAmountSource.setOnClickListener(
+                    v -> selectAccountRowDetailSource(v, accRow, AccDetail.AMOUNT));
+        }
+        private void selectAccountRowDetailSource(View v, PatternDetailsItem.AccountRow accRow,
+                                                  AccDetail detail) {
+            final PatternDetailsItem.Header header = getHeader();
+            Logger.debug(D_PATTERN_UI, "header is " + header);
+            PatternDetailSourceSelectorFragment sel =
+                    PatternDetailSourceSelectorFragment.newInstance(1, header.getPattern(),
+                            header.getTestText());
+            sel.setOnSourceSelectedListener((literal, group) -> {
+                if (literal) {
+                    switch (detail) {
+                        case ACCOUNT:
+                            accRow.switchToLiteralAccountName();
+                            break;
+                        case COMMENT:
+                            accRow.switchToLiteralAccountComment();
+                            break;
+                        case AMOUNT:
+                            accRow.switchToLiteralAmount();
+                            break;
+                        default:
+                            throw new IllegalStateException("Unexpected detail " + detail);
+                    }
+                }
+                else {
+                    switch (detail) {
+                        case ACCOUNT:
+                            accRow.setAccountNameMatchGroup(group);
+                            break;
+                        case COMMENT:
+                            accRow.setAccountCommentMatchGroup(group);
+                            break;
+                        case AMOUNT:
+                            accRow.setAmountMatchGroup(group);
+                            break;
+                        default:
+                            throw new IllegalStateException("Unexpected detail " + detail);
+                    }
+                }
+
+                notifyItemChanged(getAdapterPosition());
+            });
+            final AppCompatActivity activity = (AppCompatActivity) v.getContext();
+            sel.show(activity.getSupportFragmentManager(), "pattern-details-source-selector");
+        }
+    }
+}