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