]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateDetailsAdapter.java
adapter/debug: dump new item list
[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.1f;
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         @NotNull
404         private TemplateDetailsItem.Header getItem() {
405             int pos = getAdapterPosition();
406             return differ.getCurrentList()
407                          .get(pos)
408                          .asHeaderItem();
409         }
410         private void selectHeaderDetailSource(View v, HeaderDetail detail) {
411             TemplateDetailsItem.Header header = getItem();
412             Logger.debug(D_TEMPLATE_UI, "header is " + header);
413             TemplateDetailSourceSelectorFragment sel =
414                     TemplateDetailSourceSelectorFragment.newInstance(1, header.getPattern(),
415                             header.getTestText());
416             sel.setOnSourceSelectedListener((literal, group) -> {
417                 if (literal) {
418                     switch (detail) {
419                         case DESCRIPTION:
420                             header.switchToLiteralTransactionDescription();
421                             break;
422                         case COMMENT:
423                             header.switchToLiteralTransactionComment();
424                             break;
425                         case DATE_YEAR:
426                             header.switchToLiteralDateYear();
427                             break;
428                         case DATE_MONTH:
429                             header.switchToLiteralDateMonth();
430                             break;
431                         case DATE_DAY:
432                             header.switchToLiteralDateDay();
433                             break;
434                         default:
435                             throw new IllegalStateException("Unexpected detail " + detail);
436                     }
437                 }
438                 else {
439                     switch (detail) {
440                         case DESCRIPTION:
441                             header.setTransactionDescriptionMatchGroup(group);
442                             break;
443                         case COMMENT:
444                             header.setTransactionCommentMatchGroup(group);
445                             break;
446                         case DATE_YEAR:
447                             header.setDateYearMatchGroup(group);
448                             break;
449                         case DATE_MONTH:
450                             header.setDateMonthMatchGroup(group);
451                             break;
452                         case DATE_DAY:
453                             header.setDateDayMatchGroup(group);
454                             break;
455                         default:
456                             throw new IllegalStateException("Unexpected detail " + detail);
457                     }
458                 }
459
460                 notifyItemChanged(getAdapterPosition());
461             });
462             final AppCompatActivity activity = (AppCompatActivity) v.getContext();
463             sel.show(activity.getSupportFragmentManager(), "template-details-source-selector");
464         }
465         @Override
466         void bind(TemplateDetailsItem item) {
467             TemplateDetailsItem.Header header = item.asHeaderItem();
468             Logger.debug(D_TEMPLATE_UI, "Binding to header " + header);
469
470             disableUpdatePropagation();
471             try {
472                 String groupNoText = b.getRoot()
473                                       .getResources()
474                                       .getString(R.string.template_item_match_group_source);
475
476                 b.templateName.setText(header.getName());
477                 b.pattern.setText(header.getPattern());
478                 b.testText.setText(header.getTestText());
479
480                 if (header.hasLiteralDateYear()) {
481                     b.templateDetailsYearSource.setText(R.string.template_details_source_literal);
482                     final Integer dateYear = header.getDateYear();
483                     b.templateDetailsDateYear.setText(
484                             (dateYear == null) ? null : String.valueOf(dateYear));
485                     b.templateDetailsDateYearLayout.setVisibility(View.VISIBLE);
486                 }
487                 else {
488                     b.templateDetailsDateYearLayout.setVisibility(View.GONE);
489                     b.templateDetailsYearSource.setText(
490                             String.format(Locale.US, groupNoText, header.getDateYearMatchGroup(),
491                                     getMatchGroupText(header.getDateYearMatchGroup())));
492                 }
493                 b.templateDetailsYearSourceLabel.setOnClickListener(
494                         v -> selectHeaderDetailSource(v, HeaderDetail.DATE_YEAR));
495                 b.templateDetailsYearSource.setOnClickListener(
496                         v -> selectHeaderDetailSource(v, HeaderDetail.DATE_YEAR));
497
498                 if (header.hasLiteralDateMonth()) {
499                     b.templateDetailsMonthSource.setText(R.string.template_details_source_literal);
500                     final Integer dateMonth = header.getDateMonth();
501                     b.templateDetailsDateMonth.setText(
502                             (dateMonth == null) ? null : String.valueOf(dateMonth));
503                     b.templateDetailsDateMonthLayout.setVisibility(View.VISIBLE);
504                 }
505                 else {
506                     b.templateDetailsDateMonthLayout.setVisibility(View.GONE);
507                     b.templateDetailsMonthSource.setText(
508                             String.format(Locale.US, groupNoText, header.getDateMonthMatchGroup(),
509                                     getMatchGroupText(header.getDateMonthMatchGroup())));
510                 }
511                 b.templateDetailsMonthSourceLabel.setOnClickListener(
512                         v -> selectHeaderDetailSource(v, HeaderDetail.DATE_MONTH));
513                 b.templateDetailsMonthSource.setOnClickListener(
514                         v -> selectHeaderDetailSource(v, HeaderDetail.DATE_MONTH));
515
516                 if (header.hasLiteralDateDay()) {
517                     b.templateDetailsDaySource.setText(R.string.template_details_source_literal);
518                     final Integer dateDay = header.getDateDay();
519                     b.templateDetailsDateDay.setText(
520                             (dateDay == null) ? null : String.valueOf(dateDay));
521                     b.templateDetailsDateDayLayout.setVisibility(View.VISIBLE);
522                 }
523                 else {
524                     b.templateDetailsDateDayLayout.setVisibility(View.GONE);
525                     b.templateDetailsDaySource.setText(
526                             String.format(Locale.US, groupNoText, header.getDateDayMatchGroup(),
527                                     getMatchGroupText(header.getDateDayMatchGroup())));
528                 }
529                 b.templateDetailsDaySourceLabel.setOnClickListener(
530                         v -> selectHeaderDetailSource(v, HeaderDetail.DATE_DAY));
531                 b.templateDetailsDaySource.setOnClickListener(
532                         v -> selectHeaderDetailSource(v, HeaderDetail.DATE_DAY));
533
534                 if (header.hasLiteralTransactionDescription()) {
535                     b.templateTransactionDescriptionSource.setText(
536                             R.string.template_details_source_literal);
537                     b.transactionDescription.setText(header.getTransactionDescription());
538                     b.transactionDescriptionLayout.setVisibility(View.VISIBLE);
539                 }
540                 else {
541                     b.transactionDescriptionLayout.setVisibility(View.GONE);
542                     b.templateTransactionDescriptionSource.setText(
543                             String.format(Locale.US, groupNoText,
544                                     header.getTransactionDescriptionMatchGroup(), getMatchGroupText(
545                                             header.getTransactionDescriptionMatchGroup())));
546
547                 }
548                 b.templateTransactionDescriptionSourceLabel.setOnClickListener(
549                         v -> selectHeaderDetailSource(v, HeaderDetail.DESCRIPTION));
550                 b.templateTransactionDescriptionSource.setOnClickListener(
551                         v -> selectHeaderDetailSource(v, HeaderDetail.DESCRIPTION));
552
553                 if (header.hasLiteralTransactionComment()) {
554                     b.templateTransactionCommentSource.setText(
555                             R.string.template_details_source_literal);
556                     b.transactionComment.setText(header.getTransactionComment());
557                     b.transactionCommentLayout.setVisibility(View.VISIBLE);
558                 }
559                 else {
560                     b.transactionCommentLayout.setVisibility(View.GONE);
561                     b.templateTransactionCommentSource.setText(String.format(Locale.US, groupNoText,
562                             header.getTransactionCommentMatchGroup(),
563                             getMatchGroupText(header.getTransactionCommentMatchGroup())));
564
565                 }
566                 b.templateTransactionCommentSourceLabel.setOnClickListener(
567                         v -> selectHeaderDetailSource(v, HeaderDetail.COMMENT));
568                 b.templateTransactionCommentSource.setOnClickListener(
569                         v -> selectHeaderDetailSource(v, HeaderDetail.COMMENT));
570
571                 b.templateDetailsHeadScanQrButton.setOnClickListener(this::scanTestQR);
572
573                 checkPatternError(header);
574             }
575             finally {
576                 enableUpdatePropagation();
577             }
578         }
579         private void checkPatternError(TemplateDetailsItem.Header item) {
580             if (item.getPatternError() != null) {
581                 b.patternLayout.setError(item.getPatternError());
582                 b.patternHintTitle.setVisibility(View.GONE);
583                 b.patternHintText.setVisibility(View.GONE);
584             }
585             else {
586                 b.patternLayout.setError(null);
587                 if (item.testMatch() != null) {
588                     b.patternHintText.setText(item.testMatch());
589                     b.patternHintTitle.setVisibility(View.VISIBLE);
590                     b.patternHintText.setVisibility(View.VISIBLE);
591                 }
592                 else {
593                     b.patternLayout.setError(null);
594                     b.patternHintTitle.setVisibility(View.GONE);
595                     b.patternHintText.setVisibility(View.GONE);
596                 }
597             }
598
599         }
600         private void scanTestQR(View view) {
601             QRScanCapableFragment.triggerQRScan();
602         }
603     }
604
605     public class AccountRow extends BaseItem {
606         private final TemplateDetailsAccountBinding b;
607         public AccountRow(@NonNull TemplateDetailsAccountBinding binding) {
608             super(binding.getRoot());
609             b = binding;
610
611             TextWatcher accountNameWatcher = new TextWatcher() {
612                 @Override
613                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
614                 @Override
615                 public void onTextChanged(CharSequence s, int start, int before, int count) {}
616                 @Override
617                 public void afterTextChanged(Editable s) {
618                     if (updatePropagationDisabled)
619                         return;
620
621                     TemplateDetailsItem.AccountRow accRow = getItem();
622                     Logger.debug(D_TEMPLATE_UI,
623                             "Storing changed account name " + s + "; accRow=" + accRow);
624                     accRow.setAccountName(String.valueOf(s));
625                 }
626             };
627             b.templateDetailsAccountName.addTextChangedListener(accountNameWatcher);
628             b.templateDetailsAccountName.setAdapter(new AccountAutocompleteAdapter(b.getRoot()
629                                                                                     .getContext()));
630             b.templateDetailsAccountName.setOnItemClickListener(
631                     (parent, view, position, id) -> b.templateDetailsAccountName.setText(
632                             ((TextView) view).getText()));
633             TextWatcher accountCommentWatcher = new TextWatcher() {
634                 @Override
635                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
636                 @Override
637                 public void onTextChanged(CharSequence s, int start, int before, int count) {}
638                 @Override
639                 public void afterTextChanged(Editable s) {
640                     if (updatePropagationDisabled)
641                         return;
642
643                     TemplateDetailsItem.AccountRow accRow = getItem();
644                     Logger.debug(D_TEMPLATE_UI,
645                             "Storing changed account comment " + s + "; accRow=" + accRow);
646                     accRow.setAccountComment(String.valueOf(s));
647                 }
648             };
649             b.templateDetailsAccountComment.addTextChangedListener(accountCommentWatcher);
650
651             b.templateDetailsAccountAmount.addTextChangedListener(new TextWatcher() {
652                 @Override
653                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
654
655                 }
656                 @Override
657                 public void onTextChanged(CharSequence s, int start, int before, int count) {
658
659                 }
660                 @Override
661                 public void afterTextChanged(Editable s) {
662                     if (updatePropagationDisabled)
663                         return;
664
665                     TemplateDetailsItem.AccountRow accRow = getItem();
666
667                     String str = String.valueOf(s);
668                     if (Misc.emptyIsNull(str) == null) {
669                         accRow.setAmount(null);
670                     }
671                     else {
672                         try {
673                             final float amount = Data.parseNumber(str);
674                             accRow.setAmount(amount);
675                             b.templateDetailsAccountAmountLayout.setError(null);
676
677                             Logger.debug(D_TEMPLATE_UI, String.format(Locale.US,
678                                     "Storing changed account amount %s [%4.2f]; accRow=%s", s,
679                                     amount, accRow));
680                         }
681                         catch (NumberFormatException | ParseException e) {
682                             b.templateDetailsAccountAmountLayout.setError("!");
683                         }
684                     }
685                 }
686             });
687             b.templateDetailsAccountAmount.setOnFocusChangeListener((v, hasFocus) -> {
688                 if (hasFocus)
689                     return;
690
691                 TemplateDetailsItem.AccountRow accRow = getItem();
692                 if (!accRow.hasLiteralAmount())
693                     return;
694                 Float amt = accRow.getAmount();
695                 if (amt == null)
696                     return;
697
698                 b.templateDetailsAccountAmount.setText(Data.formatNumber(amt));
699             });
700
701             b.negateAmountSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
702                 if (updatePropagationDisabled)
703                     return;
704
705                 getItem().setNegateAmount(isChecked);
706                 b.templateDetailsNegateAmountText.setText(
707                         isChecked ? R.string.template_account_change_amount_sign
708                                   : R.string.template_account_keep_amount_sign);
709             });
710             final View.OnClickListener negLabelClickListener =
711                     (view) -> b.negateAmountSwitch.toggle();
712             b.templateDetailsNegateAmountLabel.setOnClickListener(negLabelClickListener);
713             b.templateDetailsNegateAmountText.setOnClickListener(negLabelClickListener);
714             manageAccountLabelDrag();
715         }
716         @SuppressLint("ClickableViewAccessibility")
717         public void manageAccountLabelDrag() {
718             b.patternAccountLabel.setOnTouchListener((v, event) -> {
719                 if (event.getAction() == MotionEvent.ACTION_DOWN) {
720                     itemTouchHelper.startDrag(this);
721                 }
722                 return false;
723             });
724         }
725         @Override
726         void bind(TemplateDetailsItem item) {
727             disableUpdatePropagation();
728             try {
729                 final Resources resources = b.getRoot()
730                                              .getResources();
731                 String groupNoText = resources.getString(R.string.template_item_match_group_source);
732
733                 TemplateDetailsItem.AccountRow accRow = item.asAccountRowItem();
734                 b.patternAccountLabel.setText(String.format(Locale.US,
735                         resources.getString(R.string.template_details_account_row_label),
736                         accRow.getPosition()));
737                 if (accRow.hasLiteralAccountName()) {
738                     b.templateDetailsAccountNameLayout.setVisibility(View.VISIBLE);
739                     b.templateDetailsAccountName.setText(accRow.getAccountName());
740                     b.templateDetailsAccountNameSource.setText(
741                             R.string.template_details_source_literal);
742                 }
743                 else {
744                     b.templateDetailsAccountNameLayout.setVisibility(View.GONE);
745                     b.templateDetailsAccountNameSource.setText(
746                             String.format(Locale.US, groupNoText, accRow.getAccountNameMatchGroup(),
747                                     getMatchGroupText(accRow.getAccountNameMatchGroup())));
748                 }
749
750                 if (accRow.hasLiteralAccountComment()) {
751                     b.templateDetailsAccountCommentLayout.setVisibility(View.VISIBLE);
752                     b.templateDetailsAccountComment.setText(accRow.getAccountComment());
753                     b.templateDetailsAccountCommentSource.setText(
754                             R.string.template_details_source_literal);
755                 }
756                 else {
757                     b.templateDetailsAccountCommentLayout.setVisibility(View.GONE);
758                     b.templateDetailsAccountCommentSource.setText(
759                             String.format(Locale.US, groupNoText,
760                                     accRow.getAccountCommentMatchGroup(),
761                                     getMatchGroupText(accRow.getAccountCommentMatchGroup())));
762                 }
763
764                 if (accRow.hasLiteralAmount()) {
765                     b.templateDetailsAccountAmountSource.setText(
766                             R.string.template_details_source_literal);
767                     b.templateDetailsAccountAmount.setVisibility(View.VISIBLE);
768                     Float amt = accRow.getAmount();
769                     b.templateDetailsAccountAmount.setText((amt == null) ? null : String.format(
770                             Data.locale.getValue(), "%,4.2f", (accRow.getAmount())));
771                     b.negateAmountSwitch.setVisibility(View.GONE);
772                     b.templateDetailsNegateAmountLabel.setVisibility(View.GONE);
773                     b.templateDetailsNegateAmountText.setVisibility(View.GONE);
774                 }
775                 else {
776                     b.templateDetailsAccountAmountSource.setText(
777                             String.format(Locale.US, groupNoText, accRow.getAmountMatchGroup(),
778                                     getMatchGroupText(accRow.getAmountMatchGroup())));
779                     b.templateDetailsAccountAmountLayout.setVisibility(View.GONE);
780                     b.negateAmountSwitch.setVisibility(View.VISIBLE);
781                     b.negateAmountSwitch.setChecked(accRow.isNegateAmount());
782                     b.templateDetailsNegateAmountText.setText(
783                             accRow.isNegateAmount() ? R.string.template_account_change_amount_sign
784                                                     : R.string.template_account_keep_amount_sign);
785                     b.templateDetailsNegateAmountLabel.setVisibility(View.VISIBLE);
786                     b.templateDetailsNegateAmountText.setVisibility(View.VISIBLE);
787                 }
788
789                 b.templateAccountNameSourceLabel.setOnClickListener(
790                         v -> selectAccountRowDetailSource(v, AccDetail.ACCOUNT));
791                 b.templateDetailsAccountNameSource.setOnClickListener(
792                         v -> selectAccountRowDetailSource(v, AccDetail.ACCOUNT));
793                 b.templateAccountCommentSourceLabel.setOnClickListener(
794                         v -> selectAccountRowDetailSource(v, AccDetail.COMMENT));
795                 b.templateDetailsAccountCommentSource.setOnClickListener(
796                         v -> selectAccountRowDetailSource(v, AccDetail.COMMENT));
797                 b.templateAccountAmountSourceLabel.setOnClickListener(
798                         v -> selectAccountRowDetailSource(v, AccDetail.AMOUNT));
799                 b.templateDetailsAccountAmountSource.setOnClickListener(
800                         v -> selectAccountRowDetailSource(v, AccDetail.AMOUNT));
801             }
802             finally {
803                 enableUpdatePropagation();
804             }
805         }
806         private @NotNull TemplateDetailsItem.AccountRow getItem() {
807             return differ.getCurrentList()
808                          .get(getAdapterPosition())
809                          .asAccountRowItem();
810         }
811         private void selectAccountRowDetailSource(View v, AccDetail detail) {
812             TemplateDetailsItem.AccountRow accRow = getItem();
813             final TemplateDetailsItem.Header header = getHeader();
814             Logger.debug(D_TEMPLATE_UI, "header is " + header);
815             TemplateDetailSourceSelectorFragment sel =
816                     TemplateDetailSourceSelectorFragment.newInstance(1, header.getPattern(),
817                             header.getTestText());
818             sel.setOnSourceSelectedListener((literal, group) -> {
819                 if (literal) {
820                     switch (detail) {
821                         case ACCOUNT:
822                             accRow.switchToLiteralAccountName();
823                             break;
824                         case COMMENT:
825                             accRow.switchToLiteralAccountComment();
826                             break;
827                         case AMOUNT:
828                             accRow.switchToLiteralAmount();
829                             break;
830                         default:
831                             throw new IllegalStateException("Unexpected detail " + detail);
832                     }
833                 }
834                 else {
835                     switch (detail) {
836                         case ACCOUNT:
837                             accRow.setAccountNameMatchGroup(group);
838                             break;
839                         case COMMENT:
840                             accRow.setAccountCommentMatchGroup(group);
841                             break;
842                         case AMOUNT:
843                             accRow.setAmountMatchGroup(group);
844                             break;
845                         default:
846                             throw new IllegalStateException("Unexpected detail " + detail);
847                     }
848                 }
849
850                 notifyItemChanged(getAdapterPosition());
851             });
852             final AppCompatActivity activity = (AppCompatActivity) v.getContext();
853             sel.show(activity.getSupportFragmentManager(), "template-details-source-selector");
854         }
855     }
856 }