Merge pull request #1103 from vbh/feat-1074

Add notes to specific dates
pull/1212/head
Alinson S. Xavier 4 years ago committed by GitHub
commit ecb8ce105a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -116,15 +116,7 @@ class NumberButtonViewTest : BaseViewTest() {
} }
@Test @Test
fun testClick_shortToggleDisabled() { fun testClick() {
prefs.isShortToggleEnabled = false
view.performClick()
assertFalse(edited)
}
@Test
fun testClick_shortToggleEnabled() {
prefs.isShortToggleEnabled = true
view.performClick() view.performClick()
assertTrue(edited) assertTrue(edited)
} }

@ -61,7 +61,7 @@ class PerformanceTest : BaseAndroidTest() {
val habit = fixtures.createEmptyHabit() val habit = fixtures.createEmptyHabit()
for (i in 0..4999) { for (i in 0..4999) {
val timestamp: Timestamp = Timestamp(i * DAY_LENGTH) val timestamp: Timestamp = Timestamp(i * DAY_LENGTH)
CreateRepetitionCommand(habitList, habit, timestamp, 1).run() CreateRepetitionCommand(habitList, habit, timestamp, 1, "").run()
} }
db.setTransactionSuccessful() db.setTransactionSuccessful()
db.endTransaction() db.endTransaction()

@ -49,23 +49,12 @@ class AndroidDataView(
override fun onShowPress(e: MotionEvent?) = Unit override fun onShowPress(e: MotionEvent?) = Unit
override fun onSingleTapUp(e: MotionEvent?): Boolean { override fun onSingleTapUp(e: MotionEvent?): Boolean {
val x: Float return handleClick(e, true)
val y: Float
try {
val pointerId = e!!.getPointerId(0)
x = e.getX(pointerId)
y = e.getY(pointerId)
} catch (ex: RuntimeException) {
// Android often throws IllegalArgumentException here. Apparently,
// the pointer id may become invalid shortly after calling
// e.getPointerId.
return false
}
view?.onClick(x / canvas.innerDensity, y / canvas.innerDensity)
return true
} }
override fun onLongPress(e: MotionEvent?) = Unit override fun onLongPress(e: MotionEvent?) {
handleClick(e)
}
override fun onScroll( override fun onScroll(
e1: MotionEvent?, e1: MotionEvent?,
@ -137,4 +126,22 @@ class AndroidDataView(
} }
} }
} }
private fun handleClick(e: MotionEvent?, isSingleTap: Boolean = false): Boolean {
val x: Float
val y: Float
try {
val pointerId = e!!.getPointerId(0)
x = e.getX(pointerId)
y = e.getY(pointerId)
} catch (ex: RuntimeException) {
// Android often throws IllegalArgumentException here. Apparently,
// the pointer id may become invalid shortly after calling
// e.getPointerId.
return false
}
if (isSingleTap) view?.onClick(x / canvas.innerDensity, y / canvas.innerDensity)
else view?.onLongClick(x / canvas.innerDensity, y / canvas.innerDensity)
return true
}
} }

@ -0,0 +1,116 @@
package org.isoron.uhabits.activities.common.dialogs
import android.content.Context
import android.graphics.Typeface
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE
import android.widget.Button
import androidx.appcompat.app.AlertDialog
import org.isoron.platform.gui.toInt
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Entry.Companion.NO
import org.isoron.uhabits.core.models.Entry.Companion.SKIP
import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN
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.PaletteColor
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
import org.isoron.uhabits.core.ui.views.Theme
import org.isoron.uhabits.databinding.CheckmarkDialogBinding
import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.utils.InterfaceUtils
import org.isoron.uhabits.utils.StyledResources
import javax.inject.Inject
class CheckmarkDialog
@Inject constructor(
@ActivityContext private val context: Context,
private val preferences: Preferences,
) : View.OnClickListener {
private lateinit var binding: CheckmarkDialogBinding
private lateinit var fontAwesome: Typeface
private val allButtons = mutableListOf<Button>()
private var selectedButton: Button? = null
fun create(
value: Int,
notes: String,
dateString: String,
paletteColor: PaletteColor,
callback: ListHabitsBehavior.CheckMarkDialogCallback,
theme: Theme,
): AlertDialog {
binding = CheckmarkDialogBinding.inflate(LayoutInflater.from(context))
fontAwesome = InterfaceUtils.getFontAwesome(context)!!
binding.etNotes.append(notes)
setUpButtons(value, theme.color(paletteColor).toInt())
val dialog = AlertDialog.Builder(context)
.setView(binding.root)
.setTitle(dateString)
.setPositiveButton(R.string.save) { _, _ ->
val newValue = when (selectedButton?.id) {
R.id.yesBtn -> YES_MANUAL
R.id.noBtn -> NO
R.id.skippedBtn -> SKIP
else -> UNKNOWN
}
callback.onNotesSaved(newValue, binding.etNotes.text.toString())
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
callback.onNotesDismissed()
}
.setOnDismissListener {
callback.onNotesDismissed()
}
.create()
dialog.setOnShowListener {
binding.etNotes.requestFocus()
dialog.window?.setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_VISIBLE)
}
return dialog
}
private fun setUpButtons(value: Int, color: Int) {
val sres = StyledResources(context)
val mediumContrastColor = sres.getColor(R.attr.contrast60)
setButtonAttrs(binding.yesBtn, color)
setButtonAttrs(binding.noBtn, mediumContrastColor)
setButtonAttrs(binding.skippedBtn, color, visible = preferences.isSkipEnabled)
setButtonAttrs(binding.questionBtn, mediumContrastColor, visible = preferences.areQuestionMarksEnabled)
when (value) {
UNKNOWN -> if (preferences.areQuestionMarksEnabled) {
binding.questionBtn.performClick()
} else {
binding.noBtn.performClick()
}
SKIP -> binding.skippedBtn.performClick()
YES_MANUAL -> binding.yesBtn.performClick()
YES_AUTO, NO -> binding.noBtn.performClick()
}
}
private fun setButtonAttrs(button: Button, color: Int, visible: Boolean = true) {
button.apply {
visibility = if (visible) View.VISIBLE else View.GONE
typeface = fontAwesome
setTextColor(color)
setOnClickListener(this@CheckmarkDialog)
}
allButtons.add(button)
}
override fun onClick(v: View?) {
allButtons.forEach {
if (v?.id == it.id) {
it.isSelected = true
selectedButton = it
} else it.isSelected = false
}
}
}

@ -63,9 +63,10 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
paletteColor = habit.color, paletteColor = habit.color,
series = emptyList(), series = emptyList(),
defaultSquare = HistoryChart.Square.OFF, defaultSquare = HistoryChart.Square.OFF,
notesIndicators = emptyList(),
theme = themeSwitcher.currentTheme, theme = themeSwitcher.currentTheme,
today = DateUtils.getTodayWithOffset().toLocalDate(), today = DateUtils.getTodayWithOffset().toLocalDate(),
onDateClickedListener = onDateClickedListener ?: OnDateClickedListener { }, onDateClickedListener = onDateClickedListener ?: object : OnDateClickedListener {},
padding = 10.0, padding = 10.0,
) )
dataView = AndroidDataView(context!!, null) dataView = AndroidDataView(context!!, null)
@ -103,6 +104,7 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
) )
chart?.series = model.series chart?.series = model.series
chart?.defaultSquare = model.defaultSquare chart?.defaultSquare = model.defaultSquare
chart?.notesIndicators = model.notesIndicators
dataView.postInvalidate() dataView.postInvalidate()
} }

