]> git.ktnx.net Git - mobile-ledger.git/commitdiff
somewhat working pattern list/editor
authorDamyan Ivanov <dam+mobileledger@ktnx.net>
Fri, 29 Jan 2021 05:26:14 +0000 (05:26 +0000)
committerDamyan Ivanov <dam+mobileledger@ktnx.net>
Fri, 29 Jan 2021 11:27:04 +0000 (11:27 +0000)
24 files changed:
app/src/main/java/net/ktnx/mobileledger/model/PatternDetailSource.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/model/PatternDetailsItem.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/OnSourceSelectedListener.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/PatternDetailSourceSelectorFragment.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/PatternDetailSourceSelectorModel.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/PatternDetailSourceSelectorRecyclerViewAdapter.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternDetailsAdapter.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternDetailsFragment.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternDetailsViewModel.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternListFragment.java
app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternViewHolder.java
app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternsActivity.java
app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailFragment.java
app/src/main/res/layout/activity_patterns.xml
app/src/main/res/layout/activity_profile_detail.xml
app/src/main/res/layout/fragment_item_list.xml [new file with mode: 0644]
app/src/main/res/layout/fragment_pattern_detail_source_selector.xml [new file with mode: 0644]
app/src/main/res/layout/fragment_pattern_detail_source_selector_list.xml [new file with mode: 0644]
app/src/main/res/layout/fragment_pattern_list.xml
app/src/main/res/layout/pattern_details_account.xml [new file with mode: 0644]
app/src/main/res/layout/pattern_details_fragment.xml [new file with mode: 0644]
app/src/main/res/layout/pattern_details_header.xml [new file with mode: 0644]
app/src/main/res/navigation/pattern_list_navigation.xml
app/src/main/res/values/strings.xml

