]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionHeaderItemHolder.java
Room-based profile management
[mobile-ledger.git] / app / src / main / java / net / ktnx / mobileledger / ui / new_transaction / NewTransactionHeaderItemHolder.java
1 /*
2  * Copyright © 2021 Damyan Ivanov.
3  * This file is part of MoLe.
4  * MoLe is free software: you can distribute it and/or modify it
5  * under the term of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your opinion), any later version.
8  *
9  * MoLe is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License terms for details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
16  */
17
18 package net.ktnx.mobileledger.ui.new_transaction;
19
20 import android.annotation.SuppressLint;
21 import android.graphics.Typeface;
22 import android.text.Editable;
23 import android.text.TextUtils;
24 import android.text.TextWatcher;
25 import android.view.View;
26 import android.widget.EditText;
27 import android.widget.ListAdapter;
28 import android.widget.TextView;
29
30 import androidx.annotation.ColorInt;
31 import androidx.annotation.NonNull;
32
33 import net.ktnx.mobileledger.R;
34 import net.ktnx.mobileledger.databinding.NewTransactionHeaderRowBinding;
35 import net.ktnx.mobileledger.db.TransactionDescriptionAutocompleteAdapter;
36 import net.ktnx.mobileledger.model.Data;
37 import net.ktnx.mobileledger.model.FutureDates;
38 import net.ktnx.mobileledger.ui.DatePickerFragment;
39 import net.ktnx.mobileledger.utils.Logger;
40 import net.ktnx.mobileledger.utils.Misc;
41 import net.ktnx.mobileledger.utils.SimpleDate;
42
43 import java.text.DecimalFormatSymbols;
44 import java.text.ParseException;
45
46 class NewTransactionHeaderItemHolder extends NewTransactionItemViewHolder
47         implements DatePickerFragment.DatePickedListener {
48     private final NewTransactionHeaderRowBinding b;
49     private boolean ignoreFocusChanges = false;
50     private String decimalSeparator;
51     private boolean inUpdate = false;
52     private boolean syncingData = false;
53     //TODO multiple amounts with different currencies per posting?
54     NewTransactionHeaderItemHolder(@NonNull NewTransactionHeaderRowBinding b,
55                                    NewTransactionItemsAdapter adapter) {
56         super(b.getRoot(), adapter);
57         this.b = b;
58
59         b.newTransactionDescription.setNextFocusForwardId(View.NO_ID);
60
61         b.newTransactionDate.setOnClickListener(v -> pickTransactionDate());
62
63         b.transactionCommentButton.setOnClickListener(v -> {
64             b.transactionComment.setVisibility(View.VISIBLE);
65             b.transactionComment.requestFocus();
66         });
67
68         @SuppressLint("DefaultLocale") View.OnFocusChangeListener focusMonitor = (v, hasFocus) -> {
69             final int id = v.getId();
70             if (hasFocus) {
71                 boolean wasSyncing = syncingData;
72                 syncingData = true;
73                 try {
74                     final int pos = getAdapterPosition();
75                     if (id == R.id.transaction_comment) {
76                         adapter.noteFocusIsOnTransactionComment(pos);
77                     }
78                     else if (id == R.id.new_transaction_description) {
79                         adapter.noteFocusIsOnDescription(pos);
80                     }
81                     else
82                         throw new IllegalStateException("Where is the focus? " + id);
83                 }
84                 finally {
85                     syncingData = wasSyncing;
86                 }
87             }
88
89             if (id == R.id.transaction_comment) {
90                 commentFocusChanged(b.transactionComment, hasFocus);
91             }
92         };
93
94         b.newTransactionDescription.setOnFocusChangeListener(focusMonitor);
95         b.transactionComment.setOnFocusChangeListener(focusMonitor);
96
97         NewTransactionActivity activity = (NewTransactionActivity) b.getRoot()
98                                                                     .getContext();
99
100         b.newTransactionDescription.setAdapter(
101                 new TransactionDescriptionAutocompleteAdapter(activity));
102         b.newTransactionDescription.setOnItemClickListener(
103                 (parent, view, position, id) -> activity.descriptionSelected(
104                         parent.getItemAtPosition(position)
105                               .toString()));
106
107         decimalSeparator = "";
108         Data.locale.observe(activity, locale -> decimalSeparator = String.valueOf(
109                 DecimalFormatSymbols.getInstance(locale)
110                                     .getMonetaryDecimalSeparator()));
111
112         final TextWatcher tw = new TextWatcher() {
113             @Override
114             public void beforeTextChanged(CharSequence s, int start, int count, int after) {
115             }
116
117             @Override
118             public void onTextChanged(CharSequence s, int start, int before, int count) {
119             }
120
121             @Override
122             public void afterTextChanged(Editable s) {
123 //                debug("input", "text changed");
124                 if (inUpdate)
125                     return;
126
127                 Logger.debug("textWatcher", "calling syncData()");
128                 if (syncData()) {
129                     Logger.debug("textWatcher",
130                             "syncData() returned, checking if transaction is submittable");
131                     adapter.model.checkTransactionSubmittable(null);
132                 }
133                 Logger.debug("textWatcher", "done");
134             }
135         };
136         b.newTransactionDescription.addTextChangedListener(tw);
137         monitorComment(b.transactionComment);
138
139         commentFocusChanged(b.transactionComment, false);
140
141         adapter.model.getFocusInfo()
142                      .observe(activity, this::applyFocus);
143
144         adapter.model.getShowComments()
145                      .observe(activity, show -> b.transactionCommentLayout.setVisibility(
146                              show ? View.VISIBLE : View.GONE));
147     }
148     private void applyFocus(NewTransactionModel.FocusInfo focusInfo) {
149         if (ignoreFocusChanges) {
150             Logger.debug("new-trans", "Ignoring focus change");
151             return;
152         }
153         ignoreFocusChanges = true;
154         try {
155             if (((focusInfo == null) || (focusInfo.element == null) ||
156                  focusInfo.position != getAdapterPosition()))
157                 return;
158
159             NewTransactionModel.Item head = getItem().toTransactionHead();
160             // bad idea - double pop-up, and not really necessary.
161             // the user can tap the input to get the calendar
162             //if (!tvDate.hasFocus()) tvDate.requestFocus();
163             switch (focusInfo.element) {
164                 case TransactionComment:
165                     b.transactionComment.setVisibility(View.VISIBLE);
166                     b.transactionComment.requestFocus();
167                     break;
168                 case Description:
169                     boolean focused = b.newTransactionDescription.requestFocus();
170 //                            tvDescription.dismissDropDown();
171                     if (focused)
172                         Misc.showSoftKeyboard((NewTransactionActivity) b.getRoot()
173                                                                         .getContext());
174                     break;
175             }
176         }
177         finally {
178             ignoreFocusChanges = false;
179         }
180     }
181     private void monitorComment(EditText editText) {
182         editText.addTextChangedListener(new TextWatcher() {
183             @Override
184             public void beforeTextChanged(CharSequence s, int start, int count, int after) {
185             }
186             @Override
187             public void onTextChanged(CharSequence s, int start, int before, int count) {
188             }
189             @Override
190             public void afterTextChanged(Editable s) {
191 //                debug("input", "text changed");
192                 if (inUpdate)
193                     return;
194
195                 Logger.debug("textWatcher", "calling syncData()");
196                 syncData();
197                 Logger.debug("textWatcher",
198                         "syncData() returned, checking if transaction is submittable");
199                 styleComment(editText, s.toString());
200                 Logger.debug("textWatcher", "done");
201             }
202         });
203     }
204     private void commentFocusChanged(TextView textView, boolean hasFocus) {
205         @ColorInt int textColor;
206         textColor = b.dummyText.getTextColors()
207                                .getDefaultColor();
208         if (hasFocus) {
209             textView.setTypeface(null, Typeface.NORMAL);
210             textView.setHint(R.string.transaction_account_comment_hint);
211         }
212         else {
213             int alpha = (textColor >> 24 & 0xff);
214             alpha = 3 * alpha / 4;
215             textColor = (alpha << 24) | (0x00ffffff & textColor);
216             textView.setTypeface(null, Typeface.ITALIC);
217             textView.setHint("");
218             if (TextUtils.isEmpty(textView.getText())) {
219                 textView.setVisibility(View.INVISIBLE);
220             }
221         }
222         textView.setTextColor(textColor);
223
224     }
225     private void setEditable(Boolean editable) {
226         b.newTransactionDate.setEnabled(editable);
227         b.newTransactionDescription.setEnabled(editable);
228     }
229     private void beginUpdates() {
230         if (inUpdate)
231             throw new RuntimeException("Already in update mode");
232         inUpdate = true;
233     }
234     private void endUpdates() {
235         if (!inUpdate)
236             throw new RuntimeException("Not in update mode");
237         inUpdate = false;
238     }
239     /**
240      * syncData()
241      * <p>
242      * Stores the data from the UI elements into the model item
243      * Returns true if there were changes made that suggest transaction has to be
244      * checked for being submittable
245      */
246     private boolean syncData() {
247         if (syncingData) {
248             Logger.debug("new-trans", "skipping syncData() loop");
249             return false;
250         }
251
252         if (getAdapterPosition() < 0) {
253             // probably the row was swiped out
254             Logger.debug("new-trans", "Ignoring request to suncData(): adapter position negative");
255             return false;
256         }
257
258
259         boolean significantChange = false;
260
261         syncingData = true;
262         try {
263             NewTransactionModel.TransactionHead head = getItem().toTransactionHead();
264
265             head.setDate(String.valueOf(b.newTransactionDate.getText()));
266
267             // transaction description is required
268             if (TextUtils.isEmpty(head.getDescription()) !=
269                 TextUtils.isEmpty(b.newTransactionDescription.getText()))
270                 significantChange = true;
271
272             head.setDescription(String.valueOf(b.newTransactionDescription.getText()));
273             head.setComment(String.valueOf(b.transactionComment.getText()));
274
275             return significantChange;
276         }
277         catch (ParseException e) {
278             throw new RuntimeException("Should not happen", e);
279         }
280         finally {
281             syncingData = false;
282         }
283     }
284     private void pickTransactionDate() {
285         DatePickerFragment picker = new DatePickerFragment();
286         picker.setFutureDates(FutureDates.valueOf(mProfile.getFutureDates()));
287         picker.setOnDatePickedListener(this);
288         picker.setCurrentDateFromText(b.newTransactionDate.getText());
289         picker.show(((NewTransactionActivity) b.getRoot()
290                                                .getContext()).getSupportFragmentManager(), null);
291     }
292     /**
293      * bind
294      *
295      * @param item updates the UI elements with the data from the model item
296      */
297     @SuppressLint("DefaultLocale")
298     public void bind(@NonNull NewTransactionModel.Item item) {
299         beginUpdates();
300         try {
301             syncingData = true;
302             try {
303                 NewTransactionModel.TransactionHead head = item.toTransactionHead();
304                 b.newTransactionDate.setText(head.getFormattedDate());
305
306                 // avoid triggering completion pop-up
307                 ListAdapter a = b.newTransactionDescription.getAdapter();
308                 try {
309                     b.newTransactionDescription.setAdapter(null);
310                     b.newTransactionDescription.setText(head.getDescription());
311                 }
312                 finally {
313                     b.newTransactionDescription.setAdapter(
314                             (TransactionDescriptionAutocompleteAdapter) a);
315                 }
316
317                 b.transactionComment.setText(head.getComment());
318                 //styleComment(b.transactionComment, head.getComment());
319
320                 setEditable(true);
321
322                 applyFocus(mAdapter.model.getFocusInfo()
323                                          .getValue());
324             }
325             finally {
326                 syncingData = false;
327             }
328         }
329         finally {
330             endUpdates();
331         }
332     }
333     private void styleComment(EditText editText, String comment) {
334         final View focusedView = editText.findFocus();
335         editText.setTypeface(null, (focusedView == editText) ? Typeface.NORMAL : Typeface.ITALIC);
336         editText.setVisibility(
337                 ((focusedView != editText) && TextUtils.isEmpty(comment)) ? View.INVISIBLE
338                                                                           : View.VISIBLE);
339     }
340     @Override
341     public void onDatePicked(int year, int month, int day) {
342         final NewTransactionModel.TransactionHead head = getItem().toTransactionHead();
343         head.setDate(new SimpleDate(year, month + 1, day));
344         b.newTransactionDate.setText(head.getFormattedDate());
345
346         boolean focused = b.newTransactionDescription.requestFocus();
347         if (focused)
348             Misc.showSoftKeyboard((NewTransactionActivity) b.getRoot()
349                                                             .getContext());
350
351     }
352 }