Added a notes field and implemented dialog for numeric habits

pull/1103/head
Bindu 4 years ago
parent c50c5af497
commit d38f83e961

@ -47,6 +47,7 @@ class NumberPickerFactory
fun create(
value: Double,
unit: String,
notes: String,
callback: ListHabitsBehavior.NumberPickerCallback
): AlertDialog {
val inflater = LayoutInflater.from(context)
@ -54,6 +55,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 +79,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) { _, _ ->
.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(R.string.cancel) { _, _ ->
callback.onNumberPickerDismissed()
}
.setOnDismissListener {
callback.onNumberPickerDismissed()

@ -225,9 +225,10 @@ class ListHabitsScreen
override fun showNumberPicker(
value: Double,
unit: String,
notes: String,
callback: ListHabitsBehavior.NumberPickerCallback
) {
numberPickerFactory.create(value, unit, callback).show()
numberPickerFactory.create(value, unit, notes, callback).show()
}
private fun getExecuteString(command: Command): String? {

@ -71,6 +71,12 @@ class CheckmarkButtonView(
invalidate()
}
var hasNotes = false
set(value) {
field = value
invalidate()
}
var onToggle: (Int) -> Unit = {}
private var drawer = Drawer()

@ -54,6 +54,12 @@ class CheckmarkPanelView(
setupButtons()
}
var notes = BooleanArray(0)
set(values) {
field = values
setupButtons()
}
var onToggle: (Timestamp, Int) -> Unit = { _, _ -> }
set(value) {
field = value
@ -72,6 +78,10 @@ class CheckmarkPanelView(
index + dataOffset < values.size -> values[index + dataOffset]
else -> UNKNOWN
}
button.hasNotes = when {
index + dataOffset < notes.size -> notes[index + dataOffset]
else -> false
}
button.color = color
button.onToggle = { value -> onToggle(timestamp, value) }
}

@ -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.notes = notesIndicators
val detector = GestureDetector(context, CardViewGestureDetector(holder))
cardView.setOnTouchListener { _, ev ->

@ -115,6 +115,13 @@ class HabitCardView(
numberPanel.threshold = value
}
var notes
get() = numberPanel.notes
set(values) {
checkmarkPanel.notes = values
numberPanel.notes = values
}
var checkmarkPanel: CheckmarkPanelView
private var numberPanel: NumberPanelView
private var innerFrame: LinearLayout
@ -143,7 +150,7 @@ class HabitCardView(
checkmarkPanel = checkmarkPanelFactory.create().apply {
onToggle = { timestamp, value ->
triggerRipple(timestamp)
habit?.let { behavior.onToggle(it, timestamp, value) }
habit?.let { behavior.onToggle(it, timestamp, value, "") }
}
}

@ -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 {
@ -153,6 +157,8 @@ class NumberButtonView(
textAlign = Paint.Align.CENTER
}
private val pNotesIndicator: Paint = Paint()
init {
em = pNumber.measureText("m")
lowContrast = sres.getColor(R.attr.contrast40)
@ -200,6 +206,7 @@ class NumberButtonView(
pNumber.color = activeColor
pNumber.typeface = typeface
pUnit.color = activeColor
pNotesIndicator.color = activeColor
if (units.isBlank()) {
rect.set(0f, 0f, width.toFloat(), height.toFloat())
@ -211,6 +218,11 @@ class NumberButtonView(
rect.offset(0f, 1.3f * em)
canvas.drawText(units, rect.centerX(), rect.centerY(), pUnit)
}
if (hasNotes) {
val cy = 0.8f * em
canvas.drawCircle(width.toFloat() - cy, cy, 8f, pNotesIndicator)
}
}
}
}

@ -72,6 +72,12 @@ class NumberPanelView(
setupButtons()
}
var notes = 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 < notes.size -> notes[index + dataOffset]
else -> false
}
button.color = color
button.targetType = targetType
button.threshold = threshold

@ -164,9 +164,10 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
override fun showNumberPicker(
value: Double,
unit: String,
notes: String,
callback: ListHabitsBehavior.NumberPickerCallback,
) {
NumberPickerFactory(this@ShowHabitActivity).create(value, unit, callback).show()
NumberPickerFactory(this@ShowHabitActivity).create(value, unit, notes, callback).show()
}
override fun showEditHabitScreen(habit: Habit) {

@ -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,7 @@ class NumericalCheckmarkWidgetActivity : Activity(), ListHabitsBehavior.NumberPi
numberPickerFactory.create(
entry.value / 1000.0,
data.habit.unit,
entry.notes,
this
).show()
}

@ -19,33 +19,64 @@
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:gravity="center"
android:orientation="vertical"
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"/>
<LinearLayout
android:orientation="horizontal"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tvSeparator"
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: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_width="wrap_content"
android:layout_height="wrap_content"/>
<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="vertical"
android:padding="30dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/TextAppearance.AppCompat.Title"
android:paddingBottom="10dp"
android:text="@string/notes"/>
<EditText
android:id="@+id/etNotes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textCapSentences|textMultiLine"
style="@style/TextAppearance.AppCompat.Body1"
android:scrollbars="vertical"
android:hint="@string/example_notes"/>
<TextView
android:id="@+id/tvUnit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout>

@ -55,6 +55,7 @@
<string name="clear">Clear</string>
<string name="reminder">Reminder</string>
<string name="save">Save</string>
<string name="cancel">Cancel</string>
<string name="streaks">Streaks</string>
<string name="no_habits_found">You have no active habits</string>
<string name="no_habits_left_to_do">You\'re all done for today!</string>

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

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

@ -101,8 +101,8 @@ 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)
if (value != r.value || notes != r.notes) CreateRepetitionCommand(habitList, habit, t, r.value!!, r.notes!!).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 {
/**

@ -41,12 +41,16 @@ 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!!)
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 notesList: MutableList<Boolean> = ArrayList()
for ((_, value, note) in habit.computedEntries.getByInterval(dateFrom, today)) {
list.add(value)
if (note.isNotEmpty()) notesList.add(true) else notesList.add(false)
}
val entries = list.toTypedArray()
newData.checkmarks[habit.id] = ArrayUtils.toPrimitive(entries)
newData.notesIndicators[habit.id] = notesList.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)
}

@ -47,14 +47,16 @@ open class ListHabitsBehavior @Inject constructor(
}
fun onEdit(habit: Habit, timestamp: Timestamp?) {
val entries = habit.computedEntries
val oldValue = entries.get(timestamp!!).value.toDouble()
val entries = habit.computedEntries.get(timestamp!!)
val oldValue = entries.value.toDouble()
val notes = entries.notes
screen.showNumberPicker(
oldValue / 1000,
habit.unit
) { newValue: Double ->
habit.unit,
notes
) { newValue: Double, newNotes:String, ->
val value = (newValue * 1000).roundToInt()
commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, value))
commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, value, newNotes))
}
}
@ -104,9 +106,9 @@ open class ListHabitsBehavior @Inject constructor(
if (prefs.isFirstRun) onFirstRun()
}
fun onToggle(habit: Habit, timestamp: Timestamp?, value: Int) {
fun onToggle(habit: Habit, timestamp: Timestamp?, value: Int, notes: String) {
commandRunner.run(
CreateRepetitionCommand(habitList, habit, timestamp!!, value)
CreateRepetitionCommand(habitList, habit, timestamp!!, value, notes)
)
}
@ -131,7 +133,7 @@ open class ListHabitsBehavior @Inject constructor(
}
fun interface NumberPickerCallback {
fun onNumberPicked(newValue: Double)
fun onNumberPicked(newValue: Double, notes: String)
fun onNumberPickerDismissed() {}
}
@ -142,6 +144,7 @@ open class ListHabitsBehavior @Inject constructor(
fun showNumberPicker(
value: Double,
unit: String,
notes: String,
callback: NumberPickerCallback
)

@ -64,7 +64,8 @@ class HistoryCardPresenter(
if (habit.isNumerical) {
val entries = habit.computedEntries
val oldValue = entries.get(timestamp).value
screen.showNumberPicker(oldValue / 1000.0, habit.unit) { newValue: Double ->
val notes = entries.get(timestamp).notes
screen.showNumberPicker(oldValue / 1000.0, habit.unit, notes) { newValue: Double, newNotes: String ->
val thousands = (newValue * 1000).roundToInt()
commandRunner.run(
CreateRepetitionCommand(
@ -72,11 +73,14 @@ class HistoryCardPresenter(
habit,
timestamp,
thousands,
newNotes,
),
)
}
} else {
val currentValue = habit.computedEntries.get(timestamp).value
val entry = habit.computedEntries.get(timestamp)
val currentValue = entry.value
val notes = entry.notes
val nextValue = Entry.nextToggleValue(
value = currentValue,
isSkipEnabled = preferences.isSkipEnabled,
@ -88,6 +92,7 @@ class HistoryCardPresenter(
habit,
timestamp,
nextValue,
notes,
),
)
}
@ -154,6 +159,7 @@ class HistoryCardPresenter(
fun showNumberPicker(
value: Double,
unit: String,
notes: String,
callback: ListHabitsBehavior.NumberPickerCallback,
)
}

@ -46,31 +46,37 @@ class WidgetBehavior @Inject constructor(
}
fun onToggleRepetition(habit: Habit, timestamp: Timestamp) {
val currentValue = habit.originalEntries.get(timestamp).value
val entry = habit.computedEntries.get(timestamp)
val currentValue = entry.value
val notes = entry.notes
val newValue = nextToggleValue(
value = currentValue,
isSkipEnabled = preferences.isSkipEnabled,
areQuestionMarksEnabled = preferences.areQuestionMarksEnabled
)
setValue(habit, timestamp, newValue)
setValue(habit, timestamp, newValue, 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
val notes = entry.notes
setValue(habit, timestamp, currentValue + amount, 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
val notes = entry.notes
setValue(habit, timestamp, currentValue - amount, 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)
)
}
}

@ -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

@ -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"), "", picker.capture())
picker.lastValue.onNumberPicked(100.0, "")
val today = getTodayWithOffset()
assertThat(habit2.computedEntries.get(today).value, equalTo(100000))
}
@ -160,7 +160,7 @@ class ListHabitsBehaviorTest : BaseUnitTest() {
@Test
fun testOnToggle() {
assertTrue(habit1.isCompletedToday())
behavior.onToggle(habit1, getToday(), Entry.NO)
behavior.onToggle(habit1, getToday(), Entry.NO, "")
assertFalse(habit1.isCompletedToday())
}
}

Loading…
Cancel
Save