# -*- coding: utf-8 -*- # gui/components/combo_box.py """Обёртка над QComboBox с централизованными стилями.""" from PySide6.QtWidgets import QComboBox, QSizePolicy 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 class ComboBox(SContainer): """Кастомный QComboBox с темизацией и стилями APP_STYLES.""" def __init__( self, width_percent: int | None = None, height_percent: int | None = None, margin: int | tuple[int, int, int, int] = 0, style: str = "FORM_WIDGET", active_style: str | None = None, is_active: bool | None = None, content_fit: bool = True, parent=None, ): 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._combo = QComboBox() self._combo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) super().add_widget(self._combo) 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.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._combo.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._combo.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_items(self, items: list[str]) -> None: """Заменить список элементов.""" self._combo.clear() self._combo.addItems(items) def set_editable(self, editable: bool) -> None: self._combo.setEditable(editable) def set_enabled(self, enabled: bool) -> None: """Управление доступностью.""" self._combo.setEnabled(enabled) super().setEnabled(enabled) def set_min_width(self, width: int) -> None: """Минимальная ширина.""" self._combo.setMinimumWidth(width) super().setMinimumWidth(width) def set_min_height(self, height: int) -> None: """Минимальная высота.""" self._combo.setMinimumHeight(height) super().setMinimumHeight(height) def set_max_width(self, width: int) -> None: """Максимальная ширина.""" self._combo.setMaximumWidth(width) super().setMaximumWidth(width) def set_max_height(self, height: int) -> None: """Максимальная высота.""" self._combo.setMaximumHeight(height) super().setMaximumHeight(height) def set_fixed_size(self, width: int, height: int) -> None: """Фиксированный размер.""" self._combo.setMinimumSize(width, height) self._combo.setMaximumSize(width, height) super().setMinimumSize(width, height) super().setMaximumSize(width, height) def set_index(self, index: int) -> None: """Установить текущий индекс.""" self._combo.setCurrentIndex(index) def set_current_text(self, text: str) -> None: self._combo.setCurrentText(text) def set_placeholder_text(self, text: str) -> None: """Установить текст-заполнитель (если поддерживается Qt).""" line_edit = self._combo.lineEdit() if line_edit is not None: line_edit.setPlaceholderText(text) if hasattr(self._combo, "setPlaceholderText"): self._combo.setPlaceholderText(text) def get_index(self) -> int: """Получить текущий индекс.""" return self._combo.currentIndex() def get_current_text(self) -> str: return self._combo.currentText() def set_tooltip(self, text: str) -> None: """Подсказка.""" self._combo.setToolTip(text) def set_size_policy(self, horizontal, vertical) -> None: """Политика размеров.""" self._combo.setSizePolicy(horizontal, vertical) super().setSizePolicy(horizontal, vertical) @property def current_index_changed(self): return self._combo.currentIndexChanged @property def current_text_changed(self): return self._combo.currentTextChanged @property def text_edited(self): line_edit = self._combo.lineEdit() if line_edit is None: raise AttributeError("text_edited is unavailable for non-editable ComboBox") return line_edit.textEdited def set_item_enabled(self, index: int, enabled: bool) -> None: """Включить/выключить элемент списка по индексу.""" model = self._combo.model() if model is None: return item = model.item(index) if hasattr(model, "item") else None if item is not None and hasattr(item, "setEnabled"): item.setEnabled(bool(enabled)) def show_popup(self) -> None: self._combo.showPopup() def add_widget(self, widget, alignment=None): raise NotImplementedError("ComboBox can contain only one QComboBox") # --------------------------------------------------------------------------- # Module workflow notes # --------------------------------------------------------------------------- # # 1) Назначение модуля: # Обёртка над QComboBox, встроенная в SContainer, с поддержкой # централизованных стилей APP_STYLES и автоматическим # переключением тем (dark/light) через theme_bus. # # 2) Зависимости модуля: # Импорты: QComboBox, QSizePolicy (PySide6.QtWidgets), # Slot (PySide6.QtCore), # APP_STYLES (gui.styles), # theme_bus (gui.theme_bus), # SContainer (gui.containers.s_container) # Хост-класс / базовый класс: SContainer # Внешние библиотеки: PySide6 (обязательна) # # 3) Экспорт: # Класс ComboBox — публичный виджет выпадающего списка. # Методы: style(), set_theme(), set_items(), set_index(), get_index(), # set_placeholder_text(), set_enabled(), set_item_enabled(), # set_min/max_width/height(), set_fixed_size(), set_tooltip(), # set_size_policy(). # Свойство: current_index_changed — сигнал currentIndexChanged. # # 4) Состояние (поля): # _theme: str — текущая тема ("dark" | "light") # _is_active: bool — признак активного состояния # _base_style_key: str — базовый ключ стиля (по умолчанию "FORM_WIDGET") # _style_key_normal: str|None — явный ключ нормального стиля # _style_key_active: str|None — явный ключ активного стиля # _combo: QComboBox — внутренний виджет # # 5) Последовательность действий и вызовов: # __init__(style="FORM_WIDGET", ...) # -> super().__init__(...) # -> QComboBox() -> setSizePolicy -> super().add_widget(_combo) # -> style() — первичное применение # -> theme_bus.theme_changed.connect(set_theme) # style(style_key?, active_key?, is_active?) # -> определяет ключ через комбинацию base_key + тема + active # -> _combo.setStyleSheet(APP_STYLES[key]) # set_items(items) # -> _combo.clear() -> _combo.addItems(items) # # 6) Побочные эффекты: # - Устанавливает stylesheet на внутренний QComboBox. # - Подключается к theme_bus.theme_changed при создании. # # 7) Границы ответственности: # Модуль НЕ хранит бизнес-данные выбранного элемента. # НЕ валидирует содержимое списка. # add_widget() заблокирован — компонент запечатан. # # 8) Обработка ошибок: # add_widget() бросает NotImplementedError. # set_theme() молча игнорирует невалидные значения. # set_item_enabled() безопасно пропускает отсутствующий model/item. # # 9) Инварианты и контракты: # - Контейнер содержит ровно один QComboBox. # - _theme ∈ {"dark", "light"}. # - Стиль разрешается по цепочке: явный ключ → base_key + суффикс темы. # # 10) Правило сопровождения: # Новые стили — добавлять в APP_STYLES с суффиксами _DARK/_LIGHT/_DARK_ACTIVE/_LIGHT_ACTIVE. # Делегирующие методы дублировать на _combo и super().