so-gui/widgets/drop_tree_widget.py-k
baiobelfer 2017e7a4c5 init
2025-02-01 18:16:23 +01:00

403 lines
15 KiB
Plaintext
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.

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