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