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
fun testClick_shortToggleDisabled() {
prefs.isShortToggleEnabled = false
view.performClick()
assertFalse(edited)
}
@Test
fun testClick_shortToggleEnabled() {
prefs.isShortToggleEnabled = true
fun testClick() {
view.performClick()
assertTrue(edited)
}

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

@ -49,23 +49,12 @@ class AndroidDataView(
override fun onShowPress(e: MotionEvent?) = Unit
override fun onSingleTapUp(e: MotionEvent?): 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
}
view?.onClick(x / canvas.innerDensity, y / canvas.innerDensity)
return true
return handleClick(e, true)
}
override fun onLongPress(e: MotionEvent?) = Unit
override fun onLongPress(e: MotionEvent?) {
handleClick(e)
}
override fun onScroll(
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,
series = emptyList(),
defaultSquare = HistoryChart.Square.OFF,
notesIndicators = emptyList(),
theme = themeSwitcher.currentTheme,
today = DateUtils.getTodayWithOffset().toLocalDate(),
onDateClickedListener = onDateClickedListener ?: OnDateClickedListener { },
onDateClickedListener = onDateClickedListener ?: object : OnDateClickedListener {},
padding = 10.0,
)
dataView = AndroidDataView(context!!, null)
@ -103,6 +104,7 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
)
chart?.series = model.series
chart?.defaultSquare = model.defaultSquare
chart?.notesIndicators = model.notesIndicators
dataView.postInvalidate()
}

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

@ -25,6 +25,7 @@ import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import dagger.Lazy
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.ConfirmDeleteDialog
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
@ -89,6 +90,7 @@ class ListHabitsScreen
private val importTaskFactory: ImportDataTaskFactory,
private val colorPickerFactory: ColorPickerDialogFactory,
private val numberPickerFactory: NumberPickerFactory,
private val checkMarkDialog: CheckmarkDialog,
private val behavior: Lazy<ListHabitsBehavior>
) : CommandRunner.Listener,
ListHabitsBehavior.Screen,
@ -225,9 +227,28 @@ class ListHabitsScreen
override fun showNumberPicker(
value: Double,
unit: String,
notes: String,
dateString: String,
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? {

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

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

@ -124,8 +124,9 @@ class HabitCardListAdapter @Inject constructor(
val habit = cache.getHabitByPosition(position)
val score = cache.getScore(habit!!.id!!)
val checkmarks = cache.getCheckmarks(habit.id!!)
val notesIndicators = cache.getNoteIndicators(habit.id!!)
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) {

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

@ -116,6 +116,13 @@ class HabitCardView(
numberPanel.threshold = value
}
var notesIndicators
get() = checkmarkPanel.notesIndicators
set(values) {
checkmarkPanel.notesIndicators = values
numberPanel.notesIndicators = values
}
var checkmarkPanel: CheckmarkPanelView
private var numberPanel: NumberPanelView
private var innerFrame: LinearLayout
@ -150,6 +157,10 @@ class HabitCardView(
}.delay(TOGGLE_DELAY_MILLIS)
}
}
onEdit = { timestamp ->
triggerRipple(timestamp)
habit?.let { behavior.onEdit(it, timestamp) }
}
}
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.utils.InterfaceUtils.getDimension
import org.isoron.uhabits.utils.dim
import org.isoron.uhabits.utils.drawNotesIndicator
import org.isoron.uhabits.utils.getFontAwesome
import org.isoron.uhabits.utils.showMessage
import org.isoron.uhabits.utils.sres
import java.lang.Double.max
import java.text.DecimalFormat
@ -101,6 +101,11 @@ class NumberButtonView(
field = value
invalidate()
}
var hasNotes = false
set(value) {
field = value
invalidate()
}
var onEdit: () -> Unit = {}
private var drawer: Drawer = Drawer(context)
@ -111,8 +116,7 @@ class NumberButtonView(
}
override fun onClick(v: View) {
if (preferences.isShortToggleEnabled) onEdit()
else showMessage(resources.getString(R.string.long_press_to_edit))
onEdit()
}
override fun onLongClick(v: View): Boolean {
@ -211,6 +215,8 @@ class NumberButtonView(
rect.offset(0f, 1.3f * em)
canvas.drawText(units, rect.centerX(), rect.centerY(), pUnit)
}
drawNotesIndicator(canvas, color, em, hasNotes)
}
}
}

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

