Add date picker for search date/before/after keywords
- Add DatePickerModal using MonthCalendar widget from calendar app - Detect when user types 'date ', 'before ', or 'after ' and show picker - Insert selected date (YYYY-MM-DD format) into search input - Support keyboard navigation (left/right for months, Enter to select) - Today button for quick selection of current date
This commit is contained in:
@@ -4,8 +4,10 @@ Provides a search input docked to the top of the window with:
|
||||
- Live search with 1 second debounce
|
||||
- Cancel button to restore previous state
|
||||
- Help button showing Himalaya search syntax
|
||||
- Date picker for date/before/after keywords
|
||||
"""
|
||||
|
||||
from datetime import date
|
||||
from typing import Optional, List, Dict, Any
|
||||
|
||||
from textual.app import ComposeResult
|
||||
@@ -19,6 +21,8 @@ from textual.widget import Widget
|
||||
from textual.widgets import Button, Input, Label, Static
|
||||
from textual.suggester import SuggestFromList
|
||||
|
||||
from src.calendar.widgets.MonthCalendar import MonthCalendar
|
||||
|
||||
# Himalaya search keywords for autocomplete
|
||||
HIMALAYA_KEYWORDS = [
|
||||
"from ",
|
||||
@@ -132,6 +136,94 @@ class SearchHelpModal(ModalScreen[None]):
|
||||
self.dismiss(None)
|
||||
|
||||
|
||||
class DatePickerModal(ModalScreen[Optional[date]]):
|
||||
"""Modal with a calendar for selecting a date."""
|
||||
|
||||
DEFAULT_CSS = """
|
||||
DatePickerModal {
|
||||
align: center middle;
|
||||
}
|
||||
|
||||
DatePickerModal > Vertical {
|
||||
width: 30;
|
||||
height: auto;
|
||||
border: solid $primary;
|
||||
background: $surface;
|
||||
padding: 1 2;
|
||||
}
|
||||
|
||||
DatePickerModal > Vertical > Label {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
DatePickerModal > Vertical > Horizontal {
|
||||
height: auto;
|
||||
align: center middle;
|
||||
margin-top: 1;
|
||||
}
|
||||
|
||||
DatePickerModal > Vertical > Horizontal > Button {
|
||||
margin: 0 1;
|
||||
}
|
||||
"""
|
||||
|
||||
BINDINGS = [
|
||||
Binding("escape", "cancel", "Cancel"),
|
||||
Binding("left", "prev_month", "Previous month", show=False),
|
||||
Binding("right", "next_month", "Next month", show=False),
|
||||
Binding("enter", "select_date", "Select date", show=False),
|
||||
]
|
||||
|
||||
def __init__(self, keyword: str = "date") -> None:
|
||||
super().__init__()
|
||||
self.keyword = keyword
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Vertical():
|
||||
yield Label(f"Select date for '{self.keyword}':", id="picker-title")
|
||||
yield MonthCalendar(id="date-picker-calendar")
|
||||
with Horizontal():
|
||||
yield Button("Today", variant="default", id="today-btn")
|
||||
yield Button("Select", variant="primary", id="select-btn")
|
||||
yield Button("Cancel", variant="warning", id="cancel-btn")
|
||||
|
||||
def on_month_calendar_date_selected(
|
||||
self, event: MonthCalendar.DateSelected
|
||||
) -> None:
|
||||
"""Handle date selection from calendar click."""
|
||||
self.dismiss(event.date)
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
if event.button.id == "select-btn":
|
||||
calendar = self.query_one("#date-picker-calendar", MonthCalendar)
|
||||
self.dismiss(calendar.selected_date)
|
||||
elif event.button.id == "today-btn":
|
||||
calendar = self.query_one("#date-picker-calendar", MonthCalendar)
|
||||
today = date.today()
|
||||
calendar.selected_date = today
|
||||
calendar.display_month = today.replace(day=1)
|
||||
calendar.refresh()
|
||||
elif event.button.id == "cancel-btn":
|
||||
self.dismiss(None)
|
||||
|
||||
def action_cancel(self) -> None:
|
||||
self.dismiss(None)
|
||||
|
||||
def action_prev_month(self) -> None:
|
||||
calendar = self.query_one("#date-picker-calendar", MonthCalendar)
|
||||
calendar.prev_month()
|
||||
|
||||
def action_next_month(self) -> None:
|
||||
calendar = self.query_one("#date-picker-calendar", MonthCalendar)
|
||||
calendar.next_month()
|
||||
|
||||
def action_select_date(self) -> None:
|
||||
calendar = self.query_one("#date-picker-calendar", MonthCalendar)
|
||||
self.dismiss(calendar.selected_date)
|
||||
|
||||
|
||||
class SearchPanel(Widget):
|
||||
"""Docked search panel with live search capability."""
|
||||
|
||||
@@ -216,6 +308,7 @@ class SearchPanel(Widget):
|
||||
super().__init__(name=name, id=id, classes=classes)
|
||||
self._debounce_timer: Optional[Timer] = None
|
||||
self._last_query: str = ""
|
||||
self._pending_date_keyword: Optional[str] = None # Track keyword awaiting date
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Horizontal():
|
||||
@@ -304,6 +397,34 @@ class SearchPanel(Widget):
|
||||
self.is_searching = True
|
||||
self.post_message(self.SearchRequested(query))
|
||||
|
||||
def _check_date_keyword(self, value: str) -> Optional[str]:
|
||||
"""Check if the input ends with a date keyword that needs a date picker.
|
||||
|
||||
Returns the keyword (date/before/after) if found, None otherwise.
|
||||
"""
|
||||
value_lower = value.lower()
|
||||
for keyword in ("date ", "before ", "after "):
|
||||
if value_lower.endswith(keyword):
|
||||
return keyword.strip()
|
||||
return None
|
||||
|
||||
def _show_date_picker(self, keyword: str) -> None:
|
||||
"""Show the date picker modal for the given keyword."""
|
||||
self._pending_date_keyword = keyword
|
||||
|
||||
def on_date_selected(selected_date: Optional[date]) -> None:
|
||||
if selected_date:
|
||||
# Insert the date into the search input
|
||||
input_widget = self.query_one("#search-input", Input)
|
||||
date_str = selected_date.strftime("%Y-%m-%d")
|
||||
input_widget.value = input_widget.value + date_str
|
||||
input_widget.cursor_position = len(input_widget.value)
|
||||
self._pending_date_keyword = None
|
||||
# Refocus the input
|
||||
self.query_one("#search-input", Input).focus()
|
||||
|
||||
self.app.push_screen(DatePickerModal(keyword), on_date_selected)
|
||||
|
||||
def on_input_changed(self, event: Input.Changed) -> None:
|
||||
"""Handle input changes with debounce."""
|
||||
if event.input.id != "search-input":
|
||||
@@ -316,6 +437,12 @@ class SearchPanel(Widget):
|
||||
if not event.value.strip():
|
||||
return
|
||||
|
||||
# Check for date keywords and show picker
|
||||
date_keyword = self._check_date_keyword(event.value)
|
||||
if date_keyword:
|
||||
self._show_date_picker(date_keyword)
|
||||
return
|
||||
|
||||
# Set up new debounce timer (1 second)
|
||||
self._debounce_timer = self.set_timer(1.0, self._trigger_search)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user