]> git.ktnx.net Git - mobile-ledger.git/commitdiff
shuffle some classes under proper packages
authorDamyan Ivanov <dam+mobileledger@ktnx.net>
Tue, 12 Jan 2021 17:14:02 +0000 (19:14 +0200)
committerDamyan Ivanov <dam+mobileledger@ktnx.net>
Tue, 12 Jan 2021 17:14:02 +0000 (19:14 +0200)
26 files changed:
app/src/main/AndroidManifest.xml
app/src/main/java/net/ktnx/mobileledger/async/AsyncCrasher.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/model/MobileLedgerProfile.java
app/src/main/java/net/ktnx/mobileledger/ui/activity/AsyncCrasher.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/ui/activity/MainActivity.java
app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionActivity.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionFragment.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionItemHolder.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionItemsAdapter.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionModel.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/ui/activity/PatternsActivity.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/ui/activity/ProfileDetailActivity.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionActivity.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionFragment.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionItemHolder.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionItemsAdapter.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionModel.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternsActivity.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailActivity.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailFragment.java
app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfilesRecyclerViewAdapter.java
app/src/main/res/layout/activity_new_transaction.xml
app/src/main/res/layout/activity_patterns.xml
app/src/main/res/layout/activity_profile_detail.xml
app/src/main/res/layout/fragment_new_transaction.xml
app/src/main/res/navigation/new_transaction_navigation.xml

index a865bf9d67aca8cca0ab9cbf41bf08fad098edb0..21a7e5ad1e6770644286753f1cd66819b61465b1 100644 (file)
@@ -32,7 +32,7 @@
         android:supportsRtl="true"
         tools:ignore="GoogleAppIndexingWarning">
         <activity
-            android:name=".ui.activity.PatternsActivity"
+            android:name=".ui.patterns.PatternsActivity"
             android:label="@string/title_activity_patterns"
             android:theme="@style/AppTheme.default" />
         <activity
             android:label="@string/app_name"
             android:theme="@style/AppTheme.default" />
         <activity
-            android:name=".ui.activity.NewTransactionActivity"
+            android:name=".ui.new_transaction.NewTransactionActivity"
             android:label="@string/title_activity_new_transaction"
             android:parentActivityName=".ui.activity.MainActivity"
             android:theme="@style/AppTheme.default"
             android:windowSoftInputMode="stateVisible" />
         <activity
-            android:name=".ui.activity.ProfileDetailActivity"
+            android:name=".ui.profiles.ProfileDetailActivity"
             android:label="@string/title_profile_details"
             android:parentActivityName=".ui.activity.MainActivity"
             android:windowSoftInputMode="stateVisible" />
diff --git a/app/src/main/java/net/ktnx/mobileledger/async/AsyncCrasher.java b/app/src/main/java/net/ktnx/mobileledger/async/AsyncCrasher.java
new file mode 100644 (file)
index 0000000..0ec9bb2
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+ * 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.async;
+
+import android.os.AsyncTask;
+
+public class AsyncCrasher extends AsyncTask<Void, Void, Void> {
+    @Override
+    protected Void doInBackground(Void... voids) {
+        throw new RuntimeException("Simulated crash");
+    }
+}
index 21d15d162ebb1743586bdb71c5f1b210e905d16f..0e8c04485262c6fa8c603408051807914557b039 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2020 Damyan Ivanov.
+ * 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
@@ -32,7 +32,7 @@ import net.ktnx.mobileledger.App;
 import net.ktnx.mobileledger.R;
 import net.ktnx.mobileledger.async.DbOpQueue;
 import net.ktnx.mobileledger.json.API;
-import net.ktnx.mobileledger.ui.activity.ProfileDetailActivity;
+import net.ktnx.mobileledger.ui.profiles.ProfileDetailActivity;
 import net.ktnx.mobileledger.ui.profiles.ProfileDetailFragment;
 import net.ktnx.mobileledger.utils.Logger;
 import net.ktnx.mobileledger.utils.Misc;
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/AsyncCrasher.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/AsyncCrasher.java
deleted file mode 100644 (file)
index ce02ff5..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright © 2019 Damyan Ivanov.
- * This file is part of MoLe.
- * MoLe is free software: you can distribute it and/or modify it
- * under the term of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * MoLe is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger.ui.activity;
-
-import android.os.AsyncTask;
-
-class AsyncCrasher extends AsyncTask<Void, Void, Void> {
-    @Override
-    protected Void doInBackground(Void... voids) {
-        throw new RuntimeException("Simulated crash");
-    }
-}
index da39640ad45ab12bd31c637f4dbbf7887bc98748..3ed892913f5ffd85b14bda661c14fa031ea227b4 100644 (file)
@@ -54,6 +54,8 @@ import net.ktnx.mobileledger.model.Data;
 import net.ktnx.mobileledger.model.MobileLedgerProfile;
 import net.ktnx.mobileledger.ui.MainModel;
 import net.ktnx.mobileledger.ui.account_summary.AccountSummaryFragment;
