Implement option for advanced checkmark states

pull/610/head
KristianTashkov 5 years ago
parent 00bc39027f
commit 9284fc76d0

@ -71,11 +71,6 @@ public class CheckmarkWidgetTest extends BaseViewTest
button.performClick(); button.performClick();
sleep(1000); sleep(1000);
assertThat(checkmarks.getTodayValue(), equalTo(SKIPPED_EXPLICITLY));
button.performClick();
sleep(1000);
assertThat(checkmarks.getTodayValue(), equalTo(UNCHECKED)); assertThat(checkmarks.getTodayValue(), equalTo(UNCHECKED));
} }

@ -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<MaterialButton>(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
}
}

@ -143,14 +143,6 @@ public class HistoryChart extends ScrollableChart
final Timestamp timestamp = positionToTimestamp(x, y); final Timestamp timestamp = positionToTimestamp(x, y);
if (timestamp == null) return false; 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); controller.onToggleCheckmark(timestamp);
postInvalidate(); postInvalidate();
return true; return true;
@ -357,26 +349,45 @@ public class HistoryChart extends ScrollableChart
headerOverflow = Math.max(0, headerOverflow - columnWidth); 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, private void drawSquare(Canvas canvas,
RectF location, RectF location,
GregorianCalendar date, GregorianCalendar date,
int checkmarkOffset) int checkmarkOffset)
{ {
pSquareFg.setStrikeThruText(false); boolean drawCross = false;
boolean drawDash = false;
if (checkmarkOffset >= checkmarks.length) pSquareBg.setColor(colors[0]); if (checkmarkOffset >= checkmarks.length) pSquareBg.setColor(colors[0]);
else else
{ {
int checkmark = checkmarks[checkmarkOffset]; int checkmark = checkmarks[checkmarkOffset];
if(checkmark == 0) pSquareBg.setColor(colors[0]); if(isFailed(checkmark)) pSquareBg.setColor(colors[0]);
else if(checkmark < target) else if(isImplicitlySuccessful(checkmark))
{ {
pSquareBg.setColor(isNumerical ? textColor : colors[1]); pSquareBg.setColor(isNumerical ? textColor : colors[1]);
} }
else if (!isNumerical && checkmark == 3) {
pSquareFg.setStrikeThruText(true);
pSquareBg.setColor(colors[1]);
}
else pSquareBg.setColor(colors[2]); 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); pSquareFg.setColor(reverseTextColor);
@ -385,6 +396,28 @@ public class HistoryChart extends ScrollableChart
String text = Integer.toString(date.get(Calendar.DAY_OF_MONTH)); String text = Integer.toString(date.get(Calendar.DAY_OF_MONTH));
canvas.drawText(text, location.centerX(), canvas.drawText(text, location.centerX(),
location.centerY() + squareTextOffset, pSquareFg); 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() private float getWeekdayLabelWidth()

@ -65,6 +65,7 @@ class ListHabitsScreen
private val confirmDeleteDialogFactory: ConfirmDeleteDialogFactory, private val confirmDeleteDialogFactory: ConfirmDeleteDialogFactory,
private val colorPickerFactory: ColorPickerDialogFactory, private val colorPickerFactory: ColorPickerDialogFactory,
private val numberPickerFactory: NumberPickerFactory, private val numberPickerFactory: NumberPickerFactory,
private val checkmarkOptionPickerFactory: CheckmarkOptionPickerFactory,
private val behavior: Lazy<ListHabitsBehavior>, private val behavior: Lazy<ListHabitsBehavior>,
private val menu: Lazy<ListHabitsMenu>, private val menu: Lazy<ListHabitsMenu>,
private val selectionMenu: Lazy<ListHabitsSelectionMenu> private val selectionMenu: Lazy<ListHabitsSelectionMenu>
@ -204,6 +205,13 @@ class ListHabitsScreen
numberPickerFactory.create(value, unit, callback).show() 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 @StringRes
private fun getExecuteString(command: Command): Int? { private fun getExecuteString(command: Command): Int? {
when (command) { when (command) {

@ -52,6 +52,7 @@ class CheckmarkButtonView(
} }
var onToggle: () -> Unit = {} var onToggle: () -> Unit = {}
var onToggleWithOptions: () -> Unit = {}
private var drawer = Drawer() private var drawer = Drawer()
init { init {
@ -63,21 +64,29 @@ class CheckmarkButtonView(
fun performToggle() { fun performToggle() {
onToggle() onToggle()
value = when (value) { value = when (value) {
CHECKED_EXPLICITLY -> SKIPPED_EXPLICITLY UNCHECKED -> CHECKED_EXPLICITLY
SKIPPED_EXPLICITLY -> UNCHECKED else -> UNCHECKED
else -> CHECKED_EXPLICITLY
} }
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
invalidate() invalidate()
} }
fun performToggleWithOptions() {
onToggleWithOptions()
performHapticFeedback(HapticFeedbackConstants.KEYBOARD_PRESS)
}
override fun onClick(v: View) { override fun onClick(v: View) {
if (preferences.isShortToggleEnabled) performToggle() if (preferences.isShortToggleEnabled) performToggle()
else if (preferences.isAdvancedCheckmarksEnabled) performToggleWithOptions()
else showMessage(R.string.long_press_to_toggle) else showMessage(R.string.long_press_to_toggle)
} }
override fun onLongClick(v: View): Boolean { override fun onLongClick(v: View): Boolean {
performToggle() if (preferences.isShortToggleEnabled && preferences.isAdvancedCheckmarksEnabled) {
performToggleWithOptions()
}
else performToggle()
return true return true
} }
@ -110,12 +119,16 @@ class CheckmarkButtonView(
fun draw(canvas: Canvas) { fun draw(canvas: Canvas) {
paint.color = when (value) { paint.color = when (value) {
CHECKED_EXPLICITLY -> color CHECKED_EXPLICITLY -> color
FAILED_EXPLICITLY_UNNECESSARY -> mediumContrastTextColor
SKIPPED_EXPLICITLY -> mediumContrastTextColor SKIPPED_EXPLICITLY -> mediumContrastTextColor
FAILED_EXPLICITLY_NECESSARY -> mediumContrastTextColor
else -> lowContrastColor else -> lowContrastColor
} }
val id = when (value) { val id = when (value) {
SKIPPED_EXPLICITLY -> R.string.fa_skipped SKIPPED_EXPLICITLY -> R.string.fa_skipped
UNCHECKED -> R.string.fa_times UNCHECKED -> R.string.fa_times
FAILED_EXPLICITLY_NECESSARY -> R.string.fa_times
FAILED_EXPLICITLY_UNNECESSARY -> R.string.fa_check
else -> R.string.fa_check else -> R.string.fa_check
} }
val label = resources.getString(id) val label = resources.getString(id)

@ -52,6 +52,12 @@ class CheckmarkPanelView(
setupButtons() setupButtons()
} }
var onToggleWithOptions: (Timestamp) -> Unit = {}
set(value) {
field = value
setupButtons()
}
override fun createButton(): CheckmarkButtonView = buttonFactory.create() override fun createButton(): CheckmarkButtonView = buttonFactory.create()
@Synchronized @Synchronized
@ -66,6 +72,7 @@ class CheckmarkPanelView(
} }
button.color = color button.color = color
button.onToggle = { onToggle(timestamp) } button.onToggle = { onToggle(timestamp) }
button.onToggleWithOptions = { onToggleWithOptions (timestamp) }
} }
} }
} }

@ -125,6 +125,10 @@ class HabitCardView(
triggerRipple(timestamp) triggerRipple(timestamp)
habit?.let { behavior.onToggle(it, timestamp) } habit?.let { behavior.onToggle(it, timestamp) }
} }
onToggleWithOptions = { timestamp ->
triggerRipple(timestamp)
habit?.let { behavior.onToggleWithOptions(it, timestamp) }
}
} }
numberPanel = numberPanelFactory.create().apply { numberPanel = numberPanelFactory.create().apply {

@ -26,8 +26,8 @@ import androidx.annotation.NonNull;
import org.isoron.androidbase.activities.*; import org.isoron.androidbase.activities.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.common.dialogs.*; 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.models.*;
import org.isoron.uhabits.core.preferences.Preferences;
import org.isoron.uhabits.core.ui.callbacks.*; import org.isoron.uhabits.core.ui.callbacks.*;
import org.isoron.uhabits.core.ui.screens.habits.show.*; import org.isoron.uhabits.core.ui.screens.habits.show.*;
import org.isoron.uhabits.intents.*; import org.isoron.uhabits.intents.*;
@ -49,19 +49,27 @@ public class ShowHabitScreen extends BaseScreen
@NonNull @NonNull
private final ConfirmDeleteDialogFactory confirmDeleteDialogFactory; private final ConfirmDeleteDialogFactory confirmDeleteDialogFactory;
@NonNull
private final CheckmarkOptionPickerFactory checkmarkOptionPickerFactory;
private final Lazy<ShowHabitBehavior> behavior; private final Lazy<ShowHabitBehavior> behavior;
@NonNull @NonNull
private final IntentFactory intentFactory; private final IntentFactory intentFactory;
@NonNull
private final Preferences prefs;
@Inject @Inject
public ShowHabitScreen(@NonNull BaseActivity activity, public ShowHabitScreen(@NonNull BaseActivity activity,
@NonNull Habit habit, @NonNull Habit habit,
@NonNull ShowHabitRootView view, @NonNull ShowHabitRootView view,
@NonNull ShowHabitsMenu menu, @NonNull ShowHabitsMenu menu,
@NonNull ConfirmDeleteDialogFactory confirmDeleteDialogFactory, @NonNull ConfirmDeleteDialogFactory confirmDeleteDialogFactory,
@NonNull CheckmarkOptionPickerFactory checkmarkOptionPickerFactory,
@NonNull IntentFactory intentFactory, @NonNull IntentFactory intentFactory,
@NonNull Lazy<ShowHabitBehavior> behavior) @NonNull Lazy<ShowHabitBehavior> behavior,
@NonNull Preferences prefs)
{ {
super(activity); super(activity);
this.intentFactory = intentFactory; this.intentFactory = intentFactory;
@ -71,6 +79,8 @@ public class ShowHabitScreen extends BaseScreen
this.habit = habit; this.habit = habit;
this.behavior = behavior; this.behavior = behavior;
this.confirmDeleteDialogFactory = confirmDeleteDialogFactory; this.confirmDeleteDialogFactory = confirmDeleteDialogFactory;
this.checkmarkOptionPickerFactory = checkmarkOptionPickerFactory;
this.prefs = prefs;
view.setController(this); view.setController(this);
} }
@ -83,7 +93,17 @@ public class ShowHabitScreen extends BaseScreen
@Override @Override
public void onToggleCheckmark(Timestamp timestamp) 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 @Override

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
~
~ 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 <http://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="horizontal"
android:gravity="center"
android:paddingHorizontal="5dp"
android:paddingVertical="25dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:divider="?android:dividerVertical"
android:showDividers="middle">
<Button
android:id="@+id/check_button"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:drawableStart="@drawable/ic_action_check"
android:text="@string/done_button_text"
app:cornerRadius="0dp" />
<Button
android:id="@+id/fail_button"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/fail_button_text"
app:cornerRadius="0dp" />
<Button
android:id="@+id/skip_button"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/skip_button_text"
app:cornerRadius="0dp" />
<Button
android:id="@+id/clear_button"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/delete_button_text"
app:cornerRadius="0dp" />
</LinearLayout>

@ -67,6 +67,8 @@
<string name="interval_custom">Custom...</string> <string name="interval_custom">Custom...</string>
<string name="pref_toggle_title">Toggle with short press</string> <string name="pref_toggle_title">Toggle with short press</string>
<string name="pref_toggle_description">Put checkmarks with a single tap instead of press-and-hold. More convenient, but might cause accidental toggles.</string> <string name="pref_toggle_description">Put checkmarks with a single tap instead of press-and-hold. More convenient, but might cause accidental toggles.</string>
<string name="pref_advanced_checkmarks_title">Enable advanced checkmarks</string>
<string name="pref_advanced_checkmarks_description">Enables checkmark selection to have additional states like explicitly failing a habit or skipping it for a day.</string>
<string name="pref_snooze_interval_title">Snooze interval on reminders</string> <string name="pref_snooze_interval_title">Snooze interval on reminders</string>
<string name="pref_rate_this_app">Rate this app on Google Play</string> <string name="pref_rate_this_app">Rate this app on Google Play</string>
<string name="pref_send_feedback">Send feedback to developer</string> <string name="pref_send_feedback">Send feedback to developer</string>
@ -162,6 +164,7 @@
<string name="export">Export</string> <string name="export">Export</string>
<string name="long_press_to_edit">Press-and-hold to change the value</string> <string name="long_press_to_edit">Press-and-hold to change the value</string>
<string name="change_value">Change value</string> <string name="change_value">Change value</string>
<string name="choose_checkmark_option">%1$s on %2$s</string>
<string name="calendar">Calendar</string> <string name="calendar">Calendar</string>
<string name="unit">Unit</string> <string name="unit">Unit</string>
<string name="example_question_boolean">e.g. Did you exercise today?</string> <string name="example_question_boolean">e.g. Did you exercise today?</string>
@ -194,4 +197,8 @@
<string name="every_month">Every month</string> <string name="every_month">Every month</string>
<string name="validation_cannot_be_blank">Cannot be blank</string> <string name="validation_cannot_be_blank">Cannot be blank</string>
<string name="today">Today</string> <string name="today">Today</string>
<string name="done_button_text">&#xf14a;\nDone</string>
<string name="fail_button_text">&#xf057;\nFail</string>
<string name="skip_button_text">&#xf146;\nSkip</string>
<string name="delete_button_text">&#xf05e;\nDelete</string>
</resources> </resources>

@ -31,6 +31,13 @@
android:title="@string/pref_toggle_title" android:title="@string/pref_toggle_title"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<CheckBoxPreference
android:defaultValue="false"
android:key="pref_advanced_checkmarks"
android:summary="@string/pref_advanced_checkmarks_description"
android:title="@string/pref_advanced_checkmarks_title"
app:iconSpaceReserved="false" />
<CheckBoxPreference <CheckBoxPreference
android:defaultValue="false" android:defaultValue="false"
android:key="pref_checkmark_reverse_order" android:key="pref_checkmark_reverse_order"

@ -58,8 +58,11 @@ public class CreateRepetitionCommand extends Command
previousRep = reps.getByTimestamp(timestamp); previousRep = reps.getByTimestamp(timestamp);
if (previousRep != null) reps.remove(previousRep); if (previousRep != null) reps.remove(previousRep);
if (value != 0)
{
newRep = new Repetition(timestamp, value); newRep = new Repetition(timestamp, value);
reps.add(newRep); reps.add(newRep);
}
habit.invalidateNewerThan(timestamp); habit.invalidateNewerThan(timestamp);
} }

@ -70,7 +70,6 @@ public class ToggleRepetitionCommand extends Command
public void undo() public void undo()
{ {
execute(); execute();
execute();
} }
public static class Record public static class Record

@ -37,6 +37,18 @@ import static org.isoron.uhabits.core.utils.StringUtils.defaultToStringStyle;
@ThreadSafe @ThreadSafe
public final class Checkmark public final class Checkmark
{ {
/**
* Indicates that there was a failed repetition at the timestamp and a
* repetition was expected.
*/
public static final int FAILED_EXPLICITLY_NECESSARY = 5;
/**
* Indicates that there was a failed repetition at the timestamp and a
* repetition wasn't expected.
*/
public static final int FAILED_EXPLICITLY_UNNECESSARY = 4;
/** /**
* Indicates that there was an explicit skip at the timestamp. * Indicates that there was an explicit skip at the timestamp.
*/ */
@ -65,7 +77,7 @@ public final class Checkmark
* The value of the checkmark. * The value of the checkmark.
* <p> * <p>
* For boolean habits, this equals either UNCHECKED, SKIPPED_EXPLICITLY, CHECKED_EXPLICITLY, * For boolean habits, this equals either UNCHECKED, SKIPPED_EXPLICITLY, CHECKED_EXPLICITLY,
* or CHECKED_IMPLICITLY. * CHECKED_IMPLICITLY, FAILED_EXPLICITLY_UNNECESSARY, FAILED_EXPLICITLY_NECESSARY.
* <p> * <p>
* For numerical habits, this number is stored in thousandths. That * For numerical habits, this number is stored in thousandths. That
* is, if the user enters value 1.50 on the app, it is stored as 1500. * is, if the user enters value 1.50 on the app, it is stored as 1500.

@ -79,7 +79,14 @@ public abstract class CheckmarkList
{ {
Timestamp date = rep.getTimestamp(); Timestamp date = rep.getTimestamp();
int offset = date.daysUntil(today); int offset = date.daysUntil(today);
checkmarks.set(offset, new Checkmark(date, rep.getValue())); int checkmarkValue = rep.getValue();
if (checkmarkValue == FAILED_EXPLICITLY_NECESSARY)
{
int oldValue = checkmarks.get(offset).getValue();
checkmarkValue = (oldValue == CHECKED_IMPLICITLY) ?
FAILED_EXPLICITLY_UNNECESSARY : FAILED_EXPLICITLY_NECESSARY;
}
checkmarks.set(offset, new Checkmark(date, checkmarkValue));
} }
return checkmarks; return checkmarks;
@ -380,11 +387,11 @@ public abstract class CheckmarkList
{ {
ArrayList<Interval> intervals; ArrayList<Interval> intervals;
List<Repetition> successful_repetitions = new ArrayList<>(); List<Repetition> successful_repetitions = new ArrayList<>();
for (Repetition rep : reps) { for (Repetition rep : reps)
if (rep.getValue() != SKIPPED_EXPLICITLY) { {
if (rep.getValue() == CHECKED_EXPLICITLY)
successful_repetitions.add(rep); successful_repetitions.add(rep);
} }
}
intervals = buildIntervals( intervals = buildIntervals(
habit.getFrequency(), successful_repetitions.toArray(new Repetition[0])); habit.getFrequency(), successful_repetitions.toArray(new Repetition[0]));
snapIntervalsTogether(intervals); snapIntervalsTogether(intervals);

@ -30,8 +30,8 @@ import java.util.GregorianCalendar;
import static org.isoron.uhabits.core.utils.StringUtils.defaultToStringStyle; import static org.isoron.uhabits.core.utils.StringUtils.defaultToStringStyle;
/** /**
* Represents a record that the user has performed or skipped a certain habit at a certain * Represents a record that the user has performed, failed to perform or skipped a certain habit at
* date. * a certain date.
*/ */
public final class Repetition public final class Repetition
{ {
@ -41,8 +41,10 @@ public final class Repetition
/** /**
* The value of the repetition. * The value of the repetition.
* *
* For boolean habits, this equals Checkmark.CHECKED_EXPLICITLY if performed * For boolean habits, this equals:
* or Checkmark.SKIPPED_EXPLICITLY if skipped. * Checkmark.CHECKED_EXPLICITLY if performed
* Checkmark.SKIPPED_EXPLICITLY if skipped.
* Checkmark.FAILED_EXPLICITLY_NECESSARY if failed.
* For numerical habits, this number is stored in thousandths. That * For numerical habits, this number is stored in thousandths. That
* is, if the user enters value 1.50 on the app, it is here stored as 1500. * is, if the user enters value 1.50 on the app, it is here stored as 1500.
*/ */

@ -151,10 +151,9 @@ public abstract class RepetitionList
for (Repetition r : reps) for (Repetition r : reps)
{ {
if ((habit.getData().type == Habit.YES_NO_HABIT) if (habit.getData().type == Habit.YES_NO_HABIT &&
&& (r.getValue() == Checkmark.SKIPPED_EXPLICITLY)) { r.getValue() != Checkmark.CHECKED_EXPLICITLY)
continue; continue;
}
Calendar date = r.getTimestamp().toCalendar(); Calendar date = r.getTimestamp().toCalendar();
int weekday = r.getTimestamp().getWeekday(); int weekday = r.getTimestamp().getWeekday();
@ -207,13 +206,7 @@ public abstract class RepetitionList
throw new IllegalStateException("habit must NOT be numerical"); throw new IllegalStateException("habit must NOT be numerical");
Repetition rep = getByTimestamp(timestamp); Repetition rep = getByTimestamp(timestamp);
if (rep != null) { if (rep != null) remove(rep);
remove(rep);
if (rep.getValue() == Checkmark.CHECKED_EXPLICITLY) {
rep = new Repetition(timestamp, Checkmark.SKIPPED_EXPLICITLY);
add(rep);
}
}
else else
{ {
rep = new Repetition(timestamp, Checkmark.CHECKED_EXPLICITLY); rep = new Repetition(timestamp, Checkmark.CHECKED_EXPLICITLY);

@ -275,6 +275,7 @@ public abstract class ScoreList implements Iterable<Score>
for (int i = 0; i < checkmarkValues.length; i++) for (int i = 0; i < checkmarkValues.length; i++)
{ {
double value = checkmarkValues[checkmarkValues.length - i - 1]; double value = checkmarkValues[checkmarkValues.length - i - 1];
boolean skip_calculation = false;
if (habit.isNumerical()) if (habit.isNumerical())
{ {
@ -282,14 +283,20 @@ public abstract class ScoreList implements Iterable<Score>
value /= habit.getTargetValue(); value /= habit.getTargetValue();
value = Math.min(1, value); value = Math.min(1, value);
} }
else
if (!habit.isNumerical() && value > 0) { {
value = value != Checkmark.SKIPPED_EXPLICITLY ? 1 : -1; if (value == Checkmark.UNCHECKED ||
value == Checkmark.FAILED_EXPLICITLY_NECESSARY)
value = 0;
else if (value == Checkmark.SKIPPED_EXPLICITLY)
skip_calculation = true;
else
value = 1;
} }
if (value > -1) { if (!skip_calculation)
previousValue = Score.compute(freq, previousValue, value); previousValue = Score.compute(freq, previousValue, value);
}
scores.add(new Score(from.plus(i), previousValue)); scores.add(new Score(from.plus(i), previousValue));
} }

@ -138,17 +138,24 @@ public abstract class StreakList
{ {
boolean isCurrentChecked = ( boolean isCurrentChecked = (
checks[i] == Checkmark.CHECKED_EXPLICITLY || checks[i] == Checkmark.CHECKED_EXPLICITLY ||
checks[i] == Checkmark.CHECKED_IMPLICITLY checks[i] == Checkmark.CHECKED_IMPLICITLY ||
checks[i] == Checkmark.FAILED_EXPLICITLY_UNNECESSARY
); );
if (habit.getData().type == Habit.NUMBER_HABIT || isCurrentChecked) { boolean isCurrentFailed = (
checks[i] == Checkmark.UNCHECKED ||
checks[i] == Checkmark.FAILED_EXPLICITLY_NECESSARY
);
if (habit.getData().type == Habit.NUMBER_HABIT || isCurrentChecked)
lastSuccesful = current; lastSuccesful = current;
}
if (isInStreak && checks[i] == 0) { if (isInStreak && isCurrentFailed)
{
list.add(lastSuccesful); list.add(lastSuccesful);
isInStreak = false; isInStreak = false;
} }
if (!isInStreak && isCurrentChecked) { if (!isInStreak && isCurrentChecked)
{
list.add(current); list.add(current);
isInStreak = true; isInStreak = true;
} }
@ -156,7 +163,8 @@ public abstract class StreakList
current = current.plus(1); current = current.plus(1);
} }
if (isInStreak) list.add(lastSuccesful); if (isInStreak)
list.add(lastSuccesful);
return list; return list;
} }

@ -101,10 +101,10 @@ public class MemoryRepetitionList extends RepetitionList
for (Repetition rep : list) for (Repetition rep : list)
{ {
if (habit.getData().type == Habit.YES_NO_HABIT if (habit.getData().type == Habit.YES_NO_HABIT &&
&& rep.getValue() == Checkmark.SKIPPED_EXPLICITLY) { rep.getValue() != Checkmark.CHECKED_EXPLICITLY)
continue; continue;
}
if (rep.getTimestamp().isOlderThan(oldestTimestamp)) if (rep.getTimestamp().isOlderThan(oldestTimestamp))
{ {
oldestRep = rep; oldestRep = rep;
@ -145,10 +145,10 @@ public class MemoryRepetitionList extends RepetitionList
public long getTotalSuccessfulCount() public long getTotalSuccessfulCount()
{ {
int count = 0; int count = 0;
for (Repetition rep : list) { for (Repetition rep : list)
if (rep.getValue() != Checkmark.SKIPPED_EXPLICITLY) { {
++count; if (rep.getValue() == Checkmark.CHECKED_EXPLICITLY)
} count++;
} }
return count; return count;
} }

@ -244,6 +244,16 @@ public class Preferences
storage.putBoolean("pref_short_toggle", enabled); storage.putBoolean("pref_short_toggle", enabled);
} }
public boolean isAdvancedCheckmarksEnabled()
{
return storage.getBoolean("pref_advanced_checkmarks", false);
}
public void setAdvancedCheckmarksEnabled(boolean enabled)
{
storage.putBoolean("pref_advanced_checkmarks", enabled);
}
public boolean isSyncEnabled() public boolean isSyncEnabled()
{ {
return storage.getBoolean("pref_feature_sync", false); return storage.getBoolean("pref_feature_sync", false);

@ -156,6 +156,18 @@ public class ListHabitsBehavior
habit.getId()); habit.getId());
} }
public void onToggleWithOptions(@NonNull Habit habit, Timestamp timestamp)
{
CheckmarkList checkmarks = habit.getCheckmarks();
int oldValue = checkmarks.getValues(timestamp, timestamp)[0];
screen.showCheckmarkOptions(habit.getName(), timestamp, oldValue, newValue ->
{
commandRunner.execute(
new CreateRepetitionCommand(habit, timestamp, newValue),
habit.getId());
});
}
public enum Message public enum Message
{ {
COULD_NOT_EXPORT, IMPORT_SUCCESSFUL, IMPORT_FAILED, DATABASE_REPAIRED, COULD_NOT_EXPORT, IMPORT_SUCCESSFUL, IMPORT_FAILED, DATABASE_REPAIRED,
@ -181,6 +193,13 @@ public class ListHabitsBehavior
default void onNumberPickerDismissed() {} default void onNumberPickerDismissed() {}
} }
public interface CheckmarkOptionsCallback
{
void onCheckmarkOptionPicked(int newValue);
default void onCheckmarkOptionDismissed() {}
}
public interface Screen public interface Screen
{ {
void showHabitScreen(@NonNull Habit h); void showHabitScreen(@NonNull Habit h);
@ -193,6 +212,11 @@ public class ListHabitsBehavior
@NonNull String unit, @NonNull String unit,
@NonNull NumberPickerCallback callback); @NonNull NumberPickerCallback callback);
void showCheckmarkOptions(String habitName,
Timestamp timestamp,
int value,
@NonNull CheckmarkOptionsCallback callback);
void showSendBugReportToDeveloperScreen(String log); void showSendBugReportToDeveloperScreen(String log);
void showSendFileScreen(@NonNull String filename); void showSendFileScreen(@NonNull String filename);

@ -62,6 +62,12 @@ public class ShowHabitBehavior
new ToggleRepetitionCommand(habitList, habit, timestamp), null); new ToggleRepetitionCommand(habitList, habit, timestamp), null);
} }
public void onCreateRepetition(Timestamp timestamp, int value)
{
commandRunner.execute(
new CreateRepetitionCommand(habit, timestamp, value), null);
}
public interface Screen public interface Screen
{ {
void showEditHistoryScreen(); void showEditHistoryScreen();

@ -53,19 +53,10 @@ public class ToggleRepetitionCommandTest extends BaseUnitTest
{ {
assertTrue(habit.getRepetitions().containsTimestamp(today)); assertTrue(habit.getRepetitions().containsTimestamp(today));
command.execute();
assertEquals(
habit.getRepetitions().getByTimestamp(today).getValue(),
Checkmark.SKIPPED_EXPLICITLY
);
command.execute(); command.execute();
assertFalse(habit.getRepetitions().containsTimestamp(today)); assertFalse(habit.getRepetitions().containsTimestamp(today));
command.undo(); command.undo();
assertEquals(
habit.getRepetitions().getByTimestamp(today).getValue(),
Checkmark.SKIPPED_EXPLICITLY
);
command.execute(); command.execute();
assertFalse(habit.getRepetitions().containsTimestamp(today)); assertFalse(habit.getRepetitions().containsTimestamp(today));

@ -31,7 +31,6 @@ import static java.util.Calendar.*;
import static junit.framework.TestCase.assertFalse; import static junit.framework.TestCase.assertFalse;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.IsEqual.*; import static org.hamcrest.core.IsEqual.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@ -158,20 +157,16 @@ public class RepetitionListTest extends BaseUnitTest
@Test @Test
public void test_toggle() public void test_toggle()
{ {
assertEquals(reps.getByTimestamp(today).getValue(), Checkmark.CHECKED_EXPLICITLY); assertTrue(reps.containsTimestamp(today));
reps.toggle(today);
assertEquals(reps.getByTimestamp(today).getValue(), Checkmark.SKIPPED_EXPLICITLY);
reps.toggle(today); reps.toggle(today);
assertFalse(reps.containsTimestamp(today)); assertFalse(reps.containsTimestamp(today));
verify(listener, times(3)).onModelChange(); verify(listener).onModelChange();
reset(listener); reset(listener);
assertFalse(reps.containsTimestamp(today.minus(1))); assertFalse(reps.containsTimestamp(today.minus(1)));
reps.toggle(today.minus(1)); reps.toggle(today.minus(1));
assertEquals(reps.getByTimestamp(today.minus(1)).getValue(), Checkmark.CHECKED_EXPLICITLY); assertTrue(reps.containsTimestamp(today.minus(1)));
reps.toggle(today.minus(1)); verify(listener).onModelChange();
assertEquals(reps.getByTimestamp(today.minus(1)).getValue(), Checkmark.SKIPPED_EXPLICITLY);
verify(listener, times(3)).onModelChange();
reset(listener); reset(listener);
habit.setType(Habit.NUMBER_HABIT); habit.setType(Habit.NUMBER_HABIT);

@ -172,8 +172,6 @@ public class ListHabitsBehaviorTest extends BaseUnitTest
@Test @Test
public void testOnToggle() public void testOnToggle()
{ {
assertTrue(habit1.isCompletedToday());
behavior.onToggle(habit1, DateUtils.getToday());
assertTrue(habit1.isCompletedToday()); assertTrue(habit1.isCompletedToday());
behavior.onToggle(habit1, DateUtils.getToday()); behavior.onToggle(habit1, DateUtils.getToday());
assertFalse(habit1.isCompletedToday()); assertFalse(habit1.isCompletedToday());

Loading…
Cancel
Save