This commit is contained in:
baiobelfer 2025-02-01 18:16:23 +01:00
commit 2017e7a4c5
20 changed files with 5237 additions and 0 deletions

1
__ Normal file
View File

@ -0,0 +1 @@
QT_QPA_PLATFORM=xcb python main.py

0
__init__.py Normal file
View File

116
commands.py Normal file
View File

@ -0,0 +1,116 @@
# commands.py
from PySide6.QtGui import QUndoCommand
from PySide6.QtWidgets import QTreeWidgetItem
from PySide6.QtCore import Qt
def clone_item_recursive(source_item: QTreeWidgetItem) -> QTreeWidgetItem:
"""
Rekurencyjnie klonuje QTreeWidgetItem wraz z jego dziećmi,
zachowując tekst i ewentualne dane w kolumnach.
"""
cloned = QTreeWidgetItem([
source_item.text(0),
source_item.text(1),
source_item.text(2),
source_item.text(3),
])
# Ewentualnie można kopiować Qt.UserRole itp.:
# for col in range(source_item.columnCount()):
# data_val = source_item.data(col, Qt.UserRole)
# cloned.setData(col, Qt.UserRole, data_val)
for i in range(source_item.childCount()):
child = source_item.child(i)
cloned_child = clone_item_recursive(child)
cloned.addChild(cloned_child)
return cloned
class AddItemCommand(QUndoCommand):
"""
Dodaje nowy (już utworzony) węzeł (cloned_item) do tree:
- jeśli parent_item is None -> top-level
- w przeciwnym razie child w parent_item
"""
def __init__(self, tree, parent_item, index: int, cloned_item, description="Add Item"):
super().__init__(description)
self.tree = tree
self.parent_item = parent_item
self.index = index
self.cloned_item = cloned_item
def redo(self):
if self.parent_item is None:
self.tree.insertTopLevelItem(self.index, self.cloned_item)
else:
self.parent_item.insertChild(self.index, self.cloned_item)
self.tree.expandItem(self.cloned_item)
def undo(self):
if self.parent_item:
self.parent_item.removeChild(self.cloned_item)
else:
idx = self.tree.indexOfTopLevelItem(self.cloned_item)
if idx >= 0:
self.tree.takeTopLevelItem(idx)
class RemoveItemCommand(QUndoCommand):
"""
Usuwa wybrany węzeł z drzewa i zapamiętuje jego kopię,
by móc cofnąć (undo) i przywrócić go w to samo miejsce.
"""
def __init__(self, tree, item, description="Remove Item"):
super().__init__(description)
self.tree = tree
self.item_original = item
self.parent_item = item.parent()
if self.parent_item:
self.index = self.parent_item.indexOfChild(item)
else:
self.index = self.tree.indexOfTopLevelItem(item)
# Tworzymy kopię rekurencyjną (z dziećmi)
self.item_clone = clone_item_recursive(item)
def redo(self):
# usuwamy oryginalny węzeł z widoku
if self.parent_item:
self.parent_item.removeChild(self.item_original)
else:
idx = self.tree.indexOfTopLevelItem(self.item_original)
if idx >= 0:
self.tree.takeTopLevelItem(idx)
def undo(self):
# przywracamy klon w to samo miejsce
if self.parent_item:
self.parent_item.insertChild(self.index, self.item_clone)
else:
self.tree.insertTopLevelItem(self.index, self.item_clone)
self.tree.expandItem(self.item_clone)
class EditPunktyCommand(QUndoCommand):
"""
Cofalne/ponawialne ustawienie nowej wartości punktów w kolumnie 0.
W 'redo()' dajemy new_val, w 'undo()' old_val.
"""
def __init__(self, item: QTreeWidgetItem, old_val: str, new_val: str, description="Edit punkty"):
super().__init__(description)
self.item = item
self.old_val = old_val
self.new_val = new_val
def redo(self):
self.item.setText(0, self.new_val)
# Ewentualnie odśwież kolor:
# from utils.colors import color_item_by_title
# color_item_by_title(self.item)
def undo(self):
self.item.setText(0, self.old_val)
# from utils.colors import color_item_by_title
# color_item_by_title(self.item)

515
data/pp-math-latex.json Normal file
View File