@ -47,6 +47,8 @@ class NumberPickerFactory
fun create( fun create(
value: Double, value: Double,
unit: String, unit: String,
notes: String,
dateString: String,
callback: ListHabitsBehavior.NumberPickerCallback callback: ListHabitsBehavior.NumberPickerCallback
): AlertDialog { ): AlertDialog {
val inflater = LayoutInflater.from(context) val inflater = LayoutInflater.from(context)
@ -54,6 +56,7 @@ class NumberPickerFactory
val picker = view.findViewById<NumberPicker>(R.id.picker) val picker = view.findViewById<NumberPicker>(R.id.picker)
val picker2 = view.findViewById<NumberPicker>(R.id.picker2) val picker2 = view.findViewById<NumberPicker>(R.id.picker2)
val etNotes = view.findViewById<EditText>(R.id.etNotes)
val watcherFilter: InputFilter = SeparatorWatcherInputFilter(picker2) val watcherFilter: InputFilter = SeparatorWatcherInputFilter(picker2)
val numberPickerInputText = getNumberPickerInputText(picker) val numberPickerInputText = getNumberPickerInputText(picker)
@ -77,13 +80,18 @@ class NumberPickerFactory
picker2.setFormatter { v -> String.format("%02d", v) } picker2.setFormatter { v -> String.format("%02d", v) }
picker2.value = intValue % 100 picker2.value = intValue % 100
etNotes.setText(notes)
val dialog = AlertDialog.Builder(context) val dialog = AlertDialog.Builder(context)
.setView(view) .setView(view)
.setTitle(R.string.change_value) .setTitle(dateString)
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(R.string.save) { _, _ ->
picker.clearFocus() picker.clearFocus()
val v = picker.value + 0.01 * picker2.value val v = picker.value + 0.01 * picker2.value
callback.onNumberPicked(v) val note = etNotes.text.toString()
callback.onNumberPicked(v, note)
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
callback.onNumberPickerDismissed()
} }
.setOnDismissListener { .setOnDismissListener {
callback.onNumberPickerDismissed() callback.onNumberPickerDismissed()

@ -25,6 +25,7 @@ import android.content.Intent
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import dagger.Lazy import dagger.Lazy
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.dialogs.CheckmarkDialog
import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialogFactory import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialogFactory
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
@ -89,6 +90,7 @@ class ListHabitsScreen
private val importTaskFactory: ImportDataTaskFactory, private val importTaskFactory: ImportDataTaskFactory,
private val colorPickerFactory: ColorPickerDialogFactory, private val colorPickerFactory: ColorPickerDialogFactory,
private val numberPickerFactory: NumberPickerFactory, private val numberPickerFactory: NumberPickerFactory,
private val checkMarkDialog: CheckmarkDialog,
private val behavior: Lazy<ListHabitsBehavior> private val behavior: Lazy<ListHabitsBehavior>
) : CommandRunner.Listener, ) : CommandRunner.Listener,
ListHabitsBehavior.Screen, ListHabitsBehavior.Screen,
@ -225,9 +227,28 @@ class ListHabitsScreen
override fun showNumberPicker( override fun showNumberPicker(
value: Double, value: Double,
unit: String, unit: String,
notes: String,
dateString: String,
callback: ListHabitsBehavior.NumberPickerCallback callback: ListHabitsBehavior.NumberPickerCallback
) { ) {
numberPickerFactory.create(value, unit, callback).show() numberPickerFactory.create(value, unit, notes, dateString, callback).show()
}
override fun showCheckmarkDialog(
value: Int,
notes: String,
dateString: String,
color: PaletteColor,
callback: ListHabitsBehavior.CheckMarkDialogCallback
) {
checkMarkDialog.create(
value,
notes,
dateString,
color,
callback,
themeSwitcher.currentTheme!!,
).show()
} }
private fun getExecuteString(command: Command): String? { private fun getExecuteString(command: Command): String? {

@ -37,8 +37,8 @@ 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.Entry.Companion.YES_MANUAL
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.inject.ActivityContext import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.utils.drawNotesIndicator
import org.isoron.uhabits.utils.getFontAwesome import org.isoron.uhabits.utils.getFontAwesome
import org.isoron.uhabits.utils.showMessage
import org.isoron.uhabits.utils.sp import org.isoron.uhabits.utils.sp
import org.isoron.uhabits.utils.sres import org.isoron.uhabits.utils.sres
import org.isoron.uhabits.utils.toMeasureSpec import org.isoron.uhabits.utils.toMeasureSpec
@ -71,7 +71,15 @@ class CheckmarkButtonView(
invalidate() invalidate()
} }
var hasNotes = false
set(value) {
field = value
invalidate()
}
var onToggle: (Int) -> Unit = {} var onToggle: (Int) -> Unit = {}
var onEdit: () -> Unit = {}
private var drawer = Drawer() private var drawer = Drawer()
init { init {
@ -93,11 +101,12 @@ class CheckmarkButtonView(
override fun onClick(v: View) { override fun onClick(v: View) {
if (preferences.isShortToggleEnabled) performToggle() if (preferences.isShortToggleEnabled) performToggle()
else showMessage(resources.getString(R.string.long_press_to_toggle)) else onEdit()
} }
override fun onLongClick(v: View): Boolean { override fun onLongClick(v: View): Boolean {
performToggle() if (preferences.isShortToggleEnabled) onEdit()
else performToggle()
return true return true
} }
@ -170,6 +179,8 @@ class CheckmarkButtonView(
paint.style = Paint.Style.FILL paint.style = Paint.Style.FILL
canvas.drawText(label, rect.centerX(), rect.centerY(), paint) canvas.drawText(label, rect.centerX(), rect.centerY(), paint)
} }
drawNotesIndicator(canvas, color, em, hasNotes)
} }
} }
} }

@ -54,12 +54,24 @@ class CheckmarkPanelView(
setupButtons() setupButtons()
} }
var notesIndicators = BooleanArray(0)
set(values) {
field = values
setupButtons()
}
var onToggle: (Timestamp, Int) -> Unit = { _, _ -> } var onToggle: (Timestamp, Int) -> Unit = { _, _ -> }
set(value) { set(value) {
field = value field = value
setupButtons() setupButtons()
} }
var onEdit: (Timestamp) -> Unit = {}
set(value) {
field = value
setupButtons()
}
override fun createButton(): CheckmarkButtonView = buttonFactory.create() override fun createButton(): CheckmarkButtonView = buttonFactory.create()
@Synchronized @Synchronized
@ -72,8 +84,13 @@ class CheckmarkPanelView(
index + dataOffset < values.size -> values[index + dataOffset] index + dataOffset < values.size -> values[index + dataOffset]
else -> UNKNOWN else -> UNKNOWN
} }
button.hasNotes = when {
index + dataOffset < notesIndicators.size -> notesIndicators[index + dataOffset]
else -> false
}
button.color = color button.color = color
button.onToggle = { value -> onToggle(timestamp, value) } button.onToggle = { value -> onToggle(timestamp, value) }
button.onEdit = { onEdit(timestamp) }
} }
} }
} }

@ -124,8 +124,9 @@ class HabitCardListAdapter @Inject constructor(
val habit = cache.getHabitByPosition(position) val habit = cache.getHabitByPosition(position)
val score = cache.getScore(habit!!.id!!) val score = cache.getScore(habit!!.id!!)
val checkmarks = cache.getCheckmarks(habit.id!!) val checkmarks = cache.getCheckmarks(habit.id!!)
val notesIndicators = cache.getNoteIndicators(habit.id!!)
val selected = selected.contains(habit) val selected = selected.contains(habit)
listView!!.bindCardView(holder, habit, score, checkmarks, selected) listView!!.bindCardView(holder, habit, score, checkmarks, notesIndicators, selected)
} }
override fun onViewAttachedToWindow(holder: HabitCardViewHolder) { override fun onViewAttachedToWindow(holder: HabitCardViewHolder) {

@ -87,6 +87,7 @@ class HabitCardListView(
habit: Habit, habit: Habit,
score: Double, score: Double,
checkmarks: IntArray, checkmarks: IntArray,
notesIndicators: BooleanArray,
selected: Boolean selected: Boolean
): View { ): View {
val cardView = holder.itemView as HabitCardView val cardView = holder.itemView as HabitCardView
@ -98,6 +99,7 @@ class HabitCardListView(
cardView.score = score cardView.score = score
cardView.unit = habit.unit cardView.unit = habit.unit
cardView.threshold = habit.targetValue / habit.frequency.denominator cardView.threshold = habit.targetValue / habit.frequency.denominator
cardView.notesIndicators = notesIndicators
val detector = GestureDetector(context, CardViewGestureDetector(holder)) val detector = GestureDetector(context, CardViewGestureDetector(holder))
cardView.setOnTouchListener { _, ev -> cardView.setOnTouchListener { _, ev ->

@ -116,6 +116,13 @@ class HabitCardView(
numberPanel.threshold = value numberPanel.threshold = value
} }
var notesIndicators
get() = checkmarkPanel.notesIndicators
set(values) {
checkmarkPanel.notesIndicators = values
numberPanel.notesIndicators = values
}
var checkmarkPanel: CheckmarkPanelView var checkmarkPanel: CheckmarkPanelView
private var numberPanel: NumberPanelView private var numberPanel: NumberPanelView
private var innerFrame: LinearLayout private var innerFrame: LinearLayout
@ -150,6 +157,10 @@ class HabitCardView(
}.delay(TOGGLE_DELAY_MILLIS) }.delay(TOGGLE_DELAY_MILLIS)
} }
} }
onEdit = { timestamp ->
triggerRipple(timestamp)
habit?.let { behavior.onEdit(it, timestamp) }
}
} }
numberPanel = numberPanelFactory.create().apply { numberPanel = numberPanelFactory.create().apply {

@ -34,8 +34,8 @@ import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.inject.ActivityContext import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.utils.InterfaceUtils.getDimension import org.isoron.uhabits.utils.InterfaceUtils.getDimension
import org.isoron.uhabits.utils.dim import org.isoron.uhabits.utils.dim
import org.isoron.uhabits.utils.drawNotesIndicator
import org.isoron.uhabits.utils.getFontAwesome import org.isoron.uhabits.utils.getFontAwesome
import org.isoron.uhabits.utils.showMessage
import org.isoron.uhabits.utils.sres import org.isoron.uhabits.utils.sres
import java.lang.Double.max import java.lang.Double.max
import java.text.DecimalFormat import java.text.DecimalFormat
@ -101,6 +101,11 @@ class NumberButtonView(
field = value field = value
invalidate() invalidate()
} }
var hasNotes = false
set(value) {
field = value
invalidate()
}
var onEdit: () -> Unit = {} var onEdit: () -> Unit = {}
private var drawer: Drawer = Drawer(context) private var drawer: Drawer = Drawer(context)
@ -111,8 +116,7 @@ class NumberButtonView(
} }
override fun onClick(v: View) { override fun onClick(v: View) {
if (preferences.isShortToggleEnabled) onEdit() onEdit()
else showMessage(resources.getString(R.string.long_press_to_edit))
} }
override fun onLongClick(v: View): Boolean { override fun onLongClick(v: View): Boolean {
@ -211,6 +215,8 @@ class NumberButtonView(
rect.offset(0f, 1.3f * em) rect.offset(0f, 1.3f * em)
canvas.drawText(units, rect.centerX(), rect.centerY(), pUnit) canvas.drawText(units, rect.centerX(), rect.centerY(), pUnit)
} }
drawNotesIndicator(canvas, color, em, hasNotes)
} }
} }
} }

