dataframe-textual 0.1.0__py3-none-any.whl → 1.1.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.
Potentially problematic release.
This version of dataframe-textual might be problematic. Click here for more details.
- dataframe_textual/__main__.py +65 -0
- dataframe_textual/common.py +340 -0
- {dataframe_viewer → dataframe_textual}/data_frame_help_panel.py +22 -4
- dataframe_textual/data_frame_table.py +2768 -0
- dataframe_textual/data_frame_viewer.py +472 -0
- dataframe_textual/table_screen.py +490 -0
- dataframe_textual/yes_no_screen.py +672 -0
- dataframe_textual-1.1.0.dist-info/METADATA +726 -0
- dataframe_textual-1.1.0.dist-info/RECORD +13 -0
- dataframe_textual-1.1.0.dist-info/entry_points.txt +2 -0
- dataframe_textual-0.1.0.dist-info/METADATA +0 -522
- dataframe_textual-0.1.0.dist-info/RECORD +0 -13
- dataframe_textual-0.1.0.dist-info/entry_points.txt +0 -2
- dataframe_viewer/__main__.py +0 -48
- dataframe_viewer/common.py +0 -204
- dataframe_viewer/data_frame_table.py +0 -1395
- dataframe_viewer/data_frame_viewer.py +0 -320
- dataframe_viewer/table_screen.py +0 -311
- dataframe_viewer/yes_no_screen.py +0 -409
- {dataframe_viewer → dataframe_textual}/__init__.py +0 -0
- {dataframe_textual-0.1.0.dist-info → dataframe_textual-1.1.0.dist-info}/WHEEL +0 -0
- {dataframe_textual-0.1.0.dist-info → dataframe_textual-1.1.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
"""Modal screens with Yes/No buttons and their specialized variants."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .data_frame_table import DataFrameTable
|
|
7
|
+
|
|
8
|
+
import polars as pl
|
|
9
|
+
from textual.app import ComposeResult
|
|
10
|
+
from textual.containers import Container, Horizontal
|
|
11
|
+
from textual.screen import ModalScreen
|
|
12
|
+
from textual.widgets import Button, Checkbox, Input, Label, Static
|
|
13
|
+
|
|
14
|
+
from .common import NULL, DtypeConfig, tentative_expr, validate_expr
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class YesNoScreen(ModalScreen):
|
|
18
|
+
"""Reusable modal screen with Yes/No buttons and customizable label and input.
|
|
19
|
+
|
|
20
|
+
This widget handles:
|
|
21
|
+
- Yes/No button responses
|
|
22
|
+
- Enter key for Yes, Escape for No
|
|
23
|
+
- Optional callback function for Yes action
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
DEFAULT_CSS = """
|
|
27
|
+
YesNoScreen {
|
|
28
|
+
align: center middle;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
YesNoScreen > Static {
|
|
32
|
+
width: auto;
|
|
33
|
+
min-width: 40;
|
|
34
|
+
max-width: 60;
|
|
35
|
+
height: auto;
|
|
36
|
+
border: heavy $primary;
|
|
37
|
+
border-title-color: $primary-lighten-3;
|
|
38
|
+
background: $surface;
|
|
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;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
YesNoScreen Input {
|
|
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;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
YesNoScreen #button-container {
|
|
76
|
+
margin: 1 0 0 0;
|
|
77
|
+
width: 100%;
|
|
78
|
+
height: 3;
|
|
79
|
+
align: center middle;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
YesNoScreen Button {
|
|
83
|
+
margin: 0 2;
|
|
84
|
+
}
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
title: str = None,
|
|
90
|
+
label: str | dict | Label = None,
|
|
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,
|
|
96
|
+
yes: str | dict | Button = "Yes",
|
|
97
|
+
maybe: str | dict | Button = None,
|
|
98
|
+
no: str | dict | Button = "No",
|
|
99
|
+
on_yes_callback=None,
|
|
100
|
+
on_maybe_callback=None,
|
|
101
|
+
) -> None:
|
|
102
|
+
"""Initialize the modal screen.
|
|
103
|
+
|
|
104
|
+
Creates a customizable Yes/No dialog with optional input fields, labels, and checkboxes.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
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
|
|
121
|
+
"""
|
|
122
|
+
super().__init__()
|
|
123
|
+
self.title = title
|
|
124
|
+
self.label = label
|
|
125
|
+
self.input = input
|
|
126
|
+
self.label2 = label2
|
|
127
|
+
self.input2 = input2
|
|
128
|
+
self.checkbox = checkbox
|
|
129
|
+
self.checkbox2 = checkbox2
|
|
130
|
+
self.yes = yes
|
|
131
|
+
self.maybe = maybe
|
|
132
|
+
self.no = no
|
|
133
|
+
self.on_yes_callback = on_yes_callback
|
|
134
|
+
self.on_maybe_callback = on_maybe_callback
|
|
135
|
+
|
|
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
|
+
"""
|
|
145
|
+
with Static(id="modal-container") as container:
|
|
146
|
+
if self.title:
|
|
147
|
+
container.border_title = self.title
|
|
148
|
+
|
|
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:
|
|
212
|
+
with Horizontal(id="button-container"):
|
|
213
|
+
if self.yes:
|
|
214
|
+
if isinstance(self.yes, Button):
|
|
215
|
+
pass
|
|
216
|
+
elif isinstance(self.yes, dict):
|
|
217
|
+
self.yes = Button(**self.yes, id="yes", variant="success")
|
|
218
|
+
else:
|
|
219
|
+
self.yes = Button(self.yes, id="yes", variant="success")
|
|
220
|
+
|
|
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
|
+
|
|
233
|
+
if self.no:
|
|
234
|
+
if isinstance(self.no, Button):
|
|
235
|
+
pass
|
|
236
|
+
elif isinstance(self.no, dict):
|
|
237
|
+
self.no = Button(**self.no, id="no", variant="error")
|
|
238
|
+
else:
|
|
239
|
+
self.no = Button(self.no, id="no", variant="error")
|
|
240
|
+
|
|
241
|
+
yield self.no
|
|
242
|
+
|
|
243
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
244
|
+
if event.button.id == "yes":
|
|
245
|
+
self._handle_yes()
|
|
246
|
+
elif event.button.id == "maybe":
|
|
247
|
+
self._handle_maybe()
|
|
248
|
+
elif event.button.id == "no":
|
|
249
|
+
self.dismiss(None)
|
|
250
|
+
|
|
251
|
+
def on_key(self, event) -> None:
|
|
252
|
+
if event.key == "enter":
|
|
253
|
+
self._handle_yes()
|
|
254
|
+
event.stop()
|
|
255
|
+
elif event.key == "escape":
|
|
256
|
+
self.dismiss(None)
|
|
257
|
+
event.stop()
|
|
258
|
+
|
|
259
|
+
def _handle_yes(self) -> None:
|
|
260
|
+
"""Handle Yes button/Enter key press."""
|
|
261
|
+
if self.on_yes_callback:
|
|
262
|
+
result = self.on_yes_callback()
|
|
263
|
+
self.dismiss(result)
|
|
264
|
+
else:
|
|
265
|
+
self.dismiss(True)
|
|
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
|
+
|
|
275
|
+
|
|
276
|
+
class SaveFileScreen(YesNoScreen):
|
|
277
|
+
"""Modal screen to save the dataframe to a CSV file."""
|
|
278
|
+
|
|
279
|
+
CSS = YesNoScreen.DEFAULT_CSS.replace("YesNoScreen", "SaveFileScreen")
|
|
280
|
+
|
|
281
|
+
def __init__(self, filename: str, title="Save Tab"):
|
|
282
|
+
super().__init__(
|
|
283
|
+
title=title,
|
|
284
|
+
input=filename,
|
|
285
|
+
on_yes_callback=self.handle_save,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
def handle_save(self):
|
|
289
|
+
if self.input:
|
|
290
|
+
input_filename = self.input.value.strip()
|
|
291
|
+
if input_filename:
|
|
292
|
+
return input_filename
|
|
293
|
+
else:
|
|
294
|
+
self.notify("Filename cannot be empty", title="Save", severity="error")
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
return None
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
class ConfirmScreen(YesNoScreen):
|
|
301
|
+
"""Modal screen to confirm file overwrite."""
|
|
302
|
+
|
|
303
|
+
CSS = YesNoScreen.DEFAULT_CSS.replace("YesNoScreen", "ConfirmScreen")
|
|
304
|
+
|
|
305
|
+
def __init__(self, title: str, label=None, yes="Yes", maybe: str = None, no="No"):
|
|
306
|
+
super().__init__(
|
|
307
|
+
title=title,
|
|
308
|
+
label=label,
|
|
309
|
+
yes=yes,
|
|
310
|
+
maybe=maybe,
|
|
311
|
+
no=no,
|
|
312
|
+
on_yes_callback=self.handle_yes,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
def handle_yes(self) -> bool:
|
|
316
|
+
return True
|
|
317
|
+
|
|
318
|
+
def handle_maybe(self) -> bool:
|
|
319
|
+
return False
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
class EditCellScreen(YesNoScreen):
|
|
323
|
+
"""Modal screen to edit a single cell value."""
|
|
324
|
+
|
|
325
|
+
CSS = YesNoScreen.DEFAULT_CSS.replace("YesNoScreen", "EditCellScreen")
|
|
326
|
+
|
|
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]
|
|
331
|
+
|
|
332
|
+
# Label
|
|
333
|
+
content = f"[$success]{df.columns[cidx]}[/] ([$accent]{self.dtype}[/])"
|
|
334
|
+
|
|
335
|
+
# Input
|
|
336
|
+
df_value = df.item(ridx, cidx)
|
|
337
|
+
self.input_value = "" if df_value is None else str(df_value).strip()
|
|
338
|
+
|
|
339
|
+
super().__init__(
|
|
340
|
+
title="Edit Cell",
|
|
341
|
+
label=content,
|
|
342
|
+
input={
|
|
343
|
+
"value": self.input_value,
|
|
344
|
+
"type": DtypeConfig(self.dtype).itype,
|
|
345
|
+
},
|
|
346
|
+
on_yes_callback=self._validate_input,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
def _validate_input(self) -> None:
|
|
350
|
+
"""Validate and save the edited value."""
|
|
351
|
+
new_value_str = self.input.value.strip()
|
|
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
|
+
)
|
|
361
|
+
# Check if value changed
|
|
362
|
+
elif new_value_str == self.input_value:
|
|
363
|
+
new_value = None
|
|
364
|
+
self.notify("No changes made", title="Edit", severity="warning")
|
|
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
|
|
376
|
+
|
|
377
|
+
# New value
|
|
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
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
class SearchScreen(YesNoScreen):
|
|
428
|
+
"""Modal screen to search for values in a column."""
|
|
429
|
+
|
|
430
|
+
CSS = YesNoScreen.DEFAULT_CSS.replace("YesNoScreen", "SearchScreen")
|
|
431
|
+
|
|
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}"
|
|
443
|
+
|
|
444
|
+
super().__init__(
|
|
445
|
+
title=title,
|
|
446
|
+
label=label,
|
|
447
|
+
input=term,
|
|
448
|
+
checkbox="Match Nocase",
|
|
449
|
+
checkbox2="Match Whole",
|
|
450
|
+
on_yes_callback=self._validate_input,
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
def _validate_input(self) -> tuple[str, int, bool, bool]:
|
|
454
|
+
"""Validate the input and return it."""
|
|
455
|
+
term = self.input.value.strip()
|
|
456
|
+
|
|
457
|
+
if not term:
|
|
458
|
+
self.notify("Term cannot be empty", title=self.title, severity="error")
|
|
459
|
+
return
|
|
460
|
+
|
|
461
|
+
match_nocase = self.checkbox.value
|
|
462
|
+
match_whole = self.checkbox2.value
|
|
463
|
+
|
|
464
|
+
return term, self.cidx, match_nocase, match_whole
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
class FilterScreen(YesNoScreen):
|
|
468
|
+
"""Modal screen to filter rows by column expression."""
|
|
469
|
+
|
|
470
|
+
CSS = YesNoScreen.DEFAULT_CSS.replace("YesNoScreen", "FilterScreen")
|
|
471
|
+
|
|
472
|
+
def __init__(self, df: pl.DataFrame, cidx: int, input_value: str | None = None):
|
|
473
|
+
self.df = df
|
|
474
|
+
self.cidx = cidx
|
|
475
|
+
super().__init__(
|
|
476
|
+
title="Filter by Expression",
|
|
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,
|
|
482
|
+
)
|
|
483
|
+
|
|
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
|
|
489
|
+
|
|
490
|
+
return term, self.cidx, match_nocase, match_whole
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
class FreezeScreen(YesNoScreen):
|
|
494
|
+
"""Modal screen to pin rows and columns.
|
|
495
|
+
|
|
496
|
+
Accepts one value for fixed rows, or two space-separated values for fixed rows and columns.
|
|
497
|
+
"""
|
|
498
|
+
|
|
499
|
+
CSS = YesNoScreen.DEFAULT_CSS.replace("YesNoScreen", "PinScreen")
|
|
500
|
+
|
|
501
|
+
def __init__(self):
|
|
502
|
+
super().__init__(
|
|
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,
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
def _get_input(self) -> tuple[int, int] | None:
|
|
512
|
+
"""Parse and validate the pin input.
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
Tuple of (fixed_rows, fixed_columns) or None if invalid.
|
|
516
|
+
"""
|
|
517
|
+
fixed_rows = int(self.input.value.strip())
|
|
518
|
+
fixed_cols = int(self.input2.value.strip())
|
|
519
|
+
|
|
520
|
+
if fixed_rows < 0 or fixed_cols < 0:
|
|
521
|
+
self.notify("Values must be non-negative", title="Pin", severity="error")
|
|
522
|
+
return None
|
|
523
|
+
|
|
524
|
+
return fixed_rows, fixed_cols
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
class OpenFileScreen(YesNoScreen):
|
|
528
|
+
"""Modal screen to open a CSV file."""
|
|
529
|
+
|
|
530
|
+
CSS = YesNoScreen.DEFAULT_CSS.replace("YesNoScreen", "OpenFileScreen")
|
|
531
|
+
|
|
532
|
+
def __init__(self):
|
|
533
|
+
super().__init__(
|
|
534
|
+
title="Open File",
|
|
535
|
+
input="Enter relative or absolute file path",
|
|
536
|
+
yes="Open",
|
|
537
|
+
no="Cancel",
|
|
538
|
+
on_yes_callback=self.handle_open,
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
def handle_open(self):
|
|
542
|
+
if self.input:
|
|
543
|
+
filename_input = self.input.value.strip()
|
|
544
|
+
if filename_input:
|
|
545
|
+
return filename_input
|
|
546
|
+
else:
|
|
547
|
+
self.notify("Filename cannot be empty", title="Open", severity="error")
|
|
548
|
+
return None
|
|
549
|
+
|
|
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
|