diff --git a/src/clinical_dbs_annotator/controllers/wizard_controller.py b/src/clinical_dbs_annotator/controllers/wizard_controller.py index 9b2193f..95be4d1 100644 --- a/src/clinical_dbs_annotator/controllers/wizard_controller.py +++ b/src/clinical_dbs_annotator/controllers/wizard_controller.py @@ -5,6 +5,8 @@ views and models, handling user interactions and data flow. """ +import csv + from PySide6.QtWidgets import QMessageBox from ..config import ( @@ -427,6 +429,70 @@ def insert_session_row(self, view) -> None: animate_button(view.insert_button) view.session_notes_edit.clear() + # Enable undo button after successful insert + if hasattr(view, "undo_button"): + view.undo_button.setEnabled(True) + + def undo_last_session_entry(self, view) -> None: + """ + Delete the last block_ID entry from the TSV file. + + Args: + view: The Step3View instance + """ + if not self.session_data.is_file_open(): + QMessageBox.warning(view, "Error", "No file is currently open.") + return + + # Get the last written block_id (current block_id is the next one to write) + last_written_block_id = self.session_data.block_id - 1 + + if last_written_block_id < 0: + QMessageBox.warning(view, "Error", "No entries to undo.") + return + + # Read the TSV file and filter out rows with the last block_id + file_path = self.session_data.file_path + rows_to_keep = [] + rows_to_delete = [] + + with open(file_path, newline="", encoding="utf-8") as f: + reader = csv.DictReader(f, delimiter="\t") + fieldnames = reader.fieldnames + + for row in reader: + block_id = row.get("block_id", "") + try: + if int(block_id) != last_written_block_id: + rows_to_keep.append(row) + else: + rows_to_delete.append(row) + except ValueError, TypeError: + # If block_id is not a number, keep the row + rows_to_keep.append(row) + + if not rows_to_delete: + QMessageBox.warning( + view, + "Error", + f"No entries found with block_id {last_written_block_id}.", + ) + return + + # Decrement block_id to point to the previous entry + self.session_data.block_id = last_written_block_id + + # Rewrite the TSV file with the filtered rows + with open(file_path, "w", newline="", encoding="utf-8") as f: + writer = csv.DictWriter(f, fieldnames=fieldnames, delimiter="\t") + writer.writeheader() + writer.writerows(rows_to_keep) + + # Disable undo button if no more entries to undo + if self.session_data.block_id == 0 or len(rows_to_keep) == 0: + if hasattr(view, "undo_button"): + view.undo_button.setEnabled(False) + def close_session(self, parent) -> None: """ Close the current session and file. @@ -445,9 +511,6 @@ def close_session(self, parent) -> None: if reply == QMessageBox.Ok: self.session_data.close_file() - QMessageBox.information( - parent, "Session closed", "Session closed and file saved." - ) parent.close() def export_session_word(self, parent, scale_prefs=None, sections=None) -> None: diff --git a/src/clinical_dbs_annotator/views/step3_view.py b/src/clinical_dbs_annotator/views/step3_view.py index 6c9a625..0c79217 100644 --- a/src/clinical_dbs_annotator/views/step3_view.py +++ b/src/clinical_dbs_annotator/views/step3_view.py @@ -5,7 +5,7 @@ session data including stimulation parameters and scale values. """ -from PySide6.QtCore import Qt +from PySide6.QtCore import Qt, Signal from PySide6.QtGui import QDoubleValidator, QIcon, QIntValidator, QPixmap from PySide6.QtWidgets import ( QComboBox, @@ -61,6 +61,8 @@ class Step3View(BaseStepView): - Data insertion and session closing """ + undo_requested = Signal() + def __init__(self, parent_style): """ Initialize Step 3 view. @@ -111,6 +113,20 @@ def _get_theme_icon_color(self) -> str: return get_theme_manager().get_theme_color("Icon") + def _undo_last_entry(self) -> None: + """Show confirmation dialog and delete the last block_ID entry from TSV.""" + reply = QMessageBox.question( + self, + "Confirm Undo", + "Are you sure you want to delete the last session entry?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No, + ) + + if reply == QMessageBox.Yes: + # Emit signal to request undo + self.undo_requested.emit() + def _setup_ui(self) -> None: """Set up the UI layout.""" @@ -196,6 +212,13 @@ def _setup_ui(self) -> None: self.main_layout.addWidget(splitter) # self.main_layout.addStretch(1) + self.undo_button = QPushButton("Undo") + self.undo_button.setIcon( + self.parent_style.standardIcon(QStyle.SP_DialogCancelButton) + ) + self.undo_button.setMinimumWidth(100) + self.undo_button.setEnabled(False) + self.insert_button = QPushButton("Insert") self.insert_button.setIcon( self.parent_style.standardIcon(QStyle.SP_DialogApplyButton) @@ -711,6 +734,7 @@ def _create_session_scales_group(self) -> QGroupBox: self.step3_session_scales_form.setLabelAlignment(Qt.AlignRight) self.step3_session_scales_form.setFormAlignment(Qt.AlignTop) layout.addLayout(self.step3_session_scales_form) + layout.addStretch() return gb_scales diff --git a/src/clinical_dbs_annotator/views/wizard_window.py b/src/clinical_dbs_annotator/views/wizard_window.py index b707ca2..2fb9727 100644 --- a/src/clinical_dbs_annotator/views/wizard_window.py +++ b/src/clinical_dbs_annotator/views/wizard_window.py @@ -686,6 +686,13 @@ def _connect_step2_signals(self) -> None: def _connect_step3_signals(self) -> None: """Connect Step 3 view signals to controller.""" self.controller.prepare_step3(self.step3_view) + if hasattr(self.step3_view, "undo_button"): + self.step3_view.undo_button.clicked.connect( + self.step3_view._undo_last_entry + ) + self.step3_view.undo_requested.connect( + lambda: self.controller.undo_last_session_entry(self.step3_view) + ) self.step3_view.insert_button.clicked.connect( lambda: self.controller.insert_session_row(self.step3_view) ) @@ -793,6 +800,9 @@ def _refresh_nav_right(self) -> None: if hasattr(current, "next_button"): widgets.append(current.next_button) + if hasattr(current, "undo_button"): + widgets.append(current.undo_button) + if hasattr(current, "insert_button"): widgets.append(current.insert_button)