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