Merge branch 'iSoron:dev' into dev

pull/1458/head
Jakub Kalinowski 3 years ago committed by GitHub
commit b82181bb2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -87,8 +87,8 @@ android {
dependencies { dependencies {
val daggerVersion = "2.41" val daggerVersion = "2.41"
val kotlinVersion = "1.6.10" val kotlinVersion = "1.6.21"
val kxCoroutinesVersion = "1.6.0" val kxCoroutinesVersion = "1.6.1"
val ktorVersion = "1.6.8" val ktorVersion = "1.6.8"
val espressoVersion = "3.4.0" val espressoVersion = "3.4.0"

@ -36,6 +36,7 @@ class EntryButtonViewTest : BaseViewTest() {
lateinit var view: CheckmarkButtonView lateinit var view: CheckmarkButtonView
var toggled = false var toggled = false
var edited = false
@Before @Before
override fun setUp() { override fun setUp() {
@ -43,7 +44,8 @@ class EntryButtonViewTest : BaseViewTest() {
view = component.getEntryButtonViewFactory().create().apply { view = component.getEntryButtonViewFactory().create().apply {
value = Entry.NO value = Entry.NO
color = PaletteUtils.getAndroidTestColor(5) color = PaletteUtils.getAndroidTestColor(5)
onToggle = { toggled = true } onToggle = { _, _, _ -> toggled = true }
onEdit = { _ -> edited = true }
} }
measureView(view, dpToPixels(48), dpToPixels(48)) measureView(view, dpToPixels(48), dpToPixels(48))
} }
@ -70,20 +72,28 @@ class EntryButtonViewTest : BaseViewTest() {
fun testClick_withShortToggleDisabled() { fun testClick_withShortToggleDisabled() {
prefs.isShortToggleEnabled = false prefs.isShortToggleEnabled = false
view.performClick() view.performClick()
assertFalse(toggled) assertTrue(!toggled and edited)
} }
@Test @Test
fun testClick_withShortToggleEnabled() { fun testClick_withShortToggleEnabled() {
prefs.isShortToggleEnabled = true prefs.isShortToggleEnabled = true
view.performClick() view.performClick()
assertTrue(toggled) assertTrue(toggled and !edited)
} }
@Test @Test
fun testLongClick() { fun testLongClick_withShortToggleDisabled() {
prefs.isShortToggleEnabled = false
view.performLongClick()
assertTrue(toggled and !edited)
}
@Test
fun testLongClick_withShortToggleEnabled() {
prefs.isShortToggleEnabled = true
view.performLongClick() view.performLongClick()
assertTrue(toggled) assertTrue(!toggled and edited)
} }
private fun assertRendersCheckedExplicitly() { private fun assertRendersCheckedExplicitly() {

@ -77,7 +77,7 @@ class EntryPanelViewTest : BaseViewTest() {
@Test @Test
fun testToggle() { fun testToggle() {
val timestamps = mutableListOf<Timestamp>() val timestamps = mutableListOf<Timestamp>()
view.onToggle = { t, _ -> timestamps.add(t) } view.onToggle = { t, _, _, _ -> timestamps.add(t) }
view.buttons[0].performLongClick() view.buttons[0].performLongClick()
view.buttons[2].performLongClick() view.buttons[2].performLongClick()
view.buttons[3].performLongClick() view.buttons[3].performLongClick()
@ -88,7 +88,7 @@ class EntryPanelViewTest : BaseViewTest() {
fun testToggle_withOffset() { fun testToggle_withOffset() {
val timestamps = mutableListOf<Timestamp>() val timestamps = mutableListOf<Timestamp>()
view.dataOffset = 3 view.dataOffset = 3
view.onToggle = { t, _ -> timestamps += t } view.onToggle = { t, _, _, _ -> timestamps += t }
view.buttons[0].performLongClick() view.buttons[0].performLongClick()
view.buttons[2].performLongClick() view.buttons[2].performLongClick()
view.buttons[3].performLongClick() view.buttons[3].performLongClick()

@ -76,7 +76,7 @@ class NumberPanelViewTest : BaseViewTest() {
@Test @Test
fun testEdit() { fun testEdit() {
val timestamps = mutableListOf<Timestamp>() val timestamps = mutableListOf<Timestamp>()
view.onEdit = { timestamps.plusAssign(it) } view.onEdit = { _, t -> timestamps.plusAssign(t) }
view.buttons[0].performLongClick() view.buttons[0].performLongClick()
view.buttons[2].performLongClick() view.buttons[2].performLongClick()
view.buttons[3].performLongClick() view.buttons[3].performLongClick()
@ -87,7 +87,7 @@ class NumberPanelViewTest : BaseViewTest() {
fun testEdit_withOffset() { fun testEdit_withOffset() {
val timestamps = mutableListOf<Timestamp>() val timestamps = mutableListOf<Timestamp>()
view.dataOffset = 3 view.dataOffset = 3
view.onEdit = { timestamps += it } view.onEdit = { _, t -> timestamps += t }
view.buttons[0].performLongClick() view.buttons[0].performLongClick()
view.buttons[2].performLongClick() view.buttons[2].performLongClick()
view.buttons[3].performLongClick() view.buttons[3].performLongClick()

@ -1,119 +0,0 @@
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.platform.time.JavaLocalDateFormatter
import org.isoron.platform.time.LocalDate
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 java.util.Locale
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(
selectedValue: Int,
notes: String,
date: LocalDate,
paletteColor: PaletteColor,
callback: ListHabitsBehavior.CheckMarkDialogCallback,
theme: Theme,
): AlertDialog {
binding = CheckmarkDialogBinding.inflate(LayoutInflater.from(context))
fontAwesome = InterfaceUtils.getFontAwesome(context)!!
binding.etNotes.append(notes)
setUpButtons(selectedValue, theme.color(paletteColor).toInt())
val dialog = AlertDialog.Builder(context)
.setView(binding.root)
.setTitle(JavaLocalDateFormatter(Locale.getDefault()).longFormat(date))
.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
}
}
}

@ -0,0 +1,127 @@
/*
* 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/>.
*/
package org.isoron.uhabits.activities.common.dialogs
import android.content.Context
import android.graphics.drawable.ColorDrawable
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.widget.PopupWindow
import org.isoron.platform.gui.ScreenLocation
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.preferences.Preferences
import org.isoron.uhabits.databinding.CheckmarkPopupBinding
import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome
import org.isoron.uhabits.utils.dimBehind
import org.isoron.uhabits.utils.dp
import org.isoron.uhabits.utils.sres
const val POPUP_WIDTH = 4 * 48f + 16f
const val POPUP_HEIGHT = 48f * 2.5f + 8f
class CheckmarkPopup(
private val context: Context,
private val color: Int,
private var notes: String,
private var value: Int,
private val prefs: Preferences,
private val anchor: View,
) {
var onToggle: (Int, String) -> Unit = { _, _ -> }
private val view = CheckmarkPopupBinding.inflate(LayoutInflater.from(context)).apply {
// Required for round corners
container.clipToOutline = true
}
init {
initColors()
initTypefaces()
hideDisabledButtons()
populate()
}
private fun initColors() {
arrayOf(view.yesBtn, view.skipBtn).forEach {
it.setTextColor(color)
}
arrayOf(view.noBtn, view.unknownBtn).forEach {
it.setTextColor(view.root.sres.getColor(R.attr.contrast60))
}
}
private fun initTypefaces() {
arrayOf(view.yesBtn, view.noBtn, view.skipBtn, view.unknownBtn).forEach {
it.typeface = getFontAwesome(context)
}
}
private fun hideDisabledButtons() {
if (!prefs.isSkipEnabled) view.skipBtn.visibility = GONE
if (!prefs.areQuestionMarksEnabled) view.unknownBtn.visibility = GONE
}
private fun populate() {
val selectedBtn = when (value) {
YES_MANUAL -> view.yesBtn
YES_AUTO -> view.noBtn
NO -> view.noBtn
UNKNOWN -> if (prefs.areQuestionMarksEnabled) view.unknownBtn else view.noBtn
SKIP -> if (prefs.isSkipEnabled) view.skipBtn else view.noBtn
else -> null
}
selectedBtn?.background = ColorDrawable(view.root.sres.getColor(R.attr.contrast40))
view.notes.setText(notes)
}
fun show(location: ScreenLocation) {
val popup = PopupWindow()
popup.contentView = view.root
popup.width = view.root.dp(POPUP_WIDTH).toInt()
popup.height = view.root.dp(POPUP_HEIGHT).toInt()
popup.isFocusable = true
popup.elevation = view.root.dp(24f)
fun onClick(v: Int) {
this.value = v
popup.dismiss()
}
view.yesBtn.setOnClickListener { onClick(YES_MANUAL) }
view.noBtn.setOnClickListener { onClick(NO) }
view.skipBtn.setOnClickListener { onClick(SKIP) }
view.unknownBtn.setOnClickListener { onClick(UNKNOWN) }
popup.setOnDismissListener {
onToggle(value, view.notes.text.toString())
}
popup.showAtLocation(
anchor,
Gravity.NO_GRAVITY,
view.root.dp(location.x.toFloat()).toInt(),
view.root.dp(location.y.toFloat()).toInt(),
)
popup.dimBehind()
}
}

@ -43,7 +43,7 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
private lateinit var commandRunner: CommandRunner private lateinit var commandRunner: CommandRunner
private lateinit var habit: Habit private lateinit var habit: Habit
private lateinit var preferences: Preferences private lateinit var preferences: Preferences
private lateinit var dataView: AndroidDataView lateinit var dataView: AndroidDataView
private var chart: HistoryChart? = null private var chart: HistoryChart? = null
private var onDateClickedListener: OnDateClickedListener? = null private var onDateClickedListener: OnDateClickedListener? = null

@ -50,7 +50,6 @@ class NumberPickerFactory
@Inject constructor( @Inject constructor(
@ActivityContext private val context: Context @ActivityContext private val context: Context
) { ) {
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun create( fun create(
value: Double, value: Double,
@ -60,6 +59,8 @@ class NumberPickerFactory
frequency: Frequency, frequency: Frequency,
callback: ListHabitsBehavior.NumberPickerCallback callback: ListHabitsBehavior.NumberPickerCallback
): AlertDialog { ): AlertDialog {
clearCurrentDialog()
val inflater = LayoutInflater.from(context) val inflater = LayoutInflater.from(context)
val view = inflater.inflate(R.layout.number_picker_dialog, null) val view = inflater.inflate(R.layout.number_picker_dialog, null)
@ -113,6 +114,7 @@ class NumberPickerFactory
} }
.setOnDismissListener { .setOnDismissListener {
callback.onNumberPickerDismissed() callback.onNumberPickerDismissed()
currentDialog = null
} }
if (frequency == DAILY) { if (frequency == DAILY) {
@ -153,6 +155,7 @@ class NumberPickerFactory
false false
} }
currentDialog = dialog
return dialog return dialog
} }
@ -169,6 +172,14 @@ class NumberPickerFactory
val inputMethodManager = context.getSystemService(InputMethodManager::class.java) val inputMethodManager = context.getSystemService(InputMethodManager::class.java)
inputMethodManager?.showSoftInput(v, 0) inputMethodManager?.showSoftInput(v, 0)
} }
companion object {
private var currentDialog: AlertDialog? = null
fun clearCurrentDialog() {
currentDialog?.dismiss()
currentDialog = null
}
}
} }
class SeparatorWatcherInputFilter(private val nextPicker: NumberPicker) : InputFilter { class SeparatorWatcherInputFilter(private val nextPicker: NumberPicker) : InputFilter {

@ -24,12 +24,14 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import dagger.Lazy import dagger.Lazy
import org.isoron.platform.time.LocalDate import org.isoron.platform.gui.ScreenLocation
import org.isoron.platform.gui.toInt
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.CheckmarkPopup
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
import org.isoron.uhabits.activities.common.dialogs.POPUP_WIDTH
import org.isoron.uhabits.activities.habits.edit.HabitTypeDialog import org.isoron.uhabits.activities.habits.edit.HabitTypeDialog
import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
import org.isoron.uhabits.core.commands.ArchiveHabitsCommand import org.isoron.uhabits.core.commands.ArchiveHabitsCommand
@ -43,6 +45,7 @@ import org.isoron.uhabits.core.commands.UnarchiveHabitsCommand
import org.isoron.uhabits.core.models.Frequency import org.isoron.uhabits.core.models.Frequency
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.models.PaletteColor
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.tasks.TaskRunner import org.isoron.uhabits.core.tasks.TaskRunner
import org.isoron.uhabits.core.ui.ThemeSwitcher import org.isoron.uhabits.core.ui.ThemeSwitcher
import org.isoron.uhabits.core.ui.callbacks.OnColorPickedCallback import org.isoron.uhabits.core.ui.callbacks.OnColorPickedCallback
@ -63,6 +66,7 @@ import org.isoron.uhabits.tasks.ExportDBTaskFactory
import org.isoron.uhabits.tasks.ImportDataTask import org.isoron.uhabits.tasks.ImportDataTask
import org.isoron.uhabits.tasks.ImportDataTaskFactory import org.isoron.uhabits.tasks.ImportDataTaskFactory
import org.isoron.uhabits.utils.copyTo import org.isoron.uhabits.utils.copyTo
import org.isoron.uhabits.utils.currentTheme
import org.isoron.uhabits.utils.restartWithFade import org.isoron.uhabits.utils.restartWithFade
import org.isoron.uhabits.utils.showMessage import org.isoron.uhabits.utils.showMessage
import org.isoron.uhabits.utils.showSendEmailScreen import org.isoron.uhabits.utils.showSendEmailScreen
@ -92,8 +96,9 @@ 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> private val preferences: Preferences,
private val rootView: Lazy<ListHabitsRootView>,
) : CommandRunner.Listener, ) : CommandRunner.Listener,
ListHabitsBehavior.Screen, ListHabitsBehavior.Screen,
ListHabitsMenuBehavior.Screen, ListHabitsMenuBehavior.Screen,
@ -237,22 +242,30 @@ class ListHabitsScreen
numberPickerFactory.create(value, unit, notes, dateString, frequency, callback).show() numberPickerFactory.create(value, unit, notes, dateString, frequency, callback).show()
} }
override fun showCheckmarkDialog( override fun showCheckmarkPopup(
selectedValue: Int, selectedValue: Int,
notes: String, notes: String,
date: LocalDate,
dateString: String,
color: PaletteColor, color: PaletteColor,
location: ScreenLocation,
callback: ListHabitsBehavior.CheckMarkDialogCallback callback: ListHabitsBehavior.CheckMarkDialogCallback
) { ) {
checkMarkDialog.create( val view = rootView.get()
selectedValue, CheckmarkPopup(
notes, context = context,
date, prefs = preferences,
color, anchor = view,
callback, color = view.currentTheme().color(color).toInt(),
themeSwitcher.currentTheme!!, notes = notes,
).show() value = selectedValue,
).apply {
onToggle = { value, notes -> callback.onNotesSaved(value, notes) }
show(
ScreenLocation(
x = location.x - POPUP_WIDTH / 2,
y = location.y
)
)
}
} }
private fun getExecuteString(command: Command): String? { private fun getExecuteString(command: Command): String? {

@ -28,6 +28,7 @@ import android.text.TextPaint
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import android.view.View import android.view.View
import android.view.View.MeasureSpec.EXACTLY import android.view.View.MeasureSpec.EXACTLY
import org.isoron.platform.gui.ScreenLocation
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Entry import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.Entry.Companion.NO import org.isoron.uhabits.core.models.Entry.Companion.NO
@ -38,12 +39,15 @@ 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.drawNotesIndicator
import org.isoron.uhabits.utils.getCenter
import org.isoron.uhabits.utils.getFontAwesome import org.isoron.uhabits.utils.getFontAwesome
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
import javax.inject.Inject import javax.inject.Inject
const val TOGGLE_DELAY_MILLIS = 2000L
class CheckmarkButtonViewFactory class CheckmarkButtonViewFactory
@Inject constructor( @Inject constructor(
@ActivityContext val context: Context, @ActivityContext val context: Context,
@ -71,15 +75,16 @@ class CheckmarkButtonView(
invalidate() invalidate()
} }
var hasNotes = false var notes = ""
set(value) { set(value) {
field = value field = value
invalidate() invalidate()
} }
var onToggle: (Int) -> Unit = {} var onToggle: (Int, String, Long) -> Unit = { _, _, _ -> }
var onEdit: (ScreenLocation) -> Unit = { _ -> }
var onEdit: () -> Unit = {}
private var drawer = Drawer() private var drawer = Drawer()
init { init {
@ -87,25 +92,25 @@ class CheckmarkButtonView(
setOnLongClickListener(this) setOnLongClickListener(this)
} }
fun performToggle() { fun performToggle(delay: Long) {
value = Entry.nextToggleValue( value = Entry.nextToggleValue(
value = value, value = value,
isSkipEnabled = preferences.isSkipEnabled, isSkipEnabled = preferences.isSkipEnabled,
areQuestionMarksEnabled = preferences.areQuestionMarksEnabled areQuestionMarksEnabled = preferences.areQuestionMarksEnabled
) )
onToggle(value) onToggle(value, notes, delay)
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
invalidate() invalidate()
} }
override fun onClick(v: View) { override fun onClick(v: View) {
if (preferences.isShortToggleEnabled) performToggle() if (preferences.isShortToggleEnabled) performToggle(TOGGLE_DELAY_MILLIS)
else onEdit() else onEdit(getCenter())
} }
override fun onLongClick(v: View): Boolean { override fun onLongClick(v: View): Boolean {
if (preferences.isShortToggleEnabled) onEdit() if (preferences.isShortToggleEnabled) onEdit(getCenter())
else performToggle() else performToggle(TOGGLE_DELAY_MILLIS)
return true return true
} }
@ -179,7 +184,7 @@ class CheckmarkButtonView(
canvas.drawText(label, rect.centerX(), rect.centerY(), paint) canvas.drawText(label, rect.centerX(), rect.centerY(), paint)
} }
drawNotesIndicator(canvas, color, em, hasNotes) drawNotesIndicator(canvas, color, em, notes)
} }
} }
} }

@ -20,6 +20,7 @@
package org.isoron.uhabits.activities.habits.list.views package org.isoron.uhabits.activities.habits.list.views
import android.content.Context import android.content.Context
import org.isoron.platform.gui.ScreenLocation
import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN
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
@ -54,19 +55,19 @@ class CheckmarkPanelView(
setupButtons() setupButtons()
} }
var notesIndicators = BooleanArray(0) var notes = arrayOf<String>()
set(values) { set(values) {
field = values field = values
setupButtons() setupButtons()
} }
var onToggle: (Timestamp, Int) -> Unit = { _, _ -> } var onToggle: (Timestamp, Int, String, Long) -> Unit = { _, _, _, _ -> }
set(value) { set(value) {
field = value field = value
setupButtons() setupButtons()
} }
var onEdit: (Timestamp) -> Unit = {} var onEdit: (ScreenLocation, Timestamp) -> Unit = { _, _ -> }
set(value) { set(value) {
field = value field = value
setupButtons() setupButtons()
@ -84,13 +85,13 @@ class CheckmarkPanelView(
index + dataOffset < values.size -> values[index + dataOffset] index + dataOffset < values.size -> values[index + dataOffset]
else -> UNKNOWN else -> UNKNOWN
} }
button.hasNotes = when { button.notes = when {
index + dataOffset < notesIndicators.size -> notesIndicators[index + dataOffset] index + dataOffset < notes.size -> notes[index + dataOffset]
else -> false else -> ""
} }
button.color = color button.color = color
button.onToggle = { value -> onToggle(timestamp, value) } button.onToggle = { value, notes, delay -> onToggle(timestamp, value, notes, delay) }
button.onEdit = { onEdit(timestamp) } button.onEdit = { location -> onEdit(location, timestamp) }
} }
} }
} }

@ -124,9 +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 notes = cache.getNotes(habit.id!!)
val selected = selected.contains(habit) val selected = selected.contains(habit)
listView!!.bindCardView(holder, habit, score, checkmarks, notesIndicators, selected) listView!!.bindCardView(holder, habit, score, checkmarks, notes, selected)
} }
override fun onViewAttachedToWindow(holder: HabitCardViewHolder) { override fun onViewAttachedToWindow(holder: HabitCardViewHolder) {

@ -87,7 +87,7 @@ class HabitCardListView(
habit: Habit, habit: Habit,
score: Double, score: Double,
checkmarks: IntArray, checkmarks: IntArray,
notesIndicators: BooleanArray, notes: Array<String>,
selected: Boolean selected: Boolean
): View { ): View {
val cardView = holder.itemView as HabitCardView val cardView = holder.itemView as HabitCardView
@ -99,7 +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 cardView.notes = notes
val detector = GestureDetector(context, CardViewGestureDetector(holder)) val detector = GestureDetector(context, CardViewGestureDetector(holder))
cardView.setOnTouchListener { _, ev -> cardView.setOnTouchListener { _, ev ->

@ -60,7 +60,8 @@ class HabitCardViewFactory
data class DelayedToggle( data class DelayedToggle(
var habit: Habit, var habit: Habit,
var timestamp: Timestamp, var timestamp: Timestamp,
var value: Int var value: Int,
var notes: String
) )
class HabitCardView( class HabitCardView(
@ -121,11 +122,11 @@ class HabitCardView(
numberPanel.threshold = value numberPanel.threshold = value
} }
var notesIndicators var notes
get() = checkmarkPanel.notesIndicators get() = checkmarkPanel.notes
set(values) { set(values) {
checkmarkPanel.notesIndicators = values checkmarkPanel.notes = values
numberPanel.notesIndicators = values numberPanel.notes = values
} }
var checkmarkPanel: CheckmarkPanelView var checkmarkPanel: CheckmarkPanelView
@ -159,24 +160,24 @@ class HabitCardView(
} }
checkmarkPanel = checkmarkPanelFactory.create().apply { checkmarkPanel = checkmarkPanelFactory.create().apply {
onToggle = { timestamp, value -> onToggle = { timestamp, value, notes, delay ->
triggerRipple(timestamp) if (delay > 0) triggerRipple(timestamp)
habit?.let { habit?.let {
val taskId = queueToggle(it, timestamp, value); val taskId = queueToggle(it, timestamp, value, notes);
{ runPendingToggles(taskId) }.delay(TOGGLE_DELAY_MILLIS) { runPendingToggles(taskId) }.delay(delay)
} }
} }
onEdit = { timestamp -> onEdit = { location, timestamp ->
triggerRipple(timestamp) triggerRipple(timestamp)
habit?.let { behavior.onEdit(it, timestamp) } habit?.let { behavior.onEdit(location, it, timestamp) }
} }
} }
numberPanel = numberPanelFactory.create().apply { numberPanel = numberPanelFactory.create().apply {
visibility = GONE visibility = GONE
onEdit = { timestamp -> onEdit = { location, timestamp ->
triggerRipple(timestamp) triggerRipple(timestamp)
habit?.let { behavior.onEdit(it, timestamp) } habit?.let { behavior.onEdit(location, it, timestamp) }
} }
} }
@ -207,7 +208,7 @@ class HabitCardView(
@Synchronized @Synchronized
private fun runPendingToggles(id: Int) { private fun runPendingToggles(id: Int) {
if (currentToggleTaskId != id) return if (currentToggleTaskId != id) return
for ((h, t, v) in queuedToggles) behavior.onToggle(h, t, v) for ((h, t, v, n) in queuedToggles) behavior.onToggle(h, t, v, n)
queuedToggles.clear() queuedToggles.clear()
} }
@ -215,10 +216,11 @@ class HabitCardView(
private fun queueToggle( private fun queueToggle(
it: Habit, it: Habit,
timestamp: Timestamp, timestamp: Timestamp,
value: Int value: Int,
notes: String,
): Int { ): Int {
currentToggleTaskId += 1 currentToggleTaskId += 1
queuedToggles.add(DelayedToggle(it, timestamp, value)) queuedToggles.add(DelayedToggle(it, timestamp, value, notes))
return currentToggleTaskId return currentToggleTaskId
} }
@ -308,8 +310,6 @@ class HabitCardView(
} }
companion object { companion object {
const val TOGGLE_DELAY_MILLIS = 1000L
fun (() -> Unit).delay(delayInMillis: Long) { fun (() -> Unit).delay(delayInMillis: Long) {
Handler(Looper.getMainLooper()).postDelayed(this, delayInMillis) Handler(Looper.getMainLooper()).postDelayed(this, delayInMillis)
} }

@ -102,7 +102,7 @@ class NumberButtonView(
field = value field = value
invalidate() invalidate()
} }
var hasNotes = false var notes = ""
set(value) { set(value) {
field = value field = value
invalidate() invalidate()
@ -221,7 +221,7 @@ class NumberButtonView(
canvas.drawText(units, rect.centerX(), rect.centerY(), pUnit) canvas.drawText(units, rect.centerX(), rect.centerY(), pUnit)
} }
drawNotesIndicator(canvas, color, em, hasNotes) drawNotesIndicator(canvas, color, em, notes)
} }
} }
} }

@ -20,11 +20,13 @@
package org.isoron.uhabits.activities.habits.list.views package org.isoron.uhabits.activities.habits.list.views
import android.content.Context import android.content.Context
import org.isoron.platform.gui.ScreenLocation
import org.isoron.uhabits.core.models.NumericalHabitType import org.isoron.uhabits.core.models.NumericalHabitType
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.utils.DateUtils import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.inject.ActivityContext import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.utils.getCenter
import javax.inject.Inject import javax.inject.Inject
class NumberPanelViewFactory class NumberPanelViewFactory
@ -72,13 +74,13 @@ class NumberPanelView(
setupButtons() setupButtons()
} }
var notesIndicators = BooleanArray(0) var notes = arrayOf<String>()
set(values) { set(values) {
field = values field = values
setupButtons() setupButtons()
} }
var onEdit: (Timestamp) -> Unit = {} var onEdit: (ScreenLocation, Timestamp) -> Unit = { _, _ -> }
set(value) { set(value) {
field = value field = value
setupButtons() setupButtons()
@ -96,15 +98,15 @@ 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 { button.notes = when {
index + dataOffset < notesIndicators.size -> notesIndicators[index + dataOffset] index + dataOffset < notes.size -> notes[index + dataOffset]
else -> false else -> ""
} }
button.color = color button.color = color
button.targetType = targetType button.targetType = targetType
button.threshold = threshold button.threshold = threshold
button.units = units button.units = units
button.onEdit = { onEdit(timestamp) } button.onEdit = { onEdit(getCenter(), timestamp) }
} }
} }
} }

