dataframe-textual 0.3.2__py3-none-any.whl → 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dataframe_textual/__init__.py +1 -2
- dataframe_textual/__main__.py +32 -12
- dataframe_textual/common.py +348 -91
- dataframe_textual/data_frame_help_panel.py +22 -4
- dataframe_textual/data_frame_table.py +2188 -632
- dataframe_textual/data_frame_viewer.py +198 -159
- dataframe_textual/table_screen.py +271 -86
- dataframe_textual/yes_no_screen.py +428 -163
- dataframe_textual-1.2.0.dist-info/METADATA +756 -0
- dataframe_textual-1.2.0.dist-info/RECORD +13 -0
- dataframe_textual-1.2.0.dist-info/entry_points.txt +2 -0
- dataframe_textual-0.3.2.dist-info/METADATA +0 -548
- dataframe_textual-0.3.2.dist-info/RECORD +0 -13
- dataframe_textual-0.3.2.dist-info/entry_points.txt +0 -2
- {dataframe_textual-0.3.2.dist-info → dataframe_textual-1.2.0.dist-info}/WHEEL +0 -0
- {dataframe_textual-0.3.2.dist-info → dataframe_textual-1.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
"""Modal screens with Yes/No buttons and their specialized variants."""
|
|
2
2
|
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .data_frame_table import DataFrameTable
|
|
7
|
+
|
|
3
8
|
import polars as pl
|
|
4
9
|
from textual.app import ComposeResult
|
|
5
|
-
from textual.containers import Horizontal
|
|
10
|
+
from textual.containers import Container, Horizontal
|
|
6
11
|
from textual.screen import ModalScreen
|
|
7
|
-
from textual.widgets import Button, Input, Label, Static
|
|
12
|
+
from textual.widgets import Button, Checkbox, Input, Label, Static
|
|
8
13
|
|
|
9
|
-
from .common import DtypeConfig,
|
|
14
|
+
from .common import NULL, DtypeConfig, tentative_expr, validate_expr
|
|
10
15
|
|
|
11
16
|
|
|
12
17
|
class YesNoScreen(ModalScreen):
|
|
@@ -25,26 +30,57 @@ class YesNoScreen(ModalScreen):
|
|
|
25
30
|
|
|
26
31
|
YesNoScreen > Static {
|
|
27
32
|
width: auto;
|
|
28
|
-
min-width:
|
|
33
|
+
min-width: 40;
|
|
29
34
|
max-width: 60;
|
|
30
35
|
height: auto;
|
|
31
|
-
border:
|
|
36
|
+
border: heavy $primary;
|
|
37
|
+
border-title-color: $primary-lighten-3;
|
|
32
38
|
background: $surface;
|
|
33
|
-
padding: 2;
|
|
39
|
+
padding: 1 2;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
YesNoScreen Container {
|
|
43
|
+
margin: 1 0 0 0;
|
|
44
|
+
height: auto;
|
|
45
|
+
width: 100%;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
YesNoScreen Label {
|
|
49
|
+
width: 100%;
|
|
50
|
+
text-wrap: wrap;
|
|
34
51
|
}
|
|
35
52
|
|
|
36
53
|
YesNoScreen Input {
|
|
37
|
-
margin: 1 0;
|
|
54
|
+
margin: 1 0 0 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
YesNoScreen Input:blur {
|
|
58
|
+
border: solid $secondary;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
YesNoScreen #checkbox-container {
|
|
62
|
+
margin: 1 0 0 0;
|
|
63
|
+
height: auto;
|
|
64
|
+
align: left middle;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
YesNoScreen Checkbox {
|
|
68
|
+
margin: 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
YesNoScreen Checkbox:blur {
|
|
72
|
+
border: solid $secondary;
|
|
38
73
|
}
|
|
39
74
|
|
|
40
75
|
YesNoScreen #button-container {
|
|
76
|
+
margin: 1 0 0 0;
|
|
41
77
|
width: 100%;
|
|
42
78
|
height: 3;
|
|
43
79
|
align: center middle;
|
|
44
80
|
}
|
|
45
81
|
|
|
46
82
|
YesNoScreen Button {
|
|
47
|
-
margin: 0
|
|
83
|
+
margin: 0 2;
|
|
48
84
|
}
|
|
49
85
|
"""
|
|
50
86
|
|
|
@@ -53,53 +89,126 @@ class YesNoScreen(ModalScreen):
|
|
|
53
89
|
title: str = None,
|
|
54
90
|
label: str | dict | Label = None,
|
|
55
91
|
input: str | dict | Input = None,
|
|
92
|
+
label2: str | dict | Label = None,
|
|
93
|
+
input2: str | dict | Input = None,
|
|
94
|
+
checkbox: str | dict | Checkbox = None,
|
|
95
|
+
checkbox2: str | dict | Checkbox = None,
|
|
56
96
|
yes: str | dict | Button = "Yes",
|
|
97
|
+
maybe: str | dict | Button = None,
|
|
57
98
|
no: str | dict | Button = "No",
|
|
58
99
|
on_yes_callback=None,
|
|
59
|
-
|
|
100
|
+
on_maybe_callback=None,
|
|
101
|
+
) -> None:
|
|
60
102
|
"""Initialize the modal screen.
|
|
61
103
|
|
|
104
|
+
Creates a customizable Yes/No dialog with optional input fields, labels, and checkboxes.
|
|
105
|
+
|
|
62
106
|
Args:
|
|
63
|
-
title: The title to display in the border
|
|
64
|
-
label: Optional label to display below title as a Label
|
|
65
|
-
input: Optional input value to pre-fill
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
107
|
+
title: The title to display in the border. Defaults to None.
|
|
108
|
+
label: Optional label to display below title as a Label. Defaults to None.
|
|
109
|
+
input: Optional input widget or value to pre-fill. If None, no Input is shown. Defaults to None.
|
|
110
|
+
label2: Optional second label widget. Defaults to None.
|
|
111
|
+
input2: Optional second input widget or value. Defaults to None.
|
|
112
|
+
checkbox: Optional checkbox widget or label. Defaults to None.
|
|
113
|
+
checkbox2: Optional second checkbox widget or label. Defaults to None.
|
|
114
|
+
yes: Text or dict for the Yes button. If None, hides the Yes button. Defaults to "Yes".
|
|
115
|
+
maybe: Optional Maybe button text/dict. Defaults to None.
|
|
116
|
+
no: Text or dict for the No button. If None, hides the No button. Defaults to "No".
|
|
117
|
+
on_yes_callback: Optional callable that takes no args and returns the value to dismiss with when Yes is pressed. Defaults to None.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
None
|
|
69
121
|
"""
|
|
70
122
|
super().__init__()
|
|
71
123
|
self.title = title
|
|
72
124
|
self.label = label
|
|
73
125
|
self.input = input
|
|
126
|
+
self.label2 = label2
|
|
127
|
+
self.input2 = input2
|
|
128
|
+
self.checkbox = checkbox
|
|
129
|
+
self.checkbox2 = checkbox2
|
|
74
130
|
self.yes = yes
|
|
131
|
+
self.maybe = maybe
|
|
75
132
|
self.no = no
|
|
76
133
|
self.on_yes_callback = on_yes_callback
|
|
134
|
+
self.on_maybe_callback = on_maybe_callback
|
|
77
135
|
|
|
78
136
|
def compose(self) -> ComposeResult:
|
|
137
|
+
"""Compose the modal screen widget structure.
|
|
138
|
+
|
|
139
|
+
Builds the widget hierarchy with optional title, labels, inputs, checkboxes,
|
|
140
|
+
and action buttons based on initialization parameters.
|
|
141
|
+
|
|
142
|
+
Yields:
|
|
143
|
+
Widget: The components of the modal screen in rendering order.
|
|
144
|
+
"""
|
|
79
145
|
with Static(id="modal-container") as container:
|
|
80
146
|
if self.title:
|
|
81
147
|
container.border_title = self.title
|
|
82
148
|
|
|
83
|
-
if self.label:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
149
|
+
if self.label or self.input:
|
|
150
|
+
with Container(id="input-container"):
|
|
151
|
+
if self.label:
|
|
152
|
+
if isinstance(self.label, Label):
|
|
153
|
+
pass
|
|
154
|
+
elif isinstance(self.label, dict):
|
|
155
|
+
self.label = Label(**self.label)
|
|
156
|
+
else:
|
|
157
|
+
self.label = Label(self.label)
|
|
158
|
+
yield self.label
|
|
159
|
+
|
|
160
|
+
if self.input:
|
|
161
|
+
if isinstance(self.input, Input):
|
|
162
|
+
pass
|
|
163
|
+
elif isinstance(self.input, dict):
|
|
164
|
+
self.input = Input(**self.input)
|
|
165
|
+
else:
|
|
166
|
+
self.input = Input(self.input)
|
|
167
|
+
self.input.select_all()
|
|
168
|
+
yield self.input
|
|
169
|
+
|
|
170
|
+
if self.label2 or self.input2:
|
|
171
|
+
with Container(id="input-container-2"):
|
|
172
|
+
if self.label2:
|
|
173
|
+
if isinstance(self.label2, Label):
|
|
174
|
+
pass
|
|
175
|
+
elif isinstance(self.label2, dict):
|
|
176
|
+
self.label2 = Label(**self.label2)
|
|
177
|
+
else:
|
|
178
|
+
self.label2 = Label(self.label2)
|
|
179
|
+
yield self.label2
|
|
180
|
+
|
|
181
|
+
if self.input2:
|
|
182
|
+
if isinstance(self.input2, Input):
|
|
183
|
+
pass
|
|
184
|
+
elif isinstance(self.input2, dict):
|
|
185
|
+
self.input2 = Input(**self.input2)
|
|
186
|
+
else:
|
|
187
|
+
self.input2 = Input(self.input2)
|
|
188
|
+
self.input2.select_all()
|
|
189
|
+
yield self.input2
|
|
190
|
+
|
|
191
|
+
if self.checkbox or self.checkbox2:
|
|
192
|
+
with Horizontal(id="checkbox-container"):
|
|
193
|
+
if self.checkbox:
|
|
194
|
+
if isinstance(self.checkbox, Checkbox):
|
|
195
|
+
pass
|
|
196
|
+
elif isinstance(self.checkbox, dict):
|
|
197
|
+
self.checkbox = Checkbox(**self.checkbox)
|
|
198
|
+
else:
|
|
199
|
+
self.checkbox = Checkbox(self.checkbox)
|
|
200
|
+
yield self.checkbox
|
|
201
|
+
|
|
202
|
+
if self.checkbox2:
|
|
203
|
+
if isinstance(self.checkbox2, Checkbox):
|
|
204
|
+
pass
|
|
205
|
+
elif isinstance(self.checkbox2, dict):
|
|
206
|
+
self.checkbox2 = Checkbox(**self.checkbox2)
|
|
207
|
+
else:
|
|
208
|
+
self.checkbox2 = Checkbox(self.checkbox2)
|
|
209
|
+
yield self.checkbox2
|
|
210
|
+
|
|
211
|
+
if self.yes or self.no or self.maybe:
|
|
103
212
|
with Horizontal(id="button-container"):
|
|
104
213
|
if self.yes:
|
|
105
214
|
if isinstance(self.yes, Button):
|
|
@@ -110,6 +219,17 @@ class YesNoScreen(ModalScreen):
|
|
|
110
219
|
self.yes = Button(self.yes, id="yes", variant="success")
|
|
111
220
|
|
|
112
221
|
yield self.yes
|
|
222
|
+
|
|
223
|
+
if self.maybe:
|
|
224
|
+
if isinstance(self.maybe, Button):
|
|
225
|
+
pass
|
|
226
|
+
elif isinstance(self.maybe, dict):
|
|
227
|
+
self.maybe = Button(**self.maybe, id="maybe", variant="warning")
|
|
228
|
+
else:
|
|
229
|
+
self.maybe = Button(self.maybe, id="maybe", variant="warning")
|
|
230
|
+
|
|
231
|
+
yield self.maybe
|
|
232
|
+
|
|
113
233
|
if self.no:
|
|
114
234
|
if isinstance(self.no, Button):
|
|
115
235
|
pass
|
|
@@ -123,6 +243,8 @@ class YesNoScreen(ModalScreen):
|
|
|
123
243
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
124
244
|
if event.button.id == "yes":
|
|
125
245
|
self._handle_yes()
|
|
246
|
+
elif event.button.id == "maybe":
|
|
247
|
+
self._handle_maybe()
|
|
126
248
|
elif event.button.id == "no":
|
|
127
249
|
self.dismiss(None)
|
|
128
250
|
|
|
@@ -142,6 +264,14 @@ class YesNoScreen(ModalScreen):
|
|
|
142
264
|
else:
|
|
143
265
|
self.dismiss(True)
|
|
144
266
|
|
|
267
|
+
def _handle_maybe(self) -> None:
|
|
268
|
+
"""Handle Maybe button press."""
|
|
269
|
+
if self.on_maybe_callback:
|
|
270
|
+
result = self.on_maybe_callback()
|
|
271
|
+
self.dismiss(result)
|
|
272
|
+
else:
|
|
273
|
+
self.dismiss(False)
|
|
274
|
+
|
|
145
275
|
|
|
146
276
|
class SaveFileScreen(YesNoScreen):
|
|
147
277
|
"""Modal screen to save the dataframe to a CSV file."""
|
|
@@ -157,9 +287,9 @@ class SaveFileScreen(YesNoScreen):
|
|
|
157
287
|
|
|
158
288
|
def handle_save(self):
|
|
159
289
|
if self.input:
|
|
160
|
-
|
|
161
|
-
if
|
|
162
|
-
return
|
|
290
|
+
input_filename = self.input.value.strip()
|
|
291
|
+
if input_filename:
|
|
292
|
+
return input_filename
|
|
163
293
|
else:
|
|
164
294
|
self.notify("Filename cannot be empty", title="Save", severity="error")
|
|
165
295
|
return None
|
|
@@ -172,64 +302,126 @@ class ConfirmScreen(YesNoScreen):
|
|
|
172
302
|
|
|
173
303
|
CSS = YesNoScreen.DEFAULT_CSS.replace("YesNoScreen", "ConfirmScreen")
|
|
174
304
|
|
|
175
|
-
def __init__(self, title: str):
|
|
305
|
+
def __init__(self, title: str, label=None, yes="Yes", maybe: str = None, no="No"):
|
|
176
306
|
super().__init__(
|
|
177
307
|
title=title,
|
|
178
|
-
|
|
308
|
+
label=label,
|
|
309
|
+
yes=yes,
|
|
310
|
+
maybe=maybe,
|
|
311
|
+
no=no,
|
|
312
|
+
on_yes_callback=self.handle_yes,
|
|
179
313
|
)
|
|
180
314
|
|
|
181
|
-
def
|
|
315
|
+
def handle_yes(self) -> bool:
|
|
182
316
|
return True
|
|
183
317
|
|
|
318
|
+
def handle_maybe(self) -> bool:
|
|
319
|
+
return False
|
|
320
|
+
|
|
184
321
|
|
|
185
322
|
class EditCellScreen(YesNoScreen):
|
|
186
323
|
"""Modal screen to edit a single cell value."""
|
|
187
324
|
|
|
188
325
|
CSS = YesNoScreen.DEFAULT_CSS.replace("YesNoScreen", "EditCellScreen")
|
|
189
326
|
|
|
190
|
-
def __init__(self,
|
|
191
|
-
self.
|
|
192
|
-
self.
|
|
193
|
-
self.
|
|
194
|
-
self.col_dtype = str(df.dtypes[col_idx])
|
|
195
|
-
|
|
196
|
-
row_idx = int(row_key.value) - 1 # Convert to 0-based
|
|
327
|
+
def __init__(self, ridx: int, cidx: int, df: pl.DataFrame):
|
|
328
|
+
self.ridx = ridx
|
|
329
|
+
self.cidx = cidx
|
|
330
|
+
self.dtype = df.dtypes[cidx]
|
|
197
331
|
|
|
198
332
|
# Label
|
|
199
|
-
content = f"[$
|
|
333
|
+
content = f"[$success]{df.columns[cidx]}[/] ([$accent]{self.dtype}[/])"
|
|
200
334
|
|
|
201
335
|
# Input
|
|
202
|
-
df_value = df.item(
|
|
203
|
-
self.input_value =
|
|
336
|
+
df_value = df.item(ridx, cidx)
|
|
337
|
+
self.input_value = "" if df_value is None else str(df_value).strip()
|
|
204
338
|
|
|
205
339
|
super().__init__(
|
|
206
340
|
title="Edit Cell",
|
|
207
341
|
label=content,
|
|
208
342
|
input={
|
|
209
343
|
"value": self.input_value,
|
|
210
|
-
"type": DtypeConfig(self.
|
|
344
|
+
"type": DtypeConfig(self.dtype).itype,
|
|
211
345
|
},
|
|
212
|
-
on_yes_callback=self.
|
|
346
|
+
on_yes_callback=self._validate_input,
|
|
213
347
|
)
|
|
214
348
|
|
|
215
|
-
def
|
|
349
|
+
def _validate_input(self) -> None:
|
|
216
350
|
"""Validate and save the edited value."""
|
|
217
351
|
new_value_str = self.input.value.strip()
|
|
218
352
|
|
|
353
|
+
# Handle empty input
|
|
354
|
+
if not new_value_str:
|
|
355
|
+
new_value = None
|
|
356
|
+
self.notify(
|
|
357
|
+
"Empty value provided. If you want to clear the cell, press [$accent]x[/].",
|
|
358
|
+
title="Edit",
|
|
359
|
+
severity="warning",
|
|
360
|
+
)
|
|
219
361
|
# Check if value changed
|
|
220
|
-
|
|
362
|
+
elif new_value_str == self.input_value:
|
|
363
|
+
new_value = None
|
|
221
364
|
self.notify("No changes made", title="Edit", severity="warning")
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
365
|
+
else:
|
|
366
|
+
# Parse and validate based on column dtype
|
|
367
|
+
try:
|
|
368
|
+
new_value = DtypeConfig(self.dtype).convert(new_value_str)
|
|
369
|
+
except Exception as e:
|
|
370
|
+
self.notify(
|
|
371
|
+
f"Failed to convert [$accent]{new_value_str}[/] to [$error]{self.dtype}[/]: {str(e)}",
|
|
372
|
+
title="Edit",
|
|
373
|
+
severity="error",
|
|
374
|
+
)
|
|
375
|
+
return None
|
|
230
376
|
|
|
231
377
|
# New value
|
|
232
|
-
return self.
|
|
378
|
+
return self.ridx, self.cidx, new_value
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class RenameColumnScreen(YesNoScreen):
|
|
382
|
+
"""Modal screen to rename a column."""
|
|
383
|
+
|
|
384
|
+
CSS = YesNoScreen.DEFAULT_CSS.replace("YesNoScreen", "RenameColumnScreen")
|
|
385
|
+
|
|
386
|
+
def __init__(self, col_idx: int, col_name: str, existing_columns: list[str]):
|
|
387
|
+
self.col_idx = col_idx
|
|
388
|
+
self.col_name = col_name
|
|
389
|
+
self.existing_columns = [c for c in existing_columns if c != col_name]
|
|
390
|
+
|
|
391
|
+
# Label
|
|
392
|
+
content = f"Rename header [$success]{col_name}[/]"
|
|
393
|
+
|
|
394
|
+
super().__init__(
|
|
395
|
+
title="Rename Column",
|
|
396
|
+
label=content,
|
|
397
|
+
input={"value": col_name},
|
|
398
|
+
on_yes_callback=self._validate_input,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
def _validate_input(self) -> None:
|
|
402
|
+
"""Validate and save the new column name."""
|
|
403
|
+
new_name = self.input.value.strip()
|
|
404
|
+
|
|
405
|
+
# Check if name is empty
|
|
406
|
+
if not new_name:
|
|
407
|
+
self.notify("Column name cannot be empty", title="Rename", severity="error")
|
|
408
|
+
|
|
409
|
+
# Check if name changed
|
|
410
|
+
elif new_name == self.col_name:
|
|
411
|
+
self.notify("No changes made", title="Rename", severity="warning")
|
|
412
|
+
new_name = None
|
|
413
|
+
|
|
414
|
+
# Check if name already exists
|
|
415
|
+
elif new_name in self.existing_columns:
|
|
416
|
+
self.notify(
|
|
417
|
+
f"Column [$accent]{new_name}[/] already exists",
|
|
418
|
+
title="Rename",
|
|
419
|
+
severity="error",
|
|
420
|
+
)
|
|
421
|
+
new_name = None
|
|
422
|
+
|
|
423
|
+
# Return new name
|
|
424
|
+
return self.col_idx, self.col_name, new_name
|
|
233
425
|
|
|
234
426
|
|
|
235
427
|
class SearchScreen(YesNoScreen):
|
|
@@ -237,34 +429,39 @@ class SearchScreen(YesNoScreen):
|
|
|
237
429
|
|
|
238
430
|
CSS = YesNoScreen.DEFAULT_CSS.replace("YesNoScreen", "SearchScreen")
|
|
239
431
|
|
|
240
|
-
def __init__(
|
|
241
|
-
self
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
432
|
+
def __init__(self, title, term, df: pl.DataFrame, cidx: int):
|
|
433
|
+
self.cidx = cidx
|
|
434
|
+
|
|
435
|
+
EXPR = f"ABC, (?i)abc, ^abc$, {NULL}, $_ > 50, $1 < $HP, $_.str.contains('sub')"
|
|
436
|
+
|
|
437
|
+
if "Search" in title:
|
|
438
|
+
col_name = df.columns[cidx]
|
|
439
|
+
col_dtype = df.dtypes[cidx]
|
|
440
|
+
label = f"{title} in [$success]{col_name}[/] ([$warning]{col_dtype}[/]) with value or Polars expression, e.g., {EXPR}"
|
|
441
|
+
else:
|
|
442
|
+
label = f"{title} by value or Polars expression, e.g., {EXPR}"
|
|
250
443
|
|
|
251
444
|
super().__init__(
|
|
252
|
-
title=
|
|
253
|
-
label=
|
|
254
|
-
input=
|
|
255
|
-
|
|
445
|
+
title=title,
|
|
446
|
+
label=label,
|
|
447
|
+
input=term,
|
|
448
|
+
checkbox="Match Nocase",
|
|
449
|
+
checkbox2="Match Whole",
|
|
450
|
+
on_yes_callback=self._validate_input,
|
|
256
451
|
)
|
|
257
452
|
|
|
258
|
-
def
|
|
259
|
-
"""
|
|
453
|
+
def _validate_input(self) -> tuple[str, int, bool, bool]:
|
|
454
|
+
"""Validate the input and return it."""
|
|
260
455
|
term = self.input.value.strip()
|
|
261
456
|
|
|
262
457
|
if not term:
|
|
263
|
-
self.notify("
|
|
458
|
+
self.notify("Term cannot be empty", title=self.title, severity="error")
|
|
264
459
|
return
|
|
265
460
|
|
|
266
|
-
|
|
267
|
-
|
|
461
|
+
match_nocase = self.checkbox.value
|
|
462
|
+
match_whole = self.checkbox2.value
|
|
463
|
+
|
|
464
|
+
return term, self.cidx, match_nocase, match_whole
|
|
268
465
|
|
|
269
466
|
|
|
270
467
|
class FilterScreen(YesNoScreen):
|
|
@@ -272,49 +469,25 @@ class FilterScreen(YesNoScreen):
|
|
|
272
469
|
|
|
273
470
|
CSS = YesNoScreen.DEFAULT_CSS.replace("YesNoScreen", "FilterScreen")
|
|
274
471
|
|
|
275
|
-
def __init__(
|
|
276
|
-
self,
|
|
277
|
-
df: pl.DataFrame,
|
|
278
|
-
current_col_idx: int | None = None,
|
|
279
|
-
current_cell_value: str | None = None,
|
|
280
|
-
):
|
|
472
|
+
def __init__(self, df: pl.DataFrame, cidx: int, input_value: str | None = None):
|
|
281
473
|
self.df = df
|
|
282
|
-
self.
|
|
474
|
+
self.cidx = cidx
|
|
283
475
|
super().__init__(
|
|
284
476
|
title="Filter by Expression",
|
|
285
|
-
label="e.g., $1 > 50, $name == 'text', $_ > 100, $a < $b",
|
|
286
|
-
input=
|
|
287
|
-
|
|
477
|
+
label="e.g., NULL, $1 > 50, $name == 'text', $_ > 100, $a < $b, $_.str.contains('sub')",
|
|
478
|
+
input=input_value,
|
|
479
|
+
checkbox="Match Nocase",
|
|
480
|
+
checkbox2="Match Whole",
|
|
481
|
+
on_yes_callback=self._get_input,
|
|
288
482
|
)
|
|
289
483
|
|
|
290
|
-
def
|
|
291
|
-
"""
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
# Try to parse the expression to ensure it's valid
|
|
296
|
-
expr_str = parse_filter_expression(
|
|
297
|
-
expression, self.df, self.current_col_idx
|
|
298
|
-
)
|
|
299
|
-
|
|
300
|
-
try:
|
|
301
|
-
# Test the expression by evaluating it
|
|
302
|
-
expr = eval(expr_str, {"pl": pl})
|
|
303
|
-
|
|
304
|
-
# Expression is valid
|
|
305
|
-
return expr, expr_str
|
|
306
|
-
except Exception as e:
|
|
307
|
-
self.notify(
|
|
308
|
-
f"Error evaluating expression: {str(e)}",
|
|
309
|
-
title="Filter",
|
|
310
|
-
severity="error",
|
|
311
|
-
)
|
|
312
|
-
except ValueError as ve:
|
|
313
|
-
self.notify(
|
|
314
|
-
f"Invalid expression: {str(ve)}", title="Filter", severity="error"
|
|
315
|
-
)
|
|
484
|
+
def _get_input(self) -> tuple[str, int, bool, bool]:
|
|
485
|
+
"""Get input."""
|
|
486
|
+
term = self.input.value.strip()
|
|
487
|
+
match_nocase = self.checkbox.value
|
|
488
|
+
match_whole = self.checkbox2.value
|
|
316
489
|
|
|
317
|
-
return
|
|
490
|
+
return term, self.cidx, match_nocase, match_whole
|
|
318
491
|
|
|
319
492
|
|
|
320
493
|
class FreezeScreen(YesNoScreen):
|
|
@@ -327,58 +500,28 @@ class FreezeScreen(YesNoScreen):
|
|
|
327
500
|
|
|
328
501
|
def __init__(self):
|
|
329
502
|
super().__init__(
|
|
330
|
-
title="Pin Rows
|
|
331
|
-
label="Enter number of fixed rows
|
|
332
|
-
input="
|
|
333
|
-
|
|
503
|
+
title="Pin Rows / Columns",
|
|
504
|
+
label="Enter number of fixed rows",
|
|
505
|
+
input={"value": "0", "type": "number"},
|
|
506
|
+
label2="Enter number of fixed columns",
|
|
507
|
+
input2={"value": "0", "type": "number"},
|
|
508
|
+
on_yes_callback=self._get_input,
|
|
334
509
|
)
|
|
335
510
|
|
|
336
|
-
def
|
|
511
|
+
def _get_input(self) -> tuple[int, int] | None:
|
|
337
512
|
"""Parse and validate the pin input.
|
|
338
513
|
|
|
339
514
|
Returns:
|
|
340
515
|
Tuple of (fixed_rows, fixed_columns) or None if invalid.
|
|
341
516
|
"""
|
|
342
|
-
|
|
517
|
+
fixed_rows = int(self.input.value.strip())
|
|
518
|
+
fixed_cols = int(self.input2.value.strip())
|
|
343
519
|
|
|
344
|
-
if
|
|
345
|
-
self.notify("
|
|
520
|
+
if fixed_rows < 0 or fixed_cols < 0:
|
|
521
|
+
self.notify("Values must be non-negative", title="Pin", severity="error")
|
|
346
522
|
return None
|
|
347
523
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
if len(parts) == 1:
|
|
351
|
-
# Only fixed rows provided
|
|
352
|
-
try:
|
|
353
|
-
fixed_rows = int(parts[0])
|
|
354
|
-
if fixed_rows < 0:
|
|
355
|
-
raise ValueError("must be non-negative")
|
|
356
|
-
return (fixed_rows, 0)
|
|
357
|
-
except ValueError as e:
|
|
358
|
-
self.notify(
|
|
359
|
-
f"Invalid fixed rows value: {str(e)}", title="Pin", severity="error"
|
|
360
|
-
)
|
|
361
|
-
return None
|
|
362
|
-
elif len(parts) == 2:
|
|
363
|
-
# Both fixed rows and columns provided
|
|
364
|
-
try:
|
|
365
|
-
fixed_rows = int(parts[0])
|
|
366
|
-
fixed_cols = int(parts[1])
|
|
367
|
-
if fixed_rows < 0 or fixed_cols < 0:
|
|
368
|
-
raise ValueError("values must be non-negative")
|
|
369
|
-
return (fixed_rows, fixed_cols)
|
|
370
|
-
except ValueError as e:
|
|
371
|
-
self.notify(
|
|
372
|
-
f"Invalid input values: {str(e)}", title="Pin", severity="error"
|
|
373
|
-
)
|
|
374
|
-
return None
|
|
375
|
-
else:
|
|
376
|
-
self.notify(
|
|
377
|
-
"Provide one or two space-separated integers",
|
|
378
|
-
title="Pin",
|
|
379
|
-
severity="error",
|
|
380
|
-
)
|
|
381
|
-
return None
|
|
524
|
+
return fixed_rows, fixed_cols
|
|
382
525
|
|
|
383
526
|
|
|
384
527
|
class OpenFileScreen(YesNoScreen):
|
|
@@ -404,4 +547,126 @@ class OpenFileScreen(YesNoScreen):
|
|
|
404
547
|
self.notify("Filename cannot be empty", title="Open", severity="error")
|
|
405
548
|
return None
|
|
406
549
|
|
|
407
|
-
|
|
550
|
+
|
|
551
|
+
class EditColumnScreen(YesNoScreen):
|
|
552
|
+
"""Modal screen to edit an entire column with an expression."""
|
|
553
|
+
|
|
554
|
+
CSS = YesNoScreen.DEFAULT_CSS.replace("YesNoScreen", "EditColumnScreen")
|
|
555
|
+
|
|
556
|
+
def __init__(self, cidx: int, df: pl.DataFrame):
|
|
557
|
+
self.cidx = cidx
|
|
558
|
+
self.df = df
|
|
559
|
+
super().__init__(
|
|
560
|
+
title="Edit Column",
|
|
561
|
+
label=f"by value or Polars expression, e.g., abc, pl.lit(7), {NULL}, $_ * 2, $1 + $2, $_.str.to_uppercase(), pl.arange(0, pl.len())",
|
|
562
|
+
input="$_",
|
|
563
|
+
on_yes_callback=self._get_input,
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
def _get_input(self) -> tuple[str, int]:
|
|
567
|
+
"""Get input."""
|
|
568
|
+
term = self.input.value.strip()
|
|
569
|
+
return term, self.cidx
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
class AddColumnScreen(YesNoScreen):
|
|
573
|
+
"""Modal screen to add a new column with an expression."""
|
|
574
|
+
|
|
575
|
+
CSS = YesNoScreen.DEFAULT_CSS.replace("YesNoScreen", "AddColumnScreen")
|
|
576
|
+
|
|
577
|
+
def __init__(self, cidx: int, df: pl.DataFrame):
|
|
578
|
+
self.cidx = cidx
|
|
579
|
+
self.df = df
|
|
580
|
+
self.existing_columns = set(df.columns)
|
|
581
|
+
super().__init__(
|
|
582
|
+
title="Add Column",
|
|
583
|
+
label="Enter column name",
|
|
584
|
+
input="column name",
|
|
585
|
+
label2="Enter value or Polars expression, e.g., 123, NULL, $_ * 2",
|
|
586
|
+
input2="column value or expression",
|
|
587
|
+
on_yes_callback=self._get_input,
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
def _get_input(self) -> tuple[int, str, str] | None:
|
|
591
|
+
"""Validate and return the new column configuration."""
|
|
592
|
+
col_name = self.input.value.strip()
|
|
593
|
+
term = self.input2.value.strip()
|
|
594
|
+
|
|
595
|
+
# Validate column name
|
|
596
|
+
if not col_name:
|
|
597
|
+
self.notify("Column name cannot be empty", title="Add Column", severity="error")
|
|
598
|
+
return None
|
|
599
|
+
|
|
600
|
+
if col_name in self.existing_columns:
|
|
601
|
+
self.notify(
|
|
602
|
+
f"Column [$accent]{col_name}[/] already exists",
|
|
603
|
+
title="Add Column",
|
|
604
|
+
severity="error",
|
|
605
|
+
)
|
|
606
|
+
return None
|
|
607
|
+
|
|
608
|
+
if term == NULL:
|
|
609
|
+
return self.cidx, col_name, pl.lit(None)
|
|
610
|
+
elif tentative_expr(term):
|
|
611
|
+
try:
|
|
612
|
+
expr = validate_expr(term, self.df, self.cidx)
|
|
613
|
+
return self.cidx, col_name, expr
|
|
614
|
+
except ValueError as e:
|
|
615
|
+
self.notify(f"Invalid expression [$error]{term}[/]: {str(e)}", title="Add Column", severity="error")
|
|
616
|
+
return None
|
|
617
|
+
else:
|
|
618
|
+
# Treat as literal value
|
|
619
|
+
dtype = self.df.dtypes[self.cidx]
|
|
620
|
+
try:
|
|
621
|
+
value = DtypeConfig(dtype).convert(term)
|
|
622
|
+
return self.cidx, col_name, pl.lit(value)
|
|
623
|
+
except Exception:
|
|
624
|
+
self.notify(
|
|
625
|
+
f"Unable to convert [$accent]{term}[/] to [$warning]{dtype}[/]. Cast to string.",
|
|
626
|
+
title="Add Column",
|
|
627
|
+
severity="warning",
|
|
628
|
+
)
|
|
629
|
+
return self.cidx, col_name, pl.lit(term)
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
class FindReplaceScreen(YesNoScreen):
|
|
633
|
+
"""Modal screen to replace column values with an expression."""
|
|
634
|
+
|
|
635
|
+
CSS = YesNoScreen.DEFAULT_CSS.replace("YesNoScreen", "ReplaceScreen")
|
|
636
|
+
|
|
637
|
+
def __init__(self, dftable: DataFrameTable):
|
|
638
|
+
term_find = str(dftable.cursor_value)
|
|
639
|
+
super().__init__(
|
|
640
|
+
title="Find and Replace",
|
|
641
|
+
label="Find",
|
|
642
|
+
input=term_find,
|
|
643
|
+
label2="Replace with",
|
|
644
|
+
input2="new value or expression",
|
|
645
|
+
checkbox="Match Nocase",
|
|
646
|
+
checkbox2="Match Whole",
|
|
647
|
+
yes="Replace",
|
|
648
|
+
maybe="Replace All",
|
|
649
|
+
no="Cancel",
|
|
650
|
+
on_yes_callback=self._get_input,
|
|
651
|
+
on_maybe_callback=self._get_input_replace_all,
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
def _get_input(self) -> tuple[str, str, bool, bool, bool]:
|
|
655
|
+
"""Get input."""
|
|
656
|
+
term_find = self.input.value.strip()
|
|
657
|
+
term_replace = self.input2.value.strip()
|
|
658
|
+
match_nocase = self.checkbox.value
|
|
659
|
+
match_whole = self.checkbox2.value
|
|
660
|
+
replace_all = False
|
|
661
|
+
|
|
662
|
+
return term_find, term_replace, match_nocase, match_whole, replace_all
|
|
663
|
+
|
|
664
|
+
def _get_input_replace_all(self) -> tuple[str, str, bool, bool, bool]:
|
|
665
|
+
"""Get input for 'Replace All'."""
|
|
666
|
+
term_find = self.input.value.strip()
|
|
667
|
+
term_replace = self.input2.value.strip()
|
|
668
|
+
match_nocase = self.checkbox.value
|
|
669
|
+
match_whole = self.checkbox2.value
|
|
670
|
+
replace_all = True
|
|
671
|
+
|
|
672
|
+
return term_find, term_replace, match_nocase, match_whole, replace_all
|