Implement CheckmarkPopup

pull/1394/head
Alinson S. Xavier 4 years ago
parent 0de6896691
commit 825a5f2cb9

@ -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() {
@ -44,6 +45,7 @@ class EntryButtonViewTest : BaseViewTest() {
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() {

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

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

@ -24,12 +24,16 @@ 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.gui.ScreenLocation
import org.isoron.platform.gui.toInt
import org.isoron.platform.time.LocalDate import org.isoron.platform.time.LocalDate
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.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 +47,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 +68,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
@ -93,7 +99,9 @@ class ListHabitsScreen
private val colorPickerFactory: ColorPickerDialogFactory, private val colorPickerFactory: ColorPickerDialogFactory,
private val numberPickerFactory: NumberPickerFactory, private val numberPickerFactory: NumberPickerFactory,
private val checkMarkDialog: CheckmarkDialog, 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,6 +245,32 @@ class ListHabitsScreen
numberPickerFactory.create(value, unit, notes, dateString, frequency, callback).show() numberPickerFactory.create(value, unit, notes, dateString, frequency, callback).show()
} }
override fun showCheckmarkPopup(
selectedValue: Int,
notes: String,
color: PaletteColor,
location: ScreenLocation,
callback: ListHabitsBehavior.CheckMarkDialogCallback
) {
val view = rootView.get()
CheckmarkPopup(
context = context,
prefs = preferences,
anchor = view,
color = view.currentTheme().color(color).toInt(),
notes = notes,
value = selectedValue,
).apply {
onToggle = { value, notes -> callback.onNotesSaved(value, notes) }
show(
ScreenLocation(
x = location.x - POPUP_WIDTH / 2,
y = location.y
)
)
}
}
override fun showCheckmarkDialog( override fun showCheckmarkDialog(
selectedValue: Int, selectedValue: Int,
notes: String, notes: 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,6 +39,7 @@ 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
@ -81,7 +83,8 @@ class CheckmarkButtonView(
var onToggle: (Int, String, Long) -> Unit = { _, _, _ -> } var onToggle: (Int, String, Long) -> Unit = { _, _, _ -> }
var onEdit: () -> Unit = {} var onEdit: (ScreenLocation) -> Unit = { _ -> }
private var drawer = Drawer() private var drawer = Drawer()
init { init {
@ -102,11 +105,11 @@ class CheckmarkButtonView(
override fun onClick(v: View) { override fun onClick(v: View) {
if (preferences.isShortToggleEnabled) performToggle(TOGGLE_DELAY_MILLIS) 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(TOGGLE_DELAY_MILLIS) else performToggle(TOGGLE_DELAY_MILLIS)
return true return true
} }

@ -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
@ -66,7 +67,7 @@ class CheckmarkPanelView(
setupButtons() setupButtons()
} }
var onEdit: (Timestamp) -> Unit = {} var onEdit: (ScreenLocation, Timestamp) -> Unit = { _, _ -> }
set(value) { set(value) {
field = value field = value
setupButtons() setupButtons()
@ -90,7 +91,7 @@ class CheckmarkPanelView(
} }
button.color = color button.color = color
button.onToggle = { value, notes, delay -> onToggle(timestamp, value, notes, delay) } button.onToggle = { value, notes, delay -> onToggle(timestamp, value, notes, delay) }
button.onEdit = { onEdit(timestamp) } button.onEdit = { location -> onEdit(location, timestamp) }
} }
} }
} }

@ -167,17 +167,17 @@ class HabitCardView(
{ runPendingToggles(taskId) }.delay(delay) { 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) }
} }
} }

@ -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
@ -78,7 +80,7 @@ class NumberPanelView(
setupButtons() setupButtons()
} }
var onEdit: (Timestamp) -> Unit = {} var onEdit: (ScreenLocation, Timestamp) -> Unit = { _, _ -> }
set(value) { set(value) {
field = value field = value
setupButtons() setupButtons()
@ -104,7 +106,7 @@ class NumberPanelView(
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,6 +27,8 @@ 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.gui.ScreenLocation
import org.isoron.platform.gui.toInt
import org.isoron.platform.time.LocalDate import org.isoron.platform.time.LocalDate
import org.isoron.uhabits.AndroidDirFinder import org.isoron.uhabits.AndroidDirFinder
import org.isoron.uhabits.HabitsApplication import org.isoron.uhabits.HabitsApplication
@ -34,9 +36,11 @@ 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.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 +53,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,7 +179,14 @@ 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 showCheckmarkDialog(
@ -194,6 +207,37 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
).show() ).show()
} }
override fun showCheckmarkPopup(
selectedValue: Int,
notes: String,
preferences: Preferences,
color: PaletteColor,
location: ScreenLocation,
callback: ListHabitsBehavior.CheckMarkDialogCallback
) {
val dialog =
supportFragmentManager.findFragmentByTag("historyEditor") as HistoryEditorDialog?
?: return
val view = dialog.dataView
val corner = view.getTopLeftCorner()
CheckmarkPopup(
context = this@ShowHabitActivity,
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) {
startActivity(IntentFactory().startEditActivity(this@ShowHabitActivity, habit)) startActivity(IntentFactory().startEditActivity(this@ShowHabitActivity, 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
@ -213,3 +217,42 @@ fun View.drawNotesIndicator(canvas: Canvas, color: Int, size: Float, notes: Stri
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>

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

@ -399,4 +399,15 @@
<item name="selectable">true</item> <item name="selectable">true</item>
</style> </style>
<style name="CheckmarkPopupBtn">
<item name="android:layout_width">0dp</item>
<item name="android:layout_weight">1</item>
<item name="android:layout_height">match_parent</item>
<item name="android:gravity">center</item>
<item name="android:clickable">true</item>
<item name="android:focusable">true</item>
<item name="android:background">@drawable/ripple_transparent</item>
<item name="android:textSize">@dimen/smallerTextSize</item>
</style>
</resources> </resources>

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

@ -18,6 +18,7 @@
*/ */
package org.isoron.uhabits.core.ui.screens.habits.list package org.isoron.uhabits.core.ui.screens.habits.list
import org.isoron.platform.gui.ScreenLocation
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
import org.isoron.uhabits.core.commands.CreateRepetitionCommand import org.isoron.uhabits.core.commands.CreateRepetitionCommand
@ -50,7 +51,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 +66,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))
} }
@ -171,6 +171,13 @@ open class ListHabitsBehavior @Inject constructor(
frequency: Frequency, frequency: Frequency,
callback: NumberPickerCallback callback: NumberPickerCallback
) )
fun showCheckmarkPopup(
selectedValue: Int,
notes: String,
color: PaletteColor,
location: ScreenLocation,
callback: CheckMarkDialogCallback
)
fun showCheckmarkDialog( fun showCheckmarkDialog(
selectedValue: Int, selectedValue: Int,
notes: String, notes: 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) {
@ -211,5 +222,14 @@ class HistoryCardPresenter(
color: PaletteColor, color: PaletteColor,
callback: ListHabitsBehavior.CheckMarkDialogCallback, callback: ListHabitsBehavior.CheckMarkDialogCallback,
) )
fun showCheckmarkPopup(
selectedValue: Int,
notes: String,
preferences: Preferences,
color: PaletteColor,
location: ScreenLocation,
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"),

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

Loading…
Cancel
Save