Source code for core.generator.ui.contents.respo_content

# -*- coding: utf-8 -*-
"""
:File: EuljiroWorship/core/generator/ui/contents/respo_content.py
:Author: Benjamin Jaedon Choi - https://github.com/saintbenjamin
:Affiliated Church: The Eulji-ro Presbyterian Church [대한예수교장로회(통합) 을지로교회]
:Address: The Eulji-ro Presbyterian Church, 24-10, Eulji-ro 20-gil, Jung-gu, Seoul 04549, South Korea
:Telephone: +82-2-2266-3070
:E-mail: euljirochurch [at] G.M.A.I.L. (replace [at] with @ and G.M.A.I.L as you understood.)
:License: MIT License with Attribution Requirement (see LICENSE file for details); Copyright (c) 2025 The Eulji-ro Presbyterian Church.

UI content widget for editing "respo" (responsive reading) style slides.

This module defines :class:`core.generator.ui.contents.respo_content.RespoContent`, a `QWidget <https://doc.qt.io/qt-6/qwidget.html>`_ that provides a table-based
editor for responsive readings (교독문). Each slide consists of a title
and a sequence of speaker-response pairs, which are rendered as formatted
HTML for slide output.

The widget supports loading and saving responsive readings from JSON files
stored under ``data/respo/``, and integrates with :class:`core.generator.utils.slide_input_submitter.SlideInputSubmitter` for
automatic synchronization with the slide generator.
"""

import os
import re
import json

from PySide6.QtWidgets import (
    QWidget, QVBoxLayout, QHBoxLayout, 
    QLabel, QLineEdit, QPushButton, 
    QTableWidget, QTableWidgetItem, 
    QHeaderView, QMessageBox, QSizePolicy
)
from core.generator.utils.icon_helpers import set_svg_icon, get_icon_path
from core.generator.utils.slide_input_submitter import SlideInputSubmitter

