Files
2026-04-29 08:18:54 +04:00

282 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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().