-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy patheditable_data_table.py
More file actions
155 lines (128 loc) · 5.14 KB
/
editable_data_table.py
File metadata and controls
155 lines (128 loc) · 5.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
"""Example to demonstrate an editable data table widget.
A regular DataTable is not editable. You _can_ update cells, but that has to be
done in code. This example demonstrates a widget with a key binding ('e') to
edit cells. If you press the edit key a modal screen is displayed with a single
Input widget containing the cell's original value. The widget is placed such
that the value of the Input widget precisely overlays the value in the cell.
Visually, pressing the edit key just draws a border around the cell and dims the
rest of the screen. You can edit the cell to your hearts content, but the type
must remain the same. So if you edit an integer, you cannot replace it with a
string (or even a float).
"""
from typing import Any
from textual import work
from textual.app import App, ComposeResult
from textual.containers import Horizontal
from textual.coordinate import Coordinate
from textual.geometry import Offset, Region
from textual.screen import ModalScreen
from textual.widgets import DataTable, Footer, Input, OptionList
ROWS = [
("lane", "swimmer", "country", "time"),
(4, "Joseph Schooling", "Singapore", 50.39),
(2, "Michael Phelps", "United States", 51.14),
(5, "Chad le Clos", "South Africa", 51.14),
(6, "László Cseh", "Hungary", 51.14),
(3, "Li Zhuhao", "China", 51.26),
(8, "Mehdy Metella", "France", 51.58),
(7, "Tom Shields", "United States", 51.73),
(1, "Aleksandr Sadovnikov", "Russia", 51.84),
(10, "Darren Burns", "Scotland", 51.84),
]
class EditWidgetScreen(ModalScreen):
"""A modal screen with a single input widget."""
CSS = """
Input {
border: solid $secondary-darken-3;
padding: 0;
&:focus {
border: round $secondary;
}
}
"""
def __init__(self, value: Any, region: Region, *args, **kwargs) -> None:
"""Initialization.
Args:
value (Any): the original value.
region (Region): the region available for the input widget contents.
"""
super().__init__(*args, **kwargs)
self.value = value
# store type to later cast the new value to the old type
self.value_type = type(value)
self.widget_region = region
def compose(self) -> ComposeResult:
yield Input(value=str(self.value))
def on_mount(self) -> None:
"""Calculate and set the input widget's position and size.
This takes into account any padding you might have set on the input
widget, although the default padding is 0.
"""
input = self.query_one(Input)
input.offset = Offset(
self.widget_region.offset.x - input.styles.padding.left - 1,
self.widget_region.offset.y - input.styles.padding.top - 1,
)
input.styles.width = (
self.widget_region.width
+ input.styles.padding.left
+ input.styles.padding.right
# include the borders _and_ the cursor at the end of the line
+ 3
)
input.styles.height = (
self.widget_region.height
+ input.styles.padding.top
+ input.styles.padding.bottom
# include the borders
+ 2
)
def on_input_submitted(self, event: Input.Submitted) -> None:
"""Return the new value.
The new value is cast to the original type. If that is not possible
(e.g. you try to replace a number with a string), returns None to
indicate that the cell should _not_ be updated.
"""
try:
self.dismiss(self.value_type(event.value))
except ValueError:
self.dismiss(None)
class EditableDataTable(DataTable):
"""A datatable where you can edit cells."""
BINDINGS = [("e", "edit", "Edit Cell")]
def action_edit(self) -> None:
self.edit_cell(coordinate=self.cursor_coordinate)
@work()
async def edit_cell(self, coordinate: Coordinate) -> None:
"""Edit cell contents.
Args:
coordinate (Coordinate): the coordinate of the cell to update.
"""
region = self._get_cell_region(coordinate)
# the region containing the cell contents, without padding
contents_region = Region(
region.x + self.cell_padding,
region.y,
region.width - 2 * self.cell_padding,
region.height,
)
absolute_offset = self.screen.get_offset(self)
absolute_region = contents_region.translate(absolute_offset)
new_value = await self.app.push_screen_wait(
EditWidgetScreen(value=self.get_cell_at(coordinate), region=absolute_region)
)
if new_value is not None:
self.update_cell_at(coordinate, new_value, update_width=True)
class TableApp(App):
def compose(self) -> ComposeResult:
yield Footer()
with Horizontal():
yield OptionList("One", "Two", "Three")
yield EditableDataTable()
def on_mount(self) -> None:
table = self.query_one(EditableDataTable)
table.add_columns(*ROWS[0])
table.add_rows(ROWS[1:])
app = TableApp()
if __name__ == "__main__":
app.run()