@ -27,16 +27,18 @@ import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.isoron.platform.time.LocalDate import org.isoron.platform.gui.ScreenLocation
import org.isoron.platform.gui.toInt
import org.isoron.uhabits.AndroidDirFinder import org.isoron.uhabits.AndroidDirFinder
import org.isoron.uhabits.HabitsApplication 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.CheckmarkPopup
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.activities.common.dialogs.POPUP_WIDTH
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.Frequency import org.isoron.uhabits.core.models.Frequency
@ -49,6 +51,8 @@ import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitMenuPresenter
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitPresenter import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitPresenter
import org.isoron.uhabits.core.ui.views.OnDateClickedListener import org.isoron.uhabits.core.ui.views.OnDateClickedListener
import org.isoron.uhabits.intents.IntentFactory import org.isoron.uhabits.intents.IntentFactory
import org.isoron.uhabits.utils.currentTheme
import org.isoron.uhabits.utils.getTopLeftCorner
import org.isoron.uhabits.utils.showMessage import org.isoron.uhabits.utils.showMessage
import org.isoron.uhabits.utils.showSendFileScreen import org.isoron.uhabits.utils.showSendFileScreen
import org.isoron.uhabits.widgets.WidgetUpdater import org.isoron.uhabits.widgets.WidgetUpdater
@ -173,25 +177,45 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
frequency: Frequency, frequency: Frequency,
callback: ListHabitsBehavior.NumberPickerCallback callback: ListHabitsBehavior.NumberPickerCallback
) { ) {
NumberPickerFactory(this@ShowHabitActivity).create(value, unit, notes, dateString, frequency, callback).show() NumberPickerFactory(this@ShowHabitActivity).create(
value,
unit,
notes,
dateString,
frequency,
callback
).show()
} }
override fun showCheckmarkDialog( override fun showCheckmarkPopup(
selectedValue: Int, selectedValue: Int,
notes: String, notes: String,
date: LocalDate,
preferences: Preferences, preferences: Preferences,
color: PaletteColor, color: PaletteColor,
location: ScreenLocation,
callback: ListHabitsBehavior.CheckMarkDialogCallback callback: ListHabitsBehavior.CheckMarkDialogCallback
) { ) {
CheckmarkDialog(this@ShowHabitActivity, preferences).create( val dialog =
selectedValue, supportFragmentManager.findFragmentByTag("historyEditor") as HistoryEditorDialog?
notes, ?: return
date, val view = dialog.dataView
color, val corner = view.getTopLeftCorner()
callback, CheckmarkPopup(
themeSwitcher.currentTheme!!, context = this@ShowHabitActivity,
).show() prefs = preferences,
notes = notes,
color = view.currentTheme().color(color).toInt(),
anchor = view,
value = selectedValue,
).apply {
onToggle = { v, n -> callback.onNotesSaved(v, n) }
show(
ScreenLocation(
x = corner.x + location.x - POPUP_WIDTH / 2,
y = corner.y + location.y,
)
)
}
} }
override fun showEditHabitScreen(habit: Habit) { override fun showEditHabitScreen(habit: Habit) {

@ -21,6 +21,7 @@ package org.isoron.uhabits.utils
import android.app.Activity import android.app.Activity
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
@ -32,6 +33,8 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.view.WindowManager
import android.widget.PopupWindow
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.RelativeLayout.ALIGN_PARENT_BOTTOM import android.widget.RelativeLayout.ALIGN_PARENT_BOTTOM
import android.widget.RelativeLayout.ALIGN_PARENT_TOP import android.widget.RelativeLayout.ALIGN_PARENT_TOP
@ -42,6 +45,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import org.isoron.platform.gui.ScreenLocation
import org.isoron.platform.gui.toInt import org.isoron.platform.gui.toInt
import org.isoron.uhabits.HabitsApplication import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R import org.isoron.uhabits.R
@ -202,10 +206,10 @@ 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) { fun View.drawNotesIndicator(canvas: Canvas, color: Int, size: Float, notes: String) {
val pNotesIndicator = Paint() val pNotesIndicator = Paint()
pNotesIndicator.color = color pNotesIndicator.color = color
if (hasNotes) { if (notes.isNotBlank()) {
val cy = 0.8f * size val cy = 0.8f * size
canvas.drawCircle(width.toFloat() - cy, cy, 8f, pNotesIndicator) canvas.drawCircle(width.toFloat() - cy, cy, 8f, pNotesIndicator)
} }
@ -213,3 +217,42 @@ fun View.drawNotesIndicator(canvas: Canvas, color: Int, size: Float, hasNotes: B
val View.sres: StyledResources val View.sres: StyledResources
get() = StyledResources(context) get() = StyledResources(context)
fun PopupWindow.dimBehind() {
// https://stackoverflow.com/questions/35874001/dim-the-background-using-popupwindow-in-android
val container = contentView.rootView
val context = contentView.context
val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
val p = container.layoutParams as WindowManager.LayoutParams
p.flags = p.flags or WindowManager.LayoutParams.FLAG_DIM_BEHIND
p.dimAmount = 0.5f
wm.updateViewLayout(container, p)
}
/**
* Returns the absolute screen coordinates for the center of this view (in density-independent
* pixels).
*/
fun View.getCenter(): ScreenLocation {
val density = resources.displayMetrics.density
val loc = IntArray(2)
this.getLocationInWindow(loc)
return ScreenLocation(
x = ((loc[0] + width / 2) / density).toDouble(),
y = ((loc[1] + height / 2) / density).toDouble(),
)
}
/**
* Returns the absolute screen coordinates for the top left corner of this view (in
* density-independent pixels).
*/
fun View.getTopLeftCorner(): ScreenLocation {
val density = resources.displayMetrics.density
val loc = IntArray(2)
this.getLocationInWindow(loc)
return ScreenLocation(
x = (loc[0] / density).toDouble(),
y = (loc[1] / density).toDouble(),
)
}

@ -0,0 +1,27 @@
<?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"
android:shape="rectangle">
<solid android:color="?attr/contrast0" />
<stroke
android:width="2dp"
android:color="?contrast40" />
<corners android:radius="5dp" />
</shape>

@ -0,0 +1,25 @@
<?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">
<size android:width="1dip"/>
<size android:height="1dip"/>
<solid android:color="?contrast40"/>
</shape>

@ -1,95 +0,0 @@
<?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>

@ -0,0 +1,72 @@
<?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/>.
-->
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:divider="@drawable/checkmark_dialog_divider"
app:showDividers="middle"
android:orientation="vertical"
android:background="@drawable/checkmark_dialog_bg">
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/notes"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center"
android:inputType="textCapSentences|textMultiLine"
android:textSize="@dimen/smallTextSize"
android:padding="4dp"
android:background="@color/transparent"
android:hint="@string/notes"
android:text="" />
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="48dp"
android:orientation="horizontal"
app:divider="@drawable/checkmark_dialog_divider"
app:showDividers="middle">
<TextView
android:id="@+id/yesBtn"
style="@style/CheckmarkPopupBtn"
android:text="@string/fa_check" />
<TextView
android:id="@+id/skipBtn"
style="@style/CheckmarkPopupBtn"
android:text="@string/fa_skipped" />
<TextView
android:id="@+id/noBtn"
style="@style/CheckmarkPopupBtn"
android:text="@string/fa_times" />
<TextView
android:id="@+id/unknownBtn"
style="@style/CheckmarkPopupBtn"
android:text="@string/fa_question" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>

@ -387,16 +387,15 @@
<item name="android:textSize">@dimen/smallTextSize</item> <item name="android:textSize">@dimen/smallTextSize</item>
</style> </style>
<style name="CheckmarkDialogBtn"> <style name="CheckmarkPopupBtn">
<item name="android:layout_width">48dp</item> <item name="android:layout_width">0dp</item>
<item name="android:layout_height">48dp</item> <item name="android:layout_weight">1</item>
<item name="android:layout_marginTop">8dp</item> <item name="android:layout_height">match_parent</item>
<item name="android:layout_marginBottom">8dp</item> <item name="android:gravity">center</item>
<item name="android:layout_marginEnd">12dp</item> <item name="android:clickable">true</item>
<item name="android:textSize">@dimen/regularTextSize</item> <item name="android:focusable">true</item>
<item name="backgroundTint">@null</item> <item name="android:background">@drawable/ripple_transparent</item>
<item name="android:background">@drawable/bg_select_button</item> <item name="android:textSize">@dimen/smallerTextSize</item>
<item name="selectable">true</item>
</style> </style>
</resources> </resources>

@ -45,8 +45,8 @@ kotlin {
implementation(kotlin("stdlib-jdk8")) implementation(kotlin("stdlib-jdk8"))
compileOnly("com.google.dagger:dagger:2.41") compileOnly("com.google.dagger:dagger:2.41")
implementation("com.google.guava:guava:31.1-android") implementation("com.google.guava:guava:31.1-android")
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.6.10") implementation("org.jetbrains.kotlin:kotlin-stdlib:1.6.21")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.1")
implementation("androidx.annotation:annotation:1.3.0") implementation("androidx.annotation:annotation:1.3.0")
implementation("com.google.code.findbugs:jsr305:3.0.2") implementation("com.google.code.findbugs:jsr305:3.0.2")
implementation("com.opencsv:opencsv:5.6") implementation("com.opencsv:opencsv:5.6")

@ -29,6 +29,11 @@ enum class Font {
FONT_AWESOME FONT_AWESOME
} }
data class ScreenLocation(
val x: Double,
val y: Double,
)
interface Canvas { interface Canvas {
fun setColor(color: Color) fun setColor(color: Color)
fun drawLine(x1: Double, y1: Double, x2: Double, y2: Double) fun drawLine(x1: Double, y1: Double, x2: Double, y2: Double)

@ -79,8 +79,8 @@ class HabitCardListCache @Inject constructor(
} }
@Synchronized @Synchronized
fun getNoteIndicators(habitId: Long): BooleanArray { fun getNotes(habitId: Long): Array<String> {
return data.notesIndicators[habitId]!! return data.notes[habitId]!!
} }
@Synchronized @Synchronized
@ -168,7 +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.notes.remove(id)
data.scores.remove(id) data.scores.remove(id)
listener.onItemRemoved(position) listener.onItemRemoved(position)
} }
@ -213,7 +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> val notes: HashMap<Long?, Array<String>>
@Synchronized @Synchronized
fun copyCheckmarksFrom(oldData: CacheData) { fun copyCheckmarksFrom(oldData: CacheData) {
@ -226,10 +226,10 @@ class HabitCardListCache @Inject constructor(
@Synchronized @Synchronized
fun copyNoteIndicatorsFrom(oldData: CacheData) { fun copyNoteIndicatorsFrom(oldData: CacheData) {
val empty = BooleanArray(checkmarkCount) val empty = (0..checkmarkCount).map { "" }.toTypedArray()
for (id in idToHabit.keys) { for (id in idToHabit.keys) {
if (oldData.notesIndicators.containsKey(id)) notesIndicators[id] = if (oldData.notes.containsKey(id)) notes[id] =
oldData.notesIndicators[id]!! else notesIndicators[id] = empty oldData.notes[id]!! else notes[id] = empty
} }
} }
@ -257,7 +257,7 @@ class HabitCardListCache @Inject constructor(
habits = LinkedList() habits = LinkedList()
checkmarks = HashMap() checkmarks = HashMap()
scores = HashMap() scores = HashMap()
notesIndicators = HashMap() notes = HashMap()
} }
} }
@ -298,14 +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()
val notesIndicators: MutableList<Boolean> = ArrayList() val notes: MutableList<String> = ArrayList()
for ((_, value, note) in habit.computedEntries.getByInterval(dateFrom, today)) { for ((_, value, note) in habit.computedEntries.getByInterval(dateFrom, today)) {
list.add(value) list.add(value)
notesIndicators.add(note.isNotEmpty()) notes.add(note)
} }
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() newData.notes[habit.id] = notes.toTypedArray()
runner!!.publishProgress(this, position) runner!!.publishProgress(this, position)
} }
} }
@ -333,7 +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]!! data.notes[id] = newData.notes[id]!!
listener.onItemInserted(position) listener.onItemInserted(position)
} }
@ -361,10 +361,10 @@ 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 oldNoteIndicators = data.notes[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]!! val newNoteIndicators = newData.notes[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
@ -372,7 +372,7 @@ class HabitCardListCache @Inject constructor(
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 data.notes[id] = newNoteIndicators
listener.onItemChanged(position) listener.onItemChanged(position)
} }

