]> git.ktnx.net Git - mobile-ledger.git/commitdiff
rework new transaction activity/model/etc with proper concept separation
authorDamyan Ivanov <dam+mobileledger@ktnx.net>
Sat, 27 Feb 2021 20:06:18 +0000 (20:06 +0000)
committerDamyan Ivanov <dam+mobileledger@ktnx.net>
Mon, 1 Mar 2021 06:00:42 +0000 (06:00 +0000)
activity monitors the model and sends changes to adapter; view holder
propagates changes to the model

this makes it possible to properly handle QR scan results in the
activity without dirty tricks

app/src/main/java/net/ktnx/mobileledger/dao/TemplateHeaderDAO.java
app/src/main/java/net/ktnx/mobileledger/db/AccountAutocompleteAdapter.java
app/src/main/java/net/ktnx/mobileledger/model/MatchedTemplate.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionActivity.java
app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionFragment.java
app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionItemHolder.java
app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionItemsAdapter.java
app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionModel.java
app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplatesActivity.java
app/src/main/res/layout/activity_new_transaction.xml
app/src/main/res/layout/fragment_new_transaction.xml

index 2d3d7ee48884eb3012218dcb70f1322d3fb8d793..bfc2a562d156d0e795213b7aefc52af98fecbd6d 100644 (file)
@@ -106,7 +106,7 @@ public abstract class TemplateHeaderDAO {
         }
     }
 
-    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>() {
index ecc21db74e69e624c28d56f02a53bc696a451262..6cf1dc8c3c16808c7b9f310c1b04229d4d5fc6de 100644 (file)
@@ -24,6 +24,7 @@ import android.widget.Filter;
 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;
@@ -37,6 +38,10 @@ public class AccountAutocompleteAdapter extends ArrayAdapter<String> {
     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;
     }
diff --git a/app/src/main/java/net/ktnx/mobileledger/model/MatchedTemplate.java b/app/src/main/java/net/ktnx/mobileledger/model/MatchedTemplate.java
new file mode 100644 (file)
index 0000000..607d6c7
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * 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;
+    }
+}
index 413365db4f48e8db860a3e595d738f1da5206de8..ab28ca16d05a2ff43d5d57404692c17a8f021177 100644 (file)
 
 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));
@@ -63,6 +90,23 @@ public class NewTransactionActivity extends ProfileThemedActivity implements Tas
                .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() {
@@ -95,7 +139,7 @@ public class NewTransactionActivity extends ProfileThemedActivity implements Tas
         try {
 
             SendTransactionTask saver =
-                    new SendTransactionTask(this, mProfile, model.getSimulateSave());
+                    new SendTransactionTask(this, mProfile, model.getSimulateSaveFlag());
             saver.execute(tr);
         }
         catch (Exception e) {
@@ -121,11 +165,12 @@ public class NewTransactionActivity extends ProfileThemedActivity implements Tas
                 .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;
     }
@@ -149,4 +194,229 @@ public class NewTransactionActivity extends ProfileThemedActivity implements Tas
         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);
+    }
 }
index 5edf252d72858ceccacef8663fe388e897a7810f..d1f19c46bc51c7018904d874cda141e411c095cc 100644 (file)
 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;
@@ -37,39 +34,22 @@ import androidx.annotation.Nullable;
 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
@@ -79,310 +59,15 @@ import java.util.regex.Pattern;
 
 // 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);
@@ -395,7 +80,7 @@ public class NewTransactionFragment extends QRScanCapableFragment {
 
         menu.findItem(R.id.action_reset_new_transaction_activity)
             .setOnMenuItemClickListener(item -> {
-                listAdapter.reset();
+                viewModel.reset();
                 return true;
             });
 
@@ -405,7 +90,8 @@ public class NewTransactionFragment extends QRScanCapableFragment {
             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 -> {
@@ -413,11 +99,14 @@ public class NewTransactionFragment extends QRScanCapableFragment {
             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);
@@ -445,6 +134,9 @@ public class NewTransactionFragment extends QRScanCapableFragment {
         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));
@@ -453,25 +145,6 @@ public class NewTransactionFragment extends QRScanCapableFragment {
             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();
@@ -512,73 +185,45 @@ public class NewTransactionFragment extends QRScanCapableFragment {
         }
 
         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
index 3bf09d27e5e20a9738c203f3f6e05a9cdcdae5d2..a69a65af40a50cdd95e5a8f0f7da3b5f605c1d59 100644 (file)
@@ -25,21 +25,20 @@ import android.text.TextWatcher;
 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;
@@ -52,41 +51,24 @@ import net.ktnx.mobileledger.utils.SimpleDate;
 
 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);
@@ -114,7 +96,6 @@ class NewTransactionItemHolder extends RecyclerView.ViewHolder
                 syncingData = true;
                 try {
                     final int pos = getAdapterPosition();
-                    adapter.updateFocusedItem(pos);
                     if (id == R.id.account_row_acc_name) {
                         adapter.noteFocusIsOnAccount(pos);
                     }
@@ -130,6 +111,8 @@ class NewTransactionItemHolder extends RecyclerView.ViewHolder
                     else if (id == R.id.new_transaction_description) {
                         adapter.noteFocusIsOnDescription(pos);
                     }
+                    else
+                        throw new IllegalStateException("Where is the focus?");
                 }
                 finally {
                     syncingData = wasSyncing;
@@ -150,18 +133,18 @@ class NewTransactionItemHolder extends RecyclerView.ViewHolder
         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 = ".";
 
@@ -184,229 +167,207 @@ class NewTransactionItemHolder extends RecyclerView.ViewHolder
                 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;
@@ -515,62 +476,60 @@ class NewTransactionItemHolder extends RecyclerView.ViewHolder
      * 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;
@@ -591,117 +550,96 @@ class NewTransactionItemHolder extends RecyclerView.ViewHolder
                                                .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);
index 29b4e9aa68997b472710ae74737a062fc7f48c8b..f8686dcfa72c4690320ebd962d4c19ca951e489a 100644 (file)
 
 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;
 
@@ -111,35 +107,25 @@ class NewTransactionItemsAdapter extends RecyclerView.Adapter<NewTransactionItem
                                   @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) {
@@ -152,230 +138,42 @@ class NewTransactionItemsAdapter extends RecyclerView.Adapter<NewTransactionItem
     @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++;
@@ -385,313 +183,12 @@ class NewTransactionItemsAdapter extends RecyclerView.Adapter<NewTransactionItem
             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);
     }
 }
index b96b917a01a622e641cefa71182f4222099f1ae4..c84c26d20bf21b532ce6a4e982e688f8841b5a18 100644 (file)
 
 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;
@@ -36,171 +48,301 @@ 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);
@@ -215,178 +357,580 @@ public class NewTransactionModel extends ViewModel {
         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()
@@ -396,75 +940,271 @@ public class NewTransactionModel extends ViewModel {
         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();
         }
     }
 }
index b277489e4eccc0b03af7c169ed9a33ae166c81aa..410b7d62878ac4027ce418f55280a5175674ea23 100644 (file)
@@ -145,7 +145,7 @@ public class TemplatesActivity extends CrashReportingActivity
         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);
index 2461b3be3bc12db39b762a8acbeb016863849305..995f9386c401cf713c71412feb5d4da7222d19b7 100644 (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: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
index 0e60c4fc0ff3f916919a51df456e41ad4e6f43df..6ddb6b10fb8e5f0568574317acad5be9916a6c62 100644 (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