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