333 lines
12 KiB
Python
333 lines
12 KiB
Python
![]() |
import os
|
||
|
import json
|
||
|
|
||
|
from PySide6.QtWidgets import (
|
||
|
QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel,
|
||
|
QToolBar, QFileDialog, QMessageBox
|
||
|
)
|
||
|
from PySide6.QtGui import (QKeySequence, QAction, QWheelEvent)
|
||
|
from PySide6.QtCore import Qt
|
||
|
from PySide6.QtGui import QUndoStack
|
||
|
|
||
|
# Importy lokalne
|
||
|
from widgets.drag_tree_widget import DragTreeWidget
|
||
|
from widgets.drop_tree_widget import DropTreeWidget
|
||
|
from widgets.common_tree_delegate import CommonTreeDelegate
|
||
|
from utils.colors import color_item_by_title
|
||
|
from utils.indentation import apply_indentation_to_all_columns
|
||
|
|
||
|
from utils.helpers import display_number_or_dash, display_obligatory
|
||
|
|
||
|
|
||
|
class MainWindow(QMainWindow):
|
||
|
def __init__(self):
|
||
|
super().__init__()
|
||
|
self.setWindowTitle("Drag & Drop - Dół -> Góra, Pastelowe kolory węzłów i dzieci")
|
||
|
|
||
|
self.undo_stack = QUndoStack(self)
|
||
|
|
||
|
# Górne drzewo: DROP (puste na starcie)
|
||
|
self.drop_tree = DropTreeWidget(self.undo_stack)
|
||
|
self.drop_tree.setHeaderLabels(["Punkty", "Obowiązkowe", "Nr", "Opis"])
|
||
|
self.configure_header(self.drop_tree)
|
||
|
|
||
|
# Dolne drzewo: DRAG (załadowane z pp.json)
|
||
|
self.drag_tree = DragTreeWidget()
|
||
|
self.drag_tree.setHeaderLabels(["Punkty", "Obowiązkowe", "Nr", "Opis"])
|
||
|
self.configure_header(self.drag_tree)
|
||
|
|
||
|
self.init_ui()
|
||
|
self.resize(1100, 700)
|
||
|
|
||
|
# Ładujemy ./data/pp.json -> DOLNE drzewo
|
||
|
self.init_load_data() # <-- kluczowy fragment
|
||
|
|
||
|
# Stosujemy wcięcia dla obu drzew
|
||
|
apply_indentation_to_all_columns(self.drag_tree)
|
||
|
apply_indentation_to_all_columns(self.drop_tree)
|
||
|
|
||
|
def configure_header(self, tw):
|
||
|
"""Ustawienie parametrów nagłówka w TreeWidget."""
|
||
|
hd = tw.header()
|
||
|
# Możesz tu dostosować szerokości kolumn itd.
|
||
|
hd.setStretchLastSection(True)
|
||
|
|
||
|
def init_ui(self):
|
||
|
"""Tworzenie toolbaru i layoutu głównego."""
|
||
|
tb = QToolBar("Toolbar", self)
|
||
|
self.addToolBar(tb)
|
||
|
|
||
|
# Undo / Redo
|
||
|
undo_act = self.undo_stack.createUndoAction(self, "Undo")
|
||
|
undo_act.setShortcut(QKeySequence.Undo)
|
||
|
tb.addAction(undo_act)
|
||
|
|
||
|
redo_act = self.undo_stack.createRedoAction(self, "Redo")
|
||
|
redo_act.setShortcut(QKeySequence("Ctrl+Y"))
|
||
|
tb.addAction(redo_act)
|
||
|
|
||
|
tb.addSeparator()
|
||
|
|
||
|
# Przyciski import/export dla testów (opcjonalnie)
|
||
|
imp_d = QAction("Import Dolne", self)
|
||
|
imp_d.triggered.connect(lambda: self.import_tree(self.drag_tree, True))
|
||
|
|
||
|
exp_d = QAction("Export Dolne", self)
|
||
|
exp_d.triggered.connect(lambda: self.export_tree(self.drag_tree))
|
||
|
|
||
|
imp_g = QAction("Import Górne", self)
|
||
|
imp_g.triggered.connect(lambda: self.import_tree(self.drop_tree, False))
|
||
|
|
||
|
exp_g = QAction("Export Górne", self)
|
||
|
exp_g.triggered.connect(lambda: self.export_tree(self.drop_tree))
|
||
|
|
||
|
tb.addAction(imp_d)
|
||
|
tb.addAction(exp_d)
|
||
|
tb.addAction(imp_g)
|
||
|
tb.addAction(exp_g)
|
||
|
|
||
|
# Dodaj akcję Ctrl + T do rozwijania/zwijania
|
||
|
toggle_expand_action = QAction("Toggle Expand/Collapse (Ctrl+T)", self)
|
||
|
toggle_expand_action.setShortcut(QKeySequence("Ctrl+T"))
|
||
|
toggle_expand_action.triggered.connect(self.drop_tree.toggle_expand_selected_items)
|
||
|
tb.addAction(toggle_expand_action)
|
||
|
|
||
|
# Dodaj akcję usuwania
|
||
|
delete_action = QAction("Usuń zaznaczone (Backspace)", self)
|
||
|
delete_action.setShortcut(QKeySequence(Qt.Key_Backspace))
|
||
|
delete_action.triggered.connect(self.drop_tree.delete_selected_items)
|
||
|
tb.addAction(delete_action)
|
||
|
|
||
|
# Ustaw kontekst skrótu tylko dla drzewa docelowego
|
||
|
delete_action.setShortcutContext(Qt.WidgetWithChildrenShortcut)
|
||
|
self.drop_tree.addAction(delete_action)
|
||
|
|
||
|
# Layout główny
|
||
|
layout = QVBoxLayout()
|
||
|
layout.addWidget(QLabel("System Oceniania"))
|
||
|
layout.addWidget(self.drop_tree)
|
||
|
layout.addWidget(QLabel("Podstawa Programowa"))
|
||
|
layout.addWidget(self.drag_tree)
|
||
|
|
||
|
container = QWidget()
|
||
|
container.setLayout(layout)
|
||
|
self.setCentralWidget(container)
|
||
|
|
||
|
# ----------------------------------------------------------------
|
||
|
# Wczytywanie pliku pp.json do DRAG TREE (dolnego)
|
||
|
# ----------------------------------------------------------------
|
||
|
# main.py (fragment)
|
||
|
def init_load_data(self):
|
||
|
path = os.path.join("data", "pp.json")
|
||
|
if not os.path.isfile(path):
|
||
|
print("Brak pliku data/pp.json. Pomijam.")
|
||
|
return
|
||
|
|
||
|
try:
|
||
|
with open(path, "r", encoding="utf-8") as f:
|
||
|
data = json.load(f)
|
||
|
|
||
|
self.drag_tree.clear()
|
||
|
|
||
|
# 1) Wymagania ogólne
|
||
|
it_ogolne = self.create_top_item([
|
||
|
"-", # punkty
|
||
|
display_obligatory(-1), # '-'
|
||
|
"-", # nr
|
||
|
"Wymagania ogólne"
|
||
|
])
|
||
|
self.drag_tree.addTopLevelItem(it_ogolne)
|
||
|
|
||
|
if "wymagania_ogolne" in data:
|
||
|
for req_data in data["wymagania_ogolne"]:
|
||
|
self.add_tree_item(it_ogolne, req_data, keep_removable=True, subitems_key="wymagania")
|
||
|
|
||
|
# 2) Wymagania szczegółowe
|
||
|
it_szcz = self.create_top_item([
|
||
|
"-", # punkty
|
||
|
display_obligatory(-1), # '-'
|
||
|
"-", # nr
|
||
|
"Wymagania szczegółowe"
|
||
|
])
|
||
|
self.drag_tree.addTopLevelItem(it_szcz)
|
||
|
|
||
|
if "wymagania_szczegolowe" in data:
|
||
|
for block in data["wymagania_szczegolowe"]:
|
||
|
# block.get("nr", -1) => może zwrócić -1, wtedy na ekranie będzie '-'
|
||
|
block_atryb = block.get("atrybuty", {})
|
||
|
punkty_val = block_atryb.get("punkty", -1)
|
||
|
obow_val = block_atryb.get("obowiązkowe", -1)
|
||
|
nr_val = block.get("nr", -1)
|
||
|
opis_val = block.get("opis", "")
|
||
|
|
||
|
block_item = self.create_top_item([
|
||
|
display_number_or_dash(punkty_val),
|
||
|
display_obligatory(obow_val),
|
||
|
display_number_or_dash(nr_val),
|
||
|
opis_val
|
||
|
])
|
||
|
|
||
|
it_szcz.addChild(block_item)
|
||
|
|
||
|
# Zakresy
|
||
|
for zakres_type in ["zakres_podstawowy", "zakres_rozszerzony"]:
|
||
|
zakres_data = block.get(zakres_type, {})
|
||
|
atrybuty = zakres_data.get("atrybuty", {})
|
||
|
|
||
|
punkty_val = atrybuty.get("punkty", -1)
|
||
|
obow_val = atrybuty.get("obowiązkowe", -1)
|
||
|
nr_val = zakres_data.get("nr", -1)
|
||
|
opis_val = zakres_data.get("opis", "")
|
||
|
|
||
|
zakres_item = self.create_top_item([
|
||
|
display_number_or_dash(punkty_val),
|
||
|
display_obligatory(obow_val),
|
||
|
display_number_or_dash(nr_val),
|
||
|
opis_val
|
||
|
])
|
||
|
block_item.addChild(zakres_item)
|
||
|
|
||
|
# Rekurencja dla "uczen"
|
||
|
for uczen_data in zakres_data.get("uczen", []):
|
||
|
self.add_tree_item(zakres_item, uczen_data, keep_removable=True)
|
||
|
|
||
|
apply_indentation_to_all_columns(self.drag_tree)
|
||
|
|
||
|
except Exception as e:
|
||
|
QMessageBox.warning(self, "Błąd", f"Błąd ładowania danych: {str(e)}")
|
||
|
|
||
|
def create_top_item(self, texts):
|
||
|
"""Tworzy węzeł top-level z automatycznym kolorowaniem."""
|
||
|
from PySide6.QtWidgets import QTreeWidgetItem
|
||
|
it = QTreeWidgetItem(texts)
|
||
|
# Ustaw userRole (opcjonalnie - np. flaga 'removable')
|
||
|
it.setData(0, Qt.UserRole, True)
|
||
|
color_item_by_title(it)
|
||
|
return it
|
||
|
|
||
|
def add_tree_item(self, parent_item, item_data, keep_removable=False, subitems_key="podpunkty"):
|
||
|
from PySide6.QtWidgets import QTreeWidgetItem
|
||
|
from utils.colors import color_item_by_title
|
||
|
from utils.helpers import display_number_or_dash, display_obligatory
|
||
|
|
||
|
opis = item_data.get("opis", "")
|
||
|
atrybuty = item_data.get("atrybuty", {})
|
||
|
|
||
|
nr_val = item_data.get("nr", -1)
|
||
|
punkty_val = atrybuty.get("punkty", -1)
|
||
|
obow_val = atrybuty.get("obowiązkowe", 0)
|
||
|
|
||
|
nr_str = display_number_or_dash(nr_val)
|
||
|
punkty_str = display_number_or_dash(punkty_val)
|
||
|
obow_str = display_obligatory(obow_val)
|
||
|
|
||
|
it = QTreeWidgetItem([punkty_str, obow_str, nr_str, opis])
|
||
|
if keep_removable:
|
||
|
it.setData(0, Qt.UserRole, True)
|
||
|
|
||
|
color_item_by_title(it)
|
||
|
parent_item.addChild(it)
|
||
|
|
||
|
# Obsługa podpunktów
|
||
|
for podpunkt in item_data.get(subitems_key, []):
|
||
|
self.add_tree_item(it, podpunkt, keep_removable, subitems_key)
|
||
|
|
||
|
# ----------------------------------------------------------------
|
||
|
# Funkcje import/export (dla testów)
|
||
|
# ----------------------------------------------------------------
|
||
|
def import_tree(self, tree, keep_drag_flag=False):
|
||
|
fn, _ = QFileDialog.getOpenFileName(self, "Import JSON", "./data", "JSON Files (*.json)")
|
||
|
if not fn:
|
||
|
return
|
||
|
try:
|
||
|
with open(fn, "r", encoding="utf-8") as f:
|
||
|
data = json.load(f)
|
||
|
self.json_to_tree(data, tree, keep_drag_flag)
|
||
|
QMessageBox.information(self, "Import", f"Wczytano: {fn}")
|
||
|
except Exception as e:
|
||
|
QMessageBox.warning(self, "Błąd importu", str(e))
|
||
|
|
||
|
def export_tree(self, tree):
|
||
|
fn, _ = QFileDialog.getSaveFileName(self, "Export JSON", "./data", "JSON Files (*.json)")
|
||
|
if not fn:
|
||
|
return
|
||
|
data = self.tree_to_json(tree)
|
||
|
try:
|
||
|
with open(fn, "w", encoding="utf-8") as f:
|
||
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||
|
QMessageBox.information(self, "Export", f"Zapisano: {fn}")
|
||
|
except Exception as e:
|
||
|
QMessageBox.warning(self, "Błąd zapisu", str(e))
|
||
|
|
||
|
def tree_to_json(self, tree):
|
||
|
def item_to_dict(it):
|
||
|
node = {
|
||
|
"punkty": it.text(0).strip(),
|
||
|
"obowiązkowe": it.text(1).strip(),
|
||
|
"nr": it.text(2).strip(),
|
||
|
"opis": it.text(3).strip(),
|
||
|
"children": []
|
||
|
}
|
||
|
for i in range(it.childCount()):
|
||
|
node["children"].append(item_to_dict(it.child(i)))
|
||
|
return node
|
||
|
|
||
|
data = {"items": []}
|
||
|
for i in range(tree.topLevelItemCount()):
|
||
|
top_item = tree.topLevelItem(i)
|
||
|
data["items"].append(item_to_dict(top_item))
|
||
|
return data
|
||
|
|
||
|
def json_to_tree(self, data: dict, tree, keep_drag_flag=False):
|
||
|
tree.clear()
|
||
|
|
||
|
def dict_to_item(nd):
|
||
|
from PySide6.QtWidgets import QTreeWidgetItem
|
||
|
p_s = nd.get("punkty", "0").strip()
|
||
|
o_s = nd.get("obowiązkowe", "nie").strip()
|
||
|
nr_s = nd.get("nr", "").strip()
|
||
|
op_s = nd.get("opis", "").strip()
|
||
|
|
||
|
it = QTreeWidgetItem([p_s, o_s, nr_s, op_s])
|
||
|
if keep_drag_flag:
|
||
|
it.setData(0, Qt.UserRole, True)
|
||
|
|
||
|
color_item_by_title(it)
|
||
|
for ch_d in nd.get("children", []):
|
||
|
ch_it = dict_to_item(ch_d)
|
||
|
it.addChild(ch_it)
|
||
|
return it
|
||
|
|
||
|
for nd in data.get("items", []):
|
||
|
t_it = dict_to_item(nd)
|
||
|
tree.addTopLevelItem(t_it)
|
||
|
|
||
|
apply_indentation_to_all_columns(tree)
|
||
|
|
||
|
# ----------------------------------------------------------------
|
||
|
# Zoom (Ctrl + scroll)
|
||
|
# ----------------------------------------------------------------
|
||
|
def wheelEvent(self, event: QWheelEvent):
|
||
|
if event.modifiers() & Qt.ControlModifier:
|
||
|
delta = event.angleDelta().y()
|
||
|
step = 1 if delta > 0 else -1
|
||
|
self.change_font_size(step)
|
||
|
event.accept()
|
||
|
else:
|
||
|
super().wheelEvent(event)
|
||
|
|
||
|
def change_font_size(self, delta):
|
||
|
font = self.drop_tree.font()
|
||
|
sz = font.pointSize() + delta
|
||
|
sz = max(6, sz)
|
||
|
font.setPointSize(sz)
|
||
|
self.drop_tree.setFont(font)
|
||
|
self.drag_tree.setFont(font)
|
||
|
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
app = QApplication([])
|
||
|
w = MainWindow()
|
||
|
w.show()
|
||
|
app.exec()
|