diff --git a/app/src/main/java/net/ktnx/mobileledger/model/PatternDetailSource.java b/app/src/main/java/net/ktnx/mobileledger/model/PatternDetailSource.java
new file mode 100644 (file)
index 0000000..5b8f378
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * 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.model;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.DiffUtil;
+
+import java.io.Serializable;
+
+public class PatternDetailSource implements Serializable {
+    public static final DiffUtil.ItemCallback<PatternDetailSource> DIFF_CALLBACK =
+            new DiffUtil.ItemCallback<PatternDetailSource>() {
+                @Override
+                public boolean areItemsTheSame(@NonNull PatternDetailSource oldItem,
+                                               @NonNull PatternDetailSource newItem) {
+                    return oldItem.groupNumber == newItem.groupNumber;
+                }
+                @Override
+                public boolean areContentsTheSame(@NonNull PatternDetailSource oldItem,
+                                                  @NonNull PatternDetailSource newItem) {
+                    return oldItem.matchedText.equals(newItem.matchedText);
+                }
+            };
+
+    private short groupNumber;
+    private String matchedText;
+    public PatternDetailSource() {
+    }
+    public PatternDetailSource(short groupNumber, String matchedText) {
+        this.groupNumber = groupNumber;
+        this.matchedText = matchedText;
+    }
+    public short getGroupNumber() {
+        return groupNumber;
+    }
+    public void setGroupNumber(short groupNumber) {
+        this.groupNumber = groupNumber;
+    }
+    public String getMatchedText() {
+        return matchedText;
+    }
+    public void setMatchedText(String matchedText) {
+        this.matchedText = matchedText;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/model/PatternDetailsItem.java b/app/src/main/java/net/ktnx/mobileledger/model/PatternDetailsItem.java
new file mode 100644 (file)
index 0000000..82b1bb4
--- /dev/null
@@ -0,0 +1,557 @@
+/*
+ * 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.model;
+
+import android.content.res.Resources;
+
+import androidx.annotation.NonNull;
+
+import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.db.PatternAccount;
+import net.ktnx.mobileledger.db.PatternBase;
+import net.ktnx.mobileledger.db.PatternHeader;
+import net.ktnx.mobileledger.utils.Misc;
+
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+abstract public class PatternDetailsItem {
+    private final Type type;
+    protected long id;
+    protected long position;
+
+    protected PatternDetailsItem(Type type, long id, long position) {
+        this.type = type;
+        this.id = (id <= 0) ? -position - 2 : id;
+        this.position = position;
+    }
+    @Contract(" -> new")
+    public static @NotNull PatternDetailsItem.Header createHeader() {
+        return new Header();
+    }
+    public static @NotNull PatternDetailsItem.Header createHeader(Header origin) {
+        return new Header(origin);
+    }
+    @Contract("_ -> new")
+    public static @NotNull PatternDetailsItem.AccountRow createAccountRow(long position) {
+        return new AccountRow(-1, position);
+    }
+    public static PatternDetailsItem fromRoomObject(PatternBase p) {
+        if (p instanceof PatternHeader) {
+            PatternHeader ph = (PatternHeader) p;
+            Header header = createHeader();
+            header.setName(ph.getName());
+            header.setPattern(ph.getRegularExpression());
+            header.setTestText(null);
+            header.setTransactionDescription(ph.getTransactionDescription());
+            header.setTransactionComment(ph.getTransactionComment());
+            header.setDateDayMatchGroup(ph.getDateDayMatchGroup());
+            header.setDateMonthMatchGroup(ph.getDateMonthMatchGroup());
+            header.setDateYearMatchGroup(ph.getDateYearMatchGroup());
+
+            return header;
+        }
+        else if (p instanceof PatternAccount) {
+            PatternAccount pa = (PatternAccount) p;
+            AccountRow acc = createAccountRow(pa.getPosition());
+
+            if (Misc.emptyIsNull(pa.getAccountName()) != null)
+                acc.setAccountName(pa.getAccountName());
+            else
+                acc.setAccountNameMatchGroup(pa.getAccountNameMatchGroup());
+
+            if (Misc.emptyIsNull(pa.getAccountComment()) == null)
+                acc.setAccountCommentMatchGroup(pa.getAccountCommentMatchGroup());
+            else
+                acc.setAccountComment(pa.getAccountComment());
+
+            if (pa.getCurrency() == null) {
+                acc.setCurrencyMatchGroup(pa.getCurrencyMatchGroup());
+            }
+            else {
+                acc.setCurrency(Currency.loadById(pa.getCurrency()));
+            }
+
+            if (pa.getAmount() == null)
+                acc.setAmountMatchGroup(pa.getAmountMatchGroup());
+            else
+                acc.setAmount(pa.getAmount());
+
+            return acc;
+        }
+        else {
+            throw new IllegalStateException("Unexpected item class " + p.getClass());
+        }
+    }
+    public Header asHeaderItem() {
+        ensureType(Type.HEADER);
+        return (Header) this;
+    }
+    public AccountRow asAccountRowItem() {
+        ensureType(Type.ACCOUNT_ITEM);
+        return (AccountRow) this;
+    }
+    private void ensureType(Type type) {
+        if (this.type != type)
+            throw new IllegalStateException(
+                    String.format("Type is %s, but %s is required", this.type.toString(),
+                            type.toString()));
+    }
+    void ensureTrue(boolean flag) {
+        if (!flag)
+            throw new IllegalStateException(
+                    "Literal value requested, but it is matched via a pattern group");
+    }
+    void ensureFalse(boolean flag) {
+        if (flag)
+            throw new IllegalStateException("Matching group requested, but the value is a literal");
+    }
+    public long getId() {
+        return id;
+    }
+    public void setId(int id) {
+        this.id = id;
+    }
+    public long getPosition() {
+        return position;
+    }
+    public void setPosition(int position) {
+        this.position = position;
+    }
+    abstract public String getProblem(@NonNull Resources r, int patternGroupCount);
+    public Type getType() {
+        return type;
+    }
+    public enum Type {
+        HEADER(TYPE.header), ACCOUNT_ITEM(TYPE.accountItem);
+        final int index;
+        Type(int i) {
+            index = i;
+        }
+        public int toInt() {
+            return index;
+        }
+    }
+
+    static class PossiblyMatchedValue<T> {
+        private boolean literalValue;
+        private T value;
+        private int matchGroup;
+        public PossiblyMatchedValue() {
+            literalValue = true;
+            value = null;
+        }
+        public PossiblyMatchedValue(@NonNull PossiblyMatchedValue<T> origin) {
+            literalValue = origin.literalValue;
+            value = origin.value;
+            matchGroup = origin.matchGroup;
+        }
+        @NonNull
+        public static PossiblyMatchedValue<Integer> withLiteralInt(int initialValue) {
+            PossiblyMatchedValue<Integer> result = new PossiblyMatchedValue<>();
+            result.setValue(initialValue);
+            return result;
+        }
+        @NonNull
+        public static PossiblyMatchedValue<Float> withLiteralFloat(float initialValue) {
+            PossiblyMatchedValue<Float> result = new PossiblyMatchedValue<>();
+            result.setValue(initialValue);
+            return result;
+        }
+        public static PossiblyMatchedValue<Short> withLiteralShort(short initialValue) {
+            PossiblyMatchedValue<Short> result = new PossiblyMatchedValue<>();
+            result.setValue(initialValue);
+            return result;
+        }
+        @NonNull
+        public static PossiblyMatchedValue<String> withLiteralString(String initialValue) {
+            PossiblyMatchedValue<String> result = new PossiblyMatchedValue<>();
+            result.setValue(initialValue);
+            return result;
+        }
+        public T getValue() {
+            if (!literalValue)
+                throw new IllegalStateException("Value is not literal");
+            return value;
+        }
+        public void setValue(T newValue) {
+            value = newValue;
+            literalValue = true;
+        }
+        public boolean hasLiteralValue() {
+            return literalValue;
+        }
+        public int getMatchGroup() {
+            if (literalValue)
+                throw new IllegalStateException("Value is literal");
+            return matchGroup;
+        }
+        public void setMatchGroup(int group) {
+            this.matchGroup = group;
+            literalValue = false;
+        }
+        public boolean equals(PossiblyMatchedValue<T> other) {
+            if (!other.literalValue == literalValue)
+                return false;
+            if (literalValue)
+                return value.equals(other.value);
+            else
+                return matchGroup == other.matchGroup;
+        }
+        public void switchToLiteral() {
+            literalValue = true;
+        }
+    }
+
+    public static class TYPE {
+        public static final int header = 0;
+        public static final int accountItem = 1;
+    }
+
+    public static class AccountRow extends PatternDetailsItem {
+        private final PossiblyMatchedValue<String> accountName =
+                PossiblyMatchedValue.withLiteralString("");
+        private final PossiblyMatchedValue<String> accountComment =
+                PossiblyMatchedValue.withLiteralString("");
+        private final PossiblyMatchedValue<Float> amount =
+                PossiblyMatchedValue.withLiteralFloat(0f);
+        private final PossiblyMatchedValue<Currency> currency = new PossiblyMatchedValue<>();
+        private AccountRow(long id, long position) {
+            super(Type.ACCOUNT_ITEM, id, position);
+        }
+        public int getAccountCommentMatchGroup() {
+            return accountComment.getMatchGroup();
+        }
+        public void setAccountCommentMatchGroup(int group) {
+            accountComment.setMatchGroup(group);
+        }
+        public String getAccountComment() {
+            return accountComment.getValue();
+        }
+        public void setAccountComment(String comment) {
+            this.accountComment.setValue(comment);
+        }
+        public int getCurrencyMatchGroup() {
+            return currency.getMatchGroup();
+        }
+        public void setCurrencyMatchGroup(int group) {
+            currency.setMatchGroup(group);
+        }
+        public Currency getCurrency() {
+            return currency.getValue();
+        }
+        public void setCurrency(Currency currency) {
+            this.currency.setValue(currency);
+        }
+        public int getAccountNameMatchGroup() {
+            return accountName.getMatchGroup();
+        }
+        public void setAccountNameMatchGroup(int group) {
+            accountName.setMatchGroup(group);
+        }
+        public String getAccountName() {
+            return accountName.getValue();
+        }
+        public void setAccountName(String accountName) {
+            this.accountName.setValue(accountName);
+        }
+        public boolean hasLiteralAccountName() { return accountName.hasLiteralValue(); }
+        public boolean hasLiteralAmount() {
+            return amount.hasLiteralValue();
+        }
+        public int getAmountMatchGroup() {
+            return amount.getMatchGroup();
+        }
+        public void setAmountMatchGroup(int group) {
+            amount.setMatchGroup(group);
+        }
+        public float getAmount() {
+            return amount.getValue();
+        }
+        public void setAmount(float amount) {
+            this.amount.setValue(amount);
+        }
+        public String getProblem(@NonNull Resources r, int patternGroupCount) {
+            if (Misc.emptyIsNull(accountName.getValue()) == null)
+                return r.getString(R.string.account_name_is_empty);
+            if (!amount.hasLiteralValue() &&
+                (amount.getMatchGroup() < 1 || amount.getMatchGroup() > patternGroupCount))
+                return r.getString(R.string.invalid_matching_group_number);
+
+            return null;
+        }
+        public boolean hasLiteralAccountComment() {
+            return accountComment.hasLiteralValue();
+        }
+        public boolean equalContents(AccountRow o) {
+            return amount.equals(o.amount) && accountName.equals(o.accountName) &&
+                   accountComment.equals(o.accountComment);
+        }
+        public void switchToLiteralAmount() {
+            amount.switchToLiteral();
+        }
+        public void switchToLiteralAccountName() {
+            accountName.switchToLiteral();
+        }
+        public void switchToLiteralAccountComment() {
+            accountComment.switchToLiteral();
+        }
+        public PatternAccount toDBO(@NonNull Long patternId) {
+            PatternAccount result = new PatternAccount((id <= 0L) ? null : id, patternId, position);
+
+            if (accountName.hasLiteralValue())
+                result.setAccountName(accountName.getValue());
+            else
+                result.setAccountNameMatchGroup(accountName.getMatchGroup());
+
+            if (accountComment.hasLiteralValue())
+                result.setAccountComment(accountComment.getValue());
+            else
+                result.setAccountCommentMatchGroup(accountComment.getMatchGroup());
+
+            if (amount.hasLiteralValue())
+                result.setAmount(amount.getValue());
+            else
+                result.setAmountMatchGroup(amount.getMatchGroup());
+
+            return result;
+        }
+    }
+
+    public static class Header extends PatternDetailsItem {
+        private String pattern = "";
+        private String testText = "";
+        private Pattern compiledPattern;
+        private String patternError;
+        private String name = "";
+        private PossiblyMatchedValue<String> transactionDescription =
+                PossiblyMatchedValue.withLiteralString("");
+        private PossiblyMatchedValue<String> transactionComment =
+                PossiblyMatchedValue.withLiteralString("");
+        private PossiblyMatchedValue<Short> dateYear =
+                PossiblyMatchedValue.withLiteralShort((short) 0);
+        private PossiblyMatchedValue<Short> dateMonth =
+                PossiblyMatchedValue.withLiteralShort((short) 0);
+        private PossiblyMatchedValue<Short> dateDay =
+                PossiblyMatchedValue.withLiteralShort((short) 0);
+        private Header() {
+            super(Type.HEADER, -1, -1);
+        }
+        public Header(Header origin) {
+            this();
+            name = origin.name;
+            testText = origin.testText;
+            setPattern(origin.pattern);
+
+            transactionDescription = new PossiblyMatchedValue<>(origin.transactionDescription);
+            transactionComment = new PossiblyMatchedValue<>(origin.transactionComment);
+
+            dateYear = new PossiblyMatchedValue<>(origin.dateYear);
+            dateMonth = new PossiblyMatchedValue<>(origin.dateMonth);
+            dateDay = new PossiblyMatchedValue<>(origin.dateDay);
+        }
+        public String getName() {
+            return name;
+        }
+        public void setName(String name) {
+            this.name = name;
+        }
+        public String getPattern() {
+            return pattern;
+        }
+        public void setPattern(String pattern) {
+            this.pattern = pattern;
+            if (pattern != null) {
+                try {
+                    this.compiledPattern = Pattern.compile(pattern);
+                    this.patternError = null;
+                }
+                catch (PatternSyntaxException e) {
+                    this.compiledPattern = null;
+                    this.patternError = e.getMessage();
+                }
+            }
+            else {
+                patternError = "Missing pattern";
+            }
+        }
+        @NonNull
+        @Override
+        public String toString() {
+            return super.toString() +
+                   String.format(" name[%s] pat[%s] test[%s]", name, pattern, testText);
+        }
+        public String getTestText() {
+            return testText;
+        }
+        public void setTestText(String testText) {
+            this.testText = testText;
+        }
+        public String getTransactionDescription() {
+            return transactionDescription.getValue();
+        }
+        public void setTransactionDescription(String transactionDescription) {
+            this.transactionDescription.setValue(transactionDescription);
+        }
+        public String getTransactionComment() {
+            return transactionComment.getValue();
+        }
+        public void setTransactionComment(String transactionComment) {
+            this.transactionComment.setValue(transactionComment);
+        }
+        public short getDateYear() {
+            return dateYear.getValue();
+        }
+        public void setDateYear(short dateYear) {
+            this.dateYear.setValue(dateYear);
+        }
+        public short getDateMonth() {
+            return dateMonth.getValue();
+        }
+        public void setDateMonth(short dateMonth) {
+            this.dateMonth.setValue(dateMonth);
+        }
+        public short getDateDay() {
+            return dateDay.getValue();
+        }
+        public void setDateDay(short dateDay) {
+            this.dateDay.setValue(dateDay);
+        }
+        public int getDateYearMatchGroup() {
+            return dateYear.getMatchGroup();
+        }
+        public void setDateYearMatchGroup(int dateYearMatchGroup) {
+            this.dateYear.setMatchGroup(dateYearMatchGroup);
+        }
+        public int getDateMonthMatchGroup() {
+            return dateMonth.getMatchGroup();
+        }
+        public void setDateMonthMatchGroup(int dateMonthMatchGroup) {
+            this.dateMonth.setMatchGroup(dateMonthMatchGroup);
+        }
+        public int getDateDayMatchGroup() {
+            return dateDay.getMatchGroup();
+        }
+        public void setDateDayMatchGroup(int dateDayMatchGroup) {
+            this.dateDay.setMatchGroup(dateDayMatchGroup);
+        }
+        public boolean hasLiteralDateYear() {
+            return dateYear.hasLiteralValue();
+        }
+        public boolean hasLiteralDateMonth() {
+            return dateMonth.hasLiteralValue();
+        }
+        public boolean hasLiteralDateDay() {
+            return dateDay.hasLiteralValue();
+        }
+        public boolean hasLiteralTransactionDescription() { return transactionDescription.hasLiteralValue(); }
+        public boolean hasLiteralTransactionComment() { return transactionComment.hasLiteralValue(); }
+        public String getProblem(@NonNull Resources r, int patternGroupCount) {
+            if (patternError != null)
+                return r.getString(R.string.pattern_has_errors) + ": " + patternError;
+            if (Misc.emptyIsNull(pattern) == null)
+                return r.getString(R.string.pattern_is_empty);
+
+            if (!dateYear.hasLiteralValue() && compiledPattern != null &&
+                (dateDay.getMatchGroup() < 1 || dateDay.getMatchGroup() > patternGroupCount))
+                return r.getString(R.string.invalid_matching_group_number);
+
+            if (!dateMonth.hasLiteralValue() && compiledPattern != null &&
+                (dateMonth.getMatchGroup() < 1 || dateMonth.getMatchGroup() > patternGroupCount))
+                return r.getString(R.string.invalid_matching_group_number);
+
+            if (!dateDay.hasLiteralValue() && compiledPattern != null &&
+                (dateDay.getMatchGroup() < 1 || dateDay.getMatchGroup() > patternGroupCount))
+                return r.getString(R.string.invalid_matching_group_number);
+
+            return null;
+        }
+
+        public boolean equalContents(Header o) {
+            if (!dateDay.equals(o.dateDay))
+                return false;
+            if (!dateMonth.equals(o.dateMonth))
+                return false;
+            if (!dateYear.equals(o.dateYear))
+                return false;
+            if (!transactionDescription.equals(o.transactionDescription))
+                return false;
+            if (!transactionComment.equals(o.transactionComment))
+                return true;
+
+            return Misc.equalStrings(name, o.name) && Misc.equalStrings(pattern, o.pattern) &&
+                   Misc.equalStrings(testText, o.testText);
+        }
+        public String getMatchGroupText(int group) {
+            if (compiledPattern != null && testText != null) {
+                Matcher m = compiledPattern.matcher(testText);
+                if (m.matches())
+                    return m.group(group);
+            }
+
+            return "ø";
+        }
+        public Pattern getCompiledPattern() {
+            return compiledPattern;
+        }
+        public void switchToLiteralTransactionDescription() {
+            transactionDescription.switchToLiteral();
+        }
+        public void switchToLiteralTransactionComment() {
+            transactionComment.switchToLiteral();
+        }
+        public int getTransactionDescriptionMatchGroup() {
+            return transactionDescription.getMatchGroup();
+        }
+        public void setTransactionDescriptionMatchGroup(short group) {
+            transactionDescription.setMatchGroup(group);
+        }
+        public int getTransactionCommentMatchGroup() {
+            return transactionComment.getMatchGroup();
+        }
+        public void setTransactionCommentMatchGroup(short group) {
+            transactionComment.setMatchGroup(group);
+        }
+        public void switchToLiteralDateYear() {
+            dateYear.switchToLiteral();
+        }
+        public void switchToLiteralDateMonth() {
+            dateMonth.switchToLiteral();
+        }
+        public void switchToLiteralDateDay() { dateDay.switchToLiteral(); }
+        public PatternHeader toDBO() {
+            PatternHeader result =
+                    new PatternHeader((id <= 0) ? null : id, name, position, pattern);
+            if (transactionDescription.hasLiteralValue())
+                result.setTransactionDescription(transactionDescription.getValue());
+            else
+                result.setTransactionDescriptionMatchGroup(transactionDescription.getMatchGroup());
+
+            if (transactionComment.hasLiteralValue())
+                result.setTransactionComment(transactionComment.getValue());
+            else
+                result.setTransactionCommentMatchGroup(transactionComment.getMatchGroup());
+
+            return result;
+        }
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/OnSourceSelectedListener.java b/app/src/main/java/net/ktnx/mobileledger/ui/OnSourceSelectedListener.java
new file mode 100644 (file)
index 0000000..a294adb
--- /dev/null
@@ -0,0 +1,22 @@
+/*
+ * 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;
+
+public interface OnSourceSelectedListener {
+    void onSourceSelected(boolean literal, short group);
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/PatternDetailSourceSelectorFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/PatternDetailSourceSelectorFragment.java
new file mode 100644 (file)
index 0000000..7a8a954
--- /dev/null
@@ -0,0 +1,188 @@
+/*
+ * Copyright © 2019 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;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.appcompat.app.AppCompatDialogFragment;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.databinding.FragmentPatternDetailSourceSelectorListBinding;
+import net.ktnx.mobileledger.model.PatternDetailSource;
+import net.ktnx.mobileledger.utils.Logger;
+import net.ktnx.mobileledger.utils.Misc;
+
+import java.util.ArrayList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A fragment representing a list of Items.
+ * <p/>
+ * Activities containing this fragment MUST implement the {@link OnSourceSelectedListener}
+ * interface.
+ */
+public class PatternDetailSourceSelectorFragment extends AppCompatDialogFragment
+        implements OnSourceSelectedListener {
+
+    public static final int DEFAULT_COLUMN_COUNT = 1;
+    public static final String ARG_COLUMN_COUNT = "column-count";
+    public static final String ARG_PATTERN = "pattern";
+    public static final String ARG_TEST_TEXT = "test-text";
+    private int mColumnCount = DEFAULT_COLUMN_COUNT;
+    private ArrayList<PatternDetailSource> mSources;
+    private PatternDetailSourceSelectorModel model;
+    private OnSourceSelectedListener onSourceSelectedListener;
+    private @StringRes
+    int mPatternProblem;
+
+    /**
+     * Mandatory empty constructor for the fragment manager to instantiate the
+     * fragment (e.g. upon screen orientation changes).
+     */
+    public PatternDetailSourceSelectorFragment() {
+    }
+    @SuppressWarnings("unused")
+    public static PatternDetailSourceSelectorFragment newInstance() {
+        return newInstance(DEFAULT_COLUMN_COUNT, null, null);
+    }
+    public static PatternDetailSourceSelectorFragment newInstance(int columnCount,
+                                                                  @Nullable String pattern,
+                                                                  @Nullable String testText) {
+        PatternDetailSourceSelectorFragment fragment = new PatternDetailSourceSelectorFragment();
+        Bundle args = new Bundle();
+        args.putInt(ARG_COLUMN_COUNT, columnCount);
+        if (pattern != null)
+            args.putString(ARG_PATTERN, pattern);
+        if (testText != null)
+            args.putString(ARG_TEST_TEXT, testText);
+        fragment.setArguments(args);
+        return fragment;
+    }
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        if (getArguments() != null) {
+            mColumnCount = getArguments().getInt(ARG_COLUMN_COUNT, DEFAULT_COLUMN_COUNT);
+            final String patternText = getArguments().getString(ARG_PATTERN);
+            final String testText = getArguments().getString(ARG_TEST_TEXT);
+            if (Misc.emptyIsNull(patternText) == null) {
+                mPatternProblem = R.string.missing_pattern_error;
+            }
+            else {
+                if (Misc.emptyIsNull(testText) == null) {
+                    mPatternProblem = R.string.missing_test_text;
+                }
+                else {
+                    Pattern pattern = Pattern.compile(patternText);
+                    Matcher matcher = pattern.matcher(testText);
+                    Logger.debug("patterns",
+                            String.format("Trying to match pattern '%s' against text '%s'",
+                                    patternText, testText));
+                    if (matcher.matches()) {
+                        if (matcher.groupCount() >= 0) {
+                            ArrayList<PatternDetailSource> list = new ArrayList<>();
+                            for (short g = 1; g <= matcher.groupCount(); g++) {
+                                list.add(new PatternDetailSource(g, matcher.group(g)));
+                            }
+                            mSources = list;
+                        }
+                        else {
+                            mPatternProblem = R.string.pattern_without_groups;
+                        }
+                    }
+                    else {
+                        mPatternProblem = R.string.pattern_does_not_match;
+                    }
+                }
+            }
+        }
+    }
+    @NonNull
+    @Override
+    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+        Context context = requireContext();
+        Dialog csd = new Dialog(context);
+        FragmentPatternDetailSourceSelectorListBinding b =
+                FragmentPatternDetailSourceSelectorListBinding.inflate(
+                        LayoutInflater.from(context));
+        csd.setContentView(b.getRoot());
+        csd.setTitle(R.string.choose_pattern_detail_source_label);
+
+        if (mSources != null && !mSources.isEmpty()) {
+            RecyclerView recyclerView = b.list;
+
+            if (mColumnCount <= 1) {
+                recyclerView.setLayoutManager(new LinearLayoutManager(context));
+            }
+            else {
+                recyclerView.setLayoutManager(new GridLayoutManager(context, mColumnCount));
+            }
+            model = new ViewModelProvider(this).get(PatternDetailSourceSelectorModel.class);
+            if (onSourceSelectedListener != null)
+                model.setOnSourceSelectedListener(onSourceSelectedListener);
+            model.setSourcesList(mSources);
+
+            PatternDetailSourceSelectorRecyclerViewAdapter adapter =
+                    new PatternDetailSourceSelectorRecyclerViewAdapter();
+            model.groups.observe(this, adapter::submitList);
+
+            recyclerView.setAdapter(adapter);
+            adapter.setSourceSelectedListener(this);
+        }
+        else {
+            b.list.setVisibility(View.GONE);
+            b.patternError.setText(mPatternProblem);
+            b.patternError.setVisibility(View.VISIBLE);
+        }
+
+        b.literalButton.setOnClickListener(v -> onSourceSelected(true, (short) -1));
+
+        return csd;
+    }
+    public void setOnSourceSelectedListener(OnSourceSelectedListener listener) {
+        onSourceSelectedListener = listener;
+
+        if (model != null)
+            model.setOnSourceSelectedListener(listener);
+    }
+    public void resetOnSourceSelectedListener() {
+        model.resetOnSourceSelectedListener();
+    }
+    @Override
+    public void onSourceSelected(boolean literal, short group) {
+        if (model != null)
+            model.triggerOnSourceSelectedListener(literal, group);
+        if (onSourceSelectedListener != null)
+            onSourceSelectedListener.onSourceSelected(literal, group);
+
+        dismiss();
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/PatternDetailSourceSelectorModel.java b/app/src/main/java/net/ktnx/mobileledger/ui/PatternDetailSourceSelectorModel.java
new file mode 100644 (file)
index 0000000..b88c676
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * Copyright © 2020 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;
+
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+
+import net.ktnx.mobileledger.model.PatternDetailSource;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PatternDetailSourceSelectorModel extends ViewModel {
+    public final MutableLiveData<List<PatternDetailSource>> groups = new MutableLiveData<>();
+    private OnSourceSelectedListener selectionListener;
+    public PatternDetailSourceSelectorModel() {
+    }
+    void setOnSourceSelectedListener(OnSourceSelectedListener listener) {
+        selectionListener = listener;
+    }
+    void resetOnSourceSelectedListener() {
+        selectionListener = null;
+    }
+    void triggerOnSourceSelectedListener(boolean literal, short group) {
+        if (selectionListener != null)
+            selectionListener.onSourceSelected(literal, group);
+    }
+    public void setSourcesList(ArrayList<PatternDetailSource> mSources) {
+        groups.setValue(mSources);
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/PatternDetailSourceSelectorRecyclerViewAdapter.java b/app/src/main/java/net/ktnx/mobileledger/ui/PatternDetailSourceSelectorRecyclerViewAdapter.java
new file mode 100644 (file)
index 0000000..91a3c4d
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * Copyright © 2019 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;
+
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.recyclerview.widget.ListAdapter;
+import androidx.recyclerview.widget.RecyclerView;
+
+import net.ktnx.mobileledger.databinding.FragmentPatternDetailSourceSelectorBinding;
+import net.ktnx.mobileledger.model.PatternDetailSource;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * {@link RecyclerView.Adapter} that can display a {@link PatternDetailSource} and makes a call
+ * to the
+ * specified {@link OnSourceSelectedListener}.
+ */
+public class PatternDetailSourceSelectorRecyclerViewAdapter extends
+        ListAdapter<PatternDetailSource,
+                PatternDetailSourceSelectorRecyclerViewAdapter.ViewHolder> {
+
+    private OnSourceSelectedListener sourceSelectedListener;
+    public PatternDetailSourceSelectorRecyclerViewAdapter() {
+        super(PatternDetailSource.DIFF_CALLBACK);
+    }
+    @NotNull
+    @Override
+    public ViewHolder onCreateViewHolder(@NotNull ViewGroup parent, int viewType) {
+        FragmentPatternDetailSourceSelectorBinding b =
+                FragmentPatternDetailSourceSelectorBinding.inflate(
+                        LayoutInflater.from(parent.getContext()), parent, false);
+        return new ViewHolder(b);
+    }
+
+    @Override
+    public void onBindViewHolder(final ViewHolder holder, int position) {
+        holder.bindTo(getItem(position));
+    }
+    public void setSourceSelectedListener(OnSourceSelectedListener listener) {
+        this.sourceSelectedListener = listener;
+    }
+    public void resetSourceSelectedListener() {
+        sourceSelectedListener = null;
+    }
+    public void notifySourceSelected(PatternDetailSource item) {
+        if (null != sourceSelectedListener)
+            sourceSelectedListener.onSourceSelected(false, item.getGroupNumber());
+    }
+    public void notifyLiteralSelected() {
+        if (null != sourceSelectedListener)
+            sourceSelectedListener.onSourceSelected(true, (short) -1);
+    }
+    public class ViewHolder extends RecyclerView.ViewHolder {
+        private final FragmentPatternDetailSourceSelectorBinding b;
+        private PatternDetailSource mItem;
+
+        ViewHolder(FragmentPatternDetailSourceSelectorBinding binding) {
+            super(binding.getRoot());
+            b = binding;
+
+            b.getRoot()
+             .setOnClickListener(v -> notifySourceSelected(mItem));
+        }
+
+        @NotNull
+        @Override
+        public String toString() {
+            return super.toString() + " " + b.groupNumber.getText() + ": '" +
+                   b.matchedText.getText() + "'";
+        }
+        void bindTo(PatternDetailSource item) {
+            mItem = item;
+            b.groupNumber.setText(String.valueOf(item.getGroupNumber()));
+            b.matchedText.setText(item.getMatchedText());
+        }
+    }
+}
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");
+        }
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternDetailsFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternDetailsFragment.java
new file mode 100644 (file)
index 0000000..83aa17d
--- /dev/null
@@ -0,0 +1,114 @@
+/*
+ * 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.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.lifecycle.ViewModelStoreOwner;
+import androidx.navigation.NavController;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
+
+import com.google.android.material.snackbar.Snackbar;
+
+import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.databinding.PatternDetailsFragmentBinding;
+import net.ktnx.mobileledger.ui.QRScanAbleFragment;
+import net.ktnx.mobileledger.utils.Logger;
+
+public class PatternDetailsFragment extends QRScanAbleFragment {
+    static final String ARG_PATTERN_ID = "pattern-id";
+    private static final String ARG_COLUMN_COUNT = "column-count";
+    PatternDetailsFragmentBinding b;
+    private PatternDetailsViewModel mViewModel;
+    private int mColumnCount = 1;
+    private int mPatternId = PatternDetailsViewModel.NEW_PATTERN;
+    public PatternDetailsFragment() {
+    }
+    public static PatternDetailsFragment newInstance(int columnCount, int patternId) {
+        final PatternDetailsFragment fragment = new PatternDetailsFragment();
+        Bundle args = new Bundle();
+        args.putInt(ARG_COLUMN_COUNT, columnCount);
+        if (patternId > 0)
+            args.putInt(ARG_PATTERN_ID, patternId);
+        fragment.setArguments(args);
+        return fragment;
+    }
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        final Bundle args = getArguments();
+        if (args != null) {
+            mColumnCount = args.getInt(ARG_COLUMN_COUNT, 1);
+            mPatternId = args.getInt(ARG_PATTERN_ID, PatternDetailsViewModel.NEW_PATTERN);
+        }
+        mViewModel.setPatternId(mPatternId);
+    }
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+                             @Nullable Bundle savedInstanceState) {
+        b = PatternDetailsFragmentBinding.inflate(inflater);
+        Context context = b.patternDetailsRecyclerView.getContext();
+        if (mColumnCount <= 1) {
+            b.patternDetailsRecyclerView.setLayoutManager(new LinearLayoutManager(context));
+        }
+        else {
+            b.patternDetailsRecyclerView.setLayoutManager(
+                    new GridLayoutManager(context, mColumnCount));
+        }
+
+
+        PatternDetailsAdapter adapter = new PatternDetailsAdapter();
+        b.patternDetailsRecyclerView.setAdapter(adapter);
+        mViewModel.getItems()
+                  .observe(getViewLifecycleOwner(), adapter::setItems);
+        return b.getRoot();
+    }
+    @Override
+    public void onAttach(@NonNull Context context) {
+        super.onAttach(context);
+        NavController controller = ((PatternsActivity) context).getNavController();
+        final ViewModelStoreOwner viewModelStoreOwner =
+                controller.getViewModelStoreOwner(R.id.pattern_list_navigation);
+        mViewModel = new ViewModelProvider(viewModelStoreOwner).get(PatternDetailsViewModel.class);
+        mViewModel.setDefaultPatternName(getString(R.string.unnamed_pattern));
+        Logger.debug("flow", "PatternDetailsFragment.onAttach(): model=" + mViewModel);
+
+    }
+    @Override
+    protected void onQrScanned(String text) {
+        Logger.debug("PatDet_fr", String.format("Got scanned text '%s'", text));
+        mViewModel.setTestText(text);
+    }
+    public void onSavePattern() {
+        mViewModel.onSavePattern();
+        final Snackbar snackbar = Snackbar.make(b.getRoot(),
+                "One Save pattern action coming up soon in a fragment near you",
+                Snackbar.LENGTH_INDEFINITE);
+//        snackbar.setAction("Action", v -> snackbar.dismiss());
+        snackbar.show();
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternDetailsViewModel.java b/app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternDetailsViewModel.java
new file mode 100644 (file)
index 0000000..1c6e035
--- /dev/null
@@ -0,0 +1,219 @@
+/*
+ * 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.database.Cursor;
+import android.os.AsyncTask;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+
+import net.ktnx.mobileledger.App;
+import net.ktnx.mobileledger.dao.PatternAccountDAO;
+import net.ktnx.mobileledger.dao.PatternHeaderDAO;
+import net.ktnx.mobileledger.db.DB;
+import net.ktnx.mobileledger.db.PatternAccount;
+import net.ktnx.mobileledger.db.PatternHeader;
+import net.ktnx.mobileledger.model.Currency;
+import net.ktnx.mobileledger.model.PatternDetailsItem;
+import net.ktnx.mobileledger.utils.Logger;
+import net.ktnx.mobileledger.utils.MLDB;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+public class PatternDetailsViewModel extends ViewModel {
+    static final int NEW_PATTERN = -1;
+    private final MutableLiveData<List<PatternDetailsItem>> items = new MutableLiveData<>();
+    private long mPatternId;
+    private String mDefaultPatternName;
+    public String getDefaultPatternName() {
+        return mDefaultPatternName;
+    }
+    public void setDefaultPatternName(String name) {
+        mDefaultPatternName = name;
+    }
+    public LiveData<List<PatternDetailsItem>> getItems() {
+        return items;
+    }
+
+    public void resetItems() {
+        items.setValue(Collections.emptyList());
+        checkItemConsistency();
+    }
+    private void checkItemConsistency() {
+        ArrayList<PatternDetailsItem> newList = new ArrayList<>(items.getValue());
+        boolean changes = false;
+        if (newList.size() < 1) {
+            final PatternDetailsItem.Header header = PatternDetailsItem.createHeader();
+            header.setName(mDefaultPatternName);
+            newList.add(header);
+            changes = true;
+        }
+
+        while (newList.size() < 3) {
+            newList.add(PatternDetailsItem.createAccountRow(newList.size() - 1));
+            changes = true;
+        }
+
+        if (changes)
+            items.setValue(newList);
+    }
+    public void loadItems(long patternId) {
+        DB db = App.getRoomDB();
+        LiveData<PatternHeader> ph = db.getPatternDAO()
+                                       .getPattern(patternId);
+        ArrayList<PatternDetailsItem> list = new ArrayList<>();
+
+        MLDB.queryInBackground(
+                "SELECT name, regular_expression, transaction_description, transaction_comment, " +
+                "date_year_match_group, date_month_match_group, date_day_match_group FROM " +
+                "patterns WHERE id=?", new String[]{String.valueOf(patternId)},
+                new MLDB.CallbackHelper() {
+                    @Override
+                    public void onDone() {
+                        super.onDone();
+
+                        MLDB.queryInBackground(
+                                "SELECT id, position, acc, acc_match_group, currency, " +
+                                "currency_match_group, amount, amount_match_group," +
+                                " comment, comment_match_group FROM " +
+                                "pattern_accounts WHERE pattern_id=? ORDER BY " + "position ASC",
+                                new String[]{String.valueOf(patternId)}, new MLDB.CallbackHelper() {
+                                    @Override
+                                    public void onDone() {
+                                        super.onDone();
+                                        items.postValue(list);
+                                    }
+                                    @Override
+                                    public boolean onRow(@NonNull Cursor cursor) {
+                                        PatternDetailsItem.AccountRow item =
+                                                PatternDetailsItem.createAccountRow(
+                                                        cursor.getInt(1));
+                                        list.add(item);
+
+                                        item.setId(cursor.getInt(0));
+
+                                        if (cursor.isNull(3)) {
+                                            item.setAccountName(cursor.getString(2));
+                                        }
+                                        else {
+                                            item.setAccountNameMatchGroup(cursor.getShort(3));
+                                        }
+
+                                        if (cursor.isNull(5)) {
+                                            final int currId = cursor.getInt(4);
+                                            if (currId > 0)
+                                                item.setCurrency(Currency.loadById(currId));
+                                        }
+                                        else {
+                                            item.setCurrencyMatchGroup(cursor.getShort(5));
+                                        }
+
+                                        if (cursor.isNull(7)) {
+                                            item.setAmount(cursor.getFloat(6));
+                                        }
+                                        else {
+                                            item.setAmountMatchGroup(cursor.getShort(7));
+                                        }
+
+                                        if (cursor.isNull(9)) {
+                                            item.setAccountComment(cursor.getString(8));
+                                        }
+                                        else {
+                                            item.setAccountCommentMatchGroup(cursor.getShort(9));
+                                        }
+
+                                        return true;
+                                    }
+                                });
+                    }
+                    @Override
+                    public boolean onRow(@NonNull Cursor cursor) {
+                        PatternDetailsItem.Header header = PatternDetailsItem.createHeader();
+                        header.setName(cursor.getString(0));
+                        header.setPattern(cursor.getString(1));
+                        header.setTransactionDescription(cursor.getString(2));
+                        header.setTransactionComment(cursor.getString(3));
+                        header.setDateYearMatchGroup(cursor.getShort(4));
+                        header.setDateMonthMatchGroup(cursor.getShort(5));
+                        header.setDateDayMatchGroup(cursor.getShort(6));
+
+                        list.add(header);
+
+                        return false;
+                    }
+                });
+    }
+    public void setTestText(String text) {
+        List<PatternDetailsItem> list = new ArrayList<>(items.getValue());
+        PatternDetailsItem.Header header = new PatternDetailsItem.Header(list.get(0)
+                                                                             .asHeaderItem());
+        header.setTestText(text);
+        list.set(0, header);
+
+        items.setValue(list);
+    }
+    public void setPatternId(int patternId) {
+        if (mPatternId != patternId) {
+            if (patternId == NEW_PATTERN) {
+                resetItems();
+            }
+            else {
+                loadItems(patternId);
+            }
+            mPatternId = patternId;
+        }
+
+    }
+    public void onSavePattern() {
+        Logger.debug("flow", "PatternDetailsViewModel.onSavePattern(); model=" + this);
+        final List<PatternDetailsItem> list = Objects.requireNonNull(items.getValue());
+
+        AsyncTask.execute(() -> {
+            PatternDetailsItem.Header modelHeader = list.get(0)
+                                                        .asHeaderItem();
+            PatternHeaderDAO headerDAO = App.getRoomDB()
+                                            .getPatternDAO();
+            PatternHeader dbHeader = modelHeader.toDBO();
+            if (mPatternId <= 0) {
+                dbHeader.setId(mPatternId = headerDAO.insert(dbHeader));
+            }
+            else
+                headerDAO.update(dbHeader);
+
+
+            PatternAccountDAO paDAO = App.getRoomDB()
+                                         .getPatternAccountDAO();
+            for (int i = 1; i < list.size(); i++) {
+                PatternAccount dbAccount = list.get(i)
+                                               .asAccountRowItem()
+                                               .toDBO(dbHeader.getId());
+                dbAccount.setPatternId(mPatternId);
+                if (dbAccount.getId() == null || dbAccount.getId() <= 0)
+                    dbAccount.setId(paDAO.insert(dbAccount));
+                else
+                    paDAO.update(dbAccount);
+            }
+        });
+    }
+}
\ No newline at end of file
index ad3564565dbee4fab3049196e09299e9aeedee3f..52aab60eef374d168dad6018e06be7f978b008e9 100644 (file)
@@ -31,8 +31,8 @@ import androidx.lifecycle.LifecycleOwner;
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
 
-import net.ktnx.mobileledger.R;
 import net.ktnx.mobileledger.databinding.FragmentPatternListBinding;
+import net.ktnx.mobileledger.utils.Logger;
 
 import org.jetbrains.annotations.NotNull;
 
@@ -73,12 +73,9 @@ public class PatternListFragment extends Fragment {
     @Override
     public View onCreateView(@NotNull LayoutInflater inflater, ViewGroup container,
                              Bundle savedInstanceState) {
+        Logger.debug("flow", "PatternListFragment.onCreateView()");
         b = FragmentPatternListBinding.inflate(inflater);
 
-        b.toolbarLayout.setTitle(getString(R.string.title_activity_patterns));
-
-        b.fab.setOnClickListener(this::fabClicked);
-
         PatternsRecyclerViewAdapter modelAdapter = new PatternsRecyclerViewAdapter();
 
         b.patternList.setAdapter(modelAdapter);
@@ -129,7 +126,7 @@ public class PatternListFragment extends Fragment {
      */
     public interface OnPatternListFragmentInteractionListener {
         void onNewPattern();
-
+        void onSavePattern();
         void onEditPattern(int id);
     }
 }
\ No newline at end of file
index 0a60ab3b835a6078e3daa45161d249c9b2872da1..829260b976f3658b4e8c09c27926f443cedd3242 100644 (file)
@@ -31,5 +31,8 @@ class PatternViewHolder extends RecyclerView.ViewHolder {
     }
     public void bindToItem(PatternEntry item) {
         b.patternName.setText(item.getName());
+        b.editButon.setOnClickListener(v -> {
+            ((PatternsActivity) v.getContext()).onEditPattern(item.getId());
+        });
     }
 }
index 43a6227df1c8e20e951f59c964a1a4057f40aae1..da258d48a7a0ec12425802c1d61b26f0f8f766fc 100644 (file)
@@ -20,14 +20,19 @@ package net.ktnx.mobileledger.ui.patterns;
 import android.os.Bundle;
 import android.view.Menu;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.ActionBar;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.lifecycle.ViewModelStoreOwner;
 import androidx.navigation.NavController;
+import androidx.navigation.NavDestination;
 import androidx.navigation.fragment.NavHostFragment;
 
-import com.google.android.material.snackbar.Snackbar;
-
 import net.ktnx.mobileledger.R;
 import net.ktnx.mobileledger.databinding.ActivityPatternsBinding;
 import net.ktnx.mobileledger.ui.activity.CrashReportingActivity;
+import net.ktnx.mobileledger.utils.Logger;
 
 import java.util.Objects;
 
@@ -46,27 +51,66 @@ public class PatternsActivity extends CrashReportingActivity
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         b = ActivityPatternsBinding.inflate(getLayoutInflater());
-        setContentView(b.fragmentContainer);
+        setContentView(b.getRoot());
+        setSupportActionBar(b.toolbar);
+        // Show the Up button in the action bar.
+        ActionBar actionBar = getSupportActionBar();
+        if (actionBar != null) {
+            actionBar.setDisplayHomeAsUpEnabled(true);
+        }
 
         NavHostFragment navHostFragment = (NavHostFragment) Objects.requireNonNull(
                 getSupportFragmentManager().findFragmentById(R.id.fragment_container));
         navController = navHostFragment.getNavController();
+
+        navController.addOnDestinationChangedListener(
+                new NavController.OnDestinationChangedListener() {
+                    @Override
+                    public void onDestinationChanged(@NonNull NavController controller,
+                                                     @NonNull NavDestination destination,
+                                                     @Nullable Bundle arguments) {
+                        if (destination.getId() == R.id.patternListFragment) {
+                            b.fabAdd.show();
+                            b.fabSave.hide();
+                        }
+                        if (destination.getId() == R.id.patternDetailsFragment) {
+                            b.fabAdd.hide();
+                            b.fabSave.show();
+                        }
+                    }
+                });
+
+        b.toolbarLayout.setTitle(getString(R.string.title_activity_patterns));
+
+        b.fabAdd.setOnClickListener(v -> onNewPattern());
+        b.fabSave.setOnClickListener(v -> onSavePattern());
     }
     @Override
     public void onNewPattern() {
-//        navController.navigate
-        final Snackbar snackbar =
-                Snackbar.make(b.fragmentContainer, "New pattern action coming up soon",
-                        Snackbar.LENGTH_INDEFINITE);
+        navController.navigate(R.id.action_patternListFragment_to_patternDetailsFragment);
+//        final Snackbar snackbar =
+//                Snackbar.make(b.fragmentContainer, "New pattern action coming up soon",
+//                        Snackbar.LENGTH_INDEFINITE);
 //        snackbar.setAction("Action", v -> snackbar.dismiss());
-        snackbar.show();
+//        snackbar.show();
     }
     @Override
     public void onEditPattern(int id) {
-        final Snackbar snackbar =
-                Snackbar.make(b.fragmentContainer, "One Edit pattern action coming up soon",
-                        Snackbar.LENGTH_INDEFINITE);
-//        snackbar.setAction("Action", v -> snackbar.dismiss());
-        snackbar.show();
+        Bundle bundle = new Bundle();
+        bundle.putInt(PatternDetailsFragment.ARG_PATTERN_ID, id);
+        navController.navigate(R.id.action_patternListFragment_to_patternDetailsFragment, bundle);
+    }
+    @Override
+    public void onSavePattern() {
+        final ViewModelStoreOwner viewModelStoreOwner =
+                navController.getViewModelStoreOwner(R.id.pattern_list_navigation);
+        PatternDetailsViewModel model =
+                new ViewModelProvider(viewModelStoreOwner).get(PatternDetailsViewModel.class);
+        Logger.debug("flow", "PatternsActivity.onSavePattern(): model=" + model);
+        model.onSavePattern();
+        navController.navigate(R.id.patternListFragment);
+    }
+    public NavController getNavController() {
+        return navController;
     }
 }