@ -0,0 +1,515 @@
{
"podstawa_programowa": {
"tytul": "ROZPORZĄDZENIE MINISTRA EDUKACJI",
"data": "z dnia 28 czerwca 2024 r.",
"poz": "1019",
"opis": "Zmieniające rozporządzenie w sprawie podstawy programowej kształcenia ogólnego dla liceum ogólnokształcącego, technikum oraz branżowej szkoły II stopnia.",
"miejsce": "Warszawa, dnia 10 lipca 2024 r.",
"przedmiot": "Matematyka"
},
"autor": "Mateusz Pabiszczak",
"data": "2025-01-27",
"wymagania_ogolne": [
{
"nr": 1,
"opis": "Sprawność rachunkowa.",
"atrybuty": {
"punkty": -1,
"obowiązkowe": 1
},
"wymagania": [
{
"nr": 1,
"opis": "Wykonywanie obliczeń na liczbach rzeczywistych, także przy użyciu kalkulatora, stosowanie praw działań matematycznych przy przekształcaniu wyrażeń algebraicznych oraz wykorzystywanie tych umiejętności przy rozwiązywaniu problemów w kontekstach rzeczywistych i teoretycznych.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
}
]
},
{
"nr": 2,
"opis": "Wykorzystanie i tworzenie informacji.",
"atrybuty": {
"punkty": -1,
"obowiązkowe": 1
},
"wymagania": [
{
"nr": 1,
"opis": "Interpretowanie i operowanie informacjami przedstawionymi w tekście, zarówno matematycznym, jak i popularnonaukowym, a także w formie wykresów, diagramów, tabel.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 2,
"opis": "Używanie języka matematycznego do tworzenia tekstów matematycznych, w tym do opisu prowadzonych rozumowań i uzasadniania wniosków, a także do przedstawiania danych.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
}
]
},
{
"nr": 3,
"opis": "Wykorzystanie i interpretowanie reprezentacji.",
"atrybuty": {
"punkty": -1,
"obowiązkowe": 1
},
"wymagania": [
{
"nr": 1,
"opis": "Stosowanie obiektów matematycznych i operowanie nimi, interpretowanie pojęć matematycznych.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 2,
"opis": "Dobieranie i tworzenie modeli matematycznych przy rozwiązywaniu problemów praktycznych i teoretycznych.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 3,
"opis": "Tworzenie pomocniczych obiektów matematycznych na podstawie istniejących, w celu przeprowadzenia argumentacji lub rozwiązania problemu.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 4,
"opis": "Wskazywanie konieczności lub możliwości modyfikacji modelu matematycznego w przypadkach wymagających specjalnych zastrzeżeń, dodatkowych założeń, rozważenia szczególnych uwarunkowań.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
}
]
},
{
"nr": 4,
"opis": "Rozumowanie i argumentacja.",
"atrybuty": {
"punkty": -1,
"obowiązkowe": 1
},
"wymagania": [
{
"nr": 1,
"opis": "Przeprowadzanie rozumowań, także kilkuetapowych, podawanie argumentów uzasadniających poprawność rozumowania, odróżnianie dowodu od przykładu.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 2,
"opis": "Dostrzeganie regularności, podobieństw oraz analogii, formułowanie wniosków na ich podstawie i uzasadnianie ich poprawności.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 3,
"opis": "Dobieranie argumentów do uzasadnienia poprawności rozwiązywania problemów, tworzenie ciągu argumentów gwarantujących poprawność rozwiązania i skuteczność w poszukiwaniu rozwiązań zagadnienia.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 4,
"opis": "Stosowanie i tworzenie strategii przy rozwiązywaniu zadań, również w sytuacjach nietypowych.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
}
]
}
],
"wymagania_szczegolowe": [
{
"nr": 1,
"opis": "Liczby rzeczywiste",
"atrybuty": {
"punkty": -1,
"obowiązkowe": -1
},
"zakres_podstawowy": {
"nr": -1,
"opis": "Zakres podstawowy",
"atrybuty": {
"punkty": -1,
"obowiązkowe": -1
},
"uczen": [
{
"nr": 1,
"opis": "Wykonuje działania (dodawanie, odejmowanie, mnożenie, dzielenie, potęgowanie, pierwiastkowanie, logarytmowanie) w zbiorze liczb rzeczywistych.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 2,
"opis": "Przeprowadza proste dowody dotyczące podzielności liczb całkowitych i reszt z dzielenia.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
},
"podpunkty": [
{
"nr": "a",
"opis": "Dowód podzielności przez 24 iloczynu czterech kolejnych liczb naturalnych.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": "b",
"opis": "Dowód własności: jeśli liczba przy dzieleniu przez 4 daje resztę 3, to nie jest kwadratem liczby całkowitej.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
}
]
},
{
"nr": 3,
"opis": "Stosuje własności pierwiastków dowolnego stopnia, w tym pierwiastków stopnia nieparzystego z liczb ujemnych.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 4,
"opis": "Stosuje związek pierwiastkowania z potęgowaniem oraz prawa działań na potęgach i pierwiastkach.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 5,
"opis": "Stosuje monotoniczność potęgowania, w szczególności własności: jeśli $x < y$ oraz $a > 1$, to $a^x < a^y$, zaś gdy $x < y$ i $0 < a < 1$, to $a^x > a^y$.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 6,
"opis": "Posługuje się pojęciem przedziału liczbowego, zaznacza przedziały na osi liczbowej.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 7,
"opis": "Stosuje interpretację geometryczną i algebraiczną wartości bezwzględnej, rozwiązuje równania typu: $|x + 4| = 5$.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 8,
"opis": "Wykorzystuje własności potęgowania i pierwiastkowania w sytuacjach praktycznych, w tym do obliczania procentów składanych, zysków z lokat i kosztów kredytów.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 9,
"opis": "Stosuje związek logarytmowania z potęgowaniem, posługuje się wzorami na logarytm iloczynu, logarytm ilorazu i logarytm potęgi.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
}
]
},
"zakres_rozszerzony": {
"nr": -1,
"opis": "Zakres rozszerzony",
"atrybuty": {
"punkty": -1,
"obowiązkowe": -1
},
"uczen": [
{
"nr": 1,
"opis": "Spełnia wymagania określone dla zakresu podstawowego.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 2,
"opis": "Stosuje wzór na zamianę podstawy logarytmu.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
}
]
}
},
{
"nr": 2,
"opis": "Wyrażenia algebraiczne",
"atrybuty": {
"punkty": -1,
"obowiązkowe": -1
},
"zakres_podstawowy": {
"nr": -1,
"opis": "Zakres podstawowy",
"atrybuty": {
"punkty": -1,
"obowiązkowe": -1
},
"uczen": [
{
"nr": 1,
"opis": "Stosuje wzory skróconego mnożenia na: $(a + b)^2$, $(a - b)^2$, $a^2 - b^2$.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 2,
"opis": "Dodaje, odejmuje i mnoży wielomiany jednej i wielu zmiennych.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 3,
"opis": "Wyłącza poza nawias jednomian z sumy algebraicznej.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 4,
"opis": "Mnoży i dzieli wyrażenia wymierne.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
}
]
},
"zakres_rozszerzony": {
"nr": -1,
"opis": "Zakres rozszerzony",
"atrybuty": {
"punkty": -1,
"obowiązkowe": -1
},
"uczen": [
{
"nr": 1,
"opis": "Dzieli wielomian jednej zmiennej $W(x)$ przez dwumian postaci $x - a$.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 2,
"opis": "Rozkłada wielomiany na czynniki metodą wyłączania wspólnego czynnika przed nawias oraz metodą grupowania wyrazów.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 3,
"opis": "Znajduje pierwiastki całkowite wielomianu o współczynnikach całkowitych.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 4,
"opis": "Stosuje podstawowe własności trójkąta Pascala oraz własności symbolu Newtona, w tym: $\\binom{n}{0} = 1$, $\\binom{n}{1} = n$, $\\binom{n}{n-1} = n$, $\\binom{n}{k} = \\binom{n}{n-k}$, $\\binom{n}{k} + \\binom{n}{k+1} = \\binom{n+1}{k+1}$.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 5,
"opis": "Korzysta ze wzorów na: $a^3 + b^3$, $a^3 - b^3$, $a^n - b^n$, $(a + b)^n$ i $(a - b)^n$.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 6,
"opis": "Dodaje i odejmuje wyrażenia wymierne, np.: $\\frac{1}{x + 1} - \\frac{1}{x}$, $\\frac{1}{x} + \\frac{1}{x^2} + \\frac{1}{x^3}$, $\\frac{x + 1}{x + 2} + \\frac{x - 1}{x + 1}$.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
}
]
}
},
{
"nr": 3,
"opis": "Równania i nierówności",
"atrybuty": {
"punkty": -1,
"obowiązkowe": -1
},
"zakres_podstawowy": {
"nr": -1,
"opis": "Zakres podstawowy",
"atrybuty": {
"punkty": -1,
"obowiązkowe": -1
},
"uczen": [
{
"nr": 1,
"opis": "Przekształca równania i nierówności w sposób równoważny, w tym np. przekształca równoważnie równanie $5x + 1 = \\frac{x + 3}{2x - 1}$.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 2,
"opis": "Interpretuje równania i nierówności liniowe sprzeczne oraz tożsamościowe.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 3,
"opis": "Rozwiązuje nierówności liniowe z jedną niewiadomą.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 4,
"opis": "Rozwiązuje równania i nierówności kwadratowe.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 5,
"opis": "Rozwiązuje równania wielomianowe postaci $W(x) = 0$ dla wielomianów doprowadzonych do postaci iloczynowej.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
}
]
},
"zakres_rozszerzony": {
"nr": -1,
"opis": "Zakres rozszerzony",
"atrybuty": {
"punkty": -1,
"obowiązkowe": -1
},
"uczen": [
{
"nr": 1,
"opis": "Rozwiązuje równania wielomianowe postaci $W(x) = 0$ oraz nierówności wielomianowe typu: $W(x) > 0$, $W(x) \\geq 0$, $W(x) < 0$, $W(x) \\leq 0$ dla wielomianów doprowadzonych do postaci iloczynowej lub takich, które dają się doprowadzić do tej postaci metodą wyłączania wspólnego czynnika przed nawias lub metodą grupowania.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 2,
"opis": "Rozwiązuje równania i nierówności wymierne, które dają się sprowadzić do równania lub nierówności liniowej lub kwadratowej.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 3,
"opis": "Stosuje wzory Viètea dla równań kwadratowych.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 4,
"opis": "Rozwiązuje równania i nierówności z wartością bezwzględną.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 5,
"opis": "Analizuje równania i nierówności liniowe z parametrami oraz równania i nierówności kwadratowe z parametrami, w szczególności: wyznacza liczbę rozwiązań w zależności od parametrów, podaje warunki, przy których rozwiązania mają określone znaki bądź należą do określonego przedziału, wyznacza rozwiązania w zależności od parametrów.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 6,
"opis": "Rozwiązuje równania wielomianowe, które dają się doprowadzić do równania kwadratowego, w szczególności równania dwukwadratowe.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 7,
"opis": "Rozwiązuje równania wymierne postaci $\\frac{V(x)}{W(x)} = 0$, gdzie wielomiany $V(x)$ i $W(x)$ są zapisane w postaci iloczynowej.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
}
]
}
}
]
}

