From 5545ddea3574103c2a7eea552fff0d43a0587fac Mon Sep 17 00:00:00 2001 From: Damyan Ivanov Date: Thu, 20 Feb 2020 19:58:23 +0200 Subject: [PATCH] add functional currency selector when entering new transactions --- .../json/v1_14/ParsedPosting.java | 6 +- .../json/v1_15/ParsedPosting.java | 6 +- .../net/ktnx/mobileledger/model/Currency.java | 124 +++++++++++ .../model/LedgerTransactionAccount.java | 6 +- .../model/MobileLedgerProfile.java | 17 ++ .../ui/CurrencySelectorFragment.java | 208 ++++++++++++++++++ .../ui/CurrencySelectorModel.java | 32 +++ .../CurrencySelectorRecyclerViewAdapter.java | 101 +++++++++ .../ui/OnCurrencyLongClickListener.java | 34 +++ .../ui/OnCurrencySelectedListener.java | 34 +++ .../ui/activity/NewTransactionItemHolder.java | 115 +++++++++- .../ui/activity/NewTransactionModel.java | 20 +- .../mobileledger/utils/DimensionUtils.java | 4 + .../res/layout/fragment_currency_selector.xml | 37 ++++ .../fragment_currency_selector_list.xml | 166 ++++++++++++++ .../main/res/layout/new_transaction_row.xml | 26 ++- app/src/main/res/values-bg/strings.xml | 8 + app/src/main/res/values/strings.xml | 9 + 18 files changed, 939 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/net/ktnx/mobileledger/model/Currency.java create mode 100644 app/src/main/java/net/ktnx/mobileledger/ui/CurrencySelectorFragment.java create mode 100644 app/src/main/java/net/ktnx/mobileledger/ui/CurrencySelectorModel.java create mode 100644 app/src/main/java/net/ktnx/mobileledger/ui/CurrencySelectorRecyclerViewAdapter.java create mode 100644 app/src/main/java/net/ktnx/mobileledger/ui/OnCurrencyLongClickListener.java create mode 100644 app/src/main/java/net/ktnx/mobileledger/ui/OnCurrencySelectedListener.java create mode 100644 app/src/main/res/layout/fragment_currency_selector.xml create mode 100644 app/src/main/res/layout/fragment_currency_selector_list.xml diff --git a/app/src/main/java/net/ktnx/mobileledger/json/v1_14/ParsedPosting.java b/app/src/main/java/net/ktnx/mobileledger/json/v1_14/ParsedPosting.java index e099f703..472fee7c 100644 --- a/app/src/main/java/net/ktnx/mobileledger/json/v1_14/ParsedPosting.java +++ b/app/src/main/java/net/ktnx/mobileledger/json/v1_14/ParsedPosting.java @@ -57,11 +57,13 @@ public class ParsedPosting extends net.ktnx.mobileledger.json.ParsedPosting { qty.setDecimalMantissa(Math.round(acc.getAmount() * 100)); amt.setAquantity(qty); ParsedStyle style = new ParsedStyle(); - style.setAscommodityside('L'); - style.setAscommodityspaced(false); + style.setAscommodityside(getCommoditySide()); + style.setAscommodityspaced(getCommoditySpaced()); style.setAsprecision(2); style.setAsdecimalpoint('.'); amt.setAstyle(style); + if (acc.getCurrency() != null) + amt.setAcommodity(acc.getCurrency()); amounts.add(amt); result.setPamount(amounts); return result; diff --git a/app/src/main/java/net/ktnx/mobileledger/json/v1_15/ParsedPosting.java b/app/src/main/java/net/ktnx/mobileledger/json/v1_15/ParsedPosting.java index 7e2a0573..38777684 100644 --- a/app/src/main/java/net/ktnx/mobileledger/json/v1_15/ParsedPosting.java +++ b/app/src/main/java/net/ktnx/mobileledger/json/v1_15/ParsedPosting.java @@ -57,11 +57,13 @@ public class ParsedPosting extends net.ktnx.mobileledger.json.ParsedPosting { qty.setDecimalMantissa(Math.round(acc.getAmount() * 100)); amt.setAquantity(qty); ParsedStyle style = new ParsedStyle(); - style.setAscommodityside('L'); - style.setAscommodityspaced(false); + style.setAscommodityside(getCommoditySide()); + style.setAscommodityspaced(getCommoditySpaced()); style.setAsprecision(2); style.setAsdecimalpoint('.'); amt.setAstyle(style); + if (acc.getCurrency() != null) + amt.setAcommodity(acc.getCurrency()); amounts.add(amt); result.setPamount(amounts); return result; diff --git a/app/src/main/java/net/ktnx/mobileledger/model/Currency.java b/app/src/main/java/net/ktnx/mobileledger/model/Currency.java new file mode 100644 index 00000000..a4854c05 --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/model/Currency.java @@ -0,0 +1,124 @@ +/* + * 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 . + */ + +package net.ktnx.mobileledger.model; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; + +import net.ktnx.mobileledger.App; + +public class Currency { + public static final DiffUtil.ItemCallback DIFF_CALLBACK = + new DiffUtil.ItemCallback() { + @Override + public boolean areItemsTheSame(@NonNull Currency oldItem, + @NonNull Currency newItem) { + return oldItem.id == newItem.id; + } + @Override + public boolean areContentsTheSame(@NonNull Currency oldItem, + @NonNull Currency newItem) { + return oldItem.name.equals(newItem.name) && + oldItem.position.equals(newItem.position) && + (oldItem.hasGap == newItem.hasGap); + } + }; + private int id; + private String name; + private Position position; + private boolean hasGap; + public Currency(int id, String name) { + this.id = id; + this.name = name; + position = Position.after; + hasGap = true; + } + public Currency(int id, String name, Position position, boolean hasGap) { + this.id = id; + this.name = name; + this.position = position; + this.hasGap = hasGap; + } + public Currency(MobileLedgerProfile profile, String name, Position position, boolean hasGap) { + SQLiteDatabase db = App.getDatabase(); + int attempts = 0; + while (true) { + if (++attempts > 10) + throw new RuntimeException("Giving up getting next ID after 10 attempts"); + + try (Cursor c = db.rawQuery("select max(rowid) from currencies", null)) { + c.moveToNext(); + int nextId = c.getInt(0) + 1; + db.execSQL("insert into currencies(id, name, position, has_gap) values(?, ?, ?, ?)", + new Object[]{nextId, name, position.toString(), hasGap}); + + this.id = nextId; + break; + } + } + + this.name = name; + this.position = position; + this.hasGap = hasGap; + } + public int getId() { + return id; + } + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + public Position getPosition() { + return position; + } + public void setPosition(Position position) { + this.position = position; + } + public boolean hasGap() { + return hasGap; + } + public void setHasGap(boolean hasGap) { + this.hasGap = hasGap; + } + public enum Position { + before(-1), after(1), unknown(0), none(-2); + private int value; + Position(int value) { + this.value = value; + } + static Position valueOf(int value) { + switch (value) { + case -1: + return before; + case +1: + return after; + case 0: + return unknown; + case -2: + return none; + default: + throw new IllegalStateException(String.format("Unexpected value (%d)", value)); + } + } + } +} diff --git a/app/src/main/java/net/ktnx/mobileledger/model/LedgerTransactionAccount.java b/app/src/main/java/net/ktnx/mobileledger/model/LedgerTransactionAccount.java index 59e1dab0..ba80ac61 100644 --- a/app/src/main/java/net/ktnx/mobileledger/model/LedgerTransactionAccount.java +++ b/app/src/main/java/net/ktnx/mobileledger/model/LedgerTransactionAccount.java @@ -67,22 +67,22 @@ public class LedgerTransactionAccount { return amount; } - public void setAmount(float account_amount) { this.amount = account_amount; this.amountSet = true; } - public void resetAmount() { this.amountSet = false; } - public boolean isAmountSet() { return amountSet; } public String getCurrency() { return currency; } + public void setCurrency(String currency) { + this.currency = currency; + } @NonNull public String toString() { if (!amountSet) diff --git a/app/src/main/java/net/ktnx/mobileledger/model/MobileLedgerProfile.java b/app/src/main/java/net/ktnx/mobileledger/model/MobileLedgerProfile.java index 7dc4bbfc..831dc63f 100644 --- a/app/src/main/java/net/ktnx/mobileledger/model/MobileLedgerProfile.java +++ b/app/src/main/java/net/ktnx/mobileledger/model/MobileLedgerProfile.java @@ -488,6 +488,23 @@ public final class MobileLedgerProfile { db.endTransaction(); } } + public List getCurrencies() { + SQLiteDatabase db = App.getDatabase(); + + ArrayList result = new ArrayList<>(); + + try (Cursor c = db.rawQuery("SELECT c.id, c.name, c.position, c.has_gap FROM currencies c", + new String[]{})) + { + while (c.moveToNext()) { + Currency currency = new Currency(c.getInt(0), c.getString(1), + Currency.Position.valueOf(c.getInt(2)), c.getInt(3) == 1); + result.add(currency); + } + } + + return result; + } public enum FutureDates { None(0), OneWeek(7), TwoWeeks(14), OneMonth(30), TwoMonths(60), ThreeMonths(90), SixMonths(180), OneYear(365), All(-1); diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/CurrencySelectorFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/CurrencySelectorFragment.java new file mode 100644 index 00000000..76a7d348 --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/ui/CurrencySelectorFragment.java @@ -0,0 +1,208 @@ +/* + * 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 . + */ + +package net.ktnx.mobileledger.ui; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.view.View; +import android.widget.RadioButton; +import android.widget.RadioGroup; +import android.widget.Switch; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatDialogFragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import net.ktnx.mobileledger.App; +import net.ktnx.mobileledger.R; +import net.ktnx.mobileledger.model.Currency; +import net.ktnx.mobileledger.model.Data; +import net.ktnx.mobileledger.model.MobileLedgerProfile; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * A fragment representing a list of Items. + *

+ * Activities containing this fragment MUST implement the {@link OnCurrencySelectedListener} + * interface. + */ +public class CurrencySelectorFragment extends AppCompatDialogFragment + implements OnCurrencySelectedListener, OnCurrencyLongClickListener { + + public static final int DEFAULT_COLUMN_COUNT = 2; + private static final String ARG_COLUMN_COUNT = "column-count"; + private int mColumnCount = DEFAULT_COLUMN_COUNT; + private OnCurrencySelectedListener mListener; + private CurrencySelectorModel model; + + /** + * Mandatory empty constructor for the fragment manager to instantiate the + * fragment (e.g. upon screen orientation changes). + */ + public CurrencySelectorFragment() { + } + @SuppressWarnings("unused") + public static CurrencySelectorFragment newInstance() { + return newInstance(DEFAULT_COLUMN_COUNT); + } + public static CurrencySelectorFragment newInstance(int columnCount) { + CurrencySelectorFragment fragment = new CurrencySelectorFragment(); + Bundle args = new Bundle(); + args.putInt(ARG_COLUMN_COUNT, columnCount); + fragment.setArguments(args); + return fragment; + } + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (getArguments() != null) { + mColumnCount = getArguments().getInt(ARG_COLUMN_COUNT, DEFAULT_COLUMN_COUNT); + } + } + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + Context context = Objects.requireNonNull(getContext()); + Dialog csd = new Dialog(context); + csd.setContentView(R.layout.fragment_currency_selector_list); + csd.setTitle(R.string.choose_currency_label); + + RecyclerView recyclerView = csd.findViewById(R.id.list); + + if (mColumnCount <= 1) { + recyclerView.setLayoutManager(new LinearLayoutManager(context)); + } + else { + recyclerView.setLayoutManager(new GridLayoutManager(context, mColumnCount)); + } + model = new ViewModelProvider(this).get(CurrencySelectorModel.class); + MobileLedgerProfile profile = Objects.requireNonNull(Data.profile.getValue()); + + model.currencies.setValue(new CopyOnWriteArrayList<>(profile.getCurrencies())); + CurrencySelectorRecyclerViewAdapter adapter = new CurrencySelectorRecyclerViewAdapter(); + model.currencies.observe(this, list -> adapter.submitList(list)); + + recyclerView.setAdapter(adapter); + adapter.setCurrencySelectedListener(this); + adapter.setCurrencyLongClickListener(this); + + final TextView tvNewCurrName = csd.findViewById(R.id.new_currency_name); + final TextView tvNoCurrBtn = csd.findViewById(R.id.btn_no_currency); + final TextView tvAddCurrOkBtn = csd.findViewById(R.id.btn_add_currency); + final TextView tvAddCurrBtn = csd.findViewById(R.id.btn_add_new); + + tvNewCurrName.setVisibility(View.GONE); + tvAddCurrOkBtn.setVisibility(View.GONE); + tvNoCurrBtn.setVisibility(View.VISIBLE); + tvAddCurrBtn.setVisibility(View.VISIBLE); + + tvAddCurrBtn.setOnClickListener(v -> { + tvNewCurrName.setVisibility(View.VISIBLE); + tvAddCurrOkBtn.setVisibility(View.VISIBLE); + + tvNoCurrBtn.setVisibility(View.GONE); + tvAddCurrBtn.setVisibility(View.GONE); + + tvNewCurrName.setText(null); + tvNewCurrName.requestFocus(); + net.ktnx.mobileledger.utils.Misc.showSoftKeyboard(this); + }); + + tvAddCurrOkBtn.setOnClickListener(v -> { + + + String currName = String.valueOf(tvNewCurrName.getText()); + if (!currName.isEmpty()) { + List list = new ArrayList<>( model.currencies.getValue()); + // FIXME hardcoded position and gap setting + list.add(new Currency(profile, String.valueOf(tvNewCurrName.getText()), + Currency.Position.after, false)); + model.currencies.setValue(list); + } + + tvNewCurrName.setVisibility(View.GONE); + tvAddCurrOkBtn.setVisibility(View.GONE); + + tvNoCurrBtn.setVisibility(View.VISIBLE); + tvAddCurrBtn.setVisibility(View.VISIBLE); + }); + + tvNoCurrBtn.setOnClickListener(v -> { + adapter.notifyCurrencySelected(null); + dismiss(); + }); + + RadioButton rbPositionLeft = csd.findViewById(R.id.currency_position_left); + RadioButton rbPositionRight = csd.findViewById(R.id.currency_position_right); + + if (Data.currencySymbolPosition.getValue() == Currency.Position.before) + rbPositionLeft.toggle(); + else + rbPositionRight.toggle(); + + RadioGroup rgPosition = csd.findViewById(R.id.position_radio_group); + rgPosition.setOnCheckedChangeListener((group, checkedId) -> { + if (checkedId == R.id.currency_position_left) + Data.currencySymbolPosition.setValue(Currency.Position.before); + else + Data.currencySymbolPosition.setValue(Currency.Position.after); + }); + + Switch gap = csd.findViewById(R.id.currency_gap); + + gap.setChecked(Data.currencyGap.getValue()); + + gap.setOnCheckedChangeListener((v, checked) -> { + Data.currencyGap.setValue(checked); + }); + + return csd; + } + public void setOnCurrencySelectedListener(OnCurrencySelectedListener listener) { + mListener = listener; + } + public void resetOnCurrencySelectedListener() { + mListener = null; + } + @Override + public void onCurrencySelected(Currency item) { + if (mListener != null) + mListener.onCurrencySelected(item); + + dismiss(); + } + + @Override + public void onCurrencyLongClick(Currency item) { + ArrayList list = new ArrayList<>(model.currencies.getValue()); + App.getDatabase().execSQL("delete from currencies where id=?", new Object[]{item.getId()}); + list.remove(item); + model.currencies.setValue(list); + } +} diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/CurrencySelectorModel.java b/app/src/main/java/net/ktnx/mobileledger/ui/CurrencySelectorModel.java new file mode 100644 index 00000000..10fea517 --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/ui/CurrencySelectorModel.java @@ -0,0 +1,32 @@ +/* + * 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 . + */ + +package net.ktnx.mobileledger.ui; + +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import net.ktnx.mobileledger.model.Currency; + +import java.util.List; + +public class CurrencySelectorModel extends ViewModel { + public final MutableLiveData> currencies; + public CurrencySelectorModel() { + this.currencies = new MutableLiveData<>(); + } +} diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/CurrencySelectorRecyclerViewAdapter.java b/app/src/main/java/net/ktnx/mobileledger/ui/CurrencySelectorRecyclerViewAdapter.java new file mode 100644 index 00000000..b7d90a3a --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/ui/CurrencySelectorRecyclerViewAdapter.java @@ -0,0 +1,101 @@ +/* + * 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 . + */ + +package net.ktnx.mobileledger.ui; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.recyclerview.widget.ListAdapter; +import androidx.recyclerview.widget.RecyclerView; + +import net.ktnx.mobileledger.R; +import net.ktnx.mobileledger.model.Currency; + +import org.jetbrains.annotations.NotNull; + +/** + * {@link RecyclerView.Adapter} that can display a {@link Currency} and makes a call to the + * specified {@link OnCurrencySelectedListener}. + */ +public class CurrencySelectorRecyclerViewAdapter + extends ListAdapter { + + private OnCurrencySelectedListener currencySelectedListener; + private OnCurrencyLongClickListener currencyLongClickListener; + public CurrencySelectorRecyclerViewAdapter() { + super(Currency.DIFF_CALLBACK); + } + @NotNull + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.fragment_currency_selector, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(final ViewHolder holder, int position) { + holder.bindTo(getItem(position)); + } + public void setCurrencySelectedListener(OnCurrencySelectedListener listener) { + this.currencySelectedListener = listener; + } + public void resetCurrencySelectedListener() { + currencySelectedListener = null; + } + public void notifyCurrencySelected(Currency currency) { + if (null != currencySelectedListener) + currencySelectedListener.onCurrencySelected(currency); + } + public void setCurrencyLongClickListener(OnCurrencyLongClickListener listener) { + this.currencyLongClickListener = listener; + } + public void resetCurrencyLockClickListener() { currencyLongClickListener = null; } + private void notifyCurrencyLongClicked(Currency mItem) { + if (null != currencyLongClickListener) + currencyLongClickListener.onCurrencyLongClick(mItem); + } + + public class ViewHolder extends RecyclerView.ViewHolder { + private final TextView mNameView; + private Currency mItem; + + ViewHolder(View view) { + super(view); + mNameView = view.findViewById(R.id.content); + + view.setOnClickListener(v -> notifyCurrencySelected(mItem)); + view.setOnLongClickListener(v -> { + notifyCurrencyLongClicked(mItem); + return false; + }); + } + + @NotNull + @Override + public String toString() { + return super.toString() + " '" + mNameView.getText() + "'"; + } + void bindTo(Currency item) { + mItem = item; + mNameView.setText(item.getName()); + } + } +} diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/OnCurrencyLongClickListener.java b/app/src/main/java/net/ktnx/mobileledger/ui/OnCurrencyLongClickListener.java new file mode 100644 index 00000000..f681f999 --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/ui/OnCurrencyLongClickListener.java @@ -0,0 +1,34 @@ +/* + * 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 . + */ + +package net.ktnx.mobileledger.ui; + +import net.ktnx.mobileledger.model.Currency; + +/** + * 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. + *

+ * See the Android Training lesson Communicating with Other Fragments for more information. + */ +public interface OnCurrencyLongClickListener { + void onCurrencyLongClick(Currency item); +} diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/OnCurrencySelectedListener.java b/app/src/main/java/net/ktnx/mobileledger/ui/OnCurrencySelectedListener.java new file mode 100644 index 00000000..c12f32a2 --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/ui/OnCurrencySelectedListener.java @@ -0,0 +1,34 @@ +/* + * 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 . + */ + +package net.ktnx.mobileledger.ui; + +import net.ktnx.mobileledger.model.Currency; + +/** + * 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. + *

+ * See the Android Training lesson Communicating with Other Fragments for more information. + */ +public interface OnCurrencySelectedListener { + void onCurrencySelected(Currency item); +} 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 index 0ee99136..6f410924 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionItemHolder.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionItemHolder.java @@ -22,6 +22,7 @@ import android.os.Build; import android.text.Editable; import android.text.TextWatcher; import android.text.method.DigitsKeyListener; +import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; @@ -39,13 +40,16 @@ import androidx.recyclerview.widget.RecyclerView; import net.ktnx.mobileledger.R; import net.ktnx.mobileledger.async.DescriptionSelectedCallback; +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.OnCurrencySelectedListener; import net.ktnx.mobileledger.ui.TextViewClearHelper; import net.ktnx.mobileledger.utils.Colors; +import net.ktnx.mobileledger.utils.DimensionUtils; import net.ktnx.mobileledger.utils.Logger; import net.ktnx.mobileledger.utils.MLDB; import net.ktnx.mobileledger.utils.Misc; @@ -59,9 +63,11 @@ import java.util.Locale; import static net.ktnx.mobileledger.ui.activity.NewTransactionModel.ItemType; class NewTransactionItemHolder extends RecyclerView.ViewHolder - implements DatePickerFragment.DatePickedListener, DescriptionSelectedCallback { + implements DatePickerFragment.DatePickedListener, DescriptionSelectedCallback, + OnCurrencySelectedListener { private final String decimalSeparator; private final String decimalDot; + private final TextView tvCurrency; private NewTransactionModel.Item item; private TextView tvDate; private AutoCompleteTextView tvDescription; @@ -81,10 +87,14 @@ class NewTransactionItemHolder extends RecyclerView.ViewHolder private Observer editableObserver; private Observer commentVisibleObserver; private Observer commentObserver; + private Observer currencyPositionObserver; + private Observer currencyGapObserver; private Observer localeObserver; + private Observer currencyObserver; private boolean inUpdate = false; private boolean syncingData = false; private View commentButton; + //TODO multiple amounts with different currencies per posting NewTransactionItemHolder(@NonNull View itemView, NewTransactionItemsAdapter adapter) { super(itemView); tvAccount = itemView.findViewById(R.id.account_row_acc_name); @@ -92,6 +102,7 @@ class NewTransactionItemHolder extends RecyclerView.ViewHolder new TextViewClearHelper().attachToTextView((EditText) tvComment); commentButton = itemView.findViewById(R.id.comment_button); tvAmount = itemView.findViewById(R.id.account_row_acc_amounts); + tvCurrency = itemView.findViewById(R.id.currency); tvDate = itemView.findViewById(R.id.new_transaction_date); tvDescription = itemView.findViewById(R.id.new_transaction_description); lHead = itemView.findViewById(R.id.ntr_data); @@ -178,10 +189,15 @@ class NewTransactionItemHolder extends RecyclerView.ViewHolder 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) { - + Logger.debug("num", + String.format(Locale.US, "onTextChanged: start=%d, before=%d, count=%d", + start, before, count)); } @Override public void afterTextChanged(Editable s) { @@ -207,6 +223,13 @@ class NewTransactionItemHolder extends RecyclerView.ViewHolder tvComment.addTextChangedListener(tw); tvAmount.addTextChangedListener(amountWatcher); + tvCurrency.setOnClickListener(v -> { + CurrencySelectorFragment cpf = new CurrencySelectorFragment(); + cpf.setOnCurrencySelectedListener(this); + final AppCompatActivity activity = (AppCompatActivity) v.getContext(); + cpf.show(activity.getSupportFragmentManager(), "currency-selector"); + }); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) tvAmount.setKeyListener(DigitsKeyListener.getInstance(Locale.getDefault(), true, true)); else @@ -315,10 +338,78 @@ class NewTransactionItemHolder extends RecyclerView.ViewHolder tvAmount.setImeOptions(EditorInfo.IME_ACTION_NEXT); }; + currencyPositionObserver = position -> { + updateCurrencyPositionAndPadding(position, Data.currencyGap.getValue()); + }; + + currencyGapObserver = hasGap -> { + updateCurrencyPositionAndPadding(Data.currencySymbolPosition.getValue(), hasGap); + }; + localeObserver = locale -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) tvAmount.setKeyListener(DigitsKeyListener.getInstance(locale, true, true)); }; + + currencyObserver = this::setCurrency; + } + private void updateCurrencyPositionAndPadding(Currency.Position position, boolean hasGap) { + ConstraintLayout.LayoutParams amountLP = + (ConstraintLayout.LayoutParams) tvAmount.getLayoutParams(); + ConstraintLayout.LayoutParams currencyLP = + (ConstraintLayout.LayoutParams) tvCurrency.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 = tvCurrency.getId(); + + tvCurrency.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 = tvCurrency.getId(); + + tvCurrency.setGravity(Gravity.START); + } + + amountLP.resolveLayoutDirection(tvAmount.getLayoutDirection()); + currencyLP.resolveLayoutDirection(tvCurrency.getLayoutDirection()); + + tvAmount.setLayoutParams(amountLP); + tvCurrency.setLayoutParams(currencyLP); + + // distance between the amount and the currency symbol + int gapSize = DimensionUtils.sp2px(tvCurrency.getContext(), 5); + + if (position == Currency.Position.before) { + tvCurrency.setPaddingRelative(0, 0, hasGap ? gapSize : 0, 0); + } + else { + tvCurrency.setPaddingRelative(hasGap ? gapSize : 0, 0, 0, 0); + } + } + private void setCurrencyString(String currency) { + if ((currency == null) || currency.isEmpty()) { + tvCurrency.setText(R.string.currency_symbol); + tvCurrency.setTextColor(0x7f000000 + (0x00ffffff & Colors.defaultTextColor)); + } + else { + tvCurrency.setText(currency); + tvCurrency.setTextColor(Colors.defaultTextColor); + } + } + private void setCurrency(Currency currency) { + setCurrencyString((currency == null) ? null : currency.getName()); } private void setEditable(Boolean editable) { tvDate.setEnabled(editable); @@ -397,6 +488,7 @@ class NewTransactionItemHolder extends RecyclerView.ViewHolder if (amount.isEmpty()) { account.resetAmount(); + account.setCurrency(null); } else { try { @@ -409,6 +501,14 @@ class NewTransactionItemHolder extends RecyclerView.ViewHolder "input was '%s'", amount)); account.resetAmount(); } + final String curr = String.valueOf(tvCurrency.getText()); + if (curr.equals(tvCurrency.getContext() + .getResources() + .getString(R.string.currency_symbol)) || + curr.isEmpty()) + account.setCurrency(null); + else + account.setCurrency(curr); } break; @@ -447,7 +547,10 @@ class NewTransactionItemHolder extends RecyclerView.ViewHolder .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 = null; } @@ -473,6 +576,7 @@ class NewTransactionItemHolder extends RecyclerView.ViewHolder // tvAmount.setHint(R.string.zero_amount); } tvAmount.setHint(item.getAmountHint()); + setCurrencyString(acc.getCurrency()); lHead.setVisibility(View.GONE); lAccount.setVisibility(View.VISIBLE); lPadding.setVisibility(View.GONE); @@ -500,7 +604,10 @@ class NewTransactionItemHolder extends RecyclerView.ViewHolder .observeFocusedItem(activity, focusedAccountObserver); item.getModel() .observeAccountCount(activity, accountCountObserver); + Data.currencySymbolPosition.observe(activity, currencyPositionObserver); + Data.currencyGap.observe(activity, currencyGapObserver); Data.locale.observe(activity, localeObserver); + item.observeCurrency(activity, currencyObserver); } } finally { @@ -518,6 +625,10 @@ class NewTransactionItemHolder extends RecyclerView.ViewHolder } @Override + public void onCurrencySelected(Currency item) { + this.item.setCurrency(item); + } + @Override public void descriptionSelected(String description) { tvAccount.setText(description); tvAmount.requestFocus(View.FOCUS_FORWARD); diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionModel.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionModel.java index 5bc327a7..62f85ba9 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionModel.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionModel.java @@ -26,6 +26,7 @@ import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModel; import net.ktnx.mobileledger.BuildConfig; +import net.ktnx.mobileledger.model.Currency; import net.ktnx.mobileledger.model.LedgerTransactionAccount; import net.ktnx.mobileledger.utils.Logger; import net.ktnx.mobileledger.utils.Misc; @@ -334,6 +335,7 @@ public class NewTransactionModel extends ViewModel { private FocusedElement focusedElement = FocusedElement.Account; private MutableLiveData comment = new MutableLiveData<>(null); private MutableLiveData commentVisible = new MutableLiveData<>(false); + private MutableLiveData currency = new MutableLiveData<>(null); public Item(NewTransactionModel model) { this.model = model; type = ItemType.bottomFiller; @@ -532,8 +534,7 @@ public class NewTransactionModel extends ViewModel { public void stopObservingCommentVisible(Observer observer) { commentVisible.removeObserver(observer); } - public void observeComment(NewTransactionActivity activity, - Observer observer) { + public void observeComment(NewTransactionActivity activity, Observer observer) { comment.observe(activity, observer); } public void stopObservingComment(Observer observer) { @@ -543,5 +544,20 @@ public class NewTransactionModel extends ViewModel { getAccount().setComment(comment); this.comment.postValue(comment); } + public Currency getCurrency() { + return this.currency.getValue(); + } + public void setCurrency(Currency currency) { + getAccount().setCurrency((currency != null && !currency.getName() + .isEmpty()) ? currency.getName() + : null); + this.currency.setValue(currency); + } + public void observeCurrency(NewTransactionActivity activity, Observer observer) { + currency.observe(activity, observer); + } + public void stopObservingCurrency(Observer observer) { + currency.removeObserver(observer); + } } } diff --git a/app/src/main/java/net/ktnx/mobileledger/utils/DimensionUtils.java b/app/src/main/java/net/ktnx/mobileledger/utils/DimensionUtils.java index 0a51215a..d8571bfb 100644 --- a/app/src/main/java/net/ktnx/mobileledger/utils/DimensionUtils.java +++ b/app/src/main/java/net/ktnx/mobileledger/utils/DimensionUtils.java @@ -25,5 +25,9 @@ public class DimensionUtils { return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics())); } + public static int sp2px(Context context, float sp) { + return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, + context.getResources().getDisplayMetrics())); + } } diff --git a/app/src/main/res/layout/fragment_currency_selector.xml b/app/src/main/res/layout/fragment_currency_selector.xml new file mode 100644 index 00000000..56309bec --- /dev/null +++ b/app/src/main/res/layout/fragment_currency_selector.xml @@ -0,0 +1,37 @@ + + + + + + + diff --git a/app/src/main/res/layout/fragment_currency_selector_list.xml b/app/src/main/res/layout/fragment_currency_selector_list.xml new file mode 100644 index 00000000..e67e2d0f --- /dev/null +++ b/app/src/main/res/layout/fragment_currency_selector_list.xml @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/new_transaction_row.xml b/app/src/main/res/layout/new_transaction_row.xml index 0b36bc4e..11951e58 100644 --- a/app/src/main/res/layout/new_transaction_row.xml +++ b/app/src/main/res/layout/new_transaction_row.xml @@ -116,7 +116,7 @@ app:layout_constraintStart_toEndOf="@id/comment_button" app:layout_constraintTop_toBottomOf="@id/account_row_acc_name" /> - - + android:textAlignment="viewEnd" + app:layout_constraintWidth_min="@dimen/text_margin"/> + + + diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 3f8c56cd..27f802a6 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -132,6 +132,14 @@ Версия 1.15 или по-нова Версия 1.14.x Версия на сървъра + Добавяне… + Затваряне бележка + без + Валута + Отстояние от сумата + Вляво + Вдясно + валута/ценност diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 97b40641..cd375358 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -144,5 +144,14 @@ Version 1.15 and above Detect automaticaly Backend server version + ¤ + Add… + Close comment + Currency + currency/commodity + none + Left + Right + Offset from the value -- 2.39.2