# tabs/sandbox_tab.py import subprocess from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QTableWidget, QTableWidgetItem, QHeaderView, QMessageBox, QFormLayout ) from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt import database class SandboxTab(QWidget): # Signal emitted whenever data is added, modified, or deleted data_mutated = pyqtSignal() def __init__(self, parent=None): super().__init__(parent) # Internal tracking variable to distinguish edits vs new entries self.selected_translation_id = None # Main layout structure main_layout = QVBoxLayout(self) main_layout.setContentsMargins(30, 20, 30, 20) main_layout.setSpacing(15) # --- SECTION 1: FORM INPUT CRADLE --- form_container = QWidget() form_layout = QFormLayout(form_container) form_layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight) form_layout.setSpacing(10) # Input Form Fields self.txt_english = QLineEdit() self.txt_english.setPlaceholderText("Enter English phrase or word...") self.txt_english.setStyleSheet("padding: 6px; font-size: 14px;") self.txt_english.textChanged.connect(self.handle_live_filter) self.txt_spanish = QLineEdit() self.txt_spanish.setPlaceholderText("Introduce la frase en español...") self.txt_spanish.setStyleSheet("padding: 6px; font-size: 14px;") self.txt_spanish.textChanged.connect(self.handle_live_filter) self.txt_context = QLineEdit() self.txt_context.setPlaceholderText("e.g., Camino 2027, Café, Market conversation...") self.txt_context.setStyleSheet("padding: 6px; font-size: 14px;") self.txt_context.textChanged.connect(self.handle_live_filter) # NEW: Tags input field with live filter connection self.txt_tags = QLineEdit() self.txt_tags.setPlaceholderText("e.g., verb, greeting, subjunctive, travel...") self.txt_tags.setStyleSheet("padding: 6px; font-size: 14px;") self.txt_tags.textChanged.connect(self.handle_live_filter) self.txt_notes = QLineEdit() self.txt_notes.setPlaceholderText("Grammar rules, formal vs informal nuances...") self.txt_notes.setStyleSheet("padding: 6px; font-size: 14px;") # Mount fields onto Form Layout form_layout.addRow(QLabel("English Text:"), self.txt_english) form_layout.addRow(QLabel("Spanish Text:"), self.txt_spanish) form_layout.addRow(QLabel("Source Context:"), self.txt_context) form_layout.addRow(QLabel("Tags:"), self.txt_tags) form_layout.addRow(QLabel("Historical Notes:"), self.txt_notes) main_layout.addWidget(form_container) # --- SECTION 2: AUDIO PREVIEW ACTION ROW --- audio_layout = QHBoxLayout() audio_layout.setSpacing(15) self.btn_play_en = QPushButton("🔊 Test English Voice") self.btn_play_en.setCursor(Qt.CursorShape.PointingHandCursor) self.btn_play_en.setStyleSheet(""" QPushButton { background-color: #E67E22; color: white; font-weight: bold; font-size: 13px; padding: 8px 16px; border-radius: 4px; } QPushButton:hover { background-color: #D35400; } """) self.btn_play_en.clicked.connect(self.preview_english_audio) self.btn_play_es = QPushButton("🔊 Test Mónica (Spanish)") self.btn_play_es.setCursor(Qt.CursorShape.PointingHandCursor) self.btn_play_es.setStyleSheet(""" QPushButton { background-color: #9B59B6; color: white; font-weight: bold; font-size: 13px; padding: 8px 16px; border-radius: 4px; } QPushButton:hover { background-color: #8E44AD; } """) self.btn_play_es.clicked.connect(self.preview_spanish_audio) audio_layout.addWidget(self.btn_play_en) audio_layout.addWidget(self.btn_play_es) audio_layout.addStretch() main_layout.addLayout(audio_layout) # --- SECTION 3: DATA COMMIT CONTROL BAR --- control_layout = QHBoxLayout() control_layout.setSpacing(15) self.btn_save = QPushButton("Save Translation Record") self.btn_save.setCursor(Qt.CursorShape.PointingHandCursor) self.btn_save.setStyleSheet(""" QPushButton { background-color: #2980B9; color: white; font-weight: bold; font-size: 14px; padding: 10px 20px; border-radius: 5px; } QPushButton:hover { background-color: #1F618D; } """) self.btn_save.clicked.connect(self.commit_translation_record) self.btn_new_record = QPushButton("Create New Record") self.btn_new_record.setCursor(Qt.CursorShape.PointingHandCursor) self.btn_new_record.setStyleSheet(""" QPushButton { background-color: #27AE60; color: white; font-weight: bold; font-size: 14px; padding: 10px 20px; border-radius: 5px; } QPushButton:hover { background-color: #219653; } """) self.btn_new_record.clicked.connect(self.prepare_for_new_record) self.btn_clear = QPushButton("Clear Fields") self.btn_clear.setCursor(Qt.CursorShape.PointingHandCursor) self.btn_clear.setStyleSheet(""" QPushButton { background-color: #BDC3C7; color: #34495E; font-weight: bold; font-size: 14px; padding: 10px 20px; border-radius: 5px; } QPushButton:hover { background-color: #95A5A6; } """) self.btn_clear.clicked.connect(self.clear_all_fields_manually) control_layout.addWidget(self.btn_save) control_layout.addWidget(self.btn_new_record) control_layout.addWidget(self.btn_clear) control_layout.addStretch() main_layout.addLayout(control_layout) # --- SECTION 4: DATALIST DISPLAY REGION --- self.table = QTableWidget() self.table.setColumnCount(6) # Increased to 6 to display Tags column self.table.setHorizontalHeaderLabels(["ID", "English Phrase", "Spanish Translation", "Context", "Tags", "Notes"]) self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) # Wire up row selection change signals to auto-populate form self.table.itemSelectionChanged.connect(self.handle_row_selection) # Tweak display headers to scale nicely header = self.table.horizontalHeader() header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) header.setSectionResizeMode(3, QHeaderView.ResizeMode.Interactive) header.setSectionResizeMode(4, QHeaderView.ResizeMode.Interactive) # Tags Header header.setSectionResizeMode(5, QHeaderView.ResizeMode.Interactive) # Notes Header main_layout.addWidget(self.table) # Master cache of unfiltered row records to enable instant filtering loops self.all_cached_records = [] self.reload_table_display() @pyqtSlot() def preview_english_audio(self): """Auditions current text state inside the English text box field.""" text = self.txt_english.text().strip() if text: subprocess.Popen(["say", text]) @pyqtSlot() def preview_spanish_audio(self): """Auditions current text state inside the Spanish text box field using Mónica.""" text = self.txt_spanish.text().strip() if text: subprocess.Popen(["say", "-v", "Monica", text]) @pyqtSlot() def handle_live_filter(self): """Filters the visible entries based on English, Spanish, Context, and Tags criteria.""" # Temporary block signals to prevent selection loops from fighting text changes self.table.blockSignals(True) filter_en = self.txt_english.text().lower().strip() filter_es = self.txt_spanish.text().lower().strip() filter_ctx = self.txt_context.text().lower().strip() filter_tag = self.txt_tags.text().lower().strip() # Capture tag text query self.table.setRowCount(0) visible_row_index = 0 for row in self.all_cached_records: val_en = (row["en_text"] or "").lower() val_es = (row["es_text"] or "").lower() val_ctx = (row["source_context"] or "").lower() val_tag = (row["tags"] or "").lower() # Extract tags criteria # Look for explicit matching conditions across all 4 entry variables if (filter_en in val_en) and (filter_es in val_es) and (filter_ctx in val_ctx) and (filter_tag in val_tag): self.table.insertRow(visible_row_index) item_id = QTableWidgetItem(str(row["translation_id"])) item_en = QTableWidgetItem(row["en_text"]) item_es = QTableWidgetItem(row["es_text"]) item_ctx = QTableWidgetItem(row["source_context"] or "") item_tag = QTableWidgetItem(row["tags"] or "") item_nts = QTableWidgetItem(row["notes"] or "") item_id.setTextAlignment(Qt.AlignmentFlag.AlignCenter) self.table.setItem(visible_row_index, 0, item_id) self.table.setItem(visible_row_index, 1, item_en) self.table.setItem(visible_row_index, 2, item_es) self.table.setItem(visible_row_index, 3, item_ctx) self.table.setItem(visible_row_index, 4, item_tag) self.table.setItem(visible_row_index, 5, item_nts) # If we have an active editing ID, highlight that specific row during re-renders if self.selected_translation_id == row["translation_id"]: self.table.selectRow(visible_row_index) visible_row_index += 1 self.table.blockSignals(False) @pyqtSlot() def handle_row_selection(self): """Populates the input forms when a user clicks a row in the table view.""" selected_ranges = self.table.selectedRanges() if not selected_ranges: return row_idx = selected_ranges[0].topRow() id_item = self.table.item(row_idx, 0) if not id_item: return target_id = int(id_item.text()) # Locate item match within memory cache store elements record = next((r for r in self.all_cached_records if r["translation_id"] == target_id), None) if record: # Block line edit text tracking temporarily so populating fields doesn't trigger filter loops self.txt_english.blockSignals(True) self.txt_spanish.blockSignals(True) self.txt_context.blockSignals(True) self.txt_tags.blockSignals(True) self.txt_notes.blockSignals(True) self.selected_translation_id = record["translation_id"] self.txt_english.setText(record["en_text"]) self.txt_spanish.setText(record["es_text"]) self.txt_context.setText(record["source_context"] or "") self.txt_tags.setText(record["tags"] or "") self.txt_notes.setText(record["notes"] or "") self.txt_english.blockSignals(False) self.txt_spanish.blockSignals(False) self.txt_context.blockSignals(False) self.txt_tags.blockSignals(False) self.txt_notes.blockSignals(False) @pyqtSlot() def prepare_for_new_record(self): """Clears selection state so next click on 'Save' inserts fresh rows without scrubbing fields.""" self.selected_translation_id = None self.table.blockSignals(True) self.table.clearSelection() self.table.blockSignals(False) QMessageBox.information(self, "Status Shift", "Ready to insert a new record using the current field content.") @pyqtSlot() def commit_translation_record(self): """Saves current text blocks. Dynamically detects insert vs edit based on selections.""" en_text = self.txt_english.text().strip() es_text = self.txt_spanish.text().strip() context = self.txt_context.text().strip() tags = self.txt_tags.text().strip() notes = self.txt_notes.text().strip() if not en_text or not es_text: QMessageBox.warning(self, "Validation Alert", "Both English and Spanish base text blocks are required.") return try: if self.selected_translation_id is not None: # database.update_translation_record signature: (translation_id, es_text, en_text, source_context, tags, notes) database.update_translation_record( self.selected_translation_id, es_text, en_text, context, tags, notes ) else: # database.insert_translation_record signature: (es_text, en_text, source_context, tags, notes) database.insert_translation_record( es_text, en_text, context, tags, notes ) # Clear pointer state values on successful writing commits self.selected_translation_id = None # Wipe inputs cleanly and sync UI layers self.clear_input_fields() self.reload_table_display() self.data_mutated.emit() except Exception as e: QMessageBox.critical(self, "Database Commit Safeguard", f"Failed writing database operations:\n{str(e)}") @pyqtSlot() def clear_all_fields_manually(self): """Clears explicit states alongside visual row highlights simultaneously.""" self.selected_translation_id = None self.table.blockSignals(True) self.table.clearSelection() self.table.blockSignals(False) self.clear_input_fields() self.reload_table_display() def clear_input_fields(self): """Flushes transient text inside line editors without running filtering rules.""" self.txt_english.blockSignals(True) self.txt_spanish.blockSignals(True) self.txt_context.blockSignals(True) self.txt_tags.blockSignals(True) self.txt_notes.blockSignals(True) self.txt_english.clear() self.txt_spanish.clear() self.txt_context.clear() self.txt_tags.clear() self.txt_notes.clear() self.txt_english.blockSignals(False) self.txt_spanish.blockSignals(False) self.txt_context.blockSignals(False) self.txt_tags.blockSignals(False) self.txt_notes.blockSignals(False) def reload_table_display(self): """Refetches database rows and populates the master dashboard grid view.""" self.table.blockSignals(True) self.table.setRowCount(0) # Keep internal reference arrays synced cleanly self.all_cached_records = database.get_all_translations_explicit() for idx, row in enumerate(self.all_cached_records): self.table.insertRow(idx) item_id = QTableWidgetItem(str(row["translation_id"])) item_en = QTableWidgetItem(row["en_text"]) item_es = QTableWidgetItem(row["es_text"]) item_ctx = QTableWidgetItem(row["source_context"] or "") item_tag = QTableWidgetItem(row["tags"] or "") item_nts = QTableWidgetItem(row["notes"] or "") item_id.setTextAlignment(Qt.AlignmentFlag.AlignCenter) self.table.setItem(idx, 0, item_id) self.table.setItem(idx, 1, item_en) self.table.setItem(idx, 2, item_es) self.table.setItem(idx, 3, item_ctx) self.table.setItem(idx, 4, item_tag) self.table.setItem(idx, 5, item_nts) self.table.blockSignals(False)