From 9284fc76d073dc510134def10fa88dbb4c931aca Mon Sep 17 00:00:00 2001 From: KristianTashkov Date: Sat, 25 Jul 2020 19:16:07 +0300 Subject: [PATCH] Implement option for advanced checkmark states --- .../uhabits/widgets/CheckmarkWidgetTest.java | 5 - .../dialogs/CheckmarkOptionPickerFactory.kt | 93 +++++++++++++++++++ .../activities/common/views/HistoryChart.java | 63 ++++++++++--- .../habits/list/ListHabitsScreen.kt | 8 ++ .../habits/list/views/CheckmarkButtonView.kt | 21 ++++- .../habits/list/views/CheckmarkPanelView.kt | 7 ++ .../habits/list/views/HabitCardView.kt | 4 + .../habits/show/ShowHabitScreen.java | 26 +++++- .../layout/checkmark_option_picker_dialog.xml | 69 ++++++++++++++ .../src/main/res/values/strings.xml | 7 ++ .../src/main/res/xml/preferences.xml | 7 ++ .../commands/CreateRepetitionCommand.java | 7 +- .../commands/ToggleRepetitionCommand.java | 1 - .../isoron/uhabits/core/models/Checkmark.java | 14 ++- .../uhabits/core/models/CheckmarkList.java | 15 ++- .../uhabits/core/models/Repetition.java | 10 +- .../uhabits/core/models/RepetitionList.java | 13 +-- .../isoron/uhabits/core/models/ScoreList.java | 17 +++- .../uhabits/core/models/StreakList.java | 20 ++-- .../models/memory/MemoryRepetitionList.java | 14 +-- .../uhabits/core/preferences/Preferences.java | 10 ++ .../habits/list/ListHabitsBehavior.java | 24 +++++ .../habits/show/ShowHabitBehavior.java | 6 ++ .../commands/ToggleRepetitionCommandTest.java | 9 -- .../core/models/RepetitionListTest.java | 13 +-- .../habits/list/ListHabitsBehaviorTest.java | 2 - 26 files changed, 398 insertions(+), 87 deletions(-) create mode 100644 android/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/CheckmarkOptionPickerFactory.kt create mode 100644 android/uhabits-android/src/main/res/layout/checkmark_option_picker_dialog.xml diff --git a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/CheckmarkWidgetTest.java b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/CheckmarkWidgetTest.java index 306cbb385..0aeb4762f 100644 --- a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/CheckmarkWidgetTest.java +++ b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/CheckmarkWidgetTest.java @@ -71,11 +71,6 @@ public class CheckmarkWidgetTest extends BaseViewTest button.performClick(); sleep(1000); - assertThat(checkmarks.getTodayValue(), equalTo(SKIPPED_EXPLICITLY)); - - button.performClick(); - sleep(1000); - assertThat(checkmarks.getTodayValue(), equalTo(UNCHECKED)); } diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/CheckmarkOptionPickerFactory.kt b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/CheckmarkOptionPickerFactory.kt new file mode 100644 index 000000000..c06b97523 --- /dev/null +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/CheckmarkOptionPickerFactory.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2017 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.activities.common.dialogs + +import android.content.Context +import android.graphics.BlendMode +import android.graphics.BlendModeColorFilter +import android.graphics.PorterDuff +import android.os.Build +import android.view.LayoutInflater +import android.view.WindowManager +import androidx.appcompat.app.AlertDialog +import com.google.android.material.button.MaterialButton +import org.isoron.androidbase.activities.ActivityContext +import org.isoron.androidbase.utils.InterfaceUtils +import org.isoron.uhabits.R +import org.isoron.uhabits.core.models.Checkmark +import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior +import javax.inject.Inject + +class CheckmarkOptionPickerFactory +@Inject constructor( + @ActivityContext private val context: Context +) { + fun create(habitName: String, + habitTimestamp: String, + value: Int, + callback: ListHabitsBehavior.CheckmarkOptionsCallback): AlertDialog { + + val inflater = LayoutInflater.from(context) + val view = inflater.inflate(R.layout.checkmark_option_picker_dialog, null) + val title = context.resources.getString( + R.string.choose_checkmark_option, habitName, habitTimestamp) + val dialog = AlertDialog.Builder(context) + .setView(view) + .setTitle(title) + .setOnDismissListener{ + callback.onCheckmarkOptionDismissed() + } + .create() + + val buttonValues = mapOf( + R.id.check_button to Checkmark.CHECKED_EXPLICITLY, + R.id.skip_button to Checkmark.SKIPPED_EXPLICITLY, + R.id.fail_button to Checkmark.FAILED_EXPLICITLY_NECESSARY, + R.id.clear_button to Checkmark.UNCHECKED + ) + val valuesToButton = mapOf( + Checkmark.CHECKED_EXPLICITLY to R.id.check_button, + Checkmark.SKIPPED_EXPLICITLY to R.id.skip_button , + Checkmark.FAILED_EXPLICITLY_NECESSARY to R.id.fail_button, + Checkmark.FAILED_EXPLICITLY_UNNECESSARY to R.id.fail_button + ) + + for ((buttonId, buttonValue) in buttonValues) { + val button = view.findViewById(buttonId) + button.setTypeface(InterfaceUtils.getFontAwesome(context)) + button.setOnClickListener{ + callback.onCheckmarkOptionPicked(buttonValue) + dialog.dismiss() + } + if (valuesToButton.containsKey(value) && valuesToButton[value] == buttonId) { + val color = context.resources.getColor(R.color.amber_800) + if (Build.VERSION.SDK_INT >= 29) { + button.background.colorFilter = BlendModeColorFilter(color, BlendMode.MULTIPLY) + } else { + button.background.setColorFilter(color, PorterDuff.Mode.MULTIPLY) + } + } + } + + dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) + + return dialog + } +} diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/HistoryChart.java b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/HistoryChart.java index 4ee91ae10..8da73c4cc 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/HistoryChart.java +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/HistoryChart.java @@ -143,14 +143,6 @@ public class HistoryChart extends ScrollableChart final Timestamp timestamp = positionToTimestamp(x, y); if (timestamp == null) return false; - Timestamp today = DateUtils.getToday(); - int offset = timestamp.daysUntil(today); - if (offset < checkmarks.length) - { - boolean isChecked = checkmarks[offset] == CHECKED_EXPLICITLY; - checkmarks[offset] = (isChecked ? UNCHECKED : CHECKED_EXPLICITLY); - } - controller.onToggleCheckmark(timestamp); postInvalidate(); return true; @@ -357,26 +349,45 @@ public class HistoryChart extends ScrollableChart headerOverflow = Math.max(0, headerOverflow - columnWidth); } + private boolean isFailed(int checkmark) + { + return (checkmark == 0 || + (!isNumerical && checkmark == FAILED_EXPLICITLY_NECESSARY)); + } + + private boolean isImplicitlySuccessful(int checkmark) + { + if (isNumerical) return checkmark < target; + + return (checkmark == SKIPPED_EXPLICITLY || + checkmark == FAILED_EXPLICITLY_UNNECESSARY || + checkmark == CHECKED_IMPLICITLY); + } + private void drawSquare(Canvas canvas, RectF location, GregorianCalendar date, int checkmarkOffset) { - pSquareFg.setStrikeThruText(false); + boolean drawCross = false; + boolean drawDash = false; if (checkmarkOffset >= checkmarks.length) pSquareBg.setColor(colors[0]); else { int checkmark = checkmarks[checkmarkOffset]; - if(checkmark == 0) pSquareBg.setColor(colors[0]); - else if(checkmark < target) + if(isFailed(checkmark)) pSquareBg.setColor(colors[0]); + else if(isImplicitlySuccessful(checkmark)) { pSquareBg.setColor(isNumerical ? textColor : colors[1]); } - else if (!isNumerical && checkmark == 3) { - pSquareFg.setStrikeThruText(true); - pSquareBg.setColor(colors[1]); - } else pSquareBg.setColor(colors[2]); + + if (!isNumerical) + { + if (checkmark == FAILED_EXPLICITLY_UNNECESSARY || + checkmark == FAILED_EXPLICITLY_NECESSARY) drawCross = true; + if (checkmark == SKIPPED_EXPLICITLY) drawDash = true; + } } pSquareFg.setColor(reverseTextColor); @@ -385,6 +396,28 @@ public class HistoryChart extends ScrollableChart String text = Integer.toString(date.get(Calendar.DAY_OF_MONTH)); canvas.drawText(text, location.centerX(), location.centerY() + squareTextOffset, pSquareFg); + if (drawCross) + { + for (int thickness = -1; thickness < 2; thickness ++) + { + canvas.drawLine( + location.left + thickness, location.bottom, + location.right - thickness, location.top, pSquareFg); + canvas.drawLine( + location.right - thickness, location.bottom, + location.left + thickness, location.top, pSquareFg); + } + } + if (drawDash) + { + for (int thickness = -1; thickness < 2; thickness ++) + { + canvas.drawLine( + location.left, location.centerY() + thickness + squareTextOffset / 2, + location.right,location.centerY() + thickness + squareTextOffset / 2, + pSquareFg); + } + } } private float getWeekdayLabelWidth() diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt index e4c3752a1..55928b98e 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt @@ -65,6 +65,7 @@ class ListHabitsScreen private val confirmDeleteDialogFactory: ConfirmDeleteDialogFactory, private val colorPickerFactory: ColorPickerDialogFactory, private val numberPickerFactory: NumberPickerFactory, + private val checkmarkOptionPickerFactory: CheckmarkOptionPickerFactory, private val behavior: Lazy, private val menu: Lazy, private val selectionMenu: Lazy @@ -204,6 +205,13 @@ class ListHabitsScreen numberPickerFactory.create(value, unit, callback).show() } + override fun showCheckmarkOptions(habitName: String, + timestamp: Timestamp, + value: Int, + callback: ListHabitsBehavior.CheckmarkOptionsCallback) { + checkmarkOptionPickerFactory.create(habitName, timestamp.toString(), value, callback).show() + } + @StringRes private fun getExecuteString(command: Command): Int? { when (command) { diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonView.kt b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonView.kt index 51c21e879..387128697 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonView.kt +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonView.kt @@ -52,6 +52,7 @@ class CheckmarkButtonView( } var onToggle: () -> Unit = {} + var onToggleWithOptions: () -> Unit = {} private var drawer = Drawer() init { @@ -63,21 +64,29 @@ class CheckmarkButtonView( fun performToggle() { onToggle() value = when (value) { - CHECKED_EXPLICITLY -> SKIPPED_EXPLICITLY - SKIPPED_EXPLICITLY -> UNCHECKED - else -> CHECKED_EXPLICITLY + UNCHECKED -> CHECKED_EXPLICITLY + else -> UNCHECKED } performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) invalidate() } + fun performToggleWithOptions() { + onToggleWithOptions() + performHapticFeedback(HapticFeedbackConstants.KEYBOARD_PRESS) + } + override fun onClick(v: View) { if (preferences.isShortToggleEnabled) performToggle() + else if (preferences.isAdvancedCheckmarksEnabled) performToggleWithOptions() else showMessage(R.string.long_press_to_toggle) } override fun onLongClick(v: View): Boolean { - performToggle() + if (preferences.isShortToggleEnabled && preferences.isAdvancedCheckmarksEnabled) { + performToggleWithOptions() + } + else performToggle() return true } @@ -110,12 +119,16 @@ class CheckmarkButtonView( fun draw(canvas: Canvas) { paint.color = when (value) { CHECKED_EXPLICITLY -> color + FAILED_EXPLICITLY_UNNECESSARY -> mediumContrastTextColor SKIPPED_EXPLICITLY -> mediumContrastTextColor + FAILED_EXPLICITLY_NECESSARY -> mediumContrastTextColor else -> lowContrastColor } val id = when (value) { SKIPPED_EXPLICITLY -> R.string.fa_skipped UNCHECKED -> R.string.fa_times + FAILED_EXPLICITLY_NECESSARY -> R.string.fa_times + FAILED_EXPLICITLY_UNNECESSARY -> R.string.fa_check else -> R.string.fa_check } val label = resources.getString(id) diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelView.kt b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelView.kt index c0e8ae2e9..3970d33eb 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelView.kt +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelView.kt @@ -52,6 +52,12 @@ class CheckmarkPanelView( setupButtons() } + var onToggleWithOptions: (Timestamp) -> Unit = {} + set(value) { + field = value + setupButtons() + } + override fun createButton(): CheckmarkButtonView = buttonFactory.create() @Synchronized @@ -66,6 +72,7 @@ class CheckmarkPanelView( } button.color = color button.onToggle = { onToggle(timestamp) } + button.onToggleWithOptions = { onToggleWithOptions (timestamp) } } } } diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt index f6adfcc16..dc1a495fe 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt @@ -125,6 +125,10 @@ class HabitCardView( triggerRipple(timestamp) habit?.let { behavior.onToggle(it, timestamp) } } + onToggleWithOptions = { timestamp -> + triggerRipple(timestamp) + habit?.let { behavior.onToggleWithOptions(it, timestamp) } + } } numberPanel = numberPanelFactory.create().apply { diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitScreen.java b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitScreen.java index d19eb7cb5..c1ab2a4f8 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitScreen.java +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitScreen.java @@ -26,8 +26,8 @@ import androidx.annotation.NonNull; import org.isoron.androidbase.activities.*; import org.isoron.uhabits.*; import org.isoron.uhabits.activities.common.dialogs.*; -import org.isoron.uhabits.activities.habits.edit.*; import org.isoron.uhabits.core.models.*; +import org.isoron.uhabits.core.preferences.Preferences; import org.isoron.uhabits.core.ui.callbacks.*; import org.isoron.uhabits.core.ui.screens.habits.show.*; import org.isoron.uhabits.intents.*; @@ -49,19 +49,27 @@ public class ShowHabitScreen extends BaseScreen @NonNull private final ConfirmDeleteDialogFactory confirmDeleteDialogFactory; + @NonNull + private final CheckmarkOptionPickerFactory checkmarkOptionPickerFactory; + private final Lazy behavior; @NonNull private final IntentFactory intentFactory; + @NonNull + private final Preferences prefs; + @Inject public ShowHabitScreen(@NonNull BaseActivity activity, @NonNull Habit habit, @NonNull ShowHabitRootView view, @NonNull ShowHabitsMenu menu, @NonNull ConfirmDeleteDialogFactory confirmDeleteDialogFactory, + @NonNull CheckmarkOptionPickerFactory checkmarkOptionPickerFactory, @NonNull IntentFactory intentFactory, - @NonNull Lazy behavior) + @NonNull Lazy behavior, + @NonNull Preferences prefs) { super(activity); this.intentFactory = intentFactory; @@ -71,6 +79,8 @@ public class ShowHabitScreen extends BaseScreen this.habit = habit; this.behavior = behavior; this.confirmDeleteDialogFactory = confirmDeleteDialogFactory; + this.checkmarkOptionPickerFactory = checkmarkOptionPickerFactory; + this.prefs = prefs; view.setController(this); } @@ -83,7 +93,17 @@ public class ShowHabitScreen extends BaseScreen @Override public void onToggleCheckmark(Timestamp timestamp) { - behavior.get().onToggleCheckmark(timestamp); + if (prefs.isAdvancedCheckmarksEnabled()) + { + CheckmarkList checkmarks = habit.getCheckmarks(); + int oldValue = checkmarks.getValues(timestamp, timestamp)[0]; + checkmarkOptionPickerFactory.create(habit.getName(), timestamp.toString(), oldValue, + newValue -> + { + behavior.get().onCreateRepetition(timestamp, newValue); + }).show(); + } + else behavior.get().onToggleCheckmark(timestamp); } @Override diff --git a/android/uhabits-android/src/main/res/layout/checkmark_option_picker_dialog.xml b/android/uhabits-android/src/main/res/layout/checkmark_option_picker_dialog.xml new file mode 100644 index 000000000..fe026406a --- /dev/null +++ b/android/uhabits-android/src/main/res/layout/checkmark_option_picker_dialog.xml @@ -0,0 +1,69 @@ + + + + + +