\ No newline at end of file
index e531809a38ce416dd09cac7bd7b13587624fb12d..ed73190e8a4df771e195a7011ebe805a17997966 100644 (file)
@@ -205,7 +205,7 @@ public class ProfileDetailFragment extends Fragment {
                 resetDefaultCommodity();
         });
 
-        FloatingActionButton fab = context.findViewById(R.id.fab);
+        FloatingActionButton fab = context.findViewById(R.id.fabAdd);
         fab.setOnClickListener(v -> onSaveFabClicked());
 
         hookTextChangeSyncRoutine(binding.profileName, model::setProfileName);
index 69f5aac1324322e18e5327f7a260d9a2adb4a00e..47ce9f1d74dc0d92d8329cd49306fc92485d5d38 100644 (file)
   ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
   -->
 
-<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/fragment_container"
-    android:name="androidx.navigation.fragment.NavHostFragment"
+    xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    app:defaultNavHost="true"
-    app:navGraph="@navigation/pattern_list_navigation"
-    />
\ No newline at end of file
+    tools:context=".ui.patterns.PatternsActivity"
+    >
+
+    <com.google.android.material.appbar.AppBarLayout
+        android:id="@+id/appbar"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/app_bar_height"
+        android:fitsSystemWindows="true"
+        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
+        >
+        <com.google.android.material.appbar.CollapsingToolbarLayout
+            android:id="@+id/toolbar_layout"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:fitsSystemWindows="true"
+            app:contentScrim="?attr/colorPrimary"
+            app:layout_scrollFlags="scroll|exitUntilCollapsed"
+            app:toolbarId="@+id/toolbar"
+            >
+
+            <androidx.appcompat.widget.Toolbar
+                android:id="@+id/toolbar"
+                android:layout_width="match_parent"
+                android:layout_height="?attr/actionBarSize"
+                app:layout_collapseMode="pin"
+                app:popupTheme="@style/ThemeOverlay.AppCompat.DayNight"
+                />
+        </com.google.android.material.appbar.CollapsingToolbarLayout>
+    </com.google.android.material.appbar.AppBarLayout>
+
+    <androidx.core.widget.NestedScrollView
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        app:layout_behavior="@string/appbar_scrolling_view_behavior"
+        >
+        <androidx.fragment.app.FragmentContainerView
+            android:id="@+id/fragment_container"
+            android:name="androidx.navigation.fragment.NavHostFragment"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            app:defaultNavHost="true"
+            app:navGraph="@navigation/pattern_list_navigation"
+            />
+    </androidx.core.widget.NestedScrollView>
+
+    <com.google.android.material.floatingactionbutton.FloatingActionButton
+        android:id="@+id/fabAdd"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="bottom|end"
+        android:layout_margin="@dimen/fab_margin"
+        app:srcCompat="@drawable/ic_add_white_24dp"
+        android:contentDescription="@string/add_button_description"
+        />
+
+    <com.google.android.material.floatingactionbutton.FloatingActionButton
+        android:id="@+id/fabSave"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="bottom|end"
+        android:layout_margin="@dimen/fab_margin"
+        app:srcCompat="@drawable/ic_save_white_24dp"
+        android:contentDescription="@string/save_button_description"
+        />
+
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
index 282af250b523d333ccb24b5239c45bd3fe28715b..4184146a70f2add9ccec1747ff32f238eec4e249 100644 (file)
@@ -60,7 +60,7 @@
         app:layout_behavior="@string/appbar_scrolling_view_behavior" />
 
     <com.google.android.material.floatingactionbutton.FloatingActionButton
