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