2026-06-24 05:37:22 +00:00
|
|
|
# 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)
|
|
|
|
|
|
2026-06-25 12:23:11 +00:00
|
|
|
# Internal tracking variable to distinguish edits vs new entries
|
|
|
|
|
self.selected_translation_id = None
|
|
|
|
|
|
2026-06-24 05:37:22 +00:00
|
|
|
# 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;")
|
2026-06-25 12:23:11 +00:00
|
|
|
self.txt_english.textChanged.connect(self.handle_live_filter)
|
2026-06-24 05:37:22 +00:00
|
|
|
|
|
|
|
|
self.txt_spanish = QLineEdit()
|
|
|
|
|
self.txt_spanish.setPlaceholderText("Introduce la frase en español...")
|
|
|
|
|
self.txt_spanish.setStyleSheet("padding: 6px; font-size: 14px;")
|
2026-06-25 12:23:11 +00:00
|
|
|
self.txt_spanish.textChanged.connect(self.handle_live_filter)
|
2026-06-24 05:37:22 +00:00
|
|
|
|
|
|
|
|
self.txt_context = QLineEdit()
|
|
|
|
|
self.txt_context.setPlaceholderText("e.g., Camino 2027, Café, Market conversation...")
|
|
|
|
|
self.txt_context.setStyleSheet("padding: 6px; font-size: 14px;")
|
2026-06-25 12:23:11 +00:00
|
|
|
self.txt_context.textChanged.connect(self.handle_live_filter)
|
2026-06-24 05:37:22 +00:00
|
|
|
|
|
|
|
|
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("<b>English Text:</b>"), self.txt_english)
|
|
|
|
|
form_layout.addRow(QLabel("<b>Spanish Text:</b>"), self.txt_spanish)
|
|
|
|
|
form_layout.addRow(QLabel("<b>Source Context:</b>"), self.txt_context)
|
|
|
|
|
form_layout.addRow(QLabel("<b>Historical Notes:</b>"), 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()
|
2026-06-25 12:23:11 +00:00
|
|
|
control_layout.setSpacing(15)
|
2026-06-24 05:37:22 +00:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
2026-06-25 12:23:11 +00:00
|
|
|
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)
|
|
|
|
|
|
2026-06-24 05:37:22 +00:00
|
|
|
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; }
|
|
|
|
|
""")
|
2026-06-25 12:23:11 +00:00
|
|
|
self.btn_clear.clicked.connect(self.clear_all_fields_manually)
|
2026-06-24 05:37:22 +00:00
|
|
|
|
|
|
|
|
control_layout.addWidget(self.btn_save)
|
2026-06-25 12:23:11 +00:00
|
|
|
control_layout.addWidget(self.btn_new_record)
|
2026-06-24 05:37:22 +00:00
|
|
|
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(5)
|
|
|
|
|
self.table.setHorizontalHeaderLabels(["ID", "English Phrase", "Spanish Translation", "Context", "Notes"])
|
|
|
|
|
self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
|
|
|
|
self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
|
|
|
|
|
|
2026-06-25 12:23:11 +00:00
|
|
|
# Wire up row selection change signals to auto-populate form
|
|
|
|
|
self.table.itemSelectionChanged.connect(self.handle_row_selection)
|
|
|
|
|
|
2026-06-24 05:37:22 +00:00
|
|
|
# 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)
|
|
|
|
|
|
|
|
|
|
main_layout.addWidget(self.table)
|
|
|
|
|
|
2026-06-25 12:23:11 +00:00
|
|
|
# Master cache of unfiltered row records to enable instant filtering loops
|
|
|
|
|
self.all_cached_records = []
|
2026-06-24 05:37:22 +00:00
|
|
|
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])
|
|
|
|
|
|
2026-06-25 12:23:11 +00:00
|
|
|
@pyqtSlot()
|
|
|
|
|
def handle_live_filter(self):
|
|
|
|
|
"""Filters the visible database entries based on current text input 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()
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
|
|
|
|
# Look for matches across English, Spanish, and Source Context
|
|
|
|
|
if (filter_en in val_en) and (filter_es in val_es) and (filter_ctx in val_ctx):
|
|
|
|
|
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_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_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_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_notes.setText(record["notes"] or "")
|
|
|
|
|
|
|
|
|
|
self.txt_english.blockSignals(False)
|
|
|
|
|
self.txt_spanish.blockSignals(False)
|
|
|
|
|
self.txt_context.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.")
|
|
|
|
|
|
2026-06-24 05:37:22 +00:00
|
|
|
@pyqtSlot()
|
|
|
|
|
def commit_translation_record(self):
|
2026-06-25 12:23:11 +00:00
|
|
|
"""Saves current text blocks. Dynamically detects insert vs edit based on selections."""
|
2026-06-24 05:37:22 +00:00
|
|
|
en_text = self.txt_english.text().strip()
|
|
|
|
|
es_text = self.txt_spanish.text().strip()
|
|
|
|
|
context = self.txt_context.text().strip()
|
|
|
|
|
notes = self.txt_notes.text().strip()
|
2026-06-25 12:23:11 +00:00
|
|
|
tags = "" # Default empty placeholder string to fit the backend DB method signature
|
2026-06-24 05:37:22 +00:00
|
|
|
|
|
|
|
|
if not en_text or not es_text:
|
|
|
|
|
QMessageBox.warning(self, "Validation Alert", "Both English and Spanish base text blocks are required.")
|
|
|
|
|
return
|
|
|
|
|
|
2026-06-25 12:23:11 +00:00
|
|
|
try:
|
|
|
|
|
if self.selected_translation_id is not None:
|
|
|
|
|
# Direct match for 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:
|
|
|
|
|
# Direct match for 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)}")
|
2026-06-24 05:37:22 +00:00
|
|
|
|
|
|
|
|
@pyqtSlot()
|
2026-06-25 12:23:11 +00:00
|
|
|
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()
|
|
|
|
|
|
2026-06-24 05:37:22 +00:00
|
|
|
def clear_input_fields(self):
|
2026-06-25 12:23:11 +00:00
|
|
|
"""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_notes.blockSignals(True)
|
|
|
|
|
|
2026-06-24 05:37:22 +00:00
|
|
|
self.txt_english.clear()
|
|
|
|
|
self.txt_spanish.clear()
|
|
|
|
|
self.txt_context.clear()
|
|
|
|
|
self.txt_notes.clear()
|
2026-06-25 12:23:11 +00:00
|
|
|
|
|
|
|
|
self.txt_english.blockSignals(False)
|
|
|
|
|
self.txt_spanish.blockSignals(False)
|
|
|
|
|
self.txt_context.blockSignals(False)
|
|
|
|
|
self.txt_notes.blockSignals(False)
|
2026-06-24 05:37:22 +00:00
|
|
|
|
|
|
|
|
def reload_table_display(self):
|
|
|
|
|
"""Refetches database rows and populates the master dashboard grid view."""
|
2026-06-25 12:23:11 +00:00
|
|
|
self.table.blockSignals(True)
|
2026-06-24 05:37:22 +00:00
|
|
|
self.table.setRowCount(0)
|
|
|
|
|
|
2026-06-25 12:23:11 +00:00
|
|
|
# Keep internal reference arrays synced cleanly
|
|
|
|
|
self.all_cached_records = database.get_all_translations_explicit()
|
|
|
|
|
|
|
|
|
|
for idx, row in enumerate(self.all_cached_records):
|
2026-06-24 05:37:22 +00:00
|
|
|
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_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)
|
2026-06-25 12:23:11 +00:00
|
|
|
self.table.setItem(idx, 4, item_nts)
|
|
|
|
|
|
|
|
|
|
self.table.blockSignals(False)
|