# -*- coding: utf-8 -*- # gui/components/coordinate_input.py """Виджет для ввода координат""" from PySide6.QtWidgets import QDoubleSpinBox, QSizePolicy from PySide6.QtCore import Qt, Slot from gui.containers.s_container import SContainer from gui.styles import APP_STYLES from gui.theme_bus import theme_bus from error_logger import log_exception class CoordinateInput(SContainer): """Виджет для ввода координат с валидацией""" def __init__( self, min_value: float = 0.0, max_value: float = 100000.0, decimals: int = 6, step: float = 0.000001, min_width: int = 150, alignment: Qt.Alignment = Qt.AlignCenter, parent=None, style: str = "COORDINATE_INPUT", active_style: str | None = None, is_active: bool | None = None, ): super().__init__( width_percent=None, height_percent=None, margin=0, style=style, active_style=active_style, is_active=is_active, 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._input = QDoubleSpinBox() self._input.setRange(min_value, max_value) self._input.setDecimals(decimals) self._input.setSingleStep(step) self._input.setMinimumWidth(min_width) self._input.setAlignment(alignment) 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.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_prefix(self, prefix: str): """Установка префикса""" self._input.setPrefix(prefix) def set_range(self, min_val, max_val): """Установка диапазона""" self._input.setRange(min_val, max_val) def set_decimals(self, decimals: int): """Установка количества десятичных знаков""" self._input.setDecimals(decimals) def set_step(self, step: float) -> None: """Установка шага""" self._input.setSingleStep(step) def set_value(self, value): """Безопасная установка значения""" try: self._input.setValue(float(value)) except (ValueError, TypeError) as _exc: log_exception(__name__, "set_value", _exc) def get_value(self): return self._input.value() @property def valueChanged(self): """Предоставить сигнал valueChanged из внутреннего QDoubleSpinBox.""" return self._input.valueChanged def set_enabled(self, enabled: bool) -> None: self._input.setEnabled(enabled) super().setEnabled(enabled) def set_min_width(self, width: int) -> None: self._input.setMinimumWidth(width) def set_min_height(self, height: int) -> None: self._input.setMinimumHeight(height) def set_max_width(self, width: int) -> None: self._input.setMaximumWidth(width) def set_max_height(self, height: int) -> None: self._input.setMaximumHeight(height) def set_fixed_size(self, width: int, height: int) -> None: self._input.setMinimumSize(width, height) self._input.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("CoordinateInput can contain only one QDoubleSpinBox") # --------------------------------------------------------------------------- # Module workflow notes # --------------------------------------------------------------------------- # # 1) Назначение модуля: # Виджет для ввода координат (широта, долгота и т.д.) на базе # QDoubleSpinBox внутри SContainer, с поддержкой валидации # диапазона, настраиваемой точностью и стилями APP_STYLES. # # 2) Зависимости модуля: # Импорты: QDoubleSpinBox, QSizePolicy (PySide6.QtWidgets), # Qt, Slot (PySide6.QtCore), # SContainer (gui.containers.s_container), # APP_STYLES (gui.styles), # theme_bus (gui.theme_bus) # Хост-класс / базовый класс: SContainer # Внешние библиотеки: PySide6 (обязательна) # # 3) Экспорт: # Класс CoordinateInput — публичный виджет ввода координат. # Методы: style(), set_theme(), set_prefix(), set_range(), # set_decimals(), set_step(), set_value(), get_value(), # set_enabled(), set_min/max_width/height(), set_fixed_size(), # set_tooltip(), set_size_policy(). # Свойство: valueChanged — сигнал QDoubleSpinBox.valueChanged. # # 4) Состояние (поля): # _theme: str — текущая тема # _is_active: bool — признак активного состояния # _base_style_key: str — базовый ключ стиля ("COORDINATE_INPUT") # _style_key_normal: str|None — явный ключ нормального стиля # _style_key_active: str|None — явный ключ активного стиля # _input: QDoubleSpinBox — внутренний виджет ввода # # 5) Последовательность действий и вызовов: # __init__(min_value, max_value, decimals, step, min_width, alignment, ...) # -> super().__init__(...) # -> QDoubleSpinBox() с setRange, setDecimals, setSingleStep, setMinimumWidth # -> super().add_widget(_input) # -> style() -> theme_bus.theme_changed.connect(set_theme) # set_value(value) # -> try float(value) -> _input.setValue() # -> except: pass (тихое игнорирование) # valueChanged (property) # -> делегирует к _input.valueChanged # # 6) Побочные эффекты: # - Устанавливает stylesheet на QDoubleSpinBox. # - Подключается к theme_bus.theme_changed. # # 7) Границы ответственности: # Модуль НЕ интерпретирует значения координат семантически. # НЕ выполняет геокодирование. add_widget() заблокирован. # # 8) Обработка ошибок: # set_value() глотает ValueError/TypeError при некорректном вводе. # add_widget() бросает NotImplementedError. # set_theme() молча игнорирует невалидные темы. # # 9) Инварианты и контракты: # - Контейнер содержит ровно один QDoubleSpinBox. # - Значение всегда в пределах [min_value, max_value]. # - decimals определяет точность отображения. # # 10) Правило сопровождения: # При добавлении суффикса/префикса — использовать set_prefix(). # Стили — через APP_STYLES с ключом COORDINATE_INPUT + суффиксы.