Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 82 additions & 19 deletions datashuttle/tui/screens/datatypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,13 +185,6 @@ def on_selection_list_selection_toggled(
class DatatypeCheckboxes(Static):
"""Dynamically-populated checkbox widget for convenient datatype selection.

Parameters
----------
settings_key
'create' if datatype checkboxes for the create tab,
'transfer' for the transfer tab. Transfer tab includes
additional datatype options (e.g. "all").

Attributes
----------
datatype_config
Expand All @@ -213,7 +206,7 @@ class DatatypeCheckboxes(Static):
def __init__(
self,
interface: Interface,
create_or_transfer: Literal["create", "transfer"] = "create",
tab_name: Literal["create", "transfer"] = "create",
id: Optional[str] = None,
) -> None:
"""Initialise the DatatypeCheckboxes.
Expand All @@ -223,7 +216,7 @@ def __init__(
interface
Datashuttle Interface object.

create_or_transfer
tab_name
Whether we are on the "create" or "transfer" tab.

id
Expand All @@ -233,9 +226,9 @@ def __init__(
super(DatatypeCheckboxes, self).__init__(id=id)

self.interface = interface
self.create_or_transfer = create_or_transfer
self.tab_name = tab_name

self.settings_key = get_tui_settings_key_name(self.create_or_transfer)
self.settings_key = get_tui_settings_key_name(self.tab_name)

# `datatype_config` is basically just a convenience wrapper
# around interface.get_tui_settings
Expand All @@ -249,12 +242,12 @@ def compose(self) -> ComposeResult:
if setting["displayed"]:
yield Checkbox(
datatype.replace("_", " "),
id=get_checkbox_name(self.create_or_transfer, datatype),
id=get_checkbox_name(self.tab_name, datatype),
value=setting["on"],
)

@on(Checkbox.Changed)
def on_checkbox_changed(self) -> None:
def on_checkbox_changed(self, event: Checkbox.Changed) -> None:
"""Handle a datatype checkbox change.

When a checkbox is changed, update the `self.datatype_config`
Expand All @@ -264,7 +257,7 @@ def on_checkbox_changed(self) -> None:
for datatype in self.datatype_config.keys():
if self.datatype_config[datatype]["displayed"]:
self.datatype_config[datatype]["on"] = self.query_one(
f"#{get_checkbox_name(self.create_or_transfer, datatype)}"
f"#{get_checkbox_name(self.tab_name, datatype)}"
).value

self.interface.save_tui_settings(
Expand All @@ -276,7 +269,7 @@ def on_mount(self) -> None:
for datatype in self.datatype_config.keys():
if self.datatype_config[datatype]["displayed"]:
self.query_one(
f"#{get_checkbox_name(self.create_or_transfer, datatype)}"
f"#{get_checkbox_name(self.tab_name, datatype)}"
).tooltip = tooltips[datatype]

def selected_datatypes(self) -> List[str]:
Expand All @@ -289,22 +282,92 @@ def selected_datatypes(self) -> List[str]:
return selected_datatypes


class TransferDatatypeCheckboxes(DatatypeCheckboxes):
"""Subclass of the data type checkboxes class for the transfer tab.

This subclass extends `on_checkbox_changed` by adding logic to
dynamically turn off mutually exclusive checkboxes when one is
selected, before delegating to the base implementation.

"""

def __init__(self, interface, id):
"""Initialise TransferDatatypeCheckboxes.

Parameters
----------
interface
Datashuttle Interface object.

id
Textual ID for the DatatypeCheckboxes widget.

"""
super().__init__(interface, "transfer", id)

@on(Checkbox.Changed)
def on_checkbox_changed(self, event: Checkbox.Changed) -> None:
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method requires 2 positional arguments, whereas overridden DatatypeCheckboxes.on_checkbox_changed requires 1.

Copilot uses AI. Check for mistakes.
"""Dynamically turn off checkboxes depending on the activated checkbox then save.

In the transfer tab, we have a few different checkboxes that
are mutually exclusive. For example, `all` and `all_datatypes`
are redundant. This function turns off checkboxes redundant
with the selected checkbox, before calling the super class
which saves the state of the currently selected checkboxes.
"""
checkbox = event.control
checkbox_name = get_datatype_from_checkbox_name(str(checkbox.id))

if checkbox.value:
all_datatypes = [
dtype
for dtype in self.datatype_config.keys()
if dtype not in ["all", "all_datatype", "all_non_datatype"]
]

if checkbox_name == "all":
to_turn_off = [
"all_datatype",
"all_non_datatype",
] + all_datatypes
elif checkbox_name == "all_datatype":
to_turn_off = ["all"] + all_datatypes
elif checkbox_name == "all_non_datatype":
to_turn_off = ["all"]
else:
to_turn_off = ["all", "all_datatype"]

for datatype in to_turn_off:
if self.datatype_config[datatype]["displayed"]:
self.query_one(
f"#{get_checkbox_name(self.tab_name, datatype)}"
).value = False

super().on_checkbox_changed(event)


# Helpers
# --------------------------------------------------------------------------------------


def get_checkbox_name(
create_or_transfer: Literal["create", "transfer"], datatype
tab_name: Literal["create", "transfer"], datatype
) -> str:
"""Return a canonical formatted checkbox name."""
return f"{create_or_transfer}_{datatype}_checkbox"
return f"{tab_name}_{datatype}_checkbox"


def get_datatype_from_checkbox_name(checkbox_name: str) -> str:
"""Get the datatype from the output of `get_checkbox_name()`."""
split_datatype = checkbox_name.split("_")[1:-1]
return "_".join(split_datatype)


def get_tui_settings_key_name(
create_or_transfer: Literal["create", "transfer"],
tab_name: Literal["create", "transfer"],
) -> str:
"""Return the canonical tui settings key."""
if create_or_transfer == "create":
if tab_name == "create":
settings_key = "create_checkboxes_on"
else:
settings_key = "transfer_checkboxes_on"
Expand Down
4 changes: 3 additions & 1 deletion datashuttle/tui/tabs/create_folders.py
Original file line number Diff line number Diff line change
Expand Up @@ -546,5 +546,7 @@ def update_directorytree_root(self, new_root_path: Path) -> None:
def get_datatype_checkboxes_widget(self):
"""Create the datatype checkboxes, centralised as used in multiple places."""
return DatatypeCheckboxes(
self.interface, id="create_folders_datatype_checkboxes"
self.interface,
tab_name="create",
id="create_folders_datatype_checkboxes",
)
5 changes: 2 additions & 3 deletions datashuttle/tui/tabs/transfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
TreeAndInputTab,
)
from datashuttle.tui.screens.datatypes import (
DatatypeCheckboxes,
DisplayedDatatypesScreen,
TransferDatatypeCheckboxes,
)
from datashuttle.tui.screens.modal_dialogs import (
ConfirmAndAwaitTransferPopup,
Expand Down Expand Up @@ -424,9 +424,8 @@ def transfer_data(self) -> Worker[InterfaceOutput]:

def get_datatypes_checkboxes_widget(self):
"""Create the datatype checkboxes, centralised as used in multiple places."""
return DatatypeCheckboxes(
return TransferDatatypeCheckboxes(
self.interface,
create_or_transfer="transfer",
id="transfer_custom_datatype_checkboxes",
)

Expand Down
67 changes: 65 additions & 2 deletions tests/tests_tui/test_tui_widgets_and_defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -1075,14 +1075,15 @@ async def test_all_checkboxes(self, setup_project_paths):
"ephys",
"funcimg",
"anat",
"all",
"all_datatype",
"all_non_datatype",
# "all" and "all_datatype" will turn off transfer datatypes
# and are tested separately in `test_transfer_checkboxes_dynamic_on_off()`.
]:
await self.change_checkbox(
pilot, f"#transfer_{datatype}_checkbox"
)
expected_transfer[datatype]["on"] = True

self.check_datatype_checkboxes(
pilot, "transfer", expected_transfer
)
Expand All @@ -1103,6 +1104,68 @@ async def test_all_checkboxes(self, setup_project_paths):

await pilot.pause()

@pytest.mark.asyncio
async def test_transfer_checkboxes_dynamic_on_off(
self, setup_project_paths
):
"""
This tests that mutually exclusive checkbox options turn
each other off / on when set. This is necessary for transfer
tab custom datatypes in which some checkboxes (e.g. "all")
should not be selected with other (e.g. "behav").
"""
tmp_config_path, tmp_path, project_name = setup_project_paths.values()

app = TuiApp()
async with app.run_test(size=self.tui_size()) as pilot:
# Set up the TUI on the 'transfer' tab (custom) and
# open the datatype selection screen
await self.check_and_click_onto_existing_project(
pilot, project_name
)

await self.switch_tab(pilot, "transfer")
await self.scroll_to_click_pause(
pilot, "#transfer_custom_radiobutton"
)

# Create a function to reload the settings dict, refreshing the contents
def load_dict():
return pilot.app.screen.interface.project._load_persistent_settings()[
"tui"
]["transfer_checkboxes_on"]

# turn on "behav" checkbox, check "all" is turned off
assert load_dict()["all"]["on"]
await self.change_checkbox(pilot, "#transfer_behav_checkbox")
assert not load_dict()["all"]["on"]

# Turn on "all_non_datatype" checkbox, check "behav" is kept on
await self.change_checkbox(
pilot, "#transfer_all_non_datatype_checkbox"
)
assert load_dict()["all_non_datatype"]["on"]
assert load_dict()["behav"]["on"]

# Turn on "all_datatype" checkbox, check "behav" is turned off
await self.change_checkbox(
pilot, "#transfer_all_datatype_checkbox"
)
assert load_dict()["all_datatype"]["on"]
assert not load_dict()["behav"]["on"]

# Turn on "all" checkbox and check all_non_datatype and all_datatype are switched off
await self.change_checkbox(pilot, "#transfer_all_checkbox")
assert load_dict()["all"]["on"]
assert not load_dict()["all_datatype"]["on"]
assert not load_dict()["all_non_datatype"]["on"]

# Turn on "all_non_datatype" and check "all" is now off
await self.change_checkbox(
pilot, "#transfer_all_non_datatype_checkbox"
)
assert not load_dict()["all"]["on"]

def check_datatype_checkboxes(self, pilot, tab, expected_on):
assert tab in ["create", "transfer"]
if tab == "create":
Expand Down
Loading