# 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)