HistoryChart: Fix HistoryEditorDialog
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 33 KiB |
@@ -177,4 +177,8 @@ class AndroidCanvas : Canvas {
|
||||
val bmp = innerBitmap ?: throw UnsupportedOperationException()
|
||||
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 onDown(e: MotionEvent?) = true
|
||||
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 onScroll(
|
||||
@@ -76,7 +93,16 @@ class AndroidDataView(
|
||||
velocityX: Float,
|
||||
velocityY: Float,
|
||||
): 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()
|
||||
scrollAnimator.duration = scroller.duration.toLong()
|
||||
scrollAnimator.start()
|
||||
@@ -99,11 +125,14 @@ class AndroidDataView(
|
||||
}
|
||||
|
||||
private fun updateDataOffset() {
|
||||
var newDataOffset: Int = scroller.currX / (view.dataColumnWidth * canvas.innerDensity).toInt()
|
||||
newDataOffset = Math.max(0, newDataOffset)
|
||||
if (newDataOffset != view.dataOffset) {
|
||||
view.dataOffset = newDataOffset
|
||||
postInvalidate()
|
||||
view?.let { v ->
|
||||
var newDataOffset: Int =
|
||||
scroller.currX / (v.dataColumnWidth * canvas.innerDensity).toInt()
|
||||
newDataOffset = Math.max(0, newDataOffset)
|
||||
if (newDataOffset != v.dataOffset) {
|
||||
v.dataOffset = newDataOffset
|
||||
postInvalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ open class AndroidView<T : View>(
|
||||
attrs: AttributeSet? = null,
|
||||
) : android.view.View(context, attrs) {
|
||||
|
||||
lateinit var view: T
|
||||
var view: T? = null
|
||||
val canvas = AndroidCanvas()
|
||||
|
||||
override fun onDraw(canvas: android.graphics.Canvas) {
|
||||
@@ -36,6 +36,6 @@ open class AndroidView<T : View>(
|
||||
this.canvas.innerWidth = width
|
||||
this.canvas.innerHeight = height
|
||||
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.HabitsDirFinder
|
||||
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.core.commands.Command
|
||||
import org.isoron.uhabits.core.commands.CommandRunner
|
||||
@@ -51,6 +52,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
|
||||
private lateinit var habit: Habit
|
||||
private lateinit var preferences: Preferences
|
||||
private lateinit var themeSwitcher: AndroidThemeSwitcher
|
||||
private lateinit var behavior: ShowHabitBehavior
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.Main)
|
||||
|
||||
@@ -76,7 +78,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
|
||||
widgetUpdater = appComponent.widgetUpdater,
|
||||
)
|
||||
|
||||
val behavior = ShowHabitBehavior(
|
||||
behavior = ShowHabitBehavior(
|
||||
commandRunner = commandRunner,
|
||||
habit = habit,
|
||||
habitList = habitList,
|
||||
@@ -118,6 +120,9 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
commandRunner.addListener(this)
|
||||
supportFragmentManager.findFragmentByTag("historyEditor")?.let {
|
||||
(it as HistoryEditorDialog).setOnDateClickedListener(behavior)
|
||||
}
|
||||
refresh()
|
||||
}
|
||||
|
||||
|
||||
@@ -19,16 +19,18 @@
|
||||
|
||||
package org.isoron.uhabits.activities.habits.show
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.HapticFeedbackConstants
|
||||
import org.isoron.uhabits.R
|
||||
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.core.models.Habit
|
||||
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.show.ShowHabitBehavior
|
||||
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.utils.showMessage
|
||||
import org.isoron.uhabits.utils.showSendFileScreen
|
||||
@@ -43,7 +45,11 @@ class ShowHabitScreen(
|
||||
val widgetUpdater: WidgetUpdater,
|
||||
) : 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()
|
||||
}
|
||||
|
||||
@@ -55,13 +61,19 @@ class ShowHabitScreen(
|
||||
activity.refresh()
|
||||
}
|
||||
|
||||
override fun showHistoryEditorDialog(listener: OnToggleCheckmarkListener) {
|
||||
override fun showHistoryEditorDialog(listener: OnDateClickedListener) {
|
||||
val dialog = HistoryEditorDialog()
|
||||
dialog.setHabit(habit)
|
||||
dialog.setOnToggleCheckmarkListener(listener)
|
||||
dialog.arguments = Bundle().apply {
|
||||
putLong("habit", habit.id!!)
|
||||
}
|
||||
dialog.setOnDateClickedListener(listener)
|
||||
dialog.show(activity.supportFragmentManager, "historyEditor")
|
||||
}
|
||||
|
||||
override fun touchFeedback() {
|
||||
activity.window.decorView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
|
||||
}
|
||||
|
||||
override fun showEditHabitScreen(habit: Habit) {
|
||||
activity.startActivity(intentFactory.startEditActivity(activity, habit))
|
||||
}
|
||||
|
||||
@@ -40,7 +40,6 @@ class HistoryCardView(context: Context, attrs: AttributeSet) : LinearLayout(cont
|
||||
}
|
||||
|
||||
fun update(data: HistoryCardViewModel) {
|
||||
|
||||
val androidColor = data.color.toThemedAndroidColor(context)
|
||||
binding.title.setTextColor(androidColor)
|
||||
binding.chart.view = HistoryChart(
|
||||
@@ -51,10 +50,6 @@ class HistoryCardView(context: Context, attrs: AttributeSet) : LinearLayout(cont
|
||||
series = data.series,
|
||||
firstWeekday = data.firstWeekday,
|
||||
)
|
||||
|
||||
// binding.historyChart.setSkipEnabled(data.isSkipEnabled)
|
||||
// if (data.isNumerical) {
|
||||
// binding.historyChart.setNumerical(true)
|
||||
// }
|
||||
binding.chart.postInvalidate()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,7 @@
|
||||
<dimen name="baseSize">20dp</dimen>
|
||||
<dimen name="checkmarkWidth">48dp</dimen>
|
||||
<dimen name="checkmarkHeight">48dp</dimen>
|
||||
<dimen name="history_editor_max_height">450dp</dimen>
|
||||
<dimen name="history_editor_padding">8dp</dimen>
|
||||
<dimen name="history_editor_max_height">350dp</dimen>
|
||||
<dimen name="regularTextSize">16sp</dimen>
|
||||
<dimen name="smallTextSize">14sp</dimen>
|
||||
<dimen name="smallerTextSize">12sp</dimen>
|
||||
|
||||
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
BIN
android/uhabits-core/assets/test/views/HistoryChart/weekday.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -21,6 +21,8 @@ package org.isoron.platform.gui
|
||||
|
||||
interface View {
|
||||
fun draw(canvas: Canvas)
|
||||
fun onClick(x: Double, y: Double) {
|
||||
}
|
||||
}
|
||||
|
||||
interface DataView : View {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||