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) """