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.
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.
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/>.
18 package net.ktnx.mobileledger.ui.templates;
20 import android.text.Editable;
21 import android.text.TextWatcher;
22 import android.view.LayoutInflater;
23 import android.view.View;
24 import android.view.ViewGroup;
26 import androidx.annotation.NonNull;
27 import androidx.appcompat.app.AppCompatActivity;
28 import androidx.recyclerview.widget.AsyncListDiffer;
29 import androidx.recyclerview.widget.DiffUtil;
30 import androidx.recyclerview.widget.RecyclerView;
32 import net.ktnx.mobileledger.R;
33 import net.ktnx.mobileledger.databinding.TemplateDetailsAccountBinding;
34 import net.ktnx.mobileledger.databinding.TemplateDetailsHeaderBinding;
35 import net.ktnx.mobileledger.db.TemplateBase;
36 import net.ktnx.mobileledger.model.Data;
37 import net.ktnx.mobileledger.model.TemplateDetailsItem;
38 import net.ktnx.mobileledger.ui.QRScanCapableFragment;
39 import net.ktnx.mobileledger.ui.TemplateDetailSourceSelectorFragment;
40 import net.ktnx.mobileledger.utils.Logger;
41 import net.ktnx.mobileledger.utils.Misc;
43 import org.jetbrains.annotations.NotNull;
45 import java.text.ParseException;
46 import java.util.ArrayList;
47 import java.util.List;
48 import java.util.Locale;
49 import java.util.regex.Matcher;
50 import java.util.regex.Pattern;
52 class TemplateDetailsAdapter extends RecyclerView.Adapter<TemplateDetailsAdapter.ViewHolder> {
53 private static final String D_TEMPLATE_UI = "template-ui";
54 private final AsyncListDiffer<TemplateDetailsItem> differ;
55 public TemplateDetailsAdapter() {
57 setHasStableIds(true);
58 differ = new AsyncListDiffer<>(this, new DiffUtil.ItemCallback<TemplateDetailsItem>() {
60 public boolean areItemsTheSame(@NonNull TemplateDetailsItem oldItem,
61 @NonNull TemplateDetailsItem newItem) {
62 if (oldItem.getType() != newItem.getType())
65 .equals(TemplateDetailsItem.Type.HEADER))
66 return true; // only one header item, ever
67 // the rest is comparing two account row items
68 return oldItem.asAccountRowItem()
69 .getId() == newItem.asAccountRowItem()
73 public boolean areContentsTheSame(@NonNull TemplateDetailsItem oldItem,
74 @NonNull TemplateDetailsItem newItem) {
76 .equals(TemplateDetailsItem.Type.HEADER))
78 TemplateDetailsItem.Header oldHeader = oldItem.asHeaderItem();
79 TemplateDetailsItem.Header newHeader = newItem.asHeaderItem();
81 return oldHeader.equalContents(newHeader);
84 TemplateDetailsItem.AccountRow oldAcc = oldItem.asAccountRowItem();
85 TemplateDetailsItem.AccountRow newAcc = newItem.asAccountRowItem();
87 return oldAcc.equalContents(newAcc);
93 public long getItemId(int position) {
94 // header item is always first and IDs id may duplicate some of the account IDs
97 TemplateDetailsItem.AccountRow accRow = differ.getCurrentList()
100 return accRow.getId();
103 public int getItemViewType(int position) {
105 return differ.getCurrentList()
112 public TemplateDetailsAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
114 final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
116 case TemplateDetailsItem.TYPE.header:
117 return new Header(TemplateDetailsHeaderBinding.inflate(inflater, parent, false));
118 case TemplateDetailsItem.TYPE.accountItem:
119 return new AccountRow(
120 TemplateDetailsAccountBinding.inflate(inflater, parent, false));
122 throw new IllegalStateException("Unsupported view type " + viewType);
126 public void onBindViewHolder(@NonNull TemplateDetailsAdapter.ViewHolder holder, int position) {
127 TemplateDetailsItem item = differ.getCurrentList()
132 public int getItemCount() {
133 return differ.getCurrentList()
136 public void setTemplateItems(List<TemplateBase> items) {
137 ArrayList<TemplateDetailsItem> list = new ArrayList<>();
138 for (TemplateBase p : items) {
139 TemplateDetailsItem item = TemplateDetailsItem.fromRoomObject(p);
144 public void setItems(List<TemplateDetailsItem> items) {
145 differ.submitList(items);
147 public String getMatchGroupText(int groupNumber) {
148 TemplateDetailsItem.Header header = getHeader();
149 Pattern p = header.getCompiledPattern();
153 final String testText = Misc.nullIsEmpty(header.getTestText());
154 Matcher m = p.matcher(testText);
155 if (m.matches() && m.groupCount() >= groupNumber)
156 return m.group(groupNumber);
160 protected TemplateDetailsItem.Header getHeader() {
161 return differ.getCurrentList()
166 private enum HeaderDetail {DESCRIPTION, COMMENT, DATE_YEAR, DATE_MONTH, DATE_DAY}
168 private enum AccDetail {ACCOUNT, COMMENT, AMOUNT}
170 public abstract static class ViewHolder extends RecyclerView.ViewHolder {
171 ViewHolder(@NonNull View itemView) {
174 abstract void bind(TemplateDetailsItem item);
177 public class Header extends ViewHolder {
178 private final TemplateDetailsHeaderBinding b;
179 public Header(@NonNull TemplateDetailsHeaderBinding binding) {
180 super(binding.getRoot());
183 TextWatcher templateNameWatcher = new TextWatcher() {
185 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
187 public void onTextChanged(CharSequence s, int start, int before, int count) {}
189 public void afterTextChanged(Editable s) {
190 final TemplateDetailsItem.Header header = getItem();
191 Logger.debug(D_TEMPLATE_UI,
192 "Storing changed template name " + s + "; header=" + header);
193 header.setName(String.valueOf(s));
196 b.templateName.addTextChangedListener(templateNameWatcher);
197 TextWatcher patternWatcher = new TextWatcher() {
199 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
201 public void onTextChanged(CharSequence s, int start, int before, int count) {}
203 public void afterTextChanged(Editable s) {
204 final TemplateDetailsItem.Header header = getItem();
205 Logger.debug(D_TEMPLATE_UI,
206 "Storing changed pattern " + s + "; header=" + header);
207 header.setPattern(String.valueOf(s));
210 b.pattern.addTextChangedListener(patternWatcher);
211 TextWatcher testTextWatcher = new TextWatcher() {
213 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
215 public void onTextChanged(CharSequence s, int start, int before, int count) {}
217 public void afterTextChanged(Editable s) {
218 final TemplateDetailsItem.Header header = getItem();
219 Logger.debug(D_TEMPLATE_UI,
220 "Storing changed test text " + s + "; header=" + header);
221 header.setTestText(String.valueOf(s));
224 b.testText.addTextChangedListener(testTextWatcher);
225 TextWatcher transactionDescriptionWatcher = new TextWatcher() {
227 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
230 public void onTextChanged(CharSequence s, int start, int before, int count) {
234 public void afterTextChanged(Editable s) {
235 final TemplateDetailsItem.Header header = getItem();
236 Logger.debug(D_TEMPLATE_UI,
237 "Storing changed transaction description " + s + "; header=" + header);
238 header.setTransactionDescription(String.valueOf(s));
241 b.transactionDescription.addTextChangedListener(transactionDescriptionWatcher);
242 TextWatcher transactionCommentWatcher = new TextWatcher() {
244 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
248 public void onTextChanged(CharSequence s, int start, int before, int count) {
252 public void afterTextChanged(Editable s) {
253 final TemplateDetailsItem.Header header = getItem();
254 Logger.debug(D_TEMPLATE_UI,
255 "Storing changed transaction description " + s + "; header=" + header);
256 header.setTransactionComment(String.valueOf(s));
259 b.transactionComment.addTextChangedListener(transactionCommentWatcher);
262 private TemplateDetailsItem.Header getItem() {
263 int pos = getAdapterPosition();
264 return differ.getCurrentList()
268 private void selectHeaderDetailSource(View v, HeaderDetail detail) {
269 TemplateDetailsItem.Header header = getItem();
270 Logger.debug(D_TEMPLATE_UI, "header is " + header);
271 TemplateDetailSourceSelectorFragment sel =
272 TemplateDetailSourceSelectorFragment.newInstance(1, header.getPattern(),
273 header.getTestText());
274 sel.setOnSourceSelectedListener((literal, group) -> {
278 header.switchToLiteralTransactionDescription();
281 header.switchToLiteralTransactionComment();
284 header.switchToLiteralDateYear();
287 header.switchToLiteralDateMonth();
290 header.switchToLiteralDateDay();
293 throw new IllegalStateException("Unexpected detail " + detail);
299 header.setTransactionDescriptionMatchGroup(group);
302 header.setTransactionCommentMatchGroup(group);
305 header.setDateYearMatchGroup(group);
308 header.setDateMonthMatchGroup(group);
311 header.setDateDayMatchGroup(group);
314 throw new IllegalStateException("Unexpected detail " + detail);
318 notifyItemChanged(getAdapterPosition());
320 final AppCompatActivity activity = (AppCompatActivity) v.getContext();
321 sel.show(activity.getSupportFragmentManager(), "template-details-source-selector");
324 void bind(TemplateDetailsItem item) {
325 TemplateDetailsItem.Header header = item.asHeaderItem();
326 Logger.debug(D_TEMPLATE_UI, "Binding to header " + header);
328 b.templateName.setText(header.getName());
329 b.pattern.setText(header.getPattern());
330 b.testText.setText(header.getTestText());
332 if (header.hasLiteralDateYear()) {
333 b.templateDetailsYearSource.setText(R.string.template_details_source_literal);
334 final Integer dateYear = header.getDateYear();
335 b.templateDetailsDateYear.setText(
336 (dateYear == null) ? null : String.valueOf(dateYear));
337 b.templateDetailsDateYearLayout.setVisibility(View.VISIBLE);
340 b.templateDetailsDateYearLayout.setVisibility(View.GONE);
341 b.templateDetailsYearSource.setText(
342 String.format(Locale.US, "Group %d (%s)", header.getDateYearMatchGroup(),
343 getMatchGroupText(header.getDateYearMatchGroup())));
345 b.templateDetailsYearSourceLabel.setOnClickListener(
346 v -> selectHeaderDetailSource(v, HeaderDetail.DATE_YEAR));
347 b.templateDetailsYearSource.setOnClickListener(
348 v -> selectHeaderDetailSource(v, HeaderDetail.DATE_YEAR));
350 if (header.hasLiteralDateMonth()) {
351 b.templateDetailsMonthSource.setText(R.string.template_details_source_literal);
352 final Integer dateMonth = header.getDateMonth();
353 b.templateDetailsDateMonth.setText(
354 (dateMonth == null) ? null : String.valueOf(dateMonth));
355 b.templateDetailsDateMonthLayout.setVisibility(View.VISIBLE);
358 b.templateDetailsDateMonthLayout.setVisibility(View.GONE);
359 b.templateDetailsMonthSource.setText(
360 String.format(Locale.US, "Group %d (%s)", header.getDateMonthMatchGroup(),
361 getMatchGroupText(header.getDateMonthMatchGroup())));
363 b.templateDetailsMonthSourceLabel.setOnClickListener(
364 v -> selectHeaderDetailSource(v, HeaderDetail.DATE_MONTH));
365 b.templateDetailsMonthSource.setOnClickListener(
366 v -> selectHeaderDetailSource(v, HeaderDetail.DATE_MONTH));
368 if (header.hasLiteralDateDay()) {
369 b.templateDetailsDaySource.setText(R.string.template_details_source_literal);
370 final Integer dateDay = header.getDateDay();
371 b.templateDetailsDateDay.setText(
372 (dateDay == null) ? null : String.valueOf(dateDay));
373 b.templateDetailsDateDayLayout.setVisibility(View.VISIBLE);
376 b.templateDetailsDateDayLayout.setVisibility(View.GONE);
377 b.templateDetailsDaySource.setText(
378 String.format(Locale.US, "Group %d (%s)", header.getDateDayMatchGroup(),
379 getMatchGroupText(header.getDateDayMatchGroup())));
381 b.templateDetailsDaySourceLabel.setOnClickListener(
382 v -> selectHeaderDetailSource(v, HeaderDetail.DATE_DAY));
383 b.templateDetailsDaySource.setOnClickListener(
384 v -> selectHeaderDetailSource(v, HeaderDetail.DATE_DAY));
386 if (header.hasLiteralTransactionDescription()) {
387 b.templateTransactionDescriptionSource.setText(
388 R.string.template_details_source_literal);
389 b.transactionDescription.setText(header.getTransactionDescription());
390 b.transactionDescriptionLayout.setVisibility(View.VISIBLE);
393 b.transactionDescriptionLayout.setVisibility(View.GONE);
394 b.templateTransactionDescriptionSource.setText(
395 String.format(Locale.US, "Group %d (%s)",
396 header.getTransactionDescriptionMatchGroup(),
397 getMatchGroupText(header.getTransactionDescriptionMatchGroup())));
400 b.templateTransactionDescriptionSourceLabel.setOnClickListener(
401 v -> selectHeaderDetailSource(v, HeaderDetail.DESCRIPTION));
402 b.templateTransactionDescriptionSource.setOnClickListener(
403 v -> selectHeaderDetailSource(v, HeaderDetail.DESCRIPTION));
405 if (header.hasLiteralTransactionComment()) {
406 b.templateTransactionCommentSource.setText(
407 R.string.template_details_source_literal);
408 b.transactionComment.setText(header.getTransactionComment());
409 b.transactionCommentLayout.setVisibility(View.VISIBLE);
412 b.transactionCommentLayout.setVisibility(View.GONE);
413 b.templateTransactionCommentSource.setText(String.format(Locale.US, "Group %d (%s)",
414 header.getTransactionCommentMatchGroup(),
415 getMatchGroupText(header.getTransactionCommentMatchGroup())));
418 b.templateTransactionCommentSourceLabel.setOnClickListener(
419 v -> selectHeaderDetailSource(v, HeaderDetail.COMMENT));
420 b.templateTransactionCommentSource.setOnClickListener(
421 v -> selectHeaderDetailSource(v, HeaderDetail.COMMENT));
423 b.templateDetailsHeadScanQrButton.setOnClickListener(this::scanTestQR);
426 private void scanTestQR(View view) {
427 QRScanCapableFragment.triggerQRScan();
431 public class AccountRow extends ViewHolder {
432 private final TemplateDetailsAccountBinding b;
433 public AccountRow(@NonNull TemplateDetailsAccountBinding binding) {
434 super(binding.getRoot());
437 TextWatcher accountNameWatcher = new TextWatcher() {
439 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
441 public void onTextChanged(CharSequence s, int start, int before, int count) {}
443 public void afterTextChanged(Editable s) {
444 TemplateDetailsItem.AccountRow accRow = getItem();
445 Logger.debug(D_TEMPLATE_UI,
446 "Storing changed account name " + s + "; accRow=" + accRow);
447 accRow.setAccountName(String.valueOf(s));
450 b.templateDetailsAccountName.addTextChangedListener(accountNameWatcher);
451 TextWatcher accountCommentWatcher = new TextWatcher() {
453 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
455 public void onTextChanged(CharSequence s, int start, int before, int count) {}
457 public void afterTextChanged(Editable s) {
458 TemplateDetailsItem.AccountRow accRow = getItem();
459 Logger.debug(D_TEMPLATE_UI,
460 "Storing changed account comment " + s + "; accRow=" + accRow);
461 accRow.setAccountComment(String.valueOf(s));
464 b.templateDetailsAccountComment.addTextChangedListener(accountCommentWatcher);
466 b.templateDetailsAccountAmount.addTextChangedListener(new TextWatcher() {
468 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
472 public void onTextChanged(CharSequence s, int start, int before, int count) {
476 public void afterTextChanged(Editable s) {
477 TemplateDetailsItem.AccountRow accRow = getItem();
479 String str = String.valueOf(s);
480 if (Misc.emptyIsNull(str) == null) {
481 accRow.setAmount(null);
485 final float amount = Data.parseNumber(str);
486 accRow.setAmount(amount);
487 b.templateDetailsAccountAmountLayout.setError(null);
489 Logger.debug(D_TEMPLATE_UI, String.format(Locale.US,
490 "Storing changed account amount %s [%4.2f]; accRow=%s", s,
493 catch (NumberFormatException | ParseException e) {
494 b.templateDetailsAccountAmountLayout.setError("!");
499 b.templateDetailsAccountAmount.setOnFocusChangeListener((v, hasFocus) -> {
503 TemplateDetailsItem.AccountRow accRow = getItem();
504 if (!accRow.hasLiteralAmount())
506 Float amt = accRow.getAmount();
510 b.templateDetailsAccountAmount.setText(Data.formatNumber(amt));
513 b.negateAmountSwitch.setOnCheckedChangeListener(
514 (buttonView, isChecked) -> getItem().setNegateAmount(isChecked));
517 void bind(TemplateDetailsItem item) {
518 TemplateDetailsItem.AccountRow accRow = item.asAccountRowItem();
519 if (accRow.hasLiteralAccountName()) {
520 b.templateDetailsAccountNameLayout.setVisibility(View.VISIBLE);
521 b.templateDetailsAccountName.setText(accRow.getAccountName());
522 b.templateDetailsAccountNameSource.setText(
523 R.string.template_details_source_literal);
526 b.templateDetailsAccountNameLayout.setVisibility(View.GONE);
527 b.templateDetailsAccountNameSource.setText(
528 String.format(Locale.US, "Group %d (%s)", accRow.getAccountNameMatchGroup(),
529 getMatchGroupText(accRow.getAccountNameMatchGroup())));
532 if (accRow.hasLiteralAccountComment()) {
533 b.templateDetailsAccountCommentLayout.setVisibility(View.VISIBLE);
534 b.templateDetailsAccountComment.setText(accRow.getAccountComment());
535 b.templateDetailsAccountCommentSource.setText(
536 R.string.template_details_source_literal);
539 b.templateDetailsAccountCommentLayout.setVisibility(View.GONE);
540 b.templateDetailsAccountCommentSource.setText(
541 String.format(Locale.US, "Group %d (%s)",
542 accRow.getAccountCommentMatchGroup(),
543 getMatchGroupText(accRow.getAccountCommentMatchGroup())));
546 if (accRow.hasLiteralAmount()) {
547 b.templateDetailsAccountAmountSource.setText(
548 R.string.template_details_source_literal);
549 b.templateDetailsAccountAmount.setVisibility(View.VISIBLE);
550 Float amt = accRow.getAmount();
551 b.templateDetailsAccountAmount.setText((amt == null) ? null : String.format(
552 Data.locale.getValue(), "%,4.2f", (accRow.getAmount())));
553 b.negateAmountSwitch.setVisibility(View.GONE);
556 b.templateDetailsAccountAmountSource.setText(
557 String.format(Locale.US, "Group %d (%s)", accRow.getAmountMatchGroup(),
558 getMatchGroupText(accRow.getAmountMatchGroup())));
559 b.templateDetailsAccountAmountLayout.setVisibility(View.GONE);
560 b.negateAmountSwitch.setVisibility(View.VISIBLE);
561 b.negateAmountSwitch.setChecked(accRow.isNegateAmount());
564 b.templateAccountNameSourceLabel.setOnClickListener(
565 v -> selectAccountRowDetailSource(v, AccDetail.ACCOUNT));
566 b.templateDetailsAccountNameSource.setOnClickListener(
567 v -> selectAccountRowDetailSource(v, AccDetail.ACCOUNT));
568 b.templateAccountCommentSourceLabel.setOnClickListener(
569 v -> selectAccountRowDetailSource(v, AccDetail.COMMENT));
570 b.templateDetailsAccountCommentSource.setOnClickListener(
571 v -> selectAccountRowDetailSource(v, AccDetail.COMMENT));
572 b.templateAccountAmountSourceLabel.setOnClickListener(
573 v -> selectAccountRowDetailSource(v, AccDetail.AMOUNT));
574 b.templateDetailsAccountAmountSource.setOnClickListener(
575 v -> selectAccountRowDetailSource(v, AccDetail.AMOUNT));
577 private @NotNull TemplateDetailsItem.AccountRow getItem() {
578 return differ.getCurrentList()
579 .get(getAdapterPosition())
582 private void selectAccountRowDetailSource(View v, AccDetail detail) {
583 TemplateDetailsItem.AccountRow accRow = getItem();
584 final TemplateDetailsItem.Header header = getHeader();
585 Logger.debug(D_TEMPLATE_UI, "header is " + header);
586 TemplateDetailSourceSelectorFragment sel =
587 TemplateDetailSourceSelectorFragment.newInstance(1, header.getPattern(),
588 header.getTestText());
589 sel.setOnSourceSelectedListener((literal, group) -> {
593 accRow.switchToLiteralAccountName();
596 accRow.switchToLiteralAccountComment();
599 accRow.switchToLiteralAmount();
602 throw new IllegalStateException("Unexpected detail " + detail);
608 accRow.setAccountNameMatchGroup(group);
611 accRow.setAccountCommentMatchGroup(group);
614 accRow.setAmountMatchGroup(group);
617 throw new IllegalStateException("Unexpected detail " + detail);
621 notifyItemChanged(getAdapterPosition());
623 final AppCompatActivity activity = (AppCompatActivity) v.getContext();
624 sel.show(activity.getSupportFragmentManager(), "template-details-source-selector");