Merge pull request #1370 from iSoron/number-popup

Replace NumberPickerDialog by NumberPopup
pull/1441/head
Alinson S. Xavier 3 years ago committed by GitHub
commit 459cf02642
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -45,7 +45,7 @@ class EntryButtonViewTest : BaseViewTest() {
value = Entry.NO
color = PaletteUtils.getAndroidTestColor(5)
onToggle = { _, _, _ -> toggled = true }
onEdit = { _ -> edited = true }
onEdit = { edited = true }
}
measureView(view, dpToPixels(48), dpToPixels(48))
}

@ -76,7 +76,7 @@ class NumberPanelViewTest : BaseViewTest() {
@Test
fun testEdit() {
val timestamps = mutableListOf<Timestamp>()
view.onEdit = { _, t -> timestamps.plusAssign(t) }
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<Timestamp>()
view.dataOffset = 3
view.onEdit = { _, t -> timestamps += t }
view.onEdit = { t -> timestamps += t }
view.buttons[0].performLongClick()
view.buttons[2].performLongClick()
view.buttons[3].performLongClick()

@ -125,7 +125,7 @@
android:exported="true"
android:label="NumericalCheckmarkWidget"
android:noHistory="true"
android:theme="@style/Theme.AppCompat.Light.Dialog">
android:theme="@style/Theme.Transparent">
<intent-filter>
<action android:name="org.isoron.uhabits.ACTION_SHOW_NUMERICAL_VALUE_ACTIVITY" />
</intent-filter>

@ -19,14 +19,12 @@
package org.isoron.uhabits.activities.common.dialogs
import android.app.Dialog
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 android.view.View.VISIBLE
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Entry.Companion.NO
import org.isoron.uhabits.core.models.Entry.Companion.SKIP
@ -52,6 +50,7 @@ class CheckmarkPopup(
private val anchor: View,
) {
var onToggle: (Int, String) -> Unit = { _, _ -> }
private lateinit var dialog: Dialog
private val view = CheckmarkPopupBinding.inflate(LayoutInflater.from(context)).apply {
// Required for round corners
@ -59,6 +58,7 @@ class CheckmarkPopup(
}
init {
view.booleanButtons.visibility = VISIBLE
initColors()
initTypefaces()
hideDisabledButtons()
@ -94,34 +94,34 @@ class CheckmarkPopup(
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 show() {
dialog = Dialog(context, android.R.style.Theme_NoTitleBar)
dialog.setContentView(view.root)
dialog.window?.apply {
setLayout(
view.root.dp(POPUP_WIDTH).toInt(),
view.root.dp(POPUP_HEIGHT).toInt()
)
setBackgroundDrawableResource(android.R.color.transparent)
}
fun onClick(v: Int) {
this.value = v
popup.dismiss()
save()
}
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()
dialog.setCanceledOnTouchOutside(true)
dialog.dimBehind()
dialog.show()
}
fun save() {
onToggle(value, view.notes.text.toString().trim())
dialog.dismiss()
}
}

@ -1,205 +0,0 @@
/*
* 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.annotation.SuppressLint
import android.content.Context
import android.content.DialogInterface
import android.content.DialogInterface.BUTTON_NEGATIVE
import android.text.InputFilter
import android.text.Spanned
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.NumberPicker
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.core.models.Frequency.Companion.DAILY
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.utils.InterfaceUtils
import java.text.DecimalFormatSymbols
import javax.inject.Inject
import kotlin.math.roundToLong
class NumberPickerFactory
@Inject constructor(
@ActivityContext private val context: Context
) {
@SuppressLint("SetTextI18n")
fun create(
value: Double,
unit: String,
notes: String,
dateString: String,
frequency: Frequency,
callback: ListHabitsBehavior.NumberPickerCallback
): AlertDialog {
clearCurrentDialog()
val inflater = LayoutInflater.from(context)
val view = inflater.inflate(R.layout.number_picker_dialog, null)
val picker = view.findViewById<NumberPicker>(R.id.picker)
val picker2 = view.findViewById<NumberPicker>(R.id.picker2)
val etNotes = view.findViewById<EditText>(R.id.etNotes)
// Install filter to intercept decimal separator before it is parsed
val watcherFilter: InputFilter = SeparatorWatcherInputFilter(picker2)
val pickerInputText = getNumberPickerInputText(picker)
pickerInputText.filters = arrayOf(watcherFilter).plus(pickerInputText.filters)
// Install custom focus listener to replace "5" by "50" instead of "05"
val picker2InputText = getNumberPickerInputText(picker2)
val prevFocusChangeListener = picker2InputText.onFocusChangeListener
picker2InputText.onFocusChangeListener = View.OnFocusChangeListener { v, hasFocus ->
val str = picker2InputText.text.toString()
if (str.length == 1) picker2InputText.setText("${str}0")
prevFocusChangeListener.onFocusChange(v, hasFocus)
}
view.findViewById<TextView>(R.id.tvUnit).text = unit
view.findViewById<TextView>(R.id.tvSeparator).text =
DecimalFormatSymbols.getInstance().decimalSeparator.toString()
val intValue = (value * 100).roundToLong().toInt()
picker.minValue = 0
picker.maxValue = Integer.MAX_VALUE / 100
picker.value = intValue / 100
picker.wrapSelectorWheel = false
picker2.minValue = 0
picker2.maxValue = 99
picker2.setFormatter { v -> String.format("%02d", v) }
picker2.value = intValue % 100
etNotes.setText(notes)
val dialogBuilder = AlertDialog.Builder(context)
.setView(view)
.setTitle(dateString)
.setPositiveButton(R.string.save) { _, _ ->
picker.clearFocus()
picker2.clearFocus()
val v = picker.value + 0.01 * picker2.value
val note = etNotes.text.toString().trim()
callback.onNumberPicked(v, note)
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
callback.onNumberPickerDismissed()
}
.setOnDismissListener {
callback.onNumberPickerDismissed()
currentDialog = null
}
if (frequency == DAILY) {
dialogBuilder.setNegativeButton(R.string.skip_day) { _, _ ->
picker.clearFocus()
val v = Entry.SKIP.toDouble() / 1000
val note = etNotes.text.toString()
callback.onNumberPicked(v, note)
}
}
val dialog = dialogBuilder.create()
dialog.setOnShowListener {
val preferences =
(context.applicationContext as HabitsApplication).component.preferences
if (!preferences.isSkipEnabled) {
dialog.getButton(BUTTON_NEGATIVE).visibility = View.GONE
}
showSoftInput(dialog, pickerInputText)
}
InterfaceUtils.setupEditorAction(
picker
) { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick()
}
false
}
InterfaceUtils.setupEditorAction(
picker2
) { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick()
}
false
}
currentDialog = dialog
return dialog
}
@SuppressLint("DiscouragedPrivateApi")
private fun getNumberPickerInputText(picker: NumberPicker): EditText {
val f = NumberPicker::class.java.getDeclaredField("mInputText")
f.isAccessible = true
return f.get(picker) as EditText
}
private fun showSoftInput(dialog: AlertDialog, v: View) {
dialog.window?.setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_VISIBLE)
v.requestFocus()
val inputMethodManager = context.getSystemService(InputMethodManager::class.java)
inputMethodManager?.showSoftInput(v, 0)
}
companion object {
private var currentDialog: AlertDialog? = null
fun clearCurrentDialog() {
currentDialog?.dismiss()
currentDialog = null
}
}
}
class SeparatorWatcherInputFilter(private val nextPicker: NumberPicker) : InputFilter {
override fun filter(
source: CharSequence?,
start: Int,
end: Int,
dest: Spanned?,
dstart: Int,
dend: Int
): CharSequence {
if (source == null || source.isEmpty()) {
return ""
}
for (c in source) {
if (c == DecimalFormatSymbols.getInstance().decimalSeparator || c == '.' || c == ',') {
nextPicker.performLongClick()
break
}
}
return source
}
}

@ -0,0 +1,111 @@
/*
* 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.app.Dialog
import android.content.Context
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 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 lateinit var dialog: Dialog
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() {
dialog = Dialog(context, android.R.style.Theme_NoTitleBar)
dialog.setContentView(view.root)
dialog.window?.apply {
setLayout(
view.root.dp(POPUP_WIDTH).toInt(),
view.root.dp(POPUP_HEIGHT).toInt()
)
setBackgroundDrawableResource(android.R.color.transparent)
}
view.value.setOnKeyListener { _, keyCode, event ->
if (event.action == ACTION_DOWN && keyCode == KEYCODE_ENTER) {
save()
return@setOnKeyListener true
}
return@setOnKeyListener false
}
view.saveBtn.setOnClickListener {
save()
}
view.skipBtnNumber.setOnClickListener {
view.value.setText((Entry.SKIP.toDouble() / 1000).toString())
save()
}
view.value.requestFocusWithKeyboard()
dialog.setCanceledOnTouchOutside(true)
dialog.dimBehind()
dialog.show()
}
fun save() {
val value = view.value.text.toString().toDoubleOrNull() ?: originalValue
val notes = view.notes.text.toString()
onToggle(value, notes)
dialog.dismiss()
}
}

@ -24,14 +24,12 @@ 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.uhabits.R
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.common.dialogs.NumberPopup
import org.isoron.uhabits.activities.habits.edit.HabitTypeDialog
import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
import org.isoron.uhabits.core.commands.ArchiveHabitsCommand
@ -42,7 +40,6 @@ import org.isoron.uhabits.core.commands.CreateHabitCommand
import org.isoron.uhabits.core.commands.DeleteHabitsCommand
import org.isoron.uhabits.core.commands.EditHabitCommand
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
@ -95,7 +92,6 @@ class ListHabitsScreen
private val exportDBFactory: ExportDBTaskFactory,
private val importTaskFactory: ImportDataTaskFactory,
private val colorPickerFactory: ColorPickerDialogFactory,
private val numberPickerFactory: NumberPickerFactory,
private val behavior: Lazy<ListHabitsBehavior>,
private val preferences: Preferences,
private val rootView: Lazy<ListHabitsRootView>,
@ -231,22 +227,28 @@ class ListHabitsScreen
picker.show(activity.supportFragmentManager, "picker")
}
override fun showNumberPicker(
override fun showNumberPopup(
value: Double,
unit: String,
notes: String,
dateString: String,
frequency: Frequency,
callback: ListHabitsBehavior.NumberPickerCallback
) {
numberPickerFactory.create(value, unit, notes, dateString, frequency, callback).show()
val view = rootView.get()
NumberPopup(
context = context,
prefs = preferences,
anchor = view,
notes = notes,
value = value,
).apply {
onToggle = { value, notes -> callback.onNumberPicked(value, notes) }
show()
}
}
override fun showCheckmarkPopup(
selectedValue: Int,
notes: String,
color: PaletteColor,
location: ScreenLocation,
callback: ListHabitsBehavior.CheckMarkDialogCallback
) {
val view = rootView.get()
@ -259,12 +261,7 @@ class ListHabitsScreen
value = selectedValue,
).apply {
onToggle = { value, notes -> callback.onNotesSaved(value, notes) }
show(
ScreenLocation(
x = location.x - POPUP_WIDTH / 2,
y = location.y
)
)
show()
}
}

@ -28,7 +28,6 @@ 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
@ -39,7 +38,6 @@ 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
@ -83,7 +81,7 @@ class CheckmarkButtonView(
var onToggle: (Int, String, Long) -> Unit = { _, _, _ -> }
var onEdit: (ScreenLocation) -> Unit = { _ -> }
var onEdit: () -> Unit = { }
private var drawer = Drawer()
@ -105,11 +103,11 @@ class CheckmarkButtonView(
override fun onClick(v: View) {
if (preferences.isShortToggleEnabled) performToggle(TOGGLE_DELAY_MILLIS)
else onEdit(getCenter())
else onEdit()
}
override fun onLongClick(v: View): Boolean {
if (preferences.isShortToggleEnabled) onEdit(getCenter())
if (preferences.isShortToggleEnabled) onEdit()
else performToggle(TOGGLE_DELAY_MILLIS)
return true
}

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

@ -167,17 +167,17 @@ class HabitCardView(
{ runPendingToggles(taskId) }.delay(delay)
}
}
onEdit = { location, timestamp ->
onEdit = { timestamp ->
triggerRipple(timestamp)
habit?.let { behavior.onEdit(location, it, timestamp) }
habit?.let { behavior.onEdit(it, timestamp) }
}
}
numberPanel = numberPanelFactory.create().apply {
visibility = GONE
onEdit = { location, timestamp ->
onEdit = { timestamp ->
triggerRipple(timestamp)
habit?.let { behavior.onEdit(location, it, timestamp) }
habit?.let { behavior.onEdit(it, timestamp) }
}
}

@ -108,7 +108,8 @@ class NumberButtonView(
invalidate()
}
var onEdit: () -> Unit = {}
var onEdit: () -> Unit = { }
private var drawer: Drawer = Drawer(context)
init {

@ -20,13 +20,11 @@
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
@ -80,7 +78,7 @@ class NumberPanelView(
setupButtons()
}
var onEdit: (ScreenLocation, Timestamp) -> Unit = { _, _ -> }
var onEdit: (Timestamp) -> Unit = { _ -> }
set(value) {
field = value
setupButtons()
@ -106,7 +104,7 @@ class NumberPanelView(
button.targetType = targetType
button.threshold = threshold
button.units = units
button.onEdit = { onEdit(getCenter(), timestamp) }
button.onEdit = { onEdit(timestamp) }
}
}
}

@ -23,11 +23,11 @@ import android.os.Bundle
import android.view.HapticFeedbackConstants
import android.view.Menu
import android.view.MenuItem
import android.view.View
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.uhabits.AndroidDirFinder
import org.isoron.uhabits.HabitsApplication
@ -37,11 +37,9 @@ import org.isoron.uhabits.activities.HabitsDirFinder
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.activities.common.dialogs.NumberPopup
import org.isoron.uhabits.core.commands.Command
import org.isoron.uhabits.core.commands.CommandRunner
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
@ -52,7 +50,6 @@ 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
@ -169,22 +166,23 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
window.decorView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
}
override fun showNumberPicker(
override fun showNumberPopup(
value: Double,
unit: String,
notes: String,
dateString: String,
frequency: Frequency,
preferences: Preferences,
callback: ListHabitsBehavior.NumberPickerCallback
) {
NumberPickerFactory(this@ShowHabitActivity).create(
value,
unit,
notes,
dateString,
frequency,
callback
).show()
val anchor = getPopupAnchor() ?: return
NumberPopup(
context = this@ShowHabitActivity,
prefs = preferences,
notes = notes,
anchor = anchor,
value = value,
).apply {
onToggle = { v, n -> callback.onNumberPicked(v, n) }
show()
}
}
override fun showCheckmarkPopup(
@ -192,32 +190,27 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
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()
val anchor = getPopupAnchor() ?: return
CheckmarkPopup(
context = this@ShowHabitActivity,
prefs = preferences,
notes = notes,
color = view.currentTheme().color(color).toInt(),
anchor = view,
anchor = anchor,
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,
)
)
show()
}
}
private fun getPopupAnchor(): View? {
val dialog = supportFragmentManager.findFragmentByTag("historyEditor") as HistoryEditorDialog?
return dialog?.dataView
}
override fun showEditHabitScreen(habit: Habit) {
startActivity(IntentFactory().startEditActivity(this@ShowHabitActivity, habit))
}

@ -20,21 +20,22 @@
package org.isoron.uhabits.utils
import android.app.Activity
import android.app.Dialog
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.graphics.Canvas
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.PopupWindow
import android.widget.RelativeLayout
import android.widget.RelativeLayout.ALIGN_PARENT_BOTTOM
import android.widget.RelativeLayout.ALIGN_PARENT_TOP
@ -45,7 +46,6 @@ 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
@ -218,41 +218,20 @@ 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(),
)
fun Dialog.dimBehind() {
window?.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
window?.setDimAmount(0.5f)
}
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)
}

@ -22,11 +22,12 @@ package org.isoron.uhabits.widgets.activities
import android.app.Activity
import android.content.Context
import android.os.Bundle
import android.view.Window
import android.view.View
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
import org.isoron.uhabits.activities.common.dialogs.NumberPopup
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
import org.isoron.uhabits.core.ui.widgets.WidgetBehavior
import org.isoron.uhabits.core.utils.DateUtils
@ -39,11 +40,13 @@ class NumericalCheckmarkWidgetActivity : Activity(), ListHabitsBehavior.NumberPi
private lateinit var behavior: WidgetBehavior
private lateinit var data: IntentParser.CheckmarkIntentData
private lateinit var widgetUpdater: WidgetUpdater
private lateinit var rootView: View
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
setContentView(FrameLayout(this))
rootView = FrameLayout(this)
rootView.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
setContentView(rootView)
val app = this.applicationContext as HabitsApplication
val component = app.component
val parser = app.component.intentParser
@ -55,8 +58,9 @@ class NumericalCheckmarkWidgetActivity : Activity(), ListHabitsBehavior.NumberPi
component.preferences
)
widgetUpdater = component.widgetUpdater
showNumberSelector(this)
rootView.post {
showNumberSelector(this)
}
SystemUtils.unlockScreen(this)
}
@ -73,17 +77,22 @@ class NumericalCheckmarkWidgetActivity : Activity(), ListHabitsBehavior.NumberPi
private fun showNumberSelector(context: Context) {
val app = this.applicationContext as HabitsApplication
AndroidThemeSwitcher(this, app.component.preferences).apply()
val numberPickerFactory = NumberPickerFactory(context)
val today = DateUtils.getTodayWithOffset()
val entry = data.habit.computedEntries.get(today)
numberPickerFactory.create(
entry.value / 1000.0,
data.habit.unit,
entry.notes,
today.toDialogDateString(),
data.habit.frequency,
this
).show()
NumberPopup(
context = context,
prefs = app.component.preferences,
anchor = rootView,
notes = entry.notes,
value = entry.value / 1000.0,
).apply {
onToggle = { value, notes ->
onNumberPicked(value, notes)
finish()
overridePendingTransition(0, 0)
}
show()
}
}
companion object {

@ -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
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,28 @@
<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>
<style name="Theme.Transparent" parent="android:Theme">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:backgroundDimEnabled">false</item>
<item name="android:windowAnimationStyle">@null</item>
</style>
</resources>

@ -18,10 +18,8 @@
*/
package org.isoron.uhabits.core.ui.screens.habits.list
import org.isoron.platform.gui.ScreenLocation
import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.commands.CreateRepetitionCommand
import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.HabitType
@ -50,17 +48,11 @@ open class ListHabitsBehavior @Inject constructor(
screen.showHabitScreen(h)
}
fun onEdit(location: ScreenLocation, habit: Habit, timestamp: Timestamp?) {
fun onEdit(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) { newValue: Double, newNotes: String ->
val value = (newValue * 1000).roundToInt()
commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, value, newNotes))
}
@ -69,7 +61,6 @@ open class ListHabitsBehavior @Inject constructor(
entry.value,
entry.notes,
habit.color,
location,
) { newValue, newNotes ->
commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, newValue, newNotes))
}
@ -162,19 +153,15 @@ open class ListHabitsBehavior @Inject constructor(
fun showHabitScreen(h: Habit)
fun showIntroScreen()
fun showMessage(m: Message)
fun showNumberPicker(
fun showNumberPopup(
value: Double,
unit: String,
notes: String,
dateString: String,
frequency: Frequency,
callback: NumberPickerCallback
)
fun showCheckmarkPopup(
selectedValue: Int,
notes: String,
color: PaletteColor,
location: ScreenLocation,
callback: CheckMarkDialogCallback
)
fun showSendBugReportToDeveloperScreen(log: String)

@ -19,7 +19,6 @@
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
@ -28,7 +27,6 @@ import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.Entry.Companion.SKIP
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.models.Frequency
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.NumericalHabitType.AT_LEAST
@ -66,36 +64,35 @@ class HistoryCardPresenter(
val screen: Screen,
) : OnDateClickedListener {
override fun onDateLongPress(location: ScreenLocation, date: LocalDate) {
override fun onDateLongPress(date: LocalDate) {
val timestamp = Timestamp.fromLocalDate(date)
screen.showFeedback()
if (habit.isNumerical) {
showNumberPicker(timestamp)
showNumberPopup(timestamp)
} else {
if (preferences.isShortToggleEnabled) showCheckmarkPopup(location, timestamp)
if (preferences.isShortToggleEnabled) showCheckmarkPopup(timestamp)
else toggle(timestamp)
}
}
override fun onDateShortPress(location: ScreenLocation, date: LocalDate) {
override fun onDateShortPress(date: LocalDate) {
val timestamp = Timestamp.fromLocalDate(date)
screen.showFeedback()
if (habit.isNumerical) {
showNumberPicker(timestamp)
showNumberPopup(timestamp)
} else {
if (preferences.isShortToggleEnabled) toggle(timestamp)
else showCheckmarkPopup(location, timestamp)
else showCheckmarkPopup(timestamp)
}
}
private fun showCheckmarkPopup(location: ScreenLocation, timestamp: Timestamp) {
private fun showCheckmarkPopup(timestamp: Timestamp) {
val entry = habit.computedEntries.get(timestamp)
screen.showCheckmarkPopup(
entry.value,
entry.notes,
preferences,
habit.color,
location,
) { newValue, newNotes ->
commandRunner.run(
CreateRepetitionCommand(
@ -127,15 +124,13 @@ class HistoryCardPresenter(
)
}
private fun showNumberPicker(timestamp: Timestamp) {
private fun showNumberPopup(timestamp: Timestamp) {
val entry = habit.computedEntries.get(timestamp)
val oldValue = entry.value
screen.showNumberPicker(
oldValue / 1000.0,
habit.unit,
entry.notes,
timestamp.toDialogDateString(),
frequency = habit.frequency
screen.showNumberPopup(
value = oldValue / 1000.0,
notes = entry.notes,
preferences = preferences,
) { newValue: Double, newNotes: String ->
val thousands = (newValue * 1000).roundToInt()
commandRunner.run(
@ -205,21 +200,17 @@ class HistoryCardPresenter(
interface Screen {
fun showHistoryEditorDialog(listener: OnDateClickedListener)
fun showFeedback()
fun showNumberPicker(
fun showNumberPopup(
value: Double,
unit: String,
notes: String,
dateString: String,
frequency: Frequency,
callback: ListHabitsBehavior.NumberPickerCallback
preferences: Preferences,
callback: ListHabitsBehavior.NumberPickerCallback,
)
fun showCheckmarkPopup(
selectedValue: Int,
notes: String,
preferences: Preferences,
color: PaletteColor,
location: ScreenLocation,
callback: ListHabitsBehavior.CheckMarkDialogCallback,
)
}

@ -22,7 +22,6 @@ 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
@ -34,8 +33,8 @@ import kotlin.math.min
import kotlin.math.round
interface OnDateClickedListener {
fun onDateShortPress(location: ScreenLocation, date: LocalDate) {}
fun onDateLongPress(location: ScreenLocation, date: LocalDate) {}
fun onDateShortPress(date: LocalDate) {}
fun onDateLongPress(date: LocalDate) {}
}
class HistoryChart(
@ -91,11 +90,10 @@ 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(location, clickedDate)
onDateClickedListener.onDateLongPress(clickedDate)
} else {
onDateClickedListener.onDateShortPress(location, clickedDate)
onDateClickedListener.onDateShortPress(clickedDate)
}
}

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

@ -24,7 +24,6 @@ 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
@ -90,32 +89,20 @@ class HistoryChartTest {
// Click top left date
view.onClick(20.0, 46.0)
verify(dateClickedListener).onDateShortPress(
ScreenLocation(20.0, 46.0),
LocalDate(2014, 10, 26)
)
verify(dateClickedListener).onDateShortPress(LocalDate(2014, 10, 26))
reset(dateClickedListener)
view.onClick(2.0, 28.0)
verify(dateClickedListener).onDateShortPress(
ScreenLocation(2.0, 28.0),
LocalDate(2014, 10, 26)
)
verify(dateClickedListener).onDateShortPress(LocalDate(2014, 10, 26))
reset(dateClickedListener)
// Click date in the middle
view.onClick(163.0, 113.0)
verify(dateClickedListener).onDateShortPress(
ScreenLocation(163.0, 113.0),
LocalDate(2014, 12, 10)
)
verify(dateClickedListener).onDateShortPress(LocalDate(2014, 12, 10))
reset(dateClickedListener)
// Click today
view.onClick(336.0, 37.0)
verify(dateClickedListener).onDateShortPress(
ScreenLocation(336.0, 37.0),
LocalDate(2015, 1, 25)
)
verify(dateClickedListener).onDateShortPress(LocalDate(2015, 1, 25))
reset(dateClickedListener)
// Click header
@ -133,32 +120,20 @@ class HistoryChartTest {
// Click top left date
view.onLongClick(20.0, 46.0)
verify(dateClickedListener).onDateLongPress(
ScreenLocation(20.0, 46.0),
LocalDate(2014, 10, 26)
)
verify(dateClickedListener).onDateLongPress(LocalDate(2014, 10, 26))
reset(dateClickedListener)
view.onLongClick(2.0, 28.0)
verify(dateClickedListener).onDateLongPress(
ScreenLocation(2.0, 28.0),
LocalDate(2014, 10, 26)
)
verify(dateClickedListener).onDateLongPress(LocalDate(2014, 10, 26))
reset(dateClickedListener)
// Click date in the middle
view.onLongClick(163.0, 113.0)
verify(dateClickedListener).onDateLongPress(
ScreenLocation(163.0, 113.0),
LocalDate(2014, 12, 10)
)
verify(dateClickedListener).onDateLongPress(LocalDate(2014, 12, 10))
reset(dateClickedListener)
// Click today
view.onLongClick(336.0, 37.0)
verify(dateClickedListener).onDateLongPress(
ScreenLocation(336.0, 37.0),
LocalDate(2015, 1, 25)
)
verify(dateClickedListener).onDateLongPress(LocalDate(2015, 1, 25))
reset(dateClickedListener)
// Click header

Loading…
Cancel
Save