Implement NumberPopup

pull/1370/head
Alinson S. Xavier 3 years ago
parent f04e37e905
commit d1de3a852b
No known key found for this signature in database
GPG Key ID: DCA0DAD4D2F58624

@ -25,6 +25,7 @@ import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.View.GONE import android.view.View.GONE
import android.view.View.VISIBLE
import android.widget.PopupWindow import android.widget.PopupWindow
import org.isoron.platform.gui.ScreenLocation import org.isoron.platform.gui.ScreenLocation
import org.isoron.uhabits.R import org.isoron.uhabits.R
@ -59,6 +60,7 @@ class CheckmarkPopup(
} }
init { init {
view.booleanButtons.visibility = VISIBLE
initColors() initColors()
initTypefaces() initTypefaces()
hideDisabledButtons() hideDisabledButtons()

@ -0,0 +1,112 @@
/*
* 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.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)
}
}

@ -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.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.NumberPopup
import org.isoron.uhabits.activities.common.dialogs.POPUP_WIDTH 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
@ -242,6 +243,25 @@ class ListHabitsScreen
numberPickerFactory.create(value, unit, notes, dateString, frequency, callback).show() 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( override fun showCheckmarkPopup(
selectedValue: Int, selectedValue: Int,
notes: String, notes: String,
@ -259,15 +279,15 @@ class ListHabitsScreen
value = selectedValue, value = selectedValue,
).apply { ).apply {
onToggle = { value, notes -> callback.onNotesSaved(value, notes) } onToggle = { value, notes -> callback.onNotesSaved(value, notes) }
show( show(getPopupLocation(location))
ScreenLocation(
x = location.x - POPUP_WIDTH / 2,
y = location.y
)
)
} }
} }
private fun getPopupLocation(clickLocation: ScreenLocation) = ScreenLocation(
x = clickLocation.x - POPUP_WIDTH / 2,
y = clickLocation.y
)
private fun getExecuteString(command: Command): String? { private fun getExecuteString(command: Command): String? {
when (command) { when (command) {
is ArchiveHabitsCommand -> { is ArchiveHabitsCommand -> {

@ -28,6 +28,7 @@ import android.text.TextPaint
import android.view.View import android.view.View
import android.view.View.OnClickListener import android.view.View.OnClickListener
import android.view.View.OnLongClickListener import android.view.View.OnLongClickListener
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.NumericalHabitType.AT_LEAST 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.InterfaceUtils.getDimension
import org.isoron.uhabits.utils.dim import org.isoron.uhabits.utils.dim
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.sres import org.isoron.uhabits.utils.sres
import java.text.DecimalFormat import java.text.DecimalFormat
@ -108,7 +110,8 @@ class NumberButtonView(
invalidate() invalidate()
} }
var onEdit: () -> Unit = {} var onEdit: (ScreenLocation) -> Unit = { _ -> }
private var drawer: Drawer = Drawer(context) private var drawer: Drawer = Drawer(context)
init { init {
@ -117,11 +120,11 @@ class NumberButtonView(
} }
override fun onClick(v: View) { override fun onClick(v: View) {
onEdit() onEdit(getCenter())
} }
override fun onLongClick(v: View): Boolean { override fun onLongClick(v: View): Boolean {
onEdit() onEdit(getCenter())
return true return true
} }

@ -26,7 +26,6 @@ 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
@ -106,7 +105,7 @@ class NumberPanelView(
button.targetType = targetType button.targetType = targetType
button.threshold = threshold button.threshold = threshold
button.units = units button.units = units
button.onEdit = { onEdit(getCenter(), timestamp) } button.onEdit = { location -> onEdit(location, timestamp) }
} }
} }
} }

@ -205,7 +205,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
prefs = preferences, prefs = preferences,
notes = notes, notes = notes,
color = view.currentTheme().color(color).toInt(), color = view.currentTheme().color(color).toInt(),
anchor = view, anchor = anchor,
value = selectedValue, value = selectedValue,
).apply { ).apply {
onToggle = { v, n -> callback.onNotesSaved(v, n) } onToggle = { v, n -> callback.onNotesSaved(v, n) }

@ -28,12 +28,15 @@ import android.graphics.Color
import android.graphics.Paint import android.graphics.Paint
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.os.Handler import android.os.Handler
import android.os.SystemClock
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View 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.view.WindowManager
import android.widget.EditText
import android.widget.PopupWindow 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
@ -256,3 +259,16 @@ fun View.getTopLeftCorner(): ScreenLocation {
y = (loc[1] / density).toDouble(), 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)
}

@ -42,6 +42,8 @@
android:text="" /> android:text="" />
<androidx.appcompat.widget.LinearLayoutCompat <androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/booleanButtons"
android:visibility="gone"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="48dp" android:layout_height="48dp"
android:orientation="horizontal" android:orientation="horizontal"
@ -69,4 +71,34 @@
android:text="@string/fa_question" /> android:text="@string/fa_question" />
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/numberButtons"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="48dp"
android:orientation="horizontal"
app:divider="@drawable/checkmark_dialog_divider"
app:showDividers="middle">
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/value"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@color/transparent"
android:textAlignment="center"
android:inputType="numberDecimal"
android:selectAllOnFocus="true"
android:textSize="@dimen/smallTextSize" />
<TextView
android:id="@+id/skipBtnNumber"
style="@style/NumericalPopupBtn"
android:text="@string/skip_day" />
<TextView
android:id="@+id/saveBtn"
style="@style/NumericalPopupBtn"
android:text="@string/save" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>

@ -398,4 +398,19 @@
<item name="android:textSize">@dimen/smallerTextSize</item> <item name="android:textSize">@dimen/smallerTextSize</item>
</style> </style>
<style name="NumericalPopupBtn">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">match_parent</item>
<item name="android:paddingStart">12dp</item>
<item name="android:paddingEnd">12dp</item>
<item name="android:textStyle">bold</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>
<item name="android:textAllCaps">true</item>
</style>
</resources> </resources>

@ -53,14 +53,8 @@ open class ListHabitsBehavior @Inject constructor(
fun onEdit(location: ScreenLocation, 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() / 1000
screen.showNumberPicker( screen.showNumberPopup(oldValue, entry.notes, location) { newValue: Double, newNotes: String ->
oldValue / 1000,
habit.unit,
entry.notes,
timestamp.toDialogDateString(),
habit.frequency
) { newValue: Double, newNotes: String, ->
val value = (newValue * 1000).roundToInt() val value = (newValue * 1000).roundToInt()
commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, value, newNotes)) commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, value, newNotes))
} }
@ -170,6 +164,12 @@ open class ListHabitsBehavior @Inject constructor(
frequency: Frequency, frequency: Frequency,
callback: NumberPickerCallback callback: NumberPickerCallback
) )
fun showNumberPopup(
value: Double,
notes: String,
location: ScreenLocation,
callback: NumberPickerCallback
)
fun showCheckmarkPopup( fun showCheckmarkPopup(
selectedValue: Int, selectedValue: Int,
notes: String, notes: String,

@ -34,7 +34,6 @@ import org.hamcrest.core.IsEqual.equalTo
import org.isoron.platform.gui.ScreenLocation 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.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday
@ -81,12 +80,10 @@ class ListHabitsBehaviorTest : BaseUnitTest() {
@Test @Test
fun testOnEdit() { fun testOnEdit() {
behavior.onEdit(ScreenLocation(0.0, 0.0), habit2, getToday()) behavior.onEdit(ScreenLocation(0.0, 0.0), habit2, getToday())
verify(screen).showNumberPicker( verify(screen).showNumberPopup(
eq(0.1), eq(0.1),
eq("miles"),
eq(""), eq(""),
eq("Jan 25, 2015"), any(),
eq(Frequency.DAILY),
picker.capture() picker.capture()
) )
picker.lastValue.onNumberPicked(100.0, "") picker.lastValue.onNumberPicked(100.0, "")

Loading…
Cancel
Save