@ -32,12 +32,14 @@ import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.AndroidThemeSwitcher
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.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.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
@ -164,9 +166,29 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
override fun showNumberPicker(
value: Double,
unit: String,
notes: String,
dateString: String,
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) {

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

@ -22,7 +22,9 @@ package org.isoron.uhabits.utils
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.drawable.ColorDrawable
import android.os.Handler
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.dp(value: Float) = InterfaceUtils.dpToPixels(context, value)
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
get() = StyledResources(context)

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

@ -60,8 +60,8 @@ class NumericalCheckmarkWidgetActivity : Activity(), ListHabitsBehavior.NumberPi
SystemUtils.unlockScreen(this)
}
override fun onNumberPicked(newValue: Double) {
behavior.setValue(data.habit, data.timestamp, (newValue * 1000).toInt())
override fun onNumberPicked(newValue: Double, notes: String) {
behavior.setValue(data.habit, data.timestamp, (newValue * 1000).toInt(), notes)
widgetUpdater.updateWidgets()
finish()
}
@ -79,6 +79,8 @@ class NumericalCheckmarkWidgetActivity : Activity(), ListHabitsBehavior.NumberPi
numberPickerFactory.create(
entry.value / 1000.0,
data.habit.unit,
entry.notes,
today.toDialogDateString(),
this
).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"
android:orientation="horizontal"
android:gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<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_width="wrap_content"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
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="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
android:id="@+id/picker2"
android:layout_gravity="center"
android:layout_width="wrap_content"
</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>
<TextView
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="iconArrowUp" format="reference"/>
<attr name="iconArrowDown" format="reference"/>
<attr name="dialogFormLabelColor" format="reference"/>
<attr name="toolbarPopupTheme" format="reference"/>

@ -80,7 +80,7 @@
<string name="interval_always_ask">Always ask</string>
<string name="interval_custom">Custom...</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_send_feedback">Send feedback to developer</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="export">Export</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="unit">Unit</string>
<string name="target_type">Target Type</string>

@ -63,6 +63,7 @@
<item name="windowBackgroundColor">@color/grey_200</item>
<item name="android:textColorAlertDialogListItem">@color/grey_800</item>
<item name="singleLineTitle">false</item>
<item name="dialogFormLabelColor">@color/white</item>
</style>
<style name="AppBaseThemeDark" parent="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar">
@ -110,6 +111,7 @@
<item name="buttonBarPositiveButtonStyle">@style/DialogButtonStyle</item>
<item name="android:textColorAlertDialogListItem">@color/grey_100</item>
<item name="singleLineTitle">false</item>
<item name="dialogFormLabelColor">@color/grey_800</item>
</style>
<style name="AppBaseThemeDark.PureBlack">
@ -130,6 +132,7 @@
<item name="textColorAlertDialogListItem">@color/grey_100</item>
<item name="windowBackgroundColor">@color/black</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material.PureBlack</item>
<item name="dialogFormLabelColor">@color/grey_800</item>
</style>
<style name="BaseDialog" parent="Theme.AppCompat.Light.Dialog">
@ -140,6 +143,7 @@
<item name="contrast80">@color/grey_700</item>
<item name="contrast100">@color/grey_800</item>
<item name="palette">@array/lightPalette</item>
<item name="dialogFormLabelColor">@color/white</item>
</style>
<style name="BaseDialogDark" parent="Theme.AppCompat.Dialog">
@ -150,6 +154,7 @@
<item name="contrast80">@color/grey_300</item>
<item name="contrast100">@color/grey_100</item>
<item name="palette">@array/darkPalette</item>
<item name="dialogFormLabelColor">@color/grey_800</item>
</style>
<style name="PreferenceThemeOverlay.v14.Material.PureBlack">
@ -360,4 +365,38 @@
<item name="android:layout_height">1dp</item>
<item name="android:background">?attr/contrast20</item>
</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>

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

@ -1,5 +1,5 @@
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-21,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
H1,,C1,11/5/2020,1,
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/13/2019,0,
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/12/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,
H4,Habit 4,C4,11/6/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 onClick(x: Double, y: Double) {
}
fun onLongClick(x: Double, y: Double) {
}
}
interface DataView : View {

@ -20,4 +20,4 @@ package org.isoron.uhabits.core
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 timestamp: Timestamp,
val value: Int,
val notes: String,
) : Command {
override fun run() {
val entries = habit.originalEntries
entries.add(Entry(timestamp, value))
entries.add(Entry(timestamp, value, notes))
habit.recompute()
habitList.resort()
}

@ -76,8 +76,11 @@ class HabitBullCSVImporter
map[name] = h
logger.info("Creating habit: $name")
}
val notes = cols[5] ?: ""
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) {
val t = Timestamp(r.timestamp!!)
val (_, value) = habit!!.originalEntries.get(t)
if (value != r.value) CreateRepetitionCommand(habitList, habit, t, r.value!!).run()
val (_, value, notes) = habit!!.originalEntries.get(t)
val oldNotes = r.notes ?: ""
if (value != r.value || notes != oldNotes) CreateRepetitionCommand(habitList, habit, t, r.value!!, oldNotes).run()
}
runner.notifyListeners(command)

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

