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