+import net.ktnx.mobileledger.ui.new_transaction.NewTransactionActivity;
+import net.ktnx.mobileledger.ui.patterns.PatternsActivity;
 import net.ktnx.mobileledger.ui.profiles.ProfilesRecyclerViewAdapter;
 import net.ktnx.mobileledger.ui.transaction_list.TransactionListFragment;
 import net.ktnx.mobileledger.utils.Colors;
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionActivity.java
deleted file mode 100644 (file)
index 7499641..0000000
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * Copyright © 2020 Damyan Ivanov.
- * This file is part of MoLe.
- * MoLe is free software: you can distribute it and/or modify it
- * under the term of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * MoLe is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger.ui.activity;
-
-import android.os.Bundle;
-import android.util.TypedValue;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-
-import androidx.appcompat.widget.Toolbar;
-import androidx.lifecycle.ViewModelProvider;
-import androidx.navigation.NavController;
-import androidx.navigation.fragment.NavHostFragment;
-
-import net.ktnx.mobileledger.BuildConfig;
-import net.ktnx.mobileledger.R;
-import net.ktnx.mobileledger.async.SendTransactionTask;
-import net.ktnx.mobileledger.async.TaskCallback;
-import net.ktnx.mobileledger.model.Data;
-import net.ktnx.mobileledger.model.LedgerTransaction;
-
-import java.util.Objects;
-
-import static net.ktnx.mobileledger.utils.Logger.debug;
-
-public class NewTransactionActivity extends ProfileThemedActivity implements TaskCallback,
-        NewTransactionFragment.OnNewTransactionFragmentInteractionListener {
-    private NavController navController;
-    private NewTransactionModel model;
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setContentView(R.layout.activity_new_transaction);
-        Toolbar toolbar = findViewById(R.id.toolbar);
-        setSupportActionBar(toolbar);
-        Data.observeProfile(this,
-                mobileLedgerProfile -> toolbar.setSubtitle(mobileLedgerProfile.getName()));
-
-        NavHostFragment navHostFragment = (NavHostFragment) Objects.requireNonNull(
-                getSupportFragmentManager().findFragmentById(R.id.new_transaction_nav));
-        navController = navHostFragment.getNavController();
-
-        Objects.requireNonNull(getSupportActionBar())
-               .setDisplayHomeAsUpEnabled(true);
-
-        model = new ViewModelProvider(this).get(NewTransactionModel.class);
-    }
-    @Override
-    protected void initProfile() {
-        String profileUUID = getIntent().getStringExtra("profile_uuid");
-
-        if (profileUUID != null) {
-            mProfile = Data.getProfile(profileUUID);
-            if (mProfile == null)
-                finish();
-            Data.setCurrentProfile(mProfile);
-        }
-        else
-            super.initProfile();
-    }
-    @Override
-    public void finish() {
-        super.finish();
-        overridePendingTransition(R.anim.dummy, R.anim.slide_out_down);
-    }
-    @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        if (item.getItemId() == android.R.id.home) {
-            finish();
-            return true;
-        }
-        return super.onOptionsItemSelected(item);
-    }
-    public void onTransactionSave(LedgerTransaction tr) {
-        navController.navigate(R.id.action_newTransactionFragment_to_newTransactionSavingFragment);
-        try {
-
-            SendTransactionTask saver =
-                    new SendTransactionTask(this, mProfile, model.getSimulateSave());
-            saver.execute(tr);
-        }
-        catch (Exception e) {
-            debug("new-transaction", "Unknown error", e);
-
-            Bundle b = new Bundle();
-            b.putString("error", "unknown error");
-            navController.navigate(R.id.newTransactionFragment, b);
-        }
-    }
-    public void simulateCrash(MenuItem item) {
-        debug("crash", "Will crash intentionally");
-        new AsyncCrasher().execute();
-    }
-    public boolean onCreateOptionsMenu(Menu menu) {
-        // Inflate the menu; this adds items to the action bar if it is present.
-        getMenuInflater().inflate(R.menu.new_transaction, menu);
-
-        if (BuildConfig.DEBUG) {
-            menu.findItem(R.id.action_simulate_crash)
-                .setVisible(true);
-            menu.findItem(R.id.action_simulate_save)
-                .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);
-        });
-
-        return true;
-    }
-
-
-    public int dp2px(float dp) {
-        return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
-                getResources().getDisplayMetrics()));
-    }
-    @Override
-    public void done(String error) {
-        Bundle b = new Bundle();
-        if (error != null) {
-            b.putString("error", error);
-            navController.navigate(R.id.action_newTransactionSavingFragment_Failure, b);
-        }
-        else
-            navController.navigate(R.id.action_newTransactionSavingFragment_Success, b);
-    }
-    public void toggleSimulateSave(MenuItem item) {
-        model.toggleSimulateSave();
-    }
-
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionFragment.java
deleted file mode 100644 (file)
index d9d844a..0000000
+++ /dev/null
@@ -1,377 +0,0 @@
-/*
- * Copyright © 2021 Damyan Ivanov.
- * This file is part of MoLe.
- * MoLe is free software: you can distribute it and/or modify it
- * under the term of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * MoLe is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger.ui.activity;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.os.Bundle;
-import android.renderscript.RSInvalidStateException;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ProgressBar;
-
-import androidx.activity.result.ActivityResultLauncher;
-import androidx.activity.result.contract.ActivityResultContract;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AlertDialog;
-import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentActivity;
-import androidx.lifecycle.ViewModelProvider;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.google.android.material.floatingactionbutton.FloatingActionButton;
-import com.google.android.material.snackbar.Snackbar;
-
-import net.ktnx.mobileledger.R;
-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.utils.Logger;
-import net.ktnx.mobileledger.utils.Misc;
-import net.ktnx.mobileledger.utils.SimpleDate;
-
-import org.jetbrains.annotations.NotNull;
-
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * A simple {@link Fragment} subclass.
- * Activities that contain this fragment must implement the
- * {@link OnNewTransactionFragmentInteractionListener} interface
- * to handle interaction events.
- */
-
-// TODO: offer to undo account remove-on-swipe
-
-public class NewTransactionFragment extends Fragment {
-    private NewTransactionItemsAdapter listAdapter;
-    private NewTransactionModel viewModel;
-    final ActivityResultLauncher<Void> scanQrLauncher =
-            registerForActivityResult(new ActivityResultContract<Void, String>() {
-                @NonNull
-                @Override
-                public Intent createIntent(@NonNull Context context, Void input) {
-                    final Intent intent = new Intent("com.google.zxing.client.android.SCAN");
-                    intent.putExtra("SCAN_MODE", "QR_CODE_MODE");
-                    return intent;
-                }
-                @Override
-                public String parseResult(int resultCode, @Nullable Intent intent) {
-                    if (resultCode == Activity.RESULT_CANCELED)
-                        return null;
-                    return intent.getStringExtra("SCAN_RESULT");
-                }
-            }, this::onQrScanned);
-    private FloatingActionButton fab;
-    private OnNewTransactionFragmentInteractionListener mListener;
-    private MobileLedgerProfile mProfile;
-    public NewTransactionFragment() {
-        // Required empty public constructor
-        setHasOptionsMenu(true);
-    }
-    private void onQrScanned(String text) {
-        Logger.debug("qr", String.format("Got QR scan result [%s]", text));
-        Pattern p =
-                Pattern.compile("^(\\d+)\\*(\\d+)\\*(\\d+)-(\\d+)-(\\d+)\\*([:\\d]+)\\*([\\d.]+)$");
-        Matcher m = p.matcher(text);
-        if (m.matches()) {
-            float amount = Float.parseFloat(m.group(7));
-            viewModel.setDate(
-                    new SimpleDate(Integer.parseInt(m.group(3)), Integer.parseInt(m.group(4)),
-                            Integer.parseInt(m.group(5))));
-
-            if (viewModel.accountsInInitialState()) {
-                {
-                    NewTransactionModel.Item firstItem = viewModel.getItem(1);
-                    if (firstItem == null) {
-                        viewModel.addAccount(new LedgerTransactionAccount("разход:пазар"));
-                        listAdapter.notifyItemInserted(viewModel.items.size() - 1);
-                    }
-                    else {
-                        firstItem.setAccountName("разход:пазар");
-                        firstItem.getAccount()
-                                 .resetAmount();
-                        listAdapter.notifyItemChanged(1);
-                    }
-                }
-                {
-                    NewTransactionModel.Item secondItem = viewModel.getItem(2);
-                    if (secondItem == null) {
-                        viewModel.addAccount(
-                                new LedgerTransactionAccount("актив:кеш:дам", -amount, null, null));
-                        listAdapter.notifyItemInserted(viewModel.items.size() - 1);
-                    }
-                    else {
-                        secondItem.setAccountName("актив:кеш:дам");
-                        secondItem.getAccount()
-                                  .setAmount(-amount);
-                        listAdapter.notifyItemChanged(2);
-                    }
-                }
-            }
-            else {
-                viewModel.addAccount(new LedgerTransactionAccount("разход:пазар"));
-                viewModel.addAccount(
-                        new LedgerTransactionAccount("актив:кеш:дам", -amount, null, null));
-                listAdapter.notifyItemRangeInserted(viewModel.items.size() - 1, 2);
-            }
-
-            listAdapter.checkTransactionSubmittable();
-        }
-    }
-    @Override
-    public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
-        super.onCreateOptionsMenu(menu, inflater);
-        final FragmentActivity activity = getActivity();
-
-        inflater.inflate(R.menu.new_transaction_fragment, menu);
-
-        menu.findItem(R.id.scan_qr)
-            .setOnMenuItemClickListener(this::onScanQrAction);
-
-        menu.findItem(R.id.action_reset_new_transaction_activity)
-            .setOnMenuItemClickListener(item -> {
-                listAdapter.reset();
-                return true;
-            });
-
-        final MenuItem toggleCurrencyItem = menu.findItem(R.id.toggle_currency);
-        toggleCurrencyItem.setOnMenuItemClickListener(item -> {
-            viewModel.toggleCurrencyVisible();
-            return true;
-        });
-        if (activity != null)
-            viewModel.showCurrency.observe(activity, toggleCurrencyItem::setChecked);
-
-        final MenuItem toggleCommentsItem = menu.findItem(R.id.toggle_comments);
-        toggleCommentsItem.setOnMenuItemClickListener(item -> {
-            viewModel.toggleShowComments();
-            return true;
-        });
-        if (activity != null)
-            viewModel.showComments.observe(activity, toggleCommentsItem::setChecked);
-    }
-    private boolean onScanQrAction(MenuItem item) {
-        try {
-            scanQrLauncher.launch(null);
-        }
-        catch (Exception e) {
-            Logger.debug("qr", "Error launching QR scanner", e);
-        }
-
-        return true;
-    }
-    @Override
-    public View onCreateView(LayoutInflater inflater, ViewGroup container,
-                             Bundle savedInstanceState) {
-        // Inflate the layout for this fragment
-        return inflater.inflate(R.layout.fragment_new_transaction, container, false);
-    }
-
-    @Override
-    public void onViewCreated(@NotNull View view, @Nullable Bundle savedInstanceState) {
-        super.onViewCreated(view, savedInstanceState);
-        FragmentActivity activity = getActivity();
-        if (activity == null)
-            throw new RSInvalidStateException(
-                    "getActivity() returned null within onActivityCreated()");
-
-        viewModel = new ViewModelProvider(activity).get(NewTransactionModel.class);
-        viewModel.observeDataProfile(this);
-        mProfile = Data.getProfile();
-        listAdapter = new NewTransactionItemsAdapter(viewModel, mProfile);
-
-        RecyclerView list = activity.findViewById(R.id.new_transaction_accounts);
-        list.setAdapter(listAdapter);
-        list.setLayoutManager(new LinearLayoutManager(activity));
-
-        Data.observeProfile(getViewLifecycleOwner(), profile -> {
-            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.fab);
-        fab.setOnClickListener(v -> onFabPressed());
-
-        boolean keep = false;
-
-        Bundle args = getArguments();
-        if (args != null) {
-            String error = args.getString("error");
-            if (error != null) {
-                Logger.debug("new-trans-f", String.format("Got error: %s", error));
-
-                Context context = getContext();
-                if (context != null) {
-                    AlertDialog.Builder builder = new AlertDialog.Builder(context);
-                    final Resources resources = context.getResources();
-                    final StringBuilder message = new StringBuilder();
-                    message.append(resources.getString(R.string.err_json_send_error_head));
-                    message.append("\n\n");
-                    message.append(error);
-                    if (mProfile.getApiVersion()
-                                .equals(API.auto))
-                        message.append(
-                                resources.getString(R.string.err_json_send_error_unsupported));
-                    else {
-                        message.append(resources.getString(R.string.err_json_send_error_tail));
-                        builder.setPositiveButton(R.string.btn_profile_options, (dialog, which) -> {
-                            Logger.debug("error", "will start profile editor");
-                            MobileLedgerProfile.startEditProfileActivity(context, mProfile);
-                        });
-                    }
-                    builder.setMessage(message);
-                    builder.create()
-                           .show();
-                }
-                else {
-                    Snackbar.make(list, error, Snackbar.LENGTH_INDEFINITE)
-                            .show();
-                }
-                keep = true;
-            }
-        }
-
-        int focused = 0;
-        if (savedInstanceState != null) {
-            keep |= savedInstanceState.getBoolean("keep", true);
-            focused = savedInstanceState.getInt("focused", 0);
-        }
-
-        if (!keep)
-            viewModel.reset();
-        else {
-            viewModel.setFocusedItem(focused);
-        }
-
-        ProgressBar p = activity.findViewById(R.id.progressBar);
-        viewModel.observeBusyFlag(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);
-        });
-    }
-    @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);
-        }
-    }
-
-    @Override
-    public void onAttach(@NotNull Context context) {
-        super.onAttach(context);
-        if (context instanceof OnNewTransactionFragmentInteractionListener) {
-            mListener = (OnNewTransactionFragmentInteractionListener) context;
-        }
-        else {
-            throw new RuntimeException(
-                    context.toString() + " must implement OnFragmentInteractionListener");
-        }
-    }
-
-    @Override
-    public void onDetach() {
-        super.onDetach();
-        mListener = null;
-    }
-
-    /**
-     * This interface must be implemented by activities that contain this
-     * fragment to allow an interaction in this fragment to be communicated
-     * to the activity and potentially other fragments contained in that
-     * activity.
-     * <p>
-     * See the Android Training lesson <a href=
-     * "http://developer.android.com/training/basics/fragments/communicating.html"
-     * >Communicating with Other Fragments</a> for more information.
-     */
-    public interface OnNewTransactionFragmentInteractionListener {
-        void onTransactionSave(LedgerTransaction tr);
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionItemHolder.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionItemHolder.java
deleted file mode 100644 (file)
index 94d85e7..0000000
+++ /dev/null
@@ -1,711 +0,0 @@
-/*
- * Copyright © 2021 Damyan Ivanov.
- * This file is part of MoLe.
- * MoLe is free software: you can distribute it and/or modify it
- * under the term of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * MoLe is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger.ui.activity;
-
-import android.annotation.SuppressLint;
-import android.graphics.Typeface;
-import android.text.Editable;
-import android.text.TextUtils;
-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.model.Currency;
-import net.ktnx.mobileledger.model.Data;
-import net.ktnx.mobileledger.model.LedgerTransactionAccount;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
-import net.ktnx.mobileledger.ui.CurrencySelectorFragment;
-import net.ktnx.mobileledger.ui.DatePickerFragment;
-import net.ktnx.mobileledger.ui.TextViewClearHelper;
-import net.ktnx.mobileledger.utils.DimensionUtils;
-import net.ktnx.mobileledger.utils.Logger;
-import net.ktnx.mobileledger.utils.MLDB;
-import net.ktnx.mobileledger.utils.Misc;
-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.activity.NewTransactionModel.ItemType;
-
-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 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
-    NewTransactionItemHolder(@NonNull NewTransactionRowBinding b,
-                             NewTransactionItemsAdapter adapter) {
-        super(b.getRoot());
-        this.b = b;
-        new TextViewClearHelper().attachToTextView((EditText) b.comment);
-
-        b.newTransactionDescription.setNextFocusForwardId(View.NO_ID);
-        b.accountRowAccName.setNextFocusForwardId(View.NO_ID);
-        b.accountRowAccAmounts.setNextFocusForwardId(View.NO_ID); // magic!
-
-        b.newTransactionDate.setOnClickListener(v -> pickTransactionDate());
-
-        b.accountCommentButton.setOnClickListener(v -> {
-            b.comment.setVisibility(View.VISIBLE);
-            b.comment.requestFocus();
-        });
-
-        b.transactionCommentButton.setOnClickListener(v -> {
-            b.transactionComment.setVisibility(View.VISIBLE);
-            b.transactionComment.requestFocus();
-        });
-
-        mProfile = Data.getProfile();
-
-        View.OnFocusChangeListener focusMonitor = (v, hasFocus) -> {
-            final int id = v.getId();
-            if (hasFocus) {
-                boolean wasSyncing = syncingData;
-                syncingData = true;
-                try {
-                    final int pos = getAdapterPosition();
-                    adapter.updateFocusedItem(pos);
-                    if (id == R.id.account_row_acc_name) {
-                        adapter.noteFocusIsOnAccount(pos);
-                    }
-                    else if (id == R.id.account_row_acc_amounts) {
-                        adapter.noteFocusIsOnAmount(pos);
-                    }
-                    else if (id == R.id.comment) {
-                        adapter.noteFocusIsOnComment(pos);
-                    }
-                    else if (id == R.id.transaction_comment) {
-                        adapter.noteFocusIsOnTransactionComment(pos);
-                    }
-                    else if (id == R.id.new_transaction_description) {
-                        adapter.noteFocusIsOnDescription(pos);
-                    }
-                }
-                finally {
-                    syncingData = wasSyncing;
-                }
-            }
-
-            if (id == R.id.comment) {
-                commentFocusChanged(b.comment, hasFocus);
-            }
-            else if (id == R.id.transaction_comment) {
-                commentFocusChanged(b.transactionComment, hasFocus);
-            }
-        };
-
-        b.newTransactionDescription.setOnFocusChangeListener(focusMonitor);
-        b.accountRowAccName.setOnFocusChangeListener(focusMonitor);
-        b.accountRowAccAmounts.setOnFocusChangeListener(focusMonitor);
-        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);
-
-        decimalSeparator = String.valueOf(DecimalFormatSymbols.getInstance()
-                                                              .getMonetaryDecimalSeparator());
-        localeObserver = locale -> decimalSeparator = String.valueOf(
-                DecimalFormatSymbols.getInstance(locale)
-                                    .getMonetaryDecimalSeparator());
-
-        decimalDot = ".";
-
-        final TextWatcher tw = new TextWatcher() {
-            @Override
-            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
-            }
-
-            @Override
-            public void onTextChanged(CharSequence s, int start, int before, int count) {
-            }
-
-            @Override
-            public void afterTextChanged(Editable s) {
-//                debug("input", "text changed");
-                if (inUpdate)
-                    return;
-
-                Logger.debug("textWatcher", "calling syncData()");
-                syncData();
-                Logger.debug("textWatcher",
-                        "syncData() returned, checking if transaction is submittable");
-                adapter.checkTransactionSubmittable();
-                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));
-            }
-            @Override
-            public void onTextChanged(CharSequence s, int start, int before, int count) {}
-            @Override
-            public void afterTextChanged(Editable s) {
-
-                if (syncData())
-                    adapter.checkTransactionSubmittable();
-            }
-        };
-        b.newTransactionDescription.addTextChangedListener(tw);
-        b.transactionComment.addTextChangedListener(tw);
-        b.accountRowAccName.addTextChangedListener(tw);
-        b.comment.addTextChangedListener(tw);
-        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.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 =
-                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);
-            }
-        };
-
-        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();
-
-                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);
-        };
-
-        amountValidityObserver = valid -> {
-            b.accountRowAccAmounts.setCompoundDrawablesRelativeWithIntrinsicBounds(
-                    valid ? 0 : R.drawable.ic_error_outline_black_24dp, 0, 0, 0);
-            b.accountRowAccAmounts.setMinEms(valid ? 4 : 5);
-        };
-    }
-    private void commentFocusChanged(TextView textView, boolean hasFocus) {
-        @ColorInt int textColor;
-        textColor = b.dummyText.getTextColors()
-                               .getDefaultColor();
-        if (hasFocus) {
-            textView.setTypeface(null, Typeface.NORMAL);
-            textView.setHint(R.string.transaction_account_comment_hint);
-        }
-        else {
-            int alpha = (textColor >> 24 & 0xff);
-            alpha = 3 * alpha / 4;
-            textColor = (alpha << 24) | (0x00ffffff & textColor);
-            textView.setTypeface(null, Typeface.ITALIC);
-            textView.setHint("");
-            if (TextUtils.isEmpty(textView.getText())) {
-                textView.setVisibility(View.INVISIBLE);
-            }
-        }
-        textView.setTextColor(textColor);
-
-    }
-    private void updateCurrencyPositionAndPadding(Currency.Position position, boolean hasGap) {
-        ConstraintLayout.LayoutParams amountLP =
-                (ConstraintLayout.LayoutParams) b.accountRowAccAmounts.getLayoutParams();
-        ConstraintLayout.LayoutParams currencyLP =
-                (ConstraintLayout.LayoutParams) b.currency.getLayoutParams();
-
-        if (position == Currency.Position.before) {
-            currencyLP.startToStart = ConstraintLayout.LayoutParams.PARENT_ID;
-            currencyLP.endToEnd = ConstraintLayout.LayoutParams.UNSET;
-
-            amountLP.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID;
-            amountLP.endToStart = ConstraintLayout.LayoutParams.UNSET;
-            amountLP.startToStart = ConstraintLayout.LayoutParams.UNSET;
-            amountLP.startToEnd = b.currency.getId();
-
-            b.currency.setGravity(Gravity.END);
-        }
-        else {
-            currencyLP.startToStart = ConstraintLayout.LayoutParams.UNSET;
-            currencyLP.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID;
-
-            amountLP.startToStart = ConstraintLayout.LayoutParams.PARENT_ID;
-            amountLP.startToEnd = ConstraintLayout.LayoutParams.UNSET;
-            amountLP.endToEnd = ConstraintLayout.LayoutParams.UNSET;
-            amountLP.endToStart = b.currency.getId();
-
-            b.currency.setGravity(Gravity.START);
-        }
-
-        amountLP.resolveLayoutDirection(b.accountRowAccAmounts.getLayoutDirection());
-        currencyLP.resolveLayoutDirection(b.currency.getLayoutDirection());
-
-        b.accountRowAccAmounts.setLayoutParams(amountLP);
-        b.currency.setLayoutParams(currencyLP);
-
-        // distance between the amount and the currency symbol
-        int gapSize = DimensionUtils.sp2px(b.currency.getContext(), 5);
-
-        if (position == Currency.Position.before) {
-            b.currency.setPaddingRelative(0, 0, hasGap ? gapSize : 0, 0);
-        }
-        else {
-            b.currency.setPaddingRelative(hasGap ? gapSize : 0, 0, 0, 0);
-        }
-    }
-    private void setCurrencyString(String currency) {
-        @ColorInt int textColor = b.dummyText.getTextColors()
-                                             .getDefaultColor();
-        if ((currency == null) || currency.isEmpty()) {
-            b.currency.setText(R.string.currency_symbol);
-            int alpha = (textColor >> 24) & 0xff;
-            alpha = alpha * 3 / 4;
-            b.currency.setTextColor((alpha << 24) | (0x00ffffff & textColor));
-        }
-        else {
-            b.currency.setText(currency);
-            b.currency.setTextColor(textColor);
-        }
-    }
-    private void setCurrency(Currency currency) {
-        setCurrencyString((currency == null) ? null : currency.getName());
-    }
-    private void setEditable(Boolean editable) {
-        b.newTransactionDate.setEnabled(editable);
-        b.newTransactionDescription.setEnabled(editable);
-        b.accountRowAccName.setEnabled(editable);
-        b.accountRowAccAmounts.setEnabled(editable);
-    }
-    private void beginUpdates() {
-        if (inUpdate)
-            throw new RuntimeException("Already in update mode");
-        inUpdate = true;
-    }
-    private void endUpdates() {
-        if (!inUpdate)
-            throw new RuntimeException("Not in update mode");
-        inUpdate = false;
-    }
-    /**
-     * syncData()
-     * <p>
-     * Stores the data from the UI elements into the model item
-     * Returns true if there were changes made that suggest transaction has to be
-     * checked for being submittable
-     */
-    private boolean syncData() {
-        if (item == null)
-            return false;
-
-        if (syncingData) {
-            Logger.debug("new-trans", "skipping syncData() loop");
-            return false;
-        }
-
-        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();
-                    }
-                    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);
-                    }
-
-                    break;
-                case bottomFiller:
-                    throw new RuntimeException("Should not happen");
-            }
-
-            return true;
-        }
-        catch (ParseException e) {
-            throw new RuntimeException("Should not happen", e);
-        }
-        finally {
-            syncingData = false;
-        }
-    }
-    private void pickTransactionDate() {
-        DatePickerFragment picker = new DatePickerFragment();
-        picker.setFutureDates(mProfile.getFutureDates());
-        picker.setOnDatePickedListener(this);
-        picker.setCurrentDateFromText(b.newTransactionDate.getText());
-        picker.show(((NewTransactionActivity) b.getRoot()
-                                               .getContext()).getSupportFragmentManager(), null);
-    }
-    /**
-     * setData
-     *
-     * @param item updates the UI elements with the data from the model item
-     */
-    @SuppressLint("DefaultLocale")
-    public void setData(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;
-            }
-
-            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();
-                    b.accountRowAccName.setText(acc.getAccountName());
-                    b.comment.setText(acc.getComment());
-                    if (acc.isAmountSet()) {
-                        b.accountRowAccAmounts.setText(String.format("%1.2f", acc.getAmount()));
-                    }
-                    else {
-                        b.accountRowAccAmounts.setText("");
-//                        tvAmount.setHint(R.string.zero_amount);
-                    }
-                    b.accountRowAccAmounts.setHint(item.getAmountHint());
-                    setCurrencyString(acc.getCurrency());
-                    b.ntrData.setVisibility(View.GONE);
-                    b.ntrAccount.setVisibility(View.VISIBLE);
-                    b.ntrPadding.setVisibility(View.GONE);
-                    setEditable(true);
-                    break;
-                case 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;
-                }
-            }
-        }
-        finally {
-            endUpdates();
-        }
-    }
-    @Override
-    public void onDatePicked(int year, int month, int day) {
-        item.setDate(new SimpleDate(year, month + 1, day));
-        boolean focused = b.newTransactionDescription.requestFocus();
-        if (focused)
-            Misc.showSoftKeyboard((NewTransactionActivity) b.getRoot()
-                                                            .getContext());
-
-    }
-    @Override
-    public void descriptionSelected(String description) {
-        b.accountRowAccName.setText(description);
-        b.accountRowAccAmounts.requestFocus(View.FOCUS_FORWARD);
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionItemsAdapter.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionItemsAdapter.java
deleted file mode 100644 (file)
index c91d390..0000000
+++ /dev/null
@@ -1,697 +0,0 @@
-/*
- * Copyright © 2021 Damyan Ivanov.
- * This file is part of MoLe.
- * MoLe is free software: you can distribute it and/or modify it
- * under the term of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * MoLe is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger.ui.activity;
-
-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.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;
-
-class NewTransactionItemsAdapter extends RecyclerView.Adapter<NewTransactionItemHolder>
-        implements DescriptionSelectedCallback {
-    private final NewTransactionModel model;
-    private final ItemTouchHelper touchHelper;
-    private MobileLedgerProfile mProfile;
-    private RecyclerView recyclerView;
-    private int checkHoldCounter = 0;
-    NewTransactionItemsAdapter(NewTransactionModel viewModel, MobileLedgerProfile profile) {
-        super();
-        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;
-
-        touchHelper = new ItemTouchHelper(new ItemTouchHelper.Callback() {
-            @Override
-            public boolean isLongPressDragEnabled() {
-                return true;
-            }
-            @Override
-            public boolean canDropOver(@NonNull RecyclerView recyclerView,
-                                       @NonNull RecyclerView.ViewHolder current,
-                                       @NonNull RecyclerView.ViewHolder target) {
-                final int adapterPosition = target.getAdapterPosition();
-
-                // first and last items are immovable
-                if (adapterPosition == 0)
-                    return false;
-                if (adapterPosition == adapter.getItemCount() - 1)
-                    return false;
-
-                return super.canDropOver(recyclerView, current, target);
-            }
-            @Override
-            public int getMovementFlags(@NonNull RecyclerView recyclerView,
-                                        @NonNull RecyclerView.ViewHolder viewHolder) {
-                int flags = makeFlag(ItemTouchHelper.ACTION_STATE_IDLE, ItemTouchHelper.END);
-                // the top (date and description) and the bottom (padding) items are always there
-                final int adapterPosition = viewHolder.getAdapterPosition();
-                if ((adapterPosition > 0) && (adapterPosition < adapter.getItemCount() - 1)) {
-                    flags |= makeFlag(ItemTouchHelper.ACTION_STATE_DRAG,
-                            ItemTouchHelper.UP | ItemTouchHelper.DOWN) |
-                             makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE,
-                                     ItemTouchHelper.START | ItemTouchHelper.END);
-                }
-
-                return flags;
-            }
-            @Override
-            public boolean onMove(@NonNull RecyclerView recyclerView,
-                                  @NonNull RecyclerView.ViewHolder viewHolder,
-                                  @NonNull RecyclerView.ViewHolder target) {
-
-                model.swapItems(viewHolder.getAdapterPosition(), target.getAdapterPosition());
-                notifyItemMoved(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();
-            }
-        });
-    }
-    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) {
-        NewTransactionRowBinding b =
-                NewTransactionRowBinding.inflate(LayoutInflater.from(parent.getContext()), parent,
-                        false);
-
-        return new NewTransactionItemHolder(b, this);
-    }
-    @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);
-        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;
-    }
-    @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);
-    }
-    void noteFocusIsOnAmount(int position) {
-        model.noteFocusChanged(position, NewTransactionModel.FocusedElement.Amount);
-    }
-    void noteFocusIsOnComment(int position) {
-        model.noteFocusChanged(position, NewTransactionModel.FocusedElement.Comment);
-    }
-    void noteFocusIsOnTransactionComment(int position) {
-        model.noteFocusChanged(position, NewTransactionModel.FocusedElement.TransactionComment);
-    }
-    public void noteFocusIsOnDescription(int pos) {
-        model.noteFocusChanged(pos, NewTransactionModel.FocusedElement.Description);
-    }
-    private void holdSubmittableChecks() {
-        checkHoldCounter++;
-    }
-    private void releaseSubmittableChecks() {
-        if (checkHoldCounter == 0)
-            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();
-        }
-    }
-    /*
-         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();
-        }
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionModel.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionModel.java
deleted file mode 100644 (file)
index b647eb8..0000000
+++ /dev/null
@@ -1,470 +0,0 @@
-/*
- * Copyright © 2021 Damyan Ivanov.
- * This file is part of MoLe.
- * MoLe is free software: you can distribute it and/or modify it
- * under the term of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * MoLe is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger.ui.activity;
-
-import androidx.annotation.NonNull;
-import androidx.lifecycle.LifecycleOwner;
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.MutableLiveData;
-import androidx.lifecycle.Observer;
-import androidx.lifecycle.ViewModel;
-
-import net.ktnx.mobileledger.model.Currency;
-import net.ktnx.mobileledger.model.Data;
-import net.ktnx.mobileledger.model.LedgerTransactionAccount;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
-import net.ktnx.mobileledger.utils.Globals;
-import net.ktnx.mobileledger.utils.SimpleDate;
-
-import org.jetbrains.annotations.NotNull;
-
-import java.text.ParseException;
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Collections;
-import java.util.GregorianCalendar;
-import java.util.Locale;
-import java.util.concurrent.atomic.AtomicInteger;
-
-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 AtomicInteger busyCounter = new AtomicInteger(0);
-    private final MutableLiveData<Boolean> busyFlag = new MutableLiveData<>(false);
-    private final Observer<MobileLedgerProfile> profileObserver = profile -> {
-        showCurrency.postValue(profile.getShowCommodityByDefault());
-        showComments.postValue(profile.getShowCommentsByDefault());
-    };
-    private boolean observingDataProfile;
-    void observeShowComments(LifecycleOwner owner, Observer<? super Boolean> observer) {
-        showComments.observe(owner, observer);
-    }
-    void observeBusyFlag(@NonNull LifecycleOwner owner, Observer<? super Boolean> observer) {
-        busyFlag.observe(owner, observer);
-    }
-    void observeDataProfile(LifecycleOwner activity) {
-        if (!observingDataProfile)
-            Data.observeProfile(activity, profileObserver);
-        observingDataProfile = true;
-    }
-    boolean getSimulateSave() {
-        return simulateSave.getValue();
-    }
-    public void setSimulateSave(boolean simulateSave) {
-        this.simulateSave.setValue(simulateSave);
-    }
-    void toggleSimulateSave() {
-        simulateSave.setValue(!simulateSave.getValue());
-    }
-    void observeSimulateSave(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
-                             @NonNull androidx.lifecycle.Observer<? super Boolean> observer) {
-        this.simulateSave.observe(owner, observer);
-    }
-    int getAccountCount() {
-        return items.size();
-    }
-    public SimpleDate getDate() {
-        return header.date.getValue();
-    }
-    public void setDate(SimpleDate date) {
-        header.date.setValue(date);
-    }
-    public String getDescription() {
-        return header.description.getValue();
-    }
-    public String getComment() {
-        return header.comment.getValue();
-    }
-    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();
-    }
-    boolean accountsInInitialState() {
-        for (Item item : items) {
-            LedgerTransactionAccount acc = item.getAccount();
-            if (acc.isAmountSet())
-                return false;
-            if (!acc.getAccountName()
-                    .trim()
-                    .isEmpty())
-                return false;
-        }
-
-        return true;
-    }
-    LedgerTransactionAccount getAccount(int index) {
-        return items.get(index)
-                    .getAccount();
-    }
-    Item getItem(int index) {
-        if (index == 0) {
-            return header;
-        }
-
-        if (index <= items.size())
-            return items.get(index - 1);
-
-        return trailer;
-    }
-    void removeRow(Item item, NewTransactionItemsAdapter adapter) {
-        int pos = items.indexOf(item);
-        items.remove(pos);
-        if (adapter != null) {
-            adapter.notifyItemRemoved(pos + 1);
-            sendCountNotifications();
-        }
-    }
-    void removeItem(int pos) {
-        items.remove(pos);
-        accountCount.setValue(getAccountCount());
-    }
-    void sendCountNotifications() {
-        accountCount.setValue(getAccountCount());
-    }
-    public void sendFocusedNotification() {
-        focusedItem.setValue(focusedItem.getValue());
-    }
-    void updateFocusedItem(int position) {
-        focusedItem.setValue(position);
-    }
-    void noteFocusChanged(int position, FocusedElement element) {
-        getItem(position).setFocusedElement(element);
-    }
-    void swapItems(int one, int two) {
-        Collections.swap(items, one - 1, two - 1);
-    }
-    void moveItemLast(int index) {
-        /*   0
-             1   <-- index
-             2
-             3   <-- desired position
-         */
-        int itemCount = items.size();
-
-        if (index < itemCount - 1) {
-            Item acc = items.remove(index);
-            items.add(itemCount - 1, acc);
-        }
-    }
-    void toggleCurrencyVisible() {
-        showCurrency.setValue(!showCurrency.getValue());
-    }
-    void stopObservingBusyFlag(Observer<Boolean> observer) {
-        busyFlag.removeObserver(observer);
-    }
-    void incrementBusyCounter() {
-        int newValue = busyCounter.incrementAndGet();
-        if (newValue == 1)
-            busyFlag.postValue(true);
-    }
-    void decrementBusyCounter() {
-        int newValue = busyCounter.decrementAndGet();
-        if (newValue == 0)
-            busyFlag.postValue(false);
-    }
-    public boolean getBusyFlag() {
-        return busyFlag.getValue();
-    }
-    public void toggleShowComments() {
-        showComments.setValue(!showComments.getValue());
-    }
-    enum ItemType {generalData, transactionRow, bottomFiller}
-
-    enum FocusedElement {Account, Comment, Amount, Description, TransactionComment}
-
-
-    //==========================================================================================
-
-
-    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;
-        }
-        void setFocusedElement(FocusedElement focusedElement) {
-            this.focusedElement = focusedElement;
-        }
-        public NewTransactionModel getModel() {
-            return model;
-        }
-        void setEditable(boolean editable) {
-            ensureTypeIsGeneralDataOrTransactionRow();
-            this.editable.setValue(editable);
-        }
-        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));
-            }
-        }
-        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;
-            }
-            else {
-                if (amountHint.equals(this.amountHint.getValue()))
-                    return;
-                amountHintIsSet = true;
-            }
-
-            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);
-        }
-        ItemType getType() {
-            return type;
-        }
-        void ensureType(ItemType wantedType) {
-            if (type != wantedType) {
-                throw new RuntimeException(
-                        String.format("Actual type (%s) differs from wanted (%s)", type,
-                                wantedType));
-            }
-        }
-        public SimpleDate getDate() {
-            ensureType(ItemType.generalData);
-            return date.getValue();
-        }
-        public void setDate(SimpleDate date) {
-            ensureType(ItemType.generalData);
-            this.date.setValue(date);
-        }
-        public void setDate(String text) throws ParseException {
-            if ((text == null) || text.trim()
-                                      .isEmpty())
-            {
-                setDate((SimpleDate) null);
-                return;
-            }
-
-            SimpleDate date = Globals.parseLedgerDate(text);
-            this.setDate(date);
-        }
-        void observeDate(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
-                         @NonNull androidx.lifecycle.Observer<? super SimpleDate> observer) {
-            this.date.observe(owner, observer);
-        }
-        void stopObservingDate(@NonNull androidx.lifecycle.Observer<? super SimpleDate> observer) {
-            this.date.removeObserver(observer);
-        }
-        public String getDescription() {
-            ensureType(ItemType.generalData);
-            return description.getValue();
-        }
-        public void setDescription(String description) {
-            ensureType(ItemType.generalData);
-            this.description.setValue(description);
-        }
-        void observeDescription(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
-                                @NonNull androidx.lifecycle.Observer<? super String> observer) {
-            this.description.observe(owner, observer);
-        }
-        void stopObservingDescription(
-                @NonNull androidx.lifecycle.Observer<? super String> observer) {
-            this.description.removeObserver(observer);
-        }
-        public String getTransactionComment() {
-            ensureType(ItemType.generalData);
-            return comment.getValue();
-        }
-        public void setTransactionComment(String transactionComment) {
-            ensureType(ItemType.generalData);
-            this.comment.setValue(transactionComment);
-        }
-        void observeTransactionComment(@NonNull @NotNull LifecycleOwner owner,
-                                       @NonNull Observer<? super String> observer) {
-            ensureType(ItemType.generalData);
-            this.comment.observe(owner, observer);
-        }
-        void stopObservingTransactionComment(@NonNull Observer<? super String> observer) {
-            this.comment.removeObserver(observer);
-        }
-        public LedgerTransactionAccount getAccount() {
-            ensureType(ItemType.transactionRow);
-            return account;
-        }
-        public void setAccountName(String name) {
-            account.setAccountName(name);
-        }
-        /**
-         * getFormattedDate()
-         *
-         * @return nicely formatted, shortest available date representation
-         */
-        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.MONTH) != d.month - 1) {
-                return String.format(Locale.US, "%d/%02d", d.month, d.day);
-            }
-
-            return String.valueOf(d.day);
-        }
-        void observeEditableFlag(NewTransactionActivity activity, Observer<Boolean> observer) {
-            editable.observe(activity, observer);
-        }
-        void stopObservingEditableFlag(Observer<Boolean> observer) {
-            editable.removeObserver(observer);
-        }
-        void observeComment(NewTransactionActivity activity, Observer<String> observer) {
-            comment.observe(activity, observer);
-        }
-        void stopObservingComment(Observer<String> observer) {
-            comment.removeObserver(observer);
-        }
-        public void setComment(String comment) {
-            getAccount().setComment(comment);
-            this.comment.postValue(comment);
-        }
-        public Currency getCurrency() {
-            return this.currency.getValue();
-        }
-        public void setCurrency(Currency currency) {
-            Currency present = this.currency.getValue();
-            if ((currency == null) && (present != null) ||
-                (currency != null) && !currency.equals(present))
-            {
-                getAccount().setCurrency((currency != null && !currency.getName()
-                                                                       .isEmpty())
-                                         ? currency.getName() : null);
-                this.currency.setValue(currency);
-            }
-        }
-        void observeCurrency(NewTransactionActivity activity, Observer<Currency> observer) {
-            currency.observe(activity, observer);
-        }
-        void stopObservingCurrency(Observer<Currency> observer) {
-            currency.removeObserver(observer);
-        }
-        boolean isBottomFiller() {
-            return this.type == ItemType.bottomFiller;
-        }
-        boolean isAmountHintSet() {
-            return amountHintIsSet;
-        }
-        void validateAmount() {
-            amountValid.setValue(true);
-        }
-        void invalidateAmount() {
-            amountValid.setValue(false);
-        }
-        void observeAmountValidity(NewTransactionActivity activity, Observer<Boolean> observer) {
-            amountValid.observe(activity, observer);
-        }
-        void stopObservingAmountValidity(Observer<Boolean> observer) {
-            amountValid.removeObserver(observer);
-        }
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/PatternsActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/PatternsActivity.java
deleted file mode 100644 (file)
index cc057f4..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright © 2021 Damyan Ivanov.
- * This file is part of MoLe.
- * MoLe is free software: you can distribute it and/or modify it
- * under the term of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * MoLe is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger.ui.activity;
-
-import android.os.Bundle;
-import android.view.Menu;
-import android.view.View;
-
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.google.android.material.snackbar.Snackbar;
-
-import net.ktnx.mobileledger.R;
-import net.ktnx.mobileledger.databinding.ActivityPatternsBinding;
-import net.ktnx.mobileledger.ui.patterns.PatternsModel;
-import net.ktnx.mobileledger.ui.patterns.PatternsRecyclerViewAdapter;
-
-public class PatternsActivity extends CrashReportingActivity {
-
-    @Override
-    public boolean onCreateOptionsMenu(Menu menu) {
-        super.onCreateOptionsMenu(menu);
-        getMenuInflater().inflate(R.menu.pattern_list_menu, menu);
-
-        return true;
-    }
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        ActivityPatternsBinding b = ActivityPatternsBinding.inflate(getLayoutInflater());
-        setContentView(b.getRoot());
-        setSupportActionBar(b.toolbar);
-        b.toolbarLayout.setTitle(getTitle());
-
-        b.fab.setOnClickListener(this::fabClicked);
-
-        PatternsRecyclerViewAdapter modelAdapter = new PatternsRecyclerViewAdapter();
-
-        b.patternList.setAdapter(modelAdapter);
-        PatternsModel.retrievePatterns(modelAdapter);
-        LinearLayoutManager llm = new LinearLayoutManager(this);
-        llm.setOrientation(RecyclerView.VERTICAL);
-        b.patternList.setLayoutManager(llm);
-    }
-    private void fabClicked(View view) {
-        Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_INDEFINITE)
-                .setAction("Action", null)
-                .show();
-    }
-}
\ No newline at end of file
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/ProfileDetailActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/ProfileDetailActivity.java
deleted file mode 100644 (file)
index 1bc17ec..0000000
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * Copyright © 2019 Damyan Ivanov.
- * This file is part of MoLe.
- * MoLe is free software: you can distribute it and/or modify it
- * under the term of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * MoLe is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger.ui.activity;
-
-import android.os.Bundle;
-import android.view.Menu;
-import android.view.MenuItem;
-
-import androidx.appcompat.app.ActionBar;
-import androidx.appcompat.widget.Toolbar;
-import androidx.lifecycle.ViewModelProvider;
-
-import net.ktnx.mobileledger.R;
-import net.ktnx.mobileledger.model.Data;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
-import net.ktnx.mobileledger.ui.profiles.ProfileDetailFragment;
-import net.ktnx.mobileledger.ui.profiles.ProfileDetailModel;
-import net.ktnx.mobileledger.utils.Colors;
-
-import org.jetbrains.annotations.NotNull;
-
-import java.util.ArrayList;
-import java.util.Locale;
-
-import static net.ktnx.mobileledger.utils.Logger.debug;
-
-/**
- * An activity representing a single Profile detail screen. This
- * activity is only used on narrow width devices. On tablet-size devices,
- * item details are presented side-by-side with a list of items
- * in a ProfileListActivity (not really).
- */
-public class ProfileDetailActivity extends CrashReportingActivity {
-    private MobileLedgerProfile profile = null;
-    private ProfileDetailFragment mFragment;
-    @NotNull
-    private ProfileDetailModel getModel() {
-        return new ViewModelProvider(this).get(ProfileDetailModel.class);
-    }
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        final int index = getIntent().getIntExtra(ProfileDetailFragment.ARG_ITEM_ID, -1);
-
-        if (index != -1) {
-            ArrayList<MobileLedgerProfile> profiles = Data.profiles.getValue();
-            if (profiles != null) {
-                profile = profiles.get(index);
-                if (profile == null)
-                    throw new AssertionError(
-                            String.format("Can't get profile " + "(index:%d) from the global list",
-                                    index));
-
-                debug("profiles", String.format(Locale.ENGLISH, "Editing profile %s (%s); hue=%d",
-                        profile.getName(), profile.getUuid(), profile.getThemeHue()));
-            }
-        }
-
-        super.onCreate(savedInstanceState);
-        int themeHue;
-        if (profile != null)
-            themeHue = profile.getThemeHue();
-        else {
-            themeHue = Colors.getNewProfileThemeHue(Data.profiles.getValue());
-        }
-        Colors.setupTheme(this, themeHue);
-        final ProfileDetailModel model = getModel();
-        model.initialThemeHue = themeHue;
-        setContentView(R.layout.activity_profile_detail);
-        Toolbar toolbar = findViewById(R.id.detail_toolbar);
-        setSupportActionBar(toolbar);
-
-
-        // Show the Up button in the action bar.
-        ActionBar actionBar = getSupportActionBar();
-        if (actionBar != null) {
-            actionBar.setDisplayHomeAsUpEnabled(true);
-        }
-
-        // savedInstanceState is non-null when there is fragment state
-        // saved from previous configurations of this activity
-        // (e.g. when rotating the screen from portrait to landscape).
-        // In this case, the fragment will automatically be re-added
-        // to its container so we don't need to manually add it.
-        // For more information, see the Fragments API guide at:
-        //
-        // http://developer.android.com/guide/components/fragments.html
-        //
-        if (savedInstanceState == null) {
-            // Create the detail fragment and add it to the activity
-            // using a fragment transaction.
-            Bundle arguments = new Bundle();
-            arguments.putInt(ProfileDetailFragment.ARG_ITEM_ID, index);
-            arguments.putInt(ProfileDetailFragment.ARG_HUE, themeHue);
-            mFragment = new ProfileDetailFragment();
-            mFragment.setArguments(arguments);
-            getSupportFragmentManager().beginTransaction()
-                                       .add(R.id.profile_detail_container, mFragment)
-                                       .commit();
-        }
-    }
-    @Override
-    public boolean onCreateOptionsMenu(Menu menu) {
-        super.onCreateOptionsMenu(menu);
-        debug("profiles", "[activity] Creating profile details options menu");
-        if (mFragment != null)
-            mFragment.onCreateOptionsMenu(menu, getMenuInflater());
-
-        return true;
-    }
-    @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        if (item.getItemId() == android.R.id.home) {
-            finish();
-            return true;
-        }
-        return super.onOptionsItemSelected(item);
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionActivity.java
new file mode 100644 (file)
index 0000000..413365d
--- /dev/null
@@ -0,0 +1,152 @@
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.new_transaction;
+
+import android.os.Bundle;
+import android.util.TypedValue;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+
+import androidx.appcompat.widget.Toolbar;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.navigation.NavController;
+import androidx.navigation.fragment.NavHostFragment;
+
+import net.ktnx.mobileledger.BuildConfig;
+import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.async.AsyncCrasher;
+import net.ktnx.mobileledger.async.SendTransactionTask;
+import net.ktnx.mobileledger.async.TaskCallback;
+import net.ktnx.mobileledger.model.Data;
+import net.ktnx.mobileledger.model.LedgerTransaction;
+import net.ktnx.mobileledger.ui.activity.ProfileThemedActivity;
+
+import java.util.Objects;
+
+import static net.ktnx.mobileledger.utils.Logger.debug;
+
+public class NewTransactionActivity extends ProfileThemedActivity implements TaskCallback,
+        NewTransactionFragment.OnNewTransactionFragmentInteractionListener {
+    private NavController navController;
+    private NewTransactionModel model;
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.activity_new_transaction);
+        Toolbar toolbar = findViewById(R.id.toolbar);
+        setSupportActionBar(toolbar);
+        Data.observeProfile(this,
+                mobileLedgerProfile -> toolbar.setSubtitle(mobileLedgerProfile.getName()));
+
+        NavHostFragment navHostFragment = (NavHostFragment) Objects.requireNonNull(
+                getSupportFragmentManager().findFragmentById(R.id.new_transaction_nav));
+        navController = navHostFragment.getNavController();
+
+        Objects.requireNonNull(getSupportActionBar())
+               .setDisplayHomeAsUpEnabled(true);
+
+        model = new ViewModelProvider(this).get(NewTransactionModel.class);
+    }
+    @Override
+    protected void initProfile() {
+        String profileUUID = getIntent().getStringExtra("profile_uuid");
+
+        if (profileUUID != null) {
+            mProfile = Data.getProfile(profileUUID);
+            if (mProfile == null)
+                finish();
+            Data.setCurrentProfile(mProfile);
+        }
+        else
+            super.initProfile();
+    }
+    @Override
+    public void finish() {
+        super.finish();
+        overridePendingTransition(R.anim.dummy, R.anim.slide_out_down);
+    }
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (item.getItemId() == android.R.id.home) {
+            finish();
+            return true;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+    public void onTransactionSave(LedgerTransaction tr) {
+        navController.navigate(R.id.action_newTransactionFragment_to_newTransactionSavingFragment);
+        try {
+
+            SendTransactionTask saver =
+                    new SendTransactionTask(this, mProfile, model.getSimulateSave());
+            saver.execute(tr);
+        }
+        catch (Exception e) {
+            debug("new-transaction", "Unknown error", e);
+
+            Bundle b = new Bundle();
+            b.putString("error", "unknown error");
+            navController.navigate(R.id.newTransactionFragment, b);
+        }
+    }
+    public void simulateCrash(MenuItem item) {
+        debug("crash", "Will crash intentionally");
+        new AsyncCrasher().execute();
+    }
+    public boolean onCreateOptionsMenu(Menu menu) {
+        // Inflate the menu; this adds items to the action bar if it is present.
+        getMenuInflater().inflate(R.menu.new_transaction, menu);
+
+        if (BuildConfig.DEBUG) {
+            menu.findItem(R.id.action_simulate_crash)
+                .setVisible(true);
+            menu.findItem(R.id.action_simulate_save)
+                .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);
+        });
+
+        return true;
+    }
+
+
+    public int dp2px(float dp) {
+        return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
+                getResources().getDisplayMetrics()));
+    }
+    @Override
+    public void done(String error) {
+        Bundle b = new Bundle();
+        if (error != null) {
+            b.putString("error", error);
+            navController.navigate(R.id.action_newTransactionSavingFragment_Failure, b);
+        }
+        else
+            navController.navigate(R.id.action_newTransactionSavingFragment_Success, b);
+    }
+    public void toggleSimulateSave(MenuItem item) {
+        model.toggleSimulateSave();
+    }
+
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionFragment.java
new file mode 100644 (file)
index 0000000..aa44c72
--- /dev/null
@@ -0,0 +1,377 @@
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.new_transaction;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.renderscript.RSInvalidStateException;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ProgressBar;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContract;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import com.google.android.material.snackbar.Snackbar;
+
+import net.ktnx.mobileledger.R;
+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.utils.Logger;
+import net.ktnx.mobileledger.utils.Misc;
+import net.ktnx.mobileledger.utils.SimpleDate;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A simple {@link Fragment} subclass.
+ * Activities that contain this fragment must implement the
+ * {@link OnNewTransactionFragmentInteractionListener} interface
+ * to handle interaction events.
+ */
+
+// TODO: offer to undo account remove-on-swipe
+
+public class NewTransactionFragment extends Fragment {
+    private NewTransactionItemsAdapter listAdapter;
+    private NewTransactionModel viewModel;
+    final ActivityResultLauncher<Void> scanQrLauncher =
+            registerForActivityResult(new ActivityResultContract<Void, String>() {
+                @NonNull
+                @Override
+                public Intent createIntent(@NonNull Context context, Void input) {
+                    final Intent intent = new Intent("com.google.zxing.client.android.SCAN");
+                    intent.putExtra("SCAN_MODE", "QR_CODE_MODE");
+                    return intent;
+                }
+                @Override
+                public String parseResult(int resultCode, @Nullable Intent intent) {
+                    if (resultCode == Activity.RESULT_CANCELED)
+                        return null;
+                    return intent.getStringExtra("SCAN_RESULT");
+                }
+            }, this::onQrScanned);
+    private FloatingActionButton fab;
+    private OnNewTransactionFragmentInteractionListener mListener;
+    private MobileLedgerProfile mProfile;
+    public NewTransactionFragment() {
+        // Required empty public constructor
+        setHasOptionsMenu(true);
+    }
+    private void onQrScanned(String text) {
+        Logger.debug("qr", String.format("Got QR scan result [%s]", text));
+        Pattern p =
+                Pattern.compile("^(\\d+)\\*(\\d+)\\*(\\d+)-(\\d+)-(\\d+)\\*([:\\d]+)\\*([\\d.]+)$");
+        Matcher m = p.matcher(text);
+        if (m.matches()) {
+            float amount = Float.parseFloat(m.group(7));
+            viewModel.setDate(
+                    new SimpleDate(Integer.parseInt(m.group(3)), Integer.parseInt(m.group(4)),
+                            Integer.parseInt(m.group(5))));
+
+            if (viewModel.accountsInInitialState()) {
+                {
+                    NewTransactionModel.Item firstItem = viewModel.getItem(1);
+                    if (firstItem == null) {
+                        viewModel.addAccount(new LedgerTransactionAccount("разход:пазар"));
+                        listAdapter.notifyItemInserted(viewModel.items.size() - 1);
+                    }
+                    else {
+                        firstItem.setAccountName("разход:пазар");
+                        firstItem.getAccount()
+                                 .resetAmount();
+                        listAdapter.notifyItemChanged(1);
+                    }
+                }
+                {
+                    NewTransactionModel.Item secondItem = viewModel.getItem(2);
+                    if (secondItem == null) {
+                        viewModel.addAccount(
+                                new LedgerTransactionAccount("актив:кеш:дам", -amount, null, null));
+                        listAdapter.notifyItemInserted(viewModel.items.size() - 1);
+                    }
+                    else {
+                        secondItem.setAccountName("актив:кеш:дам");
+                        secondItem.getAccount()
+                                  .setAmount(-amount);
+                        listAdapter.notifyItemChanged(2);
+                    }
+                }
+            }
+            else {
+                viewModel.addAccount(new LedgerTransactionAccount("разход:пазар"));
+                viewModel.addAccount(
+                        new LedgerTransactionAccount("актив:кеш:дам", -amount, null, null));
+                listAdapter.notifyItemRangeInserted(viewModel.items.size() - 1, 2);
+            }
+
+            listAdapter.checkTransactionSubmittable();
+        }
+    }
+    @Override
+    public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
+        super.onCreateOptionsMenu(menu, inflater);
+        final FragmentActivity activity = getActivity();
+
+        inflater.inflate(R.menu.new_transaction_fragment, menu);
+
+        menu.findItem(R.id.scan_qr)
+            .setOnMenuItemClickListener(this::onScanQrAction);
+
+        menu.findItem(R.id.action_reset_new_transaction_activity)
+            .setOnMenuItemClickListener(item -> {
+                listAdapter.reset();
+                return true;
+            });
+
+        final MenuItem toggleCurrencyItem = menu.findItem(R.id.toggle_currency);
+        toggleCurrencyItem.setOnMenuItemClickListener(item -> {
+            viewModel.toggleCurrencyVisible();
+            return true;
+        });
+        if (activity != null)
+            viewModel.showCurrency.observe(activity, toggleCurrencyItem::setChecked);
+
+        final MenuItem toggleCommentsItem = menu.findItem(R.id.toggle_comments);
+        toggleCommentsItem.setOnMenuItemClickListener(item -> {
+            viewModel.toggleShowComments();
+            return true;
+        });
+        if (activity != null)
+            viewModel.showComments.observe(activity, toggleCommentsItem::setChecked);
+    }
+    private boolean onScanQrAction(MenuItem item) {
+        try {
+            scanQrLauncher.launch(null);
+        }
+        catch (Exception e) {
+            Logger.debug("qr", "Error launching QR scanner", e);
+        }
+
+        return true;
+    }
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+                             Bundle savedInstanceState) {
+        // Inflate the layout for this fragment
+        return inflater.inflate(R.layout.fragment_new_transaction, container, false);
+    }
+
+    @Override
+    public void onViewCreated(@NotNull View view, @Nullable Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+        FragmentActivity activity = getActivity();
+        if (activity == null)
+            throw new RSInvalidStateException(
+                    "getActivity() returned null within onActivityCreated()");
+
+        viewModel = new ViewModelProvider(activity).get(NewTransactionModel.class);
+        viewModel.observeDataProfile(this);
+        mProfile = Data.getProfile();
+        listAdapter = new NewTransactionItemsAdapter(viewModel, mProfile);
+
+        RecyclerView list = activity.findViewById(R.id.new_transaction_accounts);
+        list.setAdapter(listAdapter);
+        list.setLayoutManager(new LinearLayoutManager(activity));
+
+        Data.observeProfile(getViewLifecycleOwner(), profile -> {
+            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.fab);
+        fab.setOnClickListener(v -> onFabPressed());
+
+        boolean keep = false;
+
+        Bundle args = getArguments();
+        if (args != null) {
+            String error = args.getString("error");
+            if (error != null) {
+                Logger.debug("new-trans-f", String.format("Got error: %s", error));
+
+                Context context = getContext();
+                if (context != null) {
+                    AlertDialog.Builder builder = new AlertDialog.Builder(context);
+                    final Resources resources = context.getResources();
+                    final StringBuilder message = new StringBuilder();
+                    message.append(resources.getString(R.string.err_json_send_error_head));
+                    message.append("\n\n");
+                    message.append(error);
+                    if (mProfile.getApiVersion()
+                                .equals(API.auto))
+                        message.append(
+                                resources.getString(R.string.err_json_send_error_unsupported));
+                    else {
+                        message.append(resources.getString(R.string.err_json_send_error_tail));
+                        builder.setPositiveButton(R.string.btn_profile_options, (dialog, which) -> {
+                            Logger.debug("error", "will start profile editor");
+                            MobileLedgerProfile.startEditProfileActivity(context, mProfile);
+                        });
+                    }
+                    builder.setMessage(message);
+                    builder.create()
+                           .show();
+                }
+                else {
+                    Snackbar.make(list, error, Snackbar.LENGTH_INDEFINITE)
+                            .show();
+                }
+                keep = true;
+            }
+        }
+
+        int focused = 0;
+        if (savedInstanceState != null) {
+            keep |= savedInstanceState.getBoolean("keep", true);
+            focused = savedInstanceState.getInt("focused", 0);
+        }
+
+        if (!keep)
+            viewModel.reset();
+        else {
+            viewModel.setFocusedItem(focused);
+        }
+
+        ProgressBar p = activity.findViewById(R.id.progressBar);
+        viewModel.observeBusyFlag(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);
+        });
+    }
+    @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);
+        }
+    }
+
+    @Override
+    public void onAttach(@NotNull Context context) {
+        super.onAttach(context);
+        if (context instanceof OnNewTransactionFragmentInteractionListener) {
+            mListener = (OnNewTransactionFragmentInteractionListener) context;
+        }
+        else {
+            throw new RuntimeException(
+                    context.toString() + " must implement OnFragmentInteractionListener");
+        }
+    }
+
+    @Override
+    public void onDetach() {
+        super.onDetach();
+        mListener = null;
+    }
+
+    /**
+     * This interface must be implemented by activities that contain this
+     * fragment to allow an interaction in this fragment to be communicated
+     * to the activity and potentially other fragments contained in that
+     * activity.
+     * <p>
+     * See the Android Training lesson <a href=
+     * "http://developer.android.com/training/basics/fragments/communicating.html"
+     * >Communicating with Other Fragments</a> for more information.
+     */
+    public interface OnNewTransactionFragmentInteractionListener {
+        void onTransactionSave(LedgerTransaction tr);
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionItemHolder.java b/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionItemHolder.java
new file mode 100644 (file)
index 0000000..3bf09d2
--- /dev/null
@@ -0,0 +1,710 @@
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.new_transaction;
+
+import android.annotation.SuppressLint;
+import android.graphics.Typeface;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.view.Gravity;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+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.model.Currency;
+import net.ktnx.mobileledger.model.Data;
+import net.ktnx.mobileledger.model.LedgerTransactionAccount;
+import net.ktnx.mobileledger.model.MobileLedgerProfile;
+import net.ktnx.mobileledger.ui.CurrencySelectorFragment;
+import net.ktnx.mobileledger.ui.DatePickerFragment;
+import net.ktnx.mobileledger.ui.TextViewClearHelper;
+import net.ktnx.mobileledger.utils.DimensionUtils;
+import net.ktnx.mobileledger.utils.Logger;
+import net.ktnx.mobileledger.utils.MLDB;
+import net.ktnx.mobileledger.utils.Misc;
+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;
+
+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 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
+    NewTransactionItemHolder(@NonNull NewTransactionRowBinding b,
+                             NewTransactionItemsAdapter adapter) {
+        super(b.getRoot());
+        this.b = b;
+        new TextViewClearHelper().attachToTextView(b.comment);
+
+        b.newTransactionDescription.setNextFocusForwardId(View.NO_ID);
+        b.accountRowAccName.setNextFocusForwardId(View.NO_ID);
+        b.accountRowAccAmounts.setNextFocusForwardId(View.NO_ID); // magic!
+
+        b.newTransactionDate.setOnClickListener(v -> pickTransactionDate());
+
+        b.accountCommentButton.setOnClickListener(v -> {
+            b.comment.setVisibility(View.VISIBLE);
+            b.comment.requestFocus();
+        });
+
+        b.transactionCommentButton.setOnClickListener(v -> {
+            b.transactionComment.setVisibility(View.VISIBLE);
+            b.transactionComment.requestFocus();
+        });
+
+        mProfile = Data.getProfile();
+
+        View.OnFocusChangeListener focusMonitor = (v, hasFocus) -> {
+            final int id = v.getId();
+            if (hasFocus) {
+                boolean wasSyncing = syncingData;
+                syncingData = true;
+                try {
+                    final int pos = getAdapterPosition();
+                    adapter.updateFocusedItem(pos);
+                    if (id == R.id.account_row_acc_name) {
+                        adapter.noteFocusIsOnAccount(pos);
+                    }
+                    else if (id == R.id.account_row_acc_amounts) {
+                        adapter.noteFocusIsOnAmount(pos);
+                    }
+                    else if (id == R.id.comment) {
+                        adapter.noteFocusIsOnComment(pos);
+                    }
+                    else if (id == R.id.transaction_comment) {
+                        adapter.noteFocusIsOnTransactionComment(pos);
+                    }
+                    else if (id == R.id.new_transaction_description) {
+                        adapter.noteFocusIsOnDescription(pos);
+                    }
+                }
+                finally {
+                    syncingData = wasSyncing;
+                }
+            }
+
+            if (id == R.id.comment) {
+                commentFocusChanged(b.comment, hasFocus);
+            }
+            else if (id == R.id.transaction_comment) {
+                commentFocusChanged(b.transactionComment, hasFocus);
+            }
+        };
+
+        b.newTransactionDescription.setOnFocusChangeListener(focusMonitor);
+        b.accountRowAccName.setOnFocusChangeListener(focusMonitor);
+        b.accountRowAccAmounts.setOnFocusChangeListener(focusMonitor);
+        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);
+
+        decimalSeparator = String.valueOf(DecimalFormatSymbols.getInstance()
+                                                              .getMonetaryDecimalSeparator());
+        localeObserver = locale -> decimalSeparator = String.valueOf(
+                DecimalFormatSymbols.getInstance(locale)
+                                    .getMonetaryDecimalSeparator());
+
+        decimalDot = ".";
+
+        final TextWatcher tw = new TextWatcher() {
+            @Override
+            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+            }
+
+            @Override
+            public void onTextChanged(CharSequence s, int start, int before, int count) {
+            }
+
+            @Override
+            public void afterTextChanged(Editable s) {
+//                debug("input", "text changed");
+                if (inUpdate)
+                    return;
+
+                Logger.debug("textWatcher", "calling syncData()");
+                syncData();
+                Logger.debug("textWatcher",
+                        "syncData() returned, checking if transaction is submittable");
+                adapter.checkTransactionSubmittable();
+                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));
+            }
+            @Override
+            public void onTextChanged(CharSequence s, int start, int before, int count) {}
+            @Override
+            public void afterTextChanged(Editable s) {
+
+                if (syncData())
+                    adapter.checkTransactionSubmittable();
+            }
+        };
+        b.newTransactionDescription.addTextChangedListener(tw);
+        b.transactionComment.addTextChangedListener(tw);
+        b.accountRowAccName.addTextChangedListener(tw);
+        b.comment.addTextChangedListener(tw);
+        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.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 =
+                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);
+            }
+        };
+
+        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();
+
+                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);
+        };
+
+        amountValidityObserver = valid -> {
+            b.accountRowAccAmounts.setCompoundDrawablesRelativeWithIntrinsicBounds(
+                    valid ? 0 : R.drawable.ic_error_outline_black_24dp, 0, 0, 0);
+            b.accountRowAccAmounts.setMinEms(valid ? 4 : 5);
+        };
+    }
+    private void commentFocusChanged(TextView textView, boolean hasFocus) {
+        @ColorInt int textColor;
+        textColor = b.dummyText.getTextColors()
+                               .getDefaultColor();
+        if (hasFocus) {
+            textView.setTypeface(null, Typeface.NORMAL);
+            textView.setHint(R.string.transaction_account_comment_hint);
+        }
+        else {
+            int alpha = (textColor >> 24 & 0xff);
+            alpha = 3 * alpha / 4;
+            textColor = (alpha << 24) | (0x00ffffff & textColor);
+            textView.setTypeface(null, Typeface.ITALIC);
+            textView.setHint("");
+            if (TextUtils.isEmpty(textView.getText())) {
+                textView.setVisibility(View.INVISIBLE);
+            }
+        }
+        textView.setTextColor(textColor);
+
+    }
+    private void updateCurrencyPositionAndPadding(Currency.Position position, boolean hasGap) {
+        ConstraintLayout.LayoutParams amountLP =
+                (ConstraintLayout.LayoutParams) b.accountRowAccAmounts.getLayoutParams();
+        ConstraintLayout.LayoutParams currencyLP =
+                (ConstraintLayout.LayoutParams) b.currency.getLayoutParams();
+
+        if (position == Currency.Position.before) {
+            currencyLP.startToStart = ConstraintLayout.LayoutParams.PARENT_ID;
+            currencyLP.endToEnd = ConstraintLayout.LayoutParams.UNSET;
+
+            amountLP.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID;
+            amountLP.endToStart = ConstraintLayout.LayoutParams.UNSET;
+            amountLP.startToStart = ConstraintLayout.LayoutParams.UNSET;
+            amountLP.startToEnd = b.currency.getId();
+
+            b.currency.setGravity(Gravity.END);
+        }
+        else {
+            currencyLP.startToStart = ConstraintLayout.LayoutParams.UNSET;
+            currencyLP.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID;
+
+            amountLP.startToStart = ConstraintLayout.LayoutParams.PARENT_ID;
+            amountLP.startToEnd = ConstraintLayout.LayoutParams.UNSET;
+            amountLP.endToEnd = ConstraintLayout.LayoutParams.UNSET;
+            amountLP.endToStart = b.currency.getId();
+
+            b.currency.setGravity(Gravity.START);
+        }
+
+        amountLP.resolveLayoutDirection(b.accountRowAccAmounts.getLayoutDirection());
+        currencyLP.resolveLayoutDirection(b.currency.getLayoutDirection());
+
+        b.accountRowAccAmounts.setLayoutParams(amountLP);
+        b.currency.setLayoutParams(currencyLP);
+
+        // distance between the amount and the currency symbol
+        int gapSize = DimensionUtils.sp2px(b.currency.getContext(), 5);
+
+        if (position == Currency.Position.before) {
+            b.currency.setPaddingRelative(0, 0, hasGap ? gapSize : 0, 0);
+        }
+        else {
+            b.currency.setPaddingRelative(hasGap ? gapSize : 0, 0, 0, 0);
+        }
+    }
+    private void setCurrencyString(String currency) {
+        @ColorInt int textColor = b.dummyText.getTextColors()
+                                             .getDefaultColor();
+        if ((currency == null) || currency.isEmpty()) {
+            b.currency.setText(R.string.currency_symbol);
+            int alpha = (textColor >> 24) & 0xff;
+            alpha = alpha * 3 / 4;
+            b.currency.setTextColor((alpha << 24) | (0x00ffffff & textColor));
+        }
+        else {
+            b.currency.setText(currency);
+            b.currency.setTextColor(textColor);
+        }
+    }
+    private void setCurrency(Currency currency) {
+        setCurrencyString((currency == null) ? null : currency.getName());
+    }
+    private void setEditable(Boolean editable) {
+        b.newTransactionDate.setEnabled(editable);
+        b.newTransactionDescription.setEnabled(editable);
+        b.accountRowAccName.setEnabled(editable);
+        b.accountRowAccAmounts.setEnabled(editable);
+    }
+    private void beginUpdates() {
+        if (inUpdate)
+            throw new RuntimeException("Already in update mode");
+        inUpdate = true;
+    }
+    private void endUpdates() {
+        if (!inUpdate)
+            throw new RuntimeException("Not in update mode");
+        inUpdate = false;
+    }
+    /**
+     * syncData()
+     * <p>
+     * Stores the data from the UI elements into the model item
+     * Returns true if there were changes made that suggest transaction has to be
+     * checked for being submittable
+     */
+    private boolean syncData() {
+        if (item == null)
+            return false;
+
+        if (syncingData) {
+            Logger.debug("new-trans", "skipping syncData() loop");
+            return false;
+        }
+
+        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();
+                    }
+                    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);
+                    }
+
+                    break;
+                case bottomFiller:
+                    throw new RuntimeException("Should not happen");
+            }
+
+            return true;
+        }
+        catch (ParseException e) {
+            throw new RuntimeException("Should not happen", e);
+        }
+        finally {
+            syncingData = false;
+        }
+    }
+    private void pickTransactionDate() {
+        DatePickerFragment picker = new DatePickerFragment();
+        picker.setFutureDates(mProfile.getFutureDates());
+        picker.setOnDatePickedListener(this);
+        picker.setCurrentDateFromText(b.newTransactionDate.getText());
+        picker.show(((NewTransactionActivity) b.getRoot()
+                                               .getContext()).getSupportFragmentManager(), null);
+    }
+    /**
+     * setData
+     *
+     * @param item updates the UI elements with the data from the model item
+     */
+    @SuppressLint("DefaultLocale")
+    public void setData(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;
+            }
+
+            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();
+                    b.accountRowAccName.setText(acc.getAccountName());
+                    b.comment.setText(acc.getComment());
+                    if (acc.isAmountSet()) {
+                        b.accountRowAccAmounts.setText(String.format("%1.2f", acc.getAmount()));
+                    }
+                    else {
+                        b.accountRowAccAmounts.setText("");
+//                        tvAmount.setHint(R.string.zero_amount);
+                    }
+                    b.accountRowAccAmounts.setHint(item.getAmountHint());
+                    setCurrencyString(acc.getCurrency());
+                    b.ntrData.setVisibility(View.GONE);
+                    b.ntrAccount.setVisibility(View.VISIBLE);
+                    b.ntrPadding.setVisibility(View.GONE);
+                    setEditable(true);
+                    break;
+                case 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;
+                }
+            }
+        }
+        finally {
+            endUpdates();
+        }
+    }
+    @Override
+    public void onDatePicked(int year, int month, int day) {
+        item.setDate(new SimpleDate(year, month + 1, day));
+        boolean focused = b.newTransactionDescription.requestFocus();
+        if (focused)
+            Misc.showSoftKeyboard((NewTransactionActivity) b.getRoot()
+                                                            .getContext());
+
+    }
+    @Override
+    public void descriptionSelected(String description) {
+        b.accountRowAccName.setText(description);
+        b.accountRowAccAmounts.requestFocus(View.FOCUS_FORWARD);
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionItemsAdapter.java b/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionItemsAdapter.java
new file mode 100644 (file)
index 0000000..29b4e9a
--- /dev/null
@@ -0,0 +1,697 @@
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.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.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;
+
+class NewTransactionItemsAdapter extends RecyclerView.Adapter<NewTransactionItemHolder>
+        implements DescriptionSelectedCallback {
+    private final NewTransactionModel model;
+    private final ItemTouchHelper touchHelper;
+    private MobileLedgerProfile mProfile;
+    private RecyclerView recyclerView;
+    private int checkHoldCounter = 0;
+    NewTransactionItemsAdapter(NewTransactionModel viewModel, MobileLedgerProfile profile) {
+        super();
+        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;
+
+        touchHelper = new ItemTouchHelper(new ItemTouchHelper.Callback() {
+            @Override
+            public boolean isLongPressDragEnabled() {
+                return true;
+            }
+            @Override
+            public boolean canDropOver(@NonNull RecyclerView recyclerView,
+                                       @NonNull RecyclerView.ViewHolder current,
+                                       @NonNull RecyclerView.ViewHolder target) {
+                final int adapterPosition = target.getAdapterPosition();
+
+                // first and last items are immovable
+                if (adapterPosition == 0)
+                    return false;
+                if (adapterPosition == adapter.getItemCount() - 1)
+                    return false;
+
+                return super.canDropOver(recyclerView, current, target);
+            }
+            @Override
+            public int getMovementFlags(@NonNull RecyclerView recyclerView,
+                                        @NonNull RecyclerView.ViewHolder viewHolder) {
+                int flags = makeFlag(ItemTouchHelper.ACTION_STATE_IDLE, ItemTouchHelper.END);
+                // the top (date and description) and the bottom (padding) items are always there
+                final int adapterPosition = viewHolder.getAdapterPosition();
+                if ((adapterPosition > 0) && (adapterPosition < adapter.getItemCount() - 1)) {
+                    flags |= makeFlag(ItemTouchHelper.ACTION_STATE_DRAG,
+                            ItemTouchHelper.UP | ItemTouchHelper.DOWN) |
+                             makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE,
+                                     ItemTouchHelper.START | ItemTouchHelper.END);
+                }
+
+                return flags;
+            }
+            @Override
+            public boolean onMove(@NonNull RecyclerView recyclerView,
+                                  @NonNull RecyclerView.ViewHolder viewHolder,
+                                  @NonNull RecyclerView.ViewHolder target) {
+
+                model.swapItems(viewHolder.getAdapterPosition(), target.getAdapterPosition());
+                notifyItemMoved(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();
+            }
+        });
+    }
+    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) {
+        NewTransactionRowBinding b =
+                NewTransactionRowBinding.inflate(LayoutInflater.from(parent.getContext()), parent,
+                        false);
+
+        return new NewTransactionItemHolder(b, this);
+    }
+    @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);
+        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;
+    }
+    @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);
+    }
+    void noteFocusIsOnAmount(int position) {
+        model.noteFocusChanged(position, NewTransactionModel.FocusedElement.Amount);
+    }
+    void noteFocusIsOnComment(int position) {
+        model.noteFocusChanged(position, NewTransactionModel.FocusedElement.Comment);
+    }
+    void noteFocusIsOnTransactionComment(int position) {
+        model.noteFocusChanged(position, NewTransactionModel.FocusedElement.TransactionComment);
+    }
+    public void noteFocusIsOnDescription(int pos) {
+        model.noteFocusChanged(pos, NewTransactionModel.FocusedElement.Description);
+    }
+    private void holdSubmittableChecks() {
+        checkHoldCounter++;
+    }
+    private void releaseSubmittableChecks() {
+        if (checkHoldCounter == 0)
+            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();
+        }
+    }
+    /*
+         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();
+        }
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionModel.java b/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionModel.java
new file mode 100644 (file)
index 0000000..b96b917
--- /dev/null
@@ -0,0 +1,470 @@
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.new_transaction;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Observer;
+import androidx.lifecycle.ViewModel;
+
+import net.ktnx.mobileledger.model.Currency;
+import net.ktnx.mobileledger.model.Data;
+import net.ktnx.mobileledger.model.LedgerTransactionAccount;
+import net.ktnx.mobileledger.model.MobileLedgerProfile;
+import net.ktnx.mobileledger.utils.Globals;
+import net.ktnx.mobileledger.utils.SimpleDate;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+import java.util.concurrent.atomic.AtomicInteger;
+
+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 AtomicInteger busyCounter = new AtomicInteger(0);
+    private final MutableLiveData<Boolean> busyFlag = new MutableLiveData<>(false);
+    private final Observer<MobileLedgerProfile> profileObserver = profile -> {
+        showCurrency.postValue(profile.getShowCommodityByDefault());
+        showComments.postValue(profile.getShowCommentsByDefault());
+    };
+    private boolean observingDataProfile;
+    void observeShowComments(LifecycleOwner owner, Observer<? super Boolean> observer) {
+        showComments.observe(owner, observer);
+    }
+    void observeBusyFlag(@NonNull LifecycleOwner owner, Observer<? super Boolean> observer) {
+        busyFlag.observe(owner, observer);
+    }
+    void observeDataProfile(LifecycleOwner activity) {
+        if (!observingDataProfile)
+            Data.observeProfile(activity, profileObserver);
+        observingDataProfile = true;
+    }
+    boolean getSimulateSave() {
+        return simulateSave.getValue();
+    }
+    public void setSimulateSave(boolean simulateSave) {
+        this.simulateSave.setValue(simulateSave);
+    }
+    void toggleSimulateSave() {
+        simulateSave.setValue(!simulateSave.getValue());
+    }
+    void observeSimulateSave(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
+                             @NonNull androidx.lifecycle.Observer<? super Boolean> observer) {
+        this.simulateSave.observe(owner, observer);
+    }
+    int getAccountCount() {
+        return items.size();
+    }
+    public SimpleDate getDate() {
+        return header.date.getValue();
+    }
+    public void setDate(SimpleDate date) {
+        header.date.setValue(date);
+    }
+    public String getDescription() {
+        return header.description.getValue();
+    }
+    public String getComment() {
+        return header.comment.getValue();
+    }
+    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();
+    }
+    boolean accountsInInitialState() {
+        for (Item item : items) {
+            LedgerTransactionAccount acc = item.getAccount();
+            if (acc.isAmountSet())
+                return false;
+            if (!acc.getAccountName()
+                    .trim()
+                    .isEmpty())
+                return false;
+        }
+
+        return true;
+    }
+    LedgerTransactionAccount getAccount(int index) {
+        return items.get(index)
+                    .getAccount();
+    }
+    Item getItem(int index) {
+        if (index == 0) {
+            return header;
+        }
+
+        if (index <= items.size())
+            return items.get(index - 1);
+
+        return trailer;
+    }
+    void removeRow(Item item, NewTransactionItemsAdapter adapter) {
+        int pos = items.indexOf(item);
+        items.remove(pos);
+        if (adapter != null) {
+            adapter.notifyItemRemoved(pos + 1);
+            sendCountNotifications();
+        }
+    }
+    void removeItem(int pos) {
+        items.remove(pos);
+        accountCount.setValue(getAccountCount());
+    }
+    void sendCountNotifications() {
+        accountCount.setValue(getAccountCount());
+    }
+    public void sendFocusedNotification() {
+        focusedItem.setValue(focusedItem.getValue());
+    }
+    void updateFocusedItem(int position) {
+        focusedItem.setValue(position);
+    }
+    void noteFocusChanged(int position, FocusedElement element) {
+        getItem(position).setFocusedElement(element);
+    }
+    void swapItems(int one, int two) {
+        Collections.swap(items, one - 1, two - 1);
+    }
+    void moveItemLast(int index) {
+        /*   0
+             1   <-- index
+             2
+             3   <-- desired position
+         */
+        int itemCount = items.size();
+
+        if (index < itemCount - 1) {
+            Item acc = items.remove(index);
+            items.add(itemCount - 1, acc);
+        }
+    }
+    void toggleCurrencyVisible() {
+        showCurrency.setValue(!showCurrency.getValue());
+    }
+    void stopObservingBusyFlag(Observer<Boolean> observer) {
+        busyFlag.removeObserver(observer);
+    }
+    void incrementBusyCounter() {
+        int newValue = busyCounter.incrementAndGet();
+        if (newValue == 1)
+            busyFlag.postValue(true);
+    }
+    void decrementBusyCounter() {
+        int newValue = busyCounter.decrementAndGet();
+        if (newValue == 0)
+            busyFlag.postValue(false);
+    }
+    public boolean getBusyFlag() {
+        return busyFlag.getValue();
+    }
+    public void toggleShowComments() {
+        showComments.setValue(!showComments.getValue());
+    }
+    enum ItemType {generalData, transactionRow, bottomFiller}
+
+    enum FocusedElement {Account, Comment, Amount, Description, TransactionComment}
+
+
+    //==========================================================================================
+
+
+    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;
+        }
+        void setFocusedElement(FocusedElement focusedElement) {
+            this.focusedElement = focusedElement;
+        }
+        public NewTransactionModel getModel() {
+            return model;
+        }
+        void setEditable(boolean editable) {
+            ensureTypeIsGeneralDataOrTransactionRow();
+            this.editable.setValue(editable);
+        }
+        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));
+            }
+        }
+        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;
+            }
+            else {
+                if (amountHint.equals(this.amountHint.getValue()))
+                    return;
+                amountHintIsSet = true;
+            }
+
+            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);
+        }
+        ItemType getType() {
+            return type;
+        }
+        void ensureType(ItemType wantedType) {
+            if (type != wantedType) {
+                throw new RuntimeException(
+                        String.format("Actual type (%s) differs from wanted (%s)", type,
+                                wantedType));
+            }
+        }
+        public SimpleDate getDate() {
+            ensureType(ItemType.generalData);
+            return date.getValue();
+        }
+        public void setDate(SimpleDate date) {
+            ensureType(ItemType.generalData);
+            this.date.setValue(date);
+        }
+        public void setDate(String text) throws ParseException {
+            if ((text == null) || text.trim()
+                                      .isEmpty())
+            {
+                setDate((SimpleDate) null);
+                return;
+            }
+
+            SimpleDate date = Globals.parseLedgerDate(text);
+            this.setDate(date);
+        }
+        void observeDate(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
+                         @NonNull androidx.lifecycle.Observer<? super SimpleDate> observer) {
+            this.date.observe(owner, observer);
+        }
+        void stopObservingDate(@NonNull androidx.lifecycle.Observer<? super SimpleDate> observer) {
+            this.date.removeObserver(observer);
+        }
+        public String getDescription() {
+            ensureType(ItemType.generalData);
+            return description.getValue();
+        }
+        public void setDescription(String description) {
+            ensureType(ItemType.generalData);
+            this.description.setValue(description);
+        }
+        void observeDescription(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
+                                @NonNull androidx.lifecycle.Observer<? super String> observer) {
+            this.description.observe(owner, observer);
+        }
+        void stopObservingDescription(
+                @NonNull androidx.lifecycle.Observer<? super String> observer) {
+            this.description.removeObserver(observer);
+        }
+        public String getTransactionComment() {
+            ensureType(ItemType.generalData);
+            return comment.getValue();
+        }
+        public void setTransactionComment(String transactionComment) {
+            ensureType(ItemType.generalData);
+            this.comment.setValue(transactionComment);
+        }
+        void observeTransactionComment(@NonNull @NotNull LifecycleOwner owner,
+                                       @NonNull Observer<? super String> observer) {
+            ensureType(ItemType.generalData);
+            this.comment.observe(owner, observer);
+        }
+        void stopObservingTransactionComment(@NonNull Observer<? super String> observer) {
+            this.comment.removeObserver(observer);
+        }
+        public LedgerTransactionAccount getAccount() {
+            ensureType(ItemType.transactionRow);
+            return account;
+        }
+        public void setAccountName(String name) {
+            account.setAccountName(name);
+        }
+        /**
+         * getFormattedDate()
+         *
+         * @return nicely formatted, shortest available date representation
+         */
+        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.MONTH) != d.month - 1) {
+                return String.format(Locale.US, "%d/%02d", d.month, d.day);
+            }
+
+            return String.valueOf(d.day);
+        }
+        void observeEditableFlag(NewTransactionActivity activity, Observer<Boolean> observer) {
+            editable.observe(activity, observer);
+        }
+        void stopObservingEditableFlag(Observer<Boolean> observer) {
+            editable.removeObserver(observer);
+        }
+        void observeComment(NewTransactionActivity activity, Observer<String> observer) {
+            comment.observe(activity, observer);
+        }
+        void stopObservingComment(Observer<String> observer) {
+            comment.removeObserver(observer);
+        }
+        public void setComment(String comment) {
+            getAccount().setComment(comment);
+            this.comment.postValue(comment);
+        }
+        public Currency getCurrency() {
+            return this.currency.getValue();
+        }
+        public void setCurrency(Currency currency) {
+            Currency present = this.currency.getValue();
+            if ((currency == null) && (present != null) ||
+                (currency != null) && !currency.equals(present))
+            {
+                getAccount().setCurrency((currency != null && !currency.getName()
+                                                                       .isEmpty())
+                                         ? currency.getName() : null);
+                this.currency.setValue(currency);
+            }
+        }
+        void observeCurrency(NewTransactionActivity activity, Observer<Currency> observer) {
+            currency.observe(activity, observer);
+        }
+        void stopObservingCurrency(Observer<Currency> observer) {
+            currency.removeObserver(observer);
+        }
+        boolean isBottomFiller() {
+            return this.type == ItemType.bottomFiller;
+        }
+        boolean isAmountHintSet() {
+            return amountHintIsSet;
+        }
+        void validateAmount() {
+            amountValid.setValue(true);
+        }
+        void invalidateAmount() {
+            amountValid.setValue(false);
+        }
+        void observeAmountValidity(NewTransactionActivity activity, Observer<Boolean> observer) {
+            amountValid.observe(activity, observer);
+        }
+        void stopObservingAmountValidity(Observer<Boolean> observer) {
+            amountValid.removeObserver(observer);
+        }
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternsActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternsActivity.java
new file mode 100644 (file)
index 0000000..8e57a2e
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.patterns;
+
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.View;
+
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.snackbar.Snackbar;
+
+import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.databinding.ActivityPatternsBinding;
+import net.ktnx.mobileledger.ui.activity.CrashReportingActivity;
+
+public class PatternsActivity extends CrashReportingActivity {
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        super.onCreateOptionsMenu(menu);
+        getMenuInflater().inflate(R.menu.pattern_list_menu, menu);
+
+        return true;
+    }
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        ActivityPatternsBinding b = ActivityPatternsBinding.inflate(getLayoutInflater());
+        setContentView(b.getRoot());
+        setSupportActionBar(b.toolbar);
+        b.toolbarLayout.setTitle(getTitle());
+
+        b.fab.setOnClickListener(this::fabClicked);
+
+        PatternsRecyclerViewAdapter modelAdapter = new PatternsRecyclerViewAdapter();
+
+        b.patternList.setAdapter(modelAdapter);
+        PatternsModel.retrievePatterns(modelAdapter);
+        LinearLayoutManager llm = new LinearLayoutManager(this);
+        llm.setOrientation(RecyclerView.VERTICAL);
+        b.patternList.setLayoutManager(llm);
+    }
+    private void fabClicked(View view) {
+        Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_INDEFINITE)
+                .setAction("Action", null)
+                .show();
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailActivity.java
new file mode 100644 (file)
index 0000000..8afdefc
--- /dev/null
@@ -0,0 +1,132 @@
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.profiles;
+
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.widget.Toolbar;
+import androidx.lifecycle.ViewModelProvider;
+
+import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.model.Data;
+import net.ktnx.mobileledger.model.MobileLedgerProfile;
+import net.ktnx.mobileledger.ui.activity.CrashReportingActivity;
+import net.ktnx.mobileledger.utils.Colors;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.Locale;
+
+import static net.ktnx.mobileledger.utils.Logger.debug;
+
+/**
+ * An activity representing a single Profile detail screen. This
+ * activity is only used on narrow width devices. On tablet-size devices,
+ * item details are presented side-by-side with a list of items
+ * in a ProfileListActivity (not really).
+ */
+public class ProfileDetailActivity extends CrashReportingActivity {
+    private MobileLedgerProfile profile = null;
+    private ProfileDetailFragment mFragment;
+    @NotNull
+    private ProfileDetailModel getModel() {
+        return new ViewModelProvider(this).get(ProfileDetailModel.class);
+    }
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        final int index = getIntent().getIntExtra(ProfileDetailFragment.ARG_ITEM_ID, -1);
+
+        if (index != -1) {
+            ArrayList<MobileLedgerProfile> profiles = Data.profiles.getValue();
+            if (profiles != null) {
+                profile = profiles.get(index);
+                if (profile == null)
+                    throw new AssertionError(
+                            String.format("Can't get profile " + "(index:%d) from the global list",
+                                    index));
+
+                debug("profiles", String.format(Locale.ENGLISH, "Editing profile %s (%s); hue=%d",
+                        profile.getName(), profile.getUuid(), profile.getThemeHue()));
+            }
+        }
+
+        super.onCreate(savedInstanceState);
+        int themeHue;
+        if (profile != null)
+            themeHue = profile.getThemeHue();
+        else {
+            themeHue = Colors.getNewProfileThemeHue(Data.profiles.getValue());
+        }
+        Colors.setupTheme(this, themeHue);
+        final ProfileDetailModel model = getModel();
+        model.initialThemeHue = themeHue;
+        setContentView(R.layout.activity_profile_detail);
+        Toolbar toolbar = findViewById(R.id.detail_toolbar);
+        setSupportActionBar(toolbar);
+
+
+        // Show the Up button in the action bar.
+        ActionBar actionBar = getSupportActionBar();
+        if (actionBar != null) {
+            actionBar.setDisplayHomeAsUpEnabled(true);
+        }
+
+        // savedInstanceState is non-null when there is fragment state
+        // saved from previous configurations of this activity
+        // (e.g. when rotating the screen from portrait to landscape).
+        // In this case, the fragment will automatically be re-added
+        // to its container so we don't need to manually add it.
+        // For more information, see the Fragments API guide at:
+        //
+        // http://developer.android.com/guide/components/fragments.html
+        //
+        if (savedInstanceState == null) {
+            // Create the detail fragment and add it to the activity
+            // using a fragment transaction.
+            Bundle arguments = new Bundle();
+            arguments.putInt(ProfileDetailFragment.ARG_ITEM_ID, index);
+            arguments.putInt(ProfileDetailFragment.ARG_HUE, themeHue);
+            mFragment = new ProfileDetailFragment();
+            mFragment.setArguments(arguments);
+            getSupportFragmentManager().beginTransaction()
+                                       .add(R.id.profile_detail_container, mFragment)
+                                       .commit();
+        }
+    }
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        super.onCreateOptionsMenu(menu);
+        debug("profiles", "[activity] Creating profile details options menu");
+        if (mFragment != null)
+            mFragment.onCreateOptionsMenu(menu, getMenuInflater());
+
+        return true;
+    }
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (item.getItemId() == android.R.id.home) {
+            finish();
+            return true;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+}
index 36e39af20dfb712e798a6668e07c82d5c75e5ac6..e531809a38ce416dd09cac7bd7b13587624fb12d 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2020 Damyan Ivanov.
+ * 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
@@ -52,7 +52,6 @@ import net.ktnx.mobileledger.model.Data;
 import net.ktnx.mobileledger.model.MobileLedgerProfile;
 import net.ktnx.mobileledger.ui.CurrencySelectorFragment;
 import net.ktnx.mobileledger.ui.HueRingDialog;
