Add Dispatch_V0.1.1
This commit is contained in:
279
Dispatch_V0.1.1/gui/components/double_spin_box.py
Normal file
279
Dispatch_V0.1.1/gui/components/double_spin_box.py
Normal file
@@ -0,0 +1,279 @@
|
||||
# -*- 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.
|
||||
Reference in New Issue
Block a user