168
data/pp-math-schema.json Normal file
View File

@ -0,0 +1,168 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"podstawa_programowa": {
"type": "object",
"properties": {
"tytul": { "type": "string" },
"data": { "type": "string", "format": "date" },
"poz": { "type": "string" },
"opis": { "type": "string" },
"miejsce": { "type": "string" },
"przedmiot": { "type": "string" }
},
"required": ["tytul", "data", "poz", "opis", "miejsce", "przedmiot"]
},
"autor": { "type": "string" },
"data": { "type": "string", "format": "date" },
"wymagania_ogolne": {
"type": "array",
"items": {
"type": "object",
"properties": {
"nr": { "type": "integer" },
"opis": { "type": "string" },
"atrybuty": {
"type": "object",
"properties": {
"punkty": { "type": "integer" },
"obowiązkowe": { "type": "integer" }
},
"required": ["punkty", "obowiązkowe"]
},
"wymagania": {
"type": "array",
"items": {
"type": "object",
"properties": {
"nr": { "type": "integer" },
"opis": { "type": "string" },
"atrybuty": {
"type": "object",
"properties": {
"punkty": { "type": "integer" },
"obowiązkowe": { "type": "integer" }
},
"required": ["punkty", "obowiązkowe"]
}
},
"required": ["nr", "opis", "atrybuty"]
}
}
},
"required": ["nr", "opis", "atrybuty", "wymagania"]
}
},
"wymagania_szczegolowe": {
"type": "array",
"items": {
"type": "object",
"properties": {
"nr": { "type": "integer" },
"opis": { "type": "string" },
"atrybuty": {
"type": "object",
"properties": {
"punkty": { "type": "integer" },
"obowiązkowe": { "type": "integer" }
},
"required": ["punkty", "obowiązkowe"]
},
"zakres_podstawowy": {
"type": "object",
"properties": {
"nr": { "type": "integer" },
"opis": { "type": "string" },
"atrybuty": {
"type": "object",
"properties": {
"punkty": { "type": "integer" },
"obowiązkowe": { "type": "integer" }
},
"required": ["punkty", "obowiązkowe"]
},
"uczen": {
"type": "array",
"items": {
"type": "object",
"properties": {
"nr": { "type": "integer" },
"opis": { "type": "string" },
"atrybuty": {
"type": "object",
"properties": {
"punkty": { "type": "integer" },
"obowiązkowe": { "type": "integer" }
},
"required": ["punkty", "obowiązkowe"]
},
"podpunkty": {
"type": "array",
"items": {
"type": "object",
"properties": {
"nr": { "type": "string" },
"opis": { "type": "string" },
"atrybuty": {
"type": "object",
"properties": {
"punkty": { "type": "integer" },
"obowiązkowe": { "type": "integer" }
},
"required": ["punkty", "obowiązkowe"]
}
},
"required": ["nr", "opis", "atrybuty"]
}
}
},
"required": ["nr", "opis", "atrybuty"]
}
}
},
"required": ["nr", "opis", "atrybuty", "uczen"]
},
"zakres_rozszerzony": {
"type": "object",
"properties": {
"nr": { "type": "integer" },
"opis": { "type": "string" },
"atrybuty": {
"type": "object",
"properties": {
"punkty": { "type": "integer" },
"obowiązkowe": { "type": "integer" }
},
"required": ["punkty", "obowiązkowe"]
},
"uczen": {
"type": "array",
"items": {
"type": "object",
"properties": {
"nr": { "type": "integer" },
"opis": { "type": "string" },
"atrybuty": {
"type": "object",
"properties": {
"punkty": { "type": "integer" },
"obowiązkowe": { "type": "integer" }
},
"required": ["punkty", "obowiązkowe"]
}
},
"required": ["nr", "opis", "atrybuty"]
}
}
},
"required": ["nr", "opis", "atrybuty", "uczen"]
}
},
"required": ["nr", "opis", "atrybuty", "zakres_podstawowy", "zakres_rozszerzony"]
}
}
},
"required": ["podstawa_programowa", "autor", "data", "wymagania_ogolne", "wymagania_szczegolowe"]
}

519
data/pp-math.json Normal file
View File

