dataframe-textual 1.5.0__py3-none-any.whl → 1.10.1__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/__main__.py +1 -1
- dataframe_textual/common.py +15 -7
- dataframe_textual/data_frame_table.py +1001 -897
- dataframe_textual/data_frame_viewer.py +313 -101
- dataframe_textual/sql_screen.py +47 -11
- dataframe_textual/table_screen.py +56 -58
- dataframe_textual/yes_no_screen.py +68 -7
- {dataframe_textual-1.5.0.dist-info → dataframe_textual-1.10.1.dist-info}/METADATA +106 -245
- dataframe_textual-1.10.1.dist-info/RECORD +14 -0
- dataframe_textual-1.5.0.dist-info/RECORD +0 -14
- {dataframe_textual-1.5.0.dist-info → dataframe_textual-1.10.1.dist-info}/WHEEL +0 -0
- {dataframe_textual-1.5.0.dist-info → dataframe_textual-1.10.1.dist-info}/entry_points.txt +0 -0
- {dataframe_textual-1.5.0.dist-info → dataframe_textual-1.10.1.dist-info}/licenses/LICENSE +0 -0
dataframe_textual/sql_screen.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Modal screens for Polars sql manipulation"""
|
|
2
2
|
|
|
3
|
+
from functools import partial
|
|
3
4
|
from typing import TYPE_CHECKING
|
|
4
5
|
|
|
5
6
|
if TYPE_CHECKING:
|
|
@@ -46,18 +47,20 @@ class SqlScreen(ModalScreen):
|
|
|
46
47
|
|
|
47
48
|
"""
|
|
48
49
|
|
|
49
|
-
def __init__(self, dftable: "DataFrameTable", on_yes_callback=None) -> None:
|
|
50
|
+
def __init__(self, dftable: "DataFrameTable", on_yes_callback=None, on_maybe_callback=None) -> None:
|
|
50
51
|
"""Initialize the SQL screen."""
|
|
51
52
|
super().__init__()
|
|
52
53
|
self.dftable = dftable # DataFrameTable
|
|
53
54
|
self.df: pl.DataFrame = dftable.df # Polars DataFrame
|
|
54
55
|
self.on_yes_callback = on_yes_callback
|
|
56
|
+
self.on_maybe_callback = on_maybe_callback
|
|
55
57
|
|
|
56
58
|
def compose(self) -> ComposeResult:
|
|
57
59
|
"""Compose the SQL screen widget structure."""
|
|
58
60
|
# Shared by subclasses
|
|
59
61
|
with Horizontal(id="button-container"):
|
|
60
|
-
yield Button("
|
|
62
|
+
yield Button("View", id="yes", variant="success")
|
|
63
|
+
yield Button("Filter", id="maybe", variant="warning")
|
|
61
64
|
yield Button("Cancel", id="no", variant="error")
|
|
62
65
|
|
|
63
66
|
def on_key(self, event) -> None:
|
|
@@ -66,7 +69,16 @@ class SqlScreen(ModalScreen):
|
|
|
66
69
|
self.app.pop_screen()
|
|
67
70
|
event.stop()
|
|
68
71
|
elif event.key == "enter":
|
|
69
|
-
self.
|
|
72
|
+
for button in self.query(Button):
|
|
73
|
+
if button.has_focus:
|
|
74
|
+
if button.id == "yes":
|
|
75
|
+
self._handle_yes()
|
|
76
|
+
elif button.id == "maybe":
|
|
77
|
+
self._handle_maybe()
|
|
78
|
+
break
|
|
79
|
+
else:
|
|
80
|
+
self._handle_yes()
|
|
81
|
+
|
|
70
82
|
event.stop()
|
|
71
83
|
elif event.key == "escape":
|
|
72
84
|
self.dismiss(None)
|
|
@@ -76,6 +88,8 @@ class SqlScreen(ModalScreen):
|
|
|
76
88
|
"""Handle button press events in the SQL screen."""
|
|
77
89
|
if event.button.id == "yes":
|
|
78
90
|
self._handle_yes()
|
|
91
|
+
elif event.button.id == "maybe":
|
|
92
|
+
self._handle_maybe()
|
|
79
93
|
elif event.button.id == "no":
|
|
80
94
|
self.dismiss(None)
|
|
81
95
|
|
|
@@ -87,6 +101,14 @@ class SqlScreen(ModalScreen):
|
|
|
87
101
|
else:
|
|
88
102
|
self.dismiss(True)
|
|
89
103
|
|
|
104
|
+
def _handle_maybe(self) -> None:
|
|
105
|
+
"""Handle Maybe button press."""
|
|
106
|
+
if self.on_maybe_callback:
|
|
107
|
+
result = self.on_maybe_callback()
|
|
108
|
+
self.dismiss(result)
|
|
109
|
+
else:
|
|
110
|
+
self.dismiss(True)
|
|
111
|
+
|
|
90
112
|
|
|
91
113
|
class SimpleSqlScreen(SqlScreen):
|
|
92
114
|
"""Simple SQL query screen."""
|
|
@@ -133,25 +155,35 @@ class SimpleSqlScreen(SqlScreen):
|
|
|
133
155
|
Returns:
|
|
134
156
|
None
|
|
135
157
|
"""
|
|
136
|
-
super().__init__(
|
|
158
|
+
super().__init__(
|
|
159
|
+
dftable,
|
|
160
|
+
on_yes_callback=self.handle_simple,
|
|
161
|
+
on_maybe_callback=partial(self.handle_simple, view=False),
|
|
162
|
+
)
|
|
137
163
|
|
|
138
164
|
def compose(self) -> ComposeResult:
|
|
139
165
|
"""Compose the simple SQL screen widget structure."""
|
|
140
166
|
with Container(id="sql-container") as container:
|
|
141
167
|
container.border_title = "SQL Query"
|
|
142
168
|
yield Label("Select columns (default to all):", id="select-label")
|
|
143
|
-
yield SelectionList(
|
|
169
|
+
yield SelectionList(
|
|
170
|
+
*[Selection(col, col) for col in self.df.columns if col not in self.dftable.hidden_columns],
|
|
171
|
+
id="column-selection",
|
|
172
|
+
)
|
|
144
173
|
yield Label("Where condition (optional)", id="where-label")
|
|
145
174
|
yield Input(placeholder="e.g., age > 30 and height < 180", id="where-input")
|
|
146
175
|
yield from super().compose()
|
|
147
176
|
|
|
148
|
-
def
|
|
177
|
+
def handle_simple(self, view: bool = True) -> None:
|
|
149
178
|
"""Handle Yes button/Enter key press."""
|
|
150
179
|
selections = self.query_one(SelectionList).selected
|
|
151
|
-
|
|
180
|
+
if not selections:
|
|
181
|
+
selections = [col for col in self.df.columns if col not in self.dftable.hidden_columns]
|
|
182
|
+
|
|
183
|
+
columns = ", ".join(f"`{s}`" for s in selections)
|
|
152
184
|
where = self.query_one(Input).value.strip()
|
|
153
185
|
|
|
154
|
-
return columns, where
|
|
186
|
+
return columns, where, view
|
|
155
187
|
|
|
156
188
|
|
|
157
189
|
class AdvancedSqlScreen(SqlScreen):
|
|
@@ -184,7 +216,11 @@ class AdvancedSqlScreen(SqlScreen):
|
|
|
184
216
|
Returns:
|
|
185
217
|
None
|
|
186
218
|
"""
|
|
187
|
-
super().__init__(
|
|
219
|
+
super().__init__(
|
|
220
|
+
dftable,
|
|
221
|
+
on_yes_callback=self.handle_advanced,
|
|
222
|
+
on_maybe_callback=partial(self.handle_advanced, view=False),
|
|
223
|
+
)
|
|
188
224
|
|
|
189
225
|
def compose(self) -> ComposeResult:
|
|
190
226
|
"""Compose the advanced SQL screen widget structure."""
|
|
@@ -197,6 +233,6 @@ class AdvancedSqlScreen(SqlScreen):
|
|
|
197
233
|
)
|
|
198
234
|
yield from super().compose()
|
|
199
235
|
|
|
200
|
-
def
|
|
236
|
+
def handle_advanced(self, view: bool = True) -> None:
|
|
201
237
|
"""Handle Yes button/Enter key press."""
|
|
202
|
-
return self.query_one(TextArea).text.strip()
|
|
238
|
+
return self.query_one(TextArea).text.strip(), view
|
|
@@ -97,60 +97,61 @@ class TableScreen(ModalScreen):
|
|
|
97
97
|
self.build_table()
|
|
98
98
|
event.stop()
|
|
99
99
|
|
|
100
|
-
def
|
|
101
|
-
|
|
102
|
-
) -> None:
|
|
103
|
-
"""Apply filter or highlight action by the selected value.
|
|
100
|
+
def filter_or_view_selected_value(self, cidx_name_value: tuple[int, str, Any] | None, action: str = "view") -> None:
|
|
101
|
+
"""Apply filter or view action by the selected value.
|
|
104
102
|
|
|
105
|
-
Filters or
|
|
103
|
+
Filters or views rows in the main table based on a selected value from
|
|
106
104
|
this table (typically frequency or row detail). Updates the main table's display
|
|
107
105
|
and notifies the user of the action.
|
|
108
106
|
|
|
109
107
|
Args:
|
|
110
|
-
col_name_value: Tuple of (column_name, column_value) to filter/
|
|
111
|
-
action: Either "filter" to hide non-matching rows, or "
|
|
112
|
-
|
|
113
|
-
Returns:
|
|
114
|
-
None
|
|
108
|
+
col_name_value: Tuple of (column_name, column_value) to filter/view by, or None.
|
|
109
|
+
action: Either "filter" to hide non-matching rows, or "view" to show matching rows. Defaults to "view".
|
|
115
110
|
"""
|
|
116
|
-
if
|
|
111
|
+
if cidx_name_value is None:
|
|
117
112
|
return
|
|
118
|
-
col_name, col_value =
|
|
113
|
+
cidx, col_name, col_value = cidx_name_value
|
|
114
|
+
self.log(f"Filtering or viewing by {col_name} == {col_value}")
|
|
119
115
|
|
|
120
116
|
# Handle NULL values
|
|
121
117
|
if col_value == NULL:
|
|
122
118
|
# Create expression for NULL values
|
|
123
119
|
expr = pl.col(col_name).is_null()
|
|
124
|
-
value_display = "[$success]
|
|
120
|
+
value_display = f"[$success]{NULL_DISPLAY}[/]"
|
|
125
121
|
else:
|
|
126
122
|
# Create expression for the selected value
|
|
127
123
|
expr = pl.col(col_name) == col_value
|
|
128
124
|
value_display = f"[$success]{col_value}[/]"
|
|
129
125
|
|
|
130
|
-
|
|
126
|
+
df_filtered = self.dftable.df.with_row_index(RIDX).filter(expr)
|
|
127
|
+
self.log(f"Filtered dataframe has {len(df_filtered)} rows")
|
|
128
|
+
|
|
129
|
+
matched_indices = set(df_filtered[RIDX].to_list())
|
|
130
|
+
if not matched_indices:
|
|
131
|
+
self.notify(
|
|
132
|
+
f"No matches found for [$warning]{col_name}[/] == {value_display}",
|
|
133
|
+
title="No Matches",
|
|
134
|
+
severity="warning",
|
|
135
|
+
)
|
|
136
|
+
return
|
|
131
137
|
|
|
132
138
|
# Apply the action
|
|
133
139
|
if action == "filter":
|
|
134
|
-
# Update
|
|
135
|
-
for i in range(len(self.dftable.visible_rows)):
|
|
136
|
-
self.dftable.visible_rows[i] = i in matched_indices
|
|
137
|
-
title = "Filter"
|
|
138
|
-
message = f"Filtered by [$accent]{col_name}[/] == [$success]{value_display}[/]"
|
|
139
|
-
else: # action == "highlight"
|
|
140
|
-
# Update selected_rows to reflect the highlights
|
|
140
|
+
# Update selections
|
|
141
141
|
for i in range(len(self.dftable.selected_rows)):
|
|
142
142
|
self.dftable.selected_rows[i] = i in matched_indices
|
|
143
|
-
title = "Highlight"
|
|
144
|
-
message = f"Highlighted [$accent]{col_name}[/] == [$success]{value_display}[/]"
|
|
145
143
|
|
|
146
|
-
|
|
147
|
-
|
|
144
|
+
# Update main table display
|
|
145
|
+
self.dftable.do_filter_rows()
|
|
146
|
+
|
|
147
|
+
else: # action == "view"
|
|
148
|
+
# Update visible rows
|
|
149
|
+
expr = [i in matched_indices for i in range(len(self.dftable.df))]
|
|
150
|
+
self.dftable.view_rows((expr, cidx, False, True))
|
|
148
151
|
|
|
149
152
|
# Dismiss the frequency screen
|
|
150
153
|
self.app.pop_screen()
|
|
151
154
|
|
|
152
|
-
self.notify(message, title=title)
|
|
153
|
-
|
|
154
155
|
|
|
155
156
|
class RowDetailScreen(TableScreen):
|
|
156
157
|
"""Modal screen to display a single row's details."""
|
|
@@ -199,30 +200,27 @@ class RowDetailScreen(TableScreen):
|
|
|
199
200
|
|
|
200
201
|
Args:
|
|
201
202
|
event: The key event object.
|
|
202
|
-
|
|
203
|
-
Returns:
|
|
204
|
-
None
|
|
205
203
|
"""
|
|
206
204
|
if event.key == "v":
|
|
207
|
-
#
|
|
208
|
-
self.
|
|
205
|
+
# View the main table by the selected value
|
|
206
|
+
self.filter_or_view_selected_value(self.get_cidx_name_value(), action="view")
|
|
209
207
|
event.stop()
|
|
210
208
|
elif event.key == "quotation_mark": # '"'
|
|
211
|
-
#
|
|
212
|
-
self.
|
|
209
|
+
# Filter the main table by the selected value
|
|
210
|
+
self.filter_or_view_selected_value(self.get_cidx_name_value(), action="filter")
|
|
213
211
|
event.stop()
|
|
214
212
|
elif event.key == "comma":
|
|
215
213
|
event.stop()
|
|
216
214
|
|
|
217
|
-
def
|
|
218
|
-
|
|
219
|
-
if
|
|
215
|
+
def get_cidx_name_value(self) -> tuple[int, str, Any] | None:
|
|
216
|
+
cidx = self.table.cursor_row
|
|
217
|
+
if cidx >= len(self.df.columns):
|
|
220
218
|
return None # Invalid row
|
|
221
219
|
|
|
222
|
-
col_name = self.df.columns[
|
|
223
|
-
col_value = self.df.item(self.ridx,
|
|
220
|
+
col_name = self.df.columns[cidx]
|
|
221
|
+
col_value = self.df.item(self.ridx, cidx)
|
|
224
222
|
|
|
225
|
-
return col_name, col_value
|
|
223
|
+
return cidx, col_name, col_value
|
|
226
224
|
|
|
227
225
|
|
|
228
226
|
class StatisticsScreen(TableScreen):
|
|
@@ -244,14 +242,14 @@ class StatisticsScreen(TableScreen):
|
|
|
244
242
|
|
|
245
243
|
if self.col_idx is None:
|
|
246
244
|
# Dataframe statistics
|
|
247
|
-
self.
|
|
245
|
+
self.build_dataframe_stats()
|
|
248
246
|
self.table.cursor_type = "column"
|
|
249
247
|
else:
|
|
250
248
|
# Column statistics
|
|
251
|
-
self.
|
|
249
|
+
self.build_column_stats()
|
|
252
250
|
self.table.cursor_type = "row"
|
|
253
251
|
|
|
254
|
-
def
|
|
252
|
+
def build_column_stats(self) -> None:
|
|
255
253
|
"""Build statistics for a single column."""
|
|
256
254
|
col_name = self.df.columns[self.col_idx]
|
|
257
255
|
lf = self.df.lazy()
|
|
@@ -292,7 +290,7 @@ class StatisticsScreen(TableScreen):
|
|
|
292
290
|
Text(value, style=dc.style, justify=dc.justify),
|
|
293
291
|
)
|
|
294
292
|
|
|
295
|
-
def
|
|
293
|
+
def build_dataframe_stats(self) -> None:
|
|
296
294
|
"""Build statistics for the entire dataframe."""
|
|
297
295
|
lf = self.df.lazy()
|
|
298
296
|
|
|
@@ -351,16 +349,16 @@ class FrequencyScreen(TableScreen):
|
|
|
351
349
|
|
|
352
350
|
CSS = TableScreen.DEFAULT_CSS.replace("TableScreen", "FrequencyScreen")
|
|
353
351
|
|
|
354
|
-
def __init__(self,
|
|
352
|
+
def __init__(self, cidx: int, dftable: "DataFrameTable") -> None:
|
|
355
353
|
super().__init__(dftable)
|
|
356
|
-
self.
|
|
354
|
+
self.cidx = cidx
|
|
357
355
|
self.sorted_columns = {
|
|
358
356
|
1: True, # Count
|
|
359
357
|
}
|
|
360
358
|
|
|
361
359
|
df = dftable.df.filter(dftable.visible_rows) if False in dftable.visible_rows else dftable.df
|
|
362
360
|
self.total_count = len(df)
|
|
363
|
-
self.df: pl.DataFrame = df[df.columns[self.
|
|
361
|
+
self.df: pl.DataFrame = df[df.columns[self.cidx]].value_counts(sort=True).sort("count", descending=True)
|
|
364
362
|
|
|
365
363
|
def on_mount(self) -> None:
|
|
366
364
|
"""Create the frequency table."""
|
|
@@ -369,19 +367,19 @@ class FrequencyScreen(TableScreen):
|
|
|
369
367
|
def on_key(self, event):
|
|
370
368
|
if event.key == "left_square_bracket": # '['
|
|
371
369
|
# Sort by current column in ascending order
|
|
372
|
-
self.
|
|
370
|
+
self.sort_by_column(descending=False)
|
|
373
371
|
event.stop()
|
|
374
372
|
elif event.key == "right_square_bracket": # ']'
|
|
375
373
|
# Sort by current column in descending order
|
|
376
|
-
self.
|
|
374
|
+
self.sort_by_column(descending=True)
|
|
377
375
|
event.stop()
|
|
378
376
|
elif event.key == "v":
|
|
379
377
|
# Filter the main table by the selected value
|
|
380
|
-
self.
|
|
378
|
+
self.filter_or_view_selected_value(self.get_cidx_name_value(), action="view")
|
|
381
379
|
event.stop()
|
|
382
380
|
elif event.key == "quotation_mark": # '"'
|
|
383
381
|
# Highlight the main table by the selected value
|
|
384
|
-
self.
|
|
382
|
+
self.filter_or_view_selected_value(self.get_cidx_name_value(), action="filter")
|
|
385
383
|
event.stop()
|
|
386
384
|
|
|
387
385
|
def build_table(self) -> None:
|
|
@@ -389,8 +387,8 @@ class FrequencyScreen(TableScreen):
|
|
|
389
387
|
self.table.clear(columns=True)
|
|
390
388
|
|
|
391
389
|
# Create frequency table
|
|
392
|
-
column = self.dftable.df.columns[self.
|
|
393
|
-
dtype = self.dftable.df.dtypes[self.
|
|
390
|
+
column = self.dftable.df.columns[self.cidx]
|
|
391
|
+
dtype = self.dftable.df.dtypes[self.cidx]
|
|
394
392
|
dc = DtypeConfig(dtype)
|
|
395
393
|
|
|
396
394
|
# Add column headers with sort indicators
|
|
@@ -468,7 +466,7 @@ class FrequencyScreen(TableScreen):
|
|
|
468
466
|
key="total",
|
|
469
467
|
)
|
|
470
468
|
|
|
471
|
-
def
|
|
469
|
+
def sort_by_column(self, descending: bool) -> None:
|
|
472
470
|
"""Sort the dataframe by the selected column and refresh the main table."""
|
|
473
471
|
row_idx, col_idx = self.table.cursor_coordinate
|
|
474
472
|
col_sort = col_idx if col_idx == 0 else 1
|
|
@@ -493,15 +491,15 @@ class FrequencyScreen(TableScreen):
|
|
|
493
491
|
# order = "desc" if descending else "asc"
|
|
494
492
|
# self.notify(f"Sorted by [on $primary]{col_name}[/] ({order})", title="Sort")
|
|
495
493
|
|
|
496
|
-
def
|
|
494
|
+
def get_cidx_name_value(self) -> tuple[str, str, str] | None:
|
|
497
495
|
row_idx = self.table.cursor_row
|
|
498
496
|
if row_idx >= len(self.df[:, 0]): # first column
|
|
499
497
|
return None # Skip the last `Total` row
|
|
500
498
|
|
|
501
|
-
col_name = self.dftable.df.columns[self.
|
|
502
|
-
col_dtype = self.dftable.df.dtypes[self.
|
|
499
|
+
col_name = self.dftable.df.columns[self.cidx]
|
|
500
|
+
col_dtype = self.dftable.df.dtypes[self.cidx]
|
|
503
501
|
|
|
504
502
|
cell_value = self.table.get_cell_at(Coordinate(row_idx, 0))
|
|
505
503
|
col_value = NULL if cell_value.plain == NULL_DISPLAY else DtypeConfig(col_dtype).convert(cell_value.plain)
|
|
506
504
|
|
|
507
|
-
return col_name, col_value
|
|
505
|
+
return self.cidx, col_name, col_value
|
|
@@ -5,11 +5,13 @@ from typing import TYPE_CHECKING
|
|
|
5
5
|
if TYPE_CHECKING:
|
|
6
6
|
from .data_frame_table import DataFrameTable
|
|
7
7
|
|
|
8
|
+
|
|
8
9
|
import polars as pl
|
|
9
10
|
from textual.app import ComposeResult
|
|
10
11
|
from textual.containers import Container, Horizontal
|
|
11
12
|
from textual.screen import ModalScreen
|
|
12
|
-
from textual.widgets import Button, Checkbox, Input, Label, Static
|
|
13
|
+
from textual.widgets import Button, Checkbox, Input, Label, Static, TabPane
|
|
14
|
+
from textual.widgets.tabbed_content import ContentTab
|
|
13
15
|
|
|
14
16
|
from .common import NULL, DtypeConfig, tentative_expr, validate_expr
|
|
15
17
|
|
|
@@ -254,7 +256,18 @@ class YesNoScreen(ModalScreen):
|
|
|
254
256
|
def on_key(self, event) -> None:
|
|
255
257
|
"""Handle key press events in the table screen."""
|
|
256
258
|
if event.key == "enter":
|
|
257
|
-
self.
|
|
259
|
+
for button in self.query(Button):
|
|
260
|
+
if button.has_focus:
|
|
261
|
+
if button.id == "yes":
|
|
262
|
+
self._handle_yes()
|
|
263
|
+
elif button.id == "maybe":
|
|
264
|
+
self._handle_maybe()
|
|
265
|
+
elif button.id == "no":
|
|
266
|
+
self.dismiss(None)
|
|
267
|
+
break
|
|
268
|
+
else:
|
|
269
|
+
self._handle_yes()
|
|
270
|
+
|
|
258
271
|
event.stop()
|
|
259
272
|
elif event.key == "escape":
|
|
260
273
|
self.dismiss(None)
|
|
@@ -282,18 +295,26 @@ class SaveFileScreen(YesNoScreen):
|
|
|
282
295
|
|
|
283
296
|
CSS = YesNoScreen.DEFAULT_CSS.replace("YesNoScreen", "SaveFileScreen")
|
|
284
297
|
|
|
285
|
-
def __init__(
|
|
298
|
+
def __init__(
|
|
299
|
+
self, filename: str, title: str = "Save to File", all_tabs: bool | None = None, multi_tab: bool = False
|
|
300
|
+
):
|
|
301
|
+
self.all_tabs = all_tabs or (all_tabs is None and multi_tab)
|
|
286
302
|
super().__init__(
|
|
287
303
|
title=title,
|
|
304
|
+
label="Enter filename",
|
|
288
305
|
input=filename,
|
|
306
|
+
yes="Save",
|
|
307
|
+
maybe="Save All Tabs" if self.all_tabs else None,
|
|
308
|
+
no="Cancel",
|
|
289
309
|
on_yes_callback=self.handle_save,
|
|
310
|
+
on_maybe_callback=self.handle_save,
|
|
290
311
|
)
|
|
291
312
|
|
|
292
313
|
def handle_save(self):
|
|
293
314
|
if self.input:
|
|
294
315
|
input_filename = self.input.value.strip()
|
|
295
316
|
if input_filename:
|
|
296
|
-
return input_filename
|
|
317
|
+
return input_filename, self.all_tabs
|
|
297
318
|
else:
|
|
298
319
|
self.notify("Filename cannot be empty", title="Save", severity="error")
|
|
299
320
|
return None
|
|
@@ -473,13 +494,13 @@ class FilterScreen(YesNoScreen):
|
|
|
473
494
|
|
|
474
495
|
CSS = YesNoScreen.DEFAULT_CSS.replace("YesNoScreen", "FilterScreen")
|
|
475
496
|
|
|
476
|
-
def __init__(self, df: pl.DataFrame, cidx: int,
|
|
497
|
+
def __init__(self, df: pl.DataFrame, cidx: int, term: str | None = None):
|
|
477
498
|
self.df = df
|
|
478
499
|
self.cidx = cidx
|
|
479
500
|
super().__init__(
|
|
480
501
|
title="Filter by Expression",
|
|
481
502
|
label="e.g., NULL, $1 > 50, $name == 'text', $_ > 100, $a < $b, $_.str.contains('sub')",
|
|
482
|
-
input=
|
|
503
|
+
input=term,
|
|
483
504
|
checkbox="Match Nocase",
|
|
484
505
|
checkbox2="Match Whole",
|
|
485
506
|
on_yes_callback=self._get_input,
|
|
@@ -587,7 +608,7 @@ class AddColumnScreen(YesNoScreen):
|
|
|
587
608
|
title="Add Column",
|
|
588
609
|
label="Enter column name",
|
|
589
610
|
input="Link" if link else "Column name",
|
|
590
|
-
label2="Enter link template, e.g., https://example.com/$
|
|
611
|
+
label2="Enter link template, e.g., https://example.com/$_/id/$1, PC/compound/$cid"
|
|
591
612
|
if link
|
|
592
613
|
else "Enter value or Polars expression, e.g., abc, pl.lit(123), NULL, $_ * 2, $1 + $total, $_.str.to_uppercase(), pl.concat_str($_, pl.lit('-suffix'))",
|
|
593
614
|
input2="Link template" if link else "Column value or expression",
|
|
@@ -694,3 +715,43 @@ class FindReplaceScreen(YesNoScreen):
|
|
|
694
715
|
replace_all = True
|
|
695
716
|
|
|
696
717
|
return term_find, term_replace, match_nocase, match_whole, replace_all
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
class RenameTabScreen(YesNoScreen):
|
|
721
|
+
"""Modal screen to rename a tab."""
|
|
722
|
+
|
|
723
|
+
CSS = YesNoScreen.DEFAULT_CSS.replace("YesNoScreen", "RenameTabScreen")
|
|
724
|
+
|
|
725
|
+
def __init__(self, content_tab: ContentTab, existing_tabs: list[TabPane]):
|
|
726
|
+
self.content_tab = content_tab
|
|
727
|
+
self.existing_tabs = existing_tabs
|
|
728
|
+
tab_name = content_tab.label_text
|
|
729
|
+
|
|
730
|
+
super().__init__(
|
|
731
|
+
title="Rename Tab",
|
|
732
|
+
label="New tab name",
|
|
733
|
+
input={"value": tab_name},
|
|
734
|
+
on_yes_callback=self._validate_input,
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
def _validate_input(self) -> None:
|
|
738
|
+
"""Validate and save the new tab name."""
|
|
739
|
+
new_name = self.input.value.strip()
|
|
740
|
+
|
|
741
|
+
# Check if name is empty
|
|
742
|
+
if not new_name:
|
|
743
|
+
self.notify("Tab name cannot be empty", title="Rename Tab", severity="error")
|
|
744
|
+
return None
|
|
745
|
+
|
|
746
|
+
# Check if name changed
|
|
747
|
+
if new_name == self.content_tab.label_text:
|
|
748
|
+
self.notify("No changes made", title="Rename Tab", severity="warning")
|
|
749
|
+
return None
|
|
750
|
+
|
|
751
|
+
# Check if name already exists
|
|
752
|
+
if new_name in self.existing_tabs:
|
|
753
|
+
self.notify(f"Tab [$accent]{new_name}[/] already exists", title="Rename Tab", severity="error")
|
|
754
|
+
return None
|
|
755
|
+
|
|
756
|
+
# Return new name
|
|
757
|
+
return self.content_tab, new_name
|