]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateDetailsAdapter.java
rename patterns 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.text.Editable;
21 import android.text.TextWatcher;
22 import android.view.LayoutInflater;
23 import android.view.View;
24 import android.view.ViewGroup;
25
26 import androidx.annotation.NonNull;
27 import androidx.appcompat.app.AppCompatActivity;
28 import androidx.recyclerview.widget.AsyncListDiffer;
29 import androidx.recyclerview.widget.DiffUtil;
30 import androidx.recyclerview.widget.RecyclerView;
31
32 import net.ktnx.mobileledger.R;
33 import net.ktnx.mobileledger.databinding.TemplateDetailsAccountBinding;
34 import net.ktnx.mobileledger.databinding.TemplateDetailsHeaderBinding;
35 import net.ktnx.mobileledger.db.TemplateBase;
36 import net.ktnx.mobileledger.model.Data;
37 import net.ktnx.mobileledger.model.TemplateDetailsItem;
38 import net.ktnx.mobileledger.ui.QRScanCapableFragment;
39 import net.ktnx.mobileledger.ui.TemplateDetailSourceSelectorFragment;
40 import net.ktnx.mobileledger.utils.Logger;
41 import net.ktnx.mobileledger.utils.Misc;
42
43 import org.jetbrains.annotations.NotNull;
44
45 import java.text.ParseException;
46 import java.util.ArrayList;
47 import java.util.List;
48 import java.util.Locale;
49 import java.util.regex.Matcher;
50 import java.util.regex.Pattern;
51
52 class TemplateDetailsAdapter extends RecyclerView.Adapter<TemplateDetailsAdapter.ViewHolder> {
53     private static final String D_TEMPLATE_UI = "template-ui";
54     private final AsyncListDiffer<TemplateDetailsItem> differ;
55     public TemplateDetailsAdapter() {
56         super();
57         setHasStableIds(true);
58         differ = new AsyncListDiffer<>(this, new DiffUtil.ItemCallback<TemplateDetailsItem>() {
59             @Override
60             public boolean areItemsTheSame(@NonNull TemplateDetailsItem oldItem,
61                                            @NonNull TemplateDetailsItem newItem) {
62                 if (oldItem.getType() != newItem.getType())
63                     return false;
64                 if (oldItem.getType()
65                            .equals(TemplateDetailsItem.Type.HEADER))
66                     return true;    // only one header item, ever
67                 // the rest is comparing two account row items
68                 return oldItem.asAccountRowItem()
69                               .getId() == newItem.asAccountRowItem()
70                                                  .getId();
71             }
72             @Override
73             public boolean areContentsTheSame(@NonNull TemplateDetailsItem oldItem,
74                                               @NonNull TemplateDetailsItem newItem) {
75                 if (oldItem.getType()
76                            .equals(TemplateDetailsItem.Type.HEADER))
77                 {
78                     TemplateDetailsItem.Header oldHeader = oldItem.asHeaderItem();
79                     TemplateDetailsItem.Header newHeader = newItem.asHeaderItem();
80
81                     return oldHeader.equalContents(newHeader);
82                 }
83                 else {
84                     TemplateDetailsItem.AccountRow oldAcc = oldItem.asAccountRowItem();
85                     TemplateDetailsItem.AccountRow newAcc = newItem.asAccountRowItem();
86
87                     return oldAcc.equalContents(newAcc);
88                 }
89             }
90         });
91     }
92     @Override
93     public long getItemId(int position) {
94         // header item is always first and IDs id may duplicate some of the account IDs
95         if (position == 0)
96             return -1;
97         TemplateDetailsItem.AccountRow accRow = differ.getCurrentList()
98                                                       .get(position)
99                                                       .asAccountRowItem();
100         return accRow.getId();
101     }
102     @Override
103     public int getItemViewType(int position) {
104
105         return differ.getCurrentList()
106                      .get(position)
107                      .getType()
108                      .toInt();
109     }
110     @NonNull
111     @Override
112     public TemplateDetailsAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
113                                                                 int viewType) {
114         final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
115         switch (viewType) {
116             case TemplateDetailsItem.TYPE.header:
117                 return new Header(TemplateDetailsHeaderBinding.inflate(inflater, parent, false));
118             case TemplateDetailsItem.TYPE.accountItem:
119                 return new AccountRow(
120                         TemplateDetailsAccountBinding.inflate(inflater, parent, false));
121             default:
122                 throw new IllegalStateException("Unsupported view type " + viewType);
123         }
124     }
125     @Override
126     public void onBindViewHolder(@NonNull TemplateDetailsAdapter.ViewHolder holder, int position) {
127         TemplateDetailsItem item = differ.getCurrentList()
128                                          .get(position);
129         holder.bind(item);
130     }
131     @Override
132     public int getItemCount() {
133         return differ.getCurrentList()
134                      .size();
135     }
136     public void setTemplateItems(List<TemplateBase> items) {
137         ArrayList<TemplateDetailsItem> list = new ArrayList<>();
138         for (TemplateBase p : items) {
139             TemplateDetailsItem item = TemplateDetailsItem.fromRoomObject(p);
140             list.add(item);
141         }
142         setItems(list);
143     }
144     public void setItems(List<TemplateDetailsItem> items) {
145         differ.submitList(items);
146     }
147     public String getMatchGroupText(int groupNumber) {
148         TemplateDetailsItem.Header header = getHeader();
149         Pattern p = header.getCompiledPattern();
150         if (p == null)
151             return null;
152
153         final String testText = Misc.nullIsEmpty(header.getTestText());
154         Matcher m = p.matcher(testText);
155         if (m.matches() && m.groupCount() >= groupNumber)
156             return m.group(groupNumber);
157         else
158             return null;
159     }
160     protected TemplateDetailsItem.Header getHeader() {
161         return differ.getCurrentList()
162                      .get(0)
163                      .asHeaderItem();
164     }
165
166     private enum HeaderDetail {DESCRIPTION, COMMENT, DATE_YEAR, DATE_MONTH, DATE_DAY}
167
168     private enum AccDetail {ACCOUNT, COMMENT, AMOUNT}
169
170     public abstract static class ViewHolder extends RecyclerView.ViewHolder {
171         ViewHolder(@NonNull View itemView) {
172             super(itemView);
173         }
174         abstract void bind(TemplateDetailsItem item);
175     }
176
177     public class Header extends ViewHolder {
178         private final TemplateDetailsHeaderBinding b;
179         public Header(@NonNull TemplateDetailsHeaderBinding binding) {
180             super(binding.getRoot());
181             b = binding;
182
183             TextWatcher templateNameWatcher = new TextWatcher() {
184                 @Override
185                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
186                 @Override
187                 public void onTextChanged(CharSequence s, int start, int before, int count) {}
188                 @Override
189                 public void afterTextChanged(Editable s) {
190                     final TemplateDetailsItem.Header header = getItem();
191                     Logger.debug(D_TEMPLATE_UI,
192                             "Storing changed template name " + s + "; header=" + header);
193                     header.setName(String.valueOf(s));
194                 }
195             };
196             b.templateName.addTextChangedListener(templateNameWatcher);
197             TextWatcher patternWatcher = new TextWatcher() {
198                 @Override
199                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
200                 @Override
201                 public void onTextChanged(CharSequence s, int start, int before, int count) {}
202                 @Override
203                 public void afterTextChanged(Editable s) {
204                     final TemplateDetailsItem.Header header = getItem();
205                     Logger.debug(D_TEMPLATE_UI,
206                             "Storing changed pattern " + s + "; header=" + header);
207                     header.setPattern(String.valueOf(s));
208                 }
209             };
210             b.pattern.addTextChangedListener(patternWatcher);
211             TextWatcher testTextWatcher = new TextWatcher() {
212                 @Override
213                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
214                 @Override
215                 public void onTextChanged(CharSequence s, int start, int before, int count) {}
216                 @Override
217                 public void afterTextChanged(Editable s) {
218                     final TemplateDetailsItem.Header header = getItem();
219                     Logger.debug(D_TEMPLATE_UI,
220                             "Storing changed test text " + s + "; header=" + header);
221                     header.setTestText(String.valueOf(s));
222                 }
223             };
224             b.testText.addTextChangedListener(testTextWatcher);
225             TextWatcher transactionDescriptionWatcher = new TextWatcher() {
226                 @Override
227                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
228                 }
229                 @Override
230                 public void onTextChanged(CharSequence s, int start, int before, int count) {
231
232                 }
233                 @Override
234                 public void afterTextChanged(Editable s) {
235                     final TemplateDetailsItem.Header header = getItem();
236                     Logger.debug(D_TEMPLATE_UI,
237                             "Storing changed transaction description " + s + "; header=" + header);
238                     header.setTransactionDescription(String.valueOf(s));
239                 }
240             };
241             b.transactionDescription.addTextChangedListener(transactionDescriptionWatcher);
242             TextWatcher transactionCommentWatcher = new TextWatcher() {
243                 @Override
244                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
245
246                 }
247                 @Override
248                 public void onTextChanged(CharSequence s, int start, int before, int count) {
249
250                 }
251                 @Override
252                 public void afterTextChanged(Editable s) {
253                     final TemplateDetailsItem.Header header = getItem();
254                     Logger.debug(D_TEMPLATE_UI,
255                             "Storing changed transaction description " + s + "; header=" + header);
256                     header.setTransactionComment(String.valueOf(s));
257                 }
258             };
259             b.transactionComment.addTextChangedListener(transactionCommentWatcher);
260         }
261         @NotNull
262         private TemplateDetailsItem.Header getItem() {
263             int pos = getAdapterPosition();
264             return differ.getCurrentList()
265                          .get(pos)
266                          .asHeaderItem();
267         }
268         private void selectHeaderDetailSource(View v, HeaderDetail detail) {
269             TemplateDetailsItem.Header header = getItem();
270             Logger.debug(D_TEMPLATE_UI, "header is " + header);
271             TemplateDetailSourceSelectorFragment sel =
272                     TemplateDetailSourceSelectorFragment.newInstance(1, header.getPattern(),
273                             header.getTestText());
274             sel.setOnSourceSelectedListener((literal, group) -> {
275                 if (literal) {
276                     switch (detail) {
277                         case DESCRIPTION:
278                             header.switchToLiteralTransactionDescription();
279                             break;
280                         case COMMENT:
281                             header.switchToLiteralTransactionComment();
282                             break;
283                         case DATE_YEAR:
284                             header.switchToLiteralDateYear();
285                             break;
286                         case DATE_MONTH:
287                             header.switchToLiteralDateMonth();
288                             break;
289                         case DATE_DAY:
290                             header.switchToLiteralDateDay();
291                             break;
292                         default:
293                             throw new IllegalStateException("Unexpected detail " + detail);
294                     }
295                 }
296                 else {
297                     switch (detail) {
298                         case DESCRIPTION:
299                             header.setTransactionDescriptionMatchGroup(group);
300                             break;
301                         case COMMENT:
302                             header.setTransactionCommentMatchGroup(group);
303                             break;
304                         case DATE_YEAR:
305                             header.setDateYearMatchGroup(group);
306                             break;
307                         case DATE_MONTH:
308                             header.setDateMonthMatchGroup(group);
309                             break;
310                         case DATE_DAY:
311                             header.setDateDayMatchGroup(group);
312                             break;
313                         default:
314                             throw new IllegalStateException("Unexpected detail " + detail);
315                     }
316                 }
317
318                 notifyItemChanged(getAdapterPosition());
319             });
320             final AppCompatActivity activity = (AppCompatActivity) v.getContext();
321             sel.show(activity.getSupportFragmentManager(), "template-details-source-selector");
322         }
323         @Override
324         void bind(TemplateDetailsItem item) {
325             TemplateDetailsItem.Header header = item.asHeaderItem();
326             Logger.debug(D_TEMPLATE_UI, "Binding to header " + header);
327
328             b.templateName.setText(header.getName());
329             b.pattern.setText(header.getPattern());
330             b.testText.setText(header.getTestText());
331
332             if (header.hasLiteralDateYear()) {
333                 b.templateDetailsYearSource.setText(R.string.template_details_source_literal);
334                 final Integer dateYear = header.getDateYear();
335                 b.templateDetailsDateYear.setText(
336                         (dateYear == null) ? null : String.valueOf(dateYear));
337                 b.templateDetailsDateYearLayout.setVisibility(View.VISIBLE);
338             }
339             else {
340                 b.templateDetailsDateYearLayout.setVisibility(View.GONE);
341                 b.templateDetailsYearSource.setText(
342                         String.format(Locale.US, "Group %d (%s)", header.getDateYearMatchGroup(),
343                                 getMatchGroupText(header.getDateYearMatchGroup())));
344             }
345             b.templateDetailsYearSourceLabel.setOnClickListener(
346                     v -> selectHeaderDetailSource(v, HeaderDetail.DATE_YEAR));
347             b.templateDetailsYearSource.setOnClickListener(
348                     v -> selectHeaderDetailSource(v, HeaderDetail.DATE_YEAR));
349
350             if (header.hasLiteralDateMonth()) {
351                 b.templateDetailsMonthSource.setText(R.string.template_details_source_literal);
352                 final Integer dateMonth = header.getDateMonth();
353                 b.templateDetailsDateMonth.setText(
354                         (dateMonth == null) ? null : String.valueOf(dateMonth));
355                 b.templateDetailsDateMonthLayout.setVisibility(View.VISIBLE);
356             }
357             else {
358                 b.templateDetailsDateMonthLayout.setVisibility(View.GONE);
359                 b.templateDetailsMonthSource.setText(
360                         String.format(Locale.US, "Group %d (%s)", header.getDateMonthMatchGroup(),
361                                 getMatchGroupText(header.getDateMonthMatchGroup())));
362             }
363             b.templateDetailsMonthSourceLabel.setOnClickListener(
364                     v -> selectHeaderDetailSource(v, HeaderDetail.DATE_MONTH));
365             b.templateDetailsMonthSource.setOnClickListener(
366                     v -> selectHeaderDetailSource(v, HeaderDetail.DATE_MONTH));
367
368             if (header.hasLiteralDateDay()) {
369                 b.templateDetailsDaySource.setText(R.string.template_details_source_literal);
370                 final Integer dateDay = header.getDateDay();
371                 b.templateDetailsDateDay.setText(
372                         (dateDay == null) ? null : String.valueOf(dateDay));
373                 b.templateDetailsDateDayLayout.setVisibility(View.VISIBLE);
374             }
375             else {
376                 b.templateDetailsDateDayLayout.setVisibility(View.GONE);
377                 b.templateDetailsDaySource.setText(
378                         String.format(Locale.US, "Group %d (%s)", header.getDateDayMatchGroup(),
379                                 getMatchGroupText(header.getDateDayMatchGroup())));
380             }
381             b.templateDetailsDaySourceLabel.setOnClickListener(
382                     v -> selectHeaderDetailSource(v, HeaderDetail.DATE_DAY));
383             b.templateDetailsDaySource.setOnClickListener(
384                     v -> selectHeaderDetailSource(v, HeaderDetail.DATE_DAY));
385
386             if (header.hasLiteralTransactionDescription()) {
387                 b.templateTransactionDescriptionSource.setText(
388                         R.string.template_details_source_literal);
389                 b.transactionDescription.setText(header.getTransactionDescription());
390                 b.transactionDescriptionLayout.setVisibility(View.VISIBLE);
391             }
392             else {
393                 b.transactionDescriptionLayout.setVisibility(View.GONE);
394                 b.templateTransactionDescriptionSource.setText(
395                         String.format(Locale.US, "Group %d (%s)",
396                                 header.getTransactionDescriptionMatchGroup(),
397                                 getMatchGroupText(header.getTransactionDescriptionMatchGroup())));
398
399             }
400             b.templateTransactionDescriptionSourceLabel.setOnClickListener(
401                     v -> selectHeaderDetailSource(v, HeaderDetail.DESCRIPTION));
402             b.templateTransactionDescriptionSource.setOnClickListener(
403                     v -> selectHeaderDetailSource(v, HeaderDetail.DESCRIPTION));
404
405             if (header.hasLiteralTransactionComment()) {
406                 b.templateTransactionCommentSource.setText(
407                         R.string.template_details_source_literal);
408                 b.transactionComment.setText(header.getTransactionComment());
409                 b.transactionCommentLayout.setVisibility(View.VISIBLE);
410             }
411             else {
412                 b.transactionCommentLayout.setVisibility(View.GONE);
413                 b.templateTransactionCommentSource.setText(String.format(Locale.US, "Group %d (%s)",
414                         header.getTransactionCommentMatchGroup(),
415                         getMatchGroupText(header.getTransactionCommentMatchGroup())));
416
417             }
418             b.templateTransactionCommentSourceLabel.setOnClickListener(
419                     v -> selectHeaderDetailSource(v, HeaderDetail.COMMENT));
420             b.templateTransactionCommentSource.setOnClickListener(
421                     v -> selectHeaderDetailSource(v, HeaderDetail.COMMENT));
422
423             b.templateDetailsHeadScanQrButton.setOnClickListener(this::scanTestQR);
424
425         }
426         private void scanTestQR(View view) {
427             QRScanCapableFragment.triggerQRScan();
428         }
429     }
430
431     public class AccountRow extends ViewHolder {
432         private final TemplateDetailsAccountBinding b;
433         public AccountRow(@NonNull TemplateDetailsAccountBinding binding) {
434             super(binding.getRoot());
435             b = binding;
436
437             TextWatcher accountNameWatcher = new TextWatcher() {
438                 @Override
439                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
440                 @Override
441                 public void onTextChanged(CharSequence s, int start, int before, int count) {}
442                 @Override
443                 public void afterTextChanged(Editable s) {
444                     TemplateDetailsItem.AccountRow accRow = getItem();
445                     Logger.debug(D_TEMPLATE_UI,
446                             "Storing changed account name " + s + "; accRow=" + accRow);
447                     accRow.setAccountName(String.valueOf(s));
448                 }
449             };
450             b.templateDetailsAccountName.addTextChangedListener(accountNameWatcher);
451             TextWatcher accountCommentWatcher = new TextWatcher() {
452                 @Override
453                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
454                 @Override
455                 public void onTextChanged(CharSequence s, int start, int before, int count) {}
456                 @Override
457                 public void afterTextChanged(Editable s) {
458                     TemplateDetailsItem.AccountRow accRow = getItem();
459                     Logger.debug(D_TEMPLATE_UI,
460                             "Storing changed account comment " + s + "; accRow=" + accRow);
461                     accRow.setAccountComment(String.valueOf(s));
462                 }
463             };
464             b.templateDetailsAccountComment.addTextChangedListener(accountCommentWatcher);
465
466             b.templateDetailsAccountAmount.addTextChangedListener(new TextWatcher() {
467                 @Override
468                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
469
470                 }
471                 @Override
472                 public void onTextChanged(CharSequence s, int start, int before, int count) {
473
474                 }
475                 @Override
476                 public void afterTextChanged(Editable s) {
477                     TemplateDetailsItem.AccountRow accRow = getItem();
478
479                     String str = String.valueOf(s);
480                     if (Misc.emptyIsNull(str) == null) {
481                         accRow.setAmount(null);
482                     }
483                     else {
484                         try {
485                             final float amount = Data.parseNumber(str);
486                             accRow.setAmount(amount);
487                             b.templateDetailsAccountAmountLayout.setError(null);
488
489                             Logger.debug(D_TEMPLATE_UI, String.format(Locale.US,
490                                     "Storing changed account amount %s [%4.2f]; accRow=%s", s,
491                                     amount, accRow));
492                         }
493                         catch (NumberFormatException | ParseException e) {
494                             b.templateDetailsAccountAmountLayout.setError("!");
495                         }
496                     }
497                 }
498             });
499             b.templateDetailsAccountAmount.setOnFocusChangeListener((v, hasFocus) -> {
500                 if (hasFocus)
501                     return;
502
503                 TemplateDetailsItem.AccountRow accRow = getItem();
504                 if (!accRow.hasLiteralAmount())
505                     return;
506                 Float amt = accRow.getAmount();
507                 if (amt == null)
508                     return;
509
510                 b.templateDetailsAccountAmount.setText(Data.formatNumber(amt));
511             });
512
513             b.negateAmountSwitch.setOnCheckedChangeListener(
514                     (buttonView, isChecked) -> getItem().setNegateAmount(isChecked));
515         }
516         @Override
517         void bind(TemplateDetailsItem item) {
518             TemplateDetailsItem.AccountRow accRow = item.asAccountRowItem();
519             if (accRow.hasLiteralAccountName()) {
520                 b.templateDetailsAccountNameLayout.setVisibility(View.VISIBLE);
521                 b.templateDetailsAccountName.setText(accRow.getAccountName());
522                 b.templateDetailsAccountNameSource.setText(
523                         R.string.template_details_source_literal);
524             }
525             else {
526                 b.templateDetailsAccountNameLayout.setVisibility(View.GONE);
527                 b.templateDetailsAccountNameSource.setText(
528                         String.format(Locale.US, "Group %d (%s)", accRow.getAccountNameMatchGroup(),
529                                 getMatchGroupText(accRow.getAccountNameMatchGroup())));
530             }
531
532             if (accRow.hasLiteralAccountComment()) {
533                 b.templateDetailsAccountCommentLayout.setVisibility(View.VISIBLE);
534                 b.templateDetailsAccountComment.setText(accRow.getAccountComment());
535                 b.templateDetailsAccountCommentSource.setText(
536                         R.string.template_details_source_literal);
537             }
538             else {
539                 b.templateDetailsAccountCommentLayout.setVisibility(View.GONE);
540                 b.templateDetailsAccountCommentSource.setText(
541                         String.format(Locale.US, "Group %d (%s)",
542                                 accRow.getAccountCommentMatchGroup(),
543                                 getMatchGroupText(accRow.getAccountCommentMatchGroup())));
544             }
545
546             if (accRow.hasLiteralAmount()) {
547                 b.templateDetailsAccountAmountSource.setText(
548                         R.string.template_details_source_literal);
549                 b.templateDetailsAccountAmount.setVisibility(View.VISIBLE);
550                 Float amt = accRow.getAmount();
551                 b.templateDetailsAccountAmount.setText((amt == null) ? null : String.format(
552                         Data.locale.getValue(), "%,4.2f", (accRow.getAmount())));
553                 b.negateAmountSwitch.setVisibility(View.GONE);
554             }
555             else {
556                 b.templateDetailsAccountAmountSource.setText(
557                         String.format(Locale.US, "Group %d (%s)", accRow.getAmountMatchGroup(),
558                                 getMatchGroupText(accRow.getAmountMatchGroup())));
559                 b.templateDetailsAccountAmountLayout.setVisibility(View.GONE);
560                 b.negateAmountSwitch.setVisibility(View.VISIBLE);
561                 b.negateAmountSwitch.setChecked(accRow.isNegateAmount());
562             }
563
564             b.templateAccountNameSourceLabel.setOnClickListener(
565                     v -> selectAccountRowDetailSource(v, AccDetail.ACCOUNT));
566             b.templateDetailsAccountNameSource.setOnClickListener(
567                     v -> selectAccountRowDetailSource(v, AccDetail.ACCOUNT));
568             b.templateAccountCommentSourceLabel.setOnClickListener(
569                     v -> selectAccountRowDetailSource(v, AccDetail.COMMENT));
570             b.templateDetailsAccountCommentSource.setOnClickListener(
571                     v -> selectAccountRowDetailSource(v, AccDetail.COMMENT));
572             b.templateAccountAmountSourceLabel.setOnClickListener(
573                     v -> selectAccountRowDetailSource(v, AccDetail.AMOUNT));
574             b.templateDetailsAccountAmountSource.setOnClickListener(
575                     v -> selectAccountRowDetailSource(v, AccDetail.AMOUNT));
576         }
577         private @NotNull TemplateDetailsItem.AccountRow getItem() {
578             return differ.getCurrentList()
579                          .get(getAdapterPosition())
580                          .asAccountRowItem();
581         }
582         private void selectAccountRowDetailSource(View v, AccDetail detail) {
583             TemplateDetailsItem.AccountRow accRow = getItem();
584             final TemplateDetailsItem.Header header = getHeader();
585             Logger.debug(D_TEMPLATE_UI, "header is " + header);
586             TemplateDetailSourceSelectorFragment sel =
587                     TemplateDetailSourceSelectorFragment.newInstance(1, header.getPattern(),
588                             header.getTestText());
589             sel.setOnSourceSelectedListener((literal, group) -> {
590                 if (literal) {
591                     switch (detail) {
592                         case ACCOUNT:
593                             accRow.switchToLiteralAccountName();
594                             break;
595                         case COMMENT:
596                             accRow.switchToLiteralAccountComment();
597                             break;
598                         case AMOUNT:
599                             accRow.switchToLiteralAmount();
600                             break;
601                         default:
602                             throw new IllegalStateException("Unexpected detail " + detail);
603                     }
604                 }
605                 else {
606                     switch (detail) {
607                         case ACCOUNT:
608                             accRow.setAccountNameMatchGroup(group);
609                             break;
610                         case COMMENT:
611                             accRow.setAccountCommentMatchGroup(group);
612                             break;
613                         case AMOUNT:
614                             accRow.setAmountMatchGroup(group);
615                             break;
616                         default:
617                             throw new IllegalStateException("Unexpected detail " + detail);
618                     }
619                 }
620
621                 notifyItemChanged(getAdapterPosition());
622             });
623             final AppCompatActivity activity = (AppCompatActivity) v.getContext();
624             sel.show(activity.getSupportFragmentManager(), "template-details-source-selector");
625         }
626     }
627 }