HistoryChart: Fix HistoryEditorDialog

pull/707/head
Alinson S. Xavier 5 years ago
parent e2d2b5b4b3
commit 162eac3bdf

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 33 KiB

@ -177,4 +177,8 @@ class AndroidCanvas : Canvas {
val bmp = innerBitmap ?: throw UnsupportedOperationException() val bmp = innerBitmap ?: throw UnsupportedOperationException()
return AndroidImage(bmp) return AndroidImage(bmp)
} }
override fun measureText(text: String): Double {
return textPaint.measureText(text) / innerDensity
}
} }

@ -45,7 +45,24 @@ class AndroidDataView(
override fun onTouchEvent(event: MotionEvent?) = detector.onTouchEvent(event) override fun onTouchEvent(event: MotionEvent?) = detector.onTouchEvent(event)
override fun onDown(e: MotionEvent?) = true override fun onDown(e: MotionEvent?) = true
override fun onShowPress(e: MotionEvent?) = Unit override fun onShowPress(e: MotionEvent?) = Unit
override fun onSingleTapUp(e: MotionEvent?) = false
override fun onSingleTapUp(e: MotionEvent?): Boolean {
val x: Float
val y: Float
try {
val pointerId = e!!.getPointerId(0)
x = e.getX(pointerId)
y = e.getY(pointerId)
} catch (ex: RuntimeException) {
// Android often throws IllegalArgumentException here. Apparently,
// the pointer id may become invalid shortly after calling
// e.getPointerId.
return false
}
view?.onClick(x / canvas.innerDensity, y / canvas.innerDensity)
return true
}
override fun onLongPress(e: MotionEvent?) = Unit override fun onLongPress(e: MotionEvent?) = Unit
override fun onScroll( override fun onScroll(
@ -76,7 +93,16 @@ class AndroidDataView(
velocityX: Float, velocityX: Float,
velocityY: Float, velocityY: Float,
): Boolean { ): Boolean {
scroller.fling(scroller.currX, scroller.currY, velocityX.toInt() / 2, 0, 0, 10000, 0, 0) scroller.fling(
scroller.currX,
scroller.currY,
velocityX.toInt() / 2,
0,
0,
Integer.MAX_VALUE,
0,
0
)
invalidate() invalidate()
scrollAnimator.duration = scroller.duration.toLong() scrollAnimator.duration = scroller.duration.toLong()
scrollAnimator.start() scrollAnimator.start()
@ -99,11 +125,14 @@ class AndroidDataView(
} }
private fun updateDataOffset() { private fun updateDataOffset() {
var newDataOffset: Int = scroller.currX / (view.dataColumnWidth * canvas.innerDensity).toInt() view?.let { v ->
var newDataOffset: Int =
scroller.currX / (v.dataColumnWidth * canvas.innerDensity).toInt()
newDataOffset = Math.max(0, newDataOffset) newDataOffset = Math.max(0, newDataOffset)
if (newDataOffset != view.dataOffset) { if (newDataOffset != v.dataOffset) {
view.dataOffset = newDataOffset v.dataOffset = newDataOffset
postInvalidate() postInvalidate()
} }
} }
} }
}

@ -27,7 +27,7 @@ open class AndroidView<T : View>(
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
) : android.view.View(context, attrs) { ) : android.view.View(context, attrs) {
lateinit var view: T var view: T? = null
val canvas = AndroidCanvas() val canvas = AndroidCanvas()
override fun onDraw(canvas: android.graphics.Canvas) { override fun onDraw(canvas: android.graphics.Canvas) {
@ -36,6 +36,6 @@ open class AndroidView<T : View>(
this.canvas.innerWidth = width this.canvas.innerWidth = width
this.canvas.innerHeight = height this.canvas.innerHeight = height
this.canvas.innerDensity = resources.displayMetrics.density.toDouble() this.canvas.innerDensity = resources.displayMetrics.density.toDouble()
view.draw(this.canvas) view?.draw(this.canvas)
} }
} }

@ -1,203 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* 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.*;
import android.content.*;
import android.os.*;
import android.util.*;
import androidx.annotation.Nullable;
import androidx.annotation.*;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.core.commands.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.core.tasks.*;
import org.isoron.uhabits.core.ui.callbacks.*;
import org.isoron.uhabits.core.ui.screens.habits.show.views.*;
import org.isoron.uhabits.core.ui.views.*;
import org.isoron.uhabits.utils.*;
import org.jetbrains.annotations.*;
import java.util.*;
public class HistoryEditorDialog extends AppCompatDialogFragment
implements DialogInterface.OnClickListener, CommandRunner.Listener
{
@Nullable
private Habit habit;
@Nullable
HistoryChart historyChart;
@NonNull
private OnToggleCheckmarkListener onToggleCheckmarkListener;
private HabitList habitList;
private TaskRunner taskRunner;
private Preferences prefs;
private CommandRunner commandRunner;
public HistoryEditorDialog()
{
this.onToggleCheckmarkListener = new OnToggleCheckmarkListener()
{
@Override
public void onToggleEntry(@NotNull Timestamp timestamp, int value)
{
}
};
}
@Override
public void onClick(DialogInterface dialog, int which)
{
dismiss();
}
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState)
{
Context context = getActivity();
HabitsApplication app =
(HabitsApplication) getActivity().getApplicationContext();
habitList = app.getComponent().getHabitList();
taskRunner = app.getComponent().getTaskRunner();
commandRunner = app.getComponent().getCommandRunner();
prefs = app.getComponent().getPreferences();
// historyChart = new HistoryChart(context);
// historyChart.setOnToggleCheckmarkListener(onToggleCheckmarkListener);
// historyChart.setFirstWeekday(prefs.getFirstWeekday());
// historyChart.setSkipEnabled(prefs.isSkipEnabled());
// if (savedInstanceState != null)
// {
// long id = savedInstanceState.getLong("habit", -1);
// if (id > 0) this.habit = habitList.getById(id);
// historyChart.onRestoreInstanceState(
// savedInstanceState.getParcelable("historyChart"));
// }
//
// int padding =
// (int) getDimension(getContext(), R.dimen.history_editor_padding);
//
// historyChart.setPadding(padding, 0, padding, 0);
// historyChart.setIsEditable(true);
//
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder
.setTitle(R.string.history)
// .setView(historyChart)
.setPositiveButton(android.R.string.ok, this);
//
return builder.create();
}
@Override
public void onResume()
{
super.onResume();
DisplayMetrics metrics = getResources().getDisplayMetrics();
int maxHeight = getResources().getDimensionPixelSize(
R.dimen.history_editor_max_height);
int width = metrics.widthPixels;
int height = Math.min(metrics.heightPixels, maxHeight);
getDialog().getWindow().setLayout(width, height);
commandRunner.addListener(this);
refreshData();
}
@Override
public void onPause()
{
commandRunner.removeListener(this);
super.onPause();
}
@Override
public void onSaveInstanceState(Bundle outState)
{
// outState.putLong("habit", habit.getId());
// outState.putParcelable("historyChart", historyChart.onSaveInstanceState());
}
public void setOnToggleCheckmarkListener(@NonNull OnToggleCheckmarkListener onToggleCheckmarkListener)
{
this.onToggleCheckmarkListener = onToggleCheckmarkListener;
}
public void setHabit(@Nullable Habit habit)
{
this.habit = habit;
}
private void refreshData()
{
if (habit == null) return;
taskRunner.execute(new RefreshTask());
}
@Override
public void onCommandFinished(@NonNull Command command)
{
refreshData();
}
private class RefreshTask implements Task
{
public List<HistoryChart.Square> checkmarks;
@Override
public void doInBackground()
{
HistoryCardViewModel model = new HistoryCardPresenter().present(
habit,
prefs.getFirstWeekday(),
prefs.isSkipEnabled(),
new LightTheme()
);
checkmarks = model.getSeries();
}
@Override
public void onPostExecute()
{
if (getContext() == null || habit == null || historyChart == null)
return;
int color = PaletteUtilsKt.toThemedAndroidColor(habit.getColor(), getContext());
// historyChart.setColor(color);
// historyChart.setEntries(checkmarks);
// historyChart.setNumerical(habit.isNumerical());
}
}
}