-import net.ktnx.mobileledger.ui.activity.ProfileDetailActivity;
 import net.ktnx.mobileledger.utils.Colors;
 import net.ktnx.mobileledger.utils.Misc;
 
index cef8d8fbf6803e71c08491bf4d5805b121473441..21984333500a80d4733e684a7f7bd284967eac86 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2020 Damyan Ivanov.
+ * 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
@@ -39,7 +39,6 @@ import androidx.recyclerview.widget.RecyclerView;
 import net.ktnx.mobileledger.R;
 import net.ktnx.mobileledger.model.Data;
 import net.ktnx.mobileledger.model.MobileLedgerProfile;
-import net.ktnx.mobileledger.ui.activity.ProfileDetailActivity;
 import net.ktnx.mobileledger.utils.Colors;
 
 import java.lang.ref.WeakReference;
index 706794f9df81fcda0680c2eb89871b00d2eac007..2461b3be3bc12db39b762a8acbeb016863849305 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?><!--
-  ~ Copyright © 2020 Damyan Ivanov.
+  ~ 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
@@ -20,7 +20,7 @@
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    tools:context=".ui.activity.NewTransactionActivity"
+    tools:context=".ui.new_transaction.NewTransactionActivity"
     >
 
     <androidx.constraintlayout.widget.ConstraintLayout