@ -72,6 +72,12 @@ class NumberPanelView(
setupButtons() setupButtons()
} }
var notesIndicators = BooleanArray(0)
set(values) {
field = values
setupButtons()
}
var onEdit: (Timestamp) -> Unit = {} var onEdit: (Timestamp) -> Unit = {}
set(value) { set(value) {
field = value field = value
@ -90,6 +96,10 @@ class NumberPanelView(
index + dataOffset < values.size -> values[index + dataOffset] index + dataOffset < values.size -> values[index + dataOffset]
else -> 0.0 else -> 0.0
} }
button.hasNotes = when {
index + dataOffset < notesIndicators.size -> notesIndicators[index + dataOffset]
else -> false
}
button.color = color button.color = color
button.targetType = targetType button.targetType = targetType
button.threshold = threshold button.threshold = threshold

@ -32,12 +32,14 @@ import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.AndroidThemeSwitcher import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.activities.HabitsDirFinder import org.isoron.uhabits.activities.HabitsDirFinder
import org.isoron.uhabits.activities.common.dialogs.CheckmarkDialog
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
import org.isoron.uhabits.activities.common.dialogs.HistoryEditorDialog import org.isoron.uhabits.activities.common.dialogs.HistoryEditorDialog
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
import org.isoron.uhabits.core.commands.Command import org.isoron.uhabits.core.commands.Command
import org.isoron.uhabits.core.commands.CommandRunner import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
@ -164,9 +166,29 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
override fun showNumberPicker( override fun showNumberPicker(
value: Double, value: Double,
unit: String, unit: String,
notes: String,
dateString: String,
callback: ListHabitsBehavior.NumberPickerCallback, callback: ListHabitsBehavior.NumberPickerCallback,
) { ) {
NumberPickerFactory(this@ShowHabitActivity).create(value, unit, callback).show() NumberPickerFactory(this@ShowHabitActivity).create(value, unit, notes, dateString, callback).show()
}
override fun showCheckmarkDialog(
value: Int,
notes: String,
dateString: String,
preferences: Preferences,
color: PaletteColor,
callback: ListHabitsBehavior.CheckMarkDialogCallback
) {
CheckmarkDialog(this@ShowHabitActivity, preferences).create(
value,
notes,
dateString,
color,
callback,
themeSwitcher.currentTheme!!,
).show()
} }
override fun showEditHabitScreen(habit: Habit) { override fun showEditHabitScreen(habit: Habit) {

@ -44,6 +44,7 @@ class HistoryCardView(context: Context, attrs: AttributeSet) : LinearLayout(cont
dateFormatter = JavaLocalDateFormatter(Locale.getDefault()), dateFormatter = JavaLocalDateFormatter(Locale.getDefault()),
series = state.series, series = state.series,
defaultSquare = state.defaultSquare, defaultSquare = state.defaultSquare,
notesIndicators = state.notesIndicators,
firstWeekday = state.firstWeekday, firstWeekday = state.firstWeekday,
) )
binding.chart.postInvalidate() binding.chart.postInvalidate()

@ -22,7 +22,9 @@ package org.isoron.uhabits.utils
import android.app.Activity import android.app.Activity
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.graphics.Paint
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.os.Handler import android.os.Handler
import android.view.LayoutInflater import android.view.LayoutInflater
@ -199,5 +201,15 @@ fun View.dim(id: Int) = InterfaceUtils.getDimension(context, id)
fun View.sp(value: Float) = InterfaceUtils.spToPixels(context, value) fun View.sp(value: Float) = InterfaceUtils.spToPixels(context, value)
fun View.dp(value: Float) = InterfaceUtils.dpToPixels(context, value) fun View.dp(value: Float) = InterfaceUtils.dpToPixels(context, value)
fun View.str(id: Int) = resources.getString(id) fun View.str(id: Int) = resources.getString(id)
fun View.drawNotesIndicator(canvas: Canvas, color: Int, size: Float, hasNotes: Boolean) {
val pNotesIndicator = Paint()
pNotesIndicator.color = color
if (hasNotes) {
val cy = 0.8f * size
canvas.drawCircle(width.toFloat() - cy, cy, 8f, pNotesIndicator)
}
}
val View.sres: StyledResources val View.sres: StyledResources
get() = StyledResources(context) get() = StyledResources(context)

@ -59,6 +59,7 @@ class HistoryWidget(
val historyChart = (this.view as HistoryChart) val historyChart = (this.view as HistoryChart)
historyChart.series = model.series historyChart.series = model.series
historyChart.defaultSquare = model.defaultSquare historyChart.defaultSquare = model.defaultSquare
historyChart.notesIndicators = model.notesIndicators
} }
} }
@ -74,6 +75,7 @@ class HistoryWidget(
firstWeekday = prefs.firstWeekday, firstWeekday = prefs.firstWeekday,
series = listOf(), series = listOf(),
defaultSquare = HistoryChart.Square.OFF, defaultSquare = HistoryChart.Square.OFF,
notesIndicators = listOf(),
) )
} }
).apply { ).apply {

@ -60,8 +60,8 @@ class NumericalCheckmarkWidgetActivity : Activity(), ListHabitsBehavior.NumberPi
SystemUtils.unlockScreen(this) SystemUtils.unlockScreen(this)
} }
override fun onNumberPicked(newValue: Double) { override fun onNumberPicked(newValue: Double, notes: String) {
behavior.setValue(data.habit, data.timestamp, (newValue * 1000).toInt()) behavior.setValue(data.habit, data.timestamp, (newValue * 1000).toInt(), notes)
widgetUpdater.updateWidgets() widgetUpdater.updateWidgets()
finish() finish()
} }
@ -79,6 +79,8 @@ class NumericalCheckmarkWidgetActivity : Activity(), ListHabitsBehavior.NumberPi
numberPickerFactory.create( numberPickerFactory.create(
entry.value / 1000.0, entry.value / 1000.0,
data.habit.unit, data.habit.unit,
entry.notes,
today.toDialogDateString(),
this this
).show() ).show()
} }

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true">
<shape>
<solid android:color="?attr/contrast40" />
<corners android:radius="4dp"/>
<padding
android:bottom="0dp"
android:left="8dp"
android:right="8dp"
android:top="0dp" />
</shape>
</item>
</selector>

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
~
~ 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/>.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<stroke android:width="1dp" android:color="?attr/contrast40" />
<corners android:radius="4dp"/>
<padding
android:left="0dp"
android:top="8dp"
android:right="0dp"
android:bottom="0dp" />
</shape>

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="12dp"
android:paddingStart="10dp"
android:paddingEnd="10dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp"
android:baselineAligned="false">
<FrameLayout
style="@style/FormOuterBox"
android:layout_width="0dp"
android:layout_weight="1">
<LinearLayout style="@style/DialogFormInnerBox">
<TextView
style="@style/DialogFormLabel"
android:text="@string/value" />
<LinearLayout
android:orientation="horizontal"
android:gravity="center_horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="8dp">
<Button
android:id="@+id/yesBtn"
android:text="@string/fa_check"
style="@style/CheckmarkDialogBtn"/>
<Button
android:id="@+id/skippedBtn"
android:text="@string/fa_skipped"
android:visibility="gone"
style="@style/CheckmarkDialogBtn"/>
<Button
android:id="@+id/noBtn"
android:text="@string/fa_times"
style="@style/CheckmarkDialogBtn"/>
<Button
android:id="@+id/questionBtn"
android:text="@string/fa_question"
android:visibility="gone"
style="@style/CheckmarkDialogBtn"/>
</LinearLayout>
</LinearLayout>
</FrameLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="5dp"
android:baselineAligned="false">
<FrameLayout
style="@style/FormOuterBox"
android:layout_width="0dp"
android:layout_weight="1">
<LinearLayout style="@style/DialogFormInnerBox">
<TextView
style="@style/DialogFormLabel"
android:text="@string/notes" />
<EditText
android:id="@+id/etNotes"
android:inputType="textCapSentences|textMultiLine"
style="@style/FormInput"
android:scrollbars="vertical"
android:hint="@string/example_notes"/>
</LinearLayout>
</FrameLayout>
</LinearLayout>
</LinearLayout>

