]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateDetailsAdapter.java
80c6df4948e4741925a917f8dff8cc6e6339412c
[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
200             TextWatcher patternWatcher = new TextWatcher() {
201                 @Override
202                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
203                 @Override
204                 public void onTextChanged(CharSequence s, int start, int before, int count) {}
205                 @Override
206                 public void afterTextChanged(Editable s) {
207                     final TemplateDetailsItem.Header header = getItem();
208                     Logger.debug(D_TEMPLATE_UI,
209                             "Storing changed pattern " + s + "; header=" + header);
210                     header.setPattern(String.valueOf(s));
211
212                     checkPatternError(header);
213                 }
214             };
215             b.pattern.addTextChangedListener(patternWatcher);
216
217             TextWatcher testTextWatcher = new TextWatcher() {
218                 @Override
219                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
220                 @Override
221                 public void onTextChanged(CharSequence s, int start, int before, int count) {}
222                 @Override
223                 public void afterTextChanged(Editable s) {
224                     final TemplateDetailsItem.Header header = getItem();
225                     Logger.debug(D_TEMPLATE_UI,
226                             "Storing changed test text " + s + "; header=" + header);
227                     header.setTestText(String.valueOf(s));
228
229                     checkPatternError(header);
230                 }
231             };
232             b.testText.addTextChangedListener(testTextWatcher);
233
234             TextWatcher transactionDescriptionWatcher = new TextWatcher() {
235                 @Override
236                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
237                 }
238                 @Override
239                 public void onTextChanged(CharSequence s, int start, int before, int count) {
240
241                 }
242                 @Override
243                 public void afterTextChanged(Editable s) {
244                     final TemplateDetailsItem.Header header = getItem();
245                     Logger.debug(D_TEMPLATE_UI,
246                             "Storing changed transaction description " + s + "; header=" + header);
247                     header.setTransactionDescription(String.valueOf(s));
248                 }
249             };
250             b.transactionDescription.addTextChangedListener(transactionDescriptionWatcher);
251             TextWatcher transactionCommentWatcher = new TextWatcher() {
252                 @Override
253                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
254
255                 }
256                 @Override
257                 public void onTextChanged(CharSequence s, int start, int before, int count) {
258
259                 }
260                 @Override
261                 public void afterTextChanged(Editable s) {
262                     final TemplateDetailsItem.Header header = getItem();
263                     Logger.debug(D_TEMPLATE_UI,
264                             "Storing changed transaction description " + s + "; header=" + header);
265                     header.setTransactionComment(String.valueOf(s));
266                 }
267             };
268             b.transactionComment.addTextChangedListener(transactionCommentWatcher);
269         }
270         @NotNull
271         private TemplateDetailsItem.Header getItem() {
272             int pos = getAdapterPosition();
273             return differ.getCurrentList()
274                          .get(pos)
275                          .asHeaderItem();
276         }
277         private void selectHeaderDetailSource(View v, HeaderDetail detail) {
278             TemplateDetailsItem.Header header = getItem();
279             Logger.debug(D_TEMPLATE_UI, "header is " + header);
280             TemplateDetailSourceSelectorFragment sel =
281                     TemplateDetailSourceSelectorFragment.newInstance(1, header.getPattern(),
282                             header.getTestText());
283             sel.setOnSourceSelectedListener((literal, group) -> {
284                 if (literal) {
285                     switch (detail) {
286                         case DESCRIPTION:
287                             header.switchToLiteralTransactionDescription();
288                             break;
289                         case COMMENT:
290                             header.switchToLiteralTransactionComment();
291                             break;
292                         case DATE_YEAR:
293                             header.switchToLiteralDateYear();
294                             break;
295                         case DATE_MONTH:
296                             header.switchToLiteralDateMonth();
297                             break;
298                         case DATE_DAY:
299                             header.switchToLiteralDateDay();
300                             break;
301                         default:
302                             throw new IllegalStateException("Unexpected detail " + detail);
303                     }
304                 }
305                 else {
306                     switch (detail) {
307                         case DESCRIPTION:
308                             header.setTransactionDescriptionMatchGroup(group);
309                             break;
310                         case COMMENT:
311                             header.setTransactionCommentMatchGroup(group);
312                             break;
313                         case DATE_YEAR:
314                             header.setDateYearMatchGroup(group);
315                             break;
316                         case DATE_MONTH:
317                             header.setDateMonthMatchGroup(group);
318                             break;
319                         case DATE_DAY:
320                             header.setDateDayMatchGroup(group);
321                             break;
322                         default:
323                             throw new IllegalStateException("Unexpected detail " + detail);
324                     }
325                 }
326
327                 notifyItemChanged(getAdapterPosition());
328             });
329             final AppCompatActivity activity = (AppCompatActivity) v.getContext();
330             sel.show(activity.getSupportFragmentManager(), "template-details-source-selector");
331         }
332         @Override
333         void bind(TemplateDetailsItem item) {
334             TemplateDetailsItem.Header header = item.asHeaderItem();
335             Logger.debug(D_TEMPLATE_UI, "Binding to header " + header);
336
337             String groupNoText = b.getRoot()
338                                   .getResources()
339                                   .getString(R.string.template_item_match_group_source);
340
341             b.templateName.setText(header.getName());
342             b.pattern.setText(header.getPattern());
343             b.testText.setText(header.getTestText());
344
345             if (header.hasLiteralDateYear()) {
346                 b.templateDetailsYearSource.setText(R.string.template_details_source_literal);
347                 final Integer dateYear = header.getDateYear();
348                 b.templateDetailsDateYear.setText(
349                         (dateYear == null) ? null : String.valueOf(dateYear));
350                 b.templateDetailsDateYearLayout.setVisibility(View.VISIBLE);
351             }
352             else {
353                 b.templateDetailsDateYearLayout.setVisibility(View.GONE);
354                 b.templateDetailsYearSource.setText(
355                         String.format(Locale.US, groupNoText, header.getDateYearMatchGroup(),
356                                 getMatchGroupText(header.getDateYearMatchGroup())));
357             }
358             b.templateDetailsYearSourceLabel.setOnClickListener(
359                     v -> selectHeaderDetailSource(v, HeaderDetail.DATE_YEAR));
360             b.templateDetailsYearSource.setOnClickListener(
361                     v -> selectHeaderDetailSource(v, HeaderDetail.DATE_YEAR));
362
363             if (header.hasLiteralDateMonth()) {
364                 b.templateDetailsMonthSource.setText(R.string.template_details_source_literal);
365                 final Integer dateMonth = header.getDateMonth();
366                 b.templateDetailsDateMonth.setText(
367                         (dateMonth == null) ? null : String.valueOf(dateMonth));
368                 b.templateDetailsDateMonthLayout.setVisibility(View.VISIBLE);
369             }
370             else {
371                 b.templateDetailsDateMonthLayout.setVisibility(View.GONE);
372                 b.templateDetailsMonthSource.setText(
373                         String.format(Locale.US, groupNoText, header.getDateMonthMatchGroup(),
374                                 getMatchGroupText(header.getDateMonthMatchGroup())));
375             }
376             b.templateDetailsMonthSourceLabel.setOnClickListener(
377                     v -> selectHeaderDetailSource(v, HeaderDetail.DATE_MONTH));
378             b.templateDetailsMonthSource.setOnClickListener(
379                     v -> selectHeaderDetailSource(v, HeaderDetail.DATE_MONTH));
380
381             if (header.hasLiteralDateDay()) {
382                 b.templateDetailsDaySource.setText(R.string.template_details_source_literal);
383                 final Integer dateDay = header.getDateDay();
384                 b.templateDetailsDateDay.setText(
385                         (dateDay == null) ? null : String.valueOf(dateDay));
386                 b.templateDetailsDateDayLayout.setVisibility(View.VISIBLE);
387             }
388             else {
389                 b.templateDetailsDateDayLayout.setVisibility(View.GONE);
390                 b.templateDetailsDaySource.setText(
391                         String.format(Locale.US, groupNoText, header.getDateDayMatchGroup(),
392                                 getMatchGroupText(header.getDateDayMatchGroup())));
393             }
394             b.templateDetailsDaySourceLabel.setOnClickListener(
395                     v -> selectHeaderDetailSource(v, HeaderDetail.DATE_DAY));
396             b.templateDetailsDaySource.setOnClickListener(
397                     v -> selectHeaderDetailSource(v, HeaderDetail.DATE_DAY));
398
399             if (header.hasLiteralTransactionDescription()) {
400                 b.templateTransactionDescriptionSource.setText(
401                         R.string.template_details_source_literal);
402                 b.transactionDescription.setText(header.getTransactionDescription());
403                 b.transactionDescriptionLayout.setVisibility(View.VISIBLE);
404             }
405             else {
406                 b.transactionDescriptionLayout.setVisibility(View.GONE);
407                 b.templateTransactionDescriptionSource.setText(String.format(Locale.US, groupNoText,
408                         header.getTransactionDescriptionMatchGroup(),
409                         getMatchGroupText(header.getTransactionDescriptionMatchGroup())));
410
411             }
412             b.templateTransactionDescriptionSourceLabel.setOnClickListener(
413                     v -> selectHeaderDetailSource(v, HeaderDetail.DESCRIPTION));
414             b.templateTransactionDescriptionSource.setOnClickListener(
415                     v -> selectHeaderDetailSource(v, HeaderDetail.DESCRIPTION));
416
417             if (header.hasLiteralTransactionComment()) {
418                 b.templateTransactionCommentSource.setText(
419                         R.string.template_details_source_literal);
420                 b.transactionComment.setText(header.getTransactionComment());
421                 b.transactionCommentLayout.setVisibility(View.VISIBLE);
422             }
423             else {
424                 b.transactionCommentLayout.setVisibility(View.GONE);
425                 b.templateTransactionCommentSource.setText(String.format(Locale.US, groupNoText,
426                         header.getTransactionCommentMatchGroup(),
427                         getMatchGroupText(header.getTransactionCommentMatchGroup())));
428
429             }
430             b.templateTransactionCommentSourceLabel.setOnClickListener(
431                     v -> selectHeaderDetailSource(v, HeaderDetail.COMMENT));
432             b.templateTransactionCommentSource.setOnClickListener(
433                     v -> selectHeaderDetailSource(v, HeaderDetail.COMMENT));
434
435             b.templateDetailsHeadScanQrButton.setOnClickListener(this::scanTestQR);
436
437             checkPatternError(header);
438         }
439         private void checkPatternError(TemplateDetailsItem.Header item) {
440             if (item.getPatternError() != null) {
441                 b.patternLayout.setError(item.getPatternError());
442                 b.patternHintTitle.setVisibility(View.GONE);
443                 b.patternHintText.setVisibility(View.GONE);
444             }
445             else {
446                 b.patternLayout.setError(null);
447                 if (item.testMatch() != null) {
448                     b.patternHintText.setText(item.testMatch());
449                     b.patternHintTitle.setVisibility(View.VISIBLE);
450                     b.patternHintText.setVisibility(View.VISIBLE);
451                 }
452                 else {
453                     b.patternLayout.setError(null);
454                     b.patternHintTitle.setVisibility(View.GONE);
455                     b.patternHintText.setVisibility(View.GONE);
456                 }
457             }
458
459         }
460         private void scanTestQR(View view) {
461             QRScanCapableFragment.triggerQRScan();
462         }
463     }
464
465     public class AccountRow extends ViewHolder {
466         private final TemplateDetailsAccountBinding b;
467         public AccountRow(@NonNull TemplateDetailsAccountBinding binding) {
468             super(binding.getRoot());
469             b = binding;
470
471             TextWatcher accountNameWatcher = new TextWatcher() {
472                 @Override
473                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
474                 @Override
475                 public void onTextChanged(CharSequence s, int start, int before, int count) {}
476                 @Override
477                 public void afterTextChanged(Editable s) {
478                     TemplateDetailsItem.AccountRow accRow = getItem();
479                     Logger.debug(D_TEMPLATE_UI,
480                             "Storing changed account name " + s + "; accRow=" + accRow);
481                     accRow.setAccountName(String.valueOf(s));
482                 }
483             };
484             b.templateDetailsAccountName.addTextChangedListener(accountNameWatcher);
485             b.templateDetailsAccountName.setAdapter(new AccountAutocompleteAdapter(b.getRoot()
486                                                                                     .getContext()));
487             b.templateDetailsAccountName.setOnItemClickListener(
488                     (parent, view, position, id) -> b.templateDetailsAccountName.setText(
489                             ((TextView) view).getText()));
490             TextWatcher accountCommentWatcher = new TextWatcher() {
491                 @Override
492                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
493                 @Override
494                 public void onTextChanged(CharSequence s, int start, int before, int count) {}
495                 @Override
496                 public void afterTextChanged(Editable s) {
497                     TemplateDetailsItem.AccountRow accRow = getItem();
498                     Logger.debug(D_TEMPLATE_UI,
499                             "Storing changed account comment " + s + "; accRow=" + accRow);
500                     accRow.setAccountComment(String.valueOf(s));
501                 }
502             };
503             b.templateDetailsAccountComment.addTextChangedListener(accountCommentWatcher);
504
505             b.templateDetailsAccountAmount.addTextChangedListener(new TextWatcher() {
506                 @Override
507                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
508
509                 }
510                 @Override
511                 public void onTextChanged(CharSequence s, int start, int before, int count) {
512
513                 }
514                 @Override
515                 public void afterTextChanged(Editable s) {
516                     TemplateDetailsItem.AccountRow accRow = getItem();
517
518                     String str = String.valueOf(s);
519                     if (Misc.emptyIsNull(str) == null) {
520                         accRow.setAmount(null);
521                     }
522                     else {
523                         try {
524                             final float amount = Data.parseNumber(str);
525                             accRow.setAmount(amount);
526                             b.templateDetailsAccountAmountLayout.setError(null);
527
528                             Logger.debug(D_TEMPLATE_UI, String.format(Locale.US,
529                                     "Storing changed account amount %s [%4.2f]; accRow=%s", s,
530                                     amount, accRow));
531                         }
532                         catch (NumberFormatException | ParseException e) {
533                             b.templateDetailsAccountAmountLayout.setError("!");
534                         }
535                     }
536                 }
537             });
538             b.templateDetailsAccountAmount.setOnFocusChangeListener((v, hasFocus) -> {
539                 if (hasFocus)
540                     return;
541
542                 TemplateDetailsItem.AccountRow accRow = getItem();
543                 if (!accRow.hasLiteralAmount())
544                     return;
545                 Float amt = accRow.getAmount();
546                 if (amt == null)
547                     return;
548
549                 b.templateDetailsAccountAmount.setText(Data.formatNumber(amt));
550             });
551
552             b.negateAmountSwitch.setOnCheckedChangeListener(
553                     (buttonView, isChecked) -> getItem().setNegateAmount(isChecked));
554         }
555         @Override
556         void bind(TemplateDetailsItem item) {
557             String groupNoText = b.getRoot()
558                                   .getResources()
559                                   .getString(R.string.template_item_match_group_source);
560
561             TemplateDetailsItem.AccountRow accRow = item.asAccountRowItem();
562             if (accRow.hasLiteralAccountName()) {
563                 b.templateDetailsAccountNameLayout.setVisibility(View.VISIBLE);
564                 b.templateDetailsAccountName.setText(accRow.getAccountName());
565                 b.templateDetailsAccountNameSource.setText(
566                         R.string.template_details_source_literal);
567             }
568             else {
569                 b.templateDetailsAccountNameLayout.setVisibility(View.GONE);
570                 b.templateDetailsAccountNameSource.setText(
571                         String.format(Locale.US, groupNoText, accRow.getAccountNameMatchGroup(),
572                                 getMatchGroupText(accRow.getAccountNameMatchGroup())));
573             }
574
575             if (accRow.hasLiteralAccountComment()) {
576                 b.templateDetailsAccountCommentLayout.setVisibility(View.VISIBLE);
577                 b.templateDetailsAccountComment.setText(accRow.getAccountComment());
578                 b.templateDetailsAccountCommentSource.setText(
579                         R.string.template_details_source_literal);
580             }
581             else {
582                 b.templateDetailsAccountCommentLayout.setVisibility(View.GONE);
583                 b.templateDetailsAccountCommentSource.setText(
584                         String.format(Locale.US, groupNoText, accRow.getAccountCommentMatchGroup(),
585                                 getMatchGroupText(accRow.getAccountCommentMatchGroup())));
586             }
587
588             if (accRow.hasLiteralAmount()) {
589                 b.templateDetailsAccountAmountSource.setText(
590                         R.string.template_details_source_literal);
591                 b.templateDetailsAccountAmount.setVisibility(View.VISIBLE);
592                 Float amt = accRow.getAmount();
593                 b.templateDetailsAccountAmount.setText((amt == null) ? null : String.format(
594                         Data.locale.getValue(), "%,4.2f", (accRow.getAmount())));
595                 b.negateAmountSwitch.setVisibility(View.GONE);
596             }
597             else {
598                 b.templateDetailsAccountAmountSource.setText(
599                         String.format(Locale.US, groupNoText, accRow.getAmountMatchGroup(),
600                                 getMatchGroupText(accRow.getAmountMatchGroup())));
601                 b.templateDetailsAccountAmountLayout.setVisibility(View.GONE);
602                 b.negateAmountSwitch.setVisibility(View.VISIBLE);
603                 b.negateAmountSwitch.setChecked(accRow.isNegateAmount());
604             }
605
606             b.templateAccountNameSourceLabel.setOnClickListener(
607                     v -> selectAccountRowDetailSource(v, AccDetail.ACCOUNT));
608             b.templateDetailsAccountNameSource.setOnClickListener(
609                     v -> selectAccountRowDetailSource(v, AccDetail.ACCOUNT));
610             b.templateAccountCommentSourceLabel.setOnClickListener(
611                     v -> selectAccountRowDetailSource(v, AccDetail.COMMENT));
612             b.templateDetailsAccountCommentSource.setOnClickListener(
613                     v -> selectAccountRowDetailSource(v, AccDetail.COMMENT));
614             b.templateAccountAmountSourceLabel.setOnClickListener(
615                     v -> selectAccountRowDetailSource(v, AccDetail.AMOUNT));
616             b.templateDetailsAccountAmountSource.setOnClickListener(
617                     v -> selectAccountRowDetailSource(v, AccDetail.AMOUNT));
618         }
619         private @NotNull TemplateDetailsItem.AccountRow getItem() {
620             return differ.getCurrentList()
621                          .get(getAdapterPosition())
622                          .asAccountRowItem();
623         }
624         private void selectAccountRowDetailSource(View v, AccDetail detail) {
625             TemplateDetailsItem.AccountRow accRow = getItem();
626             final TemplateDetailsItem.Header header = getHeader();
627             Logger.debug(D_TEMPLATE_UI, "header is " + header);
628             TemplateDetailSourceSelectorFragment sel =
629                     TemplateDetailSourceSelectorFragment.newInstance(1, header.getPattern(),
630                             header.getTestText());
631             sel.setOnSourceSelectedListener((literal, group) -> {
632                 if (literal) {
633                     switch (detail) {
634                         case ACCOUNT:
635                             accRow.switchToLiteralAccountName();
636                             break;
637                         case COMMENT:
638                             accRow.switchToLiteralAccountComment();
639                             break;
640                         case AMOUNT:
641                             accRow.switchToLiteralAmount();
642                             break;
643                         default:
644                             throw new IllegalStateException("Unexpected detail " + detail);
645                     }
646                 }
647                 else {
648                     switch (detail) {
649                         case ACCOUNT:
650                             accRow.setAccountNameMatchGroup(group);
651                             break;
652                         case COMMENT:
653                             accRow.setAccountCommentMatchGroup(group);
654                             break;
655                         case AMOUNT:
656                             accRow.setAmountMatchGroup(group);
657                             break;
658                         default:
659                             throw new IllegalStateException("Unexpected detail " + detail);
660                     }
661                 }
662
663                 notifyItemChanged(getAdapterPosition());
664             });
665             final AppCompatActivity activity = (AppCompatActivity) v.getContext();
666             sel.show(activity.getSupportFragmentManager(), "template-details-source-selector");
667         }
668     }
669 }