@ -0,0 +1,519 @@
{
"podstawa_programowa": {
"tytul": "ROZPORZĄDZENIE MINISTRA EDUKACJI",
"data": "z dnia 28 czerwca 2024 r.",
"poz": "1019",
"opis": "Zmieniające rozporządzenie w sprawie podstawy programowej kształcenia ogólnego dla liceum ogólnokształcącego, technikum oraz branżowej szkoły II stopnia.",
"miejsce": "Warszawa, dnia 10 lipca 2024 r.",
"przedmiot": "Matematyka"
},
"autor": "Mateusz Pabiszczak",
"data": "2025-01-27",
"wymagania_ogolne": [
{
"nr": 1,
"opis": "Sprawność rachunkowa.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
},
"wymagania": [
{
"nr": 1,
"opis": "Wykonywanie obliczeń na liczbach rzeczywistych, także przy użyciu kalkulatora, stosowanie praw działań matematycznych przy przekształcaniu wyrażeń algebraicznych oraz wykorzystywanie tych umiejętności przy rozwiązywaniu problemów w kontekstach rzeczywistych i teoretycznych.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
}
]
},
{
"nr": 2,
"opis": "Wykorzystanie i tworzenie informacji.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
},
"wymagania": [
{
"nr": 1,
"opis": "Interpretowanie i operowanie informacjami przedstawionymi w tekście, zarówno matematycznym, jak i popularnonaukowym, a także w formie wykresów, diagramów, tabel.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 2,
"opis": "Używanie języka matematycznego do tworzenia tekstów matematycznych, w tym do opisu prowadzonych rozumowań i uzasadniania wniosków, a także do przedstawiania danych.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
}
]
},
{
"nr": 3,
"opis": "Wykorzystanie i interpretowanie reprezentacji.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
},
"wymagania": [
{
"nr": 1,
"opis": "Stosowanie obiektów matematycznych i operowanie nimi, interpretowanie pojęć matematycznych.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 2,
"opis": "Dobieranie i tworzenie modeli matematycznych przy rozwiązywaniu problemów praktycznych i teoretycznych.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 3,
"opis": "Tworzenie pomocniczych obiektów matematycznych na podstawie istniejących, w celu przeprowadzenia argumentacji lub rozwiązania problemu.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 4,
"opis": "Wskazywanie konieczności lub możliwości modyfikacji modelu matematycznego w przypadkach wymagających specjalnych zastrzeżeń, dodatkowych założeń, rozważenia szczególnych uwarunkowań.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
}
]
},
{
"nr": 4,
"opis": "Rozumowanie i argumentacja.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
},
"wymagania": [
{
"nr": 1,
"opis": "Przeprowadzanie rozumowań, także kilkuetapowych, podawanie argumentów uzasadniających poprawność rozumowania, odróżnianie dowodu od przykładu.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 2,
"opis": "Dostrzeganie regularności, podobieństw oraz analogii, formułowanie wniosków na ich podstawie i uzasadnianie ich poprawności.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 3,
"opis": "Dobieranie argumentów do uzasadnienia poprawności rozwiązywania problemów, tworzenie ciągu argumentów gwarantujących poprawność rozwiązania i skuteczność w poszukiwaniu rozwiązań zagadnienia.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 4,
"opis": "Stosowanie i tworzenie strategii przy rozwiązywaniu zadań, również w sytuacjach nietypowych.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
}
]
}
],
"wymagania_szczegolowe": [
{
"nr": 1,
"opis": "Liczby rzeczywiste",
"atrybuty": {
"punkty": -1,
"obowiązkowe": -1
},
"zakres_podstawowy": {
"nr": -1,
"opis": "Zakres podstawowy",
"atrybuty": {
"punkty": -1,
"obowiązkowe": -1
},
"uczen": [
{
"nr": 1,
"opis": "Wykonuje działania (dodawanie, odejmowanie, mnożenie, dzielenie, potęgowanie, pierwiastkowanie, logarytmowanie) w zbiorze liczb rzeczywistych.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 2,
"opis": "Przeprowadza proste dowody dotyczące podzielności liczb całkowitych i reszt z dzielenia.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
},
"podpunkty": [
{
"nr": "a",
"opis": "Dowód podzielności przez 24 iloczynu czterech kolejnych liczb naturalnych.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": "b",
"opis": "Dowód własności: jeśli liczba przy dzieleniu przez 4 daje resztę 3, to nie jest kwadratem liczby całkowitej.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
}
]
},
{
"nr": 3,
"opis": "Stosuje własności pierwiastków dowolnego stopnia, w tym pierwiastków stopnia nieparzystego z liczb ujemnych.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 4,
"opis": "Stosuje związek pierwiastkowania z potęgowaniem oraz prawa działań na potęgach i pierwiastkach.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 5,
"opis": "Stosuje monotoniczność potęgowania, w szczególności własności: jeśli x < y oraz a > 1, to a^x < a^y, zaś gdy x < y i 0 < a < 1, to a^x > a^y.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 6,
"opis": "Posługuje się pojęciem przedziału liczbowego, zaznacza przedziały na osi liczbowej.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 7,
"opis": "Stosuje interpretację geometryczną i algebraiczną wartości bezwzględnej, rozwiązuje równania typu: |x + 4| = 5.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 8,
"opis": "Wykorzystuje własności potęgowania i pierwiastkowania w sytuacjach praktycznych, w tym do obliczania procentów składanych, zysków z lokat i kosztów kredytów.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 9,
"opis": "Stosuje związek logarytmowania z potęgowaniem, posługuje się wzorami na logarytm iloczynu, logarytm ilorazu i logarytm potęgi.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
}
]
},
"zakres_rozszerzony": {
"nr": -1,
"opis": "Zakres rozszerzony",
"atrybuty": {
"punkty": -1,
"obowiązkowe": -1
},
"uczen": [
{
"nr": 1,
"opis": "Spełnia wymagania określone dla zakresu podstawowego.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 2,
"opis": "Stosuje wzór na zamianę podstawy logarytmu.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
}
]
}
},
{
"nr": 2,
"opis": "Wyrażenia algebraiczne",
"atrybuty": {
"punkty": -1,
"obowiązkowe": -1
},
"zakres_podstawowy": {
"nr": -1,
"opis": "Zakres podstawowy",
"atrybuty": {
"punkty": -1,
"obowiązkowe": -1
},
"uczen": [
{
"nr": 1,
"opis": "Stosuje wzory skróconego mnożenia na: (a + b)^2, (a b)^2, a^2 b^2.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 2,
"opis": "Dodaje, odejmuje i mnoży wielomiany jednej i wielu zmiennych.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 3,
"opis": "Wyłącza poza nawias jednomian z sumy algebraicznej.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 4,
"opis": "Mnoży i dzieli wyrażenia wymierne.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
}
]
},
"zakres_rozszerzony": {
"nr": -1,
"opis": "Zakres rozszerzony",
"atrybuty": {
"punkty": -1,
"obowiązkowe": -1
},
"uczen": [
{
"nr": 1,
"opis": "Dzieli wielomian jednej zmiennej W(x) przez dwumian postaci x a.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 2,
"opis": "Rozkłada wielomiany na czynniki metodą wyłączania wspólnego czynnika przed nawias oraz metodą grupowania wyrazów.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 3,
"opis": "Znajduje pierwiastki całkowite wielomianu o współczynnikach całkowitych.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 4,
"opis": "Stosuje podstawowe własności trójkąta Pascala oraz własności symbolu Newtona, w tym: (n 0) = 1, (n 1) = n, (n n1) = n, (n k) = (n nk), (n k) + (n k+1) = (n+1 k+1).",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 5,
"opis": "Korzysta ze wzorów na: a^3 + b^3, a^3 b^3, a^n b^n, (a + b)^n i (a b)^n.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 6,
"opis": "Dodaje i odejmuje wyrażenia wymierne, np.: 1/(x + 1) 1/x, 1/x + 1/x^2 + 1/x^3, (x + 1)/(x + 2) + (x 1)/(x + 1).",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
}
]
}
},
{
"nr": 3,
"opis": "Równania i nierówności",
"atrybuty": {
"punkty": -1,
"obowiązkowe": -1
},
"zakres_podstawowy": {
"nr": -1,
"opis": "Zakres podstawowy",
"atrybuty": {
"punkty": -1,
"obowiązkowe": -1
},
"uczen": [
{
"nr": 1,
"opis": "Przekształca równania i nierówności w sposób równoważny, w tym np. przekształca równoważnie równanie 5x + 1 = (x + 3) / (2x 1).",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 2,
"opis": "Interpretuje równania i nierówności liniowe sprzeczne oraz tożsamościowe.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 3,
"opis": "Rozwiązuje nierówności liniowe z jedną niewiadomą.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 4,
"opis": "Rozwiązuje równania i nierówności kwadratowe.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 5,
"opis": "Rozwiązuje równania wielomianowe postaci W(x) = 0 dla wielomianów doprowadzonych do postaci iloczynowej.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
}
]
},
"zakres_rozszerzony": {
"nr": -1,
"opis": "Zakres rozszerzony",
"atrybuty": {
"punkty": -1,
"obowiązkowe": -1
},
"uczen": [
{
"nr": 1,
"opis": "Rozwiązuje równania wielomianowe postaci W(x) = 0 oraz nierówności wielomianowe typu: W(x) > 0, W(x) ≥ 0, W(x) < 0, W(x) ≤ 0 dla wielomianów doprowadzonych do postaci iloczynowej lub takich, które dają się doprowadzić do tej postaci metodą wyłączania wspólnego czynnika przed nawias lub metodą grupowania.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 2,
"opis": "Rozwiązuje równania i nierówności wymierne, które dają się sprowadzić do równania lub nierówności liniowej lub kwadratowej.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 3,
"opis": "Stosuje wzory Viètea dla równań kwadratowych.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 4,
"opis": "Rozwiązuje równania i nierówności z wartością bezwzględną.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 5,
"opis": "Analizuje równania i nierówności liniowe z parametrami oraz równania i nierówności kwadratowe z parametrami, w szczególności: wyznacza liczbę rozwiązań w zależności od parametrów, podaje warunki, przy których rozwiązania mają określone znaki bądź należą do określonego przedziału, wyznacza rozwiązania w zależności od parametrów.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 6,
"opis": "Rozwiązuje równania wielomianowe, które dają się doprowadzić do równania kwadratowego, w szczególności równania dwukwadratowe.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 7,
"opis": "Rozwiązuje równania wymierne postaci V(x)/W(x) = 0, gdzie wielomiany V(x) i W(x) są zapisane w postaci iloczynowej.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
}
]
}
}
]
}

