# -*- coding: utf-8 -*- # hub/ticket/ui/ticket_selection_list.py """Контейнерный список выбора Ticket на локальной GUI-библиотеке.""" from __future__ import annotations from dataclasses import dataclass from PySide6.QtCore import Qt, Signal from gui.components import Label, VSpring from gui.containers import ScrollContainer, SContainer, VContainer @dataclass(frozen=True, slots=True) class TicketSelectionEntry: """Описывает одну запись в контейнерном списке Ticket.""" entry_id: object title: str subtitle: str = "" class _TicketSelectionItem(SContainer): """Визуальный элемент выбора записи Ticket.""" clicked = Signal(object) activated = Signal(object) def __init__( self, entry: TicketSelectionEntry, parent=None, ): super().__init__( margin=0, spacing=2, content_fit=True, parent=parent, style="TICKET_LIST_ITEM", active_style="TICKET_LIST_ITEM_SELECTED", is_active=False, ) self._entry = entry self.setCursor(Qt.CursorShape.PointingHandCursor) self._setup_ui() @property def entry_id(self) -> object: return self._entry.entry_id def set_selected(self, selected: bool) -> None: self.style(is_active=selected) def mousePressEvent(self, event) -> None: if event.button() == Qt.MouseButton.LeftButton: self.clicked.emit(self._entry.entry_id) super().mousePressEvent(event) def mouseDoubleClickEvent(self, event) -> None: if event.button() == Qt.MouseButton.LeftButton: self.clicked.emit(self._entry.entry_id) self.activated.emit(self._entry.entry_id) super().mouseDoubleClickEvent(event) def _setup_ui(self) -> None: # Body элемента списка: вертикальный блок title/subtitle внутри кликабельной записи. body = VContainer(margin=10, spacing=2, parent=self) body.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True) title_label = Label( self._entry.title, alignment="left", style="TICKET_LIST_TITLE", ) title_label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True) body.add_widget(title_label) if self._entry.subtitle: subtitle_label = Label( self._entry.subtitle, alignment="left", style="TICKET_LIST_SUBTITLE", ) subtitle_label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True) body.add_widget(subtitle_label) class TicketSelectionList(SContainer): """Переиспользуемый список выбора на контейнерах и локальных компонентах.""" selection_changed = Signal(object) item_activated = Signal(object) def __init__(self, parent=None): super().__init__(spacing=0, parent=parent) self._items: dict[object, _TicketSelectionItem] = {} self._current_entry_id: object | None = None self._items_host: VContainer | None = None self._setup_ui() def set_entries(self, entries: list[TicketSelectionEntry]) -> None: previous_entry_id = self._current_entry_id self.clear_entries() for entry in entries: self._add_entry(entry) if not self._items: self._current_entry_id = None self.selection_changed.emit(None) return target_entry_id = previous_entry_id if previous_entry_id in self._items else entries[0].entry_id self.set_current_entry(target_entry_id) def clear_entries(self) -> None: if self._items_host is None: self._items.clear() self._current_entry_id = None return for item in list(self._items.values()): self._items_host.remove_widget(item) item.setParent(None) self._items.clear() self._current_entry_id = None def current_entry_id(self) -> object | None: return self._current_entry_id def has_selection(self) -> bool: return self._current_entry_id is not None def set_current_entry(self, entry_id: object | None) -> None: normalized_entry_id = entry_id if entry_id in self._items else None if normalized_entry_id == self._current_entry_id: return self._current_entry_id = normalized_entry_id for item_entry_id, item in self._items.items(): item.set_selected(item_entry_id == normalized_entry_id) self.selection_changed.emit(normalized_entry_id) def _setup_ui(self) -> None: # Scroll-контейнер списка: внешняя прокручиваемая оболочка всех записей TicketSelectionList. scroll = ScrollContainer( margin=0, content_margins=[0, 0, 0, 0], spacing=6, orientation="v", vertical_scroll_bar_policy="as_needed", horizontal_scroll_bar_policy="always_off", style="SCROLL_CONTAINER", parent=self, ) # Items-host: вертикальный стек элементов списка с нижней пружиной для прилипания вверх. self._items_host = VContainer(spacing=6, parent=scroll) self._items_host.add_widget(VSpring()) def _add_entry(self, entry: TicketSelectionEntry) -> None: if self._items_host is None: return item = _TicketSelectionItem(entry) item.clicked.connect(self._on_item_clicked) item.activated.connect(self._on_item_activated) self._items[entry.entry_id] = item self._items_host.insert_widget(len(self._items) - 1, item) def _on_item_clicked(self, entry_id: object) -> None: self.set_current_entry(entry_id) def _on_item_activated(self, entry_id: object) -> None: if entry_id != self._current_entry_id: self.set_current_entry(entry_id) self.item_activated.emit(entry_id)