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