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

445 lines
16 KiB
Python

import json
from typing import Optional
from PySide6.QtWidgets import (
QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMessageBox, QMenu,
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QCheckBox,
QPushButton, QSpinBox, QDialogButtonBox, QFormLayout, QPlainTextEdit,
QInputDialog, QStyledItemDelegate, QStyleOptionViewItem
)
from PySide6.QtCore import Qt, QPoint, QModelIndex
from PySide6.QtGui import QMouseEvent, QPainter
# --------------------------------------------------------------------
# DELEGAT DO WCIĘĆ (IndentationDelegate)
# --------------------------------------------------------------------
class IndentationDelegate(QStyledItemDelegate):
"""
Delegate rysujący wcięcie w KAŻDEJ kolumnie,
zależne od poziomu zagnieżdżenia w drzewie (rodziców).
Nie modyfikuje item.text().
"""
def paint(self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex):
level = self.get_depth(index)
indent_px = 20 * level # 20 pikseli na poziom
new_option = QStyleOptionViewItem(option)
# przesunięcie rect w prawo o indent_px
new_option.rect.translate(indent_px, 0)
# zmniejszenie szerokości o indent_px
new_option.rect.setWidth(new_option.rect.width() - indent_px)
super().paint(painter, new_option, index)
def get_depth(self, index: QModelIndex) -> int:
depth = 0
model = index.model()
parent_idx = model.parent(index)
while parent_idx.isValid():
depth += 1
parent_idx = model.parent(parent_idx)
return depth
from PySide6.QtWidgets import QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMessageBox, QMenu
from PySide6.QtCore import Qt, QPoint
from PySide6.QtGui import QMouseEvent
# Import komend (Undo/Redo)
from commands import AddItemCommand, RemoveItemCommand, EditPunktyCommand
# Narzędzia kolorowania i wcięć
from utils.colors import color_item_by_title
from utils.indentation import apply_indentation_to_all_columns
###############################################################################
# AddItemDialog #
###############################################################################
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ł")
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?")
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)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, parent=self)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
main_layout = QVBoxLayout()
main_layout.addLayout(form_layout)
main_layout.addWidget(self.button_box)
self.setLayout(main_layout)
def get_data(self) -> dict:
return {
"nr": self.nr_spin.value(),
"opis": self.opis_edit.text().strip(),
"punkty": self.punkty_spin.value(),
"obowiązkowe": self.obow_checkbox.isChecked()
}
###############################################################################
# DropTreeWidget #
###############################################################################
class DropTreeWidget(QTreeWidget):
"""
Drzewo docelowe (drop target). Przyjmuje JSON via drag&drop z DragTreeWidget,
umożliwia Undo/Redo, kasowanie, edycję punktów, itd.
Dodajemy IndentationDelegate do rysowania wcięć we wszystkich kolumnach.
"""
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
# ------- Ustaw delegata do wcięć w 4 kolumnach -------
delegate = IndentationDelegate(self)
for col in range(4):
self.setItemDelegateForColumn(col, delegate)
# -----------------------------------------------------
# ----------------------------------------------------------------
# KONTEKSTOWE MENU (PRAWY KLIK)
# ----------------------------------------------------------------
def contextMenuEvent(self, event):
menu = QMenu(self)
add_act = menu.addAction("Dodaj węzeł")
paste_act = menu.addAction("Wklej JSON Subtree")
# Jeśli mamy 1 zaznaczony węzeł z punktem != -1, pokaż edycję
selected_items = self.selectedItems()
edit_punkty_act = None
if len(selected_items) == 1:
item = selected_items[0]
punkty_str = item.text(0).strip()
if punkty_str not in ("-1", "-", ""):
try:
val = int(punkty_str)
if val != -1:
edit_punkty_act = menu.addAction("Edytuj punktację (1..10)")
except ValueError:
pass
chosen_action = menu.exec(self.mapToGlobal(event.pos()))
if chosen_action == add_act:
self.add_item_interactive()
elif chosen_action == paste_act:
self.paste_json_subtree()
elif chosen_action == edit_punkty_act:
self.edit_punkty_1_10(selected_items[0])
def add_item_interactive(self):
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()
selected_items = self.selectedItems()
if selected_items:
parent_item = selected_items[0]
index = parent_item.childCount()
else:
parent_item = None
index = self.topLevelItemCount()
punkty_str = display_number_or_dash(data["punkty"])
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, obow_str, nr_str, opis_str])
color_item_by_title(new_item)
cmd = AddItemCommand(self, parent_item, index, new_item, "Add new item")
if self.undo_stack:
self.undo_stack.push(cmd)
else:
cmd.redo()
# ----------------------------------------------------------------
# EDYTOWANIE PUNKTÓW (1..10) - QInputDialog, Undo/Redo
# ----------------------------------------------------------------
def edit_punkty_1_10(self, item: QTreeWidgetItem):
from commands import EditPunktyCommand
old_val_str = item.text(0).strip()
try:
old_val_int = int(old_val_str)
except ValueError:
return
new_val_int, ok = QInputDialog.getInt(
self,
"Edytuj punktację",
f"Aktualna wartość = {old_val_int}. Wybierz nową (1..10):",
old_val_int,
1, # min
10 # max
)
if not ok:
return
if new_val_int == old_val_int:
return
cmd = EditPunktyCommand(item, str(old_val_int), str(new_val_int), "Edit punkty")
if self.undo_stack:
self.undo_stack.push(cmd)
else:
cmd.redo()
# -------------------------------------
# Shift + = / Shift + - -> +/- 1
# -------------------------------------
def keyPressEvent(self, event):
if event.modifiers() & Qt.ShiftModifier:
if event.key() in (Qt.Key_Equal, Qt.Key_Plus):
self.handle_punkty_inc_dec(+1)
return
elif event.key() in (Qt.Key_Minus, Qt.Key_Underscore):
self.handle_punkty_inc_dec(-1)
return
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 handle_punkty_inc_dec(self, inc: int):
from commands import EditPunktyCommand
sel = self.selectedItems()
if len(sel) != 1:
return
item = sel[0]
old_val_str = item.text(0).strip()
try:
old_val_int = int(old_val_str)
except ValueError:
return
if old_val_int == -1:
return
new_val_int = old_val_int + inc
new_val_int = max(1, min(10, new_val_int))
if new_val_int == old_val_int:
return
cmd = EditPunktyCommand(item, str(old_val_int), str(new_val_int), "Inc/Dec punkty")
if self.undo_stack:
self.undo_stack.push(cmd)
else:
cmd.redo()
# ----------------------------------------------------------------
# USUWANIE - Undo/Redo
# ----------------------------------------------------------------
def delete_selected_items(self):
selected = self.selectedItems()
for item in reversed(selected):
cmd = RemoveItemCommand(self, item)
if self.undo_stack:
self.undo_stack.push(cmd)
else:
cmd.redo()
# ----------------------------------------------------------------
# Expand toggling (np. Ctrl+T w MainWindow)
# ----------------------------------------------------------------
def toggle_expand_selected_items(self):
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:
item.setSelected(not item.isSelected())
else:
super().mousePressEvent(event)
else:
super().mousePressEvent(event)
# -------------------------------------
# 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)
# POZOSTAWIAMY Twoją logikę (apply_indentation_to_all_columns),
# choć delegat robi wcięcia.
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):
existing_parent = self.find_matching_parent(target_parent, node_data)
if existing_parent:
self.merge_all_children(existing_parent, node_data.get("children", []))
else:
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)
self.merge_all_children(new_parent, node_data.get("children", []))
def find_matching_parent(self, target_parent: Optional[QTreeWidgetItem], node_data: dict) -> Optional[QTreeWidgetItem]:
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):
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:
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", []))
def find_child_by_attributes(self, parent_item: Optional[QTreeWidgetItem], nr: str, opis: str) -> Optional[QTreeWidgetItem]:
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
# ---- [PONIŻEJ DODAJEMY DELEGAT DO WCIĘĆ] ----
class IndentationDelegate(QStyledItemDelegate):
"""
Delegate rysujący wcięcia w każdej kolumnie w zależności od poziomu
zagnieżdżenia w drzewie. Nie modyfikuje samego tekstu w węźle.
"""
def paint(self, painter, option, index):
level = self.get_depth(index)
indent_px = 20 * level
new_option = QStyleOptionViewItem(option)
new_option.rect.translate(indent_px, 0)
new_option.rect.setWidth(new_option.rect.width() - indent_px)
super().paint(painter, new_option, index)
def get_depth(self, index):
depth = 0
model = index.model()
parent_index = model.parent(index)
while parent_index.isValid():
depth += 1
parent_index = model.parent(parent_index)
return depth
# (Na końcu pliku) W konstruktorze DropTreeWidget
# dopisz:
"""
delegate = IndentationDelegate(self)
for col in range(4):
self.setItemDelegateForColumn(col, delegate)
"""