index 52ea0652cc27967f7fde4cdedf16b4b86f9ec520..78cac2f68bcd3862cf205e9bc2341d14b07deb5b 100644 (file)
@@ -22,7 +22,7 @@
     android:layout_height="match_parent"
     android:fitsSystemWindows="true"
 
-    tools:context=".ui.activity.PatternsActivity"
+    tools:context=".ui.patterns.PatternsActivity"
     >
 
     <com.google.android.material.appbar.AppBarLayout
@@ -57,7 +57,7 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         app:layout_behavior="@string/appbar_scrolling_view_behavior"
-        tools:context=".ui.activity.PatternsActivity"
+        tools:context=".ui.patterns.PatternsActivity"
         tools:showIn="@layout/activity_patterns"
         >
 
index 87c7554aaa62f38dae146045438952cf6cc88d6f..282af250b523d333ccb24b5239c45bd3fe28715b 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright © 2020 Damyan Ivanov.
+  ~ 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
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    tools:context=".ui.activity.ProfileDetailActivity">
+    tools:context=".ui.profiles.ProfileDetailActivity"
+    >
 
     <com.google.android.material.appbar.AppBarLayout
         android:id="@+id/app_bar"
         android:layout_width="match_parent"
         android:layout_height="@dimen/app_bar_height"
         android:fitsSystemWindows="true"
