}
}
- public void getTemplateWitAccountsAsync(@NonNull Long id, @NonNull
+ public void getTemplateWithAccountsAsync(@NonNull Long id, @NonNull
AsyncResultCallback<TemplateWithAccounts> callback) {
LiveData<TemplateWithAccounts> resultReceiver = getTemplateWithAccounts(id);
resultReceiver.observeForever(new Observer<TemplateWithAccounts>() {
import androidx.annotation.NonNull;
import net.ktnx.mobileledger.dao.AccountDAO;
+import net.ktnx.mobileledger.model.MobileLedgerProfile;
import net.ktnx.mobileledger.utils.Logger;
import java.util.ArrayList;
public AccountAutocompleteAdapter(Context context) {
super(context, android.R.layout.simple_dropdown_item_1line, new ArrayList<>());
}
+ public AccountAutocompleteAdapter(Context context, @NonNull MobileLedgerProfile profile) {
+ this(context);
+ profileUUID = profile.getUuid();
+ }
public void setProfileUUID(String profileUUID) {
this.profileUUID = profileUUID;
}
--- /dev/null
+/*
+ * 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 net.ktnx.mobileledger.db.TemplateHeader;
+
+import java.util.regex.MatchResult;
+
+public class MatchedTemplate {
+ public TemplateHeader templateHead;
+ public MatchResult matchResult;
+ public MatchedTemplate(TemplateHeader templateHead, MatchResult matchResult) {
+ this.templateHead = templateHead;
+ this.matchResult = matchResult;
+ }
+}
package net.ktnx.mobileledger.ui.new_transaction;
+import android.content.Intent;
+import android.database.AbstractCursor;
+import android.database.Cursor;
import android.os.Bundle;
+import android.os.ParcelFormatException;
+import android.text.TextUtils;
import android.util.TypedValue;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
-import androidx.appcompat.widget.Toolbar;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavController;
import androidx.navigation.fragment.NavHostFragment;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import com.google.android.material.snackbar.Snackbar;
+
import net.ktnx.mobileledger.BuildConfig;
import net.ktnx.mobileledger.R;
import net.ktnx.mobileledger.async.AsyncCrasher;
+import net.ktnx.mobileledger.async.DescriptionSelectedCallback;
import net.ktnx.mobileledger.async.SendTransactionTask;
import net.ktnx.mobileledger.async.TaskCallback;
+import net.ktnx.mobileledger.databinding.ActivityNewTransactionBinding;
+import net.ktnx.mobileledger.db.DB;
+import net.ktnx.mobileledger.db.TemplateHeader;
import net.ktnx.mobileledger.model.Data;
import net.ktnx.mobileledger.model.LedgerTransaction;
+import net.ktnx.mobileledger.model.MatchedTemplate;
+import net.ktnx.mobileledger.ui.QR;
import net.ktnx.mobileledger.ui.activity.ProfileThemedActivity;
+import net.ktnx.mobileledger.ui.templates.TemplatesActivity;
+import net.ktnx.mobileledger.utils.Logger;
+import net.ktnx.mobileledger.utils.MLDB;
+import net.ktnx.mobileledger.utils.Misc;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import static net.ktnx.mobileledger.utils.Logger.debug;
-public class NewTransactionActivity extends ProfileThemedActivity implements TaskCallback,
- NewTransactionFragment.OnNewTransactionFragmentInteractionListener {
+public class NewTransactionActivity extends ProfileThemedActivity
+ implements TaskCallback, NewTransactionFragment.OnNewTransactionFragmentInteractionListener,
+ QR.QRScanTrigger, QR.QRScanResultReceiver, DescriptionSelectedCallback {
private NavController navController;
private NewTransactionModel model;
+ private ActivityResultLauncher<Void> qrScanLauncher;
+ private ActivityNewTransactionBinding b;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_new_transaction);
- Toolbar toolbar = findViewById(R.id.toolbar);
- setSupportActionBar(toolbar);
+ b = ActivityNewTransactionBinding.inflate(getLayoutInflater(), null, false);
+ setContentView(b.getRoot());
+ setSupportActionBar(b.toolbar);
Data.observeProfile(this,
- mobileLedgerProfile -> toolbar.setSubtitle(mobileLedgerProfile.getName()));
+ mobileLedgerProfile -> b.toolbar.setSubtitle(mobileLedgerProfile.getName()));
NavHostFragment navHostFragment = (NavHostFragment) Objects.requireNonNull(
getSupportFragmentManager().findFragmentById(R.id.new_transaction_nav));
.setDisplayHomeAsUpEnabled(true);
model = new ViewModelProvider(this).get(NewTransactionModel.class);
+
+ qrScanLauncher = QR.registerLauncher(this, this);
+
+ model.isSubmittable()
+ .observe(this, isSubmittable -> {
+ if (isSubmittable) {
+ b.fabAdd.show();
+ }
+ else {
+ b.fabAdd.hide();
+ }
+ });
+// viewModel.checkTransactionSubmittable(listAdapter);
+
+ b.fabAdd.setOnClickListener(v -> onFabPressed());
+
+
}
@Override
protected void initProfile() {
try {
SendTransactionTask saver =
- new SendTransactionTask(this, mProfile, model.getSimulateSave());
+ new SendTransactionTask(this, mProfile, model.getSimulateSaveFlag());
saver.execute(tr);
}
catch (Exception e) {
.setVisible(true);
}
- model.observeSimulateSave(this, state -> {
- menu.findItem(R.id.action_simulate_save)
- .setChecked(state);
- findViewById(R.id.simulationLabel).setVisibility(state ? View.VISIBLE : View.GONE);
- });
+ model.getSimulateSave()
+ .observe(this, state -> {
+ menu.findItem(R.id.action_simulate_save)
+ .setChecked(state);
+ b.simulationLabel.setVisibility(state ? View.VISIBLE : View.GONE);
+ });
return true;
}
model.toggleSimulateSave();
}
+ @Override
+ public void triggerQRScan() {
+ qrScanLauncher.launch(null);
+ }
+ private void startNewPatternActivity(String scanned) {
+ Intent intent = new Intent(this, TemplatesActivity.class);
+ Bundle args = new Bundle();
+ args.putString(TemplatesActivity.ARG_ADD_TEMPLATE, scanned);
+ startActivity(intent, args);
+ }
+ private void alertNoTemplateMatch(String scanned) {
+ MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
+ builder.setCancelable(true)
+ .setMessage(R.string.no_template_matches)
+ .setPositiveButton(R.string.add_button,
+ (dialog, which) -> startNewPatternActivity(scanned))
+ .create()
+ .show();
+ }
+ public void onQRScanResult(String text) {
+ Logger.debug("qr", String.format("Got QR scan result [%s]", text));
+
+ if (Misc.emptyIsNull(text) == null)
+ return;
+
+ LiveData<List<TemplateHeader>> allTemplates = DB.get()
+ .getTemplateDAO()
+ .getTemplates();
+ allTemplates.observe(this, templateHeaders -> {
+ ArrayList<MatchedTemplate> matchingFallbackTemplates = new ArrayList<>();
+ ArrayList<MatchedTemplate> matchingTemplates = new ArrayList<>();
+
+ for (TemplateHeader ph : templateHeaders) {
+ String patternSource = ph.getRegularExpression();
+ if (Misc.emptyIsNull(patternSource) == null)
+ continue;
+ try {
+ Pattern pattern = Pattern.compile(patternSource);
+ Matcher matcher = pattern.matcher(text);
+ if (!matcher.matches())
+ continue;
+
+ Logger.debug("pattern",
+ String.format("Pattern '%s' [%s] matches '%s'", ph.getName(),
+ patternSource, text));
+ if (ph.isFallback())
+ matchingFallbackTemplates.add(
+ new MatchedTemplate(ph, matcher.toMatchResult()));
+ else
+ matchingTemplates.add(new MatchedTemplate(ph, matcher.toMatchResult()));
+ }
+ catch (ParcelFormatException e) {
+ // ignored
+ Logger.debug("pattern",
+ String.format("Error compiling regular expression '%s'", patternSource),
+ e);
+ }
+ }
+
+ if (matchingTemplates.isEmpty())
+ matchingTemplates = matchingFallbackTemplates;
+
+ if (matchingTemplates.isEmpty())
+ alertNoTemplateMatch(text);
+ else if (matchingTemplates.size() == 1)
+ model.applyTemplate(matchingTemplates.get(0), text);
+ else
+ chooseTemplate(matchingTemplates, text);
+ });
+ }
+ private void chooseTemplate(ArrayList<MatchedTemplate> matchingTemplates, String matchedText) {
+ final String templateNameColumn = "name";
+ AbstractCursor cursor = new AbstractCursor() {
+ @Override
+ public int getCount() {
+ return matchingTemplates.size();
+ }
+ @Override
+ public String[] getColumnNames() {
+ return new String[]{"_id", templateNameColumn};
+ }
+ @Override
+ public String getString(int column) {
+ if (column == 0)
+ return String.valueOf(getPosition());
+ return matchingTemplates.get(getPosition()).templateHead.getName();
+ }
+ @Override
+ public short getShort(int column) {
+ if (column == 0)
+ return (short) getPosition();
+ return -1;
+ }
+ @Override
+ public int getInt(int column) {
+ return getShort(column);
+ }
+ @Override
+ public long getLong(int column) {
+ return getShort(column);
+ }
+ @Override
+ public float getFloat(int column) {
+ return getShort(column);
+ }
+ @Override
+ public double getDouble(int column) {
+ return getShort(column);
+ }
+ @Override
+ public boolean isNull(int column) {
+ return false;
+ }
+ @Override
+ public int getColumnCount() {
+ return 2;
+ }
+ };
+
+ MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
+ builder.setCancelable(true)
+ .setTitle(R.string.choose_template_to_apply)
+ .setIcon(R.drawable.ic_baseline_auto_graph_24)
+ .setSingleChoiceItems(cursor, -1, templateNameColumn, (dialog, which) -> {
+ model.applyTemplate(matchingTemplates.get(which), matchedText);
+ dialog.dismiss();
+ })
+ .create()
+ .show();
+ }
+ public void descriptionSelected(String description) {
+ debug("description selected", description);
+ if (!model.accountListIsEmpty())
+ return;
+
+ String accFilter = mProfile.getPreferredAccountsFilter();
+
+ ArrayList<String> params = new ArrayList<>();
+ StringBuilder sb = new StringBuilder("select t.profile, t.id from transactions t");
+
+ if (!TextUtils.isEmpty(accFilter)) {
+ sb.append(" JOIN transaction_accounts ta")
+ .append(" ON ta.profile = t.profile")
+ .append(" AND ta.transaction_id = t.id");
+ }
+
+ sb.append(" WHERE t.description=?");
+ params.add(description);
+
+ if (!TextUtils.isEmpty(accFilter)) {
+ sb.append(" AND ta.account_name LIKE '%'||?||'%'");
+ params.add(accFilter);
+ }
+
+ sb.append(" ORDER BY t.year desc, t.month desc, t.day desc LIMIT 1");
+
+ final String sql = sb.toString();
+ debug("description", sql);
+ debug("description", params.toString());
+
+ // FIXME: handle exceptions?
+ MLDB.queryInBackground(sql, params.toArray(new String[]{}), new MLDB.CallbackHelper() {
+ @Override
+ public void onStart() {
+ model.incrementBusyCounter();
+ }
+ @Override
+ public void onDone() {
+ model.decrementBusyCounter();
+ }
+ @Override
+ public boolean onRow(@NonNull Cursor cursor) {
+ final String profileUUID = cursor.getString(0);
+ final int transactionId = cursor.getInt(1);
+ runOnUiThread(() -> model.loadTransactionIntoModel(profileUUID, transactionId));
+ return false; // limit 1, by the way
+ }
+ @Override
+ public void onNoRows() {
+ if (TextUtils.isEmpty(accFilter))
+ return;
+
+ debug("description", "Trying transaction search without preferred account filter");
+
+ final String broaderSql =
+ "select t.profile, t.id from transactions t where t.description=?" +
+ " ORDER BY year desc, month desc, day desc LIMIT 1";
+ params.remove(1);
+ debug("description", broaderSql);
+ debug("description", description);
+
+ runOnUiThread(() -> Snackbar.make(b.newTransactionNav,
+ R.string.ignoring_preferred_account, Snackbar.LENGTH_INDEFINITE)
+ .show());
+
+ MLDB.queryInBackground(broaderSql, new String[]{description},
+ new MLDB.CallbackHelper() {
+ @Override
+ public void onStart() {
+ model.incrementBusyCounter();
+ }
+ @Override
+ public boolean onRow(@NonNull Cursor cursor) {
+ final String profileUUID = cursor.getString(0);
+ final int transactionId = cursor.getInt(1);
+ runOnUiThread(() -> model.loadTransactionIntoModel(profileUUID,
+ transactionId));
+ return false;
+ }
+ @Override
+ public void onDone() {
+ model.decrementBusyCounter();
+ }
+ });
+ }
+ });
+ }
+ private void onFabPressed() {
+ b.fabAdd.hide();
+ Misc.hideSoftKeyboard(this);
+
+ LedgerTransaction tr = model.constructLedgerTransaction();
+
+ onTransactionSave(tr);
+ }
}
package net.ktnx.mobileledger.ui.new_transaction;
import android.content.Context;
-import android.content.Intent;
import android.content.res.Resources;
-import android.database.AbstractCursor;
import android.os.Bundle;
-import android.os.ParcelFormatException;
import android.renderscript.RSInvalidStateException;
import android.view.LayoutInflater;
import android.view.Menu;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
-import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-import com.google.android.material.floatingactionbutton.FloatingActionButton;
-import com.google.android.material.snackbar.BaseTransientBottomBar;
import com.google.android.material.snackbar.Snackbar;
import net.ktnx.mobileledger.R;
-import net.ktnx.mobileledger.db.DB;
-import net.ktnx.mobileledger.db.TemplateAccount;
-import net.ktnx.mobileledger.db.TemplateHeader;
import net.ktnx.mobileledger.json.API;
import net.ktnx.mobileledger.model.Data;
import net.ktnx.mobileledger.model.LedgerTransaction;
-import net.ktnx.mobileledger.model.LedgerTransactionAccount;
import net.ktnx.mobileledger.model.MobileLedgerProfile;
-import net.ktnx.mobileledger.ui.QRScanCapableFragment;
-import net.ktnx.mobileledger.ui.templates.TemplatesActivity;
+import net.ktnx.mobileledger.ui.QR;
import net.ktnx.mobileledger.utils.Logger;
-import net.ktnx.mobileledger.utils.Misc;
-import net.ktnx.mobileledger.utils.SimpleDate;
import org.jetbrains.annotations.NotNull;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Locale;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
/**
* A simple {@link Fragment} subclass.
* Activities that contain this fragment must implement the
// TODO: offer to undo account remove-on-swipe
-public class NewTransactionFragment extends QRScanCapableFragment {
+public class NewTransactionFragment extends Fragment {
private NewTransactionItemsAdapter listAdapter;
private NewTransactionModel viewModel;
- private FloatingActionButton fab;
private OnNewTransactionFragmentInteractionListener mListener;
private MobileLedgerProfile mProfile;
public NewTransactionFragment() {
// Required empty public constructor
setHasOptionsMenu(true);
}
- private void startNewPatternActivity(String scanned) {
- Intent intent = new Intent(requireContext(), TemplatesActivity.class);
- Bundle args = new Bundle();
- args.putString(TemplatesActivity.ARG_ADD_TEMPLATE, scanned);
- requireContext().startActivity(intent, args);
- }
- private void alertNoTemplateMatch(String scanned) {
- MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext());
- builder.setCancelable(true)
- .setMessage(R.string.no_template_matches)
- .setPositiveButton(R.string.add_button,
- (dialog, which) -> startNewPatternActivity(scanned))
- .create()
- .show();
- }
- protected void onQrScanned(String text) {
- Logger.debug("qr", String.format("Got QR scan result [%s]", text));
-
- if (Misc.emptyIsNull(text) == null)
- return;
-
- LiveData<List<TemplateHeader>> allTemplates = DB.get()
- .getTemplateDAO()
- .getTemplates();
- allTemplates.observe(getViewLifecycleOwner(), templateHeaders -> {
- ArrayList<TemplateHeader> matchingFallbackTemplates = new ArrayList<>();
- ArrayList<TemplateHeader> matchingTemplates = new ArrayList<>();
-
- for (TemplateHeader ph : templateHeaders) {
- String patternSource = ph.getRegularExpression();
- if (Misc.emptyIsNull(patternSource) == null)
- continue;
- try {
- Pattern pattern = Pattern.compile(patternSource);
- Matcher matcher = pattern.matcher(text);
- if (!matcher.matches())
- continue;
-
- Logger.debug("pattern",
- String.format("Pattern '%s' [%s] matches '%s'", ph.getName(),
- patternSource, text));
- if (ph.isFallback())
- matchingFallbackTemplates.add(ph);
- else
- matchingTemplates.add(ph);
- }
- catch (ParcelFormatException e) {
- // ignored
- Logger.debug("pattern",
- String.format("Error compiling regular expression '%s'", patternSource),
- e);
- }
- }
-
- if (matchingTemplates.isEmpty())
- matchingTemplates = matchingFallbackTemplates;
-
- if (matchingTemplates.isEmpty())
- alertNoTemplateMatch(text);
- else if (matchingTemplates.size() == 1)
- applyTemplate(matchingTemplates.get(0), text);
- else
- chooseTemplate(matchingTemplates, text);
- });
- }
- private void chooseTemplate(ArrayList<TemplateHeader> matchingTemplates, String matchedText) {
- final String templateNameColumn = "name";
- AbstractCursor cursor = new AbstractCursor() {
- @Override
- public int getCount() {
- return matchingTemplates.size();
- }
- @Override
- public String[] getColumnNames() {
- return new String[]{"_id", templateNameColumn};
- }
- @Override
- public String getString(int column) {
- if (column == 0)
- return String.valueOf(getPosition());
- return matchingTemplates.get(getPosition())
- .getName();
- }
- @Override
- public short getShort(int column) {
- if (column == 0)
- return (short) getPosition();
- return -1;
- }
- @Override
- public int getInt(int column) {
- return getShort(column);
- }
- @Override
- public long getLong(int column) {
- return getShort(column);
- }
- @Override
- public float getFloat(int column) {
- return getShort(column);
- }
- @Override
- public double getDouble(int column) {
- return getShort(column);
- }
- @Override
- public boolean isNull(int column) {
- return false;
- }
- @Override
- public int getColumnCount() {
- return 2;
- }
- };
-
- MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext());
- builder.setCancelable(true)
- .setTitle(R.string.choose_template_to_apply)
- .setIcon(R.drawable.ic_baseline_auto_graph_24)
- .setSingleChoiceItems(cursor, -1, templateNameColumn, (dialog, which) -> {
- applyTemplate(matchingTemplates.get(which), matchedText);
- dialog.dismiss();
- })
- .create()
- .show();
- }
- private void applyTemplate(TemplateHeader patternHeader, String text) {
- Pattern pattern = Pattern.compile(patternHeader.getRegularExpression());
-
- Matcher m = pattern.matcher(text);
-
- if (!m.matches()) {
- Snackbar.make(requireView(), R.string.pattern_does_not_match,
- BaseTransientBottomBar.LENGTH_INDEFINITE)
- .show();
- return;
- }
-
- SimpleDate transactionDate;
- {
- int day = extractIntFromMatches(m, patternHeader.getDateDayMatchGroup(),
- patternHeader.getDateDay());
- int month = extractIntFromMatches(m, patternHeader.getDateMonthMatchGroup(),
- patternHeader.getDateMonth());
- int year = extractIntFromMatches(m, patternHeader.getDateYearMatchGroup(),
- patternHeader.getDateYear());
-
- SimpleDate today = SimpleDate.today();
- if (year <= 0)
- year = today.year;
- if (month <= 0)
- month = today.month;
- if (day <= 0)
- day = today.day;
-
- transactionDate = new SimpleDate(year, month, day);
-
- Logger.debug("pattern", "setting transaction date to " + transactionDate);
- }
-
- NewTransactionModel.Item head = viewModel.getItem(0);
- head.ensureType(NewTransactionModel.ItemType.generalData);
- final String transactionDescription =
- extractStringFromMatches(m, patternHeader.getTransactionDescriptionMatchGroup(),
- patternHeader.getTransactionDescription());
- head.setDescription(transactionDescription);
- Logger.debug("pattern", "Setting transaction description to " + transactionDescription);
- final String transactionComment =
- extractStringFromMatches(m, patternHeader.getTransactionCommentMatchGroup(),
- patternHeader.getTransactionComment());
- head.setTransactionComment(transactionComment);
- Logger.debug("pattern", "Setting transaction comment to " + transactionComment);
- head.setDate(transactionDate);
- listAdapter.notifyItemChanged(0);
-
- DB.get()
- .getTemplateDAO()
- .getTemplateWithAccounts(patternHeader.getId())
- .observe(getViewLifecycleOwner(), entry -> {
- int rowIndex = 0;
- final boolean accountsInInitialState = viewModel.accountsInInitialState();
- for (TemplateAccount acc : entry.accounts) {
- rowIndex++;
-
- String accountName = extractStringFromMatches(m, acc.getAccountNameMatchGroup(),
- acc.getAccountName());
- String accountComment =
- extractStringFromMatches(m, acc.getAccountCommentMatchGroup(),
- acc.getAccountComment());
- Float amount =
- extractFloatFromMatches(m, acc.getAmountMatchGroup(), acc.getAmount());
- if (amount != null && acc.getNegateAmount() != null && acc.getNegateAmount())
- amount = -amount;
-
- if (accountsInInitialState) {
- NewTransactionModel.Item item = viewModel.getItem(rowIndex);
- if (item == null) {
- Logger.debug("pattern", String.format(Locale.US,
- "Adding new account item [%s][c:%s][a:%s]", accountName,
- accountComment, amount));
- final LedgerTransactionAccount ledgerAccount =
- new LedgerTransactionAccount(accountName);
- ledgerAccount.setComment(accountComment);
- if (amount != null)
- ledgerAccount.setAmount(amount);
- // TODO currency
- viewModel.addAccount(ledgerAccount);
- listAdapter.notifyItemInserted(viewModel.items.size() - 1);
- }
- else {
- Logger.debug("pattern", String.format(Locale.US,
- "Stamping account item #%d [%s][c:%s][a:%s]", rowIndex,
- accountName, accountComment, amount));
-
- item.setAccountName(accountName);
- item.setComment(accountComment);
- if (amount != null)
- item.getAccount()
- .setAmount(amount);
-
- listAdapter.notifyItemChanged(rowIndex);
- }
- }
- else {
- final LedgerTransactionAccount transactionAccount =
- new LedgerTransactionAccount(accountName);
- transactionAccount.setComment(accountComment);
- if (amount != null)
- transactionAccount.setAmount(amount);
- // TODO currency
- Logger.debug("pattern", String.format(Locale.US,
- "Adding trailing account item [%s][c:%s][a:%s]", accountName,
- accountComment, amount));
-
- viewModel.addAccount(transactionAccount);
- listAdapter.notifyItemInserted(viewModel.items.size() - 1);
- }
- }
-
- listAdapter.checkTransactionSubmittable();
- });
- }
- private int extractIntFromMatches(Matcher m, Integer group, Integer literal) {
- if (literal != null)
- return literal;
-
- if (group != null) {
- int grp = group;
- if (grp > 0 & grp <= m.groupCount())
- try {
- return Integer.parseInt(m.group(grp));
- }
- catch (NumberFormatException e) {
- Snackbar.make(requireView(),
- "Error extracting transaction date: " + e.getMessage(),
- BaseTransientBottomBar.LENGTH_INDEFINITE)
- .show();
- }
- }
-
- return 0;
- }
- private String extractStringFromMatches(Matcher m, Integer group, String literal) {
- if (literal != null)
- return literal;
-
- if (group != null) {
- int grp = group;
- if (grp > 0 & grp <= m.groupCount())
- return m.group(grp);
- }
-
- return null;
- }
- private Float extractFloatFromMatches(Matcher m, Integer group, Float literal) {
- if (literal != null)
- return literal;
-
- if (group != null) {
- int grp = group;
- if (grp > 0 & grp <= m.groupCount())
- try {
- return Float.valueOf(m.group(grp));
- }
- catch (NumberFormatException e) {
- Snackbar.make(requireView(),
- "Error extracting transaction amount: " + e.getMessage(),
- BaseTransientBottomBar.LENGTH_INDEFINITE)
- .show();
- }
- }
-
- return null;
- }
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
menu.findItem(R.id.action_reset_new_transaction_activity)
.setOnMenuItemClickListener(item -> {
- listAdapter.reset();
+ viewModel.reset();
return true;
});
return true;
});
if (activity != null)
- viewModel.showCurrency.observe(activity, toggleCurrencyItem::setChecked);
+ viewModel.getShowCurrency()
+ .observe(activity, toggleCurrencyItem::setChecked);
final MenuItem toggleCommentsItem = menu.findItem(R.id.toggle_comments);
toggleCommentsItem.setOnMenuItemClickListener(item -> {
return true;
});
if (activity != null)
- viewModel.showComments.observe(activity, toggleCommentsItem::setChecked);
+ viewModel.getShowComments()
+ .observe(activity, toggleCommentsItem::setChecked);
}
private boolean onScanQrAction(MenuItem item) {
try {
- scanQrLauncher.launch(null);
+ Context ctx = requireContext();
+ if (ctx instanceof QR.QRScanTrigger)
+ ((QR.QRScanTrigger) ctx).triggerQRScan();
}
catch (Exception e) {
Logger.debug("qr", "Error launching QR scanner", e);
mProfile = Data.getProfile();
listAdapter = new NewTransactionItemsAdapter(viewModel, mProfile);
+ viewModel.getItems()
+ .observe(getViewLifecycleOwner(), newList -> listAdapter.setItems(newList));
+
RecyclerView list = activity.findViewById(R.id.new_transaction_accounts);
list.setAdapter(listAdapter);
list.setLayoutManager(new LinearLayoutManager(activity));
mProfile = profile;
listAdapter.setProfile(profile);
});
- listAdapter.notifyDataSetChanged();
- viewModel.isSubmittable()
- .observe(getViewLifecycleOwner(), isSubmittable -> {
- if (isSubmittable) {
- if (fab != null) {
- fab.show();
- }
- }
- else {
- if (fab != null) {
- fab.hide();
- }
- }
- });
-// viewModel.checkTransactionSubmittable(listAdapter);
-
- fab = activity.findViewById(R.id.fabAdd);
- fab.setOnClickListener(v -> onFabPressed());
-
boolean keep = false;
Bundle args = getArguments();
}
int focused = 0;
+ FocusedElement element = null;
if (savedInstanceState != null) {
keep |= savedInstanceState.getBoolean("keep", true);
- focused = savedInstanceState.getInt("focused", 0);
+ focused = savedInstanceState.getInt("focused-item", 0);
+ element = FocusedElement.valueOf(savedInstanceState.getString("focused-element"));
}
if (!keep)
viewModel.reset();
else {
- viewModel.setFocusedItem(focused);
+ viewModel.noteFocusChanged(focused, element);
}
ProgressBar p = activity.findViewById(R.id.progressBar);
- viewModel.observeBusyFlag(getViewLifecycleOwner(), isBusy -> {
- if (isBusy) {
+ viewModel.getBusyFlag()
+ .observe(getViewLifecycleOwner(), isBusy -> {
+ if (isBusy) {
// Handler h = new Handler();
// h.postDelayed(() -> {
// if (viewModel.getBusyFlag())
// p.setVisibility(View.VISIBLE);
//
// }, 10);
- p.setVisibility(View.VISIBLE);
- }
- else
- p.setVisibility(View.INVISIBLE);
- });
+ p.setVisibility(View.VISIBLE);
+ }
+ else
+ p.setVisibility(View.INVISIBLE);
+ });
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean("keep", true);
- final int focusedItem = viewModel.getFocusedItem();
- outState.putInt("focused", focusedItem);
- }
- private void onFabPressed() {
- fab.hide();
- Misc.hideSoftKeyboard(this);
- if (mListener != null) {
- SimpleDate date = viewModel.getDate();
- LedgerTransaction tr =
- new LedgerTransaction(null, date, viewModel.getDescription(), mProfile);
-
- tr.setComment(viewModel.getComment());
- LedgerTransactionAccount emptyAmountAccount = null;
- float emptyAmountAccountBalance = 0;
- for (int i = 0; i < viewModel.getAccountCount(); i++) {
- LedgerTransactionAccount acc =
- new LedgerTransactionAccount(viewModel.getAccount(i));
- if (acc.getAccountName()
- .trim()
- .isEmpty())
- continue;
-
- if (acc.isAmountSet()) {
- emptyAmountAccountBalance += acc.getAmount();
- }
- else {
- emptyAmountAccount = acc;
- }
-
- tr.addAccount(acc);
- }
-
- if (emptyAmountAccount != null)
- emptyAmountAccount.setAmount(-emptyAmountAccountBalance);
-
- mListener.onTransactionSave(tr);
- }
+ final NewTransactionModel.FocusInfo focusInfo = viewModel.getFocusInfo()
+ .getValue();
+ final int focusedItem = focusInfo.position;
+ if (focusedItem >= 0)
+ outState.putInt("focused-item", focusedItem);
+ outState.putString("focused-element", focusInfo.element.toString());
}
@Override
import android.view.Gravity;
import android.view.View;
import android.view.inputmethod.EditorInfo;
+import android.widget.EditText;
import android.widget.TextView;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
-import androidx.appcompat.app.AppCompatActivity;
import androidx.constraintlayout.widget.ConstraintLayout;
-import androidx.lifecycle.Observer;
import androidx.recyclerview.widget.RecyclerView;
import net.ktnx.mobileledger.R;
import net.ktnx.mobileledger.async.DescriptionSelectedCallback;
import net.ktnx.mobileledger.databinding.NewTransactionRowBinding;
+import net.ktnx.mobileledger.db.AccountAutocompleteAdapter;
import net.ktnx.mobileledger.model.Currency;
import net.ktnx.mobileledger.model.Data;
-import net.ktnx.mobileledger.model.LedgerTransactionAccount;
import net.ktnx.mobileledger.model.MobileLedgerProfile;
import net.ktnx.mobileledger.ui.CurrencySelectorFragment;
import net.ktnx.mobileledger.ui.DatePickerFragment;
import java.text.DecimalFormatSymbols;
import java.text.ParseException;
-import java.util.Date;
-import java.util.Locale;
-
-import static net.ktnx.mobileledger.ui.new_transaction.NewTransactionModel.ItemType;
+import java.util.Objects;
class NewTransactionItemHolder extends RecyclerView.ViewHolder
implements DatePickerFragment.DatePickedListener, DescriptionSelectedCallback {
private final String decimalDot;
- private final Observer<Boolean> showCommentsObserver;
private final MobileLedgerProfile mProfile;
- private final Observer<SimpleDate> dateObserver;
- private final Observer<String> descriptionObserver;
- private final Observer<String> transactionCommentObserver;
- private final Observer<String> hintObserver;
- private final Observer<Integer> focusedAccountObserver;
- private final Observer<Integer> accountCountObserver;
- private final Observer<Boolean> editableObserver;
- private final Observer<Currency.Position> currencyPositionObserver;
- private final Observer<Boolean> currencyGapObserver;
- private final Observer<Locale> localeObserver;
- private final Observer<Currency> currencyObserver;
- private final Observer<Boolean> showCurrencyObserver;
- private final Observer<String> commentObserver;
- private final Observer<Boolean> amountValidityObserver;
private final NewTransactionRowBinding b;
+ private final NewTransactionItemsAdapter mAdapter;
+ private boolean ignoreFocusChanges = false;
private String decimalSeparator;
- private NewTransactionModel.Item item;
- private Date date;
private boolean inUpdate = false;
private boolean syncingData = false;
- //TODO multiple amounts with different currencies per posting
+ //TODO multiple amounts with different currencies per posting?
NewTransactionItemHolder(@NonNull NewTransactionRowBinding b,
NewTransactionItemsAdapter adapter) {
super(b.getRoot());
this.b = b;
+ this.mAdapter = adapter;
new TextViewClearHelper().attachToTextView(b.comment);
b.newTransactionDescription.setNextFocusForwardId(View.NO_ID);
syncingData = true;
try {
final int pos = getAdapterPosition();
- adapter.updateFocusedItem(pos);
if (id == R.id.account_row_acc_name) {
adapter.noteFocusIsOnAccount(pos);
}
else if (id == R.id.new_transaction_description) {
adapter.noteFocusIsOnDescription(pos);
}
+ else
+ throw new IllegalStateException("Where is the focus?");
}
finally {
syncingData = wasSyncing;
b.comment.setOnFocusChangeListener(focusMonitor);
b.transactionComment.setOnFocusChangeListener(focusMonitor);
- MLDB.hookAutocompletionAdapter(b.getRoot()
- .getContext(), b.newTransactionDescription,
- MLDB.DESCRIPTION_HISTORY_TABLE, "description", false, adapter, mProfile);
- MLDB.hookAutocompletionAdapter(b.getRoot()
- .getContext(), b.accountRowAccName, MLDB.ACCOUNTS_TABLE,
- "name", true, this, mProfile);
+ NewTransactionActivity activity = (NewTransactionActivity) b.getRoot()
+ .getContext();
+
+ MLDB.hookAutocompletionAdapter(activity, b.newTransactionDescription,
+ MLDB.DESCRIPTION_HISTORY_TABLE, "description", false, activity, mProfile);
+ b.accountRowAccName.setAdapter(new AccountAutocompleteAdapter(b.getRoot()
+ .getContext(), mProfile));
- decimalSeparator = String.valueOf(DecimalFormatSymbols.getInstance()
- .getMonetaryDecimalSeparator());
- localeObserver = locale -> decimalSeparator = String.valueOf(
+ decimalSeparator = "";
+ Data.locale.observe(activity, locale -> decimalSeparator = String.valueOf(
DecimalFormatSymbols.getInstance(locale)
- .getMonetaryDecimalSeparator());
+ .getMonetaryDecimalSeparator()));
decimalDot = ".";
syncData();
Logger.debug("textWatcher",
"syncData() returned, checking if transaction is submittable");
- adapter.checkTransactionSubmittable();
+ adapter.model.checkTransactionSubmittable(null);
Logger.debug("textWatcher", "done");
}
};
final TextWatcher amountWatcher = new TextWatcher() {
@Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
- Logger.debug("num",
- String.format(Locale.US, "beforeTextChanged: start=%d, count=%d, after=%d",
- start, count, after));
- }
+ 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) {
+ checkAmountValid(s.toString());
if (syncData())
- adapter.checkTransactionSubmittable();
+ adapter.model.checkTransactionSubmittable(null);
}
};
b.newTransactionDescription.addTextChangedListener(tw);
- b.transactionComment.addTextChangedListener(tw);
+ monitorComment(b.transactionComment);
b.accountRowAccName.addTextChangedListener(tw);
- b.comment.addTextChangedListener(tw);
+ monitorComment(b.comment);
b.accountRowAccAmounts.addTextChangedListener(amountWatcher);
b.currencyButton.setOnClickListener(v -> {
CurrencySelectorFragment cpf = new CurrencySelectorFragment();
cpf.showPositionAndPadding();
- cpf.setOnCurrencySelectedListener(c -> item.setCurrency(c));
- final AppCompatActivity activity = (AppCompatActivity) v.getContext();
+ cpf.setOnCurrencySelectedListener(
+ c -> adapter.setItemCurrency(getAdapterPosition(), c.getName()));
cpf.show(activity.getSupportFragmentManager(), "currency-selector");
});
- dateObserver = date -> {
- if (syncingData)
- return;
- syncingData = true;
- try {
- b.newTransactionDate.setText(item.getFormattedDate());
- }
- finally {
- syncingData = false;
- }
- };
- descriptionObserver = description -> {
- if (syncingData)
- return;
- syncingData = true;
- try {
- b.newTransactionDescription.setText(description);
- }
- finally {
- syncingData = false;
- }
- };
- transactionCommentObserver = transactionComment -> {
- final View focusedView = b.transactionComment.findFocus();
- b.transactionComment.setTypeface(null,
- (focusedView == b.transactionComment) ? Typeface.NORMAL : Typeface.ITALIC);
- b.transactionComment.setVisibility(
- ((focusedView != b.transactionComment) && TextUtils.isEmpty(transactionComment))
- ? View.INVISIBLE : View.VISIBLE);
-
- };
- hintObserver = hint -> {
- if (syncingData)
- return;
- syncingData = true;
- try {
- if (hint == null)
- b.accountRowAccAmounts.setHint(R.string.zero_amount);
- else
- b.accountRowAccAmounts.setHint(hint);
- }
- finally {
- syncingData = false;
- }
- };
- editableObserver = this::setEditable;
commentFocusChanged(b.transactionComment, false);
commentFocusChanged(b.comment, false);
- focusedAccountObserver = index -> {
- if ((index == null) || !index.equals(getAdapterPosition()) || itemView.hasFocus())
- return;
-
- switch (item.getType()) {
- case generalData:
- // bad idea - double pop-up, and not really necessary.
- // the user can tap the input to get the calendar
- //if (!tvDate.hasFocus()) tvDate.requestFocus();
- switch (item.getFocusedElement()) {
- case TransactionComment:
- b.transactionComment.setVisibility(View.VISIBLE);
- b.transactionComment.requestFocus();
- break;
- case Description:
- boolean focused = b.newTransactionDescription.requestFocus();
-// tvDescription.dismissDropDown();
- if (focused)
- Misc.showSoftKeyboard((NewTransactionActivity) b.getRoot()
- .getContext());
- break;
- }
- break;
- case transactionRow:
- switch (item.getFocusedElement()) {
- case Amount:
- b.accountRowAccAmounts.requestFocus();
- break;
- case Comment:
- b.comment.setVisibility(View.VISIBLE);
- b.comment.requestFocus();
- break;
- case Account:
- boolean focused = b.accountRowAccName.requestFocus();
- b.accountRowAccName.dismissDropDown();
- if (focused)
- Misc.showSoftKeyboard((NewTransactionActivity) b.getRoot()
- .getContext());
- break;
- }
-
- break;
- }
- };
- accountCountObserver = count -> {
- final int adapterPosition = getAdapterPosition();
- final int layoutPosition = getLayoutPosition();
- Logger.debug("holder",
- String.format(Locale.US, "count=%d; pos=%d, layoutPos=%d [%s]", count,
- adapterPosition, layoutPosition, item.getType()
- .toString()
- .concat(item.getType() ==
- ItemType.transactionRow
- ? String.format(Locale.US,
- "'%s'=%s",
- item.getAccount()
- .getAccountName(),
- item.getAccount()
- .isAmountSet()
- ? String.format(Locale.US,
- "%.2f",
- item.getAccount()
- .getAmount())
- : "unset") : "")));
- if (adapterPosition == count)
- b.accountRowAccAmounts.setImeOptions(EditorInfo.IME_ACTION_DONE);
- else
- b.accountRowAccAmounts.setImeOptions(EditorInfo.IME_ACTION_NEXT);
- };
- currencyObserver = currency -> {
- setCurrency(currency);
- adapter.checkTransactionSubmittable();
- };
-
- currencyGapObserver =
+ adapter.model.getFocusInfo()
+ .observe(activity, focusInfo -> {
+ if (ignoreFocusChanges) {
+ Logger.debug("new-trans", "Ignoring focus change");
+ return;
+ }
+ ignoreFocusChanges = true;
+ try {
+ if (((focusInfo == null) ||
+ focusInfo.position != getAdapterPosition()) ||
+ itemView.hasFocus())
+ return;
+
+ NewTransactionModel.Item item = getItem();
+ if (item instanceof NewTransactionModel.TransactionHead) {
+ NewTransactionModel.TransactionHead head =
+ item.toTransactionHead();
+ // bad idea - double pop-up, and not really necessary.
+ // the user can tap the input to get the calendar
+ //if (!tvDate.hasFocus()) tvDate.requestFocus();
+ switch (focusInfo.element) {
+ case TransactionComment:
+ b.transactionComment.setVisibility(View.VISIBLE);
+ b.transactionComment.requestFocus();
+ break;
+ case Description:
+ boolean focused =
+ b.newTransactionDescription.requestFocus();
+// tvDescription.dismissDropDown();
+ if (focused)
+ Misc.showSoftKeyboard(
+ (NewTransactionActivity) b.getRoot()
+ .getContext());
+ break;
+ }
+ }
+ else if (item instanceof NewTransactionModel.TransactionAccount) {
+ NewTransactionModel.TransactionAccount acc =
+ item.toTransactionAccount();
+ switch (focusInfo.element) {
+ case Amount:
+ b.accountRowAccAmounts.requestFocus();
+ break;
+ case Comment:
+ b.comment.setVisibility(View.VISIBLE);
+ b.comment.requestFocus();
+ break;
+ case Account:
+ boolean focused = b.accountRowAccName.requestFocus();
+// b.accountRowAccName.dismissDropDown();
+ if (focused)
+ Misc.showSoftKeyboard(
+ (NewTransactionActivity) b.getRoot()
+ .getContext());
+ break;
+ }
+ }
+ }
+ finally {
+ ignoreFocusChanges = false;
+ }
+ });
+ adapter.model.getAccountCount()
+ .observe(activity, count -> {
+ final int adapterPosition = getAdapterPosition();
+ final int layoutPosition = getLayoutPosition();
+
+ if (adapterPosition == count)
+ b.accountRowAccAmounts.setImeOptions(EditorInfo.IME_ACTION_DONE);
+ else
+ b.accountRowAccAmounts.setImeOptions(EditorInfo.IME_ACTION_NEXT);
+ });
+
+ Data.currencyGap.observe(activity,
hasGap -> updateCurrencyPositionAndPadding(Data.currencySymbolPosition.getValue(),
- hasGap);
-
- currencyPositionObserver =
- position -> updateCurrencyPositionAndPadding(position, Data.currencyGap.getValue());
-
- showCurrencyObserver = showCurrency -> {
- if (showCurrency) {
- b.currency.setVisibility(View.VISIBLE);
- b.currencyButton.setVisibility(View.VISIBLE);
- String defaultCommodity = mProfile.getDefaultCommodity();
- item.setCurrency(
- (defaultCommodity == null) ? null : Currency.loadByName(defaultCommodity));
- }
- else {
- b.currency.setVisibility(View.GONE);
- b.currencyButton.setVisibility(View.GONE);
- item.setCurrency(null);
+ hasGap));
+
+ Data.currencySymbolPosition.observe(activity,
+ position -> updateCurrencyPositionAndPadding(position,
+ Data.currencyGap.getValue()));
+
+ adapter.model.getShowCurrency()
+ .observe(activity, showCurrency -> {
+ if (showCurrency) {
+ b.currency.setVisibility(View.VISIBLE);
+ b.currencyButton.setVisibility(View.VISIBLE);
+ b.currency.setText(mProfile.getDefaultCommodity());
+ }
+ else {
+ b.currency.setVisibility(View.GONE);
+ b.currencyButton.setVisibility(View.GONE);
+ b.currency.setText(null);
+ }
+ });
+
+ adapter.model.getShowComments()
+ .observe(activity, show -> {
+ ConstraintLayout.LayoutParams amountLayoutParams =
+ (ConstraintLayout.LayoutParams) b.amountLayout.getLayoutParams();
+ ConstraintLayout.LayoutParams accountParams =
+ (ConstraintLayout.LayoutParams) b.accountRowAccName.getLayoutParams();
+
+ if (show) {
+ accountParams.endToStart = ConstraintLayout.LayoutParams.UNSET;
+ accountParams.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID;
+
+ amountLayoutParams.topToTop = ConstraintLayout.LayoutParams.UNSET;
+ amountLayoutParams.topToBottom = b.accountRowAccName.getId();
+
+ b.commentLayout.setVisibility(View.VISIBLE);
+ }
+ else {
+ accountParams.endToStart = b.amountLayout.getId();
+ accountParams.endToEnd = ConstraintLayout.LayoutParams.UNSET;
+
+ amountLayoutParams.topToBottom = ConstraintLayout.LayoutParams.UNSET;
+ amountLayoutParams.topToTop = ConstraintLayout.LayoutParams.PARENT_ID;
+
+ b.commentLayout.setVisibility(View.GONE);
+ }
+
+ b.accountRowAccName.setLayoutParams(accountParams);
+ b.amountLayout.setLayoutParams(amountLayoutParams);
+
+ b.transactionCommentLayout.setVisibility(show ? View.VISIBLE : View.GONE);
+ });
+ }
+ public void checkAmountValid(String s) {
+ boolean valid = true;
+ try {
+ if (s.length() > 0) {
+ float ignored = Float.parseFloat(s.replace(decimalSeparator, decimalDot));
}
- };
-
- commentObserver = comment -> {
- final View focusedView = b.comment.findFocus();
- b.comment.setTypeface(null,
- (focusedView == b.comment) ? Typeface.NORMAL : Typeface.ITALIC);
- b.comment.setVisibility(
- ((focusedView != b.comment) && TextUtils.isEmpty(comment)) ? View.INVISIBLE
- : View.VISIBLE);
- };
-
- showCommentsObserver = show -> {
- ConstraintLayout.LayoutParams amountLayoutParams =
- (ConstraintLayout.LayoutParams) b.amountLayout.getLayoutParams();
- ConstraintLayout.LayoutParams accountParams =
- (ConstraintLayout.LayoutParams) b.accountRowAccName.getLayoutParams();
- if (show) {
- accountParams.endToStart = ConstraintLayout.LayoutParams.UNSET;
- accountParams.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID;
-
- amountLayoutParams.topToTop = ConstraintLayout.LayoutParams.UNSET;
- amountLayoutParams.topToBottom = b.accountRowAccName.getId();
+ }
+ catch (NumberFormatException ex) {
+ valid = false;
+ }
- b.commentLayout.setVisibility(View.VISIBLE);
+ displayAmountValidity(valid);
+ }
+ private void displayAmountValidity(boolean valid) {
+ b.accountRowAccAmounts.setCompoundDrawablesRelativeWithIntrinsicBounds(
+ valid ? 0 : R.drawable.ic_error_outline_black_24dp, 0, 0, 0);
+ b.accountRowAccAmounts.setMinEms(valid ? 4 : 5);
+ }
+ private void monitorComment(EditText editText) {
+ editText.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
- else {
- accountParams.endToStart = b.amountLayout.getId();
- accountParams.endToEnd = ConstraintLayout.LayoutParams.UNSET;
-
- amountLayoutParams.topToBottom = ConstraintLayout.LayoutParams.UNSET;
- amountLayoutParams.topToTop = ConstraintLayout.LayoutParams.PARENT_ID;
-
- b.commentLayout.setVisibility(View.GONE);
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
}
+ @Override
+ public void afterTextChanged(Editable s) {
+// debug("input", "text changed");
+ if (inUpdate)
+ return;
- b.accountRowAccName.setLayoutParams(accountParams);
- b.amountLayout.setLayoutParams(amountLayoutParams);
-
- b.transactionCommentLayout.setVisibility(show ? View.VISIBLE : View.GONE);
- };
-
- amountValidityObserver = valid -> {
- b.accountRowAccAmounts.setCompoundDrawablesRelativeWithIntrinsicBounds(
- valid ? 0 : R.drawable.ic_error_outline_black_24dp, 0, 0, 0);
- b.accountRowAccAmounts.setMinEms(valid ? 4 : 5);
- };
+ Logger.debug("textWatcher", "calling syncData()");
+ syncData();
+ Logger.debug("textWatcher",
+ "syncData() returned, checking if transaction is submittable");
+ styleComment(editText, s.toString());
+ Logger.debug("textWatcher", "done");
+ }
+ });
}
private void commentFocusChanged(TextView textView, boolean hasFocus) {
@ColorInt int textColor;
* checked for being submittable
*/
private boolean syncData() {
- if (item == null)
- return false;
-
if (syncingData) {
Logger.debug("new-trans", "skipping syncData() loop");
return false;
}
+ NewTransactionModel.Item item = getItem();
+
syncingData = true;
try {
- switch (item.getType()) {
- case generalData:
- item.setDate(String.valueOf(b.newTransactionDate.getText()));
- item.setDescription(String.valueOf(b.newTransactionDescription.getText()));
- item.setTransactionComment(String.valueOf(b.transactionComment.getText()));
- break;
- case transactionRow:
- final LedgerTransactionAccount account = item.getAccount();
- account.setAccountName(String.valueOf(b.accountRowAccName.getText()));
-
- item.setComment(String.valueOf(b.comment.getText()));
-
- String amount = String.valueOf(b.accountRowAccAmounts.getText());
- amount = amount.trim();
-
- if (amount.isEmpty()) {
- account.resetAmount();
- item.validateAmount();
+ if (item instanceof NewTransactionModel.TransactionHead) {
+ NewTransactionModel.TransactionHead head = item.toTransactionHead();
+
+ head.setDate(String.valueOf(b.newTransactionDate.getText()));
+ head.setDescription(String.valueOf(b.newTransactionDescription.getText()));
+ head.setComment(String.valueOf(b.transactionComment.getText()));
+ }
+ else if (item instanceof NewTransactionModel.TransactionAccount) {
+ NewTransactionModel.TransactionAccount acc = item.toTransactionAccount();
+ acc.setAccountName(String.valueOf(b.accountRowAccName.getText()));
+
+ acc.setComment(String.valueOf(b.comment.getText()));
+
+ String amount = String.valueOf(b.accountRowAccAmounts.getText());
+ amount = amount.trim();
+
+ if (amount.isEmpty()) {
+ acc.resetAmount();
+ acc.setAmountValid(true);
+ }
+ else {
+ try {
+ amount = amount.replace(decimalSeparator, decimalDot);
+ acc.setAmount(Float.parseFloat(amount));
+ acc.setAmountValid(true);
}
- else {
- try {
- amount = amount.replace(decimalSeparator, decimalDot);
- account.setAmount(Float.parseFloat(amount));
- item.validateAmount();
- }
- catch (NumberFormatException e) {
- Logger.debug("new-trans", String.format(
- "assuming amount is not set due to number format exception. " +
- "input was '%s'", amount));
- account.invalidateAmount();
- item.invalidateAmount();
- }
- final String curr = String.valueOf(b.currency.getText());
- if (curr.equals(b.currency.getContext()
- .getResources()
- .getString(R.string.currency_symbol)) ||
- curr.isEmpty())
- account.setCurrency(null);
- else
- account.setCurrency(curr);
+ catch (NumberFormatException e) {
+ Logger.debug("new-trans", String.format(
+ "assuming amount is not set due to number format exception. " +
+ "input was '%s'", amount));
+ acc.setAmountValid(false);
}
-
- break;
- case bottomFiller:
- throw new RuntimeException("Should not happen");
+ final String curr = String.valueOf(b.currency.getText());
+ if (curr.equals(b.currency.getContext()
+ .getResources()
+ .getString(R.string.currency_symbol)) ||
+ curr.isEmpty())
+ acc.setCurrency(null);
+ else
+ acc.setCurrency(curr);
+ }
+ }
+ else {
+ throw new RuntimeException("Should not happen");
}
return true;
.getContext()).getSupportFragmentManager(), null);
}
/**
- * setData
+ * bind
*
* @param item updates the UI elements with the data from the model item
*/
@SuppressLint("DefaultLocale")
- public void setData(NewTransactionModel.Item item) {
+ public void bind(@NonNull NewTransactionModel.Item item) {
beginUpdates();
try {
- if (this.item != null && !this.item.equals(item)) {
- this.item.stopObservingDate(dateObserver);
- this.item.stopObservingDescription(descriptionObserver);
- this.item.stopObservingTransactionComment(transactionCommentObserver);
- this.item.stopObservingAmountHint(hintObserver);
- this.item.stopObservingEditableFlag(editableObserver);
- this.item.getModel()
- .stopObservingFocusedItem(focusedAccountObserver);
- this.item.getModel()
- .stopObservingAccountCount(accountCountObserver);
- Data.currencySymbolPosition.removeObserver(currencyPositionObserver);
- Data.currencyGap.removeObserver(currencyGapObserver);
- Data.locale.removeObserver(localeObserver);
- this.item.stopObservingCurrency(currencyObserver);
- this.item.getModel().showCurrency.removeObserver(showCurrencyObserver);
- this.item.stopObservingComment(commentObserver);
- this.item.getModel().showComments.removeObserver(showCommentsObserver);
- this.item.stopObservingAmountValidity(amountValidityObserver);
-
- this.item = null;
- }
+ syncingData = true;
+ try {
+ if (item instanceof NewTransactionModel.TransactionHead) {
+ NewTransactionModel.TransactionHead head = item.toTransactionHead();
+ b.newTransactionDate.setText(head.getFormattedDate());
+ b.newTransactionDescription.setText(head.getDescription());
+
+ b.transactionComment.setText(head.getComment());
+ //styleComment(b.transactionComment, head.getComment());
- switch (item.getType()) {
- case generalData:
- b.newTransactionDate.setText(item.getFormattedDate());
- b.newTransactionDescription.setText(item.getDescription());
- b.transactionComment.setText(item.getTransactionComment());
b.ntrData.setVisibility(View.VISIBLE);
b.ntrAccount.setVisibility(View.GONE);
b.ntrPadding.setVisibility(View.GONE);
setEditable(true);
- break;
- case transactionRow:
- LedgerTransactionAccount acc = item.getAccount();
+ }
+ else if (item instanceof NewTransactionModel.TransactionAccount) {
+ NewTransactionModel.TransactionAccount acc = item.toTransactionAccount();
+
b.accountRowAccName.setText(acc.getAccountName());
- b.comment.setText(acc.getComment());
- if (acc.isAmountSet()) {
- b.accountRowAccAmounts.setText(String.format("%1.2f", acc.getAmount()));
+
+ final String amountHint = acc.getAmountHint();
+ if (amountHint == null) {
+ b.accountRowAccAmounts.setHint(R.string.zero_amount);
}
else {
- b.accountRowAccAmounts.setText("");
-// tvAmount.setHint(R.string.zero_amount);
+ b.accountRowAccAmounts.setHint(amountHint);
}
- b.accountRowAccAmounts.setHint(item.getAmountHint());
+
setCurrencyString(acc.getCurrency());
+ b.accountRowAccAmounts.setText(
+ acc.isAmountSet() ? String.format("%4.2f", acc.getAmount()) : null);
+ displayAmountValidity(true);
+
+ b.comment.setText(acc.getComment());
+
b.ntrData.setVisibility(View.GONE);
b.ntrAccount.setVisibility(View.VISIBLE);
b.ntrPadding.setVisibility(View.GONE);
setEditable(true);
- break;
- case bottomFiller:
+ }
+ else if (item instanceof NewTransactionModel.BottomFiller) {
b.ntrData.setVisibility(View.GONE);
b.ntrAccount.setVisibility(View.GONE);
b.ntrPadding.setVisibility(View.VISIBLE);
setEditable(false);
- break;
- }
- if (this.item == null) { // was null or has changed
- this.item = item;
- final NewTransactionActivity activity = (NewTransactionActivity) b.getRoot()
- .getContext();
-
- if (!item.isBottomFiller()) {
- item.observeEditableFlag(activity, editableObserver);
- item.getModel()
- .observeFocusedItem(activity, focusedAccountObserver);
- item.getModel()
- .observeShowComments(activity, showCommentsObserver);
}
- switch (item.getType()) {
- case generalData:
- item.observeDate(activity, dateObserver);
- item.observeDescription(activity, descriptionObserver);
- item.observeTransactionComment(activity, transactionCommentObserver);
- break;
- case transactionRow:
- item.observeAmountHint(activity, hintObserver);
- Data.currencySymbolPosition.observe(activity, currencyPositionObserver);
- Data.currencyGap.observe(activity, currencyGapObserver);
- Data.locale.observe(activity, localeObserver);
- item.observeCurrency(activity, currencyObserver);
- item.getModel().showCurrency.observe(activity, showCurrencyObserver);
- item.observeComment(activity, commentObserver);
- item.getModel()
- .observeAccountCount(activity, accountCountObserver);
- item.observeAmountValidity(activity, amountValidityObserver);
- break;
+ else {
+ throw new RuntimeException("Don't know how to handle " + item);
}
}
+ finally {
+ syncingData = false;
+ }
}
finally {
endUpdates();
}
}
+ private void styleComment(EditText editText, String comment) {
+ final View focusedView = editText.findFocus();
+ editText.setTypeface(null, (focusedView == editText) ? Typeface.NORMAL : Typeface.ITALIC);
+ editText.setVisibility(
+ ((focusedView != editText) && TextUtils.isEmpty(comment)) ? View.INVISIBLE
+ : View.VISIBLE);
+ }
@Override
public void onDatePicked(int year, int month, int day) {
- item.setDate(new SimpleDate(year, month + 1, day));
+ final NewTransactionModel.TransactionHead head = getItem().toTransactionHead();
+ head.setDate(new SimpleDate(year, month + 1, day));
+ b.newTransactionDate.setText(head.getFormattedDate());
+
boolean focused = b.newTransactionDescription.requestFocus();
if (focused)
Misc.showSoftKeyboard((NewTransactionActivity) b.getRoot()
.getContext());
}
+ private NewTransactionModel.Item getItem() {
+ return Objects.requireNonNull(mAdapter.model.getItems()
+ .getValue())
+ .get(getAdapterPosition());
+ }
@Override
public void descriptionSelected(String description) {
b.accountRowAccName.setText(description);
package net.ktnx.mobileledger.ui.new_transaction;
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.database.Cursor;
-import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.AsyncListDiffer;
+import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
-import com.google.android.material.snackbar.Snackbar;
-
-import net.ktnx.mobileledger.BuildConfig;
-import net.ktnx.mobileledger.R;
-import net.ktnx.mobileledger.async.DescriptionSelectedCallback;
import net.ktnx.mobileledger.databinding.NewTransactionRowBinding;
-import net.ktnx.mobileledger.model.Currency;
-import net.ktnx.mobileledger.model.Data;
-import net.ktnx.mobileledger.model.LedgerTransaction;
-import net.ktnx.mobileledger.model.LedgerTransactionAccount;
import net.ktnx.mobileledger.model.MobileLedgerProfile;
import net.ktnx.mobileledger.utils.Logger;
-import net.ktnx.mobileledger.utils.MLDB;
-import net.ktnx.mobileledger.utils.Misc;
-import java.util.ArrayList;
-import java.util.HashMap;
import java.util.List;
import java.util.Locale;
-import java.util.Set;
-
-import static net.ktnx.mobileledger.utils.Logger.debug;
+import java.util.Objects;
-class NewTransactionItemsAdapter extends RecyclerView.Adapter<NewTransactionItemHolder>
- implements DescriptionSelectedCallback {
- private final NewTransactionModel model;
+class NewTransactionItemsAdapter extends RecyclerView.Adapter<NewTransactionItemHolder> {
+ final NewTransactionModel model;
private final ItemTouchHelper touchHelper;
+ private final AsyncListDiffer<NewTransactionModel.Item> differ =
+ new AsyncListDiffer<>(this, new DiffUtil.ItemCallback<NewTransactionModel.Item>() {
+ @Override
+ public boolean areItemsTheSame(@NonNull NewTransactionModel.Item oldItem,
+ @NonNull NewTransactionModel.Item newItem) {
+// Logger.debug("new-trans",
+// String.format("comparing ids of {%s} and {%s}", oldItem.toString(),
+// newItem.toString()));
+ return oldItem.getId() == newItem.getId();
+ }
+ @Override
+ public boolean areContentsTheSame(@NonNull NewTransactionModel.Item oldItem,
+ @NonNull NewTransactionModel.Item newItem) {
+
+// Logger.debug("new-trans",
+// String.format("comparing contents of {%s} and {%s}", oldItem.toString(),
+// newItem.toString()));
+ return oldItem.equalContents(newItem);
+ }
+ });
private MobileLedgerProfile mProfile;
- private RecyclerView recyclerView;
private int checkHoldCounter = 0;
NewTransactionItemsAdapter(NewTransactionModel viewModel, MobileLedgerProfile profile) {
super();
+ setHasStableIds(true);
model = viewModel;
mProfile = profile;
- int size = model.getAccountCount();
- while (size < 2) {
- Logger.debug("new-transaction",
- String.format(Locale.US, "%d accounts is too little, Calling addRow()", size));
- size = addRow();
- }
+
NewTransactionItemsAdapter adapter = this;
@NonNull RecyclerView.ViewHolder viewHolder,
@NonNull RecyclerView.ViewHolder target) {
- model.swapItems(viewHolder.getAdapterPosition(), target.getAdapterPosition());
- notifyItemMoved(viewHolder.getAdapterPosition(), target.getAdapterPosition());
+ model.moveItem(viewHolder.getAdapterPosition(), target.getAdapterPosition());
return true;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
int pos = viewHolder.getAdapterPosition();
- viewModel.removeItem(pos - 1);
- notifyItemRemoved(pos);
- viewModel.sendCountNotifications(); // needed after items re-arrangement
- checkTransactionSubmittable();
+ viewModel.removeItem(pos);
}
});
}
+ @Override
+ public long getItemId(int position) {
+ return differ.getCurrentList()
+ .get(position)
+ .getId();
+ }
public void setProfile(MobileLedgerProfile profile) {
mProfile = profile;
}
- private int addRow() {
- return addRow(null);
- }
- private int addRow(String commodity) {
- final int newAccountCount = model.addAccount(new LedgerTransactionAccount("", commodity));
- Logger.debug("new-transaction",
- String.format(Locale.US, "invoking notifyItemInserted(%d)", newAccountCount));
- // the header is at position 0
- notifyItemInserted(newAccountCount);
- model.sendCountNotifications(); // needed after holders' positions have changed
- return newAccountCount;
- }
@NonNull
@Override
public NewTransactionItemHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
@Override
public void onBindViewHolder(@NonNull NewTransactionItemHolder holder, int position) {
Logger.debug("bind", String.format(Locale.US, "Binding item at position %d", position));
- NewTransactionModel.Item item = model.getItem(position);
- holder.setData(item);
+ NewTransactionModel.Item item = Objects.requireNonNull(differ.getCurrentList()
+ .get(position));
+ holder.bind(item);
Logger.debug("bind", String.format(Locale.US, "Bound %s item at position %d", item.getType()
.toString(),
position));
}
@Override
public int getItemCount() {
- return model.getAccountCount() + 2;
- }
- private boolean accountListIsEmpty() {
- for (int i = 0; i < model.getAccountCount(); i++) {
- LedgerTransactionAccount acc = model.getAccount(i);
- if (!acc.getAccountName()
- .isEmpty())
- return false;
- if (acc.isAmountSet())
- return false;
- }
-
- return true;
+ return differ.getCurrentList()
+ .size();
}
@Override
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
- this.recyclerView = recyclerView;
touchHelper.attachToRecyclerView(recyclerView);
}
@Override
public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
touchHelper.attachToRecyclerView(null);
super.onDetachedFromRecyclerView(recyclerView);
- this.recyclerView = null;
- }
- public void descriptionSelected(String description) {
- debug("description selected", description);
- if (!accountListIsEmpty())
- return;
-
- String accFilter = mProfile.getPreferredAccountsFilter();
-
- ArrayList<String> params = new ArrayList<>();
- StringBuilder sb = new StringBuilder("select t.profile, t.id from transactions t");
-
- if (!TextUtils.isEmpty(accFilter)) {
- sb.append(" JOIN transaction_accounts ta")
- .append(" ON ta.profile = t.profile")
- .append(" AND ta.transaction_id = t.id");
- }
-
- sb.append(" WHERE t.description=?");
- params.add(description);
-
- if (!TextUtils.isEmpty(accFilter)) {
- sb.append(" AND ta.account_name LIKE '%'||?||'%'");
- params.add(accFilter);
- }
-
- sb.append(" ORDER BY t.year desc, t.month desc, t.day desc LIMIT 1");
-
- final String sql = sb.toString();
- debug("description", sql);
- debug("description", params.toString());
-
- Activity activity = (Activity) recyclerView.getContext();
- // FIXME: handle exceptions?
- MLDB.queryInBackground(sql, params.toArray(new String[]{}), new MLDB.CallbackHelper() {
- @Override
- public void onStart() {
- model.incrementBusyCounter();
- }
- @Override
- public void onDone() {
- model.decrementBusyCounter();
- }
- @Override
- public boolean onRow(@NonNull Cursor cursor) {
- final String profileUUID = cursor.getString(0);
- final int transactionId = cursor.getInt(1);
- activity.runOnUiThread(() -> loadTransactionIntoModel(profileUUID, transactionId));
- return false; // limit 1, by the way
- }
- @Override
- public void onNoRows() {
- if (TextUtils.isEmpty(accFilter))
- return;
-
- debug("description", "Trying transaction search without preferred account filter");
-
- final String broaderSql =
- "select t.profile, t.id from transactions t where t.description=?" +
- " ORDER BY year desc, month desc, day desc LIMIT 1";
- params.remove(1);
- debug("description", broaderSql);
- debug("description", description);
-
- activity.runOnUiThread(
- () -> Snackbar.make(recyclerView, R.string.ignoring_preferred_account,
- Snackbar.LENGTH_INDEFINITE)
- .show());
-
- MLDB.queryInBackground(broaderSql, new String[]{description},
- new MLDB.CallbackHelper() {
- @Override
- public void onStart() {
- model.incrementBusyCounter();
- }
- @Override
- public boolean onRow(@NonNull Cursor cursor) {
- final String profileUUID = cursor.getString(0);
- final int transactionId = cursor.getInt(1);
- activity.runOnUiThread(
- () -> loadTransactionIntoModel(profileUUID, transactionId));
- return false;
- }
- @Override
- public void onDone() {
- model.decrementBusyCounter();
- }
- });
- }
- });
- }
- private void loadTransactionIntoModel(String profileUUID, int transactionId) {
- LedgerTransaction tr;
- MobileLedgerProfile profile = Data.getProfile(profileUUID);
- if (profile == null)
- throw new RuntimeException(String.format(
- "Unable to find profile %s, which is supposed to contain transaction %d",
- profileUUID, transactionId));
-
- tr = profile.loadTransaction(transactionId);
- List<LedgerTransactionAccount> accounts = tr.getAccounts();
- NewTransactionModel.Item firstNegative = null;
- NewTransactionModel.Item firstPositive = null;
- int singleNegativeIndex = -1;
- int singlePositiveIndex = -1;
- int negativeCount = 0;
- for (int i = 0; i < accounts.size(); i++) {
- LedgerTransactionAccount acc = accounts.get(i);
- NewTransactionModel.Item item;
- if (model.getAccountCount() < i + 1) {
- model.addAccount(acc);
- notifyItemInserted(i + 1);
- }
- item = model.getItem(i + 1);
-
- item.getAccount()
- .setAccountName(acc.getAccountName());
- item.setComment(acc.getComment());
- if (acc.isAmountSet()) {
- item.getAccount()
- .setAmount(acc.getAmount());
- if (acc.getAmount() < 0) {
- if (firstNegative == null) {
- firstNegative = item;
- singleNegativeIndex = i;
- }
- else
- singleNegativeIndex = -1;
- }
- else {
- if (firstPositive == null) {
- firstPositive = item;
- singlePositiveIndex = i;
- }
- else
- singlePositiveIndex = -1;
- }
- }
- else
- item.getAccount()
- .resetAmount();
- notifyItemChanged(i + 1);
- }
-
- if (singleNegativeIndex != -1) {
- firstNegative.getAccount()
- .resetAmount();
- model.moveItemLast(singleNegativeIndex);
- }
- else if (singlePositiveIndex != -1) {
- firstPositive.getAccount()
- .resetAmount();
- model.moveItemLast(singlePositiveIndex);
- }
-
- checkTransactionSubmittable();
- model.setFocusedItem(1);
- }
- public void toggleAllEditing(boolean editable) {
- // item 0 is the header
- for (int i = 0; i <= model.getAccountCount(); i++) {
- model.getItem(i)
- .setEditable(editable);
- notifyItemChanged(i);
- // TODO perhaps do only one notification about the whole range (notifyDatasetChanged)?
- }
- }
- void reset() {
- int presentItemCount = model.getAccountCount();
- model.reset();
- notifyItemChanged(0); // header changed
- notifyItemRangeChanged(1, 2); // the two empty rows
- if (presentItemCount > 2)
- notifyItemRangeRemoved(3, presentItemCount - 2); // all the rest are gone
- }
- void updateFocusedItem(int position) {
- model.updateFocusedItem(position);
}
void noteFocusIsOnAccount(int position) {
- model.noteFocusChanged(position, NewTransactionModel.FocusedElement.Account);
+ model.noteFocusChanged(position, FocusedElement.Account);
}
void noteFocusIsOnAmount(int position) {
- model.noteFocusChanged(position, NewTransactionModel.FocusedElement.Amount);
+ model.noteFocusChanged(position, FocusedElement.Amount);
}
void noteFocusIsOnComment(int position) {
- model.noteFocusChanged(position, NewTransactionModel.FocusedElement.Comment);
+ model.noteFocusChanged(position, FocusedElement.Comment);
}
void noteFocusIsOnTransactionComment(int position) {
- model.noteFocusChanged(position, NewTransactionModel.FocusedElement.TransactionComment);
+ model.noteFocusChanged(position, FocusedElement.TransactionComment);
}
public void noteFocusIsOnDescription(int pos) {
- model.noteFocusChanged(pos, NewTransactionModel.FocusedElement.Description);
+ model.noteFocusChanged(pos, FocusedElement.Description);
}
private void holdSubmittableChecks() {
checkHoldCounter++;
throw new RuntimeException("Asymmetrical call to releaseSubmittableChecks");
checkHoldCounter--;
}
- void setItemCurrency(NewTransactionModel.Item item, Currency newCurrency) {
- Currency oldCurrency = item.getCurrency();
- if (!Currency.equal(newCurrency, oldCurrency)) {
- holdSubmittableChecks();
- try {
- item.setCurrency(newCurrency);
-// for (Item i : items) {
-// if (Currency.equal(i.getCurrency(), oldCurrency))
-// i.setCurrency(newCurrency);
-// }
- }
- finally {
- releaseSubmittableChecks();
- }
-
- checkTransactionSubmittable();
- }
+ void setItemCurrency(int position, String newCurrency) {
+ model.setItemCurrency(position, newCurrency);
}
- /*
- A transaction is submittable if:
- 0) has description
- 1) has at least two account names
- 2) each row with amount has account name
- 3) for each commodity:
- 3a) amounts must balance to 0, or
- 3b) there must be exactly one empty amount (with account)
- 4) empty accounts with empty amounts are ignored
- Side effects:
- 5) a row with an empty account name or empty amount is guaranteed to exist for each
- commodity
- 6) at least two rows need to be present in the ledger
- */
- @SuppressLint("DefaultLocale")
- void checkTransactionSubmittable() {
- if (checkHoldCounter > 0)
- return;
-
- int accounts = 0;
- final BalanceForCurrency balance = new BalanceForCurrency();
- final String descriptionText = model.getDescription();
- boolean submittable = true;
- final ItemsForCurrency itemsForCurrency = new ItemsForCurrency();
- final ItemsForCurrency itemsWithEmptyAmountForCurrency = new ItemsForCurrency();
- final ItemsForCurrency itemsWithAccountAndEmptyAmountForCurrency = new ItemsForCurrency();
- final ItemsForCurrency itemsWithEmptyAccountForCurrency = new ItemsForCurrency();
- final ItemsForCurrency itemsWithAmountForCurrency = new ItemsForCurrency();
- final ItemsForCurrency itemsWithAccountForCurrency = new ItemsForCurrency();
- final ItemsForCurrency emptyRowsForCurrency = new ItemsForCurrency();
- final List<NewTransactionModel.Item> emptyRows = new ArrayList<>();
-
- try {
- if ((descriptionText == null) || descriptionText.trim()
- .isEmpty())
- {
- Logger.debug("submittable", "Transaction not submittable: missing description");
- submittable = false;
- }
-
- for (int i = 0; i < model.items.size(); i++) {
- NewTransactionModel.Item item = model.items.get(i);
-
- LedgerTransactionAccount acc = item.getAccount();
- String acc_name = acc.getAccountName()
- .trim();
- String currName = acc.getCurrency();
-
- itemsForCurrency.add(currName, item);
-
- if (acc_name.isEmpty()) {
- itemsWithEmptyAccountForCurrency.add(currName, item);
-
- 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 {
- emptyRowsForCurrency.add(currName, item);
- }
- }
- else {
- accounts++;
- itemsWithAccountForCurrency.add(currName, item);
- }
-
- if (!acc.isAmountValid()) {
- Logger.debug("submittable",
- String.format("Not submittable: row %d has an invalid amount", i + 1));
- submittable = false;
- }
- else if (acc.isAmountSet()) {
- itemsWithAmountForCurrency.add(currName, item);
- balance.add(currName, acc.getAmount());
- }
- else {
- itemsWithEmptyAmountForCurrency.add(currName, item);
-
- if (!acc_name.isEmpty())
- itemsWithAccountAndEmptyAmountForCurrency.add(currName, item);
- }
- }
-
- // 1) has at least two account names
- if (accounts < 2) {
- if (accounts == 0)
- Logger.debug("submittable",
- "Transaction not submittable: no account " + "names");
- else if (accounts == 1)
- Logger.debug("submittable",
- "Transaction not submittable: only one account name");
- else
- Logger.debug("submittable",
- String.format("Transaction not submittable: only %d account names",
- accounts));
- submittable = false;
- }
-
- // 3) for each commodity:
- // 3a) amount must balance to 0, or
- // 3b) there must be exactly one empty amount (with account)
- for (String balCurrency : itemsForCurrency.currencies()) {
- float currencyBalance = balance.get(balCurrency);
- if (Misc.isZero(currencyBalance)) {
- // remove hints from all amount inputs in that currency
- for (NewTransactionModel.Item item : model.items) {
- if (Currency.equal(item.getCurrency(), balCurrency))
- item.setAmountHint(null);
- }
- }
- else {
- List<NewTransactionModel.Item> list =
- itemsWithAccountAndEmptyAmountForCurrency.getList(balCurrency);
- int balanceReceiversCount = list.size();
- if (balanceReceiversCount != 1) {
- if (BuildConfig.DEBUG) {
- if (balanceReceiversCount == 0)
- Logger.debug("submittable", String.format(
- "Transaction not submittable [%s]: non-zero balance " +
- "with no empty amounts with accounts", balCurrency));
- else
- Logger.debug("submittable", String.format(
- "Transaction not submittable [%s]: non-zero balance " +
- "with multiple empty amounts with accounts", balCurrency));
- }
- submittable = false;
- }
-
- List<NewTransactionModel.Item> emptyAmountList =
- itemsWithEmptyAmountForCurrency.getList(balCurrency);
-
- // suggest off-balance amount to a row and remove hints on other rows
- NewTransactionModel.Item receiver = null;
- if (!list.isEmpty())
- receiver = list.get(0);
- else if (!emptyAmountList.isEmpty())
- receiver = emptyAmountList.get(0);
-
- for (NewTransactionModel.Item item : model.items) {
- if (!Currency.equal(item.getCurrency(), balCurrency))
- continue;
-
- if (item.equals(receiver)) {
- if (BuildConfig.DEBUG)
- Logger.debug("submittable",
- String.format("Setting amount hint to %1.2f [%s]",
- -currencyBalance, balCurrency));
- item.setAmountHint(String.format("%1.2f", -currencyBalance));
- }
- else {
- if (BuildConfig.DEBUG)
- Logger.debug("submittable",
- String.format("Resetting hint of '%s' [%s]",
- (item.getAccount() == null) ? "" : item.getAccount()
- .getAccountName(),
- balCurrency));
- item.setAmountHint(null);
- }
- }
- }
- }
-
- // 5) a row with an empty account name or empty amount is guaranteed to exist for
- // each commodity
- for (String balCurrency : balance.currencies()) {
- int currEmptyRows = itemsWithEmptyAccountForCurrency.size(balCurrency);
- int currRows = itemsForCurrency.size(balCurrency);
- int currAccounts = itemsWithAccountForCurrency.size(balCurrency);
- int currAmounts = itemsWithAmountForCurrency.size(balCurrency);
- if ((currEmptyRows == 0) &&
- ((currRows == currAccounts) || (currRows == currAmounts)))
- {
- // perhaps there already is an unused empty row for another currency that
- // is not used?
-// boolean foundIt = false;
-// for (Item item : emptyRows) {
-// Currency itemCurrency = item.getCurrency();
-// String itemCurrencyName =
-// (itemCurrency == null) ? "" : itemCurrency.getName();
-// if (Misc.isZero(balance.get(itemCurrencyName))) {
-// item.setCurrency(Currency.loadByName(balCurrency));
-// item.setAmountHint(
-// String.format("%1.2f", -balance.get(balCurrency)));
-// foundIt = true;
-// break;
-// }
-// }
-//
-// if (!foundIt)
- addRow(balCurrency);
- }
- }
-
- // drop extra empty rows, not needed
- for (String currName : emptyRowsForCurrency.currencies()) {
- List<NewTransactionModel.Item> emptyItems = emptyRowsForCurrency.getList(currName);
- while ((model.items.size() > 2) && (emptyItems.size() > 1)) {
- NewTransactionModel.Item item = emptyItems.get(1);
- emptyItems.remove(1);
- model.removeRow(item, this);
- }
-
- // unused currency, remove last item (which is also an empty one)
- if ((model.items.size() > 2) && (emptyItems.size() == 1)) {
- List<NewTransactionModel.Item> currItems = itemsForCurrency.getList(currName);
-
- if (currItems.size() == 1) {
- NewTransactionModel.Item item = emptyItems.get(0);
- model.removeRow(item, this);
- }
- }
- }
-
- // 6) at least two rows need to be present in the ledger
- while (model.items.size() < 2)
- addRow();
-
-
- debug("submittable", submittable ? "YES" : "NO");
- model.isSubmittable.setValue(submittable);
-
- if (BuildConfig.DEBUG) {
- debug("submittable", "== Dump of all items");
- for (int i = 0; i < model.items.size(); i++) {
- NewTransactionModel.Item item = model.items.get(i);
- LedgerTransactionAccount acc = item.getAccount();
- debug("submittable", String.format("Item %2d: [%4.2f(%s) %s] %s ; %s", i,
- acc.isAmountSet() ? acc.getAmount() : 0,
- item.isAmountHintSet() ? item.getAmountHint() : "ø", acc.getCurrency(),
- acc.getAccountName(), acc.getComment()));
- }
- }
- }
- catch (NumberFormatException e) {
- debug("submittable", "NO (because of NumberFormatException)");
- model.isSubmittable.setValue(false);
- }
- catch (Exception e) {
- e.printStackTrace();
- debug("submittable", "NO (because of an Exception)");
- model.isSubmittable.setValue(false);
- }
- }
-
- private static class BalanceForCurrency {
- private final HashMap<String, Float> hashMap = new HashMap<>();
- float get(String currencyName) {
- Float f = hashMap.get(currencyName);
- if (f == null) {
- f = 0f;
- hashMap.put(currencyName, f);
- }
- return f;
- }
- void add(String currencyName, float amount) {
- hashMap.put(currencyName, get(currencyName) + amount);
- }
- Set<String> currencies() {
- return hashMap.keySet();
- }
- boolean containsCurrency(String currencyName) {
- return hashMap.containsKey(currencyName);
- }
- }
-
- private static class ItemsForCurrency {
- private final HashMap<String, List<NewTransactionModel.Item>> hashMap = new HashMap<>();
- @NonNull
- List<NewTransactionModel.Item> getList(@Nullable String currencyName) {
- List<NewTransactionModel.Item> list = hashMap.get(currencyName);
- if (list == null) {
- list = new ArrayList<>();
- hashMap.put(currencyName, list);
- }
- return list;
- }
- void add(@Nullable String currencyName, @NonNull NewTransactionModel.Item item) {
- getList(currencyName).add(item);
- }
- int size(@Nullable String currencyName) {
- return this.getList(currencyName)
- .size();
- }
- Set<String> currencies() {
- return hashMap.keySet();
- }
+ public void setItems(List<NewTransactionModel.Item> newList) {
+ Logger.debug("new-trans", "adapter: submitting new item list");
+ differ.submitList(newList);
}
}
package net.ktnx.mobileledger.ui.new_transaction;
+import android.annotation.SuppressLint;
+import android.text.TextUtils;
+
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModel;
-import net.ktnx.mobileledger.model.Currency;
+import net.ktnx.mobileledger.BuildConfig;
+import net.ktnx.mobileledger.db.DB;
+import net.ktnx.mobileledger.db.TemplateAccount;
+import net.ktnx.mobileledger.db.TemplateHeader;
import net.ktnx.mobileledger.model.Data;
+import net.ktnx.mobileledger.model.InertMutableLiveData;
+import net.ktnx.mobileledger.model.LedgerTransaction;
import net.ktnx.mobileledger.model.LedgerTransactionAccount;
+import net.ktnx.mobileledger.model.MatchedTemplate;
import net.ktnx.mobileledger.model.MobileLedgerProfile;
import net.ktnx.mobileledger.utils.Globals;
+import net.ktnx.mobileledger.utils.Logger;
+import net.ktnx.mobileledger.utils.Misc;
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.GregorianCalendar;
+import java.util.HashMap;
+import java.util.List;
import java.util.Locale;
+import java.util.Objects;
+import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.MatchResult;
+
+enum ItemType {generalData, transactionRow, bottomFiller}
+
+enum FocusedElement {Account, Comment, Amount, Description, TransactionComment}
+
public class NewTransactionModel extends ViewModel {
- final MutableLiveData<Boolean> showCurrency = new MutableLiveData<>(false);
- final ArrayList<Item> items = new ArrayList<>();
- final MutableLiveData<Boolean> isSubmittable = new MutableLiveData<>(false);
- final MutableLiveData<Boolean> showComments = new MutableLiveData<>(true);
- private final Item header = new Item(this, "");
- private final Item trailer = new Item(this);
- private final MutableLiveData<Integer> focusedItem = new MutableLiveData<>(0);
- private final MutableLiveData<Integer> accountCount = new MutableLiveData<>(0);
- private final MutableLiveData<Boolean> simulateSave = new MutableLiveData<>(false);
+ 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 MutableLiveData<>(false);
+ private final MutableLiveData<Boolean> busyFlag = new InertMutableLiveData<>(false);
private final Observer<MobileLedgerProfile> profileObserver = profile -> {
showCurrency.postValue(profile.getShowCommodityByDefault());
showComments.postValue(profile.getShowCommentsByDefault());
};
+ private final MutableLiveData<FocusInfo> focusInfo = new MutableLiveData<>();
private boolean observingDataProfile;
- void observeShowComments(LifecycleOwner owner, Observer<? super Boolean> observer) {
- showComments.observe(owner, observer);
+ public NewTransactionModel() {
+ reset();
}
- void observeBusyFlag(@NonNull LifecycleOwner owner, Observer<? super Boolean> observer) {
- busyFlag.observe(owner, observer);
+ public LiveData<Boolean> getShowCurrency() {
+ return showCurrency;
}
- void observeDataProfile(LifecycleOwner activity) {
- if (!observingDataProfile)
- Data.observeProfile(activity, profileObserver);
- observingDataProfile = true;
+ public LiveData<List<Item>> getItems() {
+ return items;
}
- boolean getSimulateSave() {
- return simulateSave.getValue();
+ private void setItems(@NonNull List<Item> newList) {
+ checkTransactionSubmittable(newList);
+ setItemsWithoutSubmittableChecks(newList);
}
- public void setSimulateSave(boolean simulateSave) {
- this.simulateSave.setValue(simulateSave);
+ private void setItemsWithoutSubmittableChecks(@NonNull List<Item> list) {
+ Logger.debug("new-trans", "model: Setting new item list");
+ items.setValue(list);
+ accountCount.setValue(list.size() - 2);
}
- void toggleSimulateSave() {
- simulateSave.setValue(!simulateSave.getValue());
+ private List<Item> copyList() {
+ return copyList(null);
}
- void observeSimulateSave(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
- @NonNull androidx.lifecycle.Observer<? super Boolean> observer) {
- this.simulateSave.observe(owner, observer);
+ private List<Item> copyList(@Nullable List<Item> source) {
+ List<Item> copy = new ArrayList<>();
+ List<Item> oldList = (source == null) ? items.getValue() : source;
+
+ if (oldList != null)
+ for (Item item : oldList) {
+ copy.add(Item.from(item));
+ }
+
+ return copy;
}
- int getAccountCount() {
- return items.size();
+ private List<Item> shallowCopyListWithoutItem(int position) {
+ List<Item> copy = new ArrayList<>();
+ List<Item> oldList = items.getValue();
+
+ if (oldList != null) {
+ int i = 0;
+ for (Item item : oldList) {
+ if (i++ == position)
+ continue;
+ copy.add(item);
+ }
+ }
+
+ return copy;
+ }
+ private List<Item> shallowCopyList() {
+ return new ArrayList<>(items.getValue());
}
- public SimpleDate getDate() {
- return header.date.getValue();
+ LiveData<Boolean> getShowComments() {
+ return showComments;
+ }
+ void observeDataProfile(LifecycleOwner activity) {
+ if (!observingDataProfile)
+ Data.observeProfile(activity, profileObserver);
+ observingDataProfile = true;
}
- public void setDate(SimpleDate date) {
- header.date.setValue(date);
+ boolean getSimulateSaveFlag() {
+ Boolean value = simulateSave.getValue();
+ if (value == null)
+ return false;
+ return value;
}
- public String getDescription() {
- return header.description.getValue();
+ LiveData<Boolean> getSimulateSave() {
+ return simulateSave;
}
- public String getComment() {
- return header.comment.getValue();
+ void toggleSimulateSave() {
+ simulateSave.setValue(!getSimulateSaveFlag());
}
LiveData<Boolean> 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);
- }
- void observeFocusedItem(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
- @NonNull androidx.lifecycle.Observer<? super Integer> observer) {
- this.focusedItem.observe(owner, observer);
- }
- void stopObservingFocusedItem(@NonNull androidx.lifecycle.Observer<? super Integer> observer) {
- this.focusedItem.removeObserver(observer);
- }
- void observeAccountCount(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
- @NonNull androidx.lifecycle.Observer<? super Integer> observer) {
- this.accountCount.observe(owner, observer);
- }
- void stopObservingAccountCount(@NonNull androidx.lifecycle.Observer<? super Integer> observer) {
- this.accountCount.removeObserver(observer);
- }
- int getFocusedItem() { return focusedItem.getValue(); }
- void setFocusedItem(int position) {
- focusedItem.setValue(position);
- }
- int addAccount(LedgerTransactionAccount acc) {
- items.add(new Item(this, acc));
- accountCount.setValue(getAccountCount());
- return items.size();
+ List<Item> list = new ArrayList<>();
+ list.add(new TransactionHead(""));
+ list.add(new TransactionAccount(""));
+ list.add(new TransactionAccount(""));
+ list.add(new BottomFiller());
+ items.setValue(list);
}
boolean accountsInInitialState() {
- for (Item item : items) {
- LedgerTransactionAccount acc = item.getAccount();
- if (acc.isAmountSet())
- return false;
- if (!acc.getAccountName()
- .trim()
- .isEmpty())
+ final List<Item> list = items.getValue();
+
+ if (list == null)
+ return true;
+
+ for (Item item : list) {
+ if (!(item instanceof TransactionAccount))
+ continue;
+
+ TransactionAccount accRow = (TransactionAccount) item;
+ if (!accRow.isEmpty())
return false;
}
return true;
}
- LedgerTransactionAccount getAccount(int index) {
- return items.get(index)
- .getAccount();
- }
- Item getItem(int index) {
- if (index == 0) {
- return header;
+ void applyTemplate(MatchedTemplate matchedTemplate, String text) {
+ SimpleDate transactionDate = null;
+ final MatchResult matchResult = matchedTemplate.matchResult;
+ final TemplateHeader templateHead = matchedTemplate.templateHead;
+ {
+ int day = extractIntFromMatches(matchResult, templateHead.getDateDayMatchGroup(),
+ templateHead.getDateDay());
+ int month = extractIntFromMatches(matchResult, templateHead.getDateMonthMatchGroup(),
+ templateHead.getDateMonth());
+ int year = extractIntFromMatches(matchResult, templateHead.getDateYearMatchGroup(),
+ templateHead.getDateYear());
+
+ if (year > 0 || month > 0 || day > 0) {
+ SimpleDate today = SimpleDate.today();
+ if (year <= 0)
+ year = today.year;
+ if (month <= 0)
+ month = today.month;
+ if (day <= 0)
+ day = today.day;
+
+ transactionDate = new SimpleDate(year, month, day);
+
+ Logger.debug("pattern", "setting transaction date to " + transactionDate);
+ }
}
- if (index <= items.size())
- return items.get(index - 1);
+ List<Item> present = copyList();
- return trailer;
- }
- void removeRow(Item item, NewTransactionItemsAdapter adapter) {
- int pos = items.indexOf(item);
- items.remove(pos);
- if (adapter != null) {
- adapter.notifyItemRemoved(pos + 1);
- sendCountNotifications();
+ TransactionHead head = new TransactionHead(present.get(0)
+ .toTransactionHead());
+ if (transactionDate != null)
+ head.setDate(transactionDate);
+
+ final String transactionDescription = extractStringFromMatches(matchResult,
+ templateHead.getTransactionDescriptionMatchGroup(),
+ templateHead.getTransactionDescription());
+ if (Misc.emptyIsNull(transactionDescription) != null)
+ head.setDescription(transactionDescription);
+
+ final String transactionComment = extractStringFromMatches(matchResult,
+ templateHead.getTransactionCommentMatchGroup(),
+ templateHead.getTransactionComment());
+ if (Misc.emptyIsNull(transactionComment) != null)
+ head.setComment(transactionComment);
+
+ List<Item> newItems = new ArrayList<>();
+
+ newItems.add(head);
+
+ for (int i = 1; i < present.size() - 1; i++) {
+ final TransactionAccount row = present.get(i)
+ .toTransactionAccount();
+ if (!row.isEmpty())
+ newItems.add(new TransactionAccount(row));
}
+
+ DB.get()
+ .getTemplateDAO()
+ .getTemplateWithAccountsAsync(templateHead.getId(), entry -> {
+ int rowIndex = 0;
+ final boolean accountsInInitialState = accountsInInitialState();
+ for (TemplateAccount acc : entry.accounts) {
+ rowIndex++;
+
+ String accountName =
+ extractStringFromMatches(matchResult, acc.getAccountNameMatchGroup(),
+ acc.getAccountName());
+ String accountComment =
+ extractStringFromMatches(matchResult, acc.getAccountCommentMatchGroup(),
+ acc.getAccountComment());
+ Float amount = extractFloatFromMatches(matchResult, acc.getAmountMatchGroup(),
+ acc.getAmount());
+ if (amount != null && acc.getNegateAmount() != null && acc.getNegateAmount())
+ amount = -amount;
+
+ // TODO currency
+ TransactionAccount accRow = new TransactionAccount(accountName);
+ accRow.setComment(accountComment);
+ if (amount != null)
+ accRow.setAmount(amount);
+
+ newItems.add(accRow);
+ }
+
+ newItems.add(new BottomFiller());
+
+ items.postValue(newItems);
+ });
}
- void removeItem(int pos) {
- items.remove(pos);
- accountCount.setValue(getAccountCount());
+ private int extractIntFromMatches(MatchResult m, Integer group, Integer literal) {
+ if (literal != null)
+ return literal;
+
+ if (group != null) {
+ int grp = group;
+ if (grp > 0 & grp <= m.groupCount())
+ try {
+ return Integer.parseInt(m.group(grp));
+ }
+ catch (NumberFormatException e) {
+ Logger.debug("new-trans", "Error extracting matched number", e);
+ }
+ }
+
+ return 0;
}
- void sendCountNotifications() {
- accountCount.setValue(getAccountCount());
+ private String extractStringFromMatches(MatchResult m, Integer group, String literal) {
+ if (literal != null)
+ return literal;
+
+ if (group != null) {
+ int grp = group;
+ if (grp > 0 & grp <= m.groupCount())
+ return m.group(grp);
+ }
+
+ return null;
}
- public void sendFocusedNotification() {
- focusedItem.setValue(focusedItem.getValue());
+ private Float extractFloatFromMatches(MatchResult m, Integer group, Float literal) {
+ if (literal != null)
+ return literal;
+
+ if (group != null) {
+ int grp = group;
+ if (grp > 0 & grp <= m.groupCount())
+ try {
+ return Float.valueOf(m.group(grp));
+ }
+ catch (NumberFormatException e) {
+ Logger.debug("new-trans", "Error extracting matched number", e);
+ }
+ }
+
+ return null;
}
- void updateFocusedItem(int position) {
- focusedItem.setValue(position);
+ void removeItem(int pos) {
+ List<Item> newList = shallowCopyListWithoutItem(pos);
+ setItems(newList);
}
void noteFocusChanged(int position, FocusedElement element) {
- getItem(position).setFocusedElement(element);
+ FocusInfo present = focusInfo.getValue();
+ if (present == null || present.position != position || present.element != element)
+ focusInfo.setValue(new FocusInfo(position, element));
+ }
+ public LiveData<FocusInfo> getFocusInfo() {
+ return focusInfo;
}
- void swapItems(int one, int two) {
- Collections.swap(items, one - 1, two - 1);
+ void moveItem(int fromIndex, int toIndex) {
+ List<Item> newList = shallowCopyList();
+ Item item = newList.remove(fromIndex);
+ newList.add(toIndex, item);
+ items.setValue(newList); // same count, same submittable state
}
- void moveItemLast(int index) {
+ void moveItemLast(List<Item> list, int index) {
/* 0
1 <-- index
2
3 <-- desired position
+ (no bottom filler)
*/
- int itemCount = items.size();
+ int itemCount = list.size();
- if (index < itemCount - 1) {
- Item acc = items.remove(index);
- items.add(itemCount - 1, acc);
- }
+ if (index < itemCount - 1)
+ list.add(list.remove(index));
}
void toggleCurrencyVisible() {
- showCurrency.setValue(!showCurrency.getValue());
+ showCurrency.setValue(!Objects.requireNonNull(showCurrency.getValue()));
}
void stopObservingBusyFlag(Observer<Boolean> observer) {
busyFlag.removeObserver(observer);
if (newValue == 0)
busyFlag.postValue(false);
}
- public boolean getBusyFlag() {
- return busyFlag.getValue();
+ public LiveData<Boolean> getBusyFlag() {
+ return busyFlag;
}
public void toggleShowComments() {
- showComments.setValue(!showComments.getValue());
+ showComments.setValue(!Objects.requireNonNull(showComments.getValue()));
}
- enum ItemType {generalData, transactionRow, bottomFiller}
+ public LedgerTransaction constructLedgerTransaction() {
+ List<Item> list = Objects.requireNonNull(items.getValue());
+ TransactionHead head = list.get(0)
+ .toTransactionHead();
+ SimpleDate date = head.getDate();
+ LedgerTransaction tr = head.asLedgerTransaction();
- enum FocusedElement {Account, Comment, Amount, Description, TransactionComment}
+ tr.setComment(head.getComment());
+ LedgerTransactionAccount emptyAmountAccount = null;
+ float emptyAmountAccountBalance = 0;
+ for (int i = 1; i < list.size() - 1; i++) {
+ TransactionAccount item = list.get(i)
+ .toTransactionAccount();
+ LedgerTransactionAccount acc = new LedgerTransactionAccount(item.getAccountName()
+ .trim(),
+ item.getCurrency());
+ if (acc.getAccountName()
+ .isEmpty())
+ continue;
+ acc.setComment(item.getComment());
- //==========================================================================================
+ if (item.isAmountSet()) {
+ acc.setAmount(item.getAmount());
+ emptyAmountAccountBalance += item.getAmount();
+ }
+ else {
+ emptyAmountAccount = acc;
+ }
+ tr.addAccount(acc);
+ }
- static class Item {
- private final ItemType type;
- private final MutableLiveData<SimpleDate> date = new MutableLiveData<>();
- private final MutableLiveData<String> description = new MutableLiveData<>();
- private final MutableLiveData<String> amountHint = new MutableLiveData<>(null);
- private final NewTransactionModel model;
- private final MutableLiveData<Boolean> editable = new MutableLiveData<>(true);
- private final MutableLiveData<String> comment = new MutableLiveData<>(null);
- private final MutableLiveData<Currency> currency = new MutableLiveData<>(null);
- private final MutableLiveData<Boolean> amountValid = new MutableLiveData<>(true);
- private LedgerTransactionAccount account;
- private FocusedElement focusedElement = FocusedElement.Account;
- private boolean amountHintIsSet = false;
- Item(NewTransactionModel model) {
- this.model = model;
- type = ItemType.bottomFiller;
- editable.setValue(false);
- }
- Item(NewTransactionModel model, String description) {
- this.model = model;
- this.type = ItemType.generalData;
- this.description.setValue(description);
- this.editable.setValue(true);
- }
- 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);
- }
- FocusedElement getFocusedElement() {
- return focusedElement;
+ if (emptyAmountAccount != null)
+ emptyAmountAccount.setAmount(-emptyAmountAccountBalance);
+
+ return tr;
+ }
+ void loadTransactionIntoModel(String profileUUID, int transactionId) {
+ List<Item> newList = new ArrayList<>();
+ LedgerTransaction tr;
+ MobileLedgerProfile profile = Data.getProfile(profileUUID);
+ if (profile == null)
+ throw new RuntimeException(String.format(
+ "Unable to find profile %s, which is supposed to contain transaction %d",
+ profileUUID, transactionId));
+
+ tr = profile.loadTransaction(transactionId);
+ TransactionHead head = new TransactionHead(tr.getDescription());
+ head.setComment(tr.getComment());
+
+ newList.add(head);
+
+ List<LedgerTransactionAccount> accounts = tr.getAccounts();
+
+ TransactionAccount firstNegative = null;
+ TransactionAccount firstPositive = null;
+ int singleNegativeIndex = -1;
+ int singlePositiveIndex = -1;
+ int negativeCount = 0;
+ for (int i = 0; i < accounts.size(); i++) {
+ LedgerTransactionAccount acc = accounts.get(i);
+ TransactionAccount item =
+ new TransactionAccount(acc.getAccountName(), acc.getCurrency());
+ newList.add(item);
+
+ item.setAccountName(acc.getAccountName());
+ item.setComment(acc.getComment());
+ if (acc.isAmountSet()) {
+ item.setAmount(acc.getAmount());
+ if (acc.getAmount() < 0) {
+ if (firstNegative == null) {
+ firstNegative = item;
+ singleNegativeIndex = i + 1;
+ }
+ else
+ singleNegativeIndex = -1;
+ }
+ else {
+ if (firstPositive == null) {
+ firstPositive = item;
+ singlePositiveIndex = i + 1;
+ }
+ else
+ singlePositiveIndex = -1;
+ }
+ }
+ else
+ item.resetAmount();
}
- void setFocusedElement(FocusedElement focusedElement) {
- this.focusedElement = focusedElement;
+
+ if (singleNegativeIndex != -1) {
+ firstNegative.resetAmount();
+ moveItemLast(newList, singleNegativeIndex);
}
- public NewTransactionModel getModel() {
- return model;
+ else if (singlePositiveIndex != -1) {
+ firstPositive.resetAmount();
+ moveItemLast(newList, singlePositiveIndex);
}
- void setEditable(boolean editable) {
- ensureTypeIsGeneralDataOrTransactionRow();
- this.editable.setValue(editable);
+
+ noteFocusChanged(1, FocusedElement.Description);
+
+ newList.add(new BottomFiller());
+
+ setItems(newList);
+ }
+ /**
+ * A transaction is submittable if:
+ * 0) has description
+ * 1) has at least two account names
+ * 2) each row with amount has account name
+ * 3) for each commodity:
+ * 3a) amounts must balance to 0, or
+ * 3b) there must be exactly one empty amount (with account)
+ * 4) empty accounts with empty amounts are ignored
+ * Side effects:
+ * 5) a row with an empty account name or empty amount is guaranteed to exist for each
+ * commodity
+ * 6) at least two rows need to be present in the ledger
+ *
+ * @param list - the item list to check. Can be the displayed list or a list that will be
+ * displayed soon
+ */
+ @SuppressLint("DefaultLocale")
+ void checkTransactionSubmittable(@Nullable List<Item> list) {
+ boolean workingWithLiveList = false;
+ boolean liveListCopied = false;
+ if (list == null) {
+ list = Objects.requireNonNull(items.getValue());
+ workingWithLiveList = true;
}
- private void ensureTypeIsGeneralDataOrTransactionRow() {
- if ((type != ItemType.generalData) && (type != ItemType.transactionRow)) {
- throw new RuntimeException(
- String.format("Actual type (%s) differs from wanted (%s or %s)", type,
- ItemType.generalData, ItemType.transactionRow));
+
+ if (BuildConfig.DEBUG)
+ dumpItemList("Before submittable checks", list);
+
+ int accounts = 0;
+ final BalanceForCurrency balance = new BalanceForCurrency();
+ final String descriptionText = list.get(0)
+ .toTransactionHead()
+ .getDescription();
+ boolean submittable = true;
+ boolean listChanged = false;
+ final ItemsForCurrency itemsForCurrency = new ItemsForCurrency();
+ final ItemsForCurrency itemsWithEmptyAmountForCurrency = new ItemsForCurrency();
+ final ItemsForCurrency itemsWithAccountAndEmptyAmountForCurrency = new ItemsForCurrency();
+ final ItemsForCurrency itemsWithEmptyAccountForCurrency = new ItemsForCurrency();
+ final ItemsForCurrency itemsWithAmountForCurrency = new ItemsForCurrency();
+ final ItemsForCurrency itemsWithAccountForCurrency = new ItemsForCurrency();
+ final ItemsForCurrency emptyRowsForCurrency = new ItemsForCurrency();
+ final List<Item> emptyRows = new ArrayList<>();
+
+ try {
+ if ((descriptionText == null) || descriptionText.trim()
+ .isEmpty())
+ {
+ Logger.debug("submittable", "Transaction not submittable: missing description");
+ submittable = false;
}
- }
- String getAmountHint() {
- ensureType(ItemType.transactionRow);
- return amountHint.getValue();
- }
- void setAmountHint(String amountHint) {
- ensureType(ItemType.transactionRow);
- // avoid unnecessary triggers
- if (amountHint == null) {
- if (this.amountHint.getValue() == null)
- return;
- amountHintIsSet = false;
+ for (int i = 1; i < list.size() - 1; i++) {
+ TransactionAccount item = list.get(i)
+ .toTransactionAccount();
+
+ String accName = item.getAccountName()
+ .trim();
+ String currName = item.getCurrency();
+
+ itemsForCurrency.add(currName, item);
+
+ if (accName.isEmpty()) {
+ itemsWithEmptyAccountForCurrency.add(currName, item);
+
+ if (item.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, item.getAmount()));
+ submittable = false;
+ }
+ else {
+ emptyRowsForCurrency.add(currName, item);
+ }
+ }
+ else {
+ accounts++;
+ itemsWithAccountForCurrency.add(currName, item);
+ }
+
+ if (!item.isAmountValid()) {
+ Logger.debug("submittable",
+ String.format("Not submittable: row %d has an invalid amount", i + 1));
+ submittable = false;
+ }
+ else if (item.isAmountSet()) {
+ itemsWithAmountForCurrency.add(currName, item);
+ balance.add(currName, item.getAmount());
+ }
+ else {
+ itemsWithEmptyAmountForCurrency.add(currName, item);
+
+ if (!accName.isEmpty())
+ itemsWithAccountAndEmptyAmountForCurrency.add(currName, item);
+ }
}
- else {
- if (amountHint.equals(this.amountHint.getValue()))
- return;
- amountHintIsSet = true;
+
+ // 1) has at least two account names
+ if (accounts < 2) {
+ if (accounts == 0)
+ Logger.debug("submittable", "Transaction not submittable: no account names");
+ else if (accounts == 1)
+ Logger.debug("submittable",
+ "Transaction not submittable: only one account name");
+ else
+ Logger.debug("submittable",
+ String.format("Transaction not submittable: only %d account names",
+ accounts));
+ submittable = false;
}
- this.amountHint.setValue(amountHint);
- }
- void observeAmountHint(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
- @NonNull androidx.lifecycle.Observer<? super String> observer) {
- this.amountHint.observe(owner, observer);
- }
- void stopObservingAmountHint(
- @NonNull androidx.lifecycle.Observer<? super String> observer) {
- this.amountHint.removeObserver(observer);
+ // 3) for each commodity:
+ // 3a) amount must balance to 0, or
+ // 3b) there must be exactly one empty amount (with account)
+ for (String balCurrency : itemsForCurrency.currencies()) {
+ 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++) {
+ TransactionAccount acc = list.get(i)
+ .toTransactionAccount();
+ if (Misc.equalStrings(acc.getCurrency(), balCurrency)) {
+ if (BuildConfig.DEBUG)
+ Logger.debug("submittable",
+ String.format("Resetting hint of '%s' [%s]",
+ Misc.nullIsEmpty(acc.getAccountName()),
+ balCurrency));
+ if (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;
+ }
+ }
+ }
+ }
+ else {
+ List<Item> tmpList =
+ itemsWithAccountAndEmptyAmountForCurrency.getList(balCurrency);
+ int balanceReceiversCount = tmpList.size();
+ if (balanceReceiversCount != 1) {
+ if (BuildConfig.DEBUG) {
+ if (balanceReceiversCount == 0)
+ Logger.debug("submittable", String.format(
+ "Transaction not submittable [%s]: non-zero balance " +
+ "with no empty amounts with accounts", balCurrency));
+ else
+ Logger.debug("submittable", String.format(
+ "Transaction not submittable [%s]: non-zero balance " +
+ "with multiple empty amounts with accounts", balCurrency));
+ }
+ submittable = false;
+ }
+
+ List<Item> emptyAmountList =
+ itemsWithEmptyAmountForCurrency.getList(balCurrency);
+
+ // suggest off-balance amount to a row and remove hints on other rows
+ Item receiver = null;
+ if (!tmpList.isEmpty())
+ receiver = tmpList.get(0);
+ else if (!emptyAmountList.isEmpty())
+ receiver = emptyAmountList.get(0);
+
+ for (int i = 0; i < list.size(); i++) {
+ Item item = list.get(i);
+ if (!(item instanceof TransactionAccount))
+ continue;
+
+ TransactionAccount acc = item.toTransactionAccount();
+ if (!Misc.equalStrings(acc.getCurrency(), balCurrency))
+ continue;
+
+ if (item == receiver) {
+ final String hint = String.format("%1.2f", -currencyBalance);
+ if (!acc.isAmountHintSet() ||
+ !TextUtils.equals(acc.getAmountHint(), hint))
+ {
+ Logger.debug("submittable",
+ String.format("Setting amount hint of {%s} to %s [%s]",
+ acc.toString(), hint, balCurrency));
+ if (workingWithLiveList & !liveListCopied) {
+ list = copyList(list);
+ liveListCopied = true;
+ }
+ final TransactionAccount newAcc = new TransactionAccount(acc);
+ newAcc.setAmountHint(hint);
+ list.set(i, newAcc);
+ listChanged = true;
+ }
+ }
+ else {
+ if (BuildConfig.DEBUG)
+ Logger.debug("submittable",
+ String.format("Resetting hint of '%s' [%s]",
+ Misc.nullIsEmpty(acc.getAccountName()),
+ balCurrency));
+ if (acc.amountHintIsSet && !TextUtils.isEmpty(acc.getAmountHint())) {
+ if (workingWithLiveList && !liveListCopied) {
+ list = copyList(list);
+ liveListCopied = true;
+ }
+ final TransactionAccount newAcc = new TransactionAccount(acc);
+ newAcc.setAmountHint(null);
+ list.set(i, newAcc);
+ listChanged = true;
+ }
+ }
+ }
+ }
+ }
+
+ // 5) a row with an empty account name or empty amount is guaranteed to exist for
+ // each commodity
+ for (String balCurrency : balance.currencies()) {
+ int currEmptyRows = itemsWithEmptyAccountForCurrency.size(balCurrency);
+ int currRows = itemsForCurrency.size(balCurrency);
+ int currAccounts = itemsWithAccountForCurrency.size(balCurrency);
+ int currAmounts = itemsWithAmountForCurrency.size(balCurrency);
+ if ((currEmptyRows == 0) &&
+ ((currRows == currAccounts) || (currRows == currAmounts)))
+ {
+ // perhaps there already is an unused empty row for another currency that
+ // is not used?
+// boolean foundIt = false;
+// for (Item item : emptyRows) {
+// Currency itemCurrency = item.getCurrency();
+// String itemCurrencyName =
+// (itemCurrency == null) ? "" : itemCurrency.getName();
+// if (Misc.isZero(balance.get(itemCurrencyName))) {
+// item.setCurrency(Currency.loadByName(balCurrency));
+// item.setAmountHint(
+// String.format("%1.2f", -balance.get(balCurrency)));
+// foundIt = true;
+// break;
+// }
+// }
+//
+// if (!foundIt)
+ if (workingWithLiveList && !liveListCopied) {
+ list = copyList(list);
+ liveListCopied = true;
+ }
+ final TransactionAccount newAcc = new TransactionAccount("", balCurrency);
+ final float bal = balance.get(balCurrency);
+ if (!Misc.isZero(bal) && currAmounts == currRows)
+ newAcc.setAmountHint(String.format("%4.2f", -bal));
+ Logger.debug("submittable",
+ String.format("Adding new item with %s for currency %s",
+ newAcc.getAmountHint(), balCurrency));
+ list.add(list.size() - 1, newAcc);
+ listChanged = true;
+ }
+ }
+
+ // drop extra empty rows, not needed
+ for (String currName : emptyRowsForCurrency.currencies()) {
+ List<Item> emptyItems = emptyRowsForCurrency.getList(currName);
+ while ((list.size() > 4) && (emptyItems.size() > 1)) {
+ if (workingWithLiveList && !liveListCopied) {
+ list = copyList(list);
+ liveListCopied = true;
+ }
+ Item item = emptyItems.remove(1);
+ list.remove(item);
+ listChanged = true;
+ }
+
+ // unused currency, remove last item (which is also an empty one)
+ if ((list.size() > 4) && (emptyItems.size() == 1)) {
+ List<Item> currItems = itemsForCurrency.getList(currName);
+
+ if (currItems.size() == 1) {
+ if (workingWithLiveList && !liveListCopied) {
+ list = copyList(list);
+ liveListCopied = true;
+ }
+ Item item = emptyItems.get(0);
+ list.remove(item);
+ listChanged = true;
+ }
+ }
+ }
+
+ // 6) at least two rows need to be present in the ledger
+ // (the list also contains header and trailer)
+ while (list.size() < 4) {
+ if (workingWithLiveList && !liveListCopied) {
+ list = copyList(list);
+ liveListCopied = true;
+ }
+ list.add(list.size() - 1, new TransactionAccount(""));
+ listChanged = true;
+ }
+
+
+ Logger.debug("submittable", submittable ? "YES" : "NO");
+ isSubmittable.setValue(submittable);
+
+ if (BuildConfig.DEBUG)
+ dumpItemList("After submittable checks", list);
}
- ItemType getType() {
- return type;
+ catch (NumberFormatException e) {
+ Logger.debug("submittable", "NO (because of NumberFormatException)");
+ isSubmittable.setValue(false);
}
- void ensureType(ItemType wantedType) {
- if (type != wantedType) {
- throw new RuntimeException(
- String.format("Actual type (%s) differs from wanted (%s)", type,
- wantedType));
- }
+ catch (Exception e) {
+ e.printStackTrace();
+ Logger.debug("submittable", "NO (because of an Exception)");
+ isSubmittable.setValue(false);
}
- public SimpleDate getDate() {
- ensureType(ItemType.generalData);
- return date.getValue();
+
+ if (listChanged && workingWithLiveList) {
+ setItemsWithoutSubmittableChecks(list);
}
- public void setDate(SimpleDate date) {
- ensureType(ItemType.generalData);
- this.date.setValue(date);
+ }
+ @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++) {
+ TransactionAccount item = list.get(i)
+ .toTransactionAccount();
+ Logger.debug("submittable", String.format("%d:%s", i, item.toString()));
}
- public void setDate(String text) throws ParseException {
- if ((text == null) || text.trim()
- .isEmpty())
- {
- setDate((SimpleDate) null);
- return;
- }
+ }
+ public void setItemCurrency(int position, String newCurrency) {
+ TransactionAccount item = Objects.requireNonNull(items.getValue())
+ .get(position)
+ .toTransactionAccount();
+ final String oldCurrency = item.getCurrency();
- SimpleDate date = Globals.parseLedgerDate(text);
- this.setDate(date);
+ if (Misc.equalStrings(oldCurrency, newCurrency))
+ return;
+
+ List<Item> newList = copyList();
+ newList.get(position)
+ .toTransactionAccount()
+ .setCurrency(newCurrency);
+
+ setItems(newList);
+ }
+ public LiveData<Integer> getAccountCount() {
+ return accountCount;
+ }
+ public boolean accountListIsEmpty() {
+ List<Item> items = Objects.requireNonNull(this.items.getValue());
+
+ for (Item item : items) {
+ if (!(item instanceof TransactionAccount))
+ continue;
+
+ if (!((TransactionAccount) item).isEmpty())
+ return false;
}
- void observeDate(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
- @NonNull androidx.lifecycle.Observer<? super SimpleDate> observer) {
- this.date.observe(owner, observer);
+
+ return true;
+ }
+
+ public static class FocusInfo {
+ int position;
+ FocusedElement element;
+ public FocusInfo(int position, FocusedElement element) {
+ this.position = position;
+ this.element = element;
}
- void stopObservingDate(@NonNull androidx.lifecycle.Observer<? super SimpleDate> observer) {
- this.date.removeObserver(observer);
+ }
+
+ static abstract class Item {
+ private static int idDispenser = 0;
+ protected int id;
+ private Item() {
+ synchronized (Item.class) {
+ id = ++idDispenser;
+ }
}
- public String getDescription() {
- ensureType(ItemType.generalData);
- return description.getValue();
+ public static Item from(Item origin) {
+ if (origin instanceof TransactionHead)
+ 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 void setDescription(String description) {
- ensureType(ItemType.generalData);
- this.description.setValue(description);
+ public int getId() {
+ return id;
}
- void observeDescription(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
- @NonNull androidx.lifecycle.Observer<? super String> observer) {
- this.description.observe(owner, observer);
+ public abstract ItemType getType();
+ public TransactionHead toTransactionHead() {
+ if (this instanceof TransactionHead)
+ return (TransactionHead) this;
+
+ throw new IllegalStateException("Wrong item type " + this);
}
- void stopObservingDescription(
- @NonNull androidx.lifecycle.Observer<? super String> observer) {
- this.description.removeObserver(observer);
+ public TransactionAccount toTransactionAccount() {
+ if (this instanceof TransactionAccount)
+ return (TransactionAccount) this;
+
+ throw new IllegalStateException("Wrong item type " + this);
}
- public String getTransactionComment() {
- ensureType(ItemType.generalData);
- return comment.getValue();
+ public boolean equalContents(@Nullable Object item) {
+ if (item == null)
+ return false;
+
+ if (!getClass().equals(item.getClass()))
+ return false;
+
+ // shortcut - comparing same instance
+ if (item == this)
+ return true;
+
+ if (this instanceof TransactionHead)
+ 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);
}
- public void setTransactionComment(String transactionComment) {
- ensureType(ItemType.generalData);
- this.comment.setValue(transactionComment);
+ }
+
+
+//==========================================================================================
+
+ public static class TransactionHead extends Item {
+ private SimpleDate date;
+ private String description;
+ private String comment;
+ TransactionHead(String description) {
+ super();
+ this.description = description;
}
- void observeTransactionComment(@NonNull @NotNull LifecycleOwner owner,
- @NonNull Observer<? super String> observer) {
- ensureType(ItemType.generalData);
- this.comment.observe(owner, observer);
+ public TransactionHead(TransactionHead origin) {
+ id = origin.id;
+ date = origin.date;
+ description = origin.description;
+ comment = origin.comment;
}
- void stopObservingTransactionComment(@NonNull Observer<? super String> observer) {
- this.comment.removeObserver(observer);
+ public SimpleDate getDate() {
+ return date;
}
- public LedgerTransactionAccount getAccount() {
- ensureType(ItemType.transactionRow);
- return account;
+ public void setDate(SimpleDate date) {
+ this.date = date;
}
- public void setAccountName(String name) {
- account.setAccountName(name);
+ public void setDate(String text) throws ParseException {
+ if (Misc.emptyIsNull(text) == null) {
+ date = null;
+ return;
+ }
+
+ date = Globals.parseLedgerDate(text);
}
/**
* getFormattedDate()
String getFormattedDate() {
if (date == null)
return null;
- SimpleDate d = date.getValue();
- if (d == null)
- return null;
Calendar today = GregorianCalendar.getInstance();
- 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.YEAR) != date.year) {
+ return String.format(Locale.US, "%d/%02d/%02d", date.year, date.month, date.day);
}
- if (today.get(Calendar.MONTH) != d.month - 1) {
- return String.format(Locale.US, "%d/%02d", d.month, d.day);
+ if (today.get(Calendar.MONTH) + 1 != date.month) {
+ return String.format(Locale.US, "%d/%02d", date.month, date.day);
}
- return String.valueOf(d.day);
+ return String.valueOf(date.day);
}
- void observeEditableFlag(NewTransactionActivity activity, Observer<Boolean> observer) {
- editable.observe(activity, observer);
+ @NonNull
+ @Override
+ public String toString() {
+ @SuppressLint("DefaultLocale") StringBuilder b = new StringBuilder(
+ String.format("id:%d/%s", id, Integer.toHexString(hashCode())));
+
+ if (TextUtils.isEmpty(description))
+ b.append(" «no description»");
+ else
+ b.append(String.format(" descr'%s'", description));
+
+ if (date != null)
+ b.append(String.format("@%s", date.toString()));
+
+ if (!TextUtils.isEmpty(comment))
+ b.append(String.format(" /%s/", comment));
+
+ return b.toString();
}
- void stopObservingEditableFlag(Observer<Boolean> observer) {
- editable.removeObserver(observer);
+ public String getDescription() {
+ return description;
}
- void observeComment(NewTransactionActivity activity, Observer<String> observer) {
- comment.observe(activity, observer);
+ public void setDescription(String description) {
+ this.description = description;
}
- void stopObservingComment(Observer<String> observer) {
- comment.removeObserver(observer);
+ public String getComment() {
+ return comment;
}
public void setComment(String comment) {
- getAccount().setComment(comment);
- this.comment.postValue(comment);
+ this.comment = comment;
}
- public Currency getCurrency() {
- return this.currency.getValue();
+ @Override
+ public ItemType getType() {
+ return ItemType.generalData;
}
- 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);
- }
+ public LedgerTransaction asLedgerTransaction() {
+ return new LedgerTransaction(null, date, description, Data.getProfile());
+ }
+ public boolean equalContents(TransactionHead other) {
+ if (other == null)
+ return false;
+
+ return Objects.equals(date, other.date) &&
+ TextUtils.equals(description, other.description) &&
+ TextUtils.equals(comment, other.comment);
+ }
+ }
+
+ 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;
+ private String comment;
+ private String currency;
+ private float amount;
+ private boolean amountSet;
+ private boolean amountValid = true;
+ private FocusedElement focusedElement = FocusedElement.Account;
+ private boolean amountHintIsSet = false;
+ public TransactionAccount(TransactionAccount origin) {
+ id = origin.id;
+ accountName = origin.accountName;
+ amount = origin.amount;
+ amountSet = origin.amountSet;
+ amountHint = origin.amountHint;
+ amountHintIsSet = origin.amountHintIsSet;
+ comment = origin.comment;
+ currency = origin.currency;
+ amountValid = origin.amountValid;
+ focusedElement = origin.focusedElement;
+ }
+ public TransactionAccount(LedgerTransactionAccount account) {
+ super();
+ currency = account.getCurrency();
+ amount = account.getAmount();
+ }
+ public TransactionAccount(String accountName) {
+ super();
+ this.accountName = accountName;
+ }
+ public TransactionAccount(String accountName, String currency) {
+ super();
+ this.accountName = accountName;
+ this.currency = currency;
+ }
+ public boolean isAmountSet() {
+ return amountSet;
+ }
+ public String getAccountName() {
+ return accountName;
+ }
+ public void setAccountName(String accountName) {
+ this.accountName = accountName;
+ }
+ public float getAmount() {
+ if (!amountSet)
+ throw new IllegalStateException("Amount is not set");
+ return amount;
+ }
+ public void setAmount(float amount) {
+ this.amount = amount;
+ amountSet = true;
+ }
+ public void resetAmount() {
+ amountSet = false;
+ }
+ @Override
+ public ItemType getType() {
+ return ItemType.transactionRow;
+ }
+ public String getAmountHint() {
+ return amountHint;
+ }
+ public void setAmountHint(String amountHint) {
+ this.amountHint = amountHint;
+ amountHintIsSet = !TextUtils.isEmpty(amountHint);
}
- void observeCurrency(NewTransactionActivity activity, Observer<Currency> observer) {
- currency.observe(activity, observer);
+ public String getComment() {
+ return comment;
}
- void stopObservingCurrency(Observer<Currency> observer) {
- currency.removeObserver(observer);
+ public void setComment(String comment) {
+ this.comment = comment;
+ }
+ public String getCurrency() {
+ return currency;
+ }
+ public void setCurrency(String currency) {
+ this.currency = currency;
+ }
+ public boolean isAmountValid() {
+ return amountValid;
+ }
+ public void setAmountValid(boolean amountValid) {
+ this.amountValid = amountValid;
+ }
+ public FocusedElement getFocusedElement() {
+ return focusedElement;
}
- boolean isBottomFiller() {
- return this.type == ItemType.bottomFiller;
+ public void setFocusedElement(FocusedElement focusedElement) {
+ this.focusedElement = focusedElement;
}
- boolean isAmountHintSet() {
+ public boolean isAmountHintSet() {
return amountHintIsSet;
}
- void validateAmount() {
- amountValid.setValue(true);
+ public void setAmountHintIsSet(boolean amountHintIsSet) {
+ this.amountHintIsSet = amountHintIsSet;
+ }
+ public boolean isEmpty() {
+ return !amountSet && Misc.emptyIsNull(accountName) == null &&
+ Misc.emptyIsNull(comment) == null;
+ }
+ @SuppressLint("DefaultLocale")
+ @Override
+ public String toString() {
+ StringBuilder b = new StringBuilder();
+ b.append(String.format("id:%d/%s", id, Integer.toHexString(hashCode())));
+ if (!TextUtils.isEmpty(accountName))
+ b.append(String.format(" acc'%s'", accountName));
+
+ if (amountSet)
+ b.append(String.format(" %4.2f", amount));
+ else if (amountHintIsSet)
+ b.append(String.format(" (%s)", amountHint));
+
+ if (!TextUtils.isEmpty(currency))
+ b.append(" ")
+ .append(currency);
+
+ if (!TextUtils.isEmpty(comment))
+ b.append(String.format(" /%s/", comment));
+
+ 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);
+ Logger.debug("new-trans",
+ String.format("Comparing {%s} and {%s}: %s", this.toString(), other.toString(),
+ equal));
+ return equal;
+ }
+ }
+
+ private static class BalanceForCurrency {
+ private final HashMap<String, Float> hashMap = new HashMap<>();
+ float get(String currencyName) {
+ Float f = hashMap.get(currencyName);
+ if (f == null) {
+ f = 0f;
+ hashMap.put(currencyName, f);
+ }
+ return f;
+ }
+ void add(String currencyName, float amount) {
+ hashMap.put(currencyName, get(currencyName) + amount);
+ }
+ Set<String> currencies() {
+ return hashMap.keySet();
+ }
+ boolean containsCurrency(String currencyName) {
+ return hashMap.containsKey(currencyName);
+ }
+ }
+
+ private static class ItemsForCurrency {
+ private final HashMap<String, List<Item>> hashMap = new HashMap<>();
+ @NonNull
+ List<NewTransactionModel.Item> getList(@Nullable String currencyName) {
+ List<NewTransactionModel.Item> list = hashMap.get(currencyName);
+ if (list == null) {
+ list = new ArrayList<>();
+ hashMap.put(currencyName, list);
+ }
+ return list;
}
- void invalidateAmount() {
- amountValid.setValue(false);
+ void add(@Nullable String currencyName, @NonNull NewTransactionModel.Item item) {
+ getList(currencyName).add(item);
}
- void observeAmountValidity(NewTransactionActivity activity, Observer<Boolean> observer) {
- amountValid.observe(activity, observer);
+ int size(@Nullable String currencyName) {
+ return this.getList(currencyName)
+ .size();
}
- void stopObservingAmountValidity(Observer<Boolean> observer) {
- amountValid.removeObserver(observer);
+ Set<String> currencies() {
+ return hashMap.keySet();
}
}
}
TemplateHeaderDAO dao = DB.get()
.getTemplateDAO();
- dao.getTemplateWitAccountsAsync(templateId, template -> {
+ dao.getTemplateWithAccountsAsync(templateId, template -> {
TemplateWithAccounts copy = TemplateWithAccounts.from(template);
dao.deleteAsync(template.header, () -> {
navController.popBackStack(R.id.templateListFragment, false);
/>
</androidx.constraintlayout.widget.ConstraintLayout>
-
+ <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_marginEnd="@dimen/fab_margin"
+ android:layout_marginBottom="@dimen/fab_margin"
+ android:contentDescription="@string/add_button_description"
+ android:padding="@dimen/fab_margin"
+ android:tint="?android:attr/colorBackground"
+ android:visibility="visible"
+ app:backgroundTint="?colorSecondary"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:srcCompat="@drawable/ic_save_white_24dp"
+ />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
</androidx.constraintlayout.widget.ConstraintLayout>
- <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_marginEnd="@dimen/fab_margin"
- android:layout_marginBottom="@dimen/fab_margin"
- android:padding="@dimen/fab_margin"
- android:tint="?android:attr/colorBackground"
- android:visibility="visible"
- app:backgroundTint="?colorSecondary"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:srcCompat="@drawable/ic_save_white_24dp" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file