Files
Dispatch/Dispatch_V0.1.1/gui/components/double_spin_box.py
2026-04-29 08:18:54 +04:00

280 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/double_spin_box.py
"""Обёртка над QDoubleSpinBox с поддержкой централизованных APP_STYLES."""
from PySide6.QtWidgets import QDoubleSpinBox, QSizePolicy
from PySide6.QtCore import Qt, Slot, Signal
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 DoubleSpinBox(SContainer):
"""Кастомный QDoubleSpinBox с переключением стилей по теме."""
stepped = Signal()
class _StepAwareSpinBox(QDoubleSpinBox):
def __init__(self, on_step, parent=None):
super().__init__(parent)
self._on_step = on_step
def stepBy(self, steps: int) -> None: # noqa: N802 (Qt API)
super().stepBy(steps)
if steps and callable(self._on_step):
self._on_step()
def __init__(
self,
min_value: float = 0.0,
max_value: float = 100000.0,
decimals: int = 0,
step: float = 1.0,
suffix: str = " мм",
keyboard_tracking: bool = True,
width_percent: int | None = None,
height_percent: int | None = None,
margin: int | tuple[int, int, int, int] = 0,
style: str = "COORDINATE_INPUT",
active_style: str | None = None,
is_active: bool | None = None,
parent=None,
):
super().__init__(
width_percent=width_percent,
height_percent=height_percent,
margin=margin,
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 = self._StepAwareSpinBox(self._emit_stepped)
self._input.setRange(min_value, max_value)
self._input.setDecimals(decimals)
self._input.setSingleStep(step)
self._input.setSuffix(suffix)
self._input.setAlignment(Qt.AlignCenter)
self._input.setKeyboardTracking(keyboard_tracking)
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:
theme = (theme or "").strip().lower()
if theme not in ("dark", "light"):
return
if self._theme == theme:
return
self._theme = theme
self.style()
def set_value(self, value: float) -> None:
self._input.setValue(value)
def get_value(self) -> float:
return self._input.value()
def get_min_value(self) -> float:
return float(self._input.minimum())
def get_max_value(self) -> float:
return float(self._input.maximum())
@property
def valueChanged(self):
return self._input.valueChanged
@property
def editing_finished(self):
return self._input.editingFinished
@property
def return_pressed(self):
line_edit = self._input.lineEdit()
if line_edit is None:
return self._input.editingFinished
return line_edit.returnPressed
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)
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 set_range(self, min_value: float, max_value: float) -> None:
self._input.setRange(min_value, max_value)
def set_step(self, step: float) -> None:
self._input.setSingleStep(step)
def commit_pending_input(self) -> None:
"""Принудительно применить текст из редактора spinbox к текущему value."""
try:
self._input.interpretText()
except Exception as e:
log_exception(__name__, "commit_pending_input", e)
def _emit_stepped(self) -> None:
self.stepped.emit()
def add_widget(self, widget, alignment=None):
raise NotImplementedError("DoubleSpinBox 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) Экспорт:
# Класс DoubleSpinBox — публичный виджет числового ввода.
# Методы: style(), set_theme(), set_value(), get_value(),
# get_min_value(), get_max_value(), set_range(), set_step(),
# set_enabled(), set_min/max_width/height(), set_fixed_size(),
# set_tooltip(), set_size_policy().
# Свойства: valueChanged, editing_finished.
#
# 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, suffix, keyboard_tracking, ...)
# -> super().__init__(...)
# -> QDoubleSpinBox() с setRange, setDecimals, setSingleStep,
# setSuffix, setAlignment(Center), setKeyboardTracking
# -> super().add_widget(_input)
# -> style() -> theme_bus.theme_changed.connect(set_theme)
# style(style_key?, active_key?, is_active?)
# -> разрешение ключа: явный → base_key + _DARK/_LIGHT + _ACTIVE
# -> _input.setStyleSheet(APP_STYLES[key])
#
# 6) Побочные эффекты:
# - Устанавливает stylesheet на QDoubleSpinBox.
# - Подключается к theme_bus.theme_changed.
#
# 7) Границы ответственности:
# Модуль НЕ валидирует единицы измерения. НЕ конвертирует значения.
# add_widget() заблокирован.
#
# 8) Обработка ошибок:
# add_widget() бросает NotImplementedError. set_theme() игнорирует
# невалидные значения.
#
# 9) Инварианты и контракты:
# - Контейнер содержит ровно один QDoubleSpinBox.
# - Значение в пределах [min_value, max_value].
# - keyboard_tracking определяет, испускается ли valueChanged при каждом
# нажатии клавиши или только по завершении ввода.
#
# 10) Правило сопровождения:
# Отличие от CoordinateInput: суффикс, keyboard_tracking,
# decimals=0 по умолчанию. Не дублировать этот класс для целочисленного ввода —
# достаточно decimals=0.