Skip to content

Commit 071471e

Browse files
authored
Dynamically turn off transfer checkboxes. (#653)
* Dynamically turn off transfer checkboxes. * Add tests. * Add docstrings. * Small fixes and tidy ups. * Fix tests. * First pass full inheritence. * Small fixes. * Fix tests.
1 parent 45d819d commit 071471e

5 files changed

Lines changed: 196 additions & 43 deletions

File tree

datashuttle/tui/custom_widgets.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from datashuttle.tui.app import TuiApp
1818
from datashuttle.tui.interface import Interface
19+
from datashuttle.tui.screens.datatypes import BaseDatatypeCheckboxes
1920

2021
from dataclasses import dataclass
2122
from pathlib import Path
@@ -446,10 +447,14 @@ def get_sub_ses_names_and_datatype(
446447
"""See `handle_fill_input_from_directorytree` for parameters."""
447448
sub_names = self.query_one(sub_input_key).as_names_list()
448449
ses_names = self.query_one(ses_input_key).as_names_list()
449-
datatype = self.query_one("DatatypeCheckboxes").selected_datatypes()
450+
datatype = self.get_datatype_checkbox_widget().selected_datatypes()
450451

451452
return sub_names, ses_names, datatype
452453

454+
def get_datatype_checkbox_widget(self) -> BaseDatatypeCheckboxes:
455+
"""Get the Transfer or Create DatatypeCheckboxes widget."""
456+
raise NotImplementedError
457+
453458

454459
class TopLevelFolderSelect(Select):
455460
"""A Select widget for display and updating of top-level-folders.

datashuttle/tui/screens/datatypes.py

Lines changed: 106 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -178,22 +178,23 @@ def on_selection_list_selection_toggled(
178178

179179

180180
# --------------------------------------------------------------------------------------
181-
# DatatypeCheckboxes
181+
# BaseDatatypeCheckboxes
182182
# --------------------------------------------------------------------------------------
183183

184184

185-
class DatatypeCheckboxes(Static):
185+
class BaseDatatypeCheckboxes(Static):
186186
"""Dynamically-populated checkbox widget for convenient datatype selection.
187187
188-
Parameters
189-
----------
190-
settings_key
191-
'create' if datatype checkboxes for the create tab,
192-
'transfer' for the transfer tab. Transfer tab includes
193-
additional datatype options (e.g. "all").
188+
Base class for a widget that allows the user to select datatypes.
189+
The checkbox names are generated in the persistent_settings
190+
configuration file, which stores all checkbox visible and
191+
checked status across datashuttle sessions.
194192
195193
Attributes
196194
----------
195+
tab_name
196+
Set by the subclass, indicates if the settings and checkbox names
197+
should include "create" or "transfer"
197198
datatype_config
198199
A Dictionary containing datatype as key (e.g. "ephys", "behav")
199200
and values are `bool` indicating whether the checkbox is on / off.
@@ -210,32 +211,29 @@ class DatatypeCheckboxes(Static):
210211
211212
"""
212213

214+
tab_name: Literal["create", "transfer"] # must be set by subclass
215+
213216
def __init__(
214217
self,
215218
interface: Interface,
216-
create_or_transfer: Literal["create", "transfer"] = "create",
217219
id: Optional[str] = None,
218220
) -> None:
219-
"""Initialise the DatatypeCheckboxes.
221+
"""Initialise the BaseDatatypeCheckboxes.
220222
221223
Parameters
222224
----------
223225
interface
224226
Datashuttle Interface object.
225227
226-
create_or_transfer
227-
Whether we are on the "create" or "transfer" tab.
228-
229228
id
230-
Textual ID for the DatatypeCheckboxes widget.
229+
Textual ID for the BaseDatatypeCheckboxes widget.
231230
232231
"""
233-
super(DatatypeCheckboxes, self).__init__(id=id)
232+
super(BaseDatatypeCheckboxes, self).__init__(id=id)
234233

235234
self.interface = interface
236-
self.create_or_transfer = create_or_transfer
237235

238-
self.settings_key = get_tui_settings_key_name(self.create_or_transfer)
236+
self.settings_key = get_tui_settings_key_name(self.tab_name)
239237

240238
# `datatype_config` is basically just a convenience wrapper
241239
# around interface.get_tui_settings
@@ -244,17 +242,17 @@ def __init__(
244242
]
245243

246244
def compose(self) -> ComposeResult:
247-
"""Add widgets to the DatatypeCheckboxes."""
245+
"""Add widgets to the BaseDatatypeCheckboxes."""
248246
for datatype, setting in self.datatype_config.items():
249247
if setting["displayed"]:
250248
yield Checkbox(
251249
datatype.replace("_", " "),
252-
id=get_checkbox_name(self.create_or_transfer, datatype),
250+
id=get_checkbox_name(self.tab_name, datatype),
253251
value=setting["on"],
254252
)
255253

256254
@on(Checkbox.Changed)
257-
def on_checkbox_changed(self) -> None:
255+
def on_checkbox_changed(self, event: Checkbox.Changed) -> None:
258256
"""Handle a datatype checkbox change.
259257
260258
When a checkbox is changed, update the `self.datatype_config`
@@ -264,19 +262,19 @@ def on_checkbox_changed(self) -> None:
264262
for datatype in self.datatype_config.keys():
265263
if self.datatype_config[datatype]["displayed"]:
266264
self.datatype_config[datatype]["on"] = self.query_one(
267-
f"#{get_checkbox_name(self.create_or_transfer, datatype)}"
265+
f"#{get_checkbox_name(self.tab_name, datatype)}"
268266
).value
269267

270268
self.interface.save_tui_settings(
271269
self.datatype_config, self.settings_key
272270
)
273271

274272
def on_mount(self) -> None:
275-
"""Add widgets to the DatatypeCheckboxes."""
273+
"""Add widgets to the BaseDatatypeCheckboxes."""
276274
for datatype in self.datatype_config.keys():
277275
if self.datatype_config[datatype]["displayed"]:
278276
self.query_one(
279-
f"#{get_checkbox_name(self.create_or_transfer, datatype)}"
277+
f"#{get_checkbox_name(self.tab_name, datatype)}"
280278
).tooltip = tooltips[datatype]
281279

282280
def selected_datatypes(self) -> List[str]:
@@ -289,22 +287,103 @@ def selected_datatypes(self) -> List[str]:
289287
return selected_datatypes
290288

291289

290+
class CreateDatatypeCheckboxes(BaseDatatypeCheckboxes):
291+
"""Subclass of the data-type checkboxes for the "create" tab."""
292+
293+
tab_name: Literal["create", "transfer"] = "create"
294+
295+
296+
class TransferDatatypeCheckboxes(BaseDatatypeCheckboxes):
297+
"""Subclass of the data type checkboxes class for the transfer tab.
298+
299+
This subclass extends `on_checkbox_changed` by adding logic to
300+
dynamically turn off mutually exclusive checkboxes when one is
301+
selected, before delegating to the base implementation.
302+
303+
"""
304+
305+
tab_name: Literal["create", "transfer"] = "transfer"
306+
307+
def __init__(self, interface: Interface, id: Optional[str]):
308+
"""Initialise TransferDatatypeCheckboxes.
309+
310+
Parameters
311+
----------
312+
interface
313+
Datashuttle Interface object.
314+
315+
id
316+
Textual ID for the DatatypeCheckboxes widget.
317+
318+
"""
319+
super().__init__(interface, id)
320+
321+
@on(Checkbox.Changed)
322+
def on_checkbox_changed(self, event: Checkbox.Changed) -> None:
323+
"""Dynamically turn off checkboxes depending on the activated checkbox then save.
324+
325+
In the transfer tab, we have a few different checkboxes that
326+
are mutually exclusive. For example, `all` and `all_datatype`
327+
are redundant. This function turns off checkboxes redundant
328+
with the selected checkbox, before calling the super class
329+
which saves the state of the currently selected checkboxes.
330+
"""
331+
checkbox = event.control
332+
checkbox_name = get_datatype_from_checkbox_name(str(checkbox.id))
333+
334+
if checkbox.value:
335+
all_datatypes_list = [
336+
dtype
337+
for dtype in self.datatype_config.keys()
338+
if dtype not in ["all", "all_datatype", "all_non_datatype"]
339+
]
340+
341+
if checkbox_name == "all":
342+
to_turn_off = [
343+
"all_datatype",
344+
"all_non_datatype",
345+
] + all_datatypes_list
346+
347+
elif checkbox_name == "all_datatype":
348+
to_turn_off = ["all"] + all_datatypes_list
349+
350+
elif checkbox_name == "all_non_datatype":
351+
to_turn_off = ["all"]
352+
353+
else:
354+
to_turn_off = ["all", "all_datatype"]
355+
356+
for datatype in to_turn_off:
357+
if self.datatype_config[datatype]["displayed"]:
358+
self.query_one(
359+
f"#{get_checkbox_name(self.tab_name, datatype)}"
360+
).value = False
361+
362+
super().on_checkbox_changed(event)
363+
364+
292365
# Helpers
293366
# --------------------------------------------------------------------------------------
294367

295368

296369
def get_checkbox_name(
297-
create_or_transfer: Literal["create", "transfer"], datatype
370+
tab_name: Literal["create", "transfer"], datatype
298371
) -> str:
299372
"""Return a canonical formatted checkbox name."""
300-
return f"{create_or_transfer}_{datatype}_checkbox"
373+
return f"{tab_name}_{datatype}_checkbox"
374+
375+
376+
def get_datatype_from_checkbox_name(checkbox_name: str) -> str:
377+
"""Get the datatype from the output of `get_checkbox_name()`."""
378+
split_datatype = checkbox_name.split("_")[1:-1]
379+
return "_".join(split_datatype)
301380

302381

303382
def get_tui_settings_key_name(
304-
create_or_transfer: Literal["create", "transfer"],
383+
tab_name: Literal["create", "transfer"],
305384
) -> str:
306385
"""Return the canonical tui settings key."""
307-
if create_or_transfer == "create":
386+
if tab_name == "create":
308387
settings_key = "create_checkboxes_on"
309388
else:
310389
settings_key = "transfer_checkboxes_on"

datashuttle/tui/tabs/create_folders.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
CreateFoldersSettingsScreen,
3131
)
3232
from datashuttle.tui.screens.datatypes import (
33-
DatatypeCheckboxes,
33+
CreateDatatypeCheckboxes,
3434
DisplayedDatatypesScreen,
3535
)
3636
from datashuttle.tui.screens.modal_dialogs import (
@@ -95,7 +95,7 @@ def compose(self) -> ComposeResult:
9595
)
9696
yield Label("Datatype(s)", id="create_folders_datatype_label")
9797
yield Container(
98-
self.get_datatype_checkboxes_widget(),
98+
self.create_datatype_checkboxes_widget(),
9999
id="create_folders_datatype_container", #
100100
)
101101
yield Horizontal(
@@ -167,7 +167,7 @@ async def refresh_after_datatypes_changed(self, ignore) -> None:
167167
"#create_folders_datatype_checkboxes"
168168
).remove()
169169

170-
await container.mount(self.get_datatype_checkboxes_widget())
170+
await container.mount(self.create_datatype_checkboxes_widget())
171171

172172
@require_double_click
173173
def on_clickable_input_clicked(
@@ -543,8 +543,12 @@ def update_directorytree_root(self, new_root_path: Path) -> None:
543543
"""Refresh the tree through the reactive attribute `path`."""
544544
self.query_one("#create_folders_directorytree").path = new_root_path
545545

546-
def get_datatype_checkboxes_widget(self):
546+
def create_datatype_checkboxes_widget(self):
547547
"""Create the datatype checkboxes, centralised as used in multiple places."""
548-
return DatatypeCheckboxes(
548+
return CreateDatatypeCheckboxes(
549549
self.interface, id="create_folders_datatype_checkboxes"
550550
)
551+
552+
def get_datatype_checkbox_widget(self) -> CreateDatatypeCheckboxes:
553+
"""Get the datatype checkboxes widget for the `create` tab."""
554+
return self.query_one("CreateDatatypeCheckboxes")

datashuttle/tui/tabs/transfer.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from rich.text import Text
1717
from textual import work
1818
from textual.containers import Container, Horizontal, Vertical
19+
from textual.css.query import NoMatches
1920
from textual.widgets import (
2021
Button,
2122
Checkbox,
@@ -32,8 +33,8 @@
3233
TreeAndInputTab,
3334
)
3435
from datashuttle.tui.screens.datatypes import (
35-
DatatypeCheckboxes,
3636
DisplayedDatatypesScreen,
37+
TransferDatatypeCheckboxes,
3738
)
3839
from datashuttle.tui.screens.modal_dialogs import (
3940
ConfirmAndAwaitTransferPopup,
@@ -141,7 +142,7 @@ def compose(self) -> ComposeResult:
141142
),
142143
# These are almost identical to create tab
143144
Label("Datatype(s)", id="transfer_datatype_label"),
144-
self.get_datatypes_checkboxes_widget(),
145+
self.create_datatype_checkboxes_widget(),
145146
self.get_displayed_datatypes_button(),
146147
]
147148

@@ -217,8 +218,6 @@ def on_mount(self) -> None:
217218
"#transfer_tab_overwrite_select",
218219
"#transfer_tab_dry_run_checkbox",
219220
]:
220-
from textual.css.query import NoMatches
221-
222221
try:
223222
# if checkbox is removed by user, hard to predict, skip.
224223
self.query_one(id).tooltip = get_tooltip(id)
@@ -338,7 +337,7 @@ async def refresh_after_datatype_changed(self, ignore):
338337
id="transfer_tab_displayed_datatypes_button",
339338
),
340339
)
341-
await container.mount(self.get_datatypes_checkboxes_widget())
340+
await container.mount(self.create_datatype_checkboxes_widget())
342341
await container.mount(self.get_displayed_datatypes_button())
343342

344343
def on_custom_directory_tree_directory_tree_special_key_press(
@@ -422,11 +421,10 @@ def transfer_data(self) -> Worker[InterfaceOutput]:
422421

423422
return success, output
424423

425-
def get_datatypes_checkboxes_widget(self):
424+
def create_datatype_checkboxes_widget(self):
426425
"""Create the datatype checkboxes, centralised as used in multiple places."""
427-
return DatatypeCheckboxes(
426+
return TransferDatatypeCheckboxes(
428427
self.interface,
429-
create_or_transfer="transfer",
430428
id="transfer_custom_datatype_checkboxes",
431429
)
432430

@@ -436,3 +434,7 @@ def get_displayed_datatypes_button(self):
436434
"Displayed Datatypes",
437435
id="transfer_tab_displayed_datatypes_button",
438436
)
437+
438+
def get_datatype_checkbox_widget(self) -> TransferDatatypeCheckboxes:
439+
"""Get the datatype checkboxes widget for the transfer tab."""
440+
return self.query_one("TransferDatatypeCheckboxes")

0 commit comments

Comments
 (0)