515
data/pp.json Normal file
View File

@ -0,0 +1,515 @@
{
"podstawa_programowa": {
"tytul": "ROZPORZĄDZENIE MINISTRA EDUKACJI",
"data": "z dnia 28 czerwca 2024 r.",
"poz": "1019",
"opis": "Zmieniające rozporządzenie w sprawie podstawy programowej kształcenia ogólnego dla liceum ogólnokształcącego, technikum oraz branżowej szkoły II stopnia.",
"miejsce": "Warszawa, dnia 10 lipca 2024 r.",
"przedmiot": "Matematyka"
},
"autor": "Mateusz Pabiszczak",
"data": "2025-01-27",
"wymagania_ogolne": [
{
"nr": 1,
"opis": "Sprawność rachunkowa.",
"atrybuty": {
"punkty": -1,
"obowiązkowe": 1
},
"wymagania": [
{
"nr": 1,
"opis": "Wykonywanie obliczeń na liczbach rzeczywistych, także przy użyciu kalkulatora, stosowanie praw działań matematycznych przy przekształcaniu wyrażeń algebraicznych oraz wykorzystywanie tych umiejętności przy rozwiązywaniu problemów w kontekstach rzeczywistych i teoretycznych.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
}
]
},
{
"nr": 2,
"opis": "Wykorzystanie i tworzenie informacji.",
"atrybuty": {
"punkty": -1,
"obowiązkowe": 1
},
"wymagania": [
{
"nr": 1,
"opis": "Interpretowanie i operowanie informacjami przedstawionymi w tekście, zarówno matematycznym, jak i popularnonaukowym, a także w formie wykresów, diagramów, tabel.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 2,
"opis": "Używanie języka matematycznego do tworzenia tekstów matematycznych, w tym do opisu prowadzonych rozumowań i uzasadniania wniosków, a także do przedstawiania danych.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
}
]
},
{
"nr": 3,
"opis": "Wykorzystanie i interpretowanie reprezentacji.",
"atrybuty": {
"punkty": -1,
"obowiązkowe": 1
},
"wymagania": [
{
"nr": 1,
"opis": "Stosowanie obiektów matematycznych i operowanie nimi, interpretowanie pojęć matematycznych.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 2,
"opis": "Dobieranie i tworzenie modeli matematycznych przy rozwiązywaniu problemów praktycznych i teoretycznych.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 3,
"opis": "Tworzenie pomocniczych obiektów matematycznych na podstawie istniejących, w celu przeprowadzenia argumentacji lub rozwiązania problemu.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 4,
"opis": "Wskazywanie konieczności lub możliwości modyfikacji modelu matematycznego w przypadkach wymagających specjalnych zastrzeżeń, dodatkowych założeń, rozważenia szczególnych uwarunkowań.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
}
]
},
{
"nr": 4,
"opis": "Rozumowanie i argumentacja.",
"atrybuty": {
"punkty": -1,
"obowiązkowe": 1
},
"wymagania": [
{
"nr": 1,
"opis": "Przeprowadzanie rozumowań, także kilkuetapowych, podawanie argumentów uzasadniających poprawność rozumowania, odróżnianie dowodu od przykładu.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 2,
"opis": "Dostrzeganie regularności, podobieństw oraz analogii, formułowanie wniosków na ich podstawie i uzasadnianie ich poprawności.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 3,
"opis": "Dobieranie argumentów do uzasadnienia poprawności rozwiązywania problemów, tworzenie ciągu argumentów gwarantujących poprawność rozwiązania i skuteczność w poszukiwaniu rozwiązań zagadnienia.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 4,
"opis": "Stosowanie i tworzenie strategii przy rozwiązywaniu zadań, również w sytuacjach nietypowych.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
}
]
}
],
"wymagania_szczegolowe": [
{
"nr": 1,
"opis": "Liczby rzeczywiste",
"atrybuty": {
"punkty": -1,
"obowiązkowe": -1
},
"zakres_podstawowy": {
"nr": -1,
"opis": "Zakres podstawowy",
"atrybuty": {
"punkty": -1,
"obowiązkowe": -1
},
"uczen": [
{
"nr": 1,
"opis": "Wykonuje działania (dodawanie, odejmowanie, mnożenie, dzielenie, potęgowanie, pierwiastkowanie, logarytmowanie) w zbiorze liczb rzeczywistych.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 2,
"opis": "Przeprowadza proste dowody dotyczące podzielności liczb całkowitych i reszt z dzielenia.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
},
"podpunkty": [
{
"nr": "a",
"opis": "Dowód podzielności przez 24 iloczynu czterech kolejnych liczb naturalnych.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": "b",
"opis": "Dowód własności: jeśli liczba przy dzieleniu przez 4 daje resztę 3, to nie jest kwadratem liczby całkowitej.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
}
]
},
{
"nr": 3,
"opis": "Stosuje własności pierwiastków dowolnego stopnia, w tym pierwiastków stopnia nieparzystego z liczb ujemnych.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 4,
"opis": "Stosuje związek pierwiastkowania z potęgowaniem oraz prawa działań na potęgach i pierwiastkach.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 5,
"opis": "Stosuje monotoniczność potęgowania, w szczególności własności: jeśli $x < y$ oraz $a > 1$, to $a^x < a^y$, zaś gdy $x < y$ i $0 < a < 1$, to $a^x > a^y$.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 6,
"opis": "Posługuje się pojęciem przedziału liczbowego, zaznacza przedziały na osi liczbowej.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 7,
"opis": "Stosuje interpretację geometryczną i algebraiczną wartości bezwzględnej, rozwiązuje równania typu: $|x + 4| = 5$.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 8,
"opis": "Wykorzystuje własności potęgowania i pierwiastkowania w sytuacjach praktycznych, w tym do obliczania procentów składanych, zysków z lokat i kosztów kredytów.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 9,
"opis": "Stosuje związek logarytmowania z potęgowaniem, posługuje się wzorami na logarytm iloczynu, logarytm ilorazu i logarytm potęgi.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
}
]
},
"zakres_rozszerzony": {
"nr": -1,
"opis": "Zakres rozszerzony",
"atrybuty": {
"punkty": -1,
"obowiązkowe": -1
},
"uczen": [
{
"nr": 1,
"opis": "Spełnia wymagania określone dla zakresu podstawowego.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 2,
"opis": "Stosuje wzór na zamianę podstawy logarytmu.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
}
]
}
},
{
"nr": 2,
"opis": "Wyrażenia algebraiczne",
"atrybuty": {
"punkty": -1,
"obowiązkowe": -1
},
"zakres_podstawowy": {
"nr": -1,
"opis": "Zakres podstawowy",
"atrybuty": {
"punkty": -1,
"obowiązkowe": -1
},
"uczen": [
{
"nr": 1,
"opis": "Stosuje wzory skróconego mnożenia na: $(a + b)^2$, $(a - b)^2$, $a^2 - b^2$.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 2,
"opis": "Dodaje, odejmuje i mnoży wielomiany jednej i wielu zmiennych.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 3,
"opis": "Wyłącza poza nawias jednomian z sumy algebraicznej.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 4,
"opis": "Mnoży i dzieli wyrażenia wymierne.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
}
]
},
"zakres_rozszerzony": {
"nr": -1,
"opis": "Zakres rozszerzony",
"atrybuty": {
"punkty": -1,
"obowiązkowe": -1
},
"uczen": [
{
"nr": 1,
"opis": "Dzieli wielomian jednej zmiennej $W(x)$ przez dwumian postaci $x - a$.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 2,
"opis": "Rozkłada wielomiany na czynniki metodą wyłączania wspólnego czynnika przed nawias oraz metodą grupowania wyrazów.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 3,
"opis": "Znajduje pierwiastki całkowite wielomianu o współczynnikach całkowitych.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 4,
"opis": "Stosuje podstawowe własności trójkąta Pascala oraz własności symbolu Newtona, w tym: $\\binom{n}{0} = 1$, $\\binom{n}{1} = n$, $\\binom{n}{n-1} = n$, $\\binom{n}{k} = \\binom{n}{n-k}$, $\\binom{n}{k} + \\binom{n}{k+1} = \\binom{n+1}{k+1}$.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 5,
"opis": "Korzysta ze wzorów na: $a^3 + b^3$, $a^3 - b^3$, $a^n - b^n$, $(a + b)^n$ i $(a - b)^n$.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 6,
"opis": "Dodaje i odejmuje wyrażenia wymierne, np.: $\\frac{1}{x + 1} - \\frac{1}{x}$, $\\frac{1}{x} + \\frac{1}{x^2} + \\frac{1}{x^3}$, $\\frac{x + 1}{x + 2} + \\frac{x - 1}{x + 1}$.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
}
]
}
},
{
"nr": 3,
"opis": "Równania i nierówności",
"atrybuty": {
"punkty": -1,
"obowiązkowe": -1
},
"zakres_podstawowy": {
"nr": -1,
"opis": "Zakres podstawowy",
"atrybuty": {
"punkty": -1,
"obowiązkowe": -1
},
"uczen": [
{
"nr": 1,
"opis": "Przekształca równania i nierówności w sposób równoważny, w tym np. przekształca równoważnie równanie $5x + 1 = \\frac{x + 3}{2x - 1}$.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 2,
"opis": "Interpretuje równania i nierówności liniowe sprzeczne oraz tożsamościowe.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 3,
"opis": "Rozwiązuje nierówności liniowe z jedną niewiadomą.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 4,
"opis": "Rozwiązuje równania i nierówności kwadratowe.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 5,
"opis": "Rozwiązuje równania wielomianowe postaci $W(x) = 0$ dla wielomianów doprowadzonych do postaci iloczynowej.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
}
]
},
"zakres_rozszerzony": {
"nr": -1,
"opis": "Zakres rozszerzony",
"atrybuty": {
"punkty": -1,
"obowiązkowe": -1
},
"uczen": [
{
"nr": 1,
"opis": "Rozwiązuje równania wielomianowe postaci $W(x) = 0$ oraz nierówności wielomianowe typu: $W(x) > 0$, $W(x) \\geq 0$, $W(x) < 0$, $W(x) \\leq 0$ dla wielomianów doprowadzonych do postaci iloczynowej lub takich, które dają się doprowadzić do tej postaci metodą wyłączania wspólnego czynnika przed nawias lub metodą grupowania.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 2,
"opis": "Rozwiązuje równania i nierówności wymierne, które dają się sprowadzić do równania lub nierówności liniowej lub kwadratowej.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 3,
"opis": "Stosuje wzory Viètea dla równań kwadratowych.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 4,
"opis": "Rozwiązuje równania i nierówności z wartością bezwzględną.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 5,
"opis": "Analizuje równania i nierówności liniowe z parametrami oraz równania i nierówności kwadratowe z parametrami, w szczególności: wyznacza liczbę rozwiązań w zależności od parametrów, podaje warunki, przy których rozwiązania mają określone znaki bądź należą do określonego przedziału, wyznacza rozwiązania w zależności od parametrów.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 6,
"opis": "Rozwiązuje równania wielomianowe, które dają się doprowadzić do równania kwadratowego, w szczególności równania dwukwadratowe.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
},
{
"nr": 7,
"opis": "Rozwiązuje równania wymierne postaci $\\frac{V(x)}{W(x)} = 0$, gdzie wielomiany $V(x)$ i $W(x)$ są zapisane w postaci iloczynowej.",
"atrybuty": {
"punkty": 1,
"obowiązkowe": 1
}
}
]
}
}
]
}

