Initial commit
This commit is contained in:
346
skills/textual-builder/references/interactivity.md
Normal file
346
skills/textual-builder/references/interactivity.md
Normal file
@@ -0,0 +1,346 @@
|
||||
# Interactivity: Events, Bindings, and Actions
|
||||
|
||||
## Key Bindings
|
||||
|
||||
Define keyboard shortcuts:
|
||||
|
||||
```python
|
||||
from textual.app import App
|
||||
from textual.binding import Binding
|
||||
|
||||
class MyApp(App):
|
||||
BINDINGS = [
|
||||
("q", "quit", "Quit"), # key, action, description
|
||||
("s", "save", "Save"),
|
||||
("ctrl+c", "copy", "Copy"),
|
||||
Binding("f1", "help", "Help", show=True, priority=True),
|
||||
]
|
||||
|
||||
def action_save(self) -> None:
|
||||
"""Actions are methods prefixed with 'action_'."""
|
||||
self.notify("Saved!")
|
||||
|
||||
def action_copy(self) -> None:
|
||||
self.notify("Copied!")
|
||||
|
||||
def action_help(self) -> None:
|
||||
self.notify("Help content...")
|
||||
```
|
||||
|
||||
### Binding Options
|
||||
|
||||
```python
|
||||
Binding(
|
||||
key="f1",
|
||||
action="help",
|
||||
description="Help",
|
||||
show=True, # Show in footer (default: True)
|
||||
priority=True, # Prioritize over widget bindings
|
||||
)
|
||||
```
|
||||
|
||||
### Dynamic Bindings
|
||||
|
||||
Refresh bindings when state changes:
|
||||
|
||||
```python
|
||||
class MyApp(App):
|
||||
page = reactive(0, bindings=True) # Auto-refresh bindings
|
||||
|
||||
def check_action(self, action: str, parameters) -> bool | None:
|
||||
"""Control action availability."""
|
||||
if action == "next" and self.page >= MAX_PAGES:
|
||||
return None # Disables and dims the key
|
||||
if action == "previous" and self.page == 0:
|
||||
return None
|
||||
return True # Enabled
|
||||
```
|
||||
|
||||
Or manually refresh:
|
||||
|
||||
```python
|
||||
def update_state(self):
|
||||
self.state = "new_state"
|
||||
self.refresh_bindings() # Update footer
|
||||
```
|
||||
|
||||
## Mouse Events
|
||||
|
||||
Handle mouse interactions:
|
||||
|
||||
```python
|
||||
from textual import events
|
||||
|
||||
class MyWidget(Widget):
|
||||
def on_click(self, event: events.Click) -> None:
|
||||
"""Widget was clicked."""
|
||||
self.notify(f"Clicked at {event.x}, {event.y}")
|
||||
|
||||
def on_mouse_move(self, event: events.MouseMove) -> None:
|
||||
"""Mouse moved over widget."""
|
||||
pass
|
||||
|
||||
def on_enter(self, event: events.Enter) -> None:
|
||||
"""Mouse entered widget."""
|
||||
self.add_class("hover")
|
||||
|
||||
def on_leave(self, event: events.Leave) -> None:
|
||||
"""Mouse left widget."""
|
||||
self.remove_class("hover")
|
||||
```
|
||||
|
||||
## Keyboard Events
|
||||
|
||||
Handle key presses:
|
||||
|
||||
```python
|
||||
from textual import events
|
||||
|
||||
class MyApp(App):
|
||||
def on_key(self, event: events.Key) -> None:
|
||||
"""Any key pressed."""
|
||||
if event.key == "escape":
|
||||
self.exit()
|
||||
elif event.key == "space":
|
||||
self.toggle_pause()
|
||||
|
||||
def key_r(self, event: events.Key) -> None:
|
||||
"""Specific key handler (press 'r')."""
|
||||
self.reset()
|
||||
```
|
||||
|
||||
## Focus Events
|
||||
|
||||
Track focus changes:
|
||||
|
||||
```python
|
||||
def on_focus(self, event: events.Focus) -> None:
|
||||
"""Widget gained focus."""
|
||||
self.border_title = "Focused"
|
||||
|
||||
def on_blur(self, event: events.Blur) -> None:
|
||||
"""Widget lost focus."""
|
||||
self.border_title = ""
|
||||
```
|
||||
|
||||
Programmatic focus:
|
||||
|
||||
```python
|
||||
widget.focus() # Give focus to widget
|
||||
widget.can_focus = True # Enable focusing (default for inputs)
|
||||
```
|
||||
|
||||
## Widget Messages
|
||||
|
||||
Handle messages from specific widgets:
|
||||
|
||||
```python
|
||||
from textual.widgets import Button, Input, Switch
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""Any button pressed."""
|
||||
button_id = event.button.id
|
||||
self.notify(f"Button {button_id} pressed")
|
||||
|
||||
def on_input_changed(self, event: Input.Changed) -> None:
|
||||
"""Input text changed."""
|
||||
self.update_preview(event.value)
|
||||
|
||||
def on_input_submitted(self, event: Input.Submitted) -> None:
|
||||
"""User pressed Enter in input."""
|
||||
self.process(event.value)
|
||||
|
||||
def on_switch_changed(self, event: Switch.Changed) -> None:
|
||||
"""Switch toggled."""
|
||||
self.feature_enabled = event.value
|
||||
|
||||
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
||||
"""Row in table selected."""
|
||||
row_key = event.row_key
|
||||
```
|
||||
|
||||
### Message Naming Convention
|
||||
|
||||
Handler method: `on_{widget_type}_{message_name}`
|
||||
- Converts to snake_case
|
||||
- Example: `Button.Pressed` → `on_button_pressed`
|
||||
- Custom widget: `MyWidget.ValueChanged` → `on_my_widget_value_changed`
|
||||
|
||||
## Custom Messages
|
||||
|
||||
Define custom messages for your widgets:
|
||||
|
||||
```python
|
||||
from textual.message import Message
|
||||
from textual.widget import Widget
|
||||
|
||||
class Card(Widget):
|
||||
class Selected(Message):
|
||||
"""Posted when card is selected."""
|
||||
def __init__(self, card_id: str, value: int) -> None:
|
||||
super().__init__()
|
||||
self.card_id = card_id
|
||||
self.value = value
|
||||
|
||||
def on_click(self) -> None:
|
||||
self.post_message(self.Selected(self.id, self.value))
|
||||
|
||||
# Handle in parent
|
||||
def on_card_selected(self, event: Card.Selected) -> None:
|
||||
self.notify(f"Card {event.card_id} (value: {event.value}) selected")
|
||||
```
|
||||
|
||||
## Message Control
|
||||
|
||||
### Stop Propagation
|
||||
|
||||
Prevent message from bubbling to parent:
|
||||
|
||||
```python
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
event.stop() # Don't propagate to parent
|
||||
# Handle locally
|
||||
```
|
||||
|
||||
### Prevent Messages
|
||||
|
||||
Temporarily suppress messages:
|
||||
|
||||
```python
|
||||
with widget.prevent(Switch.Changed):
|
||||
widget.value = True # Won't emit Changed message
|
||||
```
|
||||
|
||||
Useful when programmatically updating to avoid infinite loops.
|
||||
|
||||
## Actions
|
||||
|
||||
Actions are methods that can be triggered by bindings or programmatically:
|
||||
|
||||
```python
|
||||
class MyApp(App):
|
||||
BINDINGS = [
|
||||
("n", "next_page", "Next"),
|
||||
("p", "prev_page", "Previous"),
|
||||
]
|
||||
|
||||
def action_next_page(self) -> None:
|
||||
self.page += 1
|
||||
self.refresh_view()
|
||||
|
||||
def action_prev_page(self) -> None:
|
||||
self.page -= 1
|
||||
self.refresh_view()
|
||||
```
|
||||
|
||||
### Parameterized Actions
|
||||
|
||||
Pass parameters to actions:
|
||||
|
||||
```python
|
||||
BINDINGS = [
|
||||
("r", "add_color('red')", "Red"),
|
||||
("g", "add_color('green')", "Green"),
|
||||
("b", "add_color('blue')", "Blue"),
|
||||
]
|
||||
|
||||
def action_add_color(self, color: str) -> None:
|
||||
self.add_widget(ColorBar(color))
|
||||
```
|
||||
|
||||
### Programmatic Action Calls
|
||||
|
||||
```python
|
||||
self.run_action("save") # Trigger action by name
|
||||
```
|
||||
|
||||
## Notifications
|
||||
|
||||
Show temporary messages to user:
|
||||
|
||||
```python
|
||||
self.notify("File saved successfully!")
|
||||
self.notify("Error occurred", severity="error")
|
||||
self.notify("Warning!", severity="warning")
|
||||
self.notify("Info message", severity="information", timeout=5)
|
||||
```
|
||||
|
||||
## Timers
|
||||
|
||||
Schedule repeated actions:
|
||||
|
||||
```python
|
||||
def on_mount(self) -> None:
|
||||
self.set_interval(1.0, self.update_timer) # Every 1 second
|
||||
|
||||
def update_timer(self) -> None:
|
||||
self.elapsed += 1
|
||||
self.query_one("#timer").update(str(self.elapsed))
|
||||
```
|
||||
|
||||
One-time delayed action:
|
||||
|
||||
```python
|
||||
self.set_timer(2.0, self.delayed_action) # After 2 seconds
|
||||
|
||||
def delayed_action(self) -> None:
|
||||
self.notify("Timer complete!")
|
||||
```
|
||||
|
||||
## Example: Interactive Card Selection
|
||||
|
||||
```python
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.containers import Horizontal
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Label, Static
|
||||
from textual.message import Message
|
||||
|
||||
class Card(Widget):
|
||||
DEFAULT_CSS = """
|
||||
Card {
|
||||
width: 12;
|
||||
height: 10;
|
||||
border: round white;
|
||||
background: $panel;
|
||||
}
|
||||
Card:hover {
|
||||
background: $boost;
|
||||
}
|
||||
Card.selected {
|
||||
border: double cyan;
|
||||
background: $accent;
|
||||
}
|
||||
"""
|
||||
|
||||
class Selected(Message):
|
||||
def __init__(self, card: "Card") -> None:
|
||||
super().__init__()
|
||||
self.card = card
|
||||
|
||||
def __init__(self, suit: str, value: str) -> None:
|
||||
super().__init__()
|
||||
self.suit = suit
|
||||
self.value = value
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Label(f"{self.value}\n{self.suit}")
|
||||
|
||||
def on_click(self) -> None:
|
||||
self.post_message(self.Selected(self))
|
||||
|
||||
class CardGame(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
with Horizontal(id="hand"):
|
||||
yield Card("♠", "A")
|
||||
yield Card("♥", "K")
|
||||
yield Card("♣", "Q")
|
||||
|
||||
def on_card_selected(self, event: Card.Selected) -> None:
|
||||
# Deselect all
|
||||
for card in self.query(Card):
|
||||
card.remove_class("selected")
|
||||
# Select clicked
|
||||
event.card.add_class("selected")
|
||||
self.notify(f"Selected {event.card.value} of {event.card.suit}")
|
||||
```
|
||||
Reference in New Issue
Block a user