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