290 lines
11 KiB
Python
290 lines
11 KiB
Python
# -*- 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 + суффиксы.
|