@ -18,7 +18,7 @@
*/ */
package org.isoron.uhabits.core.ui.screens.habits.list package org.isoron.uhabits.core.ui.screens.habits.list
import org.isoron.platform.time.LocalDate import org.isoron.platform.gui.ScreenLocation
import org.isoron.uhabits.core.commands.CommandRunner 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.Frequency import org.isoron.uhabits.core.models.Frequency
@ -50,7 +50,7 @@ open class ListHabitsBehavior @Inject constructor(
screen.showHabitScreen(h) screen.showHabitScreen(h)
} }
fun onEdit(habit: Habit, timestamp: Timestamp?) { fun onEdit(location: ScreenLocation, habit: Habit, timestamp: Timestamp?) {
val entry = habit.computedEntries.get(timestamp!!) val entry = habit.computedEntries.get(timestamp!!)
if (habit.type == HabitType.NUMERICAL) { if (habit.type == HabitType.NUMERICAL) {
val oldValue = entry.value.toDouble() val oldValue = entry.value.toDouble()
@ -65,12 +65,11 @@ open class ListHabitsBehavior @Inject constructor(
commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, value, newNotes)) commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, value, newNotes))
} }
} else { } else {
screen.showCheckmarkDialog( screen.showCheckmarkPopup(
entry.value, entry.value,
entry.notes, entry.notes,
timestamp.toLocalDate(),
timestamp.toDialogDateString(),
habit.color, habit.color,
location,
) { newValue, newNotes -> ) { newValue, newNotes ->
commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, newValue, newNotes)) commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, newValue, newNotes))
} }
@ -123,8 +122,7 @@ open class ListHabitsBehavior @Inject constructor(
if (prefs.isFirstRun) onFirstRun() if (prefs.isFirstRun) onFirstRun()
} }
fun onToggle(habit: Habit, timestamp: Timestamp?, value: Int) { fun onToggle(habit: Habit, timestamp: Timestamp, value: Int, notes: String) {
val notes = habit.computedEntries.get(timestamp!!).notes
commandRunner.run( commandRunner.run(
CreateRepetitionCommand(habitList, habit, timestamp, value, notes) CreateRepetitionCommand(habitList, habit, timestamp, value, notes)
) )
@ -172,15 +170,13 @@ open class ListHabitsBehavior @Inject constructor(
frequency: Frequency, frequency: Frequency,
callback: NumberPickerCallback callback: NumberPickerCallback
) )
fun showCheckmarkDialog( fun showCheckmarkPopup(
selectedValue: Int, selectedValue: Int,
notes: String, notes: String,
date: LocalDate,
dateString: String,
color: PaletteColor, color: PaletteColor,
location: ScreenLocation,
callback: CheckMarkDialogCallback callback: CheckMarkDialogCallback
) )
fun showSendBugReportToDeveloperScreen(log: String) fun showSendBugReportToDeveloperScreen(log: String)
fun showSendFileScreen(filename: String) fun showSendFileScreen(filename: String)
} }