-        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
+        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
+        >
 
         <com.google.android.material.appbar.CollapsingToolbarLayout
             android:id="@+id/toolbar_layout"
index 1311465aef0f5335e187e47dc938a3891ac86ff7..3473b1bd8d35db406fab690307be99e84236f5e8 100644 (file)
@@ -1,5 +1,5 @@
 <!--
-  ~ Copyright © 2020 Damyan Ivanov.
+  ~ 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
@@ -24,7 +24,8 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:animateLayoutChanges="true"
-        tools:context="net.ktnx.mobileledger.ui.activity.NewTransactionActivity">
+        tools:context="net.ktnx.mobileledger.ui.new_transaction.NewTransactionActivity"
+        >
 
         <ProgressBar
             android:id="@+id/progressBar"
index 734cd71e61680556f55c13f8a60b84c3f8ed00cb..1178f88cac9ca63a6bf944f4b02ad0b87f6948d3 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?><!--
-  ~ Copyright © 2020 Damyan Ivanov.
+  ~ 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
@@ -23,7 +23,7 @@
 
     <fragment
         android:id="@+id/newTransactionFragment"
-        android:name="net.ktnx.mobileledger.ui.activity.NewTransactionFragment"
+        android:name="net.ktnx.mobileledger.ui.new_transaction.NewTransactionFragment"
         android:label="NewTransactionFragment"
         >
         <action