]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateDetailsAdapter.java
bump androidx.constraintlayout library version
[mobile-ledger.git] / app / src / main / java / net / ktnx / mobileledger / ui / templates / TemplateDetailsAdapter.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.templates;
19
20 import android.annotation.SuppressLint;
21 import android.content.Context;
22 import android.content.res.Resources;
23 import android.text.Editable;
24 import android.text.TextWatcher;
25 import android.view.LayoutInflater;
26 import android.view.MotionEvent;
27 import android.view.View;
28 import android.view.ViewGroup;
29 import android.widget.TextView;
30
31 import androidx.annotation.NonNull;
32 import androidx.appcompat.app.AppCompatActivity;
33 import androidx.lifecycle.LifecycleOwner;
34 import androidx.recyclerview.widget.AsyncListDiffer;
35 import androidx.recyclerview.widget.DiffUtil;
36 import androidx.recyclerview.widget.ItemTouchHelper;
37 import androidx.recyclerview.widget.RecyclerView;
38
39 import net.ktnx.mobileledger.BuildConfig;
40 import net.ktnx.mobileledger.R;
41 import net.ktnx.mobileledger.databinding.TemplateDetailsAccountBinding;
42 import net.ktnx.mobileledger.databinding.TemplateDetailsHeaderBinding;
43 import net.ktnx.mobileledger.db.AccountAutocompleteAdapter;
44 import net.ktnx.mobileledger.db.DB;
45 import net.ktnx.mobileledger.model.Data;
46 import net.ktnx.mobileledger.model.TemplateDetailsItem;
47 import net.ktnx.mobileledger.ui.CurrencySelectorFragment;
48 import net.ktnx.mobileledger.ui.HelpDialog;
49 import net.ktnx.mobileledger.ui.QR;
50 import net.ktnx.mobileledger.ui.TemplateDetailSourceSelectorFragment;
51 import net.ktnx.mobileledger.utils.Logger;
52 import net.ktnx.mobileledger.utils.Misc;
53
54 import org.jetbrains.annotations.NotNull;
55
56 import java.text.ParseException;
57 import java.util.List;
58 import java.util.Locale;
59 import java.util.regex.Matcher;
60 import java.util.regex.Pattern;
61
62 class TemplateDetailsAdapter extends RecyclerView.Adapter<TemplateDetailsAdapter.ViewHolder> {
63     private static final String D_TEMPLATE_UI = "template-ui";
64     private final AsyncListDiffer<TemplateDetailsItem> differ;
65     private final TemplateDetailsViewModel mModel;
66     private final ItemTouchHelper itemTouchHelper;
67     public TemplateDetailsAdapter(TemplateDetailsViewModel model) {
68         super();
69         mModel = model;
70         setHasStableIds(true);
71         differ = new AsyncListDiffer<>(this, new DiffUtil.ItemCallback<TemplateDetailsItem>() {
72             @Override
73             public boolean areItemsTheSame(@NonNull TemplateDetailsItem oldItem,
74                                            @NonNull TemplateDetailsItem newItem) {
75                 if (oldItem.getType() != newItem.getType())
76                     return false;
77                 if (oldItem.getType()
78                            .equals(TemplateDetailsItem.Type.HEADER))
79                     return true;    // only one header item, ever
80                 // the rest is comparing two account row items
81                 return oldItem.asAccountRowItem()
82                               .getId() == newItem.asAccountRowItem()
83                                                  .getId();
84             }
85             @Override
86             public boolean areContentsTheSame(@NonNull TemplateDetailsItem oldItem,
87                                               @NonNull TemplateDetailsItem newItem) {
88                 if (oldItem.getType()
89                            .equals(TemplateDetailsItem.Type.HEADER))
90                 {
91                     TemplateDetailsItem.Header oldHeader = oldItem.asHeaderItem();
92                     TemplateDetailsItem.Header newHeader = newItem.asHeaderItem();
93
94                     return oldHeader.equalContents(newHeader);
95                 }
96                 else {
97                     TemplateDetailsItem.AccountRow oldAcc = oldItem.asAccountRowItem();
98                     TemplateDetailsItem.AccountRow newAcc = newItem.asAccountRowItem();
99
100                     return oldAcc.equalContents(newAcc);
101                 }
102             }
103         });
104         itemTouchHelper = new ItemTouchHelper(new ItemTouchHelper.Callback() {
105             @Override
106             public float getMoveThreshold(@NonNull RecyclerView.ViewHolder viewHolder) {
107                 return 0.5f;
108             }
109             @Override
110             public boolean isLongPressDragEnabled() {
111                 return false;
112             }
113             @Override
114             public RecyclerView.ViewHolder chooseDropTarget(
115                     @NonNull RecyclerView.ViewHolder selected,
116                     @NonNull List<RecyclerView.ViewHolder> dropTargets, int curX, int curY) {
117                 RecyclerView.ViewHolder best = null;
118                 int bestDistance = 0;
119                 for (RecyclerView.ViewHolder v : dropTargets) {
120                     if (v == selected)
121                         continue;
122
123                     final int viewTop = v.itemView.getTop();
124                     int distance = Math.abs(viewTop - curY);
125                     if (best == null) {
126                         best = v;
127                         bestDistance = distance;
128                     }
129                     else {
130                         if (distance < bestDistance) {
131                             bestDistance = distance;
132                             best = v;
133                         }
134                     }
135                 }
136
137                 Logger.debug("dnd", "Best target is " + best);
138                 return best;
139             }
140             @Override
141             public boolean canDropOver(@NonNull RecyclerView recyclerView,
142                                        @NonNull RecyclerView.ViewHolder current,
143                                        @NonNull RecyclerView.ViewHolder target) {
144                 final int adapterPosition = target.getBindingAdapterPosition();
145
146                 // first item is immovable
147                 if (adapterPosition == 0)
148                     return false;
149
150                 return super.canDropOver(recyclerView, current, target);
151             }
152             @Override
153             public int getMovementFlags(@NonNull RecyclerView recyclerView,
154                                         @NonNull RecyclerView.ViewHolder viewHolder) {
155                 int flags = 0;
156                 // the top item (transaction params) is always there
157                 final int adapterPosition = viewHolder.getBindingAdapterPosition();
158                 if (adapterPosition > 0)
159                     flags |= makeFlag(ItemTouchHelper.ACTION_STATE_DRAG,
160                             ItemTouchHelper.UP | ItemTouchHelper.DOWN) |
161                              makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE,
162                                      ItemTouchHelper.START | ItemTouchHelper.END);
163
164                 return flags;
165             }
166             @Override
167             public boolean onMove(@NonNull RecyclerView recyclerView,
168                                   @NonNull RecyclerView.ViewHolder viewHolder,
169                                   @NonNull RecyclerView.ViewHolder target) {
170
171                 final int fromPosition = viewHolder.getBindingAdapterPosition();
172                 final int toPosition = target.getBindingAdapterPosition();
173                 if (fromPosition == toPosition) {
174                     Logger.debug("drag", String.format(Locale.US,
175                             "Ignoring request to move an account from position %d to %d",
176                             fromPosition, toPosition));
177                     return false;
178                 }
179
180                 Logger.debug("drag",
181                         String.format(Locale.US, "Moving account from %d to %d", fromPosition,
182                                 toPosition));
183                 mModel.moveItem(fromPosition, toPosition);
184
185                 return true;
186             }
187             @Override
188             public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
189                 int pos = viewHolder.getBindingAdapterPosition();
190                 mModel.removeItem(pos);
191             }
192         });
193     }
194     @Override
195     public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
196         super.onAttachedToRecyclerView(recyclerView);
197
198         itemTouchHelper.attachToRecyclerView(recyclerView);
199     }
200     @Override
201     public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
202         super.onDetachedFromRecyclerView(recyclerView);
203
204         itemTouchHelper.attachToRecyclerView(null);
205     }
206     @Override
207     public long getItemId(int position) {
208         // header item is always first and IDs id may duplicate some of the account IDs
209         if (position == 0)
210             return 0;
211         TemplateDetailsItem.AccountRow accRow = differ.getCurrentList()
212                                                       .get(position)
213                                                       .asAccountRowItem();
214         return accRow.getId();
215     }
216     @Override
217     public int getItemViewType(int position) {
218
219         return differ.getCurrentList()
220                      .get(position)
221                      .getType()
222                      .toInt();
223     }
224     @NonNull
225     @Override
226     public TemplateDetailsAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
227                                                                 int viewType) {
228         final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
229         switch (viewType) {
230             case TemplateDetailsItem.TYPE.header:
231                 return new Header(TemplateDetailsHeaderBinding.inflate(inflater, parent, false));
232             case TemplateDetailsItem.TYPE.accountItem:
233                 return new AccountRow(
234                         TemplateDetailsAccountBinding.inflate(inflater, parent, false));
235             default:
236                 throw new IllegalStateException("Unsupported view type " + viewType);
237         }
238     }
239     @Override
240     public void onBindViewHolder(@NonNull TemplateDetailsAdapter.ViewHolder holder, int position) {
241         TemplateDetailsItem item = differ.getCurrentList()
242                                          .get(position);
243         holder.bind(item);
244     }
245     @Override
246     public int getItemCount() {
247         return differ.getCurrentList()
248                      .size();
249     }
250     public void setItems(List<TemplateDetailsItem> items) {
251         if (BuildConfig.DEBUG) {
252             Logger.debug("tmpl", "Got new list");
253             for (int i = 1; i < items.size(); i++) {
254                 final TemplateDetailsItem item = items.get(i);
255                 Logger.debug("tmpl",
256                         String.format(Locale.US, "  %d: id %d, pos %d", i, item.getId(),
257                                 item.getPosition()));
258             }
259         }
260         differ.submitList(items);
261     }
262     public String getMatchGroupText(int groupNumber) {
263         TemplateDetailsItem.Header header = getHeader();
264         Pattern p = header.getCompiledPattern();
265         if (p == null)
266             return null;
267
268         final String testText = Misc.nullIsEmpty(header.getTestText());
269         Matcher m = p.matcher(testText);
270         if (m.matches() && m.groupCount() >= groupNumber)
271             return m.group(groupNumber);
272         else
273             return null;
274     }
275     protected TemplateDetailsItem.Header getHeader() {
276         return differ.getCurrentList()
277                      .get(0)
278                      .asHeaderItem();
279     }
280
281     private enum HeaderDetail {DESCRIPTION, COMMENT, DATE_YEAR, DATE_MONTH, DATE_DAY}
282
283     private enum AccDetail {ACCOUNT, COMMENT, AMOUNT, CURRENCY}
284
285     public abstract static class ViewHolder extends RecyclerView.ViewHolder {
286         ViewHolder(@NonNull View itemView) {
287             super(itemView);
288         }
289         abstract void bind(TemplateDetailsItem item);
290     }
291
292     private abstract static class BaseItem extends ViewHolder {
293         boolean updatePropagationDisabled = false;
294         BaseItem(@NonNull View itemView) {
295             super(itemView);
296         }
297         void disableUpdatePropagation() {
298             updatePropagationDisabled = true;
299         }
300         void enableUpdatePropagation() {
301             updatePropagationDisabled = false;
302         }
303     }
304
305     public class Header extends BaseItem {
306         private final TemplateDetailsHeaderBinding b;
307         boolean updatePropagationDisabled = false;
308         public Header(@NonNull TemplateDetailsHeaderBinding binding) {
309             super(binding.getRoot());
310             b = binding;
311
312             TextWatcher templateNameWatcher = new TextWatcher() {
313                 @Override
314                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
315                 @Override
316                 public void onTextChanged(CharSequence s, int start, int before, int count) {}
317                 @Override
318                 public void afterTextChanged(Editable s) {
319                     if (updatePropagationDisabled)
320                         return;
321
322                     final TemplateDetailsItem.Header header = getItem();
323                     Logger.debug(D_TEMPLATE_UI,
324                             "Storing changed template name " + s + "; header=" + header);
325                     header.setName(String.valueOf(s));
326                 }
327             };
328             b.templateName.addTextChangedListener(templateNameWatcher);
329
330             TextWatcher patternWatcher = new TextWatcher() {
331                 @Override
332                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
333                 @Override
334                 public void onTextChanged(CharSequence s, int start, int before, int count) {}
335                 @Override
336                 public void afterTextChanged(Editable s) {
337                     if (updatePropagationDisabled)
338                         return;
339
340                     final TemplateDetailsItem.Header header = getItem();
341                     Logger.debug(D_TEMPLATE_UI,
342                             "Storing changed pattern " + s + "; header=" + header);
343                     header.setPattern(String.valueOf(s));
344
345                     checkPatternError(header);
346                 }
347             };
348             b.pattern.addTextChangedListener(patternWatcher);
349
350             TextWatcher testTextWatcher = new TextWatcher() {
351                 @Override
352                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
353                 @Override
354                 public void onTextChanged(CharSequence s, int start, int before, int count) {}
355                 @Override
356                 public void afterTextChanged(Editable s) {
357                     if (updatePropagationDisabled)
358                         return;
359
360                     final TemplateDetailsItem.Header header = getItem();
361                     Logger.debug(D_TEMPLATE_UI,
362                             "Storing changed test text " + s + "; header=" + header);
363                     header.setTestText(String.valueOf(s));
364
365                     checkPatternError(header);
366                 }
367             };
368             b.testText.addTextChangedListener(testTextWatcher);
369
370             TextWatcher transactionDescriptionWatcher = new TextWatcher() {
371                 @Override
372                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
373                 }
374                 @Override
375                 public void onTextChanged(CharSequence s, int start, int before, int count) {
376                 }
377                 @Override
378                 public void afterTextChanged(Editable s) {
379                     if (updatePropagationDisabled)
380                         return;
381
382                     final TemplateDetailsItem.Header header = getItem();
383                     Logger.debug(D_TEMPLATE_UI,
384                             "Storing changed transaction description " + s + "; header=" + header);
385                     header.setTransactionDescription(String.valueOf(s));
386                 }
387             };
388             b.transactionDescription.addTextChangedListener(transactionDescriptionWatcher);
389             TextWatcher transactionCommentWatcher = new TextWatcher() {
390                 @Override
391                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
392                 }
393                 @Override
394                 public void onTextChanged(CharSequence s, int start, int before, int count) {
395                 }
396                 @Override
397                 public void afterTextChanged(Editable s) {
398                     if (updatePropagationDisabled)
399                         return;
400
401                     final TemplateDetailsItem.Header header = getItem();
402                     Logger.debug(D_TEMPLATE_UI,
403                             "Storing changed transaction description " + s + "; header=" + header);
404                     header.setTransactionComment(String.valueOf(s));
405                 }
406             };
407             b.transactionComment.addTextChangedListener(transactionCommentWatcher);
408
409             b.templateIsFallbackSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
410                 if (updatePropagationDisabled)
411                     return;
412
413                 getItem().setFallback(isChecked);
414                 b.templateIsFallbackText.setText(isChecked ? R.string.template_is_fallback_yes
415                                                            : R.string.template_is_fallback_no);
416             });
417             final View.OnClickListener fallbackLabelClickListener =
418                     (view) -> b.templateIsFallbackSwitch.toggle();
419             b.templateIsFallbackLabel.setOnClickListener(fallbackLabelClickListener);
420             b.templateIsFallbackText.setOnClickListener(fallbackLabelClickListener);
421             b.templateParamsHelpButton.setOnClickListener(v -> HelpDialog.show(b.getRoot()
422                                                                                 .getContext(),
423                     R.string.template_details_template_params_label, R.array.template_params_help));
424         }
425         @NotNull
426         private TemplateDetailsItem.Header getItem() {
427             int pos = getBindingAdapterPosition();
428             return differ.getCurrentList()
429                          .get(pos)
430                          .asHeaderItem();
431         }
432         private void selectHeaderDetailSource(View v, HeaderDetail detail) {
433             TemplateDetailsItem.Header header = getItem();
434             Logger.debug(D_TEMPLATE_UI, "header is " + header);
435             TemplateDetailSourceSelectorFragment sel =
436                     TemplateDetailSourceSelectorFragment.newInstance(1, header.getPattern(),
437                             header.getTestText());
438             sel.setOnSourceSelectedListener((literal, group) -> {
439                 if (literal) {
440                     switch (detail) {
441                         case DESCRIPTION:
442                             header.switchToLiteralTransactionDescription();
443                             break;
444                         case COMMENT:
445                             header.switchToLiteralTransactionComment();
446                             break;
447                         case DATE_YEAR:
448                             header.switchToLiteralDateYear();
449                             break;
450                         case DATE_MONTH:
451                             header.switchToLiteralDateMonth();
452                             break;
453                         case DATE_DAY:
454                             header.switchToLiteralDateDay();
455                             break;
456                         default:
457                             throw new IllegalStateException("Unexpected detail " + detail);
458                     }
459                 }
460                 else {
461                     switch (detail) {
462                         case DESCRIPTION:
463                             header.setTransactionDescriptionMatchGroup(group);
464                             break;
465                         case COMMENT:
466                             header.setTransactionCommentMatchGroup(group);
467                             break;
468                         case DATE_YEAR:
469                             header.setDateYearMatchGroup(group);
470                             break;
471                         case DATE_MONTH:
472                             header.setDateMonthMatchGroup(group);
473                             break;
474                         case DATE_DAY:
475                             header.setDateDayMatchGroup(group);
476                             break;
477                         default:
478                             throw new IllegalStateException("Unexpected detail " + detail);
479                     }
480                 }
481
482                 notifyItemChanged(getBindingAdapterPosition());
483             });
484             final AppCompatActivity activity = (AppCompatActivity) v.getContext();
485             sel.show(activity.getSupportFragmentManager(), "template-details-source-selector");
486         }
487         @Override
488         void bind(TemplateDetailsItem item) {
489             TemplateDetailsItem.Header header = item.asHeaderItem();
490             Logger.debug(D_TEMPLATE_UI, "Binding to header " + header);
491
492             disableUpdatePropagation();
493             try {
494                 String groupNoText = b.getRoot()
495                                       .getResources()
496                                       .getString(R.string.template_item_match_group_source);
497
498                 b.templateName.setText(header.getName());
499                 b.pattern.setText(header.getPattern());
500                 b.testText.setText(header.getTestText());
501
502                 if (header.hasLiteralDateYear()) {
503                     b.yearSource.setText(R.string.template_details_source_literal);
504                     final Integer dateYear = header.getDateYear();
505                     b.templateDetailsDateYear.setText(
506                             (dateYear == null) ? null : String.valueOf(dateYear));
507                     b.yearLayout.setVisibility(View.VISIBLE);
508                 }
509                 else {
510                     b.yearLayout.setVisibility(View.GONE);
511                     b.yearSource.setText(
512                             String.format(Locale.US, groupNoText, header.getDateYearMatchGroup(),
513                                     getMatchGroupText(header.getDateYearMatchGroup())));
514                 }
515                 b.yearSourceLabel.setOnClickListener(
516                         v -> selectHeaderDetailSource(v, HeaderDetail.DATE_YEAR));
517                 b.yearSource.setOnClickListener(
518                         v -> selectHeaderDetailSource(v, HeaderDetail.DATE_YEAR));
519
520                 if (header.hasLiteralDateMonth()) {
521                     b.monthSource.setText(R.string.template_details_source_literal);
522                     final Integer dateMonth = header.getDateMonth();
523                     b.templateDetailsDateMonth.setText(
524                             (dateMonth == null) ? null : String.valueOf(dateMonth));
525                     b.monthLayout.setVisibility(View.VISIBLE);
526                 }
527                 else {
528                     b.monthLayout.setVisibility(View.GONE);
529                     b.monthSource.setText(
530                             String.format(Locale.US, groupNoText, header.getDateMonthMatchGroup(),
531                                     getMatchGroupText(header.getDateMonthMatchGroup())));
532                 }
533                 b.monthSourceLabel.setOnClickListener(
534                         v -> selectHeaderDetailSource(v, HeaderDetail.DATE_MONTH));
535                 b.monthSource.setOnClickListener(
536                         v -> selectHeaderDetailSource(v, HeaderDetail.DATE_MONTH));
537
538                 if (header.hasLiteralDateDay()) {
539                     b.daySource.setText(R.string.template_details_source_literal);
540                     final Integer dateDay = header.getDateDay();
541                     b.templateDetailsDateDay.setText(
542                             (dateDay == null) ? null : String.valueOf(dateDay));
543                     b.dayLayout.setVisibility(View.VISIBLE);
544                 }
545                 else {
546                     b.dayLayout.setVisibility(View.GONE);
547                     b.daySource.setText(
548                             String.format(Locale.US, groupNoText, header.getDateDayMatchGroup(),
549                                     getMatchGroupText(header.getDateDayMatchGroup())));
550                 }
551                 b.daySourceLabel.setOnClickListener(
552                         v -> selectHeaderDetailSource(v, HeaderDetail.DATE_DAY));
553                 b.daySource.setOnClickListener(
554                         v -> selectHeaderDetailSource(v, HeaderDetail.DATE_DAY));
555
556                 if (header.hasLiteralTransactionDescription()) {
557                     b.templateTransactionDescriptionSource.setText(
558                             R.string.template_details_source_literal);
559                     b.transactionDescription.setText(header.getTransactionDescription());
560                     b.transactionDescriptionLayout.setVisibility(View.VISIBLE);
561                 }
562                 else {
563                     b.transactionDescriptionLayout.setVisibility(View.GONE);
564                     b.templateTransactionDescriptionSource.setText(
565                             String.format(Locale.US, groupNoText,
566                                     header.getTransactionDescriptionMatchGroup(), getMatchGroupText(
567                                             header.getTransactionDescriptionMatchGroup())));
568
569                 }
570                 b.templateTransactionDescriptionSourceLabel.setOnClickListener(
571                         v -> selectHeaderDetailSource(v, HeaderDetail.DESCRIPTION));
572                 b.templateTransactionDescriptionSource.setOnClickListener(
573                         v -> selectHeaderDetailSource(v, HeaderDetail.DESCRIPTION));
574
575                 if (header.hasLiteralTransactionComment()) {
576                     b.templateTransactionCommentSource.setText(
577                             R.string.template_details_source_literal);
578                     b.transactionComment.setText(header.getTransactionComment());
579                     b.transactionCommentLayout.setVisibility(View.VISIBLE);
580                 }
581                 else {
582                     b.transactionCommentLayout.setVisibility(View.GONE);
583                     b.templateTransactionCommentSource.setText(String.format(Locale.US, groupNoText,
584                             header.getTransactionCommentMatchGroup(),
585                             getMatchGroupText(header.getTransactionCommentMatchGroup())));
586
587                 }
588                 b.templateTransactionCommentSourceLabel.setOnClickListener(
589                         v -> selectHeaderDetailSource(v, HeaderDetail.COMMENT));
590                 b.templateTransactionCommentSource.setOnClickListener(
591                         v -> selectHeaderDetailSource(v, HeaderDetail.COMMENT));
592
593                 b.templateDetailsHeadScanQrButton.setOnClickListener(this::scanTestQR);
594
595                 b.templateIsFallbackSwitch.setChecked(header.isFallback());
596                 b.templateIsFallbackText.setText(
597                         header.isFallback() ? R.string.template_is_fallback_yes
598                                             : R.string.template_is_fallback_no);
599
600                 checkPatternError(header);
601             }
602             finally {
603                 enableUpdatePropagation();
604             }
605         }
606         private void checkPatternError(TemplateDetailsItem.Header item) {
607             if (item.getPatternError() != null) {
608                 b.patternLayout.setError(item.getPatternError());
609                 b.patternHintTitle.setVisibility(View.GONE);
610                 b.patternHintText.setVisibility(View.GONE);
611             }
612             else {
613                 b.patternLayout.setError(null);
614                 if (item.testMatch() != null) {
615                     b.patternHintText.setText(item.testMatch());
616                     b.patternHintTitle.setVisibility(View.VISIBLE);
617                     b.patternHintText.setVisibility(View.VISIBLE);
618                 }
619                 else {
620                     b.patternLayout.setError(null);
621                     b.patternHintTitle.setVisibility(View.GONE);
622                     b.patternHintText.setVisibility(View.GONE);
623                 }
624             }
625
626         }
627         private void scanTestQR(View view) {
628             Context ctx = view.getContext();
629             if (ctx instanceof QR.QRScanTrigger)
630                 ((QR.QRScanTrigger) ctx).triggerQRScan();
631         }
632     }
633
634     public class AccountRow extends BaseItem {
635         private final TemplateDetailsAccountBinding b;
636         public AccountRow(@NonNull TemplateDetailsAccountBinding binding) {
637             super(binding.getRoot());
638             b = binding;
639
640             TextWatcher accountNameWatcher = new TextWatcher() {
641                 @Override
642                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
643                 @Override
644                 public void onTextChanged(CharSequence s, int start, int before, int count) {}
645                 @Override
646                 public void afterTextChanged(Editable s) {
647                     if (updatePropagationDisabled)
648                         return;
649
650                     TemplateDetailsItem.AccountRow accRow = getItem();
651                     Logger.debug(D_TEMPLATE_UI,
652                             "Storing changed account name " + s + "; accRow=" + accRow);
653                     accRow.setAccountName(String.valueOf(s));
654
655                     mModel.applyList(null);
656                 }
657             };
658             b.templateDetailsAccountName.addTextChangedListener(accountNameWatcher);
659             b.templateDetailsAccountName.setAdapter(new AccountAutocompleteAdapter(b.getRoot()
660                                                                                     .getContext()));
661             b.templateDetailsAccountName.setOnItemClickListener(
662                     (parent, view, position, id) -> b.templateDetailsAccountName.setText(
663                             ((TextView) view).getText()));
664             TextWatcher accountCommentWatcher = new TextWatcher() {
665                 @Override
666                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
667                 @Override
668                 public void onTextChanged(CharSequence s, int start, int before, int count) {}
669                 @Override
670                 public void afterTextChanged(Editable s) {
671                     if (updatePropagationDisabled)
672                         return;
673
674                     TemplateDetailsItem.AccountRow accRow = getItem();
675                     Logger.debug(D_TEMPLATE_UI,
676                             "Storing changed account comment " + s + "; accRow=" + accRow);
677                     accRow.setAccountComment(String.valueOf(s));
678
679                     mModel.applyList(null);
680                 }
681             };
682             b.templateDetailsAccountComment.addTextChangedListener(accountCommentWatcher);
683
684             b.templateDetailsAccountAmount.addTextChangedListener(new TextWatcher() {
685                 @Override
686                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
687                 }
688                 @Override
689                 public void onTextChanged(CharSequence s, int start, int before, int count) {
690                 }
691                 @Override
692                 public void afterTextChanged(Editable s) {
693                     if (updatePropagationDisabled)
694                         return;
695
696                     TemplateDetailsItem.AccountRow accRow = getItem();
697
698                     String str = String.valueOf(s);
699                     if (Misc.emptyIsNull(str) == null) {
700                         accRow.setAmount(null);
701                     }
702                     else {
703                         try {
704                             final float amount = Data.parseNumber(str);
705                             accRow.setAmount(amount);
706                             b.templateDetailsAccountAmountLayout.setError(null);
707
708                             Logger.debug(D_TEMPLATE_UI, String.format(Locale.US,
709                                     "Storing changed account amount %s [%4.2f]; accRow=%s", s,
710                                     amount, accRow));
711                         }
712                         catch (NumberFormatException | ParseException e) {
713                             b.templateDetailsAccountAmountLayout.setError("!");
714                         }
715                     }
716
717                     mModel.applyList(null);
718                 }
719             });
720             b.templateDetailsAccountAmount.setOnFocusChangeListener((v, hasFocus) -> {
721                 if (hasFocus)
722                     return;
723
724                 TemplateDetailsItem.AccountRow accRow = getItem();
725                 if (!accRow.hasLiteralAmount())
726                     return;
727                 Float amt = accRow.getAmount();
728                 if (amt == null)
729                     return;
730
731                 b.templateDetailsAccountAmount.setText(Data.formatNumber(amt));
732             });
733
734             b.negateAmountSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
735                 if (updatePropagationDisabled)
736                     return;
737
738                 getItem().setNegateAmount(isChecked);
739                 b.templateDetailsNegateAmountText.setText(
740                         isChecked ? R.string.template_account_change_amount_sign
741                                   : R.string.template_account_keep_amount_sign);
742             });
743             final View.OnClickListener negLabelClickListener =
744                     (view) -> b.negateAmountSwitch.toggle();
745             b.templateDetailsNegateAmountLabel.setOnClickListener(negLabelClickListener);
746             b.templateDetailsNegateAmountText.setOnClickListener(negLabelClickListener);
747             manageAccountLabelDrag();
748         }
749         @SuppressLint("ClickableViewAccessibility")
750         public void manageAccountLabelDrag() {
751             b.patternAccountLabel.setOnTouchListener((v, event) -> {
752                 if (event.getAction() == MotionEvent.ACTION_DOWN) {
753                     itemTouchHelper.startDrag(this);
754                 }
755                 return false;
756             });
757         }
758         @Override
759         void bind(TemplateDetailsItem item) {
760             disableUpdatePropagation();
761             try {
762                 final Resources resources = b.getRoot()
763                                              .getResources();
764                 String groupNoText = resources.getString(R.string.template_item_match_group_source);
765
766                 Logger.debug("drag", String.format(Locale.US, "Binding account id %d, pos %d at %d",
767                         item.getId(), item.getPosition(), getBindingAdapterPosition()));
768                 TemplateDetailsItem.AccountRow accRow = item.asAccountRowItem();
769                 b.patternAccountLabel.setText(String.format(Locale.US,
770                         resources.getString(R.string.template_details_account_row_label),
771                         accRow.getPosition()));
772                 if (accRow.hasLiteralAccountName()) {
773                     b.templateDetailsAccountNameLayout.setVisibility(View.VISIBLE);
774                     b.templateDetailsAccountName.setText(accRow.getAccountName());
775                     b.templateDetailsAccountNameSource.setText(
776                             R.string.template_details_source_literal);
777                 }
778                 else {
779                     b.templateDetailsAccountNameLayout.setVisibility(View.GONE);
780                     b.templateDetailsAccountNameSource.setText(
781                             String.format(Locale.US, groupNoText, accRow.getAccountNameMatchGroup(),
782                                     getMatchGroupText(accRow.getAccountNameMatchGroup())));
783                 }
784
785                 if (accRow.hasLiteralAccountComment()) {
786                     b.templateDetailsAccountCommentLayout.setVisibility(View.VISIBLE);
787                     b.templateDetailsAccountComment.setText(accRow.getAccountComment());
788                     b.templateDetailsAccountCommentSource.setText(
789                             R.string.template_details_source_literal);
790                 }
791                 else {
792                     b.templateDetailsAccountCommentLayout.setVisibility(View.GONE);
793                     b.templateDetailsAccountCommentSource.setText(
794                             String.format(Locale.US, groupNoText,
795                                     accRow.getAccountCommentMatchGroup(),
796                                     getMatchGroupText(accRow.getAccountCommentMatchGroup())));
797                 }
798
799                 if (accRow.hasLiteralAmount()) {
800                     b.templateDetailsAccountAmountSource.setText(
801                             R.string.template_details_source_literal);
802                     b.templateDetailsAccountAmount.setVisibility(View.VISIBLE);
803                     Float amt = accRow.getAmount();
804                     b.templateDetailsAccountAmount.setText((amt == null) ? null : String.format(
805                             Data.locale.getValue(), "%,4.2f", (accRow.getAmount())));
806                     b.negateAmountSwitch.setVisibility(View.GONE);
807                     b.templateDetailsNegateAmountLabel.setVisibility(View.GONE);
808                     b.templateDetailsNegateAmountText.setVisibility(View.GONE);
809                 }
810                 else {
811                     b.templateDetailsAccountAmountSource.setText(
812                             String.format(Locale.US, groupNoText, accRow.getAmountMatchGroup(),
813                                     getMatchGroupText(accRow.getAmountMatchGroup())));
814                     b.templateDetailsAccountAmountLayout.setVisibility(View.GONE);
815                     b.negateAmountSwitch.setVisibility(View.VISIBLE);
816                     b.negateAmountSwitch.setChecked(accRow.isNegateAmount());
817                     b.templateDetailsNegateAmountText.setText(
818                             accRow.isNegateAmount() ? R.string.template_account_change_amount_sign
819                                                     : R.string.template_account_keep_amount_sign);
820                     b.templateDetailsNegateAmountLabel.setVisibility(View.VISIBLE);
821                     b.templateDetailsNegateAmountText.setVisibility(View.VISIBLE);
822                 }
823
824                 if (accRow.hasLiteralCurrency()) {
825                     b.templateDetailsAccountCurrencySource.setText(
826                             R.string.template_details_source_literal);
827                     net.ktnx.mobileledger.db.Currency c = accRow.getCurrency();
828                     if (c == null)
829                         b.templateDetailsAccountCurrency.setText(R.string.btn_no_currency);
830                     else
831                         b.templateDetailsAccountCurrency.setText(c.getName());
832                     b.templateDetailsAccountCurrency.setVisibility(View.VISIBLE);
833                 }
834                 else {
835                     b.templateDetailsAccountCurrencySource.setText(
836                             String.format(Locale.US, groupNoText, accRow.getCurrencyMatchGroup(),
837                                     getMatchGroupText(accRow.getCurrencyMatchGroup())));
838                     b.templateDetailsAccountCurrency.setVisibility(View.GONE);
839                 }
840
841                 b.templateAccountNameSourceLabel.setOnClickListener(
842                         v -> selectAccountRowDetailSource(v, AccDetail.ACCOUNT));
843                 b.templateDetailsAccountNameSource.setOnClickListener(
844                         v -> selectAccountRowDetailSource(v, AccDetail.ACCOUNT));
845                 b.templateAccountCommentSourceLabel.setOnClickListener(
846                         v -> selectAccountRowDetailSource(v, AccDetail.COMMENT));
847                 b.templateDetailsAccountCommentSource.setOnClickListener(
848                         v -> selectAccountRowDetailSource(v, AccDetail.COMMENT));
849                 b.templateAccountAmountSourceLabel.setOnClickListener(
850                         v -> selectAccountRowDetailSource(v, AccDetail.AMOUNT));
851                 b.templateDetailsAccountAmountSource.setOnClickListener(
852                         v -> selectAccountRowDetailSource(v, AccDetail.AMOUNT));
853                 b.templateDetailsAccountCurrencySource.setOnClickListener(
854                         v -> selectAccountRowDetailSource(v, AccDetail.CURRENCY));
855                 b.templateAccountCurrencySourceLabel.setOnClickListener(
856                         v -> selectAccountRowDetailSource(v, AccDetail.CURRENCY));
857                 if (accRow.hasLiteralCurrency())
858                     b.templateDetailsAccountCurrency.setOnClickListener(v -> {
859                         CurrencySelectorFragment cpf = CurrencySelectorFragment.newInstance(
860                                 CurrencySelectorFragment.DEFAULT_COLUMN_COUNT, false);
861                         cpf.setOnCurrencySelectedListener(text -> {
862                             if (text == null) {
863                                 b.templateDetailsAccountCurrency.setText(R.string.btn_no_currency);
864                                 accRow.setCurrency(null);
865                             }
866                             else {
867                                 b.templateDetailsAccountCurrency.setText(text);
868                                 DB.get()
869                                   .getCurrencyDAO()
870                                   .getByName(text)
871                                   .observe((LifecycleOwner) b.getRoot()
872                                                              .getContext(), accRow::setCurrency);
873                             }
874                         });
875                         cpf.show(
876                                 ((TemplatesActivity) b.templateDetailsAccountCurrency.getContext()).getSupportFragmentManager(),
877                                 "currency-selector");
878                     });
879             }
880             finally {
881                 enableUpdatePropagation();
882             }
883         }
884         private @NotNull TemplateDetailsItem.AccountRow getItem() {
885             return differ.getCurrentList()
886                          .get(getBindingAdapterPosition())
887                          .asAccountRowItem();
888         }
889         private void selectAccountRowDetailSource(View v, AccDetail detail) {
890             TemplateDetailsItem.AccountRow accRow = getItem();
891             final TemplateDetailsItem.Header header = getHeader();
892             Logger.debug(D_TEMPLATE_UI, "header is " + header);
893             TemplateDetailSourceSelectorFragment sel =
894                     TemplateDetailSourceSelectorFragment.newInstance(1, header.getPattern(),
895                             header.getTestText());
896             sel.setOnSourceSelectedListener((literal, group) -> {
897                 if (literal) {
898                     switch (detail) {
899                         case ACCOUNT:
900                             accRow.switchToLiteralAccountName();
901                             break;
902                         case COMMENT:
903                             accRow.switchToLiteralAccountComment();
904                             break;
905                         case AMOUNT:
906                             accRow.switchToLiteralAmount();
907                             break;
908                         case CURRENCY:
909                             accRow.switchToLiteralCurrency();
910                             break;
911                         default:
912                             throw new IllegalStateException("Unexpected detail " + detail);
913                     }
914                 }
915                 else {
916                     switch (detail) {
917                         case ACCOUNT:
918                             accRow.setAccountNameMatchGroup(group);
919                             break;
920                         case COMMENT:
921                             accRow.setAccountCommentMatchGroup(group);
922                             break;
923                         case AMOUNT:
924                             accRow.setAmountMatchGroup(group);
925                             break;
926                         case CURRENCY:
927                             accRow.setCurrencyMatchGroup(group);
928                             break;
929                         default:
930                             throw new IllegalStateException("Unexpected detail " + detail);
931                     }
932                 }
933
934                 notifyItemChanged(getBindingAdapterPosition());
935             });
936             final AppCompatActivity activity = (AppCompatActivity) v.getContext();
937             sel.show(activity.getSupportFragmentManager(), "template-details-source-selector");
938         }
939     }
940 }