]> git.ktnx.net Git - mobile-ledger.git/blobdiff - app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionModel.java
drop improper list copy
[mobile-ledger.git] / app / src / main / java / net / ktnx / mobileledger / ui / new_transaction / NewTransactionModel.java
index c84c26d20bf21b532ce6a4e982e688f8841b5a18..707bc089cdf14ef3918ad74af4f7ea118f165aae 100644 (file)
@@ -18,6 +18,8 @@
 package net.ktnx.mobileledger.ui.new_transaction;
 
 import android.annotation.SuppressLint;
+import android.os.Handler;
+import android.os.Looper;
 import android.text.TextUtils;
 
 import androidx.annotation.NonNull;
@@ -57,17 +59,17 @@ import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.regex.MatchResult;
 
-enum ItemType {generalData, transactionRow, bottomFiller}
+enum ItemType {generalData, transactionRow}
 
 enum FocusedElement {Account, Comment, Amount, Description, TransactionComment}
 
 
 public class NewTransactionModel extends ViewModel {
+    private static final int MIN_ITEMS = 3;
     private final MutableLiveData<Boolean> showCurrency = new MutableLiveData<>(false);
     private final MutableLiveData<Boolean> isSubmittable = new InertMutableLiveData<>(false);
     private final MutableLiveData<Boolean> showComments = new MutableLiveData<>(true);
     private final MutableLiveData<List<Item>> items = new MutableLiveData<>();
-    private final MutableLiveData<Integer> accountCount = new InertMutableLiveData<>(0);
     private final MutableLiveData<Boolean> simulateSave = new InertMutableLiveData<>(false);
     private final AtomicInteger busyCounter = new AtomicInteger(0);
     private final MutableLiveData<Boolean> busyFlag = new InertMutableLiveData<>(false);
@@ -92,8 +94,25 @@ public class NewTransactionModel extends ViewModel {
     }
     private void setItemsWithoutSubmittableChecks(@NonNull List<Item> list) {
         Logger.debug("new-trans", "model: Setting new item list");
+        final int cnt = list.size();
+        for (int i = 1; i < cnt - 1; i++) {
+            final TransactionAccount item = list.get(i)
+                                                .toTransactionAccount();
+            if (item.isLast) {
+                TransactionAccount replacement = new TransactionAccount(item);
+                replacement.isLast = false;
+                list.set(i, replacement);
+            }
+        }
+        final TransactionAccount last = list.get(cnt - 1)
+                                            .toTransactionAccount();
+        if (!last.isLast) {
+            TransactionAccount replacement = new TransactionAccount(last);
+            replacement.isLast = true;
+            list.set(cnt - 1, replacement);
+        }
+
         items.setValue(list);
-        accountCount.setValue(list.size() - 2);
     }
     private List<Item> copyList() {
         return copyList(null);
@@ -151,12 +170,14 @@ public class NewTransactionModel extends ViewModel {
         return this.isSubmittable;
     }
     void reset() {
+        Logger.debug("new-trans", "Resetting model");
         List<Item> list = new ArrayList<>();
         list.add(new TransactionHead(""));
         list.add(new TransactionAccount(""));
         list.add(new TransactionAccount(""));
-        list.add(new BottomFiller());
-        items.setValue(list);
+        noteFocusChanged(0, FocusedElement.Description);
+        isSubmittable.setValue(false);
+        setItemsWithoutSubmittableChecks(list);
     }
     boolean accountsInInitialState() {
         final List<Item> list = items.getValue();
@@ -225,7 +246,7 @@ public class NewTransactionModel extends ViewModel {
 
         newItems.add(head);
 
-        for (int i = 1; i < present.size() - 1; i++) {
+        for (int i = 1; i < present.size(); i++) {
             final TransactionAccount row = present.get(i)
                                                   .toTransactionAccount();
             if (!row.isEmpty())
@@ -260,9 +281,7 @@ public class NewTransactionModel extends ViewModel {
                   newItems.add(accRow);
               }
 
-              newItems.add(new BottomFiller());
-
-              items.postValue(newItems);
+              new Handler(Looper.getMainLooper()).post(() -> setItems(newItems));
           });
     }
     private int extractIntFromMatches(MatchResult m, Integer group, Integer literal) {
@@ -342,7 +361,20 @@ public class NewTransactionModel extends ViewModel {
             list.add(list.remove(index));
     }
     void toggleCurrencyVisible() {
-        showCurrency.setValue(!Objects.requireNonNull(showCurrency.getValue()));
+        final boolean newValue = !Objects.requireNonNull(showCurrency.getValue());
+
+        // remove currency from all items, or reset currency to the default
+        // no need to clone the list, because the removal of the currency won't lead to
+        // visual changes -- the currency fields will be hidden or reset to default anyway
+        // still, there may be changes in the submittable state
+        final List<Item> list = Objects.requireNonNull(this.items.getValue());
+        for (int i = 1; i < list.size(); i++) {
+            ((TransactionAccount) list.get(i)).setCurrency(newValue ? Data.getProfile()
+                                                                          .getDefaultCommodity()
+                                                                    : null);
+        }
+        checkTransactionSubmittable(null);
+        showCurrency.setValue(newValue);
     }
     void stopObservingBusyFlag(Observer<Boolean> observer) {
         busyFlag.removeObserver(observer);
@@ -373,7 +405,7 @@ public class NewTransactionModel extends ViewModel {
         tr.setComment(head.getComment());
         LedgerTransactionAccount emptyAmountAccount = null;
         float emptyAmountAccountBalance = 0;
-        for (int i = 1; i < list.size() - 1; i++) {
+        for (int i = 1; i < list.size(); i++) {
             TransactionAccount item = list.get(i)
                                           .toTransactionAccount();
             LedgerTransactionAccount acc = new LedgerTransactionAccount(item.getAccountName()
@@ -453,6 +485,8 @@ public class NewTransactionModel extends ViewModel {
             else
                 item.resetAmount();
         }
+        if (BuildConfig.DEBUG)
+            dumpItemList("Loaded previous transaction", newList);
 
         if (singleNegativeIndex != -1) {
             firstNegative.resetAmount();
@@ -463,11 +497,9 @@ public class NewTransactionModel extends ViewModel {
             moveItemLast(newList, singlePositiveIndex);
         }
 
-        noteFocusChanged(1, FocusedElement.Description);
-
-        newList.add(new BottomFiller());
-
         setItems(newList);
+
+        noteFocusChanged(1, FocusedElement.Amount);
     }
     /**
      * A transaction is submittable if:
@@ -522,7 +554,7 @@ public class NewTransactionModel extends ViewModel {
                 submittable = false;
             }
 
-            for (int i = 1; i < list.size() - 1; i++) {
+            for (int i = 1; i < list.size(); i++) {
                 TransactionAccount item = list.get(i)
                                               .toTransactionAccount();
 
@@ -589,7 +621,7 @@ public class NewTransactionModel extends ViewModel {
                 float currencyBalance = balance.get(balCurrency);
                 if (Misc.isZero(currencyBalance)) {
                     // remove hints from all amount inputs in that currency
-                    for (int i = 1; i < list.size() - 1; i++) {
+                    for (int i = 1; i < list.size(); i++) {
                         TransactionAccount acc = list.get(i)
                                                      .toTransactionAccount();
                         if (Misc.equalStrings(acc.getCurrency(), balCurrency)) {
@@ -598,17 +630,17 @@ public class NewTransactionModel extends ViewModel {
                                         String.format("Resetting hint of '%s' [%s]",
                                                 Misc.nullIsEmpty(acc.getAccountName()),
                                                 balCurrency));
-                            if (acc.amountHintIsSet && !TextUtils.isEmpty(acc.getAmountHint())) {
+                            // skip if the amount is set, in which case the hint is not
+                            // important/visible
+                            if (!acc.isAmountSet() && acc.amountHintIsSet &&
+                                !TextUtils.isEmpty(acc.getAmountHint()))
+                            {
                                 if (workingWithLiveList && !liveListCopied) {
                                     list = copyList(list);
                                     liveListCopied = true;
                                 }
                                 final TransactionAccount newAcc = new TransactionAccount(acc);
                                 newAcc.setAmountHint(null);
-                                if (!liveListCopied) {
-                                    list = copyList(list);
-                                    liveListCopied = true;
-                                }
                                 list.set(i, newAcc);
                                 listChanged = true;
                             }
@@ -729,7 +761,7 @@ public class NewTransactionModel extends ViewModel {
                     Logger.debug("submittable",
                             String.format("Adding new item with %s for currency %s",
                                     newAcc.getAmountHint(), balCurrency));
-                    list.add(list.size() - 1, newAcc);
+                    list.add(newAcc);
                     listChanged = true;
                 }
             }
@@ -737,18 +769,19 @@ public class NewTransactionModel extends ViewModel {
             // drop extra empty rows, not needed
             for (String currName : emptyRowsForCurrency.currencies()) {
                 List<Item> emptyItems = emptyRowsForCurrency.getList(currName);
-                while ((list.size() > 4) && (emptyItems.size() > 1)) {
+                while ((list.size() > MIN_ITEMS) && (emptyItems.size() > 1)) {
                     if (workingWithLiveList && !liveListCopied) {
                         list = copyList(list);
                         liveListCopied = true;
                     }
-                    Item item = emptyItems.remove(1);
-                    list.remove(item);
+                    // the list is a copy, so the empty item is no longer present
+                    Item itemToRemove = emptyItems.remove(1);
+                    removeItemById(list, itemToRemove.id);
                     listChanged = true;
                 }
 
                 // unused currency, remove last item (which is also an empty one)
-                if ((list.size() > 4) && (emptyItems.size() == 1)) {
+                if ((list.size() > MIN_ITEMS) && (emptyItems.size() == 1)) {
                     List<Item> currItems = itemsForCurrency.getList(currName);
 
                     if (currItems.size() == 1) {
@@ -756,8 +789,8 @@ public class NewTransactionModel extends ViewModel {
                             list = copyList(list);
                             liveListCopied = true;
                         }
-                        Item item = emptyItems.get(0);
-                        list.remove(item);
+                        // the list is a copy, so the empty item is no longer present
+                        removeItemById(list, emptyItems.get(0).id);
                         listChanged = true;
                     }
                 }
@@ -765,12 +798,12 @@ public class NewTransactionModel extends ViewModel {
 
             // 6) at least two rows need to be present in the ledger
             //    (the list also contains header and trailer)
-            while (list.size() < 4) {
+            while (list.size() < MIN_ITEMS) {
                 if (workingWithLiveList && !liveListCopied) {
                     list = copyList(list);
                     liveListCopied = true;
                 }
-                list.add(list.size() - 1, new TransactionAccount(""));
+                list.add(new TransactionAccount(""));
                 listChanged = true;
             }
 
@@ -795,10 +828,23 @@ public class NewTransactionModel extends ViewModel {
             setItemsWithoutSubmittableChecks(list);
         }
     }
+    private void removeItemById(@NotNull List<Item> list, int id) {
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
+            list.removeIf(item -> item.id == id);
+        }
+        else {
+            for (Item item : list) {
+                if (item.id == id) {
+                    list.remove(item);
+                    break;
+                }
+            }
+        }
+    }
     @SuppressLint("DefaultLocale")
     private void dumpItemList(@NotNull String msg, @NotNull List<Item> list) {
         Logger.debug("submittable", "== Dump of all items " + msg);
-        for (int i = 1; i < list.size() - 1; i++) {
+        for (int i = 1; i < list.size(); i++) {
             TransactionAccount item = list.get(i)
                                           .toTransactionAccount();
             Logger.debug("submittable", String.format("%d:%s", i, item.toString()));
@@ -820,9 +866,6 @@ public class NewTransactionModel extends ViewModel {
 
         setItems(newList);
     }
-    public LiveData<Integer> getAccountCount() {
-        return accountCount;
-    }
     public boolean accountListIsEmpty() {
         List<Item> items = Objects.requireNonNull(this.items.getValue());
 
@@ -859,8 +902,6 @@ public class NewTransactionModel extends ViewModel {
                 return new TransactionHead((TransactionHead) origin);
             if (origin instanceof TransactionAccount)
                 return new TransactionAccount((TransactionAccount) origin);
-            if (origin instanceof BottomFiller)
-                return new BottomFiller((BottomFiller) origin);
             throw new RuntimeException("Don't know how to handle " + origin);
         }
         public int getId() {
@@ -894,8 +935,6 @@ public class NewTransactionModel extends ViewModel {
                 return ((TransactionHead) item).equalContents((TransactionHead) this);
             if (this instanceof TransactionAccount)
                 return ((TransactionAccount) item).equalContents((TransactionAccount) this);
-            if (this instanceof BottomFiller)
-                return true;
 
             throw new RuntimeException("Don't know how to handle " + this);
         }
@@ -1001,26 +1040,6 @@ public class NewTransactionModel extends ViewModel {
         }
     }
 
-    public static class BottomFiller extends Item {
-        public BottomFiller(BottomFiller origin) {
-            id = origin.id;
-            // nothing to do
-        }
-        public BottomFiller() {
-            super();
-        }
-        @Override
-        public ItemType getType() {
-            return ItemType.bottomFiller;
-        }
-        @SuppressLint("DefaultLocale")
-        @NonNull
-        @Override
-        public String toString() {
-            return String.format("id:%d «bottom filler»", id);
-        }
-    }
-
     public static class TransactionAccount extends Item {
         private String accountName;
         private String amountHint;
@@ -1030,7 +1049,9 @@ public class NewTransactionModel extends ViewModel {
         private boolean amountSet;
         private boolean amountValid = true;
         private FocusedElement focusedElement = FocusedElement.Account;
-        private boolean amountHintIsSet = false;
+        private boolean amountHintIsSet = true;
+        private boolean isLast = false;
+        private int accountNameCursorPosition;
         public TransactionAccount(TransactionAccount origin) {
             id = origin.id;
             accountName = origin.accountName;
@@ -1042,6 +1063,8 @@ public class NewTransactionModel extends ViewModel {
             currency = origin.currency;
             amountValid = origin.amountValid;
             focusedElement = origin.focusedElement;
+            isLast = origin.isLast;
+            accountNameCursorPosition = origin.accountNameCursorPosition;
         }
         public TransactionAccount(LedgerTransactionAccount account) {
             super();
@@ -1057,6 +1080,9 @@ public class NewTransactionModel extends ViewModel {
             this.accountName = accountName;
             this.currency = currency;
         }
+        public boolean isLast() {
+            return isLast;
+        }
         public boolean isAmountSet() {
             return amountSet;
         }
@@ -1143,25 +1169,37 @@ public class NewTransactionModel extends ViewModel {
             if (!TextUtils.isEmpty(comment))
                 b.append(String.format(" /%s/", comment));
 
+            if (isLast)
+                b.append(" last");
+
             return b.toString();
         }
         public boolean equalContents(TransactionAccount other) {
             if (other == null)
                 return false;
 
-            boolean equal = TextUtils.equals(accountName, other.accountName) &&
-                            TextUtils.equals(comment, other.comment) &&
-                            (amountSet ? other.amountSet && amount == other.amount
-                                       : !other.amountSet) &&
-                            (amountHintIsSet ? other.amountHintIsSet &&
-                                               TextUtils.equals(amountHint, other.amountHint)
-                                             : !other.amountHintIsSet) &&
-                            TextUtils.equals(currency, other.currency);
+            boolean equal = TextUtils.equals(accountName, other.accountName);
+            equal = equal && TextUtils.equals(comment, other.comment) &&
+                    (amountSet ? other.amountSet && amount == other.amount : !other.amountSet);
+
+            // compare amount hint only if there is no amount
+            if (!amountSet)
+                equal = equal && (amountHintIsSet ? other.amountHintIsSet &&
+                                                    TextUtils.equals(amountHint, other.amountHint)
+                                                  : !other.amountHintIsSet);
+            equal = equal && TextUtils.equals(currency, other.currency) && isLast == other.isLast;
+
             Logger.debug("new-trans",
                     String.format("Comparing {%s} and {%s}: %s", this.toString(), other.toString(),
                             equal));
             return equal;
         }
+        public int getAccountNameCursorPosition() {
+            return accountNameCursorPosition;
+        }
+        public void setAccountNameCursorPosition(int position) {
+            this.accountNameCursorPosition = position;
+        }
     }
 
     private static class BalanceForCurrency {