-        android:id="@+id/fab"
+        android:id="@+id/fabAdd"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_gravity="center_vertical|start"
diff --git a/app/src/main/res/layout/fragment_item_list.xml b/app/src/main/res/layout/fragment_item_list.xml
new file mode 100644 (file)
index 0000000..b2eda57
--- /dev/null
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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/>.
+  -->
+
+<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/list"
+    android:name="net.ktnx.mobileledger.ui.patterns.TestItemFragment"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_marginLeft="16dp"
+    android:layout_marginRight="16dp"
+    app:layoutManager="LinearLayoutManager"
+    tools:context=".ui.patterns.TestItemFragment"
+    tools:listitem="@layout/pattern_details_header"
+    />
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_pattern_detail_source_selector.xml b/app/src/main/res/layout/fragment_pattern_detail_source_selector.xml
new file mode 100644 (file)
index 0000000..b36f101
--- /dev/null
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright © 2019 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/>.
+  -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:longClickable="false"
+    >
+
+    <TextView
+        android:id="@+id/group_number"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_margin="@dimen/text_margin"
+        android:gravity="end"
+        android:minWidth="20sp"
+        android:text="1"
+        android:textAppearance="?attr/textAppearanceListItem"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        tools:ignore="HardcodedText"
+        />
+    <TextView
+        android:id="@+id/colon"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/text_margin"
+        android:text=":"
+        android:textAppearance="?attr/textAppearanceListItem"
+        app:layout_constraintStart_toEndOf="@id/group_number"
+        app:layout_constraintTop_toTopOf="parent"
+        tools:ignore="HardcodedText"
+        />
+    <TextView
+        android:id="@+id/matched_text"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_margin="@dimen/text_margin"
+        android:textAppearance="?attr/textAppearanceListItem"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toEndOf="@id/colon"
+        app:layout_constraintTop_toTopOf="parent"
+        />
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/app/src/main/res/layout/fragment_pattern_detail_source_selector_list.xml b/app/src/main/res/layout/fragment_pattern_detail_source_selector_list.xml
new file mode 100644 (file)
index 0000000..bfdc5d3
--- /dev/null
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright © 2020 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/>.
+  -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:animateLayoutChanges="true"
+    android:minWidth="60dp"
+    android:padding="@dimen/text_margin"
+    app:layout_constraintWidth_min="60dp"
+    >
+
+    <com.google.android.material.textview.MaterialTextView
+        android:id="@+id/label"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="@dimen/text_margin"
+        android:text="@string/choose_pattern_detail_source_label"
+        android:textSize="18sp"
+        app:layout_constraintBottom_toTopOf="@id/list"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        />
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/list"
+        android:name="net.ktnx.mobileledger.ui.PatternDetailSourceSelectorFragment"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        app:layout_constraintWidth_min="50dp"
+        app:layout_constraintHeight_min="150dp"
+        android:layout_marginLeft="@dimen/activity_horizontal_margin"
+        android:layout_marginRight="@dimen/activity_horizontal_margin"
+        android:minHeight="100dp"
+        app:layoutManager="LinearLayoutManager"
+        app:layout_constraintBottom_toTopOf="@id/pattern_error"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/label"
+        tools:context="net.ktnx.mobileledger.ui.CurrencySelectorFragment"
+        tools:listitem="@layout/fragment_pattern_detail_source_selector"
+        />
+    <TextView
+        android:id="@+id/pattern_error"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:layout_constraintBottom_toTopOf="@id/literal_button"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/list"
+        />
+    <com.google.android.material.button.MaterialButton
+        android:id="@+id/literal_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/text_margin"
+        android:text="@string/pattern_details_source_literal"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        />
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
index e476b0cc9161faf489214ef96ee744a16d096fba..f90abdeaee61a72ad27db0f9b220607990a4c522 100644 (file)
   ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
   -->
 
