refactored fields; added date

This commit is contained in:
jpt 2025-04-10 22:18:46 -05:00
parent 9c55adb88e
commit 0d6c20cff6
4 changed files with 163 additions and 49 deletions

View File

@ -14,7 +14,7 @@ from ..utils import (
get_text_from_editor,
)
from .keymodal import KeyModal
from .modals import ChoiceModal #DateModal
from .modals import ChoiceModal
ELLIPSIS = ""
@ -23,20 +23,6 @@ class NotifyValidationError(Exception):
"""will notify and continue if raised"""
def _enum_preprocessor(enumCls):
"""generate a default preprocessor to enforce enums"""
def preprocessor(val):
try:
enumCls(val)
return val
except ValueError:
raise NotifyValidationError(
f"Invalid value {val}. Use: {[s.value for s in enumCls]}"
)
return preprocessor
class TableColumnConfig:
def __init__(
@ -45,8 +31,6 @@ class TableColumnConfig:
display_name: str,
*,
default=None,
enum=None,
preprocessor=None,
enable_editor=False,
filterable=True,
read_only=False,
@ -54,16 +38,45 @@ class TableColumnConfig:
self.field = field
self.display_name = display_name
self.default = default
self.enum = enum
self.enable_editor = enable_editor
self.filterable = filterable
self.read_only = read_only
if preprocessor:
self.preprocessor = preprocessor
elif self.enum:
self.preprocessor = _enum_preprocessor(self.enum)
def preprocess(self, val):
return val # no-op
def start_change(self, app, current_value):
if current_value.endswith(ELLIPSIS):
app.action_start_edit()
else:
self.preprocessor = lambda x: x
# default edit mode
app._show_input("edit", current_value)
class EnumColumnConfig(TableColumnConfig):
def __init__(
self,
field: str,
display_name: str,
enum,
**kwargs
):
super().__init__(field, display_name, **kwargs)
self.enumCls = enum
def preprocess(self, val):
try:
self.enumCls(val)
return val
except ValueError:
raise NotifyValidationError(
f"Invalid value {val}. Use: {[s.value for s in self.enumCls]}"
)
def start_change(self, app, current_value):
# a weird hack? pass app here and correct modal gets pushed
app.push_screen(ChoiceModal(self.enumCls, current_value), app.apply_change)
class TableEditor(App):
@ -341,16 +354,10 @@ class TableEditor(App):
current_value = self.table.get_cell_at(
(self.table.cursor_row, self.table.cursor_column)
)
current_value = remove_rich_tag(current_value)
if cconf.enum:
self.push_screen(ChoiceModal(cconf.enum, current_value),
self.apply_change)
elif current_value.endswith(ELLIPSIS):
self.action_start_edit()
else:
# default edit mode
current_value = remove_rich_tag(current_value)
self._show_input("edit", current_value)
# delegate to start_change, which will call back to self
cconf.start_change(self, current_value)
def action_start_edit(self):
cconf = self._active_column_config()
@ -401,7 +408,7 @@ class TableEditor(App):
# preprocess/validate the field being saved
try:
update_data[field] = cconf.preprocessor(new_value)
update_data[field] = cconf.preprocess(new_value)
self.update_item_callback(item_id, **update_data)
self.refresh_data()
except NotifyValidationError as e:

View File

@ -1,11 +1,9 @@
from textual.screen import ModalScreen
from textual.containers import Grid
from textual.binding import Binding
from textual.widgets import RadioSet, RadioButton, Label
class ChoiceModal(ModalScreen):
class ChoiceModal(ModalScreen):
CSS = """
ChoiceModal {
align: center middle;
@ -19,10 +17,9 @@ class ChoiceModal(ModalScreen):
BINDINGS = [
("j", "cursor_down", "Down"),
("k", "cursor_up", "Up"),
# TODO: get this to work as override
Binding("enter", "select", "Select", priority=True),
("c", "select", "Select"),
("escape", "cancel", "cancel")
("escape", "cancel", "cancel"),
]
def __init__(self, enum, selected):
@ -34,7 +31,8 @@ class ChoiceModal(ModalScreen):
yield Label(f"{self._enum.__name__}")
yield RadioSet(
*[
RadioButton(str(e.value), value=self.selected==str(e.value)) for e in self._enum
RadioButton(str(e.value), value=self.selected == str(e.value))
for e in self._enum
]
)
@ -43,7 +41,7 @@ class ChoiceModal(ModalScreen):
def action_cursor_up(self):
self.query_one(RadioSet).action_previous_button()
def action_select(self):
rs = self.query_one(RadioSet)
rs.action_toggle_button()
@ -52,3 +50,105 @@ class ChoiceModal(ModalScreen):
def action_cancel(self):
self.app.pop_screen()
class DateModal(ModalScreen):
CSS = """
DateModal {
layout: horizontal;
align: center middle;
background: $primary 30%;
}
DateModal Label {
border: solid grey;
}
DateModal Label.selected-date {
border: solid green;
}
"""
BINDINGS = [
("j", "cursor_down", "Down"),
("k", "cursor_up", "Up"),
("h", "cursor_left", "Left"),
("l", "cursor_right", "Right"),
# ("0,1,2,3,4,5,6,7,8,9", "num_entry", "#"),
Binding("enter", "select", "Select", priority=True),
("escape", "cancel", "cancel"),
]
def __init__(self, date):
self.pieces = [int(p) for p in date.split('-')]
self.selected = 1 # start on month
super().__init__()
def compose(self):
for idx, piece in enumerate(self.pieces):
yield Label(str(piece), classes="selected-date" if idx == self.selected else "")
def action_cursor_left(self):
# cycle Y/M/D
self.selected = (self.selected - 1) % 3
self._update_highlight()
def action_cursor_right(self):
self.selected = (self.selected + 1) % 3
self._update_highlight()
def _update_highlight(self):
for idx, lbl in enumerate(self.query("Label")):
if idx == self.selected:
lbl.add_class("selected-date")
else:
lbl.remove_class("selected-date")
def max_for(self, piece):
days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
if piece == 0:
return 3000
elif piece == 1:
return 12
else:
# -1 for offset array
return days_in_month[self.pieces[1]-1]
def _move_piece(self, by):
cur_value = self.pieces[self.selected]
cur_value += by
if cur_value == 0:
cur_value = self.max_for(self.selected)
if cur_value > self.max_for(self.selected):
cur_value = 1
self.pieces[self.selected] = cur_value
cur_label = self.query("Label")[self.selected]
cur_label.update(str(cur_value))
def action_cursor_down(self):
self._move_piece(-1)
def action_cursor_up(self):
self._move_piece(1)
def on_key(self, event) -> None:
key = event.key
if key in "0123456789":
cur_value = self.pieces[self.selected]
# If we can append the values like 1+2 = 12
# then do that, otherwise reset the value
# so (for months) 1+3 = 3
appended = int(str(cur_value) + key)
if appended <= self.max_for(self.selected):
cur_value = appended
else:
cur_value = int(key)
self.pieces[self.selected] = cur_value
self._move_piece(0)
event.prevent_default()
def action_select(self):
date = "-".join(str(p) for p in self.pieces)
self.dismiss(date)
def action_cancel(self):
self.app.pop_screen()

View File

@ -11,6 +11,7 @@ from ..db import GeneratorType
from .editor import (
TableEditor,
TableColumnConfig,
EnumColumnConfig,
)
@ -18,7 +19,7 @@ class TaskGenEditor(TableEditor):
TABLE_CONFIG = (
TableColumnConfig("id", "ID"),
TableColumnConfig("template", "Template", default="recur {val}"),
TableColumnConfig(
EnumColumnConfig(
"type",
"Type",
default=GeneratorType.DAYS_BETWEEN.value,

View File

@ -15,34 +15,40 @@ from ..utils import (
get_colored_category,
get_colored_status,
get_colored_date,
remove_rich_tag,
)
from .editor import (
TableEditor,
TableColumnConfig,
EnumColumnConfig,
NotifyValidationError,
ELLIPSIS,
)
from .modals import DateModal
class DateColumnConfig(TableColumnConfig):
def preprocess(self, val):
try:
return datetime.strptime(val, "%Y-%m-%d")
except ValueError:
raise NotifyValidationError("Invalid date format. Use YYYY-MM-DD")
def due_preprocessor(val):
try:
return datetime.strptime(val, "%Y-%m-%d")
except ValueError:
raise NotifyValidationError("Invalid date format. Use YYYY-MM-DD")
def start_change(self, app, current_value):
app.push_screen(DateModal(current_value), app.apply_change)
class TT(TableEditor):
TABLE_CONFIG = (
TableColumnConfig("id", "ID"),
TableColumnConfig("text", "Task", default="new task", enable_editor=True),
TableColumnConfig(
EnumColumnConfig(
"status",
"Status",
default="zero",
enum=TaskStatus,
default="zero",
),
TableColumnConfig("type", "Type", default=""),
TableColumnConfig("due", "Due", default="", preprocessor=due_preprocessor),
DateColumnConfig("due", "Due", default=""),
TableColumnConfig("category", "Category", default="main"),
)