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