-<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
+<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/pattern_list"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:fitsSystemWindows="true"
+    android:background="#ff808000"
     tools:context=".ui.patterns.PatternsActivity"
-    >
-
-    <com.google.android.material.appbar.AppBarLayout
-        android:id="@+id/app_bar"
-        android:layout_width="match_parent"
-        android:layout_height="@dimen/app_bar_height"
-        android:fitsSystemWindows="true"
-        >
-
-        <com.google.android.material.appbar.CollapsingToolbarLayout
-            android:id="@+id/toolbar_layout"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:fitsSystemWindows="true"
-            app:contentScrim="?attr/colorPrimary"
-            app:layout_scrollFlags="scroll|exitUntilCollapsed"
-            app:toolbarId="@+id/toolbar"
-            >
-
-            <androidx.appcompat.widget.Toolbar
-                android:id="@+id/toolbar"
-                android:layout_width="match_parent"
-                android:layout_height="?attr/actionBarSize"
-                app:layout_collapseMode="pin"
-                app:popupTheme="@style/AppTheme.PopupOverlay"
-                />
-
-        </com.google.android.material.appbar.CollapsingToolbarLayout>
-    </com.google.android.material.appbar.AppBarLayout>
-
-    <androidx.core.widget.NestedScrollView
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        app:layout_behavior="@string/appbar_scrolling_view_behavior"
-        tools:context=".ui.patterns.PatternsActivity"
-        tools:showIn="@layout/activity_patterns"
-        >
-
-        <androidx.recyclerview.widget.RecyclerView
-            android:id="@+id/pattern_list"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            />
-
-    </androidx.core.widget.NestedScrollView>
-
-    <com.google.android.material.floatingactionbutton.FloatingActionButton
-        android:id="@+id/fab"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_margin="@dimen/fab_margin"
-        app:layout_anchor="@id/pattern_list"
-        app:layout_anchorGravity="top|end"
-        app:layout_behavior="com.google.android.material.floatingactionbutton.FloatingActionButton$Behavior"
-        app:srcCompat="@drawable/ic_add_white_24dp"
-        />
-
-</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
+    />
diff --git a/app/src/main/res/layout/pattern_details_account.xml b/app/src/main/res/layout/pattern_details_account.xml
new file mode 100644 (file)
index 0000000..6a40b0c
--- /dev/null
@@ -0,0 +1,154 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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/>.
+  -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/pattern_details_item_account_row"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingHorizontal="@dimen/text_margin"
+    >
+    <TextView
+        android:id="@+id/pattern_account_label"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:gravity="end"
+        android:paddingTop="@dimen/text_margin"
+        android:text="@string/pattern_details_account_row_label"
+        app:drawableBottomCompat="@drawable/dashed_border_8dp"
+        />
+    <TextView
+        android:id="@+id/pattern_account_name_source_label"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:paddingTop="@dimen/text_margin"
+        android:text="@string/account_name_source_label"
+        android:textAppearance="?attr/textAppearanceListItem"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/pattern_account_label"
+        />
+    <TextView
+        android:id="@+id/pattern_details_account_name_source"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:minWidth="100dp"
+        android:textAppearance="?attr/textAppearanceListItemSecondary"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/pattern_account_name_source_label"
+        />
+    <com.google.android.material.textfield.TextInputLayout
+        android:id="@+id/pattern_details_account_name_layout"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginHorizontal="@dimen/text_margin"
+        android:textAppearance="?attr/textAppearanceListItem"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/pattern_details_account_name_source"
+        >
+        <com.google.android.material.textfield.TextInputEditText
+            android:id="@+id/pattern_details_account_name"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:hint="@string/pattern_details_account_name_label"
+            android:inputType="text"
+            />
+    </com.google.android.material.textfield.TextInputLayout>
+
+    <TextView
+        android:id="@+id/pattern_account_comment_source_label"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+
+        android:paddingTop="@dimen/text_margin"
+        android:text="@string/account_comment_source_label"
+        android:textAppearance="?attr/textAppearanceListItem"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/pattern_details_account_name_layout"
+        />
+    <TextView
+        android:id="@+id/pattern_details_account_comment_source"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:minWidth="100dp"
+        android:textAppearance="?attr/textAppearanceListItemSecondary"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/pattern_account_comment_source_label"
+        />
+    <com.google.android.material.textfield.TextInputLayout
+        android:id="@+id/pattern_details_account_comment_layout"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginHorizontal="@dimen/text_margin"
+        android:textAppearance="?attr/textAppearanceListItem"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/pattern_details_account_comment_source"
+        >
+        <com.google.android.material.textfield.TextInputEditText
+            android:id="@+id/pattern_details_account_comment"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:hint="@string/pattern_details_account_comment_label"
+            android:inputType="text"
+            />
+    </com.google.android.material.textfield.TextInputLayout>
+
+    <TextView
+        android:id="@+id/pattern_account_amount_source_label"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:paddingTop="@dimen/text_margin"
+        android:text="@string/account_amount_source_label"
+        android:textAppearance="?attr/textAppearanceListItem"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/pattern_details_account_comment_layout"
+        />
+    <TextView
+        android:id="@+id/pattern_details_account_amount_source"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:textAppearance="?attr/textAppearanceListItemSecondary"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/pattern_account_amount_source_label"
+        />
+    <com.google.android.material.textfield.TextInputLayout
+        android:id="@+id/pattern_details_account_amount_layout"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginHorizontal="@dimen/text_margin"
+        android:textAppearance="?attr/textAppearanceListItem"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/pattern_details_account_amount_source"
+        >
+        <com.google.android.material.textfield.TextInputEditText
+            android:id="@+id/pattern_details_account_amount"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:hint="@string/pattern_details_account_amount_label"
+            android:inputType="number|numberDecimal|numberSigned"
+            />
+    </com.google.android.material.textfield.TextInputLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/pattern_details_fragment.xml b/app/src/main/res/layout/pattern_details_fragment.xml
new file mode 100644 (file)
index 0000000..01ddc4d
--- /dev/null
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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/>.
+  -->
+
+<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/pattern_details_recycler_view"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".ui.patterns.PatternDetailsFragment"
+    >
+</androidx.recyclerview.widget.RecyclerView>
\ No newline at end of file
diff --git a/app/src/main/res/layout/pattern_details_header.xml b/app/src/main/res/layout/pattern_details_header.xml
new file mode 100644 (file)
index 0000000..1e9e35f
--- /dev/null
@@ -0,0 +1,299 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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/>.
+  -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/pattern_details_item_head"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingHorizontal="@dimen/text_margin"
+    >
+    <com.google.android.material.textfield.TextInputLayout
+        android:id="@+id/pattern_name_layout"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        >
+        <com.google.android.material.textfield.TextInputEditText
+            android:id="@+id/pattern_name"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:hint="@string/pattern_name_label"
+            android:inputType="text"
+            />
+    </com.google.android.material.textfield.TextInputLayout>
+    <com.google.android.material.textfield.TextInputLayout
+        android:id="@+id/pattern_layout"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:textAppearance="?attr/textAppearanceListItem"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/pattern_name_layout"
+        >
+        <com.google.android.material.textfield.TextInputEditText
+            android:id="@+id/pattern"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:hint="@string/pattern_details_pattern_label"
+            android:inputType="text"
+            />
+    </com.google.android.material.textfield.TextInputLayout>
+    <com.google.android.material.textfield.TextInputLayout
+        android:id="@+id/test_text_layout"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:textAppearance="?attr/textAppearanceListItem"
+        app:layout_constraintEnd_toStartOf="@id/pattern_details_head_scan_qr_button"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/pattern_layout"
+        >
+        <com.google.android.material.textfield.TextInputEditText
+            android:id="@+id/test_text"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:hint="@string/pattern_details_test_text_label"
+            android:inputType="text"
+            />
+    </com.google.android.material.textfield.TextInputLayout>
+    <ImageButton
+        android:id="@+id/pattern_details_head_scan_qr_button"
+        android:layout_width="wrap_content"
+        android:layout_height="0dp"
+        android:background="@android:color/transparent"
+        android:contentDescription="@string/scan_qr"
+        android:minWidth="@dimen/thumb_row_height"
+        app:layout_constraintBottom_toBottomOf="@id/test_text_layout"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="@id/test_text_layout"
+        app:srcCompat="@drawable/ic_baseline_qr_code_scanner_24"
+        app:tint="?colorPrimary"
+        />
+    <TextView
+        android:id="@+id/transaction_parameters_label"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="end"
+        android:paddingTop="@dimen/text_margin"
+        android:text="@string/pattern_transaction_parameters_label"
+        app:layout_constraintTop_toBottomOf="@id/test_text_layout"
+        />
+    <TextView
+        android:id="@+id/pattern_transaction_date_label"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/pattern_details_date_label"
+        app:layout_constraintTop_toBottomOf="@id/transaction_parameters_label"
+        />
+    <TextView
+        android:id="@+id/pattern_details_year_source_label"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:text="@string/pattern_details_date_year_source_label"
+        android:textAlignment="center"
+        app:layout_constraintEnd_toStartOf="@id/pattern_details_month_source_label"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/pattern_transaction_date_label"
+        />
+    <TextView
+        android:id="@+id/pattern_details_month_source_label"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:text="@string/pattern_details_date_month_source_label"
+        android:textAlignment="center"
+        app:layout_constraintEnd_toStartOf="@id/pattern_details_day_source_label"
+        app:layout_constraintStart_toEndOf="@id/pattern_details_year_source_label"
+        app:layout_constraintTop_toBottomOf="@id/pattern_transaction_date_label"
+        />
+    <TextView
+        android:id="@+id/pattern_details_day_source_label"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:text="@string/pattern_details_date_day_source_label"
+        android:textAlignment="center"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toEndOf="@id/pattern_details_month_source_label"
+        app:layout_constraintTop_toBottomOf="@id/pattern_transaction_date_label"
+        />
+    <TextView
+        android:id="@+id/pattern_details_year_source"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:text="literal"
+        android:textAlignment="center"
+        app:layout_constraintEnd_toStartOf="@id/pattern_details_month_source"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/pattern_details_day_source_label"
+        />
+    <TextView
+        android:id="@+id/pattern_details_month_source"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:text=""
+        android:textAlignment="center"
+        app:layout_constraintEnd_toStartOf="@id/pattern_details_day_source"
+        app:layout_constraintStart_toEndOf="@id/pattern_details_year_source"
+        app:layout_constraintTop_toBottomOf="@id/pattern_details_month_source_label"
+        />
+    <TextView
+        android:id="@+id/pattern_details_day_source"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:text=""
+        android:textAlignment="center"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toEndOf="@id/pattern_details_month_source"
+        app:layout_constraintTop_toBottomOf="@id/pattern_details_day_source_label"
+        />
+    <androidx.constraintlayout.widget.Barrier
+        android:id="@+id/barrier_before_date_inputs"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        app:barrierDirection="bottom"
+        app:constraint_referenced_ids="pattern_details_year_source,pattern_details_month_source,pattern_details_day_source"
+        />
+    <com.google.android.material.textfield.TextInputLayout
+        android:id="@+id/pattern_details_date_year_layout"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        app:layout_constraintEnd_toStartOf="@id/pattern_details_date_month_layout"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/barrier_before_date_inputs"
+        >
+        <com.google.android.material.textfield.TextInputEditText
+            android:id="@+id/pattern_details_date_year"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:gravity="center_horizontal"
+            android:hint="@string/date_year_hint"
+            />
+    </com.google.android.material.textfield.TextInputLayout>
+    <com.google.android.material.textfield.TextInputLayout
+        android:id="@+id/pattern_details_date_month_layout"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        app:layout_constraintEnd_toStartOf="@id/pattern_details_date_day_layout"
+        app:layout_constraintStart_toEndOf="@id/pattern_details_date_year_layout"
+        app:layout_constraintTop_toBottomOf="@id/barrier_before_date_inputs"
+        >
+        <com.google.android.material.textfield.TextInputEditText
+            android:id="@+id/pattern_details_date_month"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:gravity="center_horizontal"
+            android:hint="@string/date_month_hint"
+            />
+    </com.google.android.material.textfield.TextInputLayout>
+    <com.google.android.material.textfield.TextInputLayout
+        android:id="@+id/pattern_details_date_day_layout"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        app:layout_constraintBottom_toTopOf="@id/barrier_before_description"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toEndOf="@id/pattern_details_date_month_layout"
+        app:layout_constraintTop_toBottomOf="@id/barrier_before_date_inputs"
+        >
+        <com.google.android.material.textfield.TextInputEditText
+            android:id="@+id/pattern_details_date_day"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:gravity="center_horizontal"
+            android:hint="@string/date_day_hint"
+            />
+    </com.google.android.material.textfield.TextInputLayout>
+    <androidx.constraintlayout.widget.Barrier
+        android:id="@+id/barrier_before_description"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        app:barrierDirection="bottom"
+        app:constraint_referenced_ids="pattern_details_date_day_layout,pattern_details_date_month_layout,pattern_details_date_year_layout"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        />
+    <TextView
+        android:id="@+id/pattern_transaction_description_source_label"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:paddingTop="@dimen/text_margin"
+        android:text="@string/transaction_description_source_label"
+        android:textAppearance="?attr/textAppearanceListItem"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/barrier_before_description"
+        />
+    <TextView
+        android:id="@+id/pattern_transaction_description_source"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:minWidth="100dp"
+        android:textAppearance="?attr/textAppearanceListItemSecondary"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/pattern_transaction_description_source_label"
+        />
+    <com.google.android.material.textfield.TextInputLayout
+        android:id="@+id/transaction_description_layout"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        app:layout_constraintTop_toBottomOf="@id/pattern_transaction_description_source"
+        >
+        <com.google.android.material.textfield.TextInputEditText
+            android:id="@+id/transaction_description"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:hint="@string/pattern_transaction_description_hint"
+            />
+    </com.google.android.material.textfield.TextInputLayout>
+    <TextView
+        android:id="@+id/pattern_transaction_comment_source_label"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:paddingTop="@dimen/text_margin"
+        android:text="@string/transaction_comment_source_label"
+        android:textAppearance="?attr/textAppearanceListItem"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/transaction_description_layout"
+        />
+    <TextView
+        android:id="@+id/pattern_transaction_comment_source"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:minWidth="100dp"
+        android:textAppearance="?attr/textAppearanceListItemSecondary"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/pattern_transaction_comment_source_label"
+        />
+    <com.google.android.material.textfield.TextInputLayout
+        android:id="@+id/transaction_comment_layout"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        app:layout_constraintTop_toBottomOf="@id/pattern_transaction_comment_source"
+        >
+        <com.google.android.material.textfield.TextInputEditText
+            android:id="@+id/transaction_comment"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:hint="@string/pattern_transaction_comment_hint"
+            />
+    </com.google.android.material.textfield.TextInputLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
index ba60d02b1e842be78c0de77f5b3691f9c79da18b..927ec201de8366a82e0a747cf3ab453af15dd541 100644 (file)
@@ -17,7 +17,8 @@
 
 <navigation xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/pattern_list_navigation.xml"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/pattern_list_navigation"
     app:startDestination="@id/patternListFragment"
     >
 
         android:id="@+id/patternListFragment"
         android:name="net.ktnx.mobileledger.ui.patterns.PatternListFragment"
         android:label="PatternListFragment"