@ -19,33 +19,98 @@
--> -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal" android:orientation="vertical"
android:gravity="center" android:layout_width="wrap_content"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_height="wrap_content"> android:paddingTop="12dp"
android:paddingStart="10dp"
<NumberPicker android:paddingEnd="10dp">
android:id="@+id/picker"
android:layout_gravity="center" <LinearLayout
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/tvSeparator"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
/> android:orientation="horizontal"
android:padding="5dp"
android:baselineAligned="false">
<FrameLayout
style="@style/FormOuterBox"
android:layout_width="0dp"
android:layout_weight="1">
<LinearLayout style="@style/DialogFormInnerBox">
<TextView
style="@style/DialogFormLabel"
android:text="@string/value" />
<LinearLayout
android:orientation="horizontal"
android:gravity="center_horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<NumberPicker
android:id="@+id/picker"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/tvSeparator"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<NumberPicker
android:id="@+id/picker2"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<TextView
android:id="@+id/tvUnit"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout>
</FrameLayout>
<NumberPicker </LinearLayout>
android:id="@+id/picker2"
android:layout_gravity="center"
android:layout_width="wrap_content" <LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
/> android:orientation="horizontal"
android:padding="5dp"
android:baselineAligned="false">
<FrameLayout
style="@style/FormOuterBox"
android:layout_width="0dp"
android:layout_weight="1">
<LinearLayout style="@style/DialogFormInnerBox">
<TextView
style="@style/DialogFormLabel"
android:text="@string/notes" />
<EditText
android:id="@+id/etNotes"
android:inputType="textCapSentences|textMultiLine"
style="@style/FormInput"
android:scrollbars="vertical"
android:hint="@string/example_notes"/>
</LinearLayout>
</FrameLayout>
<TextView </LinearLayout>
android:id="@+id/tvUnit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout> </LinearLayout>

@ -43,6 +43,7 @@
<attr name="iconFilter" format="reference"/> <attr name="iconFilter" format="reference"/>
<attr name="iconArrowUp" format="reference"/> <attr name="iconArrowUp" format="reference"/>
<attr name="iconArrowDown" format="reference"/> <attr name="iconArrowDown" format="reference"/>
<attr name="dialogFormLabelColor" format="reference"/>
<attr name="toolbarPopupTheme" format="reference"/> <attr name="toolbarPopupTheme" format="reference"/>

@ -80,7 +80,7 @@
<string name="interval_always_ask">Always ask</string> <string name="interval_always_ask">Always ask</string>
<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_2">Put checkmarks with a single tap instead of press-and-hold.</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>
<string name="pref_view_source_code">View source code at GitHub</string> <string name="pref_view_source_code">View source code at GitHub</string>
@ -181,7 +181,7 @@
<string name="by_status">By status</string> <string name="by_status">By status</string>
<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="value">Value</string>
<string name="calendar">Calendar</string> <string name="calendar">Calendar</string>
<string name="unit">Unit</string> <string name="unit">Unit</string>
<string name="target_type">Target Type</string> <string name="target_type">Target Type</string>

@ -63,6 +63,7 @@
<item name="windowBackgroundColor">@color/grey_200</item> <item name="windowBackgroundColor">@color/grey_200</item>
<item name="android:textColorAlertDialogListItem">@color/grey_800</item> <item name="android:textColorAlertDialogListItem">@color/grey_800</item>
<item name="singleLineTitle">false</item> <item name="singleLineTitle">false</item>
<item name="dialogFormLabelColor">@color/white</item>
</style> </style>
<style name="AppBaseThemeDark" parent="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar"> <style name="AppBaseThemeDark" parent="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar">
@ -110,6 +111,7 @@
<item name="buttonBarPositiveButtonStyle">@style/DialogButtonStyle</item> <item name="buttonBarPositiveButtonStyle">@style/DialogButtonStyle</item>
<item name="android:textColorAlertDialogListItem">@color/grey_100</item> <item name="android:textColorAlertDialogListItem">@color/grey_100</item>
<item name="singleLineTitle">false</item> <item name="singleLineTitle">false</item>
<item name="dialogFormLabelColor">@color/grey_800</item>
</style> </style>
<style name="AppBaseThemeDark.PureBlack"> <style name="AppBaseThemeDark.PureBlack">
@ -130,6 +132,7 @@
<item name="textColorAlertDialogListItem">@color/grey_100</item> <item name="textColorAlertDialogListItem">@color/grey_100</item>
<item name="windowBackgroundColor">@color/black</item> <item name="windowBackgroundColor">@color/black</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material.PureBlack</item> <item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material.PureBlack</item>
<item name="dialogFormLabelColor">@color/grey_800</item>
</style> </style>
<style name="BaseDialog" parent="Theme.AppCompat.Light.Dialog"> <style name="BaseDialog" parent="Theme.AppCompat.Light.Dialog">
@ -140,6 +143,7 @@
<item name="contrast80">@color/grey_700</item> <item name="contrast80">@color/grey_700</item>
<item name="contrast100">@color/grey_800</item> <item name="contrast100">@color/grey_800</item>
<item name="palette">@array/lightPalette</item> <item name="palette">@array/lightPalette</item>
<item name="dialogFormLabelColor">@color/white</item>
</style> </style>
<style name="BaseDialogDark" parent="Theme.AppCompat.Dialog"> <style name="BaseDialogDark" parent="Theme.AppCompat.Dialog">
@ -150,6 +154,7 @@
<item name="contrast80">@color/grey_300</item> <item name="contrast80">@color/grey_300</item>
<item name="contrast100">@color/grey_100</item> <item name="contrast100">@color/grey_100</item>
<item name="palette">@array/darkPalette</item> <item name="palette">@array/darkPalette</item>
<item name="dialogFormLabelColor">@color/grey_800</item>
</style> </style>
<style name="PreferenceThemeOverlay.v14.Material.PureBlack"> <style name="PreferenceThemeOverlay.v14.Material.PureBlack">
@ -360,4 +365,38 @@
<item name="android:layout_height">1dp</item> <item name="android:layout_height">1dp</item>
<item name="android:background">?attr/contrast20</item> <item name="android:background">?attr/contrast20</item>
</style> </style>
<style name="DialogFormInnerBox">
<item name="android:background">@drawable/dialog_bg_input_box</item>
<item name="android:clipChildren">false</item>
<item name="android:clipToPadding">false</item>
<item name="android:orientation">vertical</item>
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">match_parent</item>
</style>
<style name="DialogFormLabel">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_marginStart">8dp</item>
<item name="android:layout_marginTop">-15dp</item>
<item name="android:layout_marginBottom">-4dp</item>
<item name="android:paddingStart">8dp</item>
<item name="android:background">?attr/dialogFormLabelColor</item>
<item name="android:paddingEnd">8dp</item>
<item name="android:textSize">@dimen/smallTextSize</item>
</style>
<style name="CheckmarkDialogBtn">
<item name="android:layout_width">48dp</item>
<item name="android:layout_height">48dp</item>
<item name="android:layout_marginTop">8dp</item>
<item name="android:layout_marginBottom">8dp</item>
<item name="android:layout_marginEnd">12dp</item>
<item name="android:textSize">@dimen/regularTextSize</item>
<item name="backgroundTint">@null</item>
<item name="android:background">@drawable/bg_select_button</item>
<item name="selectable">true</item>
</style>
</resources> </resources>

@ -27,7 +27,7 @@
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:defaultValue="false" android:defaultValue="false"
android:key="pref_short_toggle" android:key="pref_short_toggle"
android:summary="@string/pref_toggle_description" android:summary="@string/pref_toggle_description_2"
android:title="@string/pref_toggle_title" android:title="@string/pref_toggle_title"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />

@ -1,5 +1,5 @@
HabitName,HabitDescription,HabitCategory,CalendarDate,Value,CommentText HabitName,HabitDescription,HabitCategory,CalendarDate,Value,CommentText
Breed dragons,with love and fire,Diet & Food,2016-03-18,1, Breed dragons,with love and fire,Diet & Food,2016-03-18,1,text
Breed dragons,with love and fire,Diet & Food,2016-03-19,1, Breed dragons,with love and fire,Diet & Food,2016-03-19,1,
Breed dragons,with love and fire,Diet & Food,2016-03-21,1, Breed dragons,with love and fire,Diet & Food,2016-03-21,1,
Reduce sleep,only 2 hours per day,Time Management,2016-03-15,1, Reduce sleep,only 2 hours per day,Time Management,2016-03-15,1,

1 HabitName HabitDescription HabitCategory CalendarDate Value CommentText
2 Breed dragons with love and fire Diet & Food 2016-03-18 1 text
3 Breed dragons with love and fire Diet & Food 2016-03-19 1
4 Breed dragons with love and fire Diet & Food 2016-03-21 1
5 Reduce sleep only 2 hours per day Time Management 2016-03-15 1

@ -1,7 +1,7 @@
HabitName,HabitDescription,HabitCategory,CalendarDate,Value,CommentText HabitName,HabitDescription,HabitCategory,CalendarDate,Value,CommentText
H1,,C1,11/5/2020,1, H1,,C1,11/5/2020,1,
H2,,C2,11/5/2020,-2150000000, H2,,C2,11/5/2020,-2150000000,
H3,Habit 3,C3,4/11/2019,1, H3,Habit 3,C3,4/11/2019,1,text
H3,Habit 3,C3,4/12/2019,1, H3,Habit 3,C3,4/12/2019,1,
H3,Habit 3,C3,4/13/2019,0, H3,Habit 3,C3,4/13/2019,0,
H3,Habit 3,C3,4/14/2019,1, H3,Habit 3,C3,4/14/2019,1,
@ -65,7 +65,7 @@ H3,Habit 3,C3,6/10/2019,1,
H3,Habit 3,C3,6/11/2019,1, H3,Habit 3,C3,6/11/2019,1,
H3,Habit 3,C3,6/12/2019,1, H3,Habit 3,C3,6/12/2019,1,
H3,Habit 3,C3,6/13/2019,1, H3,Habit 3,C3,6/13/2019,1,
H3,Habit 3,C3,6/14/2019,0, H3,Habit 3,C3,6/14/2019,0,Habit 3 notes
H3,Habit 3,C3,6/15/2019,1, H3,Habit 3,C3,6/15/2019,1,
H4,Habit 4,C4,11/6/2020,1, H4,Habit 4,C4,11/6/2020,1,
H4,Habit 4,C4,11/9/2020,1, H4,Habit 4,C4,11/9/2020,1,