332
main.py Normal file
View File

@ -0,0 +1,332 @@
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()

1433
out/out3.json Normal file

File diff suppressed because it is too large Load Diff

0
utils/__init__.py Normal file
View File

41
utils/colors.py Normal file
View File

@ -0,0 +1,41 @@
from PySide6.QtGui import QColor, QBrush
from PySide6.QtWidgets import QTreeWidgetItem
from PySide6.QtCore import Qt
###############################################################################
# Kolory pastelowe (top-level + jaśniejsze dla dzieci) #
###############################################################################
GREEN = QColor("#d2ffd2") # ogólne
RED = QColor("#ffd2d2") # szczegółowe
BLUE = QColor("#d2d2ff") # przekrojowe
GREEN_LIGHT = QColor("#f0fff0")
RED_LIGHT = QColor("#ffecec")
BLUE_LIGHT = QColor("#f0f0ff")
def color_top_level_item(item: QTreeWidgetItem, color: QColor):
"""Ustawia tło w całym wierszu (kolumnach) na podany kolor."""
brush = QBrush(color)
for c in range(item.columnCount()):
item.setBackground(c, brush)
def color_item_by_title(item: QTreeWidgetItem):
"""Koloruje w zależności od kategorii najwyższego rodzica (ogólne / szczegółowe / przekrojowe)."""
top_parent = item
while top_parent.parent() is not None:
top_parent = top_parent.parent()
title_lower = top_parent.text(3).lower()
is_child = (item != top_parent)
if "wymagania ogólne" in title_lower:
color = GREEN_LIGHT if is_child else GREEN
elif "wymagana przekrojowe" in title_lower:
color = BLUE_LIGHT if is_child else BLUE
elif "wymagania szczegółowe" in title_lower:
color = RED_LIGHT if is_child else RED
else:
return # Brak dopasowania
color_top_level_item(item, color)

View File

@ -0,0 +1,51 @@
from PySide6.QtWidgets import QStyledItemDelegate, QSpinBox
from PySide6.QtCore import Qt, QEvent, QModelIndex
from PySide6.QtGui import QMouseEvent
class CommonTreeDelegate(QStyledItemDelegate):
def createEditor(self, parent, option, index: QModelIndex):
col = index.column()
if col == 0:
# Kolumna 0: QSpinBox z zakresem [0..10]
editor = QSpinBox(parent)
editor.setRange(0, 10)
return editor
elif col == 1:
# Kolumna 1: toggle "tak/nie" obsługiwane double-clickiem
return None
return super().createEditor(parent, option, index)
def setEditorData(self, editor, index: QModelIndex):
col = index.column()
if col == 0:
val_str = index.data(Qt.EditRole)
if val_str is None:
val_str = index.data(Qt.DisplayRole)
try:
val = int(val_str)
except ValueError:
val = 0
editor.setValue(val)
else:
super().setEditorData(editor, index)
def setModelData(self, editor, model, index: QModelIndex):
col = index.column()
if col == 0:
val = editor.value()
model.setData(index, str(val), Qt.EditRole)
elif col == 1:
# Podwójny klik przełącza "tak"/"nie"
cur = index.data(Qt.DisplayRole)
new_val = "nie" if cur == "tak" else "tak"
model.setData(index, new_val, Qt.EditRole)
else:
super().setModelData(editor, model, index)
def editorEvent(self, event, model, option, index):
# Obsługa double-click w kolumnie 1 (toggle "tak"/"nie")
if index.column() == 1 and event.type() == QEvent.MouseButtonDblClick:
self.commitData.emit(None)
return True
return super().editorEvent(event, model, option, index)

