Source code for gui.ui.window_main

# -*- coding: utf-8 -*-
"""
:File: EuljiroBible/gui/ui/window_main.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.

Defines the main application window for EuljiroBible, managing tabs, settings, language switching, overlay handling, and polling controls.
"""

import platform

from PySide6.QtWidgets import (
    QApplication, QWidget, QVBoxLayout, 
    QTabWidget, QLabel, QMessageBox, 
    QMenuBar, QMainWindow, QPushButton
)
from PySide6.QtCore import QSize
from PySide6.QtGui import QAction, QIcon

from gui.config.config_manager import ConfigManager
from gui.ui.locale.message_loader import get_available_languages, load_messages
from gui.ui.monitor_memory import MonitorMemory
from gui.ui.tab_verse import TabVerse
from gui.ui.tab_keyword import TabKeyword
from gui.ui.tab_settings import TabSettings

[docs] class WindowMain(QMainWindow): """ Main application window for EuljiroBible. This window hosts the primary tab-based interface, including: - Verse lookup and export (TabVerse) - Keyword search and export (TabKeyword) - Settings and overlay configuration (TabSettings) It also manages global UI behaviors such as: - Language switching (menu-driven) - Polling enable/disable toggle and propagation to tabs - About/help actions - Optional memory monitor window Attributes: settings (dict): Current settings dictionary (reloaded as needed via ConfigManager). app_version (str): Application version string displayed in the title and About dialog. current_language (str): Active UI language code (e.g., "ko", "en"). messages (dict): Loaded translation dictionary for the active language. help_menu (QMenu): Help menu container (About action). tools_menu (QMenu): Tools menu container (memory monitor, test error). lang_menu (QMenu | None): Language selection menu if languages are available. about_action (QAction): Opens the About dialog. memory_action (QAction): Opens the memory monitor window. test_error_action (QAction): Triggers a controlled error for testing. poll_toggle_btn (QPushButton): Global polling toggle button (checkable). When enabled, export/clear controls become available and polling-dependent UI elements are shown where applicable. tabs (QTabWidget): Tab widget hosting the main UI tabs. tab_verse (TabVerse): Verse lookup tab instance. tab_keyword (TabKeyword): Keyword search tab instance. tab_settings (TabSettings): Settings tab instance. copyright_label (QLabel): Footer label showing copyright/license text. monitor_window (MonitorMemory | None): Memory monitor window instance when opened. """
[docs] def __init__(self, version_list, settings, icon_path, app_version, parent=None): """ Initialize the main window and construct menus, tabs, and global controls. Args: version_list (list): List of available Bible version identifiers for the verse/keyword tabs. settings (dict): Loaded user settings dictionary. icon_path (str): Path to the application icon file (provided by caller). app_version (str): Application version string. parent (QWidget | None): Optional parent widget. """ super().__init__() self.settings = settings self.app_version = app_version self.current_language = "ko" self.messages = load_messages(self.current_language) self.setWindowTitle(self.tr("program_title").format(self.app_version)) self.resize(900, 650) central_widget = QWidget() layout = QVBoxLayout(central_widget) menubar = QMenuBar() self.help_menu = menubar.addMenu(self.tr("menu_help")) self.about_action = QAction(self.tr("menu_about"), self) self.about_action.triggered.connect(self.show_about) self.help_menu.addAction(self.about_action) self.tools_menu = menubar.addMenu(self.tr("menu_tools")) self.memory_action = QAction(self.tr("menu_memory"), self) self.memory_action.triggered.connect(self.open_monitor_memory) self.tools_menu.addAction(self.memory_action) available_languages = get_available_languages() if available_languages: try: lang_messages = load_messages(self.current_language) menu_title = lang_messages.get("menu_lang", "언어 / Language") except Exception: menu_title = "언어 / Language" self.lang_menu = menubar.addMenu(menu_title) # Create a menu item for each available language # The lambda ensures lang_code is correctly captured in closure for lang_code in available_languages: try: lang_messages = load_messages(lang_code) display_name = lang_messages.get("language", lang_code.upper()) except Exception: display_name = lang_code.upper() action = QAction(display_name, self) action.triggered.connect(lambda checked=False, lc=lang_code: self.change_language(lc)) self.lang_menu.addAction(action) self.test_error_action = QAction(self.tr("menu_test"), self) self.test_error_action.triggered.connect(self.trigger_error) self.tools_menu.addAction(self.test_error_action) self.setMenuBar(menubar) self.current_language = "ko" self.poll_toggle_btn = QPushButton() self.poll_toggle_btn.setCheckable(True) poll_enabled = self.settings.get("poll_enabled", False) self.poll_toggle_btn.setChecked(poll_enabled) self.update_poll_button_text() self.poll_toggle_btn.clicked.connect(self.on_poll_toggle_clicked) self.tabs = QTabWidget() self.tab_verse = TabVerse( version_list, self.settings, tr=self.tr, get_polling_status=lambda: self.poll_toggle_btn.isChecked(), get_always_show_setting=lambda: self.settings.get("always_show_on_off_buttons", False) ) self.tabs.addTab(self.tab_verse, self.tr("tab_verse")) self.tab_keyword = TabKeyword( version_list, self.settings, tr=self.tr, get_polling_status=lambda: self.poll_toggle_btn.isChecked(), get_always_show_setting=lambda: self.settings.get("always_show_on_off_buttons", False) ) self.tabs.addTab(self.tab_keyword, self.tr("tab_search")) self.tab_settings = TabSettings( app=QApplication.instance(), settings=settings, tr=self.tr, get_poll_enabled_callback=lambda: self.poll_toggle_btn.isChecked(), get_main_geometry=self.frameGeometry, refresh_settings_callback=None ) self.tab_settings.refresh_settings_callback = self.refresh_settings_and_tabs self.tab_settings.logic.refresh_settings_callback = self.refresh_settings_and_tabs self.tabs.addTab(self.tab_settings, self.tr("tab_font")) self.apply_tab_icons() self.tab_settings.apply_dynamic_settings() layout.addWidget(self.poll_toggle_btn) layout.addWidget(self.tabs) self.copyright_label = QLabel(self.tr("footer_copyright")) layout.addWidget(self.copyright_label) self.setCentralWidget(central_widget) self.tab_verse.update_button_layout() self.tab_keyword.update_button_visibility() self.tab_settings.update_presentation_visibility() self.poll_toggle_btn.toggled.connect(self.tabs.widget(2).update_presentation_visibility)
[docs] def tr(self, key): """ Translate a UI key using the currently loaded language messages. Args: key (str): Translation key. Returns: str: Translated string if found; otherwise the key itself. """ return self.messages.get(key, key)
[docs] def change_language(self, lang_code): """ Translate a UI key using the currently loaded language messages. Args: key (str): Translation key. Returns: str: Translated string if found; otherwise the key itself. """ self.current_language = lang_code self.messages = load_messages(lang_code) self.setWindowTitle(self.tr("program_title").format(self.app_version)) if not hasattr(self, "use_alias"): self.use_alias = False if hasattr(self, "alias_toggle_btn"): if self.use_alias: self.alias_toggle_btn.setText(self.tr("label_alias_short")) else: self.alias_toggle_btn.setText(self.tr("label_alias_full")) if hasattr(self, "help_menu"): self.help_menu.setTitle(self.tr("menu_help")) if hasattr(self, "tools_menu"): self.tools_menu.setTitle(self.tr("menu_tools")) if hasattr(self, "about_action"): self.about_action.setText(self.tr("menu_about")) if hasattr(self, "lang_menu"): self.lang_menu.setTitle(self.tr("menu_lang")) if hasattr(self, "memory_action"): self.memory_action.setText(self.tr("menu_memory")) if hasattr(self, "test_error_action"): self.test_error_action.setText(self.tr("menu_test")) if hasattr(self, "poll_toggle_btn"): self.update_poll_button_text() if hasattr(self, "tabs"): #icon = QIcon("resources/svg/tab_verse.svg") #self.tabs.setTabIcon(0, icon) self.tabs.setTabText(0, self.tr("tab_verse")) #icon = QIcon("resources/svg/tab_search.svg") #self.tabs.setTabIcon(1, icon) self.tabs.setTabText(1, self.tr("tab_search")) #icon = QIcon("resources/svg/tab_font.svg") #self.tabs.setTabIcon(2, icon) self.tabs.setTabText(2, self.tr("tab_font")) self.tabs.tabBar().setIconSize(QSize(30, 30)) for i in range(self.tabs.count()): tab = self.tabs.widget(i) if hasattr(tab, "change_language"): tab.change_language(lang_code) if hasattr(self, "copyright_label"): self.copyright_label.setText(self.tr("footer_copyright")) ConfigManager.update_partial({"last_language": lang_code})
[docs] def refresh_settings_and_tabs(self): """ Reload settings from ConfigManager and refresh polling-dependent tab UI. This is used after settings changes to ensure: - verse tab button layout is updated - keyword tab output button visibility is updated - settings tab overlay group visibility is updated """ self.settings = ConfigManager.load() self.tab_verse.update_button_layout() self.tab_keyword.update_button_visibility() self.tab_settings.update_presentation_visibility()
[docs] def apply_tab_icons(self): """ Apply tab icons to the QTabWidget (skipped on macOS). On macOS, icons are intentionally omitted due to Qt style inconsistencies. """ if platform.system() == "Darwin": return self.tabs.setTabIcon(0, QIcon("resources/svg/tab_verse.svg")) self.tabs.setTabIcon(1, QIcon("resources/svg/tab_search.svg")) self.tabs.setTabIcon(2, QIcon("resources/svg/tab_font.svg"))
[docs] def update_poll_button_text(self): """ Update the polling toggle button label and icon. The button reflects the current checked state and uses localized labels. """ if self.poll_toggle_btn.isChecked(): self.poll_toggle_btn.setText(self.tr("toggle_btn_poll_enabled")) self.poll_toggle_btn.setIcon(QIcon("resources/svg/toggle_btn_poll_enabled.svg")) else: self.poll_toggle_btn.setText(self.tr("toggle_btn_poll_disabled")) self.poll_toggle_btn.setIcon(QIcon("resources/svg/toggle_btn_poll_disabled.svg")) self.poll_toggle_btn.setIconSize(QSize(30, 30))
[docs] def on_poll_toggle_clicked(self): """ Handle clicks on the polling toggle button. This persists the new polling state and propagates it across tabs by: - saving poll_enabled to ConfigManager - updating settings tab polling behavior (start/stop/restart timer logic) - updating button label/icon - updating verse/keyword tab button visibility/layout """ poll_enabled = self.poll_toggle_btn.isChecked() ConfigManager.update_partial({"poll_enabled": poll_enabled}) self.tab_settings.settings["poll_enabled"] = poll_enabled self.tab_settings.apply_polling_settings() self.update_poll_button_text() self.tab_verse.update_button_layout() self.tab_keyword.update_button_visibility()
[docs] def show_about(self): """ Show the About dialog. The dialog content is localized and includes the current application version. """ QMessageBox.about( self, self.tr("about_title"), self.tr("about_message").format(self.app_version) )
[docs] def open_monitor_memory(self): """ Open the real-time memory monitor window. This reads the refresh interval from settings and launches MonitorMemory. """ settings = ConfigManager.load() interval = settings.get("memory_interval_sec", 5) self.monitor_window = MonitorMemory(interval_sec=interval) self.monitor_window.show()
[docs] def trigger_error(self): """ Trigger a controlled exception to test error handling. This is intended for validating logging and critical error dialog behavior. """ try: x = 1 / 0 except Exception as e: from gui.utils.logger import handle_exception handle_exception(e, "Error Test", "This is a drill.")
[docs] def closeEvent(self, event): """ Handle application close events and persist UI state. This attempts to: - apply dynamic settings (fonts/overlay) before exit - save UI-derived settings to disk - close any active overlay window owned by the settings tab Args: event (QCloseEvent): Close event. """ try: tab_verse = self.tabs.widget(0) font_tab = self.tabs.widget(2) from gui.ui.tab_settings import TabSettings if isinstance(font_tab, TabSettings): font_tab.apply_dynamic_settings() from gui.utils.state_saver import save_settings_from_ui save_settings_from_ui(QApplication.instance(), tab_verse) tab_settings = self.tabs.widget(3) from gui.ui.tab_settings import TabSettings if isinstance(tab_settings, TabSettings) and tab_settings.overlay: tab_settings.overlay.close() except Exception as e: QMessageBox.critical(self, self.tr("error_set_saving_title"), self.tr("error_set_saving_msg").format(e)) event.accept()