1 HabitName HabitDescription HabitCategory CalendarDate Value CommentText
2 H1 C1 11/5/2020 1
3 H2 C2 11/5/2020 -2150000000
4 H3 Habit 3 C3 4/11/2019 1 text
5 H3 Habit 3 C3 4/12/2019 1
6 H3 Habit 3 C3 4/13/2019 0
7 H3 Habit 3 C3 4/14/2019 1
65 H3 Habit 3 C3 6/11/2019 1
66 H3 Habit 3 C3 6/12/2019 1
67 H3 Habit 3 C3 6/13/2019 1
68 H3 Habit 3 C3 6/14/2019 0 Habit 3 notes
69 H3 Habit 3 C3 6/15/2019 1
70 H4 Habit 4 C4 11/6/2020 1
71 H4 Habit 4 C4 11/9/2020 1

@ -23,6 +23,8 @@ interface View {
fun draw(canvas: Canvas) fun draw(canvas: Canvas)
fun onClick(x: Double, y: Double) { fun onClick(x: Double, y: Double) {
} }
fun onLongClick(x: Double, y: Double) {
}
} }
interface DataView : View { interface DataView : View {

@ -20,4 +20,4 @@ package org.isoron.uhabits.core
const val DATABASE_FILENAME = "uhabits.db" const val DATABASE_FILENAME = "uhabits.db"
const val DATABASE_VERSION = 24 const val DATABASE_VERSION = 25

@ -28,10 +28,11 @@ data class CreateRepetitionCommand(
val habit: Habit, val habit: Habit,
val timestamp: Timestamp, val timestamp: Timestamp,
val value: Int, val value: Int,
val notes: String,
) : Command { ) : Command {
override fun run() { override fun run() {
val entries = habit.originalEntries val entries = habit.originalEntries
entries.add(Entry(timestamp, value)) entries.add(Entry(timestamp, value, notes))
habit.recompute() habit.recompute()
habitList.resort() habitList.resort()
} }

@ -76,8 +76,11 @@ class HabitBullCSVImporter
map[name] = h map[name] = h
logger.info("Creating habit: $name") logger.info("Creating habit: $name")
} }
val notes = cols[5] ?: ""
if (parseInt(cols[4]) == 1) { if (parseInt(cols[4]) == 1) {
h.originalEntries.add(Entry(timestamp, Entry.YES_MANUAL)) h.originalEntries.add(Entry(timestamp, Entry.YES_MANUAL, notes))
} else {
h.originalEntries.add(Entry(timestamp, Entry.NO, notes))
} }
} }
} }

@ -101,8 +101,9 @@ class LoopDBImporter
for (r in entryRecords) { for (r in entryRecords) {
val t = Timestamp(r.timestamp!!) val t = Timestamp(r.timestamp!!)
val (_, value) = habit!!.originalEntries.get(t) val (_, value, notes) = habit!!.originalEntries.get(t)
if (value != r.value) CreateRepetitionCommand(habitList, habit, t, r.value!!).run() val oldNotes = r.notes ?: ""
if (value != r.value || notes != oldNotes) CreateRepetitionCommand(habitList, habit, t, r.value!!, oldNotes).run()
} }
runner.notifyListeners(command) runner.notifyListeners(command)

