HistoryChart: Fix HistoryEditorDialog

This commit is contained in:
2021-01-01 23:20:13 -06:00
parent e2d2b5b4b3
commit 162eac3bdf
24 changed files with 299 additions and 257 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -51,6 +51,7 @@ interface Canvas {
fun fillCircle(centerX: Double, centerY: Double, radius: Double)
fun setTextAlign(align: TextAlign)
fun toImage(): Image
fun measureText(test: String): Double
/**
* Fills entire canvas with the current color.

View File

@@ -36,13 +36,18 @@ import kotlin.math.roundToInt
class JavaCanvas(
val image: BufferedImage,
val pixelScale: Double = 2.0
val pixelScale: Double = 2.0,
) : Canvas {
override fun toImage(): Image {
return JavaImage(image)
}
override fun measureText(text: String): Double {
val metrics = g2d.getFontMetrics(g2d.font)
return toDp(metrics.stringWidth(text))
}
private val frc = FontRenderContext(null, true, true)
private var fontSize = 12.0
private var font = Font.REGULAR
@@ -121,7 +126,7 @@ class JavaCanvas(
y: Double,
width: Double,
height: Double,
cornerRadius: Double
cornerRadius: Double,
) {
g2d.fill(
RoundRectangle2D.Double(
@@ -184,7 +189,7 @@ class JavaCanvas(
centerY: Double,
radius: Double,
startAngle: Double,
swipeAngle: Double
swipeAngle: Double,
) {
g2d.fillArc(

View File

@@ -21,6 +21,8 @@ package org.isoron.platform.gui
interface View {
fun draw(canvas: Canvas)
fun onClick(x: Double, y: Double) {
}
}
interface DataView : View {

View File

@@ -128,9 +128,14 @@ data class LocalDate(val daysSince2000: Int) {
fun distanceTo(other: LocalDate): Int {
return abs(daysSince2000 - other.daysSince2000)
}
override fun toString(): String {
return "LocalDate($year-$month-$day)"
}
}
interface LocalDateFormatter {
fun shortWeekdayName(weekday: DayOfWeek): String
fun shortWeekdayName(date: LocalDate): String
fun shortMonthName(date: LocalDate): String
}

View File

@@ -56,6 +56,12 @@ class JavaLocalDateFormatter(private val locale: Locale) : LocalDateFormatter {
return if (longName.length <= 3) longName else shortName
}
override fun shortWeekdayName(weekday: DayOfWeek): String {
val cal = GregorianCalendar()
cal.set(DAY_OF_WEEK, weekday.daysSinceSunday - 1)
return shortWeekdayName(LocalDate(cal.get(YEAR), cal.get(MONTH) + 1, cal.get(DAY_OF_MONTH)))
}
override fun shortWeekdayName(date: LocalDate): String {
val cal = date.toGregorianCalendar()
return cal.getDisplayName(DAY_OF_WEEK, SHORT, locale)

View File

@@ -18,14 +18,15 @@
*/
package org.isoron.uhabits.core.ui.screens.habits.show
import org.isoron.platform.time.LocalDate
import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.commands.CreateRepetitionCommand
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.ui.callbacks.OnToggleCheckmarkListener
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
import org.isoron.uhabits.core.ui.views.OnDateClickedListener
import kotlin.math.roundToInt
class ShowHabitBehavior(
@@ -34,7 +35,7 @@ class ShowHabitBehavior(
private val habit: Habit,
private val screen: Screen,
private val preferences: Preferences,
) : OnToggleCheckmarkListener {
) : OnDateClickedListener {
fun onScoreCardSpinnerPosition(position: Int) {
preferences.scoreCardSpinnerPosition = position
@@ -58,7 +59,9 @@ class ShowHabitBehavior(
screen.showHistoryEditorDialog(this)
}
override fun onToggleEntry(timestamp: Timestamp, value: Int) {
override fun onDateClicked(date: LocalDate) {
val timestamp = date.timestamp
screen.touchFeedback()
if (habit.isNumerical) {
val entries = habit.computedEntries
val oldValue = entries.get(timestamp).value
@@ -74,12 +77,18 @@ class ShowHabitBehavior(
)
}
} else {
val currentValue = habit.computedEntries.get(timestamp).value
val nextValue = if (preferences.isSkipEnabled) {
Entry.nextToggleValueWithSkip(currentValue)
} else {
Entry.nextToggleValueWithoutSkip(currentValue)
}
commandRunner.run(
CreateRepetitionCommand(
habitList,
habit,
timestamp,
value,
nextValue,
),
)
}
@@ -89,11 +98,12 @@ class ShowHabitBehavior(
fun showNumberPicker(
value: Double,
unit: String,
callback: ListHabitsBehavior.NumberPickerCallback
callback: ListHabitsBehavior.NumberPickerCallback,
)
fun updateWidgets()
fun refresh()
fun showHistoryEditorDialog(listener: OnToggleCheckmarkListener)
fun showHistoryEditorDialog(listener: OnDateClickedListener)
fun touchFeedback()
}
}

View File

@@ -28,15 +28,23 @@ import org.isoron.platform.time.LocalDate
import org.isoron.platform.time.LocalDateFormatter
import org.isoron.uhabits.core.models.PaletteColor
import kotlin.math.floor
import kotlin.math.max
import kotlin.math.min
import kotlin.math.round
fun interface OnDateClickedListener {
fun onDateClicked(date: LocalDate)
}
class HistoryChart(
var today: LocalDate,
var paletteColor: PaletteColor,
var theme: Theme,
var dateFormatter: LocalDateFormatter,
var firstWeekday: DayOfWeek,
var paletteColor: PaletteColor,
var series: List<Square>,
var theme: Theme,
var today: LocalDate,
var onDateClickedListener: OnDateClickedListener = OnDateClickedListener { },
var padding: Double = 0.0,
) : DataView {
enum class Square {
@@ -46,38 +54,58 @@ class HistoryChart(
HATCHED,
}
// Style
var padding = 0.0
var squareSpacing = 1.0
override var dataOffset = 0
private var squareSize = 0.0
var lastPrintedMonth = ""
var lastPrintedYear = ""
private var squareSize = 0.0
private var width = 0.0
private var height = 0.0
private var nColumns = 0
private var topLeftOffset = 0
private var topLeftDate = LocalDate(2020, 1, 1)
private var lastPrintedMonth = ""
private var lastPrintedYear = ""
private var headerOverflow = 0.0
override val dataColumnWidth: Double
get() = squareSpacing + squareSize
override fun onClick(x: Double, y: Double) {
if (width <= 0.0) throw IllegalStateException("onClick must be called after draw(canvas)")
val col = ((x - padding) / squareSize).toInt()
val row = ((y - padding) / squareSize).toInt()
val offset = col * 7 + (row - 1)
if (row == 0 || col == nColumns) return
val clickedDate = topLeftDate.plus(offset)
if (clickedDate.isNewerThan(today)) return
onDateClickedListener.onDateClicked(clickedDate)
}
override fun draw(canvas: Canvas) {
val width = canvas.getWidth()
val height = canvas.getHeight()
width = canvas.getWidth()
height = canvas.getHeight()
canvas.setColor(theme.cardBackgroundColor)
canvas.fill()
squareSize = round((height - 2 * padding) / 8.0)
canvas.setFontSize(height * 0.06)
canvas.setFontSize(min(14.0, height * 0.06))
val nColumns = floor((width - 2 * padding) / squareSize).toInt() - 2
val weekdayColumnWidth = DayOfWeek.values().map { weekday ->
canvas.measureText(dateFormatter.shortWeekdayName(weekday)) + squareSize * 0.15
}.maxOrNull() ?: 0.0
nColumns = floor((width - 2 * padding - weekdayColumnWidth) / squareSize).toInt()
val firstWeekdayOffset = (
today.dayOfWeek.daysSinceSunday -
firstWeekday.daysSinceSunday + 7
) % 7
val topLeftOffset = (nColumns - 1 + dataOffset) * 7 + firstWeekdayOffset
val topLeftDate = today.minus(topLeftOffset)
topLeftOffset = (nColumns - 1 + dataOffset) * 7 + firstWeekdayOffset
topLeftDate = today.minus(topLeftOffset)
lastPrintedYear = ""
lastPrintedMonth = ""
headerOverflow = 0.0
// Draw main columns
repeat(nColumns) { column ->
@@ -93,7 +121,7 @@ class HistoryChart(
canvas.setTextAlign(TextAlign.LEFT)
canvas.drawText(
dateFormatter.shortWeekdayName(date),
padding + nColumns * squareSize + squareSpacing * 3,
padding + nColumns * squareSize + squareSize * 0.15,
padding + squareSize * (row + 1) + squareSize / 2
)
}
@@ -143,9 +171,12 @@ class HistoryChart(
canvas.setTextAlign(TextAlign.LEFT)
canvas.drawText(
headerText,
padding + column * squareSize,
headerOverflow + padding + column * squareSize,
padding + squareSize / 2
)
headerOverflow += canvas.measureText(headerText) + 0.1 * squareSize
headerOverflow = max(0.0, headerOverflow - squareSize)
}
private fun drawSquare(

View File

@@ -33,19 +33,27 @@ import org.isoron.uhabits.core.ui.views.HistoryChart.Square.HATCHED
import org.isoron.uhabits.core.ui.views.HistoryChart.Square.OFF
import org.isoron.uhabits.core.ui.views.HistoryChart.Square.ON
import org.isoron.uhabits.core.ui.views.LightTheme
import org.isoron.uhabits.core.ui.views.OnDateClickedListener
import org.isoron.uhabits.core.ui.views.WidgetTheme
import org.junit.Test
import org.mockito.Mockito.mock
import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import java.util.Locale
class HistoryChartTest {
val base = "views/HistoryChart"
val dateClickedListener = mock(OnDateClickedListener::class.java)
val view = HistoryChart(
today = LocalDate(2015, 1, 25),
paletteColor = PaletteColor(7),
theme = LightTheme(),
dateFormatter = JavaLocalDateFormatter(Locale.US),
firstWeekday = SUNDAY,
onDateClickedListener = dateClickedListener,
series = listOf(
2, // today
2, 1, 2, 1, 2, 1, 2,
@@ -71,16 +79,42 @@ class HistoryChartTest {
}
)
// TODO: Label overflow
// TODO: onClick
// TODO: HistoryEditorDialog
// TODO: Remove excessive padding on widgets
@Test
fun testDraw() = runBlocking {
assertRenders(400, 200, "$base/base.png", view)
}
@Test
fun testClick() = runBlocking {
assertRenders(400, 200, "$base/base.png", view)
// Click top left date
view.onClick(20.0, 46.0)
verify(dateClickedListener).onDateClicked(LocalDate(2014, 10, 26))
reset(dateClickedListener)
view.onClick(2.0, 28.0)
verify(dateClickedListener).onDateClicked(LocalDate(2014, 10, 26))
reset(dateClickedListener)
// Click date in the middle
view.onClick(163.0, 113.0)
verify(dateClickedListener).onDateClicked(LocalDate(2014, 12, 10))
reset(dateClickedListener)
// Click today
view.onClick(336.0, 37.0)
verify(dateClickedListener).onDateClicked(LocalDate(2015, 1, 25))
reset(dateClickedListener)
// Click header
view.onClick(160.0, 15.0)
verifyNoMoreInteractions(dateClickedListener)
// Click right axis
view.onClick(360.0, 60.0)
verifyNoMoreInteractions(dateClickedListener)
}
@Test
fun testDrawWeekDay() = runBlocking {
view.firstWeekday = DayOfWeek.MONDAY