34
utils/helpers.py Normal file
View File

@ -0,0 +1,34 @@
# utils/helpers.py
def display_number_or_dash(value) -> str:
"""
Zwraca '-' jeśli wartość == -1,
w przeciwnym razie zwraca liczbę jako string.
"""
try:
val_int = int(value)
if val_int == -1:
return "-"
return str(val_int)
except ValueError:
# Gdyby "value" nie dało się zrzutować na int
return str(value)
def display_obligatory(value) -> str:
"""
Dla pola 'obowiązkowe':
- jeśli wartość == -1 -> '-'
- jeśli wartość == 1 -> 'tak'
- w pozostałych przypadkach -> 'nie'
"""
try:
val_int = int(value)
except ValueError:
val_int = 0 # traktujemy jako 'nie', albo można zwrócić np. 'nie'
if val_int == -1:
return "-"
elif val_int == 1:
return "tak"
else:
return "nie"

24
utils/indentation.py Normal file
View File

@ -0,0 +1,24 @@
from PySide6.QtWidgets import QTreeWidget, QTreeWidgetItem
from PySide6.QtCore import Qt
def apply_indentation_to_all_columns(tree: QTreeWidget):
"""
Rekurencyjnie stosuje wizualne wcięcie dla wszystkich elementów w drzewie
we wszystkich kolumnach.
"""
for i in range(tree.topLevelItemCount()):
item = tree.topLevelItem(i)
_indent_subtree(item, 0)
def _indent_subtree(item: QTreeWidgetItem, level: int):
"""Dodaje wcięcie (np. spacje) na każdy poziom hierarchii w całym wierszu."""
indent_spaces = " " * 2 * level
for col in range(item.columnCount()):
original_text = item.text(col).lstrip()
item.setText(col, f"{indent_spaces}{original_text}")
item.setData(col, Qt.TextAlignmentRole, Qt.AlignLeft | Qt.AlignVCenter)
# Rekurencja dla dzieci
for i in range(item.childCount()):
_indent_subtree(item.child(i), level + 1)

0
widgets/__init__.py Normal file
View File

View File

@ -0,0 +1,51 @@
from PySide6.QtWidgets import QStyledItemDelegate, QSpinBox
from PySide6.QtCore import Qt, QEvent, QModelIndex
from PySide6.QtGui import QMouseEvent
class CommonTreeDelegate(QStyledItemDelegate):
def createEditor(self, parent, option, index: QModelIndex):
col = index.column()
if col == 0:
# Kolumna 0: QSpinBox z zakresem [0..10]
editor = QSpinBox(parent)
editor.setRange(0, 10)
return editor
elif col == 1:
# Kolumna 1: toggle "tak/nie" obsługiwane double-clickiem
return None
return super().createEditor(parent, option, index)
def setEditorData(self, editor, index: QModelIndex):
col = index.column()
if col == 0:
val_str = index.data(Qt.EditRole)
if val_str is None:
val_str = index.data(Qt.DisplayRole)
try:
val = int(val_str)
except ValueError:
val = 0
editor.setValue(val)
else:
super().setEditorData(editor, index)
def setModelData(self, editor, model, index: QModelIndex):
col = index.column()
if col == 0:
val = editor.value()
model.setData(index, str(val), Qt.EditRole)
elif col == 1:
# Podwójny klik przełącza "tak"/"nie"
cur = index.data(Qt.DisplayRole)
new_val = "nie" if cur == "tak" else "tak"
model.setData(index, new_val, Qt.EditRole)
else:
super().setModelData(editor, model, index)
def editorEvent(self, event, model, option, index):
# Obsługa double-click w kolumnie 1 (toggle "tak"/"nie")
if index.column() == 1 and event.type() == QEvent.MouseButtonDblClick:
self.commitData.emit(None)
return True
return super().editorEvent(event, model, option, index)

150
widgets/drag_tree_widget.py Normal file
View File

@ -0,0 +1,150 @@
import json
from PySide6.QtWidgets import (
QTreeWidget, QTreeWidgetItem,
QAbstractItemView, QApplication
)
from PySide6.QtCore import QPoint, Qt, QMimeData
from PySide6.QtGui import QDrag, QMouseEvent
class DragTreeWidget(QTreeWidget):
"""
Drzewo źródłowe (drag source). Umożliwia przeciąganie wybranych węzłów
w formacie JSON do DropTreeWidget.
"""
def __init__(self, parent=None):
super().__init__(parent)
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.setDragEnabled(True)
# Edycja np. double-click lub kliknięcie wybranego
self.setEditTriggers(QAbstractItemView.DoubleClicked | QAbstractItemView.SelectedClicked)
self._drag_start_pos = QPoint()
self._drag_in_progress = False # Flaga kontroli przeciągania
def handle_item_click(self, item, column):
"""
Po kliknięciu w węzeł kopiuje do schowka JSON zawierający
ścieżkę (rodzice) od korzenia do tego węzła ale BEZ dzieci!
"""
chain_json = self.item_to_json_parents_no_children(item)
print("[LOG] Copied item + parents (NO children) to clipboard:")
print(chain_json)
from PySide6.QtWidgets import QApplication
QApplication.clipboard().setText(chain_json)
def item_to_json_parents_no_children(self, item: QTreeWidgetItem) -> str:
"""
Buduje strukturę JSON (jako string) dla pojedynczej ścieżki
'korzeń -> ... -> item', bez dzieci. Każdy węzeł ma 'children': [].
"""
import json
# Zacznij od zaznaczonego węzła
node_dict = self.item_to_dict_no_children(item)
# Idź w górę, owijając node_dict w kolejnych rodziców
current = item.parent()
while current is not None:
parent_dict = self.item_to_dict_no_children(current)
parent_dict["children"] = [node_dict]
node_dict = parent_dict
current = current.parent()
# Zamień na ładny, sformatowany JSON
return json.dumps(node_dict, ensure_ascii=False, indent=2)
def item_to_dict_no_children(self, item: QTreeWidgetItem) -> dict:
"""
Zwraca słownik z polami (punkty, obowiązkowe, nr, opis),
ale 'children' zawsze jest pustą listą.
"""
return {
"punkty": item.text(0),
"obowiązkowe": item.text(1),
"nr": item.text(2),
"opis": item.text(3),
"children": []
}
def mousePressEvent(self, event: QMouseEvent):
item = self.itemAt(event.position().toPoint())
if item:
# 1) Alt + LPM: kopiowanie do schowka (łańcuch bez dzieci)
if (event.button() == Qt.LeftButton) and (event.modifiers() & Qt.ShiftModifier):
chain_json = self.item_to_json_parents_no_children(item)
from PySide6.QtWidgets import QApplication
QApplication.clipboard().setText(chain_json)
return # Nie zaznaczaj już tego węzła
# 2) Shift + LPM: kopiowanie *tylko* opisu (kolumna 3) klikniętego węzła
if (event.button() == Qt.LeftButton) and (event.modifiers() & Qt.AltModifier):
from PySide6.QtWidgets import QApplication
opis = item.text(3).strip() # kolumna 3
QApplication.clipboard().setText(opis)
return # Nie zaznaczaj już tego węzła
# Poniższa logika pozostaje bez zmian (drag, normalne zaznaczenie itp.)
if event.button() == Qt.LeftButton:
self._drag_start_pos = event.position().toPoint()
self._drag_in_progress = False
super().mousePressEvent(event)
def mouseMoveEvent(self, event: QMouseEvent):
if event.buttons() & Qt.LeftButton:
dist = (event.position().toPoint() - self._drag_start_pos).manhattanLength()
if dist > QApplication.startDragDistance() and not self._drag_in_progress:
self._drag_in_progress = True
self.startDrag()
super().mouseMoveEvent(event)
def startDrag(self, supportedActions=Qt.MoveAction):
if self._drag_in_progress:
selected_items = self.selectedItems()
if not selected_items:
self._drag_in_progress = False
return
# Konwertujemy zaznaczone elementy na listę JSON (uwzględniamy hierarchię)
data_list = [self.item_to_dict_with_parents(item) for item in selected_items]
formatted_json = json.dumps(data_list, ensure_ascii=False, indent=4)
drag = QDrag(self)
mime = QMimeData()
mime.setText(formatted_json)
drag.setMimeData(mime)
result = drag.exec(Qt.CopyAction)
self._drag_in_progress = False
def item_to_dict_with_parents(self, item: QTreeWidgetItem) -> dict:
"""
Buduje pełną hierarchię rodziców i dzieci od wybranego elementu do korzenia.
"""
current_item = item
full_hierarchy = None
while current_item is not None:
node = self.item_to_dict_subtree(current_item)
if full_hierarchy is not None:
node["children"] = [full_hierarchy]
full_hierarchy = node
current_item = current_item.parent()
return full_hierarchy
def item_to_dict_subtree(self, item: QTreeWidgetItem) -> dict:
"""Rekurencyjnie przekształca węzeł i jego dzieci w słownik."""
node_data = {
"punkty": item.text(0).strip(),
"obowiązkowe": item.text(1).strip(),
"nr": item.text(2).strip(),
"opis": item.text(3).strip(),
"children": []
}
for i in range(item.childCount()):
child = item.child(i)
node_data["children"].append(self.item_to_dict_subtree(child))
return node_data

