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, get_text_from_editor,
) )
from .keymodal import KeyModal from .keymodal import KeyModal
from .modals import ChoiceModal #DateModal from .modals import ChoiceModal
ELLIPSIS = "" ELLIPSIS = ""
@ -23,20 +23,6 @@ class NotifyValidationError(Exception):
"""will notify and continue if raised""" """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: class TableColumnConfig:
def __init__( def __init__(
@ -45,8 +31,6 @@ class TableColumnConfig:
display_name: str, display_name: str,
*, *,
default=None, default=None,
enum=None,
preprocessor=None,
enable_editor=False, enable_editor=False,
filterable=True, filterable=True,
read_only=False, read_only=False,
@ -54,16 +38,45 @@ class TableColumnConfig:
self.field = field self.field = field
self.display_name = display_name self.display_name = display_name
self.default = default self.default = default
self.enum = enum
self.enable_editor = enable_editor self.enable_editor = enable_editor
self.filterable = filterable self.filterable = filterable
self.read_only = read_only self.read_only = read_only
if preprocessor:
self.preprocessor = preprocessor def preprocess(self, val):
elif self.enum: return val # no-op
self.preprocessor = _enum_preprocessor(self.enum)
def start_change(self, app, current_value):
if current_value.endswith(ELLIPSIS):
app.action_start_edit()
else: 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): class TableEditor(App):
@ -341,16 +354,10 @@ class TableEditor(App):
current_value = self.table.get_cell_at( current_value = self.table.get_cell_at(
(self.table.cursor_row, self.table.cursor_column) (self.table.cursor_row, self.table.cursor_column)
) )
current_value = remove_rich_tag(current_value)
if cconf.enum: # delegate to start_change, which will call back to self
self.push_screen(ChoiceModal(cconf.enum, current_value), cconf.start_change(self, 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)
def action_start_edit(self): def action_start_edit(self):
cconf = self._active_column_config() cconf = self._active_column_config()
@ -401,7 +408,7 @@ class TableEditor(App):
# preprocess/validate the field being saved # preprocess/validate the field being saved
try: try:
update_data[field] = cconf.preprocessor(new_value) update_data[field] = cconf.preprocess(new_value)
self.update_item_callback(item_id, **update_data) self.update_item_callback(item_id, **update_data)
self.refresh_data() self.refresh_data()
except NotifyValidationError as e: except NotifyValidationError as e:

View File

@ -1,11 +1,9 @@
from textual.screen import ModalScreen from textual.screen import ModalScreen
from textual.containers import Grid
from textual.binding import Binding from textual.binding import Binding
from textual.widgets import RadioSet, RadioButton, Label from textual.widgets import RadioSet, RadioButton, Label
class ChoiceModal(ModalScreen):
class ChoiceModal(ModalScreen):
CSS = """ CSS = """
ChoiceModal { ChoiceModal {
align: center middle; align: center middle;
@ -19,10 +17,9 @@ class ChoiceModal(ModalScreen):
BINDINGS = [ BINDINGS = [
("j", "cursor_down", "Down"), ("j", "cursor_down", "Down"),
("k", "cursor_up", "Up"), ("k", "cursor_up", "Up"),
# TODO: get this to work as override
Binding("enter", "select", "Select", priority=True), Binding("enter", "select", "Select", priority=True),
("c", "select", "Select"), ("c", "select", "Select"),
("escape", "cancel", "cancel") ("escape", "cancel", "cancel"),
] ]
def __init__(self, enum, selected): def __init__(self, enum, selected):
@ -34,7 +31,8 @@ class ChoiceModal(ModalScreen):
yield Label(f"{self._enum.__name__}") yield Label(f"{self._enum.__name__}")
yield RadioSet( 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
] ]
) )
@ -52,3 +50,105 @@ class ChoiceModal(ModalScreen):
def action_cancel(self): def action_cancel(self):
self.app.pop_screen() 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 ( from .editor import (
TableEditor, TableEditor,
TableColumnConfig, TableColumnConfig,
EnumColumnConfig,
) )
@ -18,7 +19,7 @@ class TaskGenEditor(TableEditor):
TABLE_CONFIG = ( TABLE_CONFIG = (
TableColumnConfig("id", "ID"), TableColumnConfig("id", "ID"),
TableColumnConfig("template", "Template", default="recur {val}"), TableColumnConfig("template", "Template", default="recur {val}"),
TableColumnConfig( EnumColumnConfig(
"type", "type",
"Type", "Type",
default=GeneratorType.DAYS_BETWEEN.value, default=GeneratorType.DAYS_BETWEEN.value,

View File

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