# -*- coding: utf-8 -*- # gui/components/text_input.py """Поле ввода текста""" from PySide6.QtWidgets import QApplication, QLineEdit, QSizePolicy, QTextEdit from PySide6.QtCore import Slot from gui.styles import APP_STYLES from gui.theme_bus import theme_bus from gui.containers.s_container import SContainer from error_logger import log_exception class TextInput(SContainer): """Поле ввода текста на базе SContainer.""" def __init__( self, text: str = "", placeholder: str = "", width_percent: int | None = None, height_percent: int | None = None, margin: int | tuple[int, int, int, int] = 0, parent=None, style: str = "TEXT_INPUT", active_style: str | None = None, is_active: bool | None = None, content_fit: bool = True, multiline: bool = False, ): super().__init__( width_percent=width_percent, height_percent=height_percent, margin=margin, style=style, active_style=active_style, is_active=is_active, content_fit=content_fit, parent=parent, ) self._theme = "dark" self._is_active = False self._base_style_key = style self._style_key_normal = None self._style_key_active = None self._is_multiline = bool(multiline) if self._is_multiline: self._input = QTextEdit() self._input.setPlainText(text) self._input.setPlaceholderText(placeholder) else: self._input = QLineEdit(text) self._input.setPlaceholderText(placeholder) self._input.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) super().add_widget(self._input) if active_style is not None: self._style_key_normal = style self._style_key_active = active_style if is_active is not None: self._is_active = bool(is_active) self._theme = "dark" if self.palette().window().color().lightness() < 128 else "light" self.style() theme_bus.theme_changed.connect(self.set_theme) def style( self, style_key: str | None = None, active_key: str | None = None, is_active: bool | None = None, ) -> None: """Короткий метод применения стиля. Можно задать ключи и активность явно.""" if style_key is not None: self._base_style_key = style_key if active_key is not None: self._style_key_normal = style_key self._style_key_active = active_key else: self._style_key_normal = None self._style_key_active = None if is_active is not None: self._is_active = bool(is_active) if self._style_key_normal is not None: active_key = self._style_key_active or self._style_key_normal key = active_key if self._is_active else self._style_key_normal themed = f"{key}_{self._theme.upper()}" if themed in APP_STYLES: key = themed self._input.setStyleSheet(APP_STYLES.get(key, "")) return base_key = self._base_style_key key = base_key if self._theme == "light": if self._is_active and f"{base_key}_LIGHT_ACTIVE" in APP_STYLES: key = f"{base_key}_LIGHT_ACTIVE" elif f"{base_key}_LIGHT" in APP_STYLES: key = f"{base_key}_LIGHT" else: if self._is_active and f"{base_key}_DARK_ACTIVE" in APP_STYLES: key = f"{base_key}_DARK_ACTIVE" elif f"{base_key}_DARK" in APP_STYLES: key = f"{base_key}_DARK" self._input.setStyleSheet(APP_STYLES.get(key, "")) @Slot(str) def set_theme(self, theme: str) -> None: """Внешний слот: принимает 'dark' или 'light'.""" theme = (theme or "").strip().lower() if theme not in ("dark", "light"): return if self._theme == theme: return self._theme = theme self.style() def set_text(self, text: str) -> None: if self._is_multiline: self._input.setPlainText(text) return self._input.setText(text) def get_text(self) -> str: if self._is_multiline: return self._input.toPlainText() return self._input.text() def clear(self) -> None: self._input.clear() def set_placeholder(self, text: str) -> None: self._input.setPlaceholderText(text) def set_enabled(self, enabled: bool) -> None: self._input.setEnabled(enabled) super().setEnabled(enabled) def set_read_only(self, readonly: bool) -> None: self._input.setReadOnly(readonly) def set_validator(self, validator) -> None: if self._is_multiline: return self._input.setValidator(validator) def install_event_filter(self, event_filter_obj) -> None: self._input.installEventFilter(event_filter_obj) def has_focus_within(self) -> bool: focused = QApplication.focusWidget() if focused is None: return False if focused is self._input: return True try: return bool(self._input.isAncestorOf(focused)) except Exception as e: log_exception(__name__, "has_focus_within", e) return False def clear_focus(self) -> None: self._input.clearFocus() def set_min_width(self, width: int) -> None: self._input.setMinimumWidth(width) super().setMinimumWidth(width) def set_min_height(self, height: int) -> None: self._input.setMinimumHeight(height) super().setMinimumHeight(height) def set_max_width(self, width: int) -> None: self._input.setMaximumWidth(width) super().setMaximumWidth(width) def set_max_height(self, height: int) -> None: self._input.setMaximumHeight(height) super().setMaximumHeight(height) def set_fixed_size(self, width: int, height: int) -> None: self._input.setMinimumSize(width, height) self._input.setMaximumSize(width, height) super().setMinimumSize(width, height) super().setMaximumSize(width, height) def set_tooltip(self, text: str) -> None: self._input.setToolTip(text) def set_size_policy(self, horizontal, vertical) -> None: self._input.setSizePolicy(horizontal, vertical) super().setSizePolicy(horizontal, vertical) def add_widget(self, widget, alignment=None): raise NotImplementedError("TextInput can contain only one QLineEdit") @property def text_changed(self): return self._input.textChanged @property def return_pressed(self): if self._is_multiline: raise AttributeError("return_pressed is unavailable for multiline TextInput") return self._input.returnPressed @property def editing_finished(self): if self._is_multiline: raise AttributeError("editing_finished is unavailable for multiline TextInput") return self._input.editingFinished @property def text_edited(self): if self._is_multiline: raise AttributeError("text_edited is unavailable for multiline TextInput") return self._input.textEdited # --------------------------------------------------------------------------- # Module workflow notes # --------------------------------------------------------------------------- # # 1) Назначение модуля: # Поле ввода текста на базе QLineEdit в SContainer с поддержкой # placeholder, централизованных стилей APP_STYLES и автоматической # темизации. # # 2) Зависимости модуля: # Импорты: QLineEdit, QSizePolicy (PySide6.QtWidgets), # Slot (PySide6.QtCore), # APP_STYLES (gui.styles), # theme_bus (gui.theme_bus), # SContainer (gui.containers.s_container) # Хост-класс / базовый класс: SContainer # Внешние библиотеки: PySide6 (обязательна) # # 3) Экспорт: # Класс TextInput — публичный виджет поля ввода. # Методы: style(), set_theme(), set_text(), get_text(), clear(), # set_enabled(), set_read_only(), # set_min/max_width/height(), set_fixed_size(), # set_tooltip(), set_size_policy(). # Свойство: text_changed — сигнал QLineEdit.textChanged. # # 4) Состояние (поля): # _theme: str — текущая тема # _is_active: bool — признак активного состояния # _base_style_key: str — базовый ключ стиля ("TEXT_INPUT") # _style_key_normal: str|None — явный нормальный стиль # _style_key_active: str|None — явный активный стиль # _input: QLineEdit — внутренний виджет # # 5) Последовательность действий и вызовов: # __init__(text, placeholder, ...) # -> super().__init__(...) # -> QLineEdit(text) -> setPlaceholderText -> setSizePolicy # -> super().add_widget(_input) # -> style() -> theme_bus.theme_changed.connect(set_theme) # text_changed (property) # -> делегирует к _input.textChanged # # 6) Побочные эффекты: # - Устанавливает stylesheet на QLineEdit. # - Подключается к theme_bus.theme_changed. # # 7) Границы ответственности: # Модуль НЕ валидирует содержимое ввода. # НЕ поддерживает маски ввода (для этого — наследник). # add_widget() заблокирован. # # 8) Обработка ошибок: # add_widget() бросает NotImplementedError. # set_theme() молча игнорирует невалидные значения. # # 9) Инварианты и контракты: # - Контейнер содержит ровно один QLineEdit. # - _theme ∈ {"dark", "light"}. # # 10) Правило сопровождения: # Для добавления валидации — использовать QValidator извне через # _input (расширить API при необходимости). Стили — через APP_STYLES # с ключом TEXT_INPUT + суффиксы.