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

278 lines
10 KiB
Python
Raw 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/photo_view_widget.py
"""Виджет для отображения фотографий видов"""
from pathlib import Path
from PySide6.QtWidgets import QLabel, QSizePolicy
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QPixmap
from gui.containers import VContainer, SContainer
from gui.styles import APP_STYLES
class PhotoViewWidget(SContainer):
"""Виджет для отображения фотографий видов"""
error_occurred = Signal(str)
def __init__(self, content_fit: bool = True):
super().__init__(width_percent=None, height_percent=None)
self._content_fit = content_fit
self._current_zoom = 1.0
self._current_view = None
self._images = {} # {имя_вида: QPixmap}
self._base_pixmap = None
self.setup_ui()
def setup_ui(self):
main_container = VContainer(
content_fit=self._content_fit,
parent=self,
)
self._image_label = QLabel()
self._image_label.setAlignment(Qt.AlignCenter)
self._image_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self._image_label.setMinimumSize(1, 1)
# Важно: scaledContents=False, иначе некорректно комбинируется с зумом/KeepAspectRatio
self._image_label.setScaledContents(False)
# Устанавливаем фон как у главного окна
self.setStyleSheet(APP_STYLES.get("VIEW_WIDGET_BACKGROUND", ""))
self._image_label.setStyleSheet(APP_STYLES.get("VIEW_IMAGE_BACKGROUND", ""))
main_container.add_widget_with_stretch(self._image_label, 1)
def load_images(self, images_dict):
"""Загрузка изображений"""
self.clear_images()
if not images_dict:
self.show_error("Изображения не указаны")
return
loaded_count = 0
for view_name, image_path in images_dict.items():
p = Path(image_path)
if not p.exists():
continue
pixmap = QPixmap(str(p))
if pixmap.isNull():
continue
self._images[view_name] = pixmap
loaded_count += 1
if loaded_count == 0:
self.show_error("Не удалось загрузить ни одного изображения")
return
first_view = list(self._images.keys())[0]
self.show_view(first_view)
def clear_images(self):
"""Очистка изображений"""
self._images.clear()
self._current_view = None
self._base_pixmap = None
self._image_label.clear()
self._current_zoom = 1.0
def show_view(self, view_name):
"""Показать изображение вида"""
if view_name not in self._images:
return
self._current_view = view_name
self._base_pixmap = self._images[view_name]
self.update_display()
def switch_view_cycle(self, preferred_order):
"""Циклическое переключение вида с предпочтительным порядком"""
if not self._images or not self._current_view:
return
available_views = [v for v in preferred_order if v in self._images]
if not available_views:
return
if self._current_view in available_views:
current_index = available_views.index(self._current_view)
next_index = (current_index + 1) % len(available_views)
else:
next_index = 0
self.show_view(available_views[next_index])
def switch_to_isometric_or_next(self):
"""Переключение на изометрический вид или следующий в цикле"""
if not self._images:
return
if 'isometric' in self._images and self._current_view != 'isometric':
self.show_view('isometric')
return
available_views = list(self._images.keys())
if not available_views:
return
if self._current_view in available_views:
i = available_views.index(self._current_view)
self.show_view(available_views[(i + 1) % len(available_views)])
else:
self.show_view(available_views[0])
def update_display(self):
"""Обновление отображения изображения с учетом зума"""
if self._base_pixmap is None or self._base_pixmap.isNull():
return
# Базовый размер — размер label, дальше умножаем на zoom
target = self._image_label.size()
if target.width() <= 1 or target.height() <= 1:
return
w = max(1, int(target.width() * self._current_zoom))
h = max(1, int(target.height() * self._current_zoom))
scaled = self._base_pixmap.scaled(w, h, Qt.KeepAspectRatio, Qt.SmoothTransformation)
self._image_label.setPixmap(scaled)
def zoom_in(self):
self._current_zoom = min(self._current_zoom * 1.2, 5.0)
self.update_display()
def zoom_out(self):
self._current_zoom = max(self._current_zoom / 1.2, 0.2)
self.update_display()
def reset_zoom(self):
self._current_zoom = 1.0
self.update_display()
def show_error(self, message):
self.clear_images()
self.error_occurred.emit(message)
def wheelEvent(self, event):
if self._images:
delta = event.angleDelta().y()
if delta > 0:
self.zoom_in()
else:
self.zoom_out()
event.accept()
def resizeEvent(self, event):
super().resizeEvent(event)
self.update_display()
def set_enabled(self, enabled: bool) -> None:
self.setEnabled(enabled)
def set_min_width(self, width: int) -> None:
self.setMinimumWidth(width)
def set_min_height(self, height: int) -> None:
self.setMinimumHeight(height)
def set_max_width(self, width: int) -> None:
self.setMaximumWidth(width)
def set_max_height(self, height: int) -> None:
self.setMaximumHeight(height)
def set_fixed_size(self, width: int, height: int) -> None:
self.setMinimumSize(width, height)
self.setMaximumSize(width, height)
def set_tooltip(self, text: str) -> None:
self.setToolTip(text)
def set_size_policy(self, horizontal, vertical) -> None:
self.setSizePolicy(horizontal, vertical)
# ---------------------------------------------------------------------------
# Module workflow notes
# ---------------------------------------------------------------------------
#
# 1) Назначение модуля:
# Виджет для отображения фотографий видов детали с поддержкой
# зума (колесо мыши), циклического переключения видов и
# масштабирования с сохранением пропорций (KeepAspectRatio).
#
# 2) Зависимости модуля:
# Импорты: Path (pathlib),
# QLabel, QSizePolicy (PySide6.QtWidgets),
# Qt, Signal (PySide6.QtCore),
# QPixmap (PySide6.QtGui),
# VContainer, SContainer (gui.containers),
# APP_STYLES (gui.styles)
# Хост-класс / базовый класс: SContainer
# Внешние библиотеки: PySide6 (обязательна)
#
# 3) Экспорт:
# Класс PhotoViewWidget — виджет фотогалереи.
# Сигнал: error_occurred(str).
# Методы: load_images(dict), clear_images(), show_view(str),
# switch_view_cycle(list), switch_to_isometric_or_next(),
# update_display(), zoom_in(), zoom_out(), reset_zoom(),
# show_error().
#
# 4) Состояние (поля):
# _content_fit: bool — режим подгонки
# _current_zoom: float — текущий множитель зума (1.0 = 100%)
# _current_view: str|None — имя текущего вида
# _images: dict — {имя_вида: QPixmap}
# _base_pixmap: QPixmap|None— исходный pixmap текущего вида
# _image_label: QLabel — виджет отображения изображения
#
# 5) Последовательность действий и вызовов:
# __init__(content_fit)
# -> super().__init__(...)
# -> setup_ui() — VContainer + QLabel с Expanding + stylesheet
# load_images(images_dict)
# -> clear_images() -> для каждого пути: Path.exists() -> QPixmap
# -> show_view(first_view)
# show_view(view_name)
# -> _base_pixmap = _images[view_name] -> update_display()
# update_display()
# -> размер label * _current_zoom -> scaled(KeepAspectRatio, SmoothTransformation)
# -> _image_label.setPixmap(scaled)
# wheelEvent(event)
# -> delta > 0: zoom_in() | delta < 0: zoom_out()
# resizeEvent(event)
# -> update_display() — адаптация к новому размеру
#
# 6) Побочные эффекты:
# - Загружает файлы изображений с диска.
# - Устанавливает stylesheet через APP_STYLES.
# - Испускает error_occurred при ошибках.
#
# 7) Границы ответственности:
# Модуль НЕ управляет источником изображений (каталогом).
# НЕ редактирует файлы. НЕ подключается к theme_bus.
#
# 8) Обработка ошибок:
# load_images() проверяет Path.exists() и QPixmap.isNull().
# show_error() очищает изображения и испускает error_occurred.
#
# 9) Инварианты и контракты:
# - _current_zoom ∈ [0.2, 5.0].
# - scaledContents = False — масштабирование управляется вручную.
# - Если _base_pixmap is None — update_display() не делает ничего.
#
# 10) Правило сопровождения:
# Для добавления поддержки drag-and-drop или аннотаций — расширять
# подкласс, не модифицировать этот файл. Зум-границы (0.25.0) —
# константы в zoom_in/zoom_out.