X-Git-Url: https://git.ktnx.net/?p=mobile-ledger.git;a=blobdiff_plain;f=app%2Fsrc%2Fmain%2Fjava%2Fnet%2Fktnx%2Fmobileledger%2Fui%2Factivity%2FNewTransactionModel.java;h=3350664eac43c41a32d9032f8c3edbfee6696988;hp=0198b8696a12de0c3cbe929baa3a64461d43c8da;hb=217da55a224e2ae899d0b50604e2e54f882ec04f;hpb=18bc95ab692b0073bd1f2766850e8fc0841b8a9d diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionModel.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionModel.java index 0198b869..3350664e 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionModel.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionModel.java @@ -1,5 +1,5 @@ /* - * Copyright © 2019 Damyan Ivanov. + * 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 @@ -17,100 +17,114 @@ package net.ktnx.mobileledger.ui.activity; -import android.annotation.SuppressLint; - import androidx.annotation.NonNull; +import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModel; -import net.ktnx.mobileledger.BuildConfig; +import net.ktnx.mobileledger.model.Currency; +import net.ktnx.mobileledger.model.Data; import net.ktnx.mobileledger.model.LedgerTransactionAccount; -import net.ktnx.mobileledger.utils.Logger; -import net.ktnx.mobileledger.utils.Misc; +import net.ktnx.mobileledger.model.MobileLedgerProfile; +import net.ktnx.mobileledger.utils.Globals; +import net.ktnx.mobileledger.utils.SimpleDate; import org.jetbrains.annotations.NotNull; +import java.text.ParseException; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; -import java.util.Date; import java.util.GregorianCalendar; -import java.util.List; import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import static net.ktnx.mobileledger.utils.Logger.debug; +import java.util.concurrent.atomic.AtomicInteger; public class NewTransactionModel extends ViewModel { - static final Pattern reYMD = Pattern.compile("^\\s*(\\d+)\\d*/\\s*(\\d+)\\s*/\\s*(\\d+)\\s*$"); - static final Pattern reMD = Pattern.compile("^\\s*(\\d+)\\s*/\\s*(\\d+)\\s*$"); - static final Pattern reD = Pattern.compile("\\s*(\\d+)\\s*$"); + final MutableLiveData showCurrency = new MutableLiveData<>(false); + final ArrayList items = new ArrayList<>(); + final MutableLiveData isSubmittable = new MutableLiveData<>(false); + final MutableLiveData showComments = new MutableLiveData<>(true); private final Item header = new Item(this, null, ""); private final Item trailer = new Item(this); - private final ArrayList items = new ArrayList<>(); - private final MutableLiveData isSubmittable = new MutableLiveData<>(false); private final MutableLiveData focusedItem = new MutableLiveData<>(0); private final MutableLiveData accountCount = new MutableLiveData<>(0); private final MutableLiveData simulateSave = new MutableLiveData<>(false); - public boolean getSimulateSave() { + private final AtomicInteger busyCounter = new AtomicInteger(0); + private final MutableLiveData busyFlag = new MutableLiveData<>(false); + private boolean observingDataProfile; + private Observer profileObserver = profile -> { + showCurrency.postValue(profile.getShowCommodityByDefault()); + showComments.postValue(profile.getShowCommentsByDefault()); + }; + void observeShowComments(LifecycleOwner owner, Observer observer) { + showComments.observe(owner, observer); + } + void observeBusyFlag(@NonNull LifecycleOwner owner, Observer observer) { + busyFlag.observe(owner, observer); + } + void observeDataProfile(LifecycleOwner activity) { + if (!observingDataProfile) + Data.profile.observe(activity, profileObserver); + observingDataProfile = true; + } + boolean getSimulateSave() { return simulateSave.getValue(); } public void setSimulateSave(boolean simulateSave) { this.simulateSave.setValue(simulateSave); } - public void toggleSimulateSave() { + void toggleSimulateSave() { simulateSave.setValue(!simulateSave.getValue()); } - public void observeSimulateSave(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner, - @NonNull - androidx.lifecycle.Observer observer) { + void observeSimulateSave(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner, + @NonNull androidx.lifecycle.Observer observer) { this.simulateSave.observe(owner, observer); } - public int getAccountCount() { + int getAccountCount() { return items.size(); } - public Date getDate() { + public SimpleDate getDate() { return header.date.getValue(); } public String getDescription() { return header.description.getValue(); } - public LiveData isSubmittable() { + public String getComment() { + return header.comment.getValue(); + } + LiveData isSubmittable() { return this.isSubmittable; } void reset() { header.date.setValue(null); header.description.setValue(null); + header.comment.setValue(null); items.clear(); items.add(new Item(this, new LedgerTransactionAccount(""))); items.add(new Item(this, new LedgerTransactionAccount(""))); focusedItem.setValue(0); } - public void observeFocusedItem(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner, - @NonNull androidx.lifecycle.Observer observer) { + void observeFocusedItem(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner, + @NonNull androidx.lifecycle.Observer observer) { this.focusedItem.observe(owner, observer); } - public void stopObservingFocusedItem( - @NonNull androidx.lifecycle.Observer observer) { + void stopObservingFocusedItem(@NonNull androidx.lifecycle.Observer observer) { this.focusedItem.removeObserver(observer); } - public void observeAccountCount(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner, - @NonNull - androidx.lifecycle.Observer observer) { + void observeAccountCount(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner, + @NonNull androidx.lifecycle.Observer observer) { this.accountCount.observe(owner, observer); } - public void stopObservingAccountCount( - @NonNull androidx.lifecycle.Observer observer) { + void stopObservingAccountCount(@NonNull androidx.lifecycle.Observer observer) { this.accountCount.removeObserver(observer); } - public int getFocusedItem() { return focusedItem.getValue(); } - public void setFocusedItem(int position) { + int getFocusedItem() { return focusedItem.getValue(); } + void setFocusedItem(int position) { focusedItem.setValue(position); } - public int addAccount(LedgerTransactionAccount acc) { + int addAccount(LedgerTransactionAccount acc) { items.add(new Item(this, acc)); accountCount.setValue(getAccountCount()); return items.size(); @@ -132,7 +146,7 @@ public class NewTransactionModel extends ViewModel { return items.get(index) .getAccount(); } - public Item getItem(int index) { + Item getItem(int index) { if (index == 0) { return header; } @@ -142,177 +156,79 @@ public class NewTransactionModel extends ViewModel { return trailer; } - /* - A transaction is submittable if: - 0) has description - 1) has at least two account names - 2) each amount has account name - 3) amounts must balance to 0, or - 3a) there must be exactly one empty amount (with account) - 4) empty accounts with empty amounts are ignored - 5) a row with an empty account name or empty amount is guaranteed to exist - */ - @SuppressLint("DefaultLocale") - public void checkTransactionSubmittable(NewTransactionItemsAdapter adapter) { - int accounts = 0; - int amounts = 0; - int empty_rows = 0; - float balance = 0f; - final String descriptionText = getDescription(); - boolean submittable = true; - List itemsWithEmptyAmount = new ArrayList<>(); - List itemsWithAccountAndEmptyAmount = new ArrayList<>(); - - try { - if ((descriptionText == null) || descriptionText.trim() - .isEmpty()) - { - Logger.debug("submittable", "Transaction not submittable: missing description"); - submittable = false; - } - - for (int i = 0; i < this.items.size(); i++) { - Item item = this.items.get(i); - - LedgerTransactionAccount acc = item.getAccount(); - String acc_name = acc.getAccountName() - .trim(); - if (acc_name.isEmpty()) { - empty_rows++; - - if (acc.isAmountSet()) { - // 2) each amount has account name - Logger.debug("submittable", String.format( - "Transaction not submittable: row %d has no account name, but has" + - " amount %1.2f", i + 1, acc.getAmount())); - submittable = false; - } - } - else { - accounts++; - } - - if (acc.isAmountSet()) { - amounts++; - balance += acc.getAmount(); - } - else { - itemsWithEmptyAmount.add(item); - - if (!acc_name.isEmpty()) { - itemsWithAccountAndEmptyAmount.add(item); - } - } - } - - // 1) has at least two account names - if (accounts < 2) { - Logger.debug("submittable", - String.format("Transaction not submittable: only %d account names", - accounts)); - submittable = false; - } - - // 3) amount must balance to 0, or - // 3a) there must be exactly one empty amount (with account) - if (Misc.isZero(balance)) { - for (Item item : items) { - item.setAmountHint(null); - } - } - else { - int balanceReceiversCount = itemsWithAccountAndEmptyAmount.size(); - if (balanceReceiversCount != 1) { - Logger.debug("submittable", (balanceReceiversCount == 0) ? - "Transaction not submittable: non-zero balance " + - "with no empty amounts with accounts" : - "Transaction not submittable: non-zero balance " + - "with multiple empty amounts with accounts"); - submittable = false; - } - - // suggest off-balance amount to a row and remove hints on other rows - Item receiver = null; - if (!itemsWithAccountAndEmptyAmount.isEmpty()) - receiver = itemsWithAccountAndEmptyAmount.get(0); - else if (!itemsWithEmptyAmount.isEmpty()) - receiver = itemsWithEmptyAmount.get(0); - - for (Item item : items) { - if (item.equals(receiver)) { - Logger.debug("submittable", - String.format("Setting amount hint to %1.2f", -balance)); - item.setAmountHint(String.format("%1.2f", -balance)); - } - else - item.setAmountHint(null); - } - } - - // 5) a row with an empty account name or empty amount is guaranteed to exist - if ((empty_rows == 0) && - ((this.items.size() == accounts) || (this.items.size() == amounts))) - { - adapter.addRow(); - } - - - debug("submittable", submittable ? "YES" : "NO"); - isSubmittable.setValue(submittable); - - if (BuildConfig.DEBUG) { - debug("submittable", "== Dump of all items"); - for (int i = 0; i < items.size(); i++) { - Item item = items.get(i); - LedgerTransactionAccount acc = item.getAccount(); - debug("submittable", String.format("Item %2d: [%4.2f] %s (%s)", i, - acc.isAmountSet() ? acc.getAmount() : 0, acc.getAccountName(), - acc.getComment())); - } - } - } - catch (NumberFormatException e) { - debug("submittable", "NO (because of NumberFormatException)"); - isSubmittable.setValue(false); - } - catch (Exception e) { - e.printStackTrace(); - debug("submittable", "NO (because of an Exception)"); - isSubmittable.setValue(false); + void removeRow(Item item, NewTransactionItemsAdapter adapter) { + int pos = items.indexOf(item); + items.remove(pos); + if (adapter != null) { + adapter.notifyItemRemoved(pos + 1); + sendCountNotifications(); } } - public void removeItem(int pos) { + void removeItem(int pos) { items.remove(pos); accountCount.setValue(getAccountCount()); } - public void sendCountNotifications() { + void sendCountNotifications() { accountCount.setValue(getAccountCount()); } public void sendFocusedNotification() { focusedItem.setValue(focusedItem.getValue()); } - public void updateFocusedItem(int position) { + void updateFocusedItem(int position) { focusedItem.setValue(position); } - public void noteFocusChanged(int position, FocusedElement element) { + void noteFocusChanged(int position, FocusedElement element) { getItem(position).setFocusedElement(element); } - public void swapItems(int one, int two) { + void swapItems(int one, int two) { Collections.swap(items, one - 1, two - 1); } - public void toggleComment(int position) { - final MutableLiveData commentVisible = getItem(position).commentVisible; - commentVisible.postValue(!commentVisible.getValue()); + void moveItemLast(int index) { + /* 0 + 1 <-- index + 2 + 3 <-- desired position + */ + int itemCount = items.size(); + + if (index < itemCount - 1) { + Item acc = items.remove(index); + items.add(itemCount - 1, acc); + } + } + void toggleCurrencyVisible() { + showCurrency.setValue(!showCurrency.getValue()); + } + void stopObservingBusyFlag(Observer observer) { + busyFlag.removeObserver(observer); + } + void incrementBusyCounter() { + int newValue = busyCounter.incrementAndGet(); + if (newValue == 1) + busyFlag.postValue(true); + } + void decrementBusyCounter() { + int newValue = busyCounter.decrementAndGet(); + if (newValue == 0) + busyFlag.postValue(false); + } + public boolean getBusyFlag() { + return busyFlag.getValue(); + } + public void toggleShowComments() { + showComments.setValue(!showComments.getValue()); } enum ItemType {generalData, transactionRow, bottomFiller} + enum FocusedElement {Account, Comment, Amount, Description, TransactionComment} + + //========================================================================================== - enum FocusedElement {Account, Comment, Amount} - class Item { + static class Item { private ItemType type; - private MutableLiveData date = new MutableLiveData<>(); + private MutableLiveData date = new MutableLiveData<>(); private MutableLiveData description = new MutableLiveData<>(); private LedgerTransactionAccount account; private MutableLiveData amountHint = new MutableLiveData<>(null); @@ -320,35 +236,42 @@ public class NewTransactionModel extends ViewModel { private MutableLiveData editable = new MutableLiveData<>(true); private FocusedElement focusedElement = FocusedElement.Account; private MutableLiveData comment = new MutableLiveData<>(null); - private MutableLiveData commentVisible = new MutableLiveData<>(false); - public Item(NewTransactionModel model) { + private MutableLiveData currency = new MutableLiveData<>(null); + private MutableLiveData amountValid = new MutableLiveData<>(true); + private boolean amountHintIsSet = false; + Item(NewTransactionModel model) { this.model = model; type = ItemType.bottomFiller; editable.setValue(false); } - public Item(NewTransactionModel model, Date date, String description) { + Item(NewTransactionModel model, SimpleDate date, String description) { this.model = model; this.type = ItemType.generalData; this.date.setValue(date); this.description.setValue(description); this.editable.setValue(true); } - public Item(NewTransactionModel model, LedgerTransactionAccount account) { + Item(NewTransactionModel model, LedgerTransactionAccount account) { this.model = model; this.type = ItemType.transactionRow; this.account = account; + String currName = account.getCurrency(); + Currency curr = null; + if ((currName != null) && !currName.isEmpty()) + curr = Currency.loadByName(currName); + this.currency.setValue(curr); this.editable.setValue(true); } - public FocusedElement getFocusedElement() { + FocusedElement getFocusedElement() { return focusedElement; } - public void setFocusedElement(FocusedElement focusedElement) { + void setFocusedElement(FocusedElement focusedElement) { this.focusedElement = focusedElement; } public NewTransactionModel getModel() { return model; } - public void setEditable(boolean editable) { + void setEditable(boolean editable) { ensureType(ItemType.generalData, ItemType.transactionRow); this.editable.setValue(editable); } @@ -359,96 +282,69 @@ public class NewTransactionModel extends ViewModel { type1, type2)); } } - public String getAmountHint() { + String getAmountHint() { ensureType(ItemType.transactionRow); return amountHint.getValue(); } - public void setAmountHint(String amountHint) { + void setAmountHint(String amountHint) { ensureType(ItemType.transactionRow); // avoid unnecessary triggers if (amountHint == null) { if (this.amountHint.getValue() == null) return; + amountHintIsSet = false; } else { if (amountHint.equals(this.amountHint.getValue())) return; + amountHintIsSet = true; } this.amountHint.setValue(amountHint); } - public void observeAmountHint(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner, - @NonNull - androidx.lifecycle.Observer observer) { + void observeAmountHint(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner, + @NonNull androidx.lifecycle.Observer observer) { this.amountHint.observe(owner, observer); } - public void stopObservingAmountHint( + void stopObservingAmountHint( @NonNull androidx.lifecycle.Observer observer) { this.amountHint.removeObserver(observer); } - public ItemType getType() { + ItemType getType() { return type; } - public void ensureType(ItemType wantedType) { + void ensureType(ItemType wantedType) { if (type != wantedType) { throw new RuntimeException( String.format("Actual type (%s) differs from wanted (%s)", type, wantedType)); } } - public Date getDate() { + public SimpleDate getDate() { ensureType(ItemType.generalData); return date.getValue(); } - public void setDate(Date date) { + public void setDate(SimpleDate date) { ensureType(ItemType.generalData); this.date.setValue(date); } - public void setDate(String text) { + public void setDate(String text) throws ParseException { if ((text == null) || text.trim() .isEmpty()) { - setDate((Date) null); + setDate((SimpleDate) null); return; } - int year, month, day; - final Calendar c = GregorianCalendar.getInstance(); - Matcher m = reYMD.matcher(text); - if (m.matches()) { - year = Integer.parseInt(m.group(1)); - month = Integer.parseInt(m.group(2)) - 1; // month is 0-based - day = Integer.parseInt(m.group(3)); - } - else { - year = c.get(Calendar.YEAR); - m = reMD.matcher(text); - if (m.matches()) { - month = Integer.parseInt(m.group(1)) - 1; - day = Integer.parseInt(m.group(2)); - } - else { - month = c.get(Calendar.MONTH); - m = reD.matcher(text); - if (m.matches()) { - day = Integer.parseInt(m.group(1)); - } - else { - day = c.get(Calendar.DAY_OF_MONTH); - } - } - } - - c.set(year, month, day); - - this.setDate(c.getTime()); + SimpleDate date = Globals.parseLedgerDate(text); + this.setDate(date); } - public void observeDate(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner, - @NonNull androidx.lifecycle.Observer observer) { + void observeDate(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner, + @NonNull androidx.lifecycle.Observer observer) { this.date.observe(owner, observer); } - public void stopObservingDate(@NonNull androidx.lifecycle.Observer observer) { + void stopObservingDate(@NonNull androidx.lifecycle.Observer observer) { this.date.removeObserver(observer); } public String getDescription() { @@ -459,15 +355,30 @@ public class NewTransactionModel extends ViewModel { ensureType(ItemType.generalData); this.description.setValue(description); } - public void observeDescription(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner, - @NonNull - androidx.lifecycle.Observer observer) { + void observeDescription(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner, + @NonNull androidx.lifecycle.Observer observer) { this.description.observe(owner, observer); } - public void stopObservingDescription( + void stopObservingDescription( @NonNull androidx.lifecycle.Observer observer) { this.description.removeObserver(observer); } + public String getTransactionComment() { + ensureType(ItemType.generalData); + return comment.getValue(); + } + public void setTransactionComment(String transactionComment) { + ensureType(ItemType.generalData); + this.comment.setValue(transactionComment); + } + void observeTransactionComment(@NonNull @NotNull LifecycleOwner owner, + @NonNull Observer observer) { + ensureType(ItemType.generalData); + this.comment.observe(owner, observer); + } + void stopObservingTransactionComment(@NonNull Observer observer) { + this.comment.removeObserver(observer); + } public LedgerTransactionAccount getAccount() { ensureType(ItemType.transactionRow); return account; @@ -480,55 +391,78 @@ public class NewTransactionModel extends ViewModel { * * @return nicely formatted, shortest available date representation */ - public String getFormattedDate() { + String getFormattedDate() { if (date == null) return null; - Date time = date.getValue(); - if (time == null) + SimpleDate d = date.getValue(); + if (d == null) return null; - Calendar c = GregorianCalendar.getInstance(); - c.setTime(time); Calendar today = GregorianCalendar.getInstance(); - final int myYear = c.get(Calendar.YEAR); - final int myMonth = c.get(Calendar.MONTH); - final int myDay = c.get(Calendar.DAY_OF_MONTH); - - if (today.get(Calendar.YEAR) != myYear) { - return String.format(Locale.US, "%d/%02d/%02d", myYear, myMonth + 1, myDay); + if (today.get(Calendar.YEAR) != d.year) { + return String.format(Locale.US, "%d/%02d/%02d", d.year, d.month, d.day); } - if (today.get(Calendar.MONTH) != myMonth) { - return String.format(Locale.US, "%d/%02d", myMonth + 1, myDay); + if (today.get(Calendar.MONTH) != d.month - 1) { + return String.format(Locale.US, "%d/%02d", d.month, d.day); } - return String.valueOf(myDay); + return String.valueOf(d.day); } - public void observeEditableFlag(NewTransactionActivity activity, - Observer observer) { + void observeEditableFlag(NewTransactionActivity activity, Observer observer) { editable.observe(activity, observer); } - public void stopObservingEditableFlag(Observer observer) { + void stopObservingEditableFlag(Observer observer) { editable.removeObserver(observer); } - public void observeCommentVisible(NewTransactionActivity activity, - Observer observer) { - commentVisible.observe(activity, observer); - } - public void stopObservingCommentVisible(Observer observer) { - commentVisible.removeObserver(observer); - } - public void observeComment(NewTransactionActivity activity, - Observer observer) { + void observeComment(NewTransactionActivity activity, Observer observer) { comment.observe(activity, observer); } - public void stopObservingComment(Observer observer) { + void stopObservingComment(Observer observer) { comment.removeObserver(observer); } public void setComment(String comment) { getAccount().setComment(comment); this.comment.postValue(comment); } + public Currency getCurrency() { + return this.currency.getValue(); + } + public void setCurrency(Currency currency) { + Currency present = this.currency.getValue(); + if ((currency == null) && (present != null) || + (currency != null) && !currency.equals(present)) + { + getAccount().setCurrency((currency != null && !currency.getName() + .isEmpty()) + ? currency.getName() : null); + this.currency.setValue(currency); + } + } + void observeCurrency(NewTransactionActivity activity, Observer observer) { + currency.observe(activity, observer); + } + void stopObservingCurrency(Observer observer) { + currency.removeObserver(observer); + } + boolean isOfType(ItemType type) { + return this.type == type; + } + boolean isAmountHintSet() { + return amountHintIsSet; + } + void validateAmount() { + amountValid.setValue(true); + } + void invalidateAmount() { + amountValid.setValue(false); + } + void observeAmountValidity(NewTransactionActivity activity, Observer observer) { + amountValid.observe(activity, observer); + } + void stopObservingAmountValidity(Observer observer) { + amountValid.removeObserver(observer); + } } }