@ -19,6 +19,7 @@
package org.isoron.uhabits.core.ui.screens.habits.show.views package org.isoron.uhabits.core.ui.screens.habits.show.views
import org.isoron.platform.gui.ScreenLocation
import org.isoron.platform.time.DayOfWeek import org.isoron.platform.time.DayOfWeek
import org.isoron.platform.time.LocalDate import org.isoron.platform.time.LocalDate
import org.isoron.uhabits.core.commands.CommandRunner import org.isoron.uhabits.core.commands.CommandRunner
@ -65,55 +66,65 @@ class HistoryCardPresenter(
val screen: Screen, val screen: Screen,
) : OnDateClickedListener { ) : OnDateClickedListener {
override fun onDateLongPress(date: LocalDate) { override fun onDateLongPress(location: ScreenLocation, date: LocalDate) {
val timestamp = Timestamp.fromLocalDate(date) val timestamp = Timestamp.fromLocalDate(date)
screen.showFeedback() screen.showFeedback()
if (habit.isNumerical) { if (habit.isNumerical) {
showNumberPicker(timestamp) showNumberPicker(timestamp)
} else { } else {
val entry = habit.computedEntries.get(timestamp) if (preferences.isShortToggleEnabled) showCheckmarkPopup(location, timestamp)
val nextValue = Entry.nextToggleValue( else toggle(timestamp)
value = entry.value, }
isSkipEnabled = preferences.isSkipEnabled, }
areQuestionMarksEnabled = preferences.areQuestionMarksEnabled
) override fun onDateShortPress(location: ScreenLocation, date: LocalDate) {
val timestamp = Timestamp.fromLocalDate(date)
screen.showFeedback()
if (habit.isNumerical) {
showNumberPicker(timestamp)
} else {
if (preferences.isShortToggleEnabled) toggle(timestamp)
else showCheckmarkPopup(location, timestamp)
}
}
private fun showCheckmarkPopup(location: ScreenLocation, timestamp: Timestamp) {
val entry = habit.computedEntries.get(timestamp)
screen.showCheckmarkPopup(
entry.value,
entry.notes,
preferences,
habit.color,
location,
) { newValue, newNotes ->
commandRunner.run( commandRunner.run(
CreateRepetitionCommand( CreateRepetitionCommand(
habitList, habitList,
habit, habit,
timestamp, timestamp,
nextValue, newValue,
entry.notes, newNotes,
), ),
) )
} }
} }
override fun onDateShortPress(date: LocalDate) { private fun toggle(timestamp: Timestamp) {
val timestamp = Timestamp.fromLocalDate(date) val entry = habit.computedEntries.get(timestamp)
screen.showFeedback() val nextValue = Entry.nextToggleValue(
if (habit.isNumerical) { value = entry.value,
showNumberPicker(timestamp) isSkipEnabled = preferences.isSkipEnabled,
} else { areQuestionMarksEnabled = preferences.areQuestionMarksEnabled
val entry = habit.computedEntries.get(timestamp) )
screen.showCheckmarkDialog( commandRunner.run(
entry.value, CreateRepetitionCommand(
habitList,
habit,
timestamp,
nextValue,
entry.notes, entry.notes,
timestamp.toLocalDate(), ),
preferences, )
habit.color,
) { newValue, newNotes ->
commandRunner.run(
CreateRepetitionCommand(
habitList,
habit,
timestamp,
newValue,
newNotes,
),
)
}
}
} }
private fun showNumberPicker(timestamp: Timestamp) { private fun showNumberPicker(timestamp: Timestamp) {
@ -203,12 +214,12 @@ class HistoryCardPresenter(
callback: ListHabitsBehavior.NumberPickerCallback callback: ListHabitsBehavior.NumberPickerCallback
) )
fun showCheckmarkDialog( fun showCheckmarkPopup(
selectedValue: Int, selectedValue: Int,
notes: String, notes: String,
date: LocalDate,
preferences: Preferences, preferences: Preferences,
color: PaletteColor, color: PaletteColor,
location: ScreenLocation,
callback: ListHabitsBehavior.CheckMarkDialogCallback, callback: ListHabitsBehavior.CheckMarkDialogCallback,
) )
} }

