Merge branch 'number-popup' into release/2.1.0

pull/1456/head
Alinson S. Xavier 3 years ago
commit 03b02aaa06
No known key found for this signature in database
GPG Key ID: DCA0DAD4D2F58624

@ -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>

@ -25,8 +25,8 @@ 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
import org.isoron.uhabits.core.models.Entry.Companion.NO
import org.isoron.uhabits.core.models.Entry.Companion.SKIP
@ -59,6 +59,7 @@ class CheckmarkPopup(
}
init {
view.booleanButtons.visibility = VISIBLE
initColors()
initTypefaces()
hideDisabledButtons()
@ -98,7 +99,7 @@ class CheckmarkPopup(
view.notes.setText(notes)
}
fun show(location: ScreenLocation) {
fun show() {
val popup = PopupWindow()
popup.contentView = view.root
popup.width = view.root.dp(POPUP_WIDTH).toInt()
@ -116,12 +117,7 @@ class CheckmarkPopup(
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.showAtLocation(anchor, Gravity.CENTER, 0, 0)
popup.dimBehind()
}
}

@ -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,106 @@
/*
* 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.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() {
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.CENTER, 0, 0)
view.value.requestFocusWithKeyboard()
popup.dimBehind()
}
fun save() {
val value = view.value.text.toString().toDoubleOrNull() ?: originalValue
val notes = view.notes.text.toString()
onToggle(value, notes)
}
}

@ -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))
}

@ -28,7 +28,9 @@ 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
@ -45,7 +47,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
@ -229,30 +230,15 @@ fun PopupWindow.dimBehind() {
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 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
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