From 786acc1cac29c1d19bd89200cd43fed4132f4eb3 Mon Sep 17 00:00:00 2001 From: HeathenToaster Date: Wed, 10 Jun 2026 11:48:54 +0200 Subject: [PATCH 1/4] feature: favourite task --- village/gui/settings_layout.py | 19 +++++++++++++++++++ village/gui/tasks_layout.py | 9 ++++++++- village/settings.py | 8 ++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/village/gui/settings_layout.py b/village/gui/settings_layout.py index a7a623fba..7220948e8 100644 --- a/village/gui/settings_layout.py +++ b/village/gui/settings_layout.py @@ -204,6 +204,9 @@ def _flush_to_pending(self) -> None: ] with suppress(AttributeError, RuntimeError): self._pending["SOUND_DEVICE"] = self.sound_device_combobox.currentText() + with suppress(AttributeError, RuntimeError): + txt = self.favourite_task_combobox.currentText() + self._pending["FAVOURITE_TASK"] = txt with suppress(AttributeError, RuntimeError): self._pending["PROJECT_DIRECTORY"] = ( self.project_directory_combobox.currentText() @@ -760,6 +763,12 @@ def save(self, changing_project: bool, exiting: bool = False) -> None: except Exception: pass + try: + settings.set("FAVOURITE_TASK", + self.favourite_task_combobox.currentText()) + except Exception: + pass + cam_corridor.change = True cam_box.change = True @@ -871,6 +880,16 @@ def create_label_and_value( ) self.sound_device_combobox.setProperty("type", type) + elif s.key == "FAVOURITE_TASK": + possible_values = ["None"] + list(manager.tasks.keys()) + value = self._get(s.key) + index = (possible_values.index(value) + if value in possible_values else 0) + self.favourite_task_combobox = self.create_and_add_combo_box( + s.key, row, column + width, size2, 2, + possible_values, index, self.settings_changed) + self.favourite_task_combobox.setProperty("type", type) + elif s.value_type in (str, int, float): project_dir = self._get("PROJECT_DIRECTORY") value = str(self._get(s.key)) diff --git a/village/gui/tasks_layout.py b/village/gui/tasks_layout.py index b2c293767..f47f0a180 100644 --- a/village/gui/tasks_layout.py +++ b/village/gui/tasks_layout.py @@ -88,6 +88,11 @@ def draw(self) -> None: self._draw_content_area() self.check_buttons() + favourite = settings.get("FAVOURITE_TASK") + items = self._menu_items() + if favourite and favourite != "None" and favourite in items: + self.menu_list.setCurrentRow(items.index(favourite)) + # ── Left menu ────────────────────────────────────────────────────────────── def _draw_menu(self) -> None: @@ -116,8 +121,10 @@ def _draw_menu(self) -> None: ) self.menu_list.addItem(training_item) + favourite = settings.get("FAVOURITE_TASK") for key in manager.tasks: - item = QListWidgetItem(key) + label = f"★ {key}" if key == favourite else key + item = QListWidgetItem(label) item.setToolTip(f"Select the task {key}") self.menu_list.addItem(item) diff --git a/village/settings.py b/village/settings.py index 05d5c08bc..28c28ad62 100644 --- a/village/settings.py +++ b/village/settings.py @@ -48,6 +48,14 @@ """Enables the Operant Box PCB. This setting allows the Raspberry Pi to control some box components, such as LED stimuli, visible/infrared lighting, and motors.""", ), + Setting( + "FAVOURITE_TASK", + "None", + str, + """A favourite (★) task that is preselected when opening the TASKS + tab, so that the user can start a session immediately. + Set to None to disable preselection.""", + ), ] sound_settings = [ From bd22c277c2dbd984bfc29ae1e65a79d0a7cda01e Mon Sep 17 00:00:00 2001 From: HeathenToaster Date: Wed, 10 Jun 2026 12:36:27 +0200 Subject: [PATCH 2/4] bug fix? when edit subject "Next settings" and idle until UI goes to main, crash because TableView has been deleted --- village/gui/data_layout.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/village/gui/data_layout.py b/village/gui/data_layout.py index 4e4eefb82..2510b3d62 100644 --- a/village/gui/data_layout.py +++ b/village/gui/data_layout.py @@ -172,7 +172,10 @@ def mouseDoubleClickEvent(self, event) -> None: ) if reply == QMessageBox.No: return - super().mouseDoubleClickEvent(event) + try: + super().mouseDoubleClickEvent(event) + except RuntimeError: + return self.save_changes_in_df() elif flags & Qt.ItemIsEditable: text = "Wait until the box is empty or synchronization is complete" @@ -224,7 +227,10 @@ def mouseDoubleClickEvent(self, event) -> None: self.clearSelection() def save_changes_in_df(self, update: bool = True) -> None: - model = self._model() + try: + model = self._model() + except RuntimeError: + return if update: model.complete_df.loc[model.df.index] = model.df if manager.table == DataTable.SUBJECTS: From 2cbf1e42c88967df1cf0225aee20d9f8f25f23c8 Mon Sep 17 00:00:00 2001 From: HeathenToaster Date: Wed, 10 Jun 2026 13:02:24 +0200 Subject: [PATCH 3/4] feature: subject selector + slight UI redesign for ergonomics (run task button) --- village/gui/tasks_layout.py | 85 +++++++++++++++++++++++-------------- village/settings.py | 4 ++ 2 files changed, 57 insertions(+), 32 deletions(-) diff --git a/village/gui/tasks_layout.py b/village/gui/tasks_layout.py index f47f0a180..d57b8ddd3 100644 --- a/village/gui/tasks_layout.py +++ b/village/gui/tasks_layout.py @@ -71,14 +71,12 @@ def draw(self) -> None: self.line_edits: dict[str, LineEdit] = {} self.tasks_button.setDisabled(True) - # Centered between menu end (col 25) and right panel (col 89): (89-25-20)//2=22 - _btn_col = C_COL + (SETTINGS_COL - C_COL - 20) // 2 # = 47 self.run_task_button = self.create_and_add_button( "RUN TASK", C_ROW, - _btn_col, - 20, - 2, + C_COL + 24, + 34, + 3, self.run_task_button_clicked, "Run the selected task", "powderblue", @@ -86,6 +84,7 @@ def draw(self) -> None: self._draw_menu() self._draw_content_area() + self._draw_subject_selector() self.check_buttons() favourite = settings.get("FAVOURITE_TASK") @@ -145,6 +144,38 @@ def _on_menu_changed(self, row: int) -> None: cls = manager.tasks[name] self.select_task(cls, name) + # ── Subject selector ───────────────────────────────────────────────────── + def _draw_subject_selector(self) -> None: + mylist = ["None"] + manager.subjects.df["name"].tolist() + self.possible_subjects = [x for x in mylist + if not pd.isna(x) + and not (isinstance(x, str) + and x.strip() == "")] + last_subject = settings.get("LAST_SUBJECT") + if last_subject in self.possible_subjects: + self.subject_index = self.possible_subjects.index(last_subject) + else: + self.subject_index = 0 + + self.subject_label = self.create_and_add_label( + "Subject", C_ROW, C_COL + 4, 14, 1, "black") + self.subject_combo = self.create_and_add_combo_box( + "subject", C_ROW + 1, C_COL + 4, 16, 2, self.possible_subjects, + self.subject_index, self.select_subject) + self.subject_up_button = self.create_and_add_button( + "▲", C_ROW + 1, C_COL + 20, 2, 1, lambda: self._step_subject(-1), + "Previous subject") + self.subject_down_button = self.create_and_add_button( + "▼", C_ROW + 2, C_COL + 20, 2, 1, lambda: self._step_subject(1), + "Next subject") + + def _step_subject(self, delta: int) -> None: + if not self.possible_subjects: + return + n = len(self.possible_subjects) + self.subject_combo.setCurrentIndex( + (self.subject_combo.currentIndex() + delta) % n) + # ── Content area ─────────────────────────────────────────────────────────── def _draw_content_area(self) -> None: @@ -193,6 +224,11 @@ def restart_tab_panel(self) -> None: # ── Button state management ──────────────────────────────────────────────── def check_buttons(self) -> None: + subject_enabled = (not manager.state.task_is_running() + and (self.testing_training or self.selected != "")) + self.subject_combo.setEnabled(subject_enabled) + self.subject_up_button.setEnabled(subject_enabled) + self.subject_down_button.setEnabled(subject_enabled) if manager.state.task_is_running(): self.run_task_button.setEnabled(False) self.menu_list.setEnabled(False) @@ -219,7 +255,6 @@ def check_buttons(self) -> None: def select_task(self, cls: Type, name: str) -> None: if issubclass(cls, TaskBase): - self.subject_index = 0 self.testing_training = False self.selected = name self.central_sub_layout.delete_optional_widgets("optional") @@ -250,10 +285,9 @@ def select_task(self, cls: Type, name: str) -> None: row_h = self.central_sub_layout.row_height self.info_scroll.setFixedSize(60 * col_w, 30 * row_h) self.central_sub_layout.addWidget(self.info_scroll, 2, 2, 30, 60) - self.create_gui_properties(testing_training=False) + self._apply_current_subject(testing_training=False) def training_button_clicked(self) -> None: - self.subject_index = 0 self.testing_training = True self.selected = "" self.central_sub_layout.delete_optional_widgets("optional") @@ -262,7 +296,15 @@ def training_button_clicked(self) -> None: self.right_layout_general.delete_optional_widgets("optional2") self.check_buttons() manager.reset_subject_task_training() - self.create_gui_properties(testing_training=True) + self._apply_current_subject(testing_training=True) + + def _apply_current_subject(self, testing_training: bool) -> None: + """Loads the selector's subject, or builds defaults when None.""" + subject = self.subject_combo.currentText() + if subject != "None": + self.select_subject(subject, "subject") + else: + self.create_gui_properties(testing_training=testing_training) # ── GUI properties panel ─────────────────────────────────────────────────── @@ -272,29 +314,6 @@ def create_gui_properties(self, testing_training: bool) -> None: self.right_layout_general.delete_optional_widgets("optional2") self.restart_tab_panel() - self.subject_label = self.right_layout_general.create_and_add_label( - "Subject", 0, 2, 20, 2, "black" - ) - self.subject_label.setProperty("type", "optional") - - mylist = ["None"] + manager.subjects.df["name"].tolist() - self.possible_subjects = [ - x - for x in mylist - if not pd.isna(x) and not (isinstance(x, str) and x.strip() == "") - ] - self.subject_combo = self.right_layout_general.create_and_add_combo_box( - "subject", - 0, - 32, - 30, - 2, - self.possible_subjects, - self.subject_index, - self.select_subject, - ) - self.subject_combo.setProperty("type", "optional") - remove_names = [ "next_task", "maximum_duration", @@ -358,6 +377,8 @@ def find_tab_by_label(self, label: str) -> Union[QWidget, None]: def select_subject(self, value: str, key: str) -> None: self.subject_index = self.subject_combo.currentIndex() + settings.set("LAST_SUBJECT", value) + settings.sync() current_value = "" if value != "None": manager.subject.subject_series = manager.subjects.get_last_entry( diff --git a/village/settings.py b/village/settings.py index 28c28ad62..853c8a7f6 100644 --- a/village/settings.py +++ b/village/settings.py @@ -778,6 +778,10 @@ hidden_settings = [ Setting("FIRST_LAUNCH", "OFF", Active, "First launch of the system."), + Setting( + "LAST_SUBJECT", "None", str, + "The last subject selected in the TASKS tab." + ), Setting( "GITHUB_REPOSITORIES_DOWNLOADED", "OFF", From 832d39522d838a44d416558879e4260cf426f702 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:11:20 +0000 Subject: [PATCH 4/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- village/devices/led_strip.py | 2 +- village/gui/settings_layout.py | 17 ++++++++---- village/gui/tasks_layout.py | 51 ++++++++++++++++++++++++---------- village/settings.py | 5 +--- 4 files changed, 50 insertions(+), 25 deletions(-) diff --git a/village/devices/led_strip.py b/village/devices/led_strip.py index 9b508bc84..524c3c46b 100644 --- a/village/devices/led_strip.py +++ b/village/devices/led_strip.py @@ -1,7 +1,7 @@ import traceback from typing import Any -from pi5neo import Pi5Neo, EPixelType +from pi5neo import EPixelType, Pi5Neo from village.classes.null_classes import NullLEDStrip from village.scripts.log import log diff --git a/village/gui/settings_layout.py b/village/gui/settings_layout.py index 7220948e8..0e2f9be73 100644 --- a/village/gui/settings_layout.py +++ b/village/gui/settings_layout.py @@ -764,8 +764,7 @@ def save(self, changing_project: bool, exiting: bool = False) -> None: pass try: - settings.set("FAVOURITE_TASK", - self.favourite_task_combobox.currentText()) + settings.set("FAVOURITE_TASK", self.favourite_task_combobox.currentText()) except Exception: pass @@ -883,11 +882,17 @@ def create_label_and_value( elif s.key == "FAVOURITE_TASK": possible_values = ["None"] + list(manager.tasks.keys()) value = self._get(s.key) - index = (possible_values.index(value) - if value in possible_values else 0) + index = possible_values.index(value) if value in possible_values else 0 self.favourite_task_combobox = self.create_and_add_combo_box( - s.key, row, column + width, size2, 2, - possible_values, index, self.settings_changed) + s.key, + row, + column + width, + size2, + 2, + possible_values, + index, + self.settings_changed, + ) self.favourite_task_combobox.setProperty("type", type) elif s.value_type in (str, int, float): diff --git a/village/gui/tasks_layout.py b/village/gui/tasks_layout.py index d57b8ddd3..6351ad2f4 100644 --- a/village/gui/tasks_layout.py +++ b/village/gui/tasks_layout.py @@ -147,10 +147,11 @@ def _on_menu_changed(self, row: int) -> None: # ── Subject selector ───────────────────────────────────────────────────── def _draw_subject_selector(self) -> None: mylist = ["None"] + manager.subjects.df["name"].tolist() - self.possible_subjects = [x for x in mylist - if not pd.isna(x) - and not (isinstance(x, str) - and x.strip() == "")] + self.possible_subjects = [ + x + for x in mylist + if not pd.isna(x) and not (isinstance(x, str) and x.strip() == "") + ] last_subject = settings.get("LAST_SUBJECT") if last_subject in self.possible_subjects: self.subject_index = self.possible_subjects.index(last_subject) @@ -158,23 +159,44 @@ def _draw_subject_selector(self) -> None: self.subject_index = 0 self.subject_label = self.create_and_add_label( - "Subject", C_ROW, C_COL + 4, 14, 1, "black") + "Subject", C_ROW, C_COL + 4, 14, 1, "black" + ) self.subject_combo = self.create_and_add_combo_box( - "subject", C_ROW + 1, C_COL + 4, 16, 2, self.possible_subjects, - self.subject_index, self.select_subject) + "subject", + C_ROW + 1, + C_COL + 4, + 16, + 2, + self.possible_subjects, + self.subject_index, + self.select_subject, + ) self.subject_up_button = self.create_and_add_button( - "▲", C_ROW + 1, C_COL + 20, 2, 1, lambda: self._step_subject(-1), - "Previous subject") + "▲", + C_ROW + 1, + C_COL + 20, + 2, + 1, + lambda: self._step_subject(-1), + "Previous subject", + ) self.subject_down_button = self.create_and_add_button( - "▼", C_ROW + 2, C_COL + 20, 2, 1, lambda: self._step_subject(1), - "Next subject") + "▼", + C_ROW + 2, + C_COL + 20, + 2, + 1, + lambda: self._step_subject(1), + "Next subject", + ) def _step_subject(self, delta: int) -> None: if not self.possible_subjects: return n = len(self.possible_subjects) self.subject_combo.setCurrentIndex( - (self.subject_combo.currentIndex() + delta) % n) + (self.subject_combo.currentIndex() + delta) % n + ) # ── Content area ─────────────────────────────────────────────────────────── @@ -224,8 +246,9 @@ def restart_tab_panel(self) -> None: # ── Button state management ──────────────────────────────────────────────── def check_buttons(self) -> None: - subject_enabled = (not manager.state.task_is_running() - and (self.testing_training or self.selected != "")) + subject_enabled = not manager.state.task_is_running() and ( + self.testing_training or self.selected != "" + ) self.subject_combo.setEnabled(subject_enabled) self.subject_up_button.setEnabled(subject_enabled) self.subject_down_button.setEnabled(subject_enabled) diff --git a/village/settings.py b/village/settings.py index 853c8a7f6..9ec500f91 100644 --- a/village/settings.py +++ b/village/settings.py @@ -778,10 +778,7 @@ hidden_settings = [ Setting("FIRST_LAUNCH", "OFF", Active, "First launch of the system."), - Setting( - "LAST_SUBJECT", "None", str, - "The last subject selected in the TASKS tab." - ), + Setting("LAST_SUBJECT", "None", str, "The last subject selected in the TASKS tab."), Setting( "GITHUB_REPOSITORIES_DOWNLOADED", "OFF",