@ -21,6 +21,7 @@ package org.isoron.uhabits.core.models
data class Entry( data class Entry(
val timestamp: Timestamp, val timestamp: Timestamp,
val value: Int, val value: Int,
val notes: String = "",
) { ) {
companion object { companion object {
/** /**

@ -100,7 +100,7 @@ open class EntryList {
val intervals = buildIntervals(frequency, original) val intervals = buildIntervals(frequency, original)
snapIntervalsTogether(intervals) snapIntervalsTogether(intervals)
val computed = buildEntriesFromInterval(original, intervals) val computed = buildEntriesFromInterval(original, intervals)
computed.filter { it.value != UNKNOWN }.forEach { add(it) } computed.filter { it.value != UNKNOWN || it.notes.isNotEmpty() }.forEach { add(it) }
} }
} }

@ -20,6 +20,7 @@ package org.isoron.uhabits.core.models
import org.isoron.platform.time.LocalDate import org.isoron.platform.time.LocalDate
import org.isoron.uhabits.core.utils.DateFormats.Companion.getCSVDateFormat import org.isoron.uhabits.core.utils.DateFormats.Companion.getCSVDateFormat
import org.isoron.uhabits.core.utils.DateFormats.Companion.getDialogDateFormat
import org.isoron.uhabits.core.utils.DateUtils import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendar import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendar
import org.isoron.uhabits.core.utils.DateUtils.Companion.truncate import org.isoron.uhabits.core.utils.DateUtils.Companion.truncate
@ -81,6 +82,10 @@ data class Timestamp(var unixTime: Long) : Comparable<Timestamp> {
return day return day
} }
fun toDialogDateString(): String {
return getDialogDateFormat().format(Date(unixTime))
}
override fun toString(): String { override fun toString(): String {
return getCSVDateFormat().format(Date(unixTime)) return getCSVDateFormat().format(Date(unixTime))
} }

@ -41,12 +41,17 @@ class EntryRecord {
@field:Column @field:Column
var id: Long? = null var id: Long? = null
@field:Column
var notes: String? = null
fun copyFrom(entry: Entry) { fun copyFrom(entry: Entry) {
timestamp = entry.timestamp.unixTime timestamp = entry.timestamp.unixTime
value = entry.value value = entry.value
notes = entry.notes
} }
fun toEntry(): Entry { fun toEntry(): Entry {
return Entry(Timestamp(timestamp!!), value!!) val notes = notes ?: ""
return Entry(Timestamp(timestamp!!), value!!, notes)
} }
} }

@ -78,6 +78,11 @@ class HabitCardListCache @Inject constructor(
return data.checkmarks[habitId]!! return data.checkmarks[habitId]!!
} }
@Synchronized
fun getNoteIndicators(habitId: Long): BooleanArray {
return data.notesIndicators[habitId]!!
}
@Synchronized @Synchronized
fun hasNoHabit(): Boolean { fun hasNoHabit(): Boolean {
return allHabits.isEmpty return allHabits.isEmpty
@ -163,6 +168,7 @@ class HabitCardListCache @Inject constructor(
data.habits.removeAt(position) data.habits.removeAt(position)
data.idToHabit.remove(id) data.idToHabit.remove(id)
data.checkmarks.remove(id) data.checkmarks.remove(id)
data.notesIndicators.remove(id)
data.scores.remove(id) data.scores.remove(id)
listener.onItemRemoved(position) listener.onItemRemoved(position)
} }
@ -207,6 +213,7 @@ class HabitCardListCache @Inject constructor(
val habits: MutableList<Habit> val habits: MutableList<Habit>
val checkmarks: HashMap<Long?, IntArray> val checkmarks: HashMap<Long?, IntArray>
val scores: HashMap<Long?, Double> val scores: HashMap<Long?, Double>
val notesIndicators: HashMap<Long?, BooleanArray>
@Synchronized @Synchronized
fun copyCheckmarksFrom(oldData: CacheData) { fun copyCheckmarksFrom(oldData: CacheData) {
@ -217,6 +224,15 @@ class HabitCardListCache @Inject constructor(
} }
} }
@Synchronized
fun copyNoteIndicatorsFrom(oldData: CacheData) {
val empty = BooleanArray(checkmarkCount)
for (id in idToHabit.keys) {
if (oldData.notesIndicators.containsKey(id)) notesIndicators[id] =
oldData.notesIndicators[id]!! else notesIndicators[id] = empty
}
}
@Synchronized @Synchronized
fun copyScoresFrom(oldData: CacheData) { fun copyScoresFrom(oldData: CacheData) {
for (id in idToHabit.keys) { for (id in idToHabit.keys) {
@ -241,6 +257,7 @@ class HabitCardListCache @Inject constructor(
habits = LinkedList() habits = LinkedList()
checkmarks = HashMap() checkmarks = HashMap()
scores = HashMap() scores = HashMap()
notesIndicators = HashMap()
} }
} }
@ -271,6 +288,7 @@ class HabitCardListCache @Inject constructor(
newData.fetchHabits() newData.fetchHabits()
newData.copyScoresFrom(data) newData.copyScoresFrom(data)
newData.copyCheckmarksFrom(data) newData.copyCheckmarksFrom(data)
newData.copyNoteIndicatorsFrom(data)
val today = getTodayWithOffset() val today = getTodayWithOffset()
val dateFrom = today.minus(checkmarkCount - 1) val dateFrom = today.minus(checkmarkCount - 1)
if (runner != null) runner!!.publishProgress(this, -1) if (runner != null) runner!!.publishProgress(this, -1)
@ -280,10 +298,14 @@ class HabitCardListCache @Inject constructor(
if (targetId != null && targetId != habit.id) continue if (targetId != null && targetId != habit.id) continue
newData.scores[habit.id] = habit.scores[today].value newData.scores[habit.id] = habit.scores[today].value
val list: MutableList<Int> = ArrayList() val list: MutableList<Int> = ArrayList()
for ((_, value) in habit.computedEntries.getByInterval(dateFrom, today)) val notesIndicators: MutableList<Boolean> = ArrayList()
for ((_, value, note) in habit.computedEntries.getByInterval(dateFrom, today)) {
list.add(value) list.add(value)
notesIndicators.add(note.isNotEmpty())
}
val entries = list.toTypedArray() val entries = list.toTypedArray()
newData.checkmarks[habit.id] = ArrayUtils.toPrimitive(entries) newData.checkmarks[habit.id] = ArrayUtils.toPrimitive(entries)
newData.notesIndicators[habit.id] = notesIndicators.toBooleanArray()
runner!!.publishProgress(this, position) runner!!.publishProgress(this, position)
} }
} }
@ -311,6 +333,7 @@ class HabitCardListCache @Inject constructor(
data.idToHabit[id] = habit data.idToHabit[id] = habit
data.scores[id] = newData.scores[id]!! data.scores[id] = newData.scores[id]!!
data.checkmarks[id] = newData.checkmarks[id]!! data.checkmarks[id] = newData.checkmarks[id]!!
data.notesIndicators[id] = newData.notesIndicators[id]!!
listener.onItemInserted(position) listener.onItemInserted(position)
} }
@ -338,14 +361,18 @@ class HabitCardListCache @Inject constructor(
private fun performUpdate(id: Long, position: Int) { private fun performUpdate(id: Long, position: Int) {
val oldScore = data.scores[id]!! val oldScore = data.scores[id]!!
val oldCheckmarks = data.checkmarks[id] val oldCheckmarks = data.checkmarks[id]
val oldNoteIndicators = data.notesIndicators[id]
val newScore = newData.scores[id]!! val newScore = newData.scores[id]!!
val newCheckmarks = newData.checkmarks[id]!! val newCheckmarks = newData.checkmarks[id]!!
val newNoteIndicators = newData.notesIndicators[id]!!
var unchanged = true var unchanged = true
if (oldScore != newScore) unchanged = false if (oldScore != newScore) unchanged = false
if (!Arrays.equals(oldCheckmarks, newCheckmarks)) unchanged = false if (!Arrays.equals(oldCheckmarks, newCheckmarks)) unchanged = false
if (!Arrays.equals(oldNoteIndicators, newNoteIndicators)) unchanged = false
if (unchanged) return if (unchanged) return
data.scores[id] = newScore data.scores[id] = newScore
data.checkmarks[id] = newCheckmarks data.checkmarks[id] = newCheckmarks
data.notesIndicators[id] = newNoteIndicators
listener.onItemChanged(position) listener.onItemChanged(position)
} }

@ -22,6 +22,8 @@ import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.commands.CreateRepetitionCommand import org.isoron.uhabits.core.commands.CreateRepetitionCommand
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.HabitType
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.tasks.ExportCSVTask import org.isoron.uhabits.core.tasks.ExportCSVTask
@ -47,14 +49,27 @@ open class ListHabitsBehavior @Inject constructor(
} }
fun onEdit(habit: Habit, timestamp: Timestamp?) { fun onEdit(habit: Habit, timestamp: Timestamp?) {
val entries = habit.computedEntries val entry = habit.computedEntries.get(timestamp!!)
val oldValue = entries.get(timestamp!!).value.toDouble() if (habit.type == HabitType.NUMERICAL) {
screen.showNumberPicker( val oldValue = entry.value.toDouble()
oldValue / 1000, screen.showNumberPicker(
habit.unit oldValue / 1000,
) { newValue: Double -> habit.unit,
val value = (newValue * 1000).roundToInt() entry.notes,
commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, value)) timestamp.toDialogDateString(),
) { newValue: Double, newNotes: String, ->
val value = (newValue * 1000).roundToInt()
commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, value, newNotes))
}
} else {
screen.showCheckmarkDialog(
entry.value,
entry.notes,
timestamp.toDialogDateString(),
habit.color,
) { newValue, newNotes ->
commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, newValue, newNotes))
}
} }
} }
@ -105,8 +120,9 @@ open class ListHabitsBehavior @Inject constructor(
} }
fun onToggle(habit: Habit, timestamp: Timestamp?, value: Int) { fun onToggle(habit: Habit, timestamp: Timestamp?, value: Int) {
val notes = habit.computedEntries.get(timestamp!!).notes
commandRunner.run( commandRunner.run(
CreateRepetitionCommand(habitList, habit, timestamp!!, value) CreateRepetitionCommand(habitList, habit, timestamp, value, notes)
) )
} }
@ -131,10 +147,15 @@ open class ListHabitsBehavior @Inject constructor(
} }
fun interface NumberPickerCallback { fun interface NumberPickerCallback {
fun onNumberPicked(newValue: Double) fun onNumberPicked(newValue: Double, notes: String)
fun onNumberPickerDismissed() {} fun onNumberPickerDismissed() {}
} }
fun interface CheckMarkDialogCallback {
fun onNotesSaved(value: Int, notes: String)
fun onNotesDismissed() {}
}
interface Screen { interface Screen {
fun showHabitScreen(h: Habit) fun showHabitScreen(h: Habit)
fun showIntroScreen() fun showIntroScreen()
@ -142,8 +163,17 @@ open class ListHabitsBehavior @Inject constructor(
fun showNumberPicker( fun showNumberPicker(
value: Double, value: Double,
unit: String, unit: String,
notes: String,
dateString: String,
callback: NumberPickerCallback callback: NumberPickerCallback
) )
fun showCheckmarkDialog(
value: Int,
notes: String,
dateString: String,
color: PaletteColor,
callback: CheckMarkDialogCallback
)
fun showSendBugReportToDeveloperScreen(log: String) fun showSendBugReportToDeveloperScreen(log: String)
fun showSendFileScreen(filename: String) fun showSendFileScreen(filename: String)

@ -46,6 +46,7 @@ data class HistoryCardState(
val firstWeekday: DayOfWeek, val firstWeekday: DayOfWeek,
val series: List<HistoryChart.Square>, val series: List<HistoryChart.Square>,
val defaultSquare: HistoryChart.Square, val defaultSquare: HistoryChart.Square,
val notesIndicators: List<Boolean>,
val theme: Theme, val theme: Theme,
val today: LocalDate, val today: LocalDate,
) )
@ -58,36 +59,74 @@ class HistoryCardPresenter(
val screen: Screen, val screen: Screen,
) : OnDateClickedListener { ) : OnDateClickedListener {
override fun onDateClicked(date: LocalDate) { override fun onDateLongPress(date: LocalDate) {
val timestamp = Timestamp.fromLocalDate(date) val timestamp = Timestamp.fromLocalDate(date)
screen.showFeedback() screen.showFeedback()
if (habit.isNumerical) { if (habit.isNumerical) {
val entries = habit.computedEntries showNumberPicker(timestamp)
val oldValue = entries.get(timestamp).value } else {
screen.showNumberPicker(oldValue / 1000.0, habit.unit) { newValue: Double -> val entry = habit.computedEntries.get(timestamp)
val thousands = (newValue * 1000).roundToInt() val nextValue = Entry.nextToggleValue(
value = entry.value,
isSkipEnabled = preferences.isSkipEnabled,
areQuestionMarksEnabled = preferences.areQuestionMarksEnabled
)
commandRunner.run(
CreateRepetitionCommand(
habitList,
habit,
timestamp,
nextValue,
entry.notes,
),
)
}
}
override fun onDateShortPress(date: LocalDate) {
val timestamp = Timestamp.fromLocalDate(date)
screen.showFeedback()
if (habit.isNumerical) {
showNumberPicker(timestamp)
} else {
val entry = habit.computedEntries.get(timestamp)
screen.showCheckmarkDialog(
entry.value,
entry.notes,
timestamp.toDialogDateString(),
preferences,
habit.color,
) { newValue, newNotes ->
commandRunner.run( commandRunner.run(
CreateRepetitionCommand( CreateRepetitionCommand(
habitList, habitList,
habit, habit,
timestamp, timestamp,
thousands, newValue,
newNotes,
), ),
) )
} }
} else { }
val currentValue = habit.computedEntries.get(timestamp).value }
val nextValue = Entry.nextToggleValue(
value = currentValue, private fun showNumberPicker(timestamp: Timestamp) {
isSkipEnabled = preferences.isSkipEnabled, val entry = habit.computedEntries.get(timestamp)
areQuestionMarksEnabled = preferences.areQuestionMarksEnabled val oldValue = entry.value
) screen.showNumberPicker(
oldValue / 1000.0,
habit.unit,
entry.notes,
timestamp.toDialogDateString(),
) { newValue: Double, newNotes: String ->
val thousands = (newValue * 1000).roundToInt()
commandRunner.run( commandRunner.run(
CreateRepetitionCommand( CreateRepetitionCommand(
habitList, habitList,
habit, habit,
timestamp, timestamp,
nextValue, thousands,
newNotes,
), ),
) )
} }
@ -137,13 +176,21 @@ class HistoryCardPresenter(
else else
HistoryChart.Square.OFF HistoryChart.Square.OFF
val notesIndicators = entries.map {
when (it.notes) {
"" -> false
else -> true
}
}
return HistoryCardState( return HistoryCardState(
color = habit.color, color = habit.color,
firstWeekday = firstWeekday, firstWeekday = firstWeekday,
today = today.toLocalDate(), today = today.toLocalDate(),
theme = theme, theme = theme,
series = series, series = series,
defaultSquare = defaultSquare defaultSquare = defaultSquare,
notesIndicators = notesIndicators,
) )
} }
} }
@ -154,7 +201,17 @@ class HistoryCardPresenter(
fun showNumberPicker( fun showNumberPicker(
value: Double, value: Double,
unit: String, unit: String,
notes: String,
dateString: String,
callback: ListHabitsBehavior.NumberPickerCallback, callback: ListHabitsBehavior.NumberPickerCallback,
) )
fun showCheckmarkDialog(
value: Int,
notes: String,
dateString: String,
preferences: Preferences,
color: PaletteColor,
callback: ListHabitsBehavior.CheckMarkDialogCallback,
)
} }
} }

@ -32,8 +32,9 @@ import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.round import kotlin.math.round
fun interface OnDateClickedListener { interface OnDateClickedListener {
fun onDateClicked(date: LocalDate) fun onDateShortPress(date: LocalDate) {}
fun onDateLongPress(date: LocalDate) {}
} }
class HistoryChart( class HistoryChart(
@ -42,9 +43,10 @@ class HistoryChart(
var paletteColor: PaletteColor, var paletteColor: PaletteColor,
var series: List<Square>, var series: List<Square>,
var defaultSquare: Square, var defaultSquare: Square,
var notesIndicators: List<Boolean>,
var theme: Theme, var theme: Theme,
var today: LocalDate, var today: LocalDate,
var onDateClickedListener: OnDateClickedListener = OnDateClickedListener { }, var onDateClickedListener: OnDateClickedListener = object : OnDateClickedListener {},
var padding: Double = 0.0, var padding: Double = 0.0,
) : DataView { ) : DataView {
@ -72,6 +74,14 @@ class HistoryChart(
get() = squareSpacing + squareSize get() = squareSpacing + squareSize
override fun onClick(x: Double, y: Double) { override fun onClick(x: Double, y: Double) {
onDateClicked(x, y, false)
}
override fun onLongClick(x: Double, y: Double) {
onDateClicked(x, y, true)
}
private fun onDateClicked(x: Double, y: Double, isLongClick: Boolean) {
if (width <= 0.0) throw IllegalStateException("onClick must be called after draw(canvas)") if (width <= 0.0) throw IllegalStateException("onClick must be called after draw(canvas)")
val col = ((x - padding) / squareSize).toInt() val col = ((x - padding) / squareSize).toInt()
val row = ((y - padding) / squareSize).toInt() val row = ((y - padding) / squareSize).toInt()
@ -79,7 +89,11 @@ class HistoryChart(
if (row == 0 || col == nColumns) return if (row == 0 || col == nColumns) return
val clickedDate = topLeftDate.plus(offset) val clickedDate = topLeftDate.plus(offset)
if (clickedDate.isNewerThan(today)) return if (clickedDate.isNewerThan(today)) return
onDateClickedListener.onDateClicked(clickedDate) if (isLongClick) {
onDateClickedListener.onDateLongPress(clickedDate)
} else {
onDateClickedListener.onDateShortPress(clickedDate)
}
} }
override fun draw(canvas: Canvas) { override fun draw(canvas: Canvas) {
@ -191,7 +205,9 @@ class HistoryChart(
) { ) {
val value = if (offset >= series.size) defaultSquare else series[offset] val value = if (offset >= series.size) defaultSquare else series[offset]
val hasNotes = if (offset >= notesIndicators.size) false else notesIndicators[offset]
val squareColor: Color val squareColor: Color
val circleColor: Color
val color = theme.color(paletteColor.paletteIndex) val color = theme.color(paletteColor.paletteIndex)
squareColor = when (value) { squareColor = when (value) {
Square.ON -> { Square.ON -> {
@ -235,5 +251,14 @@ class HistoryChart(
canvas.setColor(textColor) canvas.setColor(textColor)
canvas.setTextAlign(TextAlign.CENTER) canvas.setTextAlign(TextAlign.CENTER)
canvas.drawText(date.day.toString(), x + width / 2, y + width / 2) canvas.drawText(date.day.toString(), x + width / 2, y + width / 2)
if (hasNotes) {
circleColor = when (value) {
Square.ON -> theme.lowContrastTextColor
else -> color
}
canvas.setColor(circleColor)
canvas.fillCircle(x + width - width / 5, y + width / 5, width / 12)
}
} }
} }

@ -37,40 +37,45 @@ class WidgetBehavior @Inject constructor(
) { ) {
fun onAddRepetition(habit: Habit, timestamp: Timestamp?) { fun onAddRepetition(habit: Habit, timestamp: Timestamp?) {
notificationTray.cancel(habit) notificationTray.cancel(habit)
setValue(habit, timestamp, Entry.YES_MANUAL) val entry = habit.originalEntries.get(timestamp!!)
setValue(habit, timestamp, Entry.YES_MANUAL, entry.notes)
} }
fun onRemoveRepetition(habit: Habit, timestamp: Timestamp?) { fun onRemoveRepetition(habit: Habit, timestamp: Timestamp?) {
notificationTray.cancel(habit) notificationTray.cancel(habit)
setValue(habit, timestamp, Entry.NO) val entry = habit.originalEntries.get(timestamp!!)
setValue(habit, timestamp, Entry.NO, entry.notes)
} }
fun onToggleRepetition(habit: Habit, timestamp: Timestamp) { fun onToggleRepetition(habit: Habit, timestamp: Timestamp) {
val currentValue = habit.originalEntries.get(timestamp).value val entry = habit.originalEntries.get(timestamp)
val currentValue = entry.value
val newValue = nextToggleValue( val newValue = nextToggleValue(
value = currentValue, value = currentValue,
isSkipEnabled = preferences.isSkipEnabled, isSkipEnabled = preferences.isSkipEnabled,
areQuestionMarksEnabled = preferences.areQuestionMarksEnabled areQuestionMarksEnabled = preferences.areQuestionMarksEnabled
) )
setValue(habit, timestamp, newValue) setValue(habit, timestamp, newValue, entry.notes)
notificationTray.cancel(habit) notificationTray.cancel(habit)
} }
fun onIncrement(habit: Habit, timestamp: Timestamp, amount: Int) { fun onIncrement(habit: Habit, timestamp: Timestamp, amount: Int) {
val currentValue = habit.computedEntries.get(timestamp).value val entry = habit.computedEntries.get(timestamp)
setValue(habit, timestamp, currentValue + amount) val currentValue = entry.value
setValue(habit, timestamp, currentValue + amount, entry.notes)
notificationTray.cancel(habit) notificationTray.cancel(habit)
} }
fun onDecrement(habit: Habit, timestamp: Timestamp, amount: Int) { fun onDecrement(habit: Habit, timestamp: Timestamp, amount: Int) {
val currentValue = habit.computedEntries.get(timestamp).value val entry = habit.computedEntries.get(timestamp)
setValue(habit, timestamp, currentValue - amount) val currentValue = entry.value
setValue(habit, timestamp, currentValue - amount, entry.notes)
notificationTray.cancel(habit) notificationTray.cancel(habit)
} }
fun setValue(habit: Habit, timestamp: Timestamp?, newValue: Int) { fun setValue(habit: Habit, timestamp: Timestamp?, newValue: Int, notes: String) {
commandRunner.run( commandRunner.run(
CreateRepetitionCommand(habitList, habit, timestamp!!, newValue) CreateRepetitionCommand(habitList, habit, timestamp!!, newValue, notes)
) )
} }
} }

@ -41,5 +41,8 @@ class DateFormats {
@JvmStatic fun getCSVDateFormat(): SimpleDateFormat = @JvmStatic fun getCSVDateFormat(): SimpleDateFormat =
fromSkeleton("yyyy-MM-dd", Locale.US) fromSkeleton("yyyy-MM-dd", Locale.US)
@JvmStatic fun getDialogDateFormat(): SimpleDateFormat =
fromSkeleton("MMM dd, yyyy", Locale.US)
} }
} }

@ -0,0 +1 @@
alter table Repetitions add column notes text;

@ -38,7 +38,7 @@ class CreateRepetitionCommandTest : BaseUnitTest() {
habit = fixtures.createShortHabit() habit = fixtures.createShortHabit()
habitList.add(habit) habitList.add(habit)
today = getToday() today = getToday()
command = CreateRepetitionCommand(habitList, habit, today, 100) command = CreateRepetitionCommand(habitList, habit, today, 100, "")
} }
@Test @Test

@ -54,6 +54,7 @@ class ImportTest : BaseUnitTest() {
assertTrue(isChecked(habit, 2016, 3, 18)) assertTrue(isChecked(habit, 2016, 3, 18))
assertTrue(isChecked(habit, 2016, 3, 19)) assertTrue(isChecked(habit, 2016, 3, 19))
assertFalse(isChecked(habit, 2016, 3, 20)) assertFalse(isChecked(habit, 2016, 3, 20))
assertTrue(isNotesEqual(habit, 2016, 3, 18, "text"))
} }
@Test @Test
@ -68,6 +69,8 @@ class ImportTest : BaseUnitTest() {
assertTrue(isChecked(habit, 2019, 4, 11)) assertTrue(isChecked(habit, 2019, 4, 11))
assertTrue(isChecked(habit, 2019, 5, 7)) assertTrue(isChecked(habit, 2019, 5, 7))
assertFalse(isChecked(habit, 2019, 6, 14)) assertFalse(isChecked(habit, 2019, 6, 14))
assertTrue(isNotesEqual(habit, 2019, 4, 11, "text"))
assertTrue(isNotesEqual(habit, 2019, 6, 14, "Habit 3 notes"))
} }
@Test @Test
@ -127,6 +130,13 @@ class ImportTest : BaseUnitTest() {
return h.originalEntries.get(timestamp).value == Entry.YES_MANUAL return h.originalEntries.get(timestamp).value == Entry.YES_MANUAL
} }
private fun isNotesEqual(h: Habit, year: Int, month: Int, day: Int, notes: String): Boolean {
val date = getStartOfTodayCalendar()
date.set(year, month - 1, day)
val timestamp = Timestamp(date)
return h.originalEntries.get(timestamp).notes == notes
}
@Throws(IOException::class) @Throws(IOException::class)
private fun importFromFile(assetFilename: String) { private fun importFromFile(assetFilename: String) {
val file = File.createTempFile("asset", "") val file = File.createTempFile("asset", "")

@ -70,7 +70,7 @@ class HabitCardListCacheTest : BaseUnitTest() {
@Test @Test
fun testCommandListener_single() { fun testCommandListener_single() {
val h2 = habitList.getByPosition(2) val h2 = habitList.getByPosition(2)
commandRunner.run(CreateRepetitionCommand(habitList, h2, today, Entry.NO)) commandRunner.run(CreateRepetitionCommand(habitList, h2, today, Entry.NO, ""))
verify(listener).onItemChanged(2) verify(listener).onItemChanged(2)
verify(listener).onRefreshFinished() verify(listener).onRefreshFinished()
verifyNoMoreInteractions(listener) verifyNoMoreInteractions(listener)

@ -79,8 +79,8 @@ class ListHabitsBehaviorTest : BaseUnitTest() {
@Test @Test
fun testOnEdit() { fun testOnEdit() {
behavior.onEdit(habit2, getToday()) behavior.onEdit(habit2, getToday())
verify(screen).showNumberPicker(eq(0.1), eq("miles"), picker.capture()) verify(screen).showNumberPicker(eq(0.1), eq("miles"), eq(""), eq("Jan 25, 2015"), picker.capture())
picker.lastValue.onNumberPicked(100.0) picker.lastValue.onNumberPicked(100.0, "")
val today = getTodayWithOffset() val today = getTodayWithOffset()
assertThat(habit2.computedEntries.get(today).value, equalTo(100000)) assertThat(habit2.computedEntries.get(today).value, equalTo(100000))
} }

@ -72,6 +72,10 @@ class HistoryChartTest {
1 -> DIMMED 1 -> DIMMED
else -> OFF else -> OFF
} }
},
notesIndicators = MutableList(85) {
index: Int ->
index % 3 == 0
} }
) )
@ -86,20 +90,20 @@ class HistoryChartTest {
// Click top left date // Click top left date
view.onClick(20.0, 46.0) view.onClick(20.0, 46.0)
verify(dateClickedListener).onDateClicked(LocalDate(2014, 10, 26)) verify(dateClickedListener).onDateShortPress(LocalDate(2014, 10, 26))
reset(dateClickedListener) reset(dateClickedListener)
view.onClick(2.0, 28.0) view.onClick(2.0, 28.0)
verify(dateClickedListener).onDateClicked(LocalDate(2014, 10, 26)) verify(dateClickedListener).onDateShortPress(LocalDate(2014, 10, 26))
reset(dateClickedListener) reset(dateClickedListener)
// Click date in the middle // Click date in the middle
view.onClick(163.0, 113.0) view.onClick(163.0, 113.0)
verify(dateClickedListener).onDateClicked(LocalDate(2014, 12, 10)) verify(dateClickedListener).onDateShortPress(LocalDate(2014, 12, 10))
reset(dateClickedListener) reset(dateClickedListener)
// Click today // Click today
view.onClick(336.0, 37.0) view.onClick(336.0, 37.0)
verify(dateClickedListener).onDateClicked(LocalDate(2015, 1, 25)) verify(dateClickedListener).onDateShortPress(LocalDate(2015, 1, 25))
reset(dateClickedListener) reset(dateClickedListener)
// Click header // Click header
@ -111,6 +115,37 @@ class HistoryChartTest {
verifyNoMoreInteractions(dateClickedListener) verifyNoMoreInteractions(dateClickedListener)
} }
@Test
fun testLongClick() = runBlocking {
assertRenders(400, 200, "$base/base.png", view)
// Click top left date
view.onLongClick(20.0, 46.0)
verify(dateClickedListener).onDateLongPress(LocalDate(2014, 10, 26))
reset(dateClickedListener)
view.onLongClick(2.0, 28.0)
verify(dateClickedListener).onDateLongPress(LocalDate(2014, 10, 26))
reset(dateClickedListener)
// Click date in the middle
view.onLongClick(163.0, 113.0)
verify(dateClickedListener).onDateLongPress(LocalDate(2014, 12, 10))
reset(dateClickedListener)
// Click today
view.onLongClick(336.0, 37.0)
verify(dateClickedListener).onDateLongPress(LocalDate(2015, 1, 25))
reset(dateClickedListener)
// Click header
view.onLongClick(160.0, 15.0)
verifyNoMoreInteractions(dateClickedListener)
// Click right axis
view.onLongClick(360.0, 60.0)
verifyNoMoreInteractions(dateClickedListener)
}
@Test @Test
fun testDrawWeekDay() = runBlocking { fun testDrawWeekDay() = runBlocking {
view.firstWeekday = DayOfWeek.MONDAY view.firstWeekday = DayOfWeek.MONDAY

@ -58,7 +58,7 @@ class WidgetBehaviorTest : BaseUnitTest() {
fun testOnAddRepetition() { fun testOnAddRepetition() {
behavior.onAddRepetition(habit, today) behavior.onAddRepetition(habit, today)
verify(commandRunner).run( verify(commandRunner).run(
CreateRepetitionCommand(habitList, habit, today, Entry.YES_MANUAL) CreateRepetitionCommand(habitList, habit, today, Entry.YES_MANUAL, "")
) )
verify(notificationTray).cancel(habit) verify(notificationTray).cancel(habit)
verifyZeroInteractions(preferences) verifyZeroInteractions(preferences)
@ -68,7 +68,7 @@ class WidgetBehaviorTest : BaseUnitTest() {
fun testOnRemoveRepetition() { fun testOnRemoveRepetition() {
behavior.onRemoveRepetition(habit, today) behavior.onRemoveRepetition(habit, today)
verify(commandRunner).run( verify(commandRunner).run(
CreateRepetitionCommand(habitList, habit, today, Entry.NO) CreateRepetitionCommand(habitList, habit, today, Entry.NO, "")
) )
verify(notificationTray).cancel(habit) verify(notificationTray).cancel(habit)
verifyZeroInteractions(preferences) verifyZeroInteractions(preferences)
@ -94,7 +94,7 @@ class WidgetBehaviorTest : BaseUnitTest() {
behavior.onToggleRepetition(habit, today) behavior.onToggleRepetition(habit, today)
verify(preferences).isSkipEnabled verify(preferences).isSkipEnabled
verify(commandRunner).run( verify(commandRunner).run(
CreateRepetitionCommand(habitList, habit, today, nextValue) CreateRepetitionCommand(habitList, habit, today, nextValue, "")
) )
verify(notificationTray).cancel( verify(notificationTray).cancel(
habit habit
@ -110,7 +110,7 @@ class WidgetBehaviorTest : BaseUnitTest() {
habit.recompute() habit.recompute()
behavior.onIncrement(habit, today, 100) behavior.onIncrement(habit, today, 100)
verify(commandRunner).run( verify(commandRunner).run(
CreateRepetitionCommand(habitList, habit, today, 600) CreateRepetitionCommand(habitList, habit, today, 600, "")
) )
verify(notificationTray).cancel(habit) verify(notificationTray).cancel(habit)
verifyZeroInteractions(preferences) verifyZeroInteractions(preferences)
@ -123,7 +123,7 @@ class WidgetBehaviorTest : BaseUnitTest() {
habit.recompute() habit.recompute()
behavior.onDecrement(habit, today, 100) behavior.onDecrement(habit, today, 100)
verify(commandRunner).run( verify(commandRunner).run(
CreateRepetitionCommand(habitList, habit, today, 400) CreateRepetitionCommand(habitList, habit, today, 400, "")
) )
verify(notificationTray).cancel(habit) verify(notificationTray).cancel(habit)
verifyZeroInteractions(preferences) verifyZeroInteractions(preferences)

Loading…
Cancel
Save