]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionHeaderItemHolder.java
fix a crash when returning to new transaction with no focused input
[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 import androidx.recyclerview.widget.RecyclerView;
33
34 import net.ktnx.mobileledger.R;
35 import net.ktnx.mobileledger.databinding.NewTransactionHeaderRowBinding;
36 import net.ktnx.mobileledger.db.TransactionDescriptionAutocompleteAdapter;
37 import net.ktnx.mobileledger.model.Data;
38 import net.ktnx.mobileledger.model.FutureDates;
39 import net.ktnx.mobileledger.ui.DatePickerFragment;
40 import net.ktnx.mobileledger.utils.Logger;
41 import net.ktnx.mobileledger.utils.Misc;
42 import net.ktnx.mobileledger.utils.SimpleDate;
43
44 import java.text.DecimalFormatSymbols;
45 import java.text.ParseException;
46
47 class NewTransactionHeaderItemHolder extends NewTransactionItemViewHolder
48         implements DatePickerFragment.DatePickedListener {
49     private final NewTransactionHeaderRowBinding b;
50     private boolean ignoreFocusChanges = false;
51     private String decimalSeparator;
52     private boolean inUpdate = false;
53     private boolean syncingData = false;
54     NewTransactionHeaderItemHolder(@NonNull NewTransactionHeaderRowBinding b,
55                                    NewTransactionItemsAdapter adapter) {
56         super(b.getRoot());
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 = getBindingAdapterPosition();
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.onDescriptionSelected(
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 != getBindingAdapterPosition()))
157                 return;
158
159             final NewTransactionModel.Item item = getItem();
160             if (item == null)
161                 return;
162
163             NewTransactionModel.Item head = item.toTransactionHead();
164             // bad idea - double pop-up, and not really necessary.
165             // the user can tap the input to get the calendar
166             //if (!tvDate.hasFocus()) tvDate.requestFocus();
167             switch (focusInfo.element) {
168                 case TransactionComment:
169                     b.transactionComment.setVisibility(View.VISIBLE);
170                     b.transactionComment.requestFocus();
171                     break;
172                 case Description:
173                     boolean focused = b.newTransactionDescription.requestFocus();
174 //                            tvDescription.dismissDropDown();
175                     if (focused)
176                         Misc.showSoftKeyboard((NewTransactionActivity) b.getRoot()
177                                                                         .getContext());
178                     break;
179             }
180         }
181         finally {
182             ignoreFocusChanges = false;
183         }
184     }
185     private void monitorComment(EditText editText) {
186         editText.addTextChangedListener(new TextWatcher() {
187             @Override
188             public void beforeTextChanged(CharSequence s, int start, int count, int after) {
189             }
190             @Override
191             public void onTextChanged(CharSequence s, int start, int before, int count) {
192             }
193             @Override
194             public void afterTextChanged(Editable s) {
195 //                debug("input", "text changed");
196                 if (inUpdate)
197                     return;
198
199                 Logger.debug("textWatcher", "calling syncData()");
200                 syncData();
201                 Logger.debug("textWatcher",
202                         "syncData() returned, checking if transaction is submittable");
203                 styleComment(editText, s.toString());
204                 Logger.debug("textWatcher", "done");
205             }
206         });
207     }
208     private void commentFocusChanged(TextView textView, boolean hasFocus) {
209         @ColorInt int textColor;
210         textColor = b.dummyText.getTextColors()
211                                .getDefaultColor();
212         if (hasFocus) {
213             textView.setTypeface(null, Typeface.NORMAL);
214             textView.setHint(R.string.transaction_account_comment_hint);
215         }
216         else {
217             int alpha = (textColor >> 24 & 0xff);
218             alpha = 3 * alpha / 4;
219             textColor = (alpha << 24) | (0x00ffffff & textColor);
220             textView.setTypeface(null, Typeface.ITALIC);
221             textView.setHint("");
222             if (TextUtils.isEmpty(textView.getText())) {
223                 textView.setVisibility(View.INVISIBLE);
224             }
225         }
226         textView.setTextColor(textColor);
227
228     }
229     private void setEditable(Boolean editable) {
230         b.newTransactionDate.setEnabled(editable);
231         b.newTransactionDescription.setEnabled(editable);
232     }
233     private void beginUpdates() {
234         if (inUpdate)
235             throw new RuntimeException("Already in update mode");
236         inUpdate = true;
237     }
238     private void endUpdates() {
239         if (!inUpdate)
240             throw new RuntimeException("Not in update mode");
241         inUpdate = false;
242     }
243     /**
244      * syncData()
245      * <p>
246      * Stores the data from the UI elements into the model item
247      * Returns true if there were changes made that suggest transaction has to be
248      * checked for being submittable
249      */
250     private boolean syncData() {
251         if (syncingData) {
252             Logger.debug("new-trans", "skipping syncData() loop");
253             return false;
254         }
255
256         if (getBindingAdapterPosition() == RecyclerView.NO_POSITION) {
257             // probably the row was swiped out
258             Logger.debug("new-trans", "Ignoring request to syncData(): adapter position negative");
259             return false;
260         }
261
262
263         boolean significantChange = false;
264
265         syncingData = true;
266         try {
267             final NewTransactionModel.Item item = getItem();
268             if (item == null)
269                 return false;
270
271             NewTransactionModel.TransactionHead head = item.toTransactionHead();
272
273             head.setDate(String.valueOf(b.newTransactionDate.getText()));
274
275             // transaction description is required
276             if (TextUtils.isEmpty(head.getDescription()) !=
277                 TextUtils.isEmpty(b.newTransactionDescription.getText()))
278                 significantChange = true;
279
280             head.setDescription(String.valueOf(b.newTransactionDescription.getText()));
281             head.setComment(String.valueOf(b.transactionComment.getText()));
282
283             return significantChange;
284         }
285         catch (ParseException e) {
286             throw new RuntimeException("Should not happen", e);
287         }
288         finally {
289             syncingData = false;
290         }
291     }
292     private void pickTransactionDate() {
293         DatePickerFragment picker = new DatePickerFragment();
294         picker.setFutureDates(FutureDates.valueOf(mProfile.getFutureDates()));
295         picker.setOnDatePickedListener(this);
296         picker.setCurrentDateFromText(b.newTransactionDate.getText());
297         picker.show(((NewTransactionActivity) b.getRoot()
298                                                .getContext()).getSupportFragmentManager(), null);
299     }
300     /**
301      * bind
302      *
303      * @param item updates the UI elements with the data from the model item
304      */
305     @SuppressLint("DefaultLocale")
306     public void bind(@NonNull NewTransactionModel.Item item) {
307         beginUpdates();
308         try {
309             syncingData = true;
310             try {
311                 NewTransactionModel.TransactionHead head = item.toTransactionHead();
312                 b.newTransactionDate.setText(head.getFormattedDate());
313
314                 // avoid triggering completion pop-up
315                 ListAdapter a = b.newTransactionDescription.getAdapter();
316                 try {
317                     b.newTransactionDescription.setAdapter(null);
318                     b.newTransactionDescription.setText(head.getDescription());
319                 }
320                 finally {
321                     b.newTransactionDescription.setAdapter(
322                             (TransactionDescriptionAutocompleteAdapter) a);
323                 }
324
325                 final String comment = head.getComment();
326                 b.transactionComment.setText(comment);
327                 styleComment(b.transactionComment, comment); // would hide or make it visible
328
329                 setEditable(true);
330
331                 NewTransactionItemsAdapter adapter =
332                         (NewTransactionItemsAdapter) getBindingAdapter();
333                 if (adapter != null)
334                     applyFocus(adapter.model.getFocusInfo()
335                                             .getValue());
336             }
337             finally {
338                 syncingData = false;
339             }
340         }
341         finally {
342             endUpdates();
343         }
344     }
345     private void styleComment(EditText editText, String comment) {
346         final View focusedView = editText.findFocus();
347         editText.setTypeface(null, (focusedView == editText) ? Typeface.NORMAL : Typeface.ITALIC);
348         editText.setVisibility(
349                 ((focusedView != editText) && TextUtils.isEmpty(comment)) ? View.INVISIBLE
350                                                                           : View.VISIBLE);
351     }
352     @Override
353     public void onDatePicked(int year, int month, int day) {
354         final NewTransactionModel.Item item = getItem();
355         if (item == null)
356             return;
357
358         final NewTransactionModel.TransactionHead head = item.toTransactionHead();
359         head.setDate(new SimpleDate(year, month + 1, day));
360         b.newTransactionDate.setText(head.getFormattedDate());
361
362         boolean focused = b.newTransactionDescription.requestFocus();
363         if (focused)
364             Misc.showSoftKeyboard((NewTransactionActivity) b.getRoot()
365                                                             .getContext());
366
367     }
368 }