172 lines
6.2 KiB
Python
172 lines
6.2 KiB
Python
# -*- 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)
|