Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue 1316: Skip measurable habit #1319

Merged
merged 19 commits into from
Mar 24, 2022
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ package org.isoron.uhabits.activities.common.dialogs
import android.annotation.SuppressLint
import android.content.Context
import android.content.DialogInterface
import android.content.DialogInterface.BUTTON_NEGATIVE
import android.text.InputFilter
import android.text.Spanned
import android.view.LayoutInflater
Expand All @@ -33,7 +34,11 @@ import android.widget.EditText
import android.widget.NumberPicker
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.core.models.Frequency.Companion.DAILY
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.utils.InterfaceUtils
Expand All @@ -52,6 +57,7 @@ class NumberPickerFactory
unit: String,
notes: String,
dateString: String,
frequency: Frequency,
callback: ListHabitsBehavior.NumberPickerCallback
): AlertDialog {
val inflater = LayoutInflater.from(context)
Expand Down Expand Up @@ -92,7 +98,7 @@ class NumberPickerFactory
picker2.value = intValue % 100

etNotes.setText(notes)
val dialog = AlertDialog.Builder(context)
val dialogBuilder = AlertDialog.Builder(context)
.setView(view)
.setTitle(dateString)
.setPositiveButton(R.string.save) { _, _ ->
Expand All @@ -108,9 +114,24 @@ class NumberPickerFactory
.setOnDismissListener {
callback.onNumberPickerDismissed()
}
.create()

if (frequency == DAILY) {
dialogBuilder.setNegativeButton(R.string.skip_day) { _, _ ->
picker.clearFocus()
val v = Entry.SKIP.toDouble() / 1000
val note = etNotes.text.toString()
callback.onNumberPicked(v, note)
}
}

val dialog = dialogBuilder.create()

dialog.setOnShowListener {
val preferences =
(context.applicationContext as HabitsApplication).component.preferences
if (!preferences.isSkipEnabled) {
dialog.getButton(BUTTON_NEGATIVE).visibility = View.GONE
}
showSoftInput(dialog, pickerInputText)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,8 @@ class FrequencyChart : ScrollableChart {
rect[0f, 0f, baseSize.toFloat()] = baseSize.toFloat()
rect.offset(prevRect!!.left, prevRect!!.top + baseSize * j)
val i = localeWeekdayList[j] % 7
if (values != null) drawMarker(canvas, rect, values[i])
if (values != null)
drawMarker(canvas, rect, values[i])
rect.offset(0f, rowHeight)
}
drawFooter(canvas, rect, date)
Expand Down Expand Up @@ -222,11 +223,14 @@ class FrequencyChart : ScrollableChart {
}

private fun drawMarker(canvas: Canvas, rect: RectF?, value: Int?) {
// value can be negative when the entry is skipped
val valueCopy = value?.let { max(0, it) }

val padding = rect!!.height() * 0.2f
// maximal allowed mark radius
val maxRadius = (rect.height() - 2 * padding) / 2.0f
// the real mark radius is scaled down by a factor depending on the maximal frequency
val scale = 1.0f / maxFreq * value!!
val scale = 1.0f / maxFreq * valueCopy!!
val radius = maxRadius * scale
val colorIndex = min((colors.size - 1), ((colors.size - 1) * scale).roundToInt())
pGraph!!.color = colors[colorIndex]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import org.isoron.uhabits.core.commands.CreateHabitCommand
import org.isoron.uhabits.core.commands.DeleteHabitsCommand
import org.isoron.uhabits.core.commands.EditHabitCommand
import org.isoron.uhabits.core.commands.UnarchiveHabitsCommand
import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.tasks.TaskRunner
Expand Down Expand Up @@ -230,9 +231,10 @@ class ListHabitsScreen
unit: String,
notes: String,
dateString: String,
frequency: Frequency,
callback: ListHabitsBehavior.NumberPickerCallback
) {
numberPickerFactory.create(value, unit, notes, dateString, callback).show()
numberPickerFactory.create(value, unit, notes, dateString, frequency, callback).show()
}

override fun showCheckmarkDialog(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import android.view.View
import android.view.View.OnClickListener
import android.view.View.OnLongClickListener
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.NumericalHabitType.AT_LEAST
import org.isoron.uhabits.core.models.NumericalHabitType.AT_MOST
import org.isoron.uhabits.core.preferences.Preferences
Expand Down Expand Up @@ -143,6 +144,12 @@ class NumberButtonView(
private val lowContrast: Int
private val mediumContrast: Int

private val paint = TextPaint().apply {
typeface = getFontAwesome()
isAntiAlias = true
textAlign = Paint.Align.CENTER
}

private val pUnit: TextPaint = TextPaint().apply {
textSize = getDimension(context, R.dimen.smallerTextSize)
typeface = NORMAL_TYPEFACE
Expand Down Expand Up @@ -181,6 +188,11 @@ class NumberButtonView(
typeface = BOLD_TYPEFACE
textSize = dim(R.dimen.smallTextSize)
}
value == Entry.SKIP.toDouble() / 1000 -> {
label = resources.getString(R.string.fa_skipped)
textSize = dim(R.dimen.smallTextSize)
typeface = getFontAwesome()
}
kalina559 marked this conversation as resolved.
Show resolved Hide resolved
preferences.areQuestionMarksEnabled -> {
label = resources.getString(R.string.fa_question)
typeface = getFontAwesome()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import org.isoron.uhabits.activities.common.dialogs.HistoryEditorDialog
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
import org.isoron.uhabits.core.commands.Command
import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.preferences.Preferences
Expand Down Expand Up @@ -169,9 +170,10 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
unit: String,
notes: String,
dateString: String,
callback: ListHabitsBehavior.NumberPickerCallback,
frequency: Frequency,
callback: ListHabitsBehavior.NumberPickerCallback
) {
NumberPickerFactory(this@ShowHabitActivity).create(value, unit, notes, dateString, callback).show()
NumberPickerFactory(this@ShowHabitActivity).create(value, unit, notes, dateString, frequency, callback).show()
}

override fun showCheckmarkDialog(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ class NumericalCheckmarkWidgetActivity : Activity(), ListHabitsBehavior.NumberPi
data.habit.unit,
entry.notes,
today.toDialogDateString(),
data.habit.frequency,
this
).show()
}
Expand Down
1 change: 1 addition & 0 deletions uhabits-android/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@
<string name="increment">Increment</string>
<string name="decrement">Decrement</string>
<string name="pref_skip_title">Enable skip days</string>
<string name="skip_day">Skip</string>
<string name="pref_skip_description">Toggle twice to add a skip instead of a checkmark. Skips keep your score unchanged and don\'t break your streak.</string>
<string name="pref_unknown_title">Show question marks for missing data</string>
<string name="pref_unknown_description">Differentiate days without data from actual lapses. To enter a lapse, toggle twice.</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,8 @@ open class EntryList {
* For numerical habits, non-positive entry values are converted to zero. For boolean habits, each
* YES_MANUAL value is converted to 1000 and all other values are converted to zero.
*
* SKIP values are treated converted to zero (if they weren't, each SKIP day would count as 0.003).
*
* The returned list is sorted by timestamp, with the newest entry coming first and the oldest entry
* coming last. If the original list has gaps in it (for example, weeks or months without any
* entries), then the list produced by this method will also have gaps.
Expand All @@ -289,7 +291,10 @@ fun List<Entry>.groupedSum(
): List<Entry> {
return this.map { (timestamp, value) ->
if (isNumerical) {
Entry(timestamp, max(0, value))
if (value == SKIP)
Entry(timestamp, 0)
else
Entry(timestamp, max(0, value))
} else {
Entry(timestamp, if (value == YES_MANUAL) 1000 else 0)
}
Expand All @@ -301,6 +306,32 @@ fun List<Entry>.groupedSum(
}.entries.map { (timestamp, entries) ->
Entry(timestamp, entries.sumOf { it.value })
}.sortedBy { (timestamp, _) ->
- timestamp.unixTime
-timestamp.unixTime
}
}

/**
* Counts the number of days with vaLue SKIP in the given perio
kalina559 marked this conversation as resolved.
Show resolved Hide resolved
*/
fun List<Entry>.countSkippedDays(
truncateField: DateUtils.TruncateField,
firstWeekday: Int = Calendar.SATURDAY,
isNumerical: Boolean,
): List<Entry> {
return this.map { (timestamp, value) ->
if (value == SKIP) {
Entry(timestamp, 1)
} else {
Entry(timestamp, 0)
}
}.groupBy { entry ->
entry.timestamp.truncate(
truncateField,
firstWeekday,
)
}.entries.map { (timestamp, entries) ->
Entry(timestamp, entries.sumOf { it.value })
}.sortedBy { (timestamp, _) ->
-timestamp.unixTime
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,20 +100,25 @@ class ScoreList {
}

val normalizedRollingSum = rollingSum / 1000
val percentageCompleted = if (!isAtMost) {
if (targetValue > 0)
min(1.0, normalizedRollingSum / targetValue)
else
1.0
} else {
if (targetValue > 0) {
(1 - ((normalizedRollingSum - targetValue) / targetValue)).coerceIn(0.0, 1.0)
if (values[offset] != Entry.SKIP) {
val percentageCompleted = if (!isAtMost) {
if (targetValue > 0)
min(1.0, normalizedRollingSum / targetValue)
else
1.0
} else {
if (normalizedRollingSum > 0) 0.0 else 1.0
if (targetValue > 0) {
(1 - ((normalizedRollingSum - targetValue) / targetValue)).coerceIn(
0.0,
1.0
)
} else {
if (normalizedRollingSum > 0) 0.0 else 1.0
}
}
}

previousValue = compute(freq, previousValue, percentageCompleted)
previousValue = compute(freq, previousValue, percentageCompleted)
}
kalina559 marked this conversation as resolved.
Show resolved Hide resolved
} else {
if (values[offset] == Entry.YES_MANUAL) {
rollingSum += 1.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package org.isoron.uhabits.core.ui.screens.habits.list
import org.isoron.platform.time.LocalDate
import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.commands.CreateRepetitionCommand
import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.HabitType
Expand Down Expand Up @@ -58,6 +59,7 @@ open class ListHabitsBehavior @Inject constructor(
habit.unit,
entry.notes,
timestamp.toDialogDateString(),
habit.frequency
) { newValue: Double, newNotes: String, ->
val value = (newValue * 1000).roundToInt()
commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, value, newNotes))
Expand Down Expand Up @@ -167,6 +169,7 @@ open class ListHabitsBehavior @Inject constructor(
unit: String,
notes: String,
dateString: String,
frequency: Frequency,
callback: NumberPickerCallback
)
fun showCheckmarkDialog(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.Entry.Companion.SKIP
import org.isoron.uhabits.core.models.Entry.Companion.YES_AUTO
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.NumericalHabitType.AT_LEAST
Expand Down Expand Up @@ -123,6 +124,7 @@ class HistoryCardPresenter(
habit.unit,
entry.notes,
timestamp.toDialogDateString(),
frequency = habit.frequency
) { newValue: Double, newNotes: String ->
val thousands = (newValue * 1000).roundToInt()
commandRunner.run(
Expand Down Expand Up @@ -154,6 +156,7 @@ class HistoryCardPresenter(
entries.map {
when {
it.value == Entry.UNKNOWN -> OFF
it.value == SKIP -> HATCHED
(habit.targetType == AT_MOST) && (it.value / 1000.0 <= habit.targetValue) -> ON
(habit.targetType == AT_LEAST) && (it.value / 1000.0 >= habit.targetValue) -> ON
else -> GREY
Expand Down Expand Up @@ -196,8 +199,10 @@ class HistoryCardPresenter(
unit: String,
notes: String,
dateString: String,
callback: ListHabitsBehavior.NumberPickerCallback,
frequency: Frequency,
callback: ListHabitsBehavior.NumberPickerCallback
)

fun showCheckmarkDialog(
selectedValue: Int,
notes: String,
Expand Down
Loading