]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateDetailsAdapter.java
1f371f35766b833e73965abe6833a7a72e06c891
[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(v -> selectHeaderDetailSource(v,
377                     HeaderDetail.DATE_MONTH));
378             b.templateDetailsMonthSource.setOnClickListener(v -> selectHeaderDetailSource(v,
379                     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((dateDay == null) ? null : String.valueOf(dateDay));
385                 b.templateDetailsDateDayLayout.setVisibility(View.VISIBLE);
386             }
387             else {
388                 b.templateDetailsDateDayLayout.setVisibility(View.GONE);
389                 b.templateDetailsDaySource.setText(
390                         String.format(Locale.US, groupNoText, header.getDateDayMatchGroup(),
391                                 getMatchGroupText(header.getDateDayMatchGroup())));
392             }
393             b.templateDetailsDaySourceLabel.setOnClickListener(v -> selectHeaderDetailSource(v, HeaderDetail.DATE_DAY));
394             b.templateDetailsDaySource.setOnClickListener(v -> selectHeaderDetailSource(v, HeaderDetail.DATE_DAY));
395
396             if (header.hasLiteralTransactionDescription()) {
397                 b.templateTransactionDescriptionSource.setText(R.string.template_details_source_literal);
398                 b.transactionDescription.setText(header.getTransactionDescription());
399                 b.transactionDescriptionLayout.setVisibility(View.VISIBLE);
400             }
401             else {
402                 b.transactionDescriptionLayout.setVisibility(View.GONE);
403                 b.templateTransactionDescriptionSource.setText(String.format(Locale.US, groupNoText,
404                         header.getTransactionDescriptionMatchGroup(),
405                         getMatchGroupText(header.getTransactionDescriptionMatchGroup())));
406
407             }
408             b.templateTransactionDescriptionSourceLabel.setOnClickListener(v -> selectHeaderDetailSource(v, HeaderDetail.DESCRIPTION));
409             b.templateTransactionDescriptionSource.setOnClickListener(v -> selectHeaderDetailSource(v, HeaderDetail.DESCRIPTION));
410
411             if (header.hasLiteralTransactionComment()) {
412                 b.templateTransactionCommentSource.setText(R.string.template_details_source_literal);
413                 b.transactionComment.setText(header.getTransactionComment());
414                 b.transactionCommentLayout.setVisibility(View.VISIBLE);
415             }
416             else {
417                 b.transactionCommentLayout.setVisibility(View.GONE);
418                 b.templateTransactionCommentSource.setText(String.format(Locale.US, groupNoText,
419                         header.getTransactionCommentMatchGroup(),
420                         getMatchGroupText(header.getTransactionCommentMatchGroup())));
421
422             }
423             b.templateTransactionCommentSourceLabel.setOnClickListener(
424                     v -> selectHeaderDetailSource(v, HeaderDetail.COMMENT));
425             b.templateTransactionCommentSource.setOnClickListener(
426                     v -> selectHeaderDetailSource(v, HeaderDetail.COMMENT));
427
428             b.templateDetailsHeadScanQrButton.setOnClickListener(this::scanTestQR);
429
430             checkPatternError(header);
431         }
432         private void checkPatternError(TemplateDetailsItem.Header item) {
433             if (item.getPatternError() != null) {
434                 b.patternLayout.setError(item.getPatternError());
435                 b.patternHintTitle.setVisibility(View.GONE);
436                 b.patternHintText.setVisibility(View.GONE);
437             }
438             else {
439                 b.patternLayout.setError(null);
440                 if (item.testMatch() != null) {
441                     b.patternHintText.setText(item.testMatch());
442                     b.patternHintTitle.setVisibility(View.VISIBLE);
443                     b.patternHintText.setVisibility(View.VISIBLE);
444                 }
445                 else {
446                     b.patternLayout.setError(null);
447                     b.patternHintTitle.setVisibility(View.GONE);
448                     b.patternHintText.setVisibility(View.GONE);
449                 }
450             }
451
452         }
453         private void scanTestQR(View view) {
454             QRScanCapableFragment.triggerQRScan();
455         }
456     }
457
458     public class AccountRow extends ViewHolder {
459         private final TemplateDetailsAccountBinding b;
460         public AccountRow(@NonNull TemplateDetailsAccountBinding binding) {
461             super(binding.getRoot());
462             b = binding;
463
464             TextWatcher accountNameWatcher = new TextWatcher() {
465                 @Override
466                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
467                 @Override
468                 public void onTextChanged(CharSequence s, int start, int before, int count) {}
469                 @Override
470                 public void afterTextChanged(Editable s) {
471                     TemplateDetailsItem.AccountRow accRow = getItem();
472                     Logger.debug(D_TEMPLATE_UI,
473                             "Storing changed account name " + s + "; accRow=" + accRow);
474                     accRow.setAccountName(String.valueOf(s));
475                 }
476             };
477             b.templateDetailsAccountName.addTextChangedListener(accountNameWatcher);
478             b.templateDetailsAccountName.setAdapter(new AccountAutocompleteAdapter(b.getRoot()
479                                                                                     .getContext()));
480             b.templateDetailsAccountName.setOnItemClickListener(
481                     (parent, view, position, id) -> b.templateDetailsAccountName.setText(
482                             ((TextView) view).getText()));
483             TextWatcher accountCommentWatcher = new TextWatcher() {
484                 @Override
485                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
486                 @Override
487                 public void onTextChanged(CharSequence s, int start, int before, int count) {}
488                 @Override
489                 public void afterTextChanged(Editable s) {
490                     TemplateDetailsItem.AccountRow accRow = getItem();
491                     Logger.debug(D_TEMPLATE_UI,
492                             "Storing changed account comment " + s + "; accRow=" + accRow);
493                     accRow.setAccountComment(String.valueOf(s));
494                 }
495             };
496             b.templateDetailsAccountComment.addTextChangedListener(accountCommentWatcher);
497
498             b.templateDetailsAccountAmount.addTextChangedListener(new TextWatcher() {
499                 @Override
500                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
501
502                 }
503                 @Override
504                 public void onTextChanged(CharSequence s, int start, int before, int count) {
505
506                 }
507                 @Override
508                 public void afterTextChanged(Editable s) {
509                     TemplateDetailsItem.AccountRow accRow = getItem();
510
511                     String str = String.valueOf(s);
512                     if (Misc.emptyIsNull(str) == null) {
513                         accRow.setAmount(null);
514                     }
515                     else {
516                         try {
517                             final float amount = Data.parseNumber(str);
518                             accRow.setAmount(amount);
519                             b.templateDetailsAccountAmountLayout.setError(null);
520
521                             Logger.debug(D_TEMPLATE_UI, String.format(Locale.US,
522                                     "Storing changed account amount %s [%4.2f]; accRow=%s", s,
523                                     amount, accRow));
524                         }
525                         catch (NumberFormatException | ParseException e) {
526                             b.templateDetailsAccountAmountLayout.setError("!");
527                         }
528                     }
529                 }
530             });
531             b.templateDetailsAccountAmount.setOnFocusChangeListener((v, hasFocus) -> {
532                 if (hasFocus)
533                     return;
534
535                 TemplateDetailsItem.AccountRow accRow = getItem();
536                 if (!accRow.hasLiteralAmount())
537                     return;
538                 Float amt = accRow.getAmount();
539                 if (amt == null)
540                     return;
541
542                 b.templateDetailsAccountAmount.setText(Data.formatNumber(amt));
543             });
544
545             b.negateAmountSwitch.setOnCheckedChangeListener(
546                     (buttonView, isChecked) -> getItem().setNegateAmount(isChecked));
547         }
548         @Override
549         void bind(TemplateDetailsItem item) {
550             String groupNoText = b.getRoot()
551                                   .getResources()
552                                   .getString(R.string.template_item_match_group_source);
553
554             TemplateDetailsItem.AccountRow accRow = item.asAccountRowItem();
555             if (accRow.hasLiteralAccountName()) {
556                 b.templateDetailsAccountNameLayout.setVisibility(View.VISIBLE);
557                 b.templateDetailsAccountName.setText(accRow.getAccountName());
558                 b.templateDetailsAccountNameSource.setText(
559                         R.string.template_details_source_literal);
560             }
561             else {
562                 b.templateDetailsAccountNameLayout.setVisibility(View.GONE);
563                 b.templateDetailsAccountNameSource.setText(
564                         String.format(Locale.US, groupNoText, accRow.getAccountNameMatchGroup(),
565                                 getMatchGroupText(accRow.getAccountNameMatchGroup())));
566             }
567
568             if (accRow.hasLiteralAccountComment()) {
569                 b.templateDetailsAccountCommentLayout.setVisibility(View.VISIBLE);
570                 b.templateDetailsAccountComment.setText(accRow.getAccountComment());
571                 b.templateDetailsAccountCommentSource.setText(
572                         R.string.template_details_source_literal);
573             }
574             else {
575                 b.templateDetailsAccountCommentLayout.setVisibility(View.GONE);
576                 b.templateDetailsAccountCommentSource.setText(
577                         String.format(Locale.US, groupNoText, accRow.getAccountCommentMatchGroup(),
578                                 getMatchGroupText(accRow.getAccountCommentMatchGroup())));
579             }
580
581             if (accRow.hasLiteralAmount()) {
582                 b.templateDetailsAccountAmountSource.setText(
583                         R.string.template_details_source_literal);
584                 b.templateDetailsAccountAmount.setVisibility(View.VISIBLE);
585                 Float amt = accRow.getAmount();
586                 b.templateDetailsAccountAmount.setText((amt == null) ? null : String.format(
587                         Data.locale.getValue(), "%,4.2f", (accRow.getAmount())));
588                 b.negateAmountSwitch.setVisibility(View.GONE);
589             }
590             else {
591                 b.templateDetailsAccountAmountSource.setText(
592                         String.format(Locale.US, groupNoText, accRow.getAmountMatchGroup(),
593                                 getMatchGroupText(accRow.getAmountMatchGroup())));
594                 b.templateDetailsAccountAmountLayout.setVisibility(View.GONE);
595                 b.negateAmountSwitch.setVisibility(View.VISIBLE);
596                 b.negateAmountSwitch.setChecked(accRow.isNegateAmount());
597             }
598
599             b.templateAccountNameSourceLabel.setOnClickListener(
600                     v -> selectAccountRowDetailSource(v, AccDetail.ACCOUNT));
601             b.templateDetailsAccountNameSource.setOnClickListener(
602                     v -> selectAccountRowDetailSource(v, AccDetail.ACCOUNT));
603             b.templateAccountCommentSourceLabel.setOnClickListener(
604                     v -> selectAccountRowDetailSource(v, AccDetail.COMMENT));
605             b.templateDetailsAccountCommentSource.setOnClickListener(
606                     v -> selectAccountRowDetailSource(v, AccDetail.COMMENT));
607             b.templateAccountAmountSourceLabel.setOnClickListener(
608                     v -> selectAccountRowDetailSource(v, AccDetail.AMOUNT));
609             b.templateDetailsAccountAmountSource.setOnClickListener(
610                     v -> selectAccountRowDetailSource(v, AccDetail.AMOUNT));
611         }
612         private @NotNull TemplateDetailsItem.AccountRow getItem() {
613             return differ.getCurrentList()
614                          .get(getAdapterPosition())
615                          .asAccountRowItem();
616         }
617         private void selectAccountRowDetailSource(View v, AccDetail detail) {
618             TemplateDetailsItem.AccountRow accRow = getItem();
619             final TemplateDetailsItem.Header header = getHeader();
620             Logger.debug(D_TEMPLATE_UI, "header is " + header);
621             TemplateDetailSourceSelectorFragment sel =
622                     TemplateDetailSourceSelectorFragment.newInstance(1, header.getPattern(),
623                             header.getTestText());
624             sel.setOnSourceSelectedListener((literal, group) -> {
625                 if (literal) {
626                     switch (detail) {
627                         case ACCOUNT:
628                             accRow.switchToLiteralAccountName();
629                             break;
630                         case COMMENT:
631                             accRow.switchToLiteralAccountComment();
632                             break;
633                         case AMOUNT:
634                             accRow.switchToLiteralAmount();
635                             break;
636                         default:
637                             throw new IllegalStateException("Unexpected detail " + detail);
638                     }
639                 }
640                 else {
641                     switch (detail) {
642                         case ACCOUNT:
643                             accRow.setAccountNameMatchGroup(group);
644                             break;
645                         case COMMENT:
646                             accRow.setAccountCommentMatchGroup(group);
647                             break;
648                         case AMOUNT:
649                             accRow.setAmountMatchGroup(group);
650                             break;
651                         default:
652                             throw new IllegalStateException("Unexpected detail " + detail);
653                     }
654                 }
655
656                 notifyItemChanged(getAdapterPosition());
657             });
658             final AppCompatActivity activity = (AppCompatActivity) v.getContext();
659             sel.show(activity.getSupportFragmentManager(), "template-details-source-selector");
660         }
661     }
662 }