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