- Fixed DataTable row selection and event handling - Added explicit column keys to prevent auto-generated keys - Implemented row-to-flow mapping for reliable selection tracking - Converted left metrics panel to horizontal top bar - Fixed all missing FlowStats/EnhancedAnalysisData attributes - Created comprehensive Textual API documentation in Documentation/textual/ - Added validation checklist to prevent future API mismatches - Preserved cursor position during data refreshes - Fixed RowKey type handling and event names The TUI now properly handles flow selection, displays metrics in a compact top bar, and correctly correlates selected rows with the details pane.
200 lines
4.7 KiB
Markdown
200 lines
4.7 KiB
Markdown
# Textual Events and Messages Reference
|
|
|
|
## Overview
|
|
|
|
Textual uses an event-driven architecture with two main types of communication:
|
|
1. **Events** - User interactions (keyboard, mouse)
|
|
2. **Messages** - Widget-to-widget communication
|
|
|
|
## Event Handler Patterns
|
|
|
|
### Basic Pattern
|
|
|
|
```python
|
|
def on_<namespace>_<event_name>(self, event: EventType) -> None:
|
|
"""Handle event"""
|
|
```
|
|
|
|
### Examples
|
|
|
|
```python
|
|
# Widget events
|
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
pass
|
|
|
|
# DataTable events
|
|
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
|
pass
|
|
|
|
# Key events
|
|
def on_key(self, event: events.Key) -> None:
|
|
if event.key == "escape":
|
|
self.exit()
|
|
```
|
|
|
|
## Common Widget Events
|
|
|
|
### DataTable Events
|
|
|
|
| Event | Handler | Description |
|
|
|-------|---------|-------------|
|
|
| `RowHighlighted` | `on_data_table_row_highlighted` | Cursor moves to row |
|
|
| `RowSelected` | `on_data_table_row_selected` | Row selected (Enter) |
|
|
| `CellHighlighted` | `on_data_table_cell_highlighted` | Cursor moves to cell |
|
|
| `CellSelected` | `on_data_table_cell_selected` | Cell selected |
|
|
| `ColumnHighlighted` | `on_data_table_column_highlighted` | Column highlighted |
|
|
| `HeaderSelected` | `on_data_table_header_selected` | Header clicked |
|
|
|
|
### Button Events
|
|
|
|
| Event | Handler | Description |
|
|
|-------|---------|-------------|
|
|
| `Pressed` | `on_button_pressed` | Button clicked/activated |
|
|
|
|
### Input Events
|
|
|
|
| Event | Handler | Description |
|
|
|-------|---------|-------------|
|
|
| `Changed` | `on_input_changed` | Text changed |
|
|
| `Submitted` | `on_input_submitted` | Enter pressed |
|
|
|
|
## Custom Messages
|
|
|
|
### Creating a Custom Message
|
|
|
|
```python
|
|
from textual.message import Message
|
|
|
|
class MyWidget(Widget):
|
|
class DataUpdated(Message):
|
|
"""Custom message when data updates"""
|
|
def __init__(self, data: dict) -> None:
|
|
self.data = data
|
|
super().__init__()
|
|
|
|
def update_data(self, new_data: dict) -> None:
|
|
# Post custom message
|
|
self.post_message(self.DataUpdated(new_data))
|
|
```
|
|
|
|
### Handling Custom Messages
|
|
|
|
```python
|
|
class MyApp(App):
|
|
def on_my_widget_data_updated(self, event: MyWidget.DataUpdated) -> None:
|
|
"""Handle custom message"""
|
|
self.process_data(event.data)
|
|
```
|
|
|
|
## Event Properties
|
|
|
|
### Common Event Attributes
|
|
|
|
```python
|
|
# All events have:
|
|
event.stop() # Stop propagation
|
|
event.prevent_default() # Prevent default behavior
|
|
|
|
# Keyboard events
|
|
event.key # Key name ("a", "ctrl+c", "escape")
|
|
event.character # Unicode character
|
|
event.aliases # List of key aliases
|
|
|
|
# Mouse events
|
|
event.x, event.y # Coordinates
|
|
event.button # Mouse button number
|
|
event.shift, event.ctrl, event.alt # Modifier keys
|
|
|
|
# DataTable events
|
|
event.row_key # Row identifier
|
|
event.row_index # Row number
|
|
event.coordinate # (row, column) tuple
|
|
```
|
|
|
|
## Event Flow
|
|
|
|
1. **Capture Phase**: Event travels down from App to target
|
|
2. **Bubble Phase**: Event travels up from target to App
|
|
|
|
```python
|
|
# Stop event propagation
|
|
def on_key(self, event: events.Key) -> None:
|
|
if event.key == "q":
|
|
event.stop() # Don't let parent widgets see this
|
|
```
|
|
|
|
## Message Posting
|
|
|
|
### Post to Self
|
|
|
|
```python
|
|
self.post_message(MyMessage())
|
|
```
|
|
|
|
### Post to Parent
|
|
|
|
```python
|
|
self.post_message_no_wait(MyMessage()) # Don't wait for processing
|
|
```
|
|
|
|
### Post to Specific Widget
|
|
|
|
```python
|
|
target_widget.post_message(MyMessage())
|
|
```
|
|
|
|
## Event Debugging
|
|
|
|
### Log All Events
|
|
|
|
```python
|
|
def on_event(self, event: events.Event) -> None:
|
|
"""Log all events for debugging"""
|
|
self.log(f"Event: {event.__class__.__name__}")
|
|
```
|
|
|
|
### Check Handler Names
|
|
|
|
```python
|
|
# List all event handlers
|
|
handlers = [m for m in dir(self) if m.startswith('on_')]
|
|
```
|
|
|
|
## Common Patterns
|
|
|
|
### Debouncing Events
|
|
|
|
```python
|
|
from textual.timer import Timer
|
|
|
|
class MyWidget(Widget):
|
|
def __init__(self):
|
|
self._update_timer: Timer | None = None
|
|
|
|
def on_input_changed(self, event: Input.Changed) -> None:
|
|
# Cancel previous timer
|
|
if self._update_timer:
|
|
self._update_timer.cancel()
|
|
|
|
# Set new timer
|
|
self._update_timer = self.set_timer(0.3, self.perform_update)
|
|
```
|
|
|
|
### Event Validation
|
|
|
|
```python
|
|
def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
|
|
# Validate event data
|
|
if not event.row_key:
|
|
return
|
|
|
|
# Convert types if needed
|
|
row_key_str = str(event.row_key)
|
|
```
|
|
|
|
## Common Mistakes
|
|
|
|
1. **Wrong handler name**: `on_data_table_cursor_moved` ❌ → `on_data_table_row_highlighted` ✅
|
|
2. **Missing namespace**: `on_row_selected` ❌ → `on_data_table_row_selected` ✅
|
|
3. **Wrong event class**: `DataTable.CursorMoved` ❌ → `DataTable.RowHighlighted` ✅
|
|
4. **Not converting types**: Assuming `row_key` is string when it's `RowKey` type |