444
widgets/drop_tree_widget.py Normal file
View File

@ -0,0 +1,444 @@
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)
"""

View File

@ -0,0 +1,402 @@
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

View File

@ -0,0 +1,441 @@
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
)
from PySide6.QtCore import Qt, 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
###############################################################################
# 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ł")
# 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)
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()
}
###############################################################################
# DropTreeWidget #
###############################################################################
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
# 1) Podłączanie sygnału itemClicked
self.itemClicked.connect(self.handle_item_click)
def handle_item_click(self, item, column):
"""
Po kliknięciu w węzeł kopiuje do schowka JSON zawierający
ścieżkę (rodzice) od korzenia do tego węzła ale BEZ dzieci!
"""
chain_json = self.item_to_json_parents_no_children(item)
print("[LOG] Copied item + parents (NO children) to clipboard:")
print(chain_json)
from PySide6.QtWidgets import QApplication
QApplication.clipboard().setText(chain_json)
def item_to_json_parents_no_children(self, item: QTreeWidgetItem) -> str:
"""
Buduje strukturę JSON (jako string) dla pojedynczej ścieżki
'korzeń -> ... -> item', bez dzieci. Każdy węzeł ma 'children': [].
"""
import json
# Zacznij od zaznaczonego węzła
node_dict = self.item_to_dict_no_children(item)
# Idź w górę, owijając node_dict w kolejnych rodziców
current = item.parent()
while current is not None:
parent_dict = self.item_to_dict_no_children(current)
parent_dict["children"] = [node_dict]
node_dict = parent_dict
current = current.parent()
# Zamień na ładny, sformatowany JSON
return json.dumps(node_dict, ensure_ascii=False, indent=2)
def item_to_dict_no_children(self, item: QTreeWidgetItem) -> dict:
"""
Zwraca słownik z polami (punkty, obowiązkowe, nr, opis),
ale 'children' zawsze jest pustą listą.
"""
return {
"punkty": item.text(0),
"obowiązkowe": item.text(1),
"nr": item.text(2),
"opis": item.text(3),
"children": []
}
# ----------------------------------------------------------------
# KONTEKSTOWE MENU (PRAWY KLIK)
# ----------------------------------------------------------------
def contextMenuEvent(self, event):
"""Wywołane przy kliknięciu prawym przyciskiem myszy (menu kontekstowe)."""
menu = QMenu(self)
add_act = menu.addAction("Dodaj węzeł")
paste_act = menu.addAction("Wklej JSON Subtree")
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()
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()
# ----------------------------------------------------------------
# NOWA OPCJA: WKLEJANIE SUBTREE Z JSON
# ----------------------------------------------------------------
def paste_json_subtree(self):
"""
Otwiera dialog, w którym użytkownik wkleja JSON w formie listy/dict.
Następnie zawartość jest dodawana rekurencyjnie do zaznaczonego węzła
(lub top-level, jeśli brak zaznaczenia).
"""
import json
from PySide6.QtWidgets import QDialog, QVBoxLayout, QDialogButtonBox, QPlainTextEdit
class JsonPasteDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Wklej JSON Subtree")
self.edit = QPlainTextEdit(self)
self.edit.setPlaceholderText("Wklej tutaj JSON...")
btn_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
btn_box.accepted.connect(self.accept)
btn_box.rejected.connect(self.reject)
layout = QVBoxLayout()
layout.addWidget(self.edit)
layout.addWidget(btn_box)
self.setLayout(layout)
dlg = JsonPasteDialog(self)
if dlg.exec() == QDialog.Accepted:
text = dlg.edit.toPlainText().strip()
if not text:
QMessageBox.information(self, "Brak danych", "Nie wklejono żadnego JSON.")
return
try:
data_list = json.loads(text)
# Jeśli ktoś wklei pojedyńczy obiekt (dict),
# to obłóżmy go w listę, żeby iterować tak samo:
if isinstance(data_list, dict):
data_list = [data_list]
except json.JSONDecodeError as e:
QMessageBox.warning(self, "Błąd JSON", f"Niepoprawny JSON:\n{e}")
return
# Wybieramy, gdzie wklejamy subtree
selected = self.selectedItems()
if selected:
parent_item = selected[0]
else:
parent_item = None
# Używamy istniejącej logiki add_subtree_recursively
for node_data in data_list:
self.add_subtree_recursively(parent_item, node_data)
# Upiększamy wcięcia
apply_indentation_to_all_columns(self)
QMessageBox.information(self, "OK", "Dodano subtree z JSON.")
# -------------------------------------
# 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 delete_selected_items(self):
"""Usunięcie zaznaczonych elementów (Undo/Redo)."""
selected = self.selectedItems()
for item in reversed(selected):
cmd = RemoveItemCommand(self, item)
if self.undo_stack:
self.undo_stack.push(cmd)
else:
cmd.redo()
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)
# -------------------------------------
# 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", []))
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