]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternDetailsAdapter.java
fix setting null literal value for date components
[mobile-ledger.git] / app / src / main / java / net / ktnx / mobileledger / ui / patterns / PatternDetailsAdapter.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.patterns;
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.PatternDetailsAccountBinding;
34 import net.ktnx.mobileledger.databinding.PatternDetailsHeaderBinding;
35 import net.ktnx.mobileledger.db.PatternBase;
36 import net.ktnx.mobileledger.model.Data;
37 import net.ktnx.mobileledger.model.PatternDetailsItem;
38 import net.ktnx.mobileledger.ui.PatternDetailSourceSelectorFragment;
39 import net.ktnx.mobileledger.ui.QRScanCapableFragment;
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 PatternDetailsAdapter extends RecyclerView.Adapter<PatternDetailsAdapter.ViewHolder> {
53     private static final String D_PATTERN_UI = "pattern-ui";
54     private final AsyncListDiffer<PatternDetailsItem> differ;
55     public PatternDetailsAdapter() {
56         super();
57         setHasStableIds(true);
58         differ = new AsyncListDiffer<>(this, new DiffUtil.ItemCallback<PatternDetailsItem>() {
59             @Override
60             public boolean areItemsTheSame(@NonNull PatternDetailsItem oldItem,
61                                            @NonNull PatternDetailsItem newItem) {
62                 if (oldItem.getType() != newItem.getType())
63                     return false;
64                 if (oldItem.getType()
65                            .equals(PatternDetailsItem.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 PatternDetailsItem oldItem,
74                                               @NonNull PatternDetailsItem newItem) {
75                 if (oldItem.getType()
76                            .equals(PatternDetailsItem.Type.HEADER))
77                 {
78                     PatternDetailsItem.Header oldHeader = oldItem.asHeaderItem();
79                     PatternDetailsItem.Header newHeader = newItem.asHeaderItem();
80
81                     return oldHeader.equalContents(newHeader);
82                 }
83                 else {
84                     PatternDetailsItem.AccountRow oldAcc = oldItem.asAccountRowItem();
85                     PatternDetailsItem.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         PatternDetailsItem.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 PatternDetailsAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
113                                                                int viewType) {
114         final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
115         switch (viewType) {
116             case PatternDetailsItem.TYPE.header:
117                 return new Header(PatternDetailsHeaderBinding.inflate(inflater, parent, false));
118             case PatternDetailsItem.TYPE.accountItem:
119                 return new AccountRow(
120                         PatternDetailsAccountBinding.inflate(inflater, parent, false));
121             default:
122                 throw new IllegalStateException("Unsupported view type " + viewType);
123         }
124     }
125     @Override
126     public void onBindViewHolder(@NonNull PatternDetailsAdapter.ViewHolder holder, int position) {
127         PatternDetailsItem 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 setPatternItems(List<PatternBase> items) {
137         ArrayList<PatternDetailsItem> list = new ArrayList<>();
138         for (PatternBase p : items) {
139             PatternDetailsItem item = PatternDetailsItem.fromRoomObject(p);
140             list.add(item);
141         }
142         setItems(list);
143     }
144     public void setItems(List<PatternDetailsItem> items) {
145         differ.submitList(items);
146     }
147     public String getMatchGroupText(int groupNumber) {
148         PatternDetailsItem.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 PatternDetailsItem.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(PatternDetailsItem item);
175     }
176
177     public class Header extends ViewHolder {
178         private final PatternDetailsHeaderBinding b;
179         public Header(@NonNull PatternDetailsHeaderBinding binding) {
180             super(binding.getRoot());
181             b = binding;
182
183             TextWatcher patternNameWatcher = 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 PatternDetailsItem.Header header = getItem();
191                     Logger.debug(D_PATTERN_UI,
192                             "Storing changed pattern name " + s + "; header=" + header);
193                     header.setName(String.valueOf(s));
194                 }
195             };
196             b.patternName.addTextChangedListener(patternNameWatcher);
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 PatternDetailsItem.Header header = getItem();
205                     Logger.debug(D_PATTERN_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 PatternDetailsItem.Header header = getItem();
219                     Logger.debug(D_PATTERN_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 PatternDetailsItem.Header header = getItem();
236                     Logger.debug(D_PATTERN_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 PatternDetailsItem.Header header = getItem();
254                     Logger.debug(D_PATTERN_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 PatternDetailsItem.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             PatternDetailsItem.Header header = getItem();
270             Logger.debug(D_PATTERN_UI, "header is " + header);
271             PatternDetailSourceSelectorFragment sel =
272                     PatternDetailSourceSelectorFragment.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(), "pattern-details-source-selector");
322         }
323         @Override
324         void bind(PatternDetailsItem item) {
325             PatternDetailsItem.Header header = item.asHeaderItem();
326             Logger.debug(D_PATTERN_UI, "Binding to header " + header);
327
328             b.patternName.setText(header.getName());
329             b.pattern.setText(header.getPattern());
330             b.testText.setText(header.getTestText());
331
332             if (header.hasLiteralDateYear()) {
333                 b.patternDetailsYearSource.setText(R.string.pattern_details_source_literal);
334                 final Integer dateYear = header.getDateYear();
335                 b.patternDetailsDateYear.setText(
336                         (dateYear == null) ? null : String.valueOf(dateYear));
337                 b.patternDetailsDateYearLayout.setVisibility(View.VISIBLE);
338             }
339             else {
340                 b.patternDetailsDateYearLayout.setVisibility(View.GONE);
341                 b.patternDetailsYearSource.setText(
342                         String.format(Locale.US, "Group %d (%s)", header.getDateYearMatchGroup(),
343                                 getMatchGroupText(header.getDateYearMatchGroup())));
344             }
345             b.patternDetailsYearSourceLabel.setOnClickListener(
346                     v -> selectHeaderDetailSource(v, HeaderDetail.DATE_YEAR));
347             b.patternDetailsYearSource.setOnClickListener(
348                     v -> selectHeaderDetailSource(v, HeaderDetail.DATE_YEAR));
349
350             if (header.hasLiteralDateMonth()) {
351                 b.patternDetailsMonthSource.setText(R.string.pattern_details_source_literal);
352                 final Integer dateMonth = header.getDateMonth();
353                 b.patternDetailsDateMonth.setText(
354                         (dateMonth == null) ? null : String.valueOf(dateMonth));
355                 b.patternDetailsDateMonthLayout.setVisibility(View.VISIBLE);
356             }
357             else {
358                 b.patternDetailsDateMonthLayout.setVisibility(View.GONE);
359                 b.patternDetailsMonthSource.setText(
360                         String.format(Locale.US, "Group %d (%s)", header.getDateMonthMatchGroup(),
361                                 getMatchGroupText(header.getDateMonthMatchGroup())));
362             }
363             b.patternDetailsMonthSourceLabel.setOnClickListener(
364                     v -> selectHeaderDetailSource(v, HeaderDetail.DATE_MONTH));
365             b.patternDetailsMonthSource.setOnClickListener(
366                     v -> selectHeaderDetailSource(v, HeaderDetail.DATE_MONTH));
367
368             if (header.hasLiteralDateDay()) {
369                 b.patternDetailsDaySource.setText(R.string.pattern_details_source_literal);
370                 final Integer dateDay = header.getDateDay();
371                 b.patternDetailsDateDay.setText((dateDay == null) ? null : String.valueOf(dateDay));
372                 b.patternDetailsDateDayLayout.setVisibility(View.VISIBLE);
373             }
374             else {
375                 b.patternDetailsDateDayLayout.setVisibility(View.GONE);
376                 b.patternDetailsDaySource.setText(
377                         String.format(Locale.US, "Group %d (%s)", header.getDateDayMatchGroup(),
378                                 getMatchGroupText(header.getDateDayMatchGroup())));
379             }
380             b.patternDetailsDaySourceLabel.setOnClickListener(
381                     v -> selectHeaderDetailSource(v, HeaderDetail.DATE_DAY));
382             b.patternDetailsDaySource.setOnClickListener(
383                     v -> selectHeaderDetailSource(v, HeaderDetail.DATE_DAY));
384
385             if (header.hasLiteralTransactionDescription()) {
386                 b.patternTransactionDescriptionSource.setText(
387                         R.string.pattern_details_source_literal);
388                 b.transactionDescription.setText(header.getTransactionDescription());
389                 b.transactionDescriptionLayout.setVisibility(View.VISIBLE);
390             }
391             else {
392                 b.transactionDescriptionLayout.setVisibility(View.GONE);
393                 b.patternTransactionDescriptionSource.setText(
394                         String.format(Locale.US, "Group %d (%s)",
395                                 header.getTransactionDescriptionMatchGroup(),
396                                 getMatchGroupText(header.getTransactionDescriptionMatchGroup())));
397
398             }
399             b.patternTransactionDescriptionSourceLabel.setOnClickListener(
400                     v -> selectHeaderDetailSource(v, HeaderDetail.DESCRIPTION));
401             b.patternTransactionDescriptionSource.setOnClickListener(
402                     v -> selectHeaderDetailSource(v, HeaderDetail.DESCRIPTION));
403
404             if (header.hasLiteralTransactionComment()) {
405                 b.patternTransactionCommentSource.setText(R.string.pattern_details_source_literal);
406                 b.transactionComment.setText(header.getTransactionComment());
407                 b.transactionCommentLayout.setVisibility(View.VISIBLE);
408             }
409             else {
410                 b.transactionCommentLayout.setVisibility(View.GONE);
411                 b.patternTransactionCommentSource.setText(String.format(Locale.US, "Group %d (%s)",
412                         header.getTransactionCommentMatchGroup(),
413                         getMatchGroupText(header.getTransactionCommentMatchGroup())));
414
415             }
416             b.patternTransactionCommentSourceLabel.setOnClickListener(
417                     v -> selectHeaderDetailSource(v, HeaderDetail.COMMENT));
418             b.patternTransactionCommentSource.setOnClickListener(
419                     v -> selectHeaderDetailSource(v, HeaderDetail.COMMENT));
420
421             b.patternDetailsHeadScanQrButton.setOnClickListener(this::scanTestQR);
422
423         }
424         private void scanTestQR(View view) {
425             QRScanCapableFragment.triggerQRScan();
426         }
427     }
428
429     public class AccountRow extends ViewHolder {
430         private final PatternDetailsAccountBinding b;
431         public AccountRow(@NonNull PatternDetailsAccountBinding binding) {
432             super(binding.getRoot());
433             b = binding;
434
435             TextWatcher accountNameWatcher = new TextWatcher() {
436                 @Override
437                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
438                 @Override
439                 public void onTextChanged(CharSequence s, int start, int before, int count) {}
440                 @Override
441                 public void afterTextChanged(Editable s) {
442                     PatternDetailsItem.AccountRow accRow = getItem();
443                     Logger.debug(D_PATTERN_UI,
444                             "Storing changed account name " + s + "; accRow=" + accRow);
445                     accRow.setAccountName(String.valueOf(s));
446                 }
447             };
448             b.patternDetailsAccountName.addTextChangedListener(accountNameWatcher);
449             TextWatcher accountCommentWatcher = new TextWatcher() {
450                 @Override
451                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
452                 @Override
453                 public void onTextChanged(CharSequence s, int start, int before, int count) {}
454                 @Override
455                 public void afterTextChanged(Editable s) {
456                     PatternDetailsItem.AccountRow accRow = getItem();
457                     Logger.debug(D_PATTERN_UI,
458                             "Storing changed account comment " + s + "; accRow=" + accRow);
459                     accRow.setAccountComment(String.valueOf(s));
460                 }
461             };
462             b.patternDetailsAccountComment.addTextChangedListener(accountCommentWatcher);
463
464             b.patternDetailsAccountAmount.addTextChangedListener(new TextWatcher() {
465                 @Override
466                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
467
468                 }
469                 @Override
470                 public void onTextChanged(CharSequence s, int start, int before, int count) {
471
472                 }
473                 @Override
474                 public void afterTextChanged(Editable s) {
475                     PatternDetailsItem.AccountRow accRow = getItem();
476
477                     String str = String.valueOf(s);
478                     if (Misc.emptyIsNull(str) == null) {
479                         accRow.setAmount(null);
480                     }
481                     else {
482                         try {
483                             final float amount = Data.parseNumber(str);
484                             accRow.setAmount(amount);
485                             b.patternDetailsAccountAmountLayout.setError(null);
486
487                             Logger.debug(D_PATTERN_UI, String.format(Locale.US,
488                                     "Storing changed account amount %s [%4.2f]; accRow=%s", s,
489                                     amount, accRow));
490                         }
491                         catch (NumberFormatException | ParseException e) {
492                             b.patternDetailsAccountAmountLayout.setError("!");
493                         }
494                     }
495                 }
496             });
497             b.patternDetailsAccountAmount.setOnFocusChangeListener((v, hasFocus) -> {
498                 if (hasFocus)
499                     return;
500
501                 PatternDetailsItem.AccountRow accRow = getItem();
502                 if (!accRow.hasLiteralAmount())
503                     return;
504                 Float amt = accRow.getAmount();
505                 if (amt == null)
506                     return;
507
508                 b.patternDetailsAccountAmount.setText(Data.formatNumber(amt));
509             });
510
511             b.negateAmountSwitch.setOnCheckedChangeListener(
512                     (buttonView, isChecked) -> getItem().setNegateAmount(isChecked));
513         }
514         @Override
515         void bind(PatternDetailsItem item) {
516             PatternDetailsItem.AccountRow accRow = item.asAccountRowItem();
517             if (accRow.hasLiteralAccountName()) {
518                 b.patternDetailsAccountNameLayout.setVisibility(View.VISIBLE);
519                 b.patternDetailsAccountName.setText(accRow.getAccountName());
520                 b.patternDetailsAccountNameSource.setText(R.string.pattern_details_source_literal);
521             }
522             else {
523                 b.patternDetailsAccountNameLayout.setVisibility(View.GONE);
524                 b.patternDetailsAccountNameSource.setText(
525                         String.format(Locale.US, "Group %d (%s)", accRow.getAccountNameMatchGroup(),
526                                 getMatchGroupText(accRow.getAccountNameMatchGroup())));
527             }
528
529             if (accRow.hasLiteralAccountComment()) {
530                 b.patternDetailsAccountCommentLayout.setVisibility(View.VISIBLE);
531                 b.patternDetailsAccountComment.setText(accRow.getAccountComment());
532                 b.patternDetailsAccountCommentSource.setText(
533                         R.string.pattern_details_source_literal);
534             }
535             else {
536                 b.patternDetailsAccountCommentLayout.setVisibility(View.GONE);
537                 b.patternDetailsAccountCommentSource.setText(
538                         String.format(Locale.US, "Group %d (%s)",
539                                 accRow.getAccountCommentMatchGroup(),
540                                 getMatchGroupText(accRow.getAccountCommentMatchGroup())));
541             }
542
543             if (accRow.hasLiteralAmount()) {
544                 b.patternDetailsAccountAmountSource.setText(
545                         R.string.pattern_details_source_literal);
546                 b.patternDetailsAccountAmount.setVisibility(View.VISIBLE);
547                 Float amt = accRow.getAmount();
548                 b.patternDetailsAccountAmount.setText((amt == null) ? null : String.format(
549                         Data.locale.getValue(), "%,4.2f", (accRow.getAmount())));
550                 b.negateAmountSwitch.setVisibility(View.GONE);
551             }
552             else {
553                 b.patternDetailsAccountAmountSource.setText(
554                         String.format(Locale.US, "Group %d (%s)", accRow.getAmountMatchGroup(),
555                                 getMatchGroupText(accRow.getAmountMatchGroup())));
556                 b.patternDetailsAccountAmountLayout.setVisibility(View.GONE);
557                 b.negateAmountSwitch.setVisibility(View.VISIBLE);
558                 b.negateAmountSwitch.setChecked(accRow.isNegateAmount());
559             }
560
561             b.patternAccountNameSourceLabel.setOnClickListener(
562                     v -> selectAccountRowDetailSource(v, AccDetail.ACCOUNT));
563             b.patternDetailsAccountNameSource.setOnClickListener(
564                     v -> selectAccountRowDetailSource(v, AccDetail.ACCOUNT));
565             b.patternAccountCommentSourceLabel.setOnClickListener(
566                     v -> selectAccountRowDetailSource(v, AccDetail.COMMENT));
567             b.patternDetailsAccountCommentSource.setOnClickListener(
568                     v -> selectAccountRowDetailSource(v, AccDetail.COMMENT));
569             b.patternAccountAmountSourceLabel.setOnClickListener(
570                     v -> selectAccountRowDetailSource(v, AccDetail.AMOUNT));
571             b.patternDetailsAccountAmountSource.setOnClickListener(
572                     v -> selectAccountRowDetailSource(v, AccDetail.AMOUNT));
573         }
574         private @NotNull PatternDetailsItem.AccountRow getItem() {
575             return differ.getCurrentList()
576                          .get(getAdapterPosition())
577                          .asAccountRowItem();
578         }
579         private void selectAccountRowDetailSource(View v, AccDetail detail) {
580             PatternDetailsItem.AccountRow accRow = getItem();
581             final PatternDetailsItem.Header header = getHeader();
582             Logger.debug(D_PATTERN_UI, "header is " + header);
583             PatternDetailSourceSelectorFragment sel =
584                     PatternDetailSourceSelectorFragment.newInstance(1, header.getPattern(),
585                             header.getTestText());
586             sel.setOnSourceSelectedListener((literal, group) -> {
587                 if (literal) {
588                     switch (detail) {
589                         case ACCOUNT:
590                             accRow.switchToLiteralAccountName();
591                             break;
592                         case COMMENT:
593                             accRow.switchToLiteralAccountComment();
594                             break;
595                         case AMOUNT:
596                             accRow.switchToLiteralAmount();
597                             break;
598                         default:
599                             throw new IllegalStateException("Unexpected detail " + detail);
600                     }
601                 }
602                 else {
603                     switch (detail) {
604                         case ACCOUNT:
605                             accRow.setAccountNameMatchGroup(group);
606                             break;
607                         case COMMENT:
608                             accRow.setAccountCommentMatchGroup(group);
609                             break;
610                         case AMOUNT:
611                             accRow.setAmountMatchGroup(group);
612                             break;
613                         default:
614                             throw new IllegalStateException("Unexpected detail " + detail);
615                     }
616                 }
617
618                 notifyItemChanged(getAdapterPosition());
619             });
620             final AppCompatActivity activity = (AppCompatActivity) v.getContext();
621             sel.show(activity.getSupportFragmentManager(), "pattern-details-source-selector");
622         }
623     }
624 }