@ -0,0 +1,107 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* 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.os.Bundle
import androidx.appcompat.app.AppCompatDialogFragment
import org.isoron.platform.gui.AndroidDataView
import org.isoron.platform.time.JavaLocalDateFormatter
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R
import org.isoron.uhabits.core.commands.Command
import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.ui.screens.habits.show.views.HistoryCardPresenter
import org.isoron.uhabits.core.ui.views.HistoryChart
import org.isoron.uhabits.core.ui.views.LightTheme
import org.isoron.uhabits.core.ui.views.OnDateClickedListener
import org.isoron.uhabits.core.utils.DateUtils
import java.util.Locale
import kotlin.math.min
class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
private lateinit var commandRunner: CommandRunner
private lateinit var habit: Habit
private lateinit var preferences: Preferences
private lateinit var dataView: AndroidDataView
private var chart: HistoryChart? = null
private var onDateClickedListener: OnDateClickedListener? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val component = (activity!!.application as HabitsApplication).component
commandRunner = component.commandRunner
habit = component.habitList.getById(arguments!!.getLong("habit"))!!
preferences = component.preferences
chart = HistoryChart(
dateFormatter = JavaLocalDateFormatter(Locale.getDefault()),
firstWeekday = preferences.firstWeekday,
paletteColor = habit.color,
series = emptyList(),
theme = LightTheme(),
today = DateUtils.getTodayWithOffset().toLocalDate(),
onDateClickedListener = onDateClickedListener ?: OnDateClickedListener { },
padding = 10.0,
)
dataView = AndroidDataView(context!!, null)
dataView.view = chart!!
return Dialog(context!!).apply {
val metrics = resources.displayMetrics
val maxHeight = resources.getDimensionPixelSize(R.dimen.history_editor_max_height)
setContentView(dataView)
window!!.setLayout(metrics.widthPixels, min(metrics.heightPixels, maxHeight))
}
}
override fun onResume() {
super.onResume()
commandRunner.addListener(this)
refreshData()
}
override fun onPause() {
commandRunner.removeListener(this)
super.onPause()
}
fun setOnDateClickedListener(listener: OnDateClickedListener) {
onDateClickedListener = listener
chart?.onDateClickedListener = listener
}
private fun refreshData() {
val model = HistoryCardPresenter().present(
habit,
preferences.firstWeekday,
preferences.isSkipEnabled,
theme = LightTheme()
)
chart?.series = model.series
dataView.postInvalidate()
}
override fun onCommandFinished(command: Command) {
refreshData()
}
}

