mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-06 01:08:50 -06:00
Implement NumberPopup
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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.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 -> {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@
|
||||
android:text="" />
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:id="@+id/booleanButtons"
|
||||
android:visibility="gone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:orientation="horizontal"
|
||||
@@ -69,4 +71,34 @@
|
||||
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>
|
||||
@@ -398,4 +398,19 @@
|
||||
<item name="android:textSize">@dimen/smallerTextSize</item>
|
||||
</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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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, "")
|
||||
|
||||
Reference in New Issue
Block a user