-        />
+        android:tag="patternListFragment"
+        >
+        <action
+            android:id="@+id/action_patternListFragment_to_patternDetailsFragment"
+            app:destination="@id/patternDetailsFragment"
+            app:enterAnim="@anim/slide_in_left"
+            app:exitAnim="@anim/slide_out_left"
+            />
+    </fragment>
+    <fragment
+        android:id="@+id/patternDetailsFragment"
+        android:name="net.ktnx.mobileledger.ui.patterns.PatternDetailsFragment"
+        android:label="pattern_details_fragment"
+        android:tag="patternDetailsFragment"
+        tools:layout="@layout/pattern_details_fragment"
+        >
+        <action
+            android:id="@+id/action_patternDetailsFragment_to_patternListFragment"
+            app:destination="@id/patternListFragment"
+            />
+    </fragment>
 </navigation>
\ No newline at end of file
index 0d1a17a0ebfebf70cc3ffb0aaa9f71a26124bf0a..69ef1d8310b478a9534e94d08536327b7e36c91c 100644 (file)
     <string name="pattern_regex_hint">Pattern (regular expression)</string>
     <string name="help_menu_item_title">Help</string>
     <string name="edit_button_description">Edit button</string>
+    <string name="large_text">
+        "Material is the metaphor.\n\n"
+
+        "A material metaphor is the unifying theory of a rationalized space and a system of motion."
+        "The material is grounded in tactile reality, inspired by the study of paper and ink, yet "
+        "technologically advanced and open to imagination and magic.\n"
+        "Surfaces and edges of the material provide visual cues that are grounded in reality. The "
+        "use of familiar tactile attributes helps users quickly understand affordances. Yet the "
+        "flexibility of the material creates new affordances that supercede those in the physical "
+        "world, without breaking the rules of physics.\n"
+        "The fundamentals of light, surface, and movement are key to conveying how objects move, "
+        "interact, and exist in space and in relation to each other. Realistic lighting shows "
+        "seams, divides space, and indicates moving parts.\n\n"
+
+        "Bold, graphic, intentional.\n\n"
+
+        "The foundational elements of print based design typography, grids, space, scale, color, "
+        "and use of imagery guide visual treatments. These elements do far more than please the "
+        "eye. They create hierarchy, meaning, and focus. Deliberate color choices, edge to edge "
+        "imagery, large scale typography, and intentional white space create a bold and graphic "
+        "interface that immerse the user in the experience.\n"
+        "An emphasis on user actions makes core functionality immediately apparent and provides "
+        "waypoints for the user.\n\n"
+
+        "Motion provides meaning.\n\n"
+
+        "Motion respects and reinforces the user as the prime mover. Primary user actions are "
+        "inflection points that initiate motion, transforming the whole design.\n"
+        "All action takes place in a single environment. Objects are presented to the user without "
+        "breaking the continuity of experience even as they transform and reorganize.\n"
+        "Motion is meaningful and appropriate, serving to focus attention and maintain continuity. "
+        "Feedback is subtle yet clear. Transitions are efficient yet coherent.\n\n"
+
+        "3D world.\n\n"
+
+        "The material environment is a 3D space, which means all objects have x, y, and z "
+        "dimensions. The z-axis is perpendicularly aligned to the plane of the display, with the "
+        "positive z-axis extending towards the viewer. Every sheet of material occupies a single "
+        "position along the z-axis and has a standard 1dp thickness.\n"
+        "On the web, the z-axis is used for layering and not for perspective. The 3D world is "
+        "emulated by manipulating the y-axis.\n\n"
+
+        "Light and shadow.\n\n"
+
+        "Within the material environment, virtual lights illuminate the scene. Key lights create "
+        "directional shadows, while ambient light creates soft shadows from all angles.\n"
+        "Shadows in the material environment are cast by these two light sources. In Android "
+        "development, shadows occur when light sources are blocked by sheets of material at "
+        "various positions along the z-axis. On the web, shadows are depicted by manipulating the "
+        "y-axis only. The following example shows the card with a height of 6dp.\n\n"
+
+        "Resting elevation.\n\n"
+
+        "All material objects, regardless of size, have a resting elevation, or default elevation "
+        "that does not change. If an object changes elevation, it should return to its resting "
+        "elevation as soon as possible.\n\n"
+
+        "Component elevations.\n\n"
+
+        "The resting elevation for a component type is consistent across apps (e.g., FAB elevation "
+        "does not vary from 6dp in one app to 16dp in another app).\n"
+        "Components may have different resting elevations across platforms, depending on the depth "
+        "of the environment (e.g., TV has a greater depth than mobile or desktop).\n\n"
+
+        "Responsive elevation and dynamic elevation offsets.\n\n"
+
+        "Some component types have responsive elevation, meaning they change elevation in response "
+        "to user input (e.g., normal, focused, and pressed) or system events. These elevation "
+        "changes are consistently implemented using dynamic elevation offsets.\n"
+        "Dynamic elevation offsets are the goal elevation that a component moves towards, relative "
+        "to the component’s resting state. They ensure that elevation changes are consistent "
+        "across actions and component types. For example, all components that lift on press have "
+        "the same elevation change relative to their resting elevation.\n"
+        "Once the input event is completed or cancelled, the component will return to its resting "
+        "elevation.\n\n"
+
+        "Avoiding elevation interference.\n\n"
+
+        "Components with responsive elevations may encounter other components as they move between "
+        "their resting elevations and dynamic elevation offsets. Because material cannot pass "
+        "through other material, components avoid interfering with one another any number of ways, "
+        "whether on a per component basis or using the entire app layout.\n"
+        "On a component level, components can move or be removed before they cause interference. "
+        "For example, a floating action button (FAB) can disappear or move off screen before a "
+        "user picks up a card, or it can move if a snackbar appears.\n"
+        "On the layout level, design your app layout to minimize opportunities for interference. "
+        "For example, position the FAB to one side of stream of a cards so the FAB won’t interfere "
+        "when a user tries to pick up one of cards.\n\n"
+    </string>
+    <string name="pattern_has_errors">Pattern has errors</string>
+    <string name="account_name_is_empty">Account name missing</string>
+    <string name="pattern_is_empty">Pattern missing</string>
+    <string name="invalid_matching_group_number">Invalid matching group number</string>
+    <string name="pattern_name_label">Pattern name</string>
+    <string name="pattern_details_pattern_label">Pattern</string>
+    <string name="pattern_details_amount_label">Amount</string>
+    <string name="pattern_details_test_text_label">Test text</string>
+    <string name="pattern_details_account_name_label">Account name</string>
+    <string name="pattern_details_account_row_label">Transaction account details</string>
+    <string name="account_name_source_label">Account name source</string>
+    <string name="pattern_details_source_literal">literal</string>
+    <string name="account_comment_source_label">Account comment source</string>
+    <string name="account_amount_source_label">Amount source</string>
+    <string name="pattern_details_account_comment_label">Account comment</string>
+    <string name="pattern_details_account_amount_label">Amount</string>
+    <string name="choose_pattern_detail_source_label">Pattern match group</string>
+    <string name="missing_pattern_error">Missing pattern</string>
+    <string name="missing_test_text">Missing test text</string>
+    <string name="pattern_without_groups">Pattern has no capturing groups</string>
+    <string name="pattern_does_not_match">Pattern doesn\'t match the test text</string>
+    <string name="pattern_transaction_parameters_label">Transaction parameters</string>
+    <string name="pattern_transaction_description_hint">Transaction description</string>
+    <string name="pattern_transaction_comment_hint">Transaction comment</string>
+    <string name="transaction_description_source_label">Transaction description source</string>
+    <string name="transaction_comment_source_label">Transaction comment source</string>
+    <string name="pattern_details_date_label">Transaction date</string>
+    <string name="date_year_hint">year</string>
+    <string name="date_month_hint">month</string>
+    <string name="date_day_hint">date</string>
+    <string name="pattern_details_date_year_source_label">year</string>
+    <string name="pattern_details_date_day_source_label">date</string>
+    <string name="pattern_details_date_month_source_label">month</string>
+    <string name="unnamed_pattern">Pattern with no name</string>
+    <string name="add_button_description">Add pattern</string>
+    <string name="save_button_description">Save pattern</string>
 </resources>