[docs] class RespoContent(QWidget): """ Content editor widget for "respo" (responsive reading) slides. This widget provides a small editor UI for creating and maintaining responsive reading (교독문) content backed by local JSON files under ``data/respo/``. Users can: - Select a responsive reading by number and load its JSON data - Edit the reading title (exported as slide ``caption``) - Edit speaker/response pairs in a 2-column table - Save the edited content back to the JSON database - Export the current state as a slide dictionary where table rows are converted into an HTML-like formatted string (exported as slide ``headline``) The widget itself is responsible for *editing and formatting* the content. Actual slide splitting / rendering is handled downstream by the generator and overlay/controller pipelines. Integration with :class:`core.generator.utils.slide_input_submitter.SlideInputSubmitter` enables automatic submission/synchronization with the generator window. Attributes: caption (str): Initial caption provided at construction time. Often contains a numbered title such as ``"12. ..."`` which may trigger auto-loading. headline (str): Initial headline provided at construction time. This is not directly edited; exported headline is rebuilt from the table via :meth:`format_responsive_text`. respo_data (dict): In-memory JSON payload for the currently loaded responsive reading. Typically contains ``title`` and ``slides``. generator_window: Reference to the generator main window that receives slide updates and manages auto-save/session state. number_input (QLineEdit): Input field for the responsive reading number. load_button (QPushButton): Button that triggers :meth:`load_respo_by_number`. capt_edit (QLineEdit): Title input field (exported as slide ``caption``). table (QTableWidget): Two-column table editor for speaker/response rows. Column 0 = speaker, column 1 = body text. save_button (QPushButton): Button that triggers :meth:`save_respo_json`. submitter (SlideInputSubmitter): Auto-submit helper that observes the title/table widgets and supplies updated slide data via :meth:`build_respo_slide`. """
[docs] def __init__(self, parent, generator_window, caption: str = "", headline: str = ""): """ Initialize the responsive reading editor. Args: parent (QWidget): Parent widget container. generator_window: Reference to the generator main window, used for submission and auto-save behavior. caption (str): Initial slide caption, typically a numbered title. headline (str): Initial slide body content (unused for direct editing; rebuilt from table data). Returns: None """ super().__init__(parent) self.caption = caption self.headline = headline self.respo_data = {} self.generator_window = generator_window self.build_ui()
[docs] def build_ui(self): """ Construct the UI layout for responsive reading editing. The layout includes: - A number input field with load button - A title input field - A two-column table for speaker and response text - A save button for writing data back to JSON This method also initializes auto-loading when a numbered caption is detected. """ layout = QVBoxLayout(self) # Load by number self.number_input = QLineEdit() self.number_input.setPlaceholderText("Enter") self.number_input.setFixedWidth(80) self.number_input.returnPressed.connect(self.load_respo_by_number) self.load_button = QPushButton() set_svg_icon(self.load_button, get_icon_path("search.svg"), size=30) self.load_button.clicked.connect(self.load_respo_by_number) self.load_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) number_layout = QHBoxLayout() number_layout.addWidget(QLabel("새교독문")) number_layout.addWidget(self.number_input) number_layout.addWidget(QLabel("번")) number_layout.addWidget(self.load_button) layout.addLayout(number_layout) self.capt_edit = QLineEdit(self.caption) layout.addWidget(QLabel("제목")) layout.addWidget(self.capt_edit) self.table = QTableWidget() self.table.setColumnCount(2) self.table.setHorizontalHeaderLabels(["화자", "본문"]) self.table.verticalHeader().setDefaultSectionSize(44) header = self.table.horizontalHeader() header.setSectionResizeMode(0, QHeaderView.ResizeToContents) header.setSectionResizeMode(1, QHeaderView.Stretch) layout.addWidget(QLabel("본문")) layout.addWidget(self.table) self.save_button = QPushButton() set_svg_icon(self.save_button, get_icon_path("database-edit.svg"), size=30) self.save_button.clicked.connect(self.save_respo_json) layout.addWidget(self.save_button) match = re.match(r"^(\d{1,3})\.", self.caption.strip()) if match: self.number_input.setText(match.group(1)) self.load_respo_by_number() inputs = { "title": self.capt_edit, "body": self.table, } self.submitter = SlideInputSubmitter(inputs, self.generator_window, self.build_respo_slide)
[docs] def load_respo_by_number(self): """ Load responsive reading data from a JSON file. The JSON file is selected based on the number entered by the user and must exist under the ``data/respo/`` directory. The loaded data populates the title field and the speaker-response table. Displays a warning dialog if the input is invalid or out of range. """ num = self.number_input.text().strip() if not num.isdigit(): QMessageBox.warning(self, "입력 오류", "숫자만 입력하세요.") return filename = f"responsive_{int(num):03d}.json" path = os.path.join("data", "respo", filename) min_num, max_num = self.get_respo_number_range() int_num = int(num) if int_num < min_num or int_num > max_num: QMessageBox.warning( self, "범위 오류", f"새교독문은 {min_num}번부터 {max_num}번까지 있습니다." ) return try: with open(path, encoding="utf-8") as f: self.respo_data = json.load(f) self.capt_edit.setText(self.respo_data.get("title", "")) slides = self.respo_data.get("slides", []) self.table.setRowCount(len(slides)) for row, slide in enumerate(slides): self.table.setItem(row, 0, QTableWidgetItem(slide.get("speaker", ""))) self.table.setItem(row, 1, QTableWidgetItem(slide.get("headline", ""))) except Exception as e: QMessageBox.warning(self, "불러오기 실패", f"파일을 읽을 수 없습니다:\n{path}")
[docs] def build_respo_slide(self): """ Conditionally generate responsive reading slide data. If both the title and formatted body are empty, no slide data is produced. Otherwise, the current table contents are formatted and returned as slide data. Returns: dict | None: Slide data dictionary if valid; otherwise, None. """ data = self.get_slide_data() if not data["caption"] and not data["headline"]: return None return data
[docs] def get_respo_number_range(self): """ Determine the valid range of responsive reading numbers. Scans the ``data/respo/`` directory for available JSON files and extracts their numeric identifiers. Returns: tuple[int, int]: Minimum and maximum available responsive reading numbers. """ files = os.listdir("data/respo") nums = [ int(f.replace("responsive_", "").replace(".json", "")) for f in files if f.startswith("responsive_") and f.endswith(".json") ] return (min(nums), max(nums)) if nums else (0, 0)
[docs] def get_slide_data(self): """ Generate the slide data dictionary for export. Returns: dict: Dictionary containing: - style: "respo" - caption: title of the responsive reading - headline: formatted HTML body """ return { "style": "respo", "caption": self.capt_edit.text().strip(), "headline": self.format_responsive_text() }
[docs] def format_responsive_text(self): """ Convert table contents into formatted HTML text. Each row in the table is rendered as a bold speaker label followed by the corresponding response text. Returns: str: HTML-formatted responsive reading content. """ lines = [] for row in range(self.table.rowCount()): speaker_item = self.table.item(row, 0) response_item = self.table.item(row, 1) if speaker_item and response_item: speaker = speaker_item.text().strip() response = response_item.text().strip() lines.append(f"<b>{speaker}:</b> {response}") return "\n".join(lines)
[docs] def save_respo_json(self): """ Save the current responsive reading data to a JSON file. The data is written back to the file corresponding to the selected responsive reading number under ``data/respo/``. A warning is shown if no valid number is provided. """ num = self.number_input.text().strip() if not num.isdigit(): QMessageBox.warning(self, "저장 오류", "먼저 번호를 입력하고 데이터를 불러오세요.") return path = os.path.join("data", "respo", f"responsive_{int(num):03d}.json") self.respo_data["title"] = self.capt_edit.text() slides = [] for row in range(self.table.rowCount()): speaker_item = self.table.item(row, 0) hdln_item = self.table.item(row, 1) if speaker_item and hdln_item: slides.append({ "speaker": speaker_item.text(), "headline": hdln_item.text() }) self.respo_data["slides"] = slides try: with open(path, "w", encoding="utf-8") as f: json.dump(self.respo_data, f, ensure_ascii=False, indent=2) QMessageBox.information(self, "저장 완료", f"새교독문 {num}번의 데이터베이스를 업데이트하였습니다.") except Exception as e: QMessageBox.critical(self, "저장 실패", f"파일을 저장할 수 없습니다:\n{path}")