@ -100,7 +100,7 @@ open class EntryList {
val intervals = buildIntervals(frequency, original)
snapIntervalsTogether(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.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.Companion.getStartOfTodayCalendar
import org.isoron.uhabits.core.utils.DateUtils.Companion.truncate
@ -81,6 +82,10 @@ data class Timestamp(var unixTime: Long) : Comparable<Timestamp> {
return day
}
fun toDialogDateString(): String {
return getDialogDateFormat().format(Date(unixTime))
}
override fun toString(): String {
return getCSVDateFormat().format(Date(unixTime))
}

@ -41,12 +41,17 @@ class EntryRecord {
@field:Column
var id: Long? = null
@field:Column
var notes: String? = null
fun copyFrom(entry: Entry) {
timestamp = entry.timestamp.unixTime
value = entry.value
notes = entry.notes
}
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]!!
}
@Synchronized
fun getNoteIndicators(habitId: Long): BooleanArray {
return data.notesIndicators[habitId]!!
}
@Synchronized
fun hasNoHabit(): Boolean {
return allHabits.isEmpty
@ -163,6 +168,7 @@ class HabitCardListCache @Inject constructor(
data.habits.removeAt(position)
data.idToHabit.remove(id)
data.checkmarks.remove(id)
data.notesIndicators.remove(id)
data.scores.remove(id)
listener.onItemRemoved(position)
}
@ -207,6 +213,7 @@ class HabitCardListCache @Inject constructor(
val habits: MutableList<Habit>
val checkmarks: HashMap<Long?, IntArray>
val scores: HashMap<Long?, Double>
val notesIndicators: HashMap<Long?, BooleanArray>
@Synchronized
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
fun copyScoresFrom(oldData: CacheData) {
for (id in idToHabit.keys) {
@ -241,6 +257,7 @@ class HabitCardListCache @Inject constructor(
habits = LinkedList()
checkmarks = HashMap()
scores = HashMap()
notesIndicators = HashMap()
}
}
@ -271,6 +288,7 @@ class HabitCardListCache @Inject constructor(
newData.fetchHabits()
newData.copyScoresFrom(data)
newData.copyCheckmarksFrom(data)
newData.copyNoteIndicatorsFrom(data)
val today = getTodayWithOffset()
val dateFrom = today.minus(checkmarkCount - 1)
if (runner != null) runner!!.publishProgress(this, -1)
@ -280,10 +298,14 @@ class HabitCardListCache @Inject constructor(
if (targetId != null && targetId != habit.id) continue
newData.scores[habit.id] = habit.scores[today].value
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)
notesIndicators.add(note.isNotEmpty())
}
val entries = list.toTypedArray()
newData.checkmarks[habit.id] = ArrayUtils.toPrimitive(entries)
newData.notesIndicators[habit.id] = notesIndicators.toBooleanArray()
runner!!.publishProgress(this, position)
}
}
@ -311,6 +333,7 @@ class HabitCardListCache @Inject constructor(
data.idToHabit[id] = habit
data.scores[id] = newData.scores[id]!!
data.checkmarks[id] = newData.checkmarks[id]!!
data.notesIndicators[id] = newData.notesIndicators[id]!!
listener.onItemInserted(position)
}
@ -338,14 +361,18 @@ class HabitCardListCache @Inject constructor(
private fun performUpdate(id: Long, position: Int) {
val oldScore = data.scores[id]!!
val oldCheckmarks = data.checkmarks[id]
val oldNoteIndicators = data.notesIndicators[id]
val newScore = newData.scores[id]!!
val newCheckmarks = newData.checkmarks[id]!!
val newNoteIndicators = newData.notesIndicators[id]!!
var unchanged = true
if (oldScore != newScore) unchanged = false
if (!Arrays.equals(oldCheckmarks, newCheckmarks)) unchanged = false
if (!Arrays.equals(oldNoteIndicators, newNoteIndicators)) unchanged = false
if (unchanged) return
data.scores[id] = newScore
data.checkmarks[id] = newCheckmarks
data.notesIndicators[id] = newNoteIndicators
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.models.Habit
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.preferences.Preferences
import org.isoron.uhabits.core.tasks.ExportCSVTask
@ -47,14 +49,27 @@ open class ListHabitsBehavior @Inject constructor(
}
fun onEdit(habit: Habit, timestamp: Timestamp?) {
val entries = habit.computedEntries
val oldValue = entries.get(timestamp!!).value.toDouble()
screen.showNumberPicker(
oldValue / 1000,
habit.unit
) { newValue: Double ->
val value = (newValue * 1000).roundToInt()
commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, value))
val entry = habit.computedEntries.get(timestamp!!)
if (habit.type == HabitType.NUMERICAL) {
val oldValue = entry.value.toDouble()
screen.showNumberPicker(
oldValue / 1000,
habit.unit,
entry.notes,
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) {
val notes = habit.computedEntries.get(timestamp!!).notes
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 onNumberPicked(newValue: Double)
fun onNumberPicked(newValue: Double, notes: String)
fun onNumberPickerDismissed() {}
}
fun interface CheckMarkDialogCallback {
fun onNotesSaved(value: Int, notes: String)
fun onNotesDismissed() {}
}
interface Screen {
fun showHabitScreen(h: Habit)
fun showIntroScreen()
@ -142,8 +163,17 @@ open class ListHabitsBehavior @Inject constructor(
fun showNumberPicker(
value: Double,
unit: String,
notes: String,
dateString: String,
callback: NumberPickerCallback
)
fun showCheckmarkDialog(
value: Int,
notes: String,
dateString: String,
color: PaletteColor,
callback: CheckMarkDialogCallback
)
fun showSendBugReportToDeveloperScreen(log: String)
fun showSendFileScreen(filename: String)

@ -46,6 +46,7 @@ data class HistoryCardState(
val firstWeekday: DayOfWeek,
val series: List<HistoryChart.Square>,
val defaultSquare: HistoryChart.Square,
val notesIndicators: List<Boolean>,
val theme: Theme,
val today: LocalDate,
)
@ -58,36 +59,74 @@ class HistoryCardPresenter(
val screen: Screen,
) : OnDateClickedListener {
override fun onDateClicked(date: LocalDate) {
override fun onDateLongPress(date: LocalDate) {
val timestamp = Timestamp.fromLocalDate(date)
screen.showFeedback()
if (habit.isNumerical) {
val entries = habit.computedEntries
val oldValue = entries.get(timestamp).value
screen.showNumberPicker(oldValue / 1000.0, habit.unit) { newValue: Double ->
val thousands = (newValue * 1000).roundToInt()
showNumberPicker(timestamp)
} else {
val entry = habit.computedEntries.get(timestamp)
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(
CreateRepetitionCommand(
habitList,
habit,
timestamp,
thousands,
newValue,
newNotes,
),
)
}
} else {
val currentValue = habit.computedEntries.get(timestamp).value
val nextValue = Entry.nextToggleValue(
value = currentValue,
isSkipEnabled = preferences.isSkipEnabled,
areQuestionMarksEnabled = preferences.areQuestionMarksEnabled
)
}
}
private fun showNumberPicker(timestamp: Timestamp) {
val entry = habit.computedEntries.get(timestamp)
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(
CreateRepetitionCommand(
habitList,
habit,
timestamp,
nextValue,
thousands,
newNotes,
),
)
}
@ -137,13 +176,21 @@ class HistoryCardPresenter(
else
HistoryChart.Square.OFF
val notesIndicators = entries.map {
when (it.notes) {
"" -> false
else -> true
}
}
return HistoryCardState(
color = habit.color,
firstWeekday = firstWeekday,
today = today.toLocalDate(),
theme = theme,
series = series,
defaultSquare = defaultSquare
defaultSquare = defaultSquare,
notesIndicators = notesIndicators,
)
}
}
@ -154,7 +201,17 @@ class HistoryCardPresenter(
fun showNumberPicker(
value: Double,
unit: String,
notes: String,
dateString: String,
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.round
fun interface OnDateClickedListener {
fun onDateClicked(date: LocalDate)
interface OnDateClickedListener {
fun onDateShortPress(date: LocalDate) {}
fun onDateLongPress(date: LocalDate) {}
}
class HistoryChart(
@ -42,9 +43,10 @@ class HistoryChart(
var paletteColor: PaletteColor,
var series: List<Square>,
var defaultSquare: Square,
var notesIndicators: List<Boolean>,
var theme: Theme,
var today: LocalDate,
var onDateClickedListener: OnDateClickedListener = OnDateClickedListener { },
var onDateClickedListener: OnDateClickedListener = object : OnDateClickedListener {},
var padding: Double = 0.0,
) : DataView {
@ -72,6 +74,14 @@ class HistoryChart(
get() = squareSpacing + squareSize
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)")
val col = ((x - padding) / squareSize).toInt()
val row = ((y - padding) / squareSize).toInt()
@ -79,7 +89,11 @@ class HistoryChart(
if (row == 0 || col == nColumns) return
val clickedDate = topLeftDate.plus(offset)
if (clickedDate.isNewerThan(today)) return
onDateClickedListener.onDateClicked(clickedDate)
if (isLongClick) {
onDateClickedListener.onDateLongPress(clickedDate)
} else {
onDateClickedListener.onDateShortPress(clickedDate)
}
}
override fun draw(canvas: Canvas) {
@ -191,7 +205,9 @@ class HistoryChart(
) {
val value = if (offset >= series.size) defaultSquare else series[offset]
val hasNotes = if (offset >= notesIndicators.size) false else notesIndicators[offset]
val squareColor: Color
val circleColor: Color
val color = theme.color(paletteColor.paletteIndex)
squareColor = when (value) {
Square.ON -> {
@ -235,5 +251,14 @@ class HistoryChart(
canvas.setColor(textColor)
canvas.setTextAlign(TextAlign.CENTER)
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?) {
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?) {
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) {
val currentValue = habit.originalEntries.get(timestamp).value
val entry = habit.originalEntries.get(timestamp)
val currentValue = entry.value
val newValue = nextToggleValue(
value = currentValue,
isSkipEnabled = preferences.isSkipEnabled,
areQuestionMarksEnabled = preferences.areQuestionMarksEnabled
)
setValue(habit, timestamp, newValue)
setValue(habit, timestamp, newValue, entry.notes)
notificationTray.cancel(habit)
}
fun onIncrement(habit: Habit, timestamp: Timestamp, amount: Int) {
val currentValue = habit.computedEntries.get(timestamp).value
setValue(habit, timestamp, currentValue + amount)
val entry = habit.computedEntries.get(timestamp)
val currentValue = entry.value
setValue(habit, timestamp, currentValue + amount, entry.notes)
notificationTray.cancel(habit)
}
fun onDecrement(habit: Habit, timestamp: Timestamp, amount: Int) {
val currentValue = habit.computedEntries.get(timestamp).value
setValue(habit, timestamp, currentValue - amount)
val entry = habit.computedEntries.get(timestamp)
val currentValue = entry.value
setValue(habit, timestamp, currentValue - amount, entry.notes)
notificationTray.cancel(habit)
}
fun setValue(habit: Habit, timestamp: Timestamp?, newValue: Int) {
fun setValue(habit: Habit, timestamp: Timestamp?, newValue: Int, notes: String) {
commandRunner.run(
CreateRepetitionCommand(habitList, habit, timestamp!!, newValue)
CreateRepetitionCommand(habitList, habit, timestamp!!, newValue, notes)
)
}
}

@ -41,5 +41,8 @@ class DateFormats {
@JvmStatic fun getCSVDateFormat(): SimpleDateFormat =
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()
habitList.add(habit)
today = getToday()
command = CreateRepetitionCommand(habitList, habit, today, 100)
command = CreateRepetitionCommand(habitList, habit, today, 100, "")
}
@Test

@ -54,6 +54,7 @@ class ImportTest : BaseUnitTest() {
assertTrue(isChecked(habit, 2016, 3, 18))
assertTrue(isChecked(habit, 2016, 3, 19))
assertFalse(isChecked(habit, 2016, 3, 20))
assertTrue(isNotesEqual(habit, 2016, 3, 18, "text"))
}
@Test
@ -68,6 +69,8 @@ class ImportTest : BaseUnitTest() {
assertTrue(isChecked(habit, 2019, 4, 11))
assertTrue(isChecked(habit, 2019, 5, 7))
assertFalse(isChecked(habit, 2019, 6, 14))
assertTrue(isNotesEqual(habit, 2019, 4, 11, "text"))
assertTrue(isNotesEqual(habit, 2019, 6, 14, "Habit 3 notes"))
}
@Test
@ -127,6 +130,13 @@ class ImportTest : BaseUnitTest() {
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)
private fun importFromFile(assetFilename: String) {
val file = File.createTempFile("asset", "")

@ -70,7 +70,7 @@ class HabitCardListCacheTest : BaseUnitTest() {
@Test
fun testCommandListener_single() {
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).onRefreshFinished()
verifyNoMoreInteractions(listener)

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

@ -72,6 +72,10 @@ class HistoryChartTest {
1 -> DIMMED
else -> OFF
}
},
notesIndicators = MutableList(85) {
index: Int ->
index % 3 == 0
}
)
@ -86,20 +90,20 @@ class HistoryChartTest {
// Click top left date
view.onClick(20.0, 46.0)
verify(dateClickedListener).onDateClicked(LocalDate(2014, 10, 26))
verify(dateClickedListener).onDateShortPress(LocalDate(2014, 10, 26))
reset(dateClickedListener)
view.onClick(2.0, 28.0)
verify(dateClickedListener).onDateClicked(LocalDate(2014, 10, 26))
verify(dateClickedListener).onDateShortPress(LocalDate(2014, 10, 26))
reset(dateClickedListener)
// Click date in the middle
view.onClick(163.0, 113.0)
verify(dateClickedListener).onDateClicked(LocalDate(2014, 12, 10))
verify(dateClickedListener).onDateShortPress(LocalDate(2014, 12, 10))
reset(dateClickedListener)
// Click today
view.onClick(336.0, 37.0)
verify(dateClickedListener).onDateClicked(LocalDate(2015, 1, 25))
verify(dateClickedListener).onDateShortPress(LocalDate(2015, 1, 25))
reset(dateClickedListener)
// Click header
@ -111,6 +115,37 @@ class HistoryChartTest {
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
fun testDrawWeekDay() = runBlocking {
view.firstWeekday = DayOfWeek.MONDAY

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

Loading…
Cancel
Save