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