@ -31,6 +31,7 @@ import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.activities.AndroidThemeSwitcher import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.activities.HabitsDirFinder import org.isoron.uhabits.activities.HabitsDirFinder
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialogFactory import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialogFactory
import org.isoron.uhabits.activities.common.dialogs.HistoryEditorDialog
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
import org.isoron.uhabits.core.commands.Command import org.isoron.uhabits.core.commands.Command
import org.isoron.uhabits.core.commands.CommandRunner import org.isoron.uhabits.core.commands.CommandRunner
@ -51,6 +52,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
private lateinit var habit: Habit private lateinit var habit: Habit
private lateinit var preferences: Preferences private lateinit var preferences: Preferences
private lateinit var themeSwitcher: AndroidThemeSwitcher private lateinit var themeSwitcher: AndroidThemeSwitcher
private lateinit var behavior: ShowHabitBehavior
private val scope = CoroutineScope(Dispatchers.Main) private val scope = CoroutineScope(Dispatchers.Main)
@ -76,7 +78,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
widgetUpdater = appComponent.widgetUpdater, widgetUpdater = appComponent.widgetUpdater,
) )
val behavior = ShowHabitBehavior( behavior = ShowHabitBehavior(
commandRunner = commandRunner, commandRunner = commandRunner,
habit = habit, habit = habit,
habitList = habitList, habitList = habitList,
@ -118,6 +120,9 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
commandRunner.addListener(this) commandRunner.addListener(this)
supportFragmentManager.findFragmentByTag("historyEditor")?.let {
(it as HistoryEditorDialog).setOnDateClickedListener(behavior)
}
refresh() refresh()
} }

@ -19,16 +19,18 @@
package org.isoron.uhabits.activities.habits.show package org.isoron.uhabits.activities.habits.show
import android.os.Bundle
import android.view.HapticFeedbackConstants
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialogFactory import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialogFactory
import org.isoron.uhabits.activities.common.dialogs.HistoryEditorDialog import org.isoron.uhabits.activities.common.dialogs.HistoryEditorDialog
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback
import org.isoron.uhabits.core.ui.callbacks.OnToggleCheckmarkListener
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitBehavior import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitBehavior
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitMenuBehavior import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitMenuBehavior
import org.isoron.uhabits.core.ui.views.OnDateClickedListener
import org.isoron.uhabits.intents.IntentFactory import org.isoron.uhabits.intents.IntentFactory
import org.isoron.uhabits.utils.showMessage import org.isoron.uhabits.utils.showMessage
import org.isoron.uhabits.utils.showSendFileScreen import org.isoron.uhabits.utils.showSendFileScreen
@ -43,7 +45,11 @@ class ShowHabitScreen(
val widgetUpdater: WidgetUpdater, val widgetUpdater: WidgetUpdater,
) : ShowHabitBehavior.Screen, ShowHabitMenuBehavior.Screen { ) : ShowHabitBehavior.Screen, ShowHabitMenuBehavior.Screen {
override fun showNumberPicker(value: Double, unit: String, callback: ListHabitsBehavior.NumberPickerCallback) { override fun showNumberPicker(
value: Double,
unit: String,
callback: ListHabitsBehavior.NumberPickerCallback,
) {
numberPickerFactory.create(value, unit, callback).show() numberPickerFactory.create(value, unit, callback).show()
} }
@ -55,13 +61,19 @@ class ShowHabitScreen(
activity.refresh() activity.refresh()
} }
override fun showHistoryEditorDialog(listener: OnToggleCheckmarkListener) { override fun showHistoryEditorDialog(listener: OnDateClickedListener) {
val dialog = HistoryEditorDialog() val dialog = HistoryEditorDialog()
dialog.setHabit(habit) dialog.arguments = Bundle().apply {
dialog.setOnToggleCheckmarkListener(listener) putLong("habit", habit.id!!)
}
dialog.setOnDateClickedListener(listener)
dialog.show(activity.supportFragmentManager, "historyEditor") dialog.show(activity.supportFragmentManager, "historyEditor")
} }
override fun touchFeedback() {
activity.window.decorView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
}
override fun showEditHabitScreen(habit: Habit) { override fun showEditHabitScreen(habit: Habit) {
activity.startActivity(intentFactory.startEditActivity(activity, habit)) activity.startActivity(intentFactory.startEditActivity(activity, habit))
} }

