so-gui/widgets/drop_tree_widget.py-k

403 lines
15 KiB
Plaintext
Raw Permalink Normal View History

2025-02-01 18:16:23 +01:00
import json
from typing import Optional
from PySide6.QtWidgets import (
QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMessageBox, QMenu
)
from PySide6.QtCore import Qt, QMimeData, QPoint
from PySide6.QtGui import QMouseEvent
# Import komend (Undo/Redo)
from commands import AddItemCommand, RemoveItemCommand
# Import narzędzi kolorowania i wcięć
from utils.colors import color_item_by_title
from utils.indentation import apply_indentation_to_all_columns
# W pliku: drop_tree_widget.py
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QCheckBox,
QPushButton, QSpinBox, QDialogButtonBox, QFormLayout
)
from PySide6.QtCore import Qt
class AddItemDialog(QDialog):
"""
Proste okno dialogowe, pozwalające wprowadzić:
- nr (int)
- opis (string)
- punkty (int, np. -1, 0, 5)
- obowiązkowe (checkbox -> True/False)
"""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Dodaj nowy węzeł")
# Pola formularza
self.nr_spin = QSpinBox()
self.nr_spin.setRange(-9999, 9999)
self.nr_spin.setValue(1)
self.opis_edit = QLineEdit()
self.opis_edit.setPlaceholderText("np. Sprawność rachunkowa...")
self.punkty_spin = QSpinBox()
self.punkty_spin.setRange(-9999, 9999)
self.punkty_spin.setValue(-1)
self.obow_checkbox = QCheckBox("Obowiązkowe?")
# Układ typu FormLayout
form_layout = QFormLayout()
form_layout.addRow("Nr:", self.nr_spin)
form_layout.addRow("Opis:", self.opis_edit)
form_layout.addRow("Punkty:", self.punkty_spin)
form_layout.addRow(self.obow_checkbox)
# Przyciski OK/Cancel
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, parent=self)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
# Główny layout
main_layout = QVBoxLayout()
main_layout.addLayout(form_layout)
main_layout.addWidget(self.button_box)
self.setLayout(main_layout)
# # Podłączamy sygnał itemClicked do własnej metody
# self.itemClicked.connect(self.handle_item_click)
# def handle_item_click(self, item, column):
# """
# Kopiuje zawartość kolumny 3 (opis) do schowka,
# gdy użytkownik kliknie wiersz w drzewie.
# """
# opis = item.text(3) # kolumna 3
# QApplication.clipboard().setText(opis)
# print(f"Skopiowano do schowka opis: {opis}")
def get_data(self) -> dict:
"""
Zwraca słownik z parametrami:
{"nr": int, "opis": str, "punkty": int, "obowiązkowe": bool}
"""
return {
"nr": self.nr_spin.value(),
"opis": self.opis_edit.text().strip(),
"punkty": self.punkty_spin.value(),
"obowiązkowe": self.obow_checkbox.isChecked()
}
class DropTreeWidget(QTreeWidget):
"""
Drzewo docelowe (drop target). Przyjmuje JSON via drag&drop z DragTreeWidget,
umożliwia Undo/Redo, kasowanie, itd.
"""
def __init__(self, undo_stack=None, parent=None):
super().__init__(parent)
self.undo_stack = undo_stack
self.setAcceptDrops(True)
self.setDropIndicatorShown(True)
self.setDragDropMode(QAbstractItemView.DropOnly)
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.drop_event_in_progress = False
def contextMenuEvent(self, event):
"""Wywołane przy kliknięciu prawym przyciskiem myszy (menu kontekstowe)."""
menu = QMenu(self)
add_act = menu.addAction("Dodaj węzeł")
# Możesz dodać więcej akcji, np. "Usuń zaznaczone", ale akurat mamy to na Backspace.
# add_delete_act = menu.addAction("Usuń zaznaczone")
chosen_action = menu.exec(self.mapToGlobal(event.pos()))
if chosen_action == add_act:
self.add_item_interactive()
# elif chosen_action == add_delete_act:
# self.delete_selected_items()
def add_item_interactive(self):
"""
Wywoływane po wybraniu opcji "Dodaj węzeł" z menu kontekstowego.
Otwiera dialog, a następnie tworzy QTreeWidgetItem i wstawia do drzewa
z użyciem AddItemCommand (undo/redo).
"""
from PySide6.QtWidgets import QTreeWidgetItem
from commands import AddItemCommand
from utils.helpers import display_number_or_dash, display_obligatory
dialog = AddItemDialog(self)
if dialog.exec() == QDialog.Accepted:
data = dialog.get_data()
# data ma postać: {"nr": int, "opis": str, "punkty": int, "obowiązkowe": bool}
# Wyznaczamy, gdzie wstawić nowy węzeł:
selected_items = self.selectedItems()
if selected_items:
parent_item = selected_items[0]
index = parent_item.childCount()
else:
parent_item = None
index = self.topLevelItemCount()
# Konwersja wartości do stringów (uwzględniając -1 => '-')
punkty_str = display_number_or_dash(data["punkty"])
# Dla bool w polu obowiązkowe -> 1 (tak) lub 0 (nie)
obow_val = 1 if data["obowiązkowe"] else 0
obow_str = display_obligatory(obow_val)
nr_str = display_number_or_dash(data["nr"])
opis_str = data["opis"]
new_item = QTreeWidgetItem([
punkty_str, # kolumna 0 -> punkty
obow_str, # kolumna 1 -> obowiązkowe
nr_str, # kolumna 2 -> nr
opis_str # kolumna 3 -> opis
])
# Kolorowanie, jeśli używasz color_item_by_title:
from utils.colors import color_item_by_title
color_item_by_title(new_item)
# Undo/Redo tworzymy komendę i wrzucamy na stos
cmd = AddItemCommand(
tree=self,
parent_item=parent_item,
index=index,
cloned_item=new_item,
description="Add new item"
)
if self.undo_stack:
self.undo_stack.push(cmd)
else:
cmd.redo()
# def add_item_interactive(self):
# """
# Wywoływane po wybraniu opcji "Dodaj węzeł" z menu kontekstowego.
# Otwiera dialog, a następnie tworzy QTreeWidgetItem i wstawia do drzewa
# z użyciem AddItemCommand (undo/redo).
# """
# from PySide6.QtWidgets import QTreeWidgetItem
# from commands import AddItemCommand
# from utils.helpers import display_number_or_dash, display_obligatory
# dialog = AddItemDialog(self)
# if dialog.exec() == QDialog.Accepted:
# data = dialog.get_data()
# # data ma postać: {"nr": int, "opis": str, "punkty": int, "obowiązkowe": bool}
# # Wyznaczamy, gdzie wstawić nowy węzeł:
# selected_items = self.selectedItems()
# if selected_items:
# parent_item = selected_items[0]
# index = parent_item.childCount()
# else:
# parent_item = None
# index = self.topLevelItemCount()
# # Konwersja wartości do stringów (uwzględniając -1 => '-')
# punkty_str = display_number_or_dash(data["punkty"])
# # Dla bool w polu obowiązkowe -> 1 (tak) lub 0 (nie)
# obow_val = 1 if data["obowiązkowe"] else 0
# obow_str = display_obligatory(obow_val)
# nr_str = display_number_or_dash(data["nr"])
# opis_str = data["opis"]
# new_item = QTreeWidgetItem([
# punkty_str, # kolumna 0 -> punkty
# obow_str, # kolumna 1 -> obowiązkowe
# nr_str, # kolumna 2 -> nr
# opis_str # kolumna 3 -> opis
# ])
# # Kolorowanie, jeśli używasz color_item_by_title:
# from utils.colors import color_item_by_title
# color_item_by_title(new_item)
# # Undo/Redo tworzymy komendę i wrzucamy na stos
# cmd = AddItemCommand(
# tree=self,
# parent_item=parent_item,
# index=index,
# cloned_item=new_item,
# description="Add new item"
# )
# if self.undo_stack:
# self.undo_stack.push(cmd)
# else:
# cmd.redo()
# -------------------------------------
# Obsługa klawiatury (Backspace, Ctrl+T)
# -------------------------------------
def keyPressEvent(self, event):
if event.key() == Qt.Key_Backspace:
self.delete_selected_items()
event.accept()
elif event.modifiers() & Qt.ControlModifier and event.key() == Qt.Key_T:
self.toggle_expand_selected_items()
event.accept()
else:
super().keyPressEvent(event)
def toggle_expand_selected_items(self):
"""Przełącza rozwinięcie/zwinięcie dla zaznaczonych elementów i ich dzieci."""
selected_items = self.selectedItems()
for item in selected_items:
self.toggle_expand_item_and_children(item)
def toggle_expand_item_and_children(self, item: QTreeWidgetItem):
if item.isExpanded():
self.collapseItem(item)
else:
self.expandItem(item)
for i in range(item.childCount()):
self.toggle_expand_item_and_children(item.child(i))
# -------------------------------------
# Obsługa myszy (zaznaczenie)
# -------------------------------------
def mousePressEvent(self, event: QMouseEvent):
item = self.itemAt(event.position().toPoint())
if item:
if event.modifiers() & Qt.ControlModifier: # Ctrl+klik
item.setSelected(not item.isSelected())
else:
super().mousePressEvent(event)
else:
super().mousePressEvent(event)
def delete_selected_items(self):
"""Usunięcie zaznaczonych elementów (Undo/Redo)."""
selected = self.selectedItems()
for item in reversed(selected):
cmd = RemoveItemCommand(self, item)
self.undo_stack.push(cmd)
# -------------------------------------
# Obsługa drag&drop
# -------------------------------------
def dragEnterEvent(self, event):
if event.mimeData().hasText():
event.acceptProposedAction()
else:
event.ignore()
def dragMoveEvent(self, event):
if event.mimeData().hasText():
event.acceptProposedAction()
else:
event.ignore()
def dropEvent(self, event):
if self.drop_event_in_progress:
event.ignore()
return
self.drop_event_in_progress = True
try:
pos = event.position().toPoint()
target_item = self.itemAt(pos)
try:
data_list = json.loads(event.mimeData().text())
except json.JSONDecodeError:
event.ignore()
return
for node_data in data_list:
self.add_subtree_recursively(target_item, node_data)
apply_indentation_to_all_columns(self)
event.acceptProposedAction()
finally:
self.drop_event_in_progress = False
# -------------------------------------
# Logika dodawania / scalania węzłów
# -------------------------------------
def add_subtree_recursively(self, target_parent: Optional[QTreeWidgetItem], node_data: dict):
"""
Dodaje/scala hierarchię, ignorując zduplikowanych rodziców
na podstawie (nr, opis).
"""
existing_parent = self.find_matching_parent(target_parent, node_data)
if existing_parent:
# Jeżeli już istnieje taki węzeł, scal dzieci
self.merge_all_children(existing_parent, node_data.get("children", []))
else:
# Tworzymy nowy element
new_parent = QTreeWidgetItem([
node_data.get("punkty", "0"),
node_data.get("obowiązkowe", "nie"),
node_data.get("nr", ""),
node_data.get("opis", "")
])
color_item_by_title(new_parent)
if target_parent:
target_parent.addChild(new_parent)
else:
self.addTopLevelItem(new_parent)
# Dodaj dzieci rekurencyjnie
self.merge_all_children(new_parent, node_data.get("children", []))
def find_matching_parent(self, target_parent: Optional[QTreeWidgetItem], node_data: dict) -> Optional[QTreeWidgetItem]:
"""Znajduje dziecko w target_parent (lub top-level) pasujące (nr, opis)."""
return self.find_child_by_attributes(target_parent, node_data.get("nr", ""), node_data.get("opis", ""))
def merge_all_children(self, parent_item: QTreeWidgetItem, children_data: list):
"""Scal wszystkie dzieci z danymi JSON."""
for child_data in children_data:
existing_child = self.find_child_by_attributes(parent_item, child_data.get("nr", ""), child_data.get("opis", ""))
if existing_child:
# Jeżeli już istnieje, rekurencyjnie scal
self.merge_all_children(existing_child, child_data.get("children", []))
else:
new_child = QTreeWidgetItem([
child_data.get("punkty", "0"),
child_data.get("obowiązkowe", "nie"),
child_data.get("nr", ""),
child_data.get("opis", "")
])
color_item_by_title(new_child)
parent_item.addChild(new_child)
self.merge_all_children(new_child, child_data.get("children", []))
# Ewentualnie można tu sortować po numerach
# self.sort_children_by_nr(parent_item)
def find_child_by_attributes(self, parent_item: Optional[QTreeWidgetItem], nr: str, opis: str) -> Optional[QTreeWidgetItem]:
"""Wyszukuje dziecko o danych atrybutach (nr, opis)."""
if parent_item:
for i in range(parent_item.childCount()):
child = parent_item.child(i)
if child.text(2).strip() == nr.strip() and child.text(3).strip() == opis.strip():
return child
else:
for i in range(self.topLevelItemCount()):
item = self.topLevelItem(i)
if item.text(2).strip() == nr.strip() and item.text(3).strip() == opis.strip():
return item
return None