@ -22,6 +22,7 @@ package org.isoron.uhabits.core.ui.views
import org.isoron.platform.gui.Canvas import org.isoron.platform.gui.Canvas
import org.isoron.platform.gui.Color import org.isoron.platform.gui.Color
import org.isoron.platform.gui.DataView import org.isoron.platform.gui.DataView
import org.isoron.platform.gui.ScreenLocation
import org.isoron.platform.gui.TextAlign import org.isoron.platform.gui.TextAlign
import org.isoron.platform.time.DayOfWeek import org.isoron.platform.time.DayOfWeek
import org.isoron.platform.time.LocalDate import org.isoron.platform.time.LocalDate
@ -33,8 +34,8 @@ import kotlin.math.min
import kotlin.math.round import kotlin.math.round
interface OnDateClickedListener { interface OnDateClickedListener {
fun onDateShortPress(date: LocalDate) {} fun onDateShortPress(location: ScreenLocation, date: LocalDate) {}
fun onDateLongPress(date: LocalDate) {} fun onDateLongPress(location: ScreenLocation, date: LocalDate) {}
} }
class HistoryChart( class HistoryChart(
@ -90,10 +91,11 @@ class HistoryChart(
if (x - padding < 0 || row == 0 || row > 7 || col == nColumns) return if (x - padding < 0 || row == 0 || row > 7 || col == nColumns) return
val clickedDate = topLeftDate.plus(offset) val clickedDate = topLeftDate.plus(offset)
if (clickedDate.isNewerThan(today)) return if (clickedDate.isNewerThan(today)) return
val location = ScreenLocation(x, y)
if (isLongClick) { if (isLongClick) {
onDateClickedListener.onDateLongPress(clickedDate) onDateClickedListener.onDateLongPress(location, clickedDate)
} else { } else {
onDateClickedListener.onDateShortPress(clickedDate) onDateClickedListener.onDateShortPress(location, clickedDate)
} }
} }

@ -31,6 +31,7 @@ import junit.framework.Assert.assertTrue
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.core.IsEqual.equalTo import org.hamcrest.core.IsEqual.equalTo
import org.isoron.platform.gui.ScreenLocation
import org.isoron.uhabits.core.BaseUnitTest import org.isoron.uhabits.core.BaseUnitTest
import org.isoron.uhabits.core.models.Entry import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.Frequency import org.isoron.uhabits.core.models.Frequency
@ -79,7 +80,7 @@ class ListHabitsBehaviorTest : BaseUnitTest() {
@Test @Test
fun testOnEdit() { fun testOnEdit() {
behavior.onEdit(habit2, getToday()) behavior.onEdit(ScreenLocation(0.0, 0.0), habit2, getToday())
verify(screen).showNumberPicker( verify(screen).showNumberPicker(
eq(0.1), eq(0.1),
eq("miles"), eq("miles"),
@ -168,7 +169,12 @@ class ListHabitsBehaviorTest : BaseUnitTest() {
@Test @Test
fun testOnToggle() { fun testOnToggle() {
assertTrue(habit1.isCompletedToday()) assertTrue(habit1.isCompletedToday())
behavior.onToggle(habit1, getToday(), Entry.NO) behavior.onToggle(
habit = habit1,
timestamp = getToday(),
value = Entry.NO,
notes = ""
)
assertFalse(habit1.isCompletedToday()) assertFalse(habit1.isCompletedToday())
} }
} }

@ -24,6 +24,7 @@ import com.nhaarman.mockitokotlin2.reset
import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.isoron.platform.gui.ScreenLocation
import org.isoron.platform.gui.assertRenders import org.isoron.platform.gui.assertRenders
import org.isoron.platform.time.DayOfWeek import org.isoron.platform.time.DayOfWeek
import org.isoron.platform.time.DayOfWeek.SUNDAY import org.isoron.platform.time.DayOfWeek.SUNDAY
@ -73,8 +74,7 @@ class HistoryChartTest {
else -> OFF else -> OFF
} }
}, },
notesIndicators = MutableList(85) { notesIndicators = MutableList(85) { index: Int ->
index: Int ->
index % 3 == 0 index % 3 == 0
} }
) )
@ -90,20 +90,32 @@ class HistoryChartTest {
// Click top left date // Click top left date
view.onClick(20.0, 46.0) view.onClick(20.0, 46.0)
verify(dateClickedListener).onDateShortPress(LocalDate(2014, 10, 26)) verify(dateClickedListener).onDateShortPress(
ScreenLocation(20.0, 46.0),
LocalDate(2014, 10, 26)
)
reset(dateClickedListener) reset(dateClickedListener)
view.onClick(2.0, 28.0) view.onClick(2.0, 28.0)
verify(dateClickedListener).onDateShortPress(LocalDate(2014, 10, 26)) verify(dateClickedListener).onDateShortPress(
ScreenLocation(2.0, 28.0),
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).onDateShortPress(LocalDate(2014, 12, 10)) verify(dateClickedListener).onDateShortPress(
ScreenLocation(163.0, 113.0),
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).onDateShortPress(LocalDate(2015, 1, 25)) verify(dateClickedListener).onDateShortPress(
ScreenLocation(336.0, 37.0),
LocalDate(2015, 1, 25)
)
reset(dateClickedListener) reset(dateClickedListener)
// Click header // Click header
@ -121,20 +133,32 @@ class HistoryChartTest {
// Click top left date // Click top left date
view.onLongClick(20.0, 46.0) view.onLongClick(20.0, 46.0)
verify(dateClickedListener).onDateLongPress(LocalDate(2014, 10, 26)) verify(dateClickedListener).onDateLongPress(
ScreenLocation(20.0, 46.0),
LocalDate(2014, 10, 26)
)
reset(dateClickedListener) reset(dateClickedListener)
view.onLongClick(2.0, 28.0) view.onLongClick(2.0, 28.0)
verify(dateClickedListener).onDateLongPress(LocalDate(2014, 10, 26)) verify(dateClickedListener).onDateLongPress(
ScreenLocation(2.0, 28.0),
LocalDate(2014, 10, 26)
)
reset(dateClickedListener) reset(dateClickedListener)
// Click date in the middle // Click date in the middle
view.onLongClick(163.0, 113.0) view.onLongClick(163.0, 113.0)
verify(dateClickedListener).onDateLongPress(LocalDate(2014, 12, 10)) verify(dateClickedListener).onDateLongPress(
ScreenLocation(163.0, 113.0),
LocalDate(2014, 12, 10)
)
reset(dateClickedListener) reset(dateClickedListener)
// Click today // Click today
view.onLongClick(336.0, 37.0) view.onLongClick(336.0, 37.0)
verify(dateClickedListener).onDateLongPress(LocalDate(2015, 1, 25)) verify(dateClickedListener).onDateLongPress(
ScreenLocation(336.0, 37.0),
LocalDate(2015, 1, 25)
)
reset(dateClickedListener) reset(dateClickedListener)
// Click header // Click header

@ -34,7 +34,7 @@ application {
dependencies { dependencies {
val ktorVersion = "1.6.8" val ktorVersion = "1.6.8"
val kotlinVersion = "1.6.10" val kotlinVersion = "1.6.21"
val logbackVersion = "1.2.11" val logbackVersion = "1.2.11"
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion")
implementation("io.ktor:ktor-server-netty:$ktorVersion") implementation("io.ktor:ktor-server-netty:$ktorVersion")

Loading…
Cancel
Save