From d1de3a852b06d16763f2ac862862de2c74707967 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Thu, 12 May 2022 09:27:06 -0500 Subject: [PATCH] Implement NumberPopup --- .../common/dialogs/CheckmarkPopup.kt | 2 + .../activities/common/dialogs/NumberPopup.kt | 112 ++++++++++++++++++ .../habits/list/ListHabitsScreen.kt | 32 ++++- .../habits/list/views/NumberButtonView.kt | 9 +- .../habits/list/views/NumberPanelView.kt | 3 +- .../habits/show/ShowHabitActivity.kt | 2 +- .../isoron/uhabits/utils/ViewExtensions.kt | 16 +++ .../src/main/res/layout/checkmark_popup.xml | 34 +++++- .../src/main/res/values/styles.xml | 15 +++ .../screens/habits/list/ListHabitsBehavior.kt | 16 +-- .../habits/list/ListHabitsBehaviorTest.kt | 7 +- 11 files changed, 222 insertions(+), 26 deletions(-) create mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/NumberPopup.kt 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 index 85d7fdfc1..bf4033a73 100644 --- 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 @@ -25,6 +25,7 @@ import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.View.GONE +import android.view.View.VISIBLE import android.widget.PopupWindow import org.isoron.platform.gui.ScreenLocation import org.isoron.uhabits.R @@ -59,6 +60,7 @@ class CheckmarkPopup( } init { + view.booleanButtons.visibility = VISIBLE initColors() initTypefaces() hideDisabledButtons() diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/NumberPopup.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/NumberPopup.kt new file mode 100644 index 000000000..6a6a557fa --- /dev/null +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/NumberPopup.kt @@ -0,0 +1,112 @@ +/* + * 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.view.Gravity +import android.view.KeyEvent.KEYCODE_ENTER +import android.view.LayoutInflater +import android.view.MotionEvent.ACTION_DOWN +import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE +import android.widget.PopupWindow +import org.isoron.platform.gui.ScreenLocation +import org.isoron.uhabits.core.models.Entry +import org.isoron.uhabits.core.preferences.Preferences +import org.isoron.uhabits.databinding.CheckmarkPopupBinding +import org.isoron.uhabits.utils.dimBehind +import org.isoron.uhabits.utils.dp +import org.isoron.uhabits.utils.requestFocusWithKeyboard +import java.text.DecimalFormat + +class NumberPopup( + private val context: Context, + private var notes: String, + private var value: Double, + private val prefs: Preferences, + private val anchor: View, +) { + var onToggle: (Double, String) -> Unit = { _, _ -> } + private val originalValue = value + + private val view = CheckmarkPopupBinding.inflate(LayoutInflater.from(context)).apply { + // Required for round corners + container.clipToOutline = true + } + + init { + view.numberButtons.visibility = VISIBLE + hideDisabledButtons() + populate() + } + + private fun hideDisabledButtons() { + if (!prefs.isSkipEnabled) view.skipBtnNumber.visibility = GONE + } + + private fun populate() { + view.notes.setText(notes) + view.value.setText( + when { + value < 0.01 -> "0" + else -> DecimalFormat("#.##").format(value) + } + ) + } + + 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) + popup.setOnDismissListener { + save() + } + view.value.setOnKeyListener { _, keyCode, event -> + if (event.action == ACTION_DOWN && keyCode == KEYCODE_ENTER) { + popup.dismiss() + return@setOnKeyListener true + } + return@setOnKeyListener false + } + view.saveBtn.setOnClickListener { popup.dismiss() } + view.skipBtnNumber.setOnClickListener { + view.value.setText((Entry.SKIP.toDouble() / 1000).toString()) + popup.dismiss() + } + popup.showAtLocation( + anchor, + Gravity.NO_GRAVITY, + view.root.dp(location.x.toFloat()).toInt(), + view.root.dp(location.y.toFloat()).toInt(), + ) + view.value.requestFocusWithKeyboard() + popup.dimBehind() + } + + fun save() { + val value = view.value.text.toString().toDoubleOrNull() ?: originalValue + val notes = view.notes.text.toString() + onToggle(value, notes) + } +} 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 d5794a388..9fda6370b 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 @@ -31,6 +31,7 @@ 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.NumberPopup 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 @@ -242,6 +243,25 @@ class ListHabitsScreen numberPickerFactory.create(value, unit, notes, dateString, frequency, callback).show() } + override fun showNumberPopup( + value: Double, + notes: String, + location: ScreenLocation, + callback: ListHabitsBehavior.NumberPickerCallback + ) { + val view = rootView.get() + NumberPopup( + context = context, + prefs = preferences, + anchor = view, + notes = notes, + value = value, + ).apply { + onToggle = { value, notes -> callback.onNumberPicked(value, notes) } + show(getPopupLocation(location)) + } + } + override fun showCheckmarkPopup( selectedValue: Int, notes: String, @@ -259,15 +279,15 @@ class ListHabitsScreen value = selectedValue, ).apply { onToggle = { value, notes -> callback.onNotesSaved(value, notes) } - show( - ScreenLocation( - x = location.x - POPUP_WIDTH / 2, - y = location.y - ) - ) + show(getPopupLocation(location)) } } + private fun getPopupLocation(clickLocation: ScreenLocation) = ScreenLocation( + x = clickLocation.x - POPUP_WIDTH / 2, + y = clickLocation.y + ) + private fun getExecuteString(command: Command): String? { when (command) { is ArchiveHabitsCommand -> { diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberButtonView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberButtonView.kt index 4d8bdda05..7f476015e 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberButtonView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberButtonView.kt @@ -28,6 +28,7 @@ import android.text.TextPaint import android.view.View import android.view.View.OnClickListener import android.view.View.OnLongClickListener +import org.isoron.platform.gui.ScreenLocation import org.isoron.uhabits.R import org.isoron.uhabits.core.models.Entry import org.isoron.uhabits.core.models.NumericalHabitType.AT_LEAST @@ -37,6 +38,7 @@ import org.isoron.uhabits.inject.ActivityContext import org.isoron.uhabits.utils.InterfaceUtils.getDimension import org.isoron.uhabits.utils.dim import org.isoron.uhabits.utils.drawNotesIndicator +import org.isoron.uhabits.utils.getCenter import org.isoron.uhabits.utils.getFontAwesome import org.isoron.uhabits.utils.sres import java.text.DecimalFormat @@ -108,7 +110,8 @@ class NumberButtonView( invalidate() } - var onEdit: () -> Unit = {} + var onEdit: (ScreenLocation) -> Unit = { _ -> } + private var drawer: Drawer = Drawer(context) init { @@ -117,11 +120,11 @@ class NumberButtonView( } override fun onClick(v: View) { - onEdit() + onEdit(getCenter()) } override fun onLongClick(v: View): Boolean { - onEdit() + onEdit(getCenter()) return true } 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 dc49b557b..c6ce2089f 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 @@ -26,7 +26,6 @@ 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 @@ -106,7 +105,7 @@ class NumberPanelView( button.targetType = targetType button.threshold = threshold button.units = units - button.onEdit = { onEdit(getCenter(), timestamp) } + button.onEdit = { location -> onEdit(location, 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 6eadb1b59..397c6f4ff 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 @@ -205,7 +205,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener { prefs = preferences, notes = notes, color = view.currentTheme().color(color).toInt(), - anchor = view, + anchor = anchor, value = selectedValue, ).apply { onToggle = { v, n -> callback.onNotesSaved(v, n) } 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 df140aa79..0a6cbf16d 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 @@ -28,12 +28,15 @@ import android.graphics.Color import android.graphics.Paint import android.graphics.drawable.ColorDrawable import android.os.Handler +import android.os.SystemClock import android.view.LayoutInflater +import android.view.MotionEvent 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.EditText import android.widget.PopupWindow import android.widget.RelativeLayout import android.widget.RelativeLayout.ALIGN_PARENT_BOTTOM @@ -256,3 +259,16 @@ fun View.getTopLeftCorner(): ScreenLocation { y = (loc[1] / density).toDouble(), ) } + +fun View.requestFocusWithKeyboard() { + // For some reason, Android does not open the soft keyboard by default when view.requestFocus + // is called. Several online solutions suggest using InputMethodManager, but these solutions + // are not reliable; sometimes the keyboard does not show, and sometimes it does not go away + // after focus is lost. Here, we simulate a click on the view, which triggers the keyboard. + // Based on: https://stackoverflow.com/a/7699556 + postDelayed({ + val time = SystemClock.uptimeMillis() + dispatchTouchEvent(MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0f, 0f, 0)) + dispatchTouchEvent(MotionEvent.obtain(time, time, MotionEvent.ACTION_UP, 0f, 0f, 0)) + }, 250) +} diff --git a/uhabits-android/src/main/res/layout/checkmark_popup.xml b/uhabits-android/src/main/res/layout/checkmark_popup.xml index f1aa569a6..72dc79786 100644 --- a/uhabits-android/src/main/res/layout/checkmark_popup.xml +++ b/uhabits-android/src/main/res/layout/checkmark_popup.xml @@ -42,6 +42,8 @@ android:text="" /> - + + + + + + + + + \ No newline at end of file diff --git a/uhabits-android/src/main/res/values/styles.xml b/uhabits-android/src/main/res/values/styles.xml index f25602748..8a016d96d 100644 --- a/uhabits-android/src/main/res/values/styles.xml +++ b/uhabits-android/src/main/res/values/styles.xml @@ -398,4 +398,19 @@ @dimen/smallerTextSize + + 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 b14ecc591..5a8fe1459 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 @@ -53,14 +53,8 @@ open class ListHabitsBehavior @Inject constructor( fun onEdit(location: ScreenLocation, habit: Habit, timestamp: Timestamp?) { val entry = habit.computedEntries.get(timestamp!!) if (habit.type == HabitType.NUMERICAL) { - val oldValue = entry.value.toDouble() - screen.showNumberPicker( - oldValue / 1000, - habit.unit, - entry.notes, - timestamp.toDialogDateString(), - habit.frequency - ) { newValue: Double, newNotes: String, -> + val oldValue = entry.value.toDouble() / 1000 + screen.showNumberPopup(oldValue, entry.notes, location) { newValue: Double, newNotes: String -> val value = (newValue * 1000).roundToInt() commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, value, newNotes)) } @@ -170,6 +164,12 @@ open class ListHabitsBehavior @Inject constructor( frequency: Frequency, callback: NumberPickerCallback ) + fun showNumberPopup( + value: Double, + notes: String, + location: ScreenLocation, + callback: NumberPickerCallback + ) fun showCheckmarkPopup( selectedValue: Int, notes: String, 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 c147fa524..97778338a 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 @@ -34,7 +34,6 @@ 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 import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday @@ -81,12 +80,10 @@ class ListHabitsBehaviorTest : BaseUnitTest() { @Test fun testOnEdit() { behavior.onEdit(ScreenLocation(0.0, 0.0), habit2, getToday()) - verify(screen).showNumberPicker( + verify(screen).showNumberPopup( eq(0.1), - eq("miles"), eq(""), - eq("Jan 25, 2015"), - eq(Frequency.DAILY), + any(), picker.capture() ) picker.lastValue.onNumberPicked(100.0, "")