diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/EntryButtonViewTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/EntryButtonViewTest.kt index e45569d99..9b807c764 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/EntryButtonViewTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/EntryButtonViewTest.kt @@ -36,6 +36,7 @@ class EntryButtonViewTest : BaseViewTest() { lateinit var view: CheckmarkButtonView var toggled = false + var edited = false @Before override fun setUp() { @@ -44,6 +45,7 @@ class EntryButtonViewTest : BaseViewTest() { value = Entry.NO color = PaletteUtils.getAndroidTestColor(5) onToggle = { _, _, _ -> toggled = true } + onEdit = { _ -> edited = true } } measureView(view, dpToPixels(48), dpToPixels(48)) } @@ -70,20 +72,28 @@ class EntryButtonViewTest : BaseViewTest() { fun testClick_withShortToggleDisabled() { prefs.isShortToggleEnabled = false view.performClick() - assertFalse(toggled) + assertTrue(!toggled and edited) } @Test fun testClick_withShortToggleEnabled() { prefs.isShortToggleEnabled = true view.performClick() - assertTrue(toggled) + assertTrue(toggled and !edited) } @Test - fun testLongClick() { + fun testLongClick_withShortToggleDisabled() { + prefs.isShortToggleEnabled = false + view.performLongClick() + assertTrue(toggled and !edited) + } + + @Test + fun testLongClick_withShortToggleEnabled() { + prefs.isShortToggleEnabled = true view.performLongClick() - assertTrue(toggled) + assertTrue(!toggled and edited) } private fun assertRendersCheckedExplicitly() { diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/NumberPanelViewTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/NumberPanelViewTest.kt index da0a2eb8a..fb816f4d4 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/NumberPanelViewTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/NumberPanelViewTest.kt @@ -76,7 +76,7 @@ class NumberPanelViewTest : BaseViewTest() { @Test fun testEdit() { val timestamps = mutableListOf() - view.onEdit = { timestamps.plusAssign(it) } + view.onEdit = { _, t -> timestamps.plusAssign(t) } view.buttons[0].performLongClick() view.buttons[2].performLongClick() view.buttons[3].performLongClick() @@ -87,7 +87,7 @@ class NumberPanelViewTest : BaseViewTest() { fun testEdit_withOffset() { val timestamps = mutableListOf() view.dataOffset = 3 - view.onEdit = { timestamps += it } + view.onEdit = { _, t -> timestamps += t } view.buttons[0].performLongClick() view.buttons[2].performLongClick() view.buttons[3].performLongClick() diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/CheckmarkPopup.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/CheckmarkPopup.kt new file mode 100644 index 000000000..85d7fdfc1 --- /dev/null +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/CheckmarkPopup.kt @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * 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 . + */ + +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() + } +} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/HistoryEditorDialog.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/HistoryEditorDialog.kt index 21999d4e1..5cfe90ea6 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/HistoryEditorDialog.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/HistoryEditorDialog.kt @@ -43,7 +43,7 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener { private lateinit var commandRunner: CommandRunner private lateinit var habit: Habit private lateinit var preferences: Preferences - private lateinit var dataView: AndroidDataView + lateinit var dataView: AndroidDataView private var chart: HistoryChart? = null private var onDateClickedListener: OnDateClickedListener? = null diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt index 4ec28f9f3..baec44b2f 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt @@ -24,12 +24,16 @@ import android.content.Context import android.content.Intent import androidx.appcompat.app.AppCompatActivity import dagger.Lazy +import org.isoron.platform.gui.ScreenLocation +import org.isoron.platform.gui.toInt import org.isoron.platform.time.LocalDate 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.ConfirmDeleteDialog 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.list.views.HabitCardListAdapter 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.Habit 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.ui.ThemeSwitcher 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.ImportDataTaskFactory import org.isoron.uhabits.utils.copyTo +import org.isoron.uhabits.utils.currentTheme import org.isoron.uhabits.utils.restartWithFade import org.isoron.uhabits.utils.showMessage import org.isoron.uhabits.utils.showSendEmailScreen @@ -93,7 +99,9 @@ class ListHabitsScreen private val colorPickerFactory: ColorPickerDialogFactory, private val numberPickerFactory: NumberPickerFactory, private val checkMarkDialog: CheckmarkDialog, - private val behavior: Lazy + private val behavior: Lazy, + private val preferences: Preferences, + private val rootView: Lazy, ) : CommandRunner.Listener, ListHabitsBehavior.Screen, ListHabitsMenuBehavior.Screen, @@ -237,6 +245,32 @@ class ListHabitsScreen 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( selectedValue: Int, notes: String, diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonView.kt index b50d3ab44..a408f92ee 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonView.kt @@ -28,6 +28,7 @@ import android.text.TextPaint import android.view.HapticFeedbackConstants import android.view.View import android.view.View.MeasureSpec.EXACTLY +import org.isoron.platform.gui.ScreenLocation import org.isoron.uhabits.R import org.isoron.uhabits.core.models.Entry 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.inject.ActivityContext import org.isoron.uhabits.utils.drawNotesIndicator +import org.isoron.uhabits.utils.getCenter import org.isoron.uhabits.utils.getFontAwesome import org.isoron.uhabits.utils.sp import org.isoron.uhabits.utils.sres @@ -81,7 +83,8 @@ class CheckmarkButtonView( var onToggle: (Int, String, Long) -> Unit = { _, _, _ -> } - var onEdit: () -> Unit = {} + var onEdit: (ScreenLocation) -> Unit = { _ -> } + private var drawer = Drawer() init { @@ -102,11 +105,11 @@ class CheckmarkButtonView( override fun onClick(v: View) { if (preferences.isShortToggleEnabled) performToggle(TOGGLE_DELAY_MILLIS) - else onEdit() + else onEdit(getCenter()) } override fun onLongClick(v: View): Boolean { - if (preferences.isShortToggleEnabled) onEdit() + if (preferences.isShortToggleEnabled) onEdit(getCenter()) else performToggle(TOGGLE_DELAY_MILLIS) return true } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelView.kt index 9a44fde10..62fb3e436 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelView.kt @@ -20,6 +20,7 @@ package org.isoron.uhabits.activities.habits.list.views 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.Timestamp import org.isoron.uhabits.core.preferences.Preferences @@ -66,7 +67,7 @@ class CheckmarkPanelView( setupButtons() } - var onEdit: (Timestamp) -> Unit = {} + var onEdit: (ScreenLocation, Timestamp) -> Unit = { _, _ -> } set(value) { field = value setupButtons() @@ -90,7 +91,7 @@ class CheckmarkPanelView( } button.color = color button.onToggle = { value, notes, delay -> onToggle(timestamp, value, notes, delay) } - button.onEdit = { onEdit(timestamp) } + button.onEdit = { location -> onEdit(location, timestamp) } } } } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt index 424aae50b..7296575e7 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt @@ -167,17 +167,17 @@ class HabitCardView( { runPendingToggles(taskId) }.delay(delay) } } - onEdit = { timestamp -> + onEdit = { location, timestamp -> triggerRipple(timestamp) - habit?.let { behavior.onEdit(it, timestamp) } + habit?.let { behavior.onEdit(location, it, timestamp) } } } numberPanel = numberPanelFactory.create().apply { visibility = GONE - onEdit = { timestamp -> + onEdit = { location, timestamp -> triggerRipple(timestamp) - habit?.let { behavior.onEdit(it, timestamp) } + habit?.let { behavior.onEdit(location, it, timestamp) } } } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberPanelView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberPanelView.kt index b9be8c820..dc49b557b 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberPanelView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberPanelView.kt @@ -20,11 +20,13 @@ package org.isoron.uhabits.activities.habits.list.views import android.content.Context +import org.isoron.platform.gui.ScreenLocation import org.isoron.uhabits.core.models.NumericalHabitType import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.utils.DateUtils import org.isoron.uhabits.inject.ActivityContext +import org.isoron.uhabits.utils.getCenter import javax.inject.Inject class NumberPanelViewFactory @@ -78,7 +80,7 @@ class NumberPanelView( setupButtons() } - var onEdit: (Timestamp) -> Unit = {} + var onEdit: (ScreenLocation, Timestamp) -> Unit = { _, _ -> } set(value) { field = value setupButtons() @@ -104,7 +106,7 @@ class NumberPanelView( button.targetType = targetType button.threshold = threshold button.units = units - button.onEdit = { onEdit(timestamp) } + button.onEdit = { onEdit(getCenter(), timestamp) } } } } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitActivity.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitActivity.kt index 4af6948ad..95c6bdbbf 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitActivity.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitActivity.kt @@ -27,6 +27,8 @@ import androidx.appcompat.app.AppCompatActivity import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers 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.uhabits.AndroidDirFinder 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.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.HistoryEditorDialog 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.CommandRunner 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.views.OnDateClickedListener 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.showSendFileScreen import org.isoron.uhabits.widgets.WidgetUpdater @@ -173,7 +179,14 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener { frequency: Frequency, 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( @@ -194,6 +207,37 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener { ).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) { startActivity(IntentFactory().startEditActivity(this@ShowHabitActivity, habit)) } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/utils/ViewExtensions.kt b/uhabits-android/src/main/java/org/isoron/uhabits/utils/ViewExtensions.kt index f406635cd..df140aa79 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/utils/ViewExtensions.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/utils/ViewExtensions.kt @@ -21,6 +21,7 @@ package org.isoron.uhabits.utils import android.app.Activity import android.content.ActivityNotFoundException +import android.content.Context import android.content.Intent import android.graphics.Canvas import android.graphics.Color @@ -32,6 +33,8 @@ import android.view.View import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import android.view.WindowManager +import android.widget.PopupWindow import android.widget.RelativeLayout import android.widget.RelativeLayout.ALIGN_PARENT_BOTTOM import android.widget.RelativeLayout.ALIGN_PARENT_TOP @@ -42,6 +45,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.core.content.FileProvider import com.google.android.material.snackbar.Snackbar +import org.isoron.platform.gui.ScreenLocation import org.isoron.platform.gui.toInt import org.isoron.uhabits.HabitsApplication import org.isoron.uhabits.R @@ -213,3 +217,42 @@ fun View.drawNotesIndicator(canvas: Canvas, color: Int, size: Float, notes: Stri val View.sres: StyledResources 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(), + ) +} diff --git a/uhabits-android/src/main/res/drawable/checkmark_dialog_bg.xml b/uhabits-android/src/main/res/drawable/checkmark_dialog_bg.xml new file mode 100644 index 000000000..cea93a111 --- /dev/null +++ b/uhabits-android/src/main/res/drawable/checkmark_dialog_bg.xml @@ -0,0 +1,27 @@ + + + + + + + \ No newline at end of file diff --git a/uhabits-android/src/main/res/drawable/checkmark_dialog_divider.xml b/uhabits-android/src/main/res/drawable/checkmark_dialog_divider.xml new file mode 100644 index 000000000..ef44b9f19 --- /dev/null +++ b/uhabits-android/src/main/res/drawable/checkmark_dialog_divider.xml @@ -0,0 +1,25 @@ + + + + + + + + \ No newline at end of file diff --git a/uhabits-android/src/main/res/layout/checkmark_popup.xml b/uhabits-android/src/main/res/layout/checkmark_popup.xml new file mode 100644 index 000000000..f1aa569a6 --- /dev/null +++ b/uhabits-android/src/main/res/layout/checkmark_popup.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + diff --git a/uhabits-android/src/main/res/values/styles.xml b/uhabits-android/src/main/res/values/styles.xml index 147d05d24..474d62d16 100644 --- a/uhabits-android/src/main/res/values/styles.xml +++ b/uhabits-android/src/main/res/values/styles.xml @@ -399,4 +399,15 @@ true + + diff --git a/uhabits-core/src/commonMain/kotlin/org/isoron/platform/gui/Canvas.kt b/uhabits-core/src/commonMain/kotlin/org/isoron/platform/gui/Canvas.kt index faab51b87..1459956ce 100644 --- a/uhabits-core/src/commonMain/kotlin/org/isoron/platform/gui/Canvas.kt +++ b/uhabits-core/src/commonMain/kotlin/org/isoron/platform/gui/Canvas.kt @@ -29,6 +29,11 @@ enum class Font { FONT_AWESOME } +data class ScreenLocation( + val x: Double, + val y: Double, +) + interface Canvas { fun setColor(color: Color) fun drawLine(x1: Double, y1: Double, x2: Double, y2: Double) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt index d4aa170cf..14fa256e9 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt @@ -18,6 +18,7 @@ */ package org.isoron.uhabits.core.ui.screens.habits.list +import org.isoron.platform.gui.ScreenLocation import org.isoron.platform.time.LocalDate import org.isoron.uhabits.core.commands.CommandRunner import org.isoron.uhabits.core.commands.CreateRepetitionCommand @@ -50,7 +51,7 @@ open class ListHabitsBehavior @Inject constructor( screen.showHabitScreen(h) } - fun onEdit(habit: Habit, timestamp: Timestamp?) { + fun onEdit(location: ScreenLocation, habit: Habit, timestamp: Timestamp?) { val entry = habit.computedEntries.get(timestamp!!) if (habit.type == HabitType.NUMERICAL) { val oldValue = entry.value.toDouble() @@ -65,12 +66,11 @@ open class ListHabitsBehavior @Inject constructor( commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, value, newNotes)) } } else { - screen.showCheckmarkDialog( + screen.showCheckmarkPopup( entry.value, entry.notes, - timestamp.toLocalDate(), - timestamp.toDialogDateString(), habit.color, + location, ) { newValue, newNotes -> commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, newValue, newNotes)) } @@ -171,6 +171,13 @@ open class ListHabitsBehavior @Inject constructor( frequency: Frequency, callback: NumberPickerCallback ) + fun showCheckmarkPopup( + selectedValue: Int, + notes: String, + color: PaletteColor, + location: ScreenLocation, + callback: CheckMarkDialogCallback + ) fun showCheckmarkDialog( selectedValue: Int, notes: String, diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/HistoryCard.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/HistoryCard.kt index 97d149906..7c932fb42 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/HistoryCard.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/HistoryCard.kt @@ -19,6 +19,7 @@ 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.LocalDate import org.isoron.uhabits.core.commands.CommandRunner @@ -65,55 +66,65 @@ class HistoryCardPresenter( val screen: Screen, ) : OnDateClickedListener { - override fun onDateLongPress(date: LocalDate) { + override fun onDateLongPress(location: ScreenLocation, date: LocalDate) { val timestamp = Timestamp.fromLocalDate(date) screen.showFeedback() if (habit.isNumerical) { showNumberPicker(timestamp) } else { - val entry = habit.computedEntries.get(timestamp) - val nextValue = Entry.nextToggleValue( - value = entry.value, - isSkipEnabled = preferences.isSkipEnabled, - areQuestionMarksEnabled = preferences.areQuestionMarksEnabled - ) + if (preferences.isShortToggleEnabled) showCheckmarkPopup(location, timestamp) + else toggle(timestamp) + } + } + + 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( CreateRepetitionCommand( habitList, habit, timestamp, - nextValue, - entry.notes, + newValue, + newNotes, ), ) } } - override fun onDateShortPress(date: LocalDate) { - val timestamp = Timestamp.fromLocalDate(date) - screen.showFeedback() - if (habit.isNumerical) { - showNumberPicker(timestamp) - } else { - val entry = habit.computedEntries.get(timestamp) - screen.showCheckmarkDialog( - entry.value, + private fun toggle(timestamp: Timestamp) { + val entry = habit.computedEntries.get(timestamp) + val nextValue = Entry.nextToggleValue( + value = entry.value, + isSkipEnabled = preferences.isSkipEnabled, + areQuestionMarksEnabled = preferences.areQuestionMarksEnabled + ) + commandRunner.run( + CreateRepetitionCommand( + habitList, + habit, + timestamp, + nextValue, entry.notes, - timestamp.toLocalDate(), - preferences, - habit.color, - ) { newValue, newNotes -> - commandRunner.run( - CreateRepetitionCommand( - habitList, - habit, - timestamp, - newValue, - newNotes, - ), - ) - } - } + ), + ) } private fun showNumberPicker(timestamp: Timestamp) { @@ -211,5 +222,14 @@ class HistoryCardPresenter( color: PaletteColor, callback: ListHabitsBehavior.CheckMarkDialogCallback, ) + + fun showCheckmarkPopup( + selectedValue: Int, + notes: String, + preferences: Preferences, + color: PaletteColor, + location: ScreenLocation, + callback: ListHabitsBehavior.CheckMarkDialogCallback, + ) } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/views/HistoryChart.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/views/HistoryChart.kt index a752e271c..8d7435540 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/views/HistoryChart.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/views/HistoryChart.kt @@ -22,6 +22,7 @@ package org.isoron.uhabits.core.ui.views import org.isoron.platform.gui.Canvas import org.isoron.platform.gui.Color import org.isoron.platform.gui.DataView +import org.isoron.platform.gui.ScreenLocation import org.isoron.platform.gui.TextAlign import org.isoron.platform.time.DayOfWeek import org.isoron.platform.time.LocalDate @@ -33,8 +34,8 @@ import kotlin.math.min import kotlin.math.round interface OnDateClickedListener { - fun onDateShortPress(date: LocalDate) {} - fun onDateLongPress(date: LocalDate) {} + fun onDateShortPress(location: ScreenLocation, date: LocalDate) {} + fun onDateLongPress(location: ScreenLocation, date: LocalDate) {} } class HistoryChart( @@ -90,10 +91,11 @@ class HistoryChart( if (x - padding < 0 || row == 0 || row > 7 || col == nColumns) return val clickedDate = topLeftDate.plus(offset) if (clickedDate.isNewerThan(today)) return + val location = ScreenLocation(x, y) if (isLongClick) { - onDateClickedListener.onDateLongPress(clickedDate) + onDateClickedListener.onDateLongPress(location, clickedDate) } else { - onDateClickedListener.onDateShortPress(clickedDate) + onDateClickedListener.onDateShortPress(location, clickedDate) } } diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehaviorTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehaviorTest.kt index 02637192a..c147fa524 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehaviorTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehaviorTest.kt @@ -31,6 +31,7 @@ import junit.framework.Assert.assertTrue import org.apache.commons.io.FileUtils import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.core.IsEqual.equalTo +import org.isoron.platform.gui.ScreenLocation import org.isoron.uhabits.core.BaseUnitTest import org.isoron.uhabits.core.models.Entry import org.isoron.uhabits.core.models.Frequency @@ -79,7 +80,7 @@ class ListHabitsBehaviorTest : BaseUnitTest() { @Test fun testOnEdit() { - behavior.onEdit(habit2, getToday()) + behavior.onEdit(ScreenLocation(0.0, 0.0), habit2, getToday()) verify(screen).showNumberPicker( eq(0.1), eq("miles"), diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/views/HistoryChartTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/views/HistoryChartTest.kt index 8a9d523ab..8373805b7 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/views/HistoryChartTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/views/HistoryChartTest.kt @@ -24,6 +24,7 @@ import com.nhaarman.mockitokotlin2.reset import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions import kotlinx.coroutines.runBlocking +import org.isoron.platform.gui.ScreenLocation import org.isoron.platform.gui.assertRenders import org.isoron.platform.time.DayOfWeek import org.isoron.platform.time.DayOfWeek.SUNDAY @@ -73,8 +74,7 @@ class HistoryChartTest { else -> OFF } }, - notesIndicators = MutableList(85) { - index: Int -> + notesIndicators = MutableList(85) { index: Int -> index % 3 == 0 } ) @@ -90,20 +90,32 @@ class HistoryChartTest { // Click top left date 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) 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) // Click date in the middle 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) // Click today 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) // Click header @@ -121,20 +133,32 @@ class HistoryChartTest { // Click top left date 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) 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) // Click date in the middle 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) // Click today 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) // Click header