@ -40,7 +40,6 @@ class HistoryCardView(context: Context, attrs: AttributeSet) : LinearLayout(cont
} }
fun update(data: HistoryCardViewModel) { fun update(data: HistoryCardViewModel) {
val androidColor = data.color.toThemedAndroidColor(context) val androidColor = data.color.toThemedAndroidColor(context)
binding.title.setTextColor(androidColor) binding.title.setTextColor(androidColor)
binding.chart.view = HistoryChart( binding.chart.view = HistoryChart(
@ -51,10 +50,6 @@ class HistoryCardView(context: Context, attrs: AttributeSet) : LinearLayout(cont
series = data.series, series = data.series,
firstWeekday = data.firstWeekday, firstWeekday = data.firstWeekday,
) )
binding.chart.postInvalidate()
// binding.historyChart.setSkipEnabled(data.isSkipEnabled)
// if (data.isNumerical) {
// binding.historyChart.setNumerical(true)
// }
} }
} }

@ -21,8 +21,7 @@
<dimen name="baseSize">20dp</dimen> <dimen name="baseSize">20dp</dimen>
<dimen name="checkmarkWidth">48dp</dimen> <dimen name="checkmarkWidth">48dp</dimen>
<dimen name="checkmarkHeight">48dp</dimen> <dimen name="checkmarkHeight">48dp</dimen>
<dimen name="history_editor_max_height">450dp</dimen> <dimen name="history_editor_max_height">350dp</dimen>
<dimen name="history_editor_padding">8dp</dimen>
<dimen name="regularTextSize">16sp</dimen> <dimen name="regularTextSize">16sp</dimen>
<dimen name="smallTextSize">14sp</dimen> <dimen name="smallTextSize">14sp</dimen>
<dimen name="smallerTextSize">12sp</dimen> <dimen name="smallerTextSize">12sp</dimen>

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

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

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

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

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

@ -56,6 +56,12 @@ class JavaLocalDateFormatter(private val locale: Locale) : LocalDateFormatter {
return if (longName.length <= 3) longName else shortName 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 { override fun shortWeekdayName(date: LocalDate): String {
val cal = date.toGregorianCalendar() val cal = date.toGregorianCalendar()
return cal.getDisplayName(DAY_OF_WEEK, SHORT, locale) return cal.getDisplayName(DAY_OF_WEEK, SHORT, locale)

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

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

@ -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.OFF
import org.isoron.uhabits.core.ui.views.HistoryChart.Square.ON 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.LightTheme
import org.isoron.uhabits.core.ui.views.OnDateClickedListener
import org.isoron.uhabits.core.ui.views.WidgetTheme import org.isoron.uhabits.core.ui.views.WidgetTheme
import org.junit.Test 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 import java.util.Locale
class HistoryChartTest { class HistoryChartTest {
val base = "views/HistoryChart" val base = "views/HistoryChart"
val dateClickedListener = mock(OnDateClickedListener::class.java)
val view = HistoryChart( val view = HistoryChart(
today = LocalDate(2015, 1, 25), today = LocalDate(2015, 1, 25),
paletteColor = PaletteColor(7), paletteColor = PaletteColor(7),
theme = LightTheme(), theme = LightTheme(),
dateFormatter = JavaLocalDateFormatter(Locale.US), dateFormatter = JavaLocalDateFormatter(Locale.US),
firstWeekday = SUNDAY, firstWeekday = SUNDAY,
onDateClickedListener = dateClickedListener,
series = listOf( series = listOf(
2, // today 2, // today
2, 1, 2, 1, 2, 1, 2, 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 @Test
fun testDraw() = runBlocking { fun testDraw() = runBlocking {
assertRenders(400, 200, "$base/base.png", view) 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 @Test
fun testDrawWeekDay() = runBlocking { fun testDrawWeekDay() = runBlocking {
view.firstWeekday = DayOfWeek.MONDAY view.firstWeekday = DayOfWeek.MONDAY

Loading…
Cancel
Save