From 8918de42192aed07d8eaf44b6dc219f104f7606d Mon Sep 17 00:00:00 2001 From: tiko Date: Thu, 26 Feb 2026 09:55:26 +0100 Subject: [PATCH] Add initial implementation of ExamCalc application with UI components and grading logic --- build.gradle.kts | 55 ++++ grades.storage | 9 + settings.gradle.kts | 1 + .../github/victorpyra/calc/ExamCalcApp.java | 32 +++ .../victorpyra/calc/grading/Grading.java | 138 ++++++++++ .../github/victorpyra/calc/result/Exam.java | 83 ++++++ .../victorpyra/calc/result/ExamRecord.java | 106 ++++++++ .../victorpyra/calc/result/ExamResult.java | 65 +++++ .../victorpyra/calc/storage/LocalStorage.java | 83 ++++++ .../github/victorpyra/calc/ui/MainFrame.java | 251 ++++++++++++++++++ .../calc/ui/component/ButtonComponent.java | 241 +++++++++++++++++ .../calc/ui/component/CardComponent.java | 69 +++++ .../calc/ui/component/InputComponent.java | 103 +++++++ .../ui/component/WindowClosingComponent.java | 62 +++++ .../victorpyra/calc/ui/theme/Theme.java | 87 ++++++ src/main/resources/calculator-taskbar.png | Bin 0 -> 22926 bytes .../calc/grading/ExamRecordTest.java | 116 ++++++++ .../victorpyra/calc/grading/GradingTest.java | 56 ++++ 18 files changed, 1557 insertions(+) create mode 100644 build.gradle.kts create mode 100644 grades.storage create mode 100644 settings.gradle.kts create mode 100644 src/main/java/com/github/victorpyra/calc/ExamCalcApp.java create mode 100644 src/main/java/com/github/victorpyra/calc/grading/Grading.java create mode 100644 src/main/java/com/github/victorpyra/calc/result/Exam.java create mode 100644 src/main/java/com/github/victorpyra/calc/result/ExamRecord.java create mode 100644 src/main/java/com/github/victorpyra/calc/result/ExamResult.java create mode 100644 src/main/java/com/github/victorpyra/calc/storage/LocalStorage.java create mode 100644 src/main/java/com/github/victorpyra/calc/ui/MainFrame.java create mode 100644 src/main/java/com/github/victorpyra/calc/ui/component/ButtonComponent.java create mode 100644 src/main/java/com/github/victorpyra/calc/ui/component/CardComponent.java create mode 100644 src/main/java/com/github/victorpyra/calc/ui/component/InputComponent.java create mode 100644 src/main/java/com/github/victorpyra/calc/ui/component/WindowClosingComponent.java create mode 100644 src/main/java/com/github/victorpyra/calc/ui/theme/Theme.java create mode 100644 src/main/resources/calculator-taskbar.png create mode 100644 src/test/java/com/github/victorpyra/calc/grading/ExamRecordTest.java create mode 100644 src/test/java/com/github/victorpyra/calc/grading/GradingTest.java diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..d085aef --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,55 @@ + +plugins { + id("java") + application + id("com.github.johnrengelman.shadow") version "8.1.1" +} + +group = "com.victorpyra" +version = "1.0.0" + +repositories { + mavenCentral() +} + +dependencies { + testImplementation(platform("org.junit:junit-bom:5.10.0")) + testImplementation("org.junit.jupiter:junit-jupiter") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +tasks.test { + useJUnitPlatform() +} + +tasks.jar { + manifest { + attributes["Main-Class"] = "com.github.victorpyra.calc.ExamCalcApp" + } +} + +tasks.javadoc { + // Bestimmt das Zielverzeichnis (standardmäßig build/docs/javadoc) + setDestinationDir(file("${layout.buildDirectory.get()}/docs/javadoc")) + + // Konfiguration des Javadoc-Tools + (options as StandardJavadocDocletOptions).apply { + // Nutzt UTF-8, damit Umlaute in deinen deutschen Texten korrekt angezeigt werden + encoding = "UTF-8" + charSet = "UTF-8" + docEncoding = "UTF-8" + + // Fügt nützliche Links zur Standard-Java-Bibliothek hinzu + links("https://docs.oracle.com/en/java/javase/17/docs/api/") + + // Erzeugt eine übersichtliche Struktur + addStringOption("Xdoclint:none", "-quiet") + isAuthor = true + isVersion = true + windowTitle = "IHK Notenberechner API" + } +} + +application { + mainClass.set("com.github.victorpyra.calc.ExamCalcApp") +} \ No newline at end of file diff --git a/grades.storage b/grades.storage new file mode 100644 index 0000000..3903776 --- /dev/null +++ b/grades.storage @@ -0,0 +1,9 @@ +#IHK Calc Auto-Save +#Thu Feb 26 09:21:47 CET 2026 +AP1=3 +AP2_DOCUMENTATION=1 +AP2_PART_1=4 +AP2_PART_2=5 +AP2_PART_3=2 +AP2_PRESENTATION=4 +AP2_TECHNICAL_DISCUSSION=2 diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..bef6674 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "pruefungsrechner" \ No newline at end of file diff --git a/src/main/java/com/github/victorpyra/calc/ExamCalcApp.java b/src/main/java/com/github/victorpyra/calc/ExamCalcApp.java new file mode 100644 index 0000000..c009d06 --- /dev/null +++ b/src/main/java/com/github/victorpyra/calc/ExamCalcApp.java @@ -0,0 +1,32 @@ +package com.github.victorpyra.calc; + +import com.github.victorpyra.calc.ui.MainFrame; + +/** + * Die Klasse {@code ExamCalcApp} bildet den Haupteinstiegspunkt der Applikation. + * Die Klasse ist als 'final' markiert, um anzuzeigen, dass diese nicht weiter mit 'extends' vererbt werden kann. + * + *

+ * Sie ist für die Initialisierung und den Start der grafischen Benutzeroberfläche zuständig. Hier wird das Hauptfenster + * instanziiert und dem Benutzer angezeigt. + *

+ * + * @author Victor Pyra + * @version 1.0.0 + */ +public final class ExamCalcApp { + /** + * Die Main-Methode startet die Anwendung. + * + *

+ * Sie erzeugt eine Instanz des Hauptfensters mit dem Titel "IHK-Notenberechner" und ruft die Methode zum Öffnen des + * Frames auf. + *

+ * + * @param args Die beim Programmstart übergebenen Argumente (werden nicht verwendet). + */ + public static void main(String[] args) { + // Erstellt eine neue Instant des MainFrames mit dem Titel "IHK-Notenberechner" und öffnet diesen Frame von alleine. + new MainFrame("IHK-Notenberechner", true); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/victorpyra/calc/grading/Grading.java b/src/main/java/com/github/victorpyra/calc/grading/Grading.java new file mode 100644 index 0000000..4f8f442 --- /dev/null +++ b/src/main/java/com/github/victorpyra/calc/grading/Grading.java @@ -0,0 +1,138 @@ +package com.github.victorpyra.calc.grading; + +import com.github.victorpyra.calc.result.Exam; +import com.github.victorpyra.calc.result.ExamRecord; +import com.github.victorpyra.calc.result.ExamResult; + +/** + * Der Record {@code Grading} ist für die Berechnung der Gesamtnote verantwortlich. + * + *

+ * Er wertet einen {@link ExamRecord} aus, prüft die Bestehensregeln der IHK-Verordnung + * und formatiert das Ergebnis für die Anzeige in der Benutzeroberfläche. + *

+ * + * @param examRecord Der Datensatz mit den Prüfungsergebnissen, die bewertet werden sollen. + * + * @author Victor Pyra + * @version 1.0 + */ +public record Grading(ExamRecord examRecord) { + /** + * Die Anzahl an Prüfungsergebnissen, die für eine vollständige Berechnung notwendig sind. + */ + public static final int NEEDED_EXAM_RESULTS = 7; + + /** + * Der Schwellenwert für die Note, bis zu dem eine Prüfung als bestanden gilt + */ + private static final double PASS_THRESHOLD_GRADE = 4.4; + + /** + * Führt die Notenberechnung durch und prüft alle Bestehenskriterien. + * + *

+ * Die Methode validiert zuerst die Vollständigkeit der Daten und prüft auf Ausschlusskriterien + * wie die Note 6.0. Anschließend werden die gewichteten Noten für AP1 und AP2 ermittelt. + *

+ * + * @return Ein {@code String} mit der Erfolgsmeldung und der Note oder einer Fehlerbeschreibung. + */ + public String calculate() { + + if (!examRecord.gradable()) { + return "Nicht berechenbar.\nMind. 1 Prüfungsteil fehlt die Note von."; + } + + if (examRecord.terminated()) { + return "Du hast nicht bestanden!\nGrund: 6.0 in einer Teilprüfung erreicht."; + } + + ExamResult ap1 = examRecord.find(Exam.AP1); + + double ap1Total = weightedGrade(ap1); + double ap2Total = calculateAp2Total(); + double totalGrade = ap1Total + ap2Total; + + if (!isPassing(totalGrade, ap2Total)) { + return "Nicht bestanden!"; + } + + return formatPassResult(totalGrade, ap1Total, ap2Total, ap1); + } + + /** + * Überprüft, ob die berechneten Noten den Bestehensregeln entsprechen. + * + * @param totalGrade Die berechnete Gesamtnote über alle Teile. + * @param ap2Total Die gewichtete Summe der Ergebnisse aus Teil 2. + * @return {@code true}, wenn alle Kriterien für das Bestehen erfüllt sind. + */ + private boolean isPassing(double totalGrade, double ap2Total) { + return totalGrade <= PASS_THRESHOLD_GRADE + && ap2Total <= PASS_THRESHOLD_GRADE + && countResultsBelowOrEqual(PASS_THRESHOLD_GRADE) >= 3; + } + + /** + * Erstellt eine formatierte Nachricht für das Bestehen der Prüfung. + * + * @param totalGrade Die erreichte Gesamtnote. + * @param ap1Total Der gewichtete Anteil von AP1. + * @param ap2Total Der gewichtete Anteil von AP2. + * @param ap1 Das {@code ExamResult}-Objekt von AP1 für Zugriff auf Formatierung. + * @return Der formatierte Ergebnis-String. + */ + private String formatPassResult( + double totalGrade, + double ap1Total, + double ap2Total, + ExamResult ap1 + ) { + return String.format( + """ + Du hast bestanden! + Deine Gesamtnote beträgt: %.2f + Sie setzt sich aus der AP1: %s und AP2: %s zusammen. + """, + totalGrade, ap1.format(ap1Total), ap1.format(ap2Total) + ); + } + + /** + * Berechnet die gewichtete Gesamtsumme für alle Prüfungsteile der AP2. + * + * @return Die Summe der gewichteten AP2-Noten. + */ + private double calculateAp2Total() { + return examRecord.results() + .stream() + .filter(result -> result.exam() != Exam.AP1) + .mapToDouble(this::weightedGrade) + .sum(); + } + + /** + * Zählt die Anzahl der Prüfungsteile in AP2, die eine bestimmte Mindestnote erreichen. + * + * @param maxGrade Die maximale Note, die noch als bestanden/ausreichend zählt. + * @return Die Anzahl der erfolgreichen Prüfungsteile. + */ + private long countResultsBelowOrEqual(double maxGrade) { + return examRecord.results() + .stream() + .filter(result -> result.exam() != Exam.AP1) + .filter(unit -> unit.grade() <= maxGrade) + .count(); + } + + /** + * Berechnet die gewichtete Note für ein einzelnes Prüfungsergebnis. + * + * @param result Das zu gewichtende Ergebnis. + * @return Das Ergebnis multipliziert mit seinem Gewichtungsfaktor. + */ + private double weightedGrade(ExamResult result) { + return result.grade() * result.exam().gradeTotal(); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/victorpyra/calc/result/Exam.java b/src/main/java/com/github/victorpyra/calc/result/Exam.java new file mode 100644 index 0000000..251dbdb --- /dev/null +++ b/src/main/java/com/github/victorpyra/calc/result/Exam.java @@ -0,0 +1,83 @@ +package com.github.victorpyra.calc.result; + +/** + * Das Enum {@code Exam} repräsentiert die verschiedenen Prüfungsbestandteile der Abschlussprüfung. + * + *

+ * Jedes Element definiert sowohl eine lokalisierte Bezeichnung als auch die jeweilige + * Gewichtung für die Berechnung der Gesamtnote. Dies ermöglicht eine strukturierte + * Handhabung der Prüfungsteile innerhalb der Berechnungslogik. + *

+ * + * @author Victor Pyra + * @version 1.0 + */ +public enum Exam { + /** + * Die erste Abschlussprüfung (Teil 1), gewichtet mit 20 %. + */ + AP1("Abschlussprüfung 1", 0.2), + + /** + * Der erste schriftliche Teil der Abschlussprüfung 2, gewichtet mit 10 %. + */ + AP2_PART_1("AP2 Teil 1 (Schriftlich)", 0.1), + + /** + * Der zweite schriftliche Teil der Abschlussprüfung 2, gewichtet mit 10 %. + */ + AP2_PART_2("AP2 Teil 2 (Schriftlich)", 0.1), + + /** + * Der dritte schriftliche Teil der Abschlussprüfung 2 (WiSo), gewichtet mit 10 %. + */ + AP2_PART_3("AP2 Teil 3 (Wirtschafts- und Sozialkunde)", 0.1), + + /** + * Die Dokumentation der praktischen Arbeit in AP2, gewichtet mit 25 %. + */ + AP2_DOCUMENTATION("AP2 Dokumentation (Praktisch)", 0.25), + + /** + * Die Präsentation der praktischen Arbeit in AP2, gewichtet mit 12,5 %. + */ + AP2_PRESENTATION("AP2 Präsentation (Praktisch)", 0.125), + + /** + * Das fachliche Gespräch zur praktischen Arbeit in AP2, gewichtet mit 12,5 %. + */ + AP2_TECHNICAL_DISCUSSION("AP2 Fachgespräch (Praktisch)", 0.125); + + private final String localizedName; + private final double gradeTotal; + + /** + * Erstellt einen neuen Prüfungsbestandteil. + * + * @param localizedName Die Anzeigebezeichnung des Prüfungsteils. + * @param gradeTotal Die Gewichtung des Teils an der Gesamtnote (0.0 bis 1.0). + */ + Exam( + final String localizedName, + final double gradeTotal + ) { + this.localizedName = localizedName; + this.gradeTotal = gradeTotal; + } + + /** + * Gibt die lokalisierte Bezeichnung des Prüfungsteils zurück. + * * @return die Bezeichnung als {@code String}. + */ + public String localizedName() { + return localizedName; + } + + /** + * Gibt die Gewichtung dieses Prüfungsteils für die Gesamtnote zurück. + * * @return der Gewichtungsfaktor als {@code double}. + */ + public double gradeTotal() { + return gradeTotal; + } +} \ No newline at end of file diff --git a/src/main/java/com/github/victorpyra/calc/result/ExamRecord.java b/src/main/java/com/github/victorpyra/calc/result/ExamRecord.java new file mode 100644 index 0000000..69e3900 --- /dev/null +++ b/src/main/java/com/github/victorpyra/calc/result/ExamRecord.java @@ -0,0 +1,106 @@ +package com.github.victorpyra.calc.result; + +import com.github.victorpyra.calc.grading.Grading; +import java.util.ArrayList; +import java.util.List; + +/** + * Die Klasse {@code ExamRecord} verwaltet eine Sammlung von Prüfungsergebnissen für einen Prüfling. + * Die Klasse ist als 'final' markiert, um anzuzeigen, dass diese nicht weiter mit 'extends' vererbt werden kann. + * + *

+ * Sie bietet Mechanismen zum Hinzufügen, Suchen und Validieren von Ergebnissen. Die Klasse stellt + * sicher, dass keine doppelten Prüfungen abgelegt werden und erkennt, ob alle notwendigen + * Leistungen für eine Benotung vorliegen. + *

+ * + * @author Victor Pyra + * @version 1.0 + */ +public final class ExamRecord { + private final List results; + /** + * Initialisiert eine neue, leere Ergebnisliste. + */ + private ExamRecord() { + this.results = new ArrayList<>(); + } + + /** + * Erstellt eine neue Instanz von {@code ExamRecord}. + * + * @return Eine frische Instanz dieser Klasse. + */ + public static ExamRecord create() { + return new ExamRecord(); + } + + /** + * Prüft intern, ob für einen bestimmten Prüfungsteil bereits ein Ergebnis vorliegt. + * + * @param exam Der zu prüfende Prüfungsteil. + * @return {@code true}, wenn bereits ein Ergebnis existiert, andernfalls {@code false}. + */ + private boolean alreadyTakenExam(Exam exam) { + return results.stream().anyMatch(entry -> entry.exam() == exam); + } + + /** + * Fügt ein neues Prüfungsergebnis hinzu, sofern die Maximalanzahl noch nicht erreicht + * und die Prüfung nicht bereits im Datensatz vorhanden ist. + * + * @param result Das hinzuzufügende {@code ExamResult}. + */ + public void add(ExamResult result) { + if (gradable()) { + return; + } + + if (alreadyTakenExam(result.exam())) { + return; + } + + results.add(result); + } + + /** + * Überprüft, ob die Anzahl der vorliegenden Ergebnisse für eine Gesamtnotenberechnung ausreicht. + * + * @return {@code true}, wenn die benötigte Anzahl an Ergebnissen erreicht ist. + */ + public boolean gradable() { + return results.size() == Grading.NEEDED_EXAM_RESULTS; + } + + /** + * Ermittelt, ob die Prüfung aufgrund einer ungenügenden Leistung (Note 6.0) als abgebrochen + * oder endgültig nicht bestanden gilt. + * + * @return {@code true}, wenn mindestens eine Note 6.0 enthalten ist. + */ + public boolean terminated() { + return results.stream().anyMatch(entry -> entry.grade() == 6.0); + } + + /** + * Sucht nach einem spezifischen Ergebnis innerhalb des Datensatzes. + * + * @param exam Der gesuchte Prüfungsteil. + * @return Das entsprechende {@code ExamResult} oder {@code null}, falls kein Eintrag gefunden wurde. + */ + public ExamResult find(Exam exam) { + return results.stream() + .filter(entry -> entry.exam() == exam) + .findFirst() + .orElse(null); + } + + /** + * Gibt die Liste aller aktuell gespeicherten Prüfungsergebnisse zurück. + * + * @return Eine Liste von {@code ExamResult}-Objekten. + */ + public List results() { + return results; + } +} \ No newline at end of file diff --git a/src/main/java/com/github/victorpyra/calc/result/ExamResult.java b/src/main/java/com/github/victorpyra/calc/result/ExamResult.java new file mode 100644 index 0000000..ce81730 --- /dev/null +++ b/src/main/java/com/github/victorpyra/calc/result/ExamResult.java @@ -0,0 +1,65 @@ +package com.github.victorpyra.calc.result; + +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.Locale; + +/** + * Der Record {@code ExamResult} repräsentiert das Ergebnis eines spezifischen Prüfungsteils. + * Die Klasse ist als 'final' markiert, um anzuzeigen, dass diese nicht weiter mit 'extends' vererbt werden kann. + * + *

+ * Er verknüpft einen {@link Exam}-Typ mit der erreichten Note und bietet Hilfsmittel + * zur einheitlichen Formatierung von numerischen Notenwerten nach deutschem Standard. + *

+ * + * @param exam Der Prüfungsteil, auf den sich dieses Ergebnis bezieht. + * @param grade Die erreichte Note als numerischer Wert. + * + * @author Victor Pyra + * @version 1.0 + */ +public record ExamResult( + Exam exam, + double grade +) { + /** + * Das Formatierungsobjekt zur Darstellung von Notenwerten. + * + *

+ * Es verwendet deutsche Lokalisierungseinstellungen, um sicherzustellen, dass + * Dezimaltrenner den regionalen Standards entsprechen. + *

+ * + */ + private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat( + "0", + DecimalFormatSymbols.getInstance(Locale.GERMAN) + ); + + /* + Hier wird die maximale Anzahl der Nachkommastellen auf zwei begrenzt, + um eine übersichtliche Darstellung der Noten zu gewährleisten. + + Hier wird einfach der Static-Block genutzt, um vor Erstellung jeglicher Objekte dem DecimalFormat-Objekt GLOBAL für + ALLE Objekte zu sagen, dass wir die Nachkommastellen mithilfe der Methode #setMaxFracDig[...] STATISCH setzen. + */ + static { + DECIMAL_FORMAT.setMaximumFractionDigits(2); + } + + /** + * Formatiert einen numerischen Notenwert in eine lesbare String-Repräsentation. + * + *

+ * Dabei werden maximal zwei Nachkommastellen berücksichtigt und die + * Formatierungsregeln des deutschen Lokals (z. B. Komma als Dezimaltrenner) angewandt. + *

+ * + * @param grade Der zu formatierende Notenwert. + * @return Die formatierte Note als {@code String}. + */ + public String format(double grade) { + return DECIMAL_FORMAT.format(grade); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/victorpyra/calc/storage/LocalStorage.java b/src/main/java/com/github/victorpyra/calc/storage/LocalStorage.java new file mode 100644 index 0000000..2f36697 --- /dev/null +++ b/src/main/java/com/github/victorpyra/calc/storage/LocalStorage.java @@ -0,0 +1,83 @@ +package com.github.victorpyra.calc.storage; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Properties; + +/** + * Die Klasse {@code LocalStorage} ist eine Utility-Klasse für die persistente Datenspeicherung. + * Die Klasse ist als 'final' markiert, um anzuzeigen, dass diese nicht weiter mit 'extends' vererbt werden kann. + * + *

+ * Sie fungiert als einfacher lokaler Speicher (ähnlich dem LocalStorage im Browser), um Notenwerte + * zwischen Programmstarts in einer Konfigurationsdatei zu erhalten. + *

+ * + * @author Victor Pyra + * @version 1.0 + */ +public final class LocalStorage { + /** + * Der Name der Datei, in der die Daten gespeichert werden. + */ + private static final String FILE_NAME = "grades.storage"; + + /** + * Der Pfad zur Speicherdatei im Dateisystem. + */ + private static final Path STORAGE_PATH = Paths.get(FILE_NAME); + + /** + * Schreibt die übergebenen Eigenschaften in die lokale Speicherdatei. + * + *

+ * Bestehende Daten werden dabei überschrieben. Tritt ein Fehler beim Schreibvorgang auf, + * wird dieser in die Standard-Fehlerausgabe geschrieben. + *

+ * + * @param data Ein {@link Properties}-Objekt, das die zu speichernden Schlüssel-Wert-Paare enthält. + */ + public static void save(Properties data) { + try (BufferedWriter writer = Files.newBufferedWriter(STORAGE_PATH)) { + data.store(writer, "IHK Calc Auto-Save"); + } catch (IOException exception) { + System.err.println("Exception occured while saving data to storage: " + exception.getMessage()); + } + } + + /** + * Lädt die gespeicherten Noten aus der lokalen Datei. + * + *

+ * Falls die Datei noch nicht existiert, wird ein leeres {@link Properties}-Objekt zurückgegeben. + *

+ * + * @return Ein {@link Properties}-Objekt mit den geladenen Werten. + */ + public static Properties load() { + Properties properties = new Properties(); + + if (!Files.exists(STORAGE_PATH)) { + return properties; + } + + try (BufferedReader reader = Files.newBufferedReader(STORAGE_PATH)) { + properties.load(reader); + } catch (IOException exception) { + System.err.println("Exception occured while loading data from storage: " + exception.getMessage()); + } + + return properties; + } + + /** + * Privater Konstruktor, um eine Instanziierung dieser Utility-Klasse zu verhindern. + */ + private LocalStorage() { + throw new UnsupportedOperationException("Dies ist eine Utility-Klasse und kann nicht instanziiert werden."); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/victorpyra/calc/ui/MainFrame.java b/src/main/java/com/github/victorpyra/calc/ui/MainFrame.java new file mode 100644 index 0000000..91f928e --- /dev/null +++ b/src/main/java/com/github/victorpyra/calc/ui/MainFrame.java @@ -0,0 +1,251 @@ +package com.github.victorpyra.calc.ui; + +import com.github.victorpyra.calc.result.Exam; +import com.github.victorpyra.calc.storage.LocalStorage; +import com.github.victorpyra.calc.ui.component.ButtonComponent; +import com.github.victorpyra.calc.ui.component.CardComponent; +import com.github.victorpyra.calc.ui.component.InputComponent; +import com.github.victorpyra.calc.ui.component.WindowClosingComponent; +import com.github.victorpyra.calc.ui.theme.Theme; +import java.awt.BorderLayout; +import java.awt.GridLayout; +import java.net.URL; +import java.util.EnumMap; +import java.util.Map; +import java.util.Properties; +import javax.swing.BorderFactory; +import javax.swing.BoxLayout; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.SwingConstants; +import javax.swing.border.EmptyBorder; + +/** + * Die Klasse {@code MainFrame} stellt das Hauptfenster der Anwendung dar. Die Klasse ist als 'final' markiert, um + * anzuzeigen, dass diese nicht weiter mit 'extends' vererbt werden kann. + * + *

+ * Sie ist für den Aufbau der gesamten Benutzeroberfläche zuständig, verwaltet die Eingabemasken für die verschiedenen + * Prüfungsteile und integriert die Berechnungslogik. Das Fenster wird basierend auf dem globalen {@link Theme} + * gestaltet. + *

+ * + * @author Victor Pyra + * @version 1.0 + */ +public final class MainFrame extends JFrame { + /** + * Eine Map, die alle Prüfungstypen auf ihre entsprechenden Eingabefelder abbildet. + */ + private final Map examInputFields = new EnumMap<>(Exam.class); + /** + * Der Container für den Eingabebereich, in dem die Labels und Textfelder liegen. + */ + private final JPanel inputSection = new JPanel(); + /** + * Das Hauptpanel der Anwendung, das als Hintergrund für alle anderen Komponenten dient. + */ + private final JPanel backgroundPanel = new JPanel(); + /** + * Ein Container für die Steuerungselemente, primär für den Berechnungs-Button. + */ + private final JPanel buttonSection = new JPanel(); + /** + * Die Schaltfläche zum Auslösen der Notenberechnung. + */ + private final JButton calcButton = new JButton("Ausrechnen"); + /** + * Der Container für den Ergebnisbereich am unteren Ende des Fensters. + */ + private final JPanel resultPanel = new JPanel(); + /** + * Der mehrzeilige Textbereich zur Anzeige der Berechnungsergebnisse oder Fehlermeldungen. + */ + private final JTextArea resultTextArea = new JTextArea(); + + /** + * Erstellt ein neues {@code MainFrame} mit einem spezifischen Titel. + * + *

+ * Der Konstruktor konfiguriert die grundlegenden Fenstereigenschaften, setzt das Anwendungs-Icon und initialisiert + * den Layout-Aufbau. + *

+ * + * @param title Der Titel, der in der Kopfzeile des Fensters angezeigt wird. + */ + public MainFrame(String title) { + super(title); + + setLocationRelativeTo(null); + setDefaultCloseOperation(EXIT_ON_CLOSE); + + setAppIcon(); + initFrame(); + + addWindowListener(new WindowClosingComponent(examInputFields)); + } + + /** + * Erstellt ein neues {@code MainFrame} mit einem spezifischen Titel und öffnet dieses von alleine. + * + *

+ * Der Konstruktor konfiguriert die grundlegenden Fenstereigenschaften, setzt das Anwendungs-Icon und initialisiert + * den Layout-Aufbau. + *

+ * + * @param title Der Titel, der in der Kopfzeile des Fensters angezeigt wird. + * @param open Die Flag, die markiert, dass das MainFrame von "alleine" geöffnet werden soll. + */ + public MainFrame(String title, boolean open) { + this(title); + + if (open) { + open(); + } + } + + /** + * Lädt und setzt das Icon für das Anwendungsfenster und die Taskleiste. + * + *

+ * Die Ressource wird aus dem Klassenpfad geladen. Falls die Datei nicht gefunden wird, erfolgt eine Fehlermeldung + * in der Konsole. + *

+ * + */ + private void setAppIcon() { + URL url = getClass().getResource("/calculator-taskbar.png"); + + if (url == null) { + System.err.println("Icon resource not found: /calculator-taskbar.png"); + return; + } + + setIconImage(new ImageIcon(url).getImage()); + } + + /** + * Initialisiert die Eingabefelder und den Ergebnisbereich. + * + *

+ * Diese Methode erzeugt für jeden {@link Exam}-Typ ein {@link InputComponent} und konfiguriert das + * {@code JTextArea} für die Ergebnisausgabe. Zudem wird der {@link ButtonComponent} zur Steuerung registriert. + *

+ * + */ + private void initFields() { + examInputFields.clear(); + + Properties savedData = LocalStorage.load(); + + for (Exam exam : Exam.values()) { + InputComponent inputComponent = new InputComponent(); + String savedValue = savedData.getProperty(exam.name()); + + if (savedValue != null) { + inputComponent.setText(savedValue); + } + + examInputFields.put(exam, inputComponent); + } + + resultTextArea.setEditable(false); + + resultTextArea.setWrapStyleWord(true); + resultTextArea.setLineWrap(true); + resultTextArea.setRows(6); + resultTextArea.setFont(Theme.FONT_RESULT); + + resultTextArea.setForeground(Theme.TEXT_MAIN); + resultTextArea.setBackground(Theme.RESULT_BG); + resultTextArea.setBorder(new EmptyBorder(10, 12, 10, 12)); + + new ButtonComponent( + calcButton, + resultTextArea, + examInputFields + ); + } + + /** + * Baut die Struktur des Frames auf und ordnet die Komponenten an. + * + *

+ * Hier werden die Eingabefelder in Karten-Komponenten verpackt, Scroll-Panels für die Ergebnisse erstellt und das + * BoxLayout für die vertikale Anordnung konfiguriert. + *

+ * + */ + private void initFrame() { + initFields(); + + inputSection.setLayout(new GridLayout(0, 2, 10, 8)); + inputSection.setOpaque(false); + + for (Exam exam : Exam.values()) { + JLabel label = styledLabel(exam.localizedName()); + inputSection.add(label); + inputSection.add(examInputFields.get(exam)); + } + + JPanel inputCard = CardComponent.create("Noten eingeben", inputSection); + + buttonSection.setOpaque(false); + buttonSection.setBorder(new EmptyBorder(4, 0, 4, 0)); + buttonSection.add(calcButton); + + JScrollPane scrollPane = new JScrollPane(resultTextArea); + scrollPane.setBorder(BorderFactory.createEmptyBorder()); + scrollPane.setBackground(Theme.RESULT_BG); + + JPanel resultInner = new JPanel(new BorderLayout()); + resultInner.setOpaque(false); + resultInner.add(scrollPane, BorderLayout.CENTER); + + resultPanel.setLayout(new BorderLayout()); + resultPanel.setOpaque(false); + resultPanel.add(CardComponent.create("Ergebnis", resultInner), BorderLayout.CENTER); + + backgroundPanel.setLayout(new BoxLayout(backgroundPanel, BoxLayout.Y_AXIS)); + backgroundPanel.setBackground(Theme.BG); + backgroundPanel.setBorder(new EmptyBorder(20, 20, 20, 20)); + + backgroundPanel.add(inputCard); + backgroundPanel.add(buttonSection); + backgroundPanel.add(resultPanel); + + setContentPane(backgroundPanel); + pack(); + + setMinimumSize(getSize()); + setResizable(false); + } + + /** + * Erstellt ein formatiertes Label für die Beschriftung der Eingabefelder. + * + * @param text Der anzuzeigende Text des Labels. + * @return Ein vorkonfiguriertes {@link JLabel} im Design des Themes. + */ + private JLabel styledLabel(String text) { + JLabel label = new JLabel(text, SwingConstants.LEFT); + + label.setFont(Theme.FONT_LABEL); + label.setForeground(Theme.TEXT_MAIN); + + return label; + } + + /** + * Macht das Fenster für den Benutzer sichtbar. + */ + public void open() { + setVisible(true); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/victorpyra/calc/ui/component/ButtonComponent.java b/src/main/java/com/github/victorpyra/calc/ui/component/ButtonComponent.java new file mode 100644 index 0000000..b450de8 --- /dev/null +++ b/src/main/java/com/github/victorpyra/calc/ui/component/ButtonComponent.java @@ -0,0 +1,241 @@ +package com.github.victorpyra.calc.ui.component; + +import com.github.victorpyra.calc.grading.Grading; +import com.github.victorpyra.calc.result.Exam; +import com.github.victorpyra.calc.result.ExamRecord; +import com.github.victorpyra.calc.result.ExamResult; +import com.github.victorpyra.calc.storage.LocalStorage; +import com.github.victorpyra.calc.ui.theme.Theme; +import java.awt.Color; +import java.awt.Cursor; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.Map; +import java.util.Properties; +import javax.swing.JButton; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.border.EmptyBorder; + +/** + * Die Klasse {@code CalcButtonComponent} steuert die Logik für den Berechnungs-Button. Die Klasse ist als 'final' + * markiert, um anzuzeigen, dass diese nicht weiter mit 'extends' vererbt werden kann. + * + *

+ * Sie implementiert den {@link ActionListener}, um bei einem Klick die Eingabewerte aus den Textfeldern zu validieren, + * in Notenwerte zu parsen und über die {@link Grading}-Logik auszuwerten. Zudem übernimmt sie das Styling des Buttons. + *

+ * + * @author Victor Pyra + * @version 1.0 + */ +public final class ButtonComponent implements ActionListener { + private static final String ERROR_MISSING = "Berechnung nicht möglich.\nNote nicht vorhanden."; + private static final String ERROR_FORMAT = "Berechnung nicht möglich.\nNote darf keine Buchstaben beinhalten."; + private static final String ERROR_RANGE = "Berechnung nicht möglich.\nNote muss zwischen 1 und 6 liegen!"; + /** + * Eine Map, die jedem {@link Exam}-Typ das entsprechende Texteingabefeld zuordnet. Dies ermöglicht den dynamischen + * Zugriff auf alle Notenwerte während der Validierung. + */ + private final Map examInputFields; + + /** + * Der Textbereich, in dem das Ergebnis der Berechnung oder Fehlermeldungen ausgegeben werden. + */ + private final JTextArea resultTextArea; + + /** + * Die Schaltfläche, welche die Berechnungslogik auslöst und visuell angepasst wird. + */ + private final JButton calcButton; + + /** + * Erstellt eine neue {@code CalcButtonComponent} und verknüpft sie mit der UI. + * + * @param calcButton Der Button, der die Berechnung auslösen soll. + * @param resultTextArea Der Textbereich zur Anzeige der Ergebnisse oder Fehlermeldungen. + * @param examInputFields Eine Map, die jedem Prüfungsteil sein entsprechendes Eingabefeld zuordnet. + */ + public ButtonComponent( + JButton calcButton, + JTextArea resultTextArea, + Map examInputFields + ) { + this.calcButton = calcButton; + this.resultTextArea = resultTextArea; + this.examInputFields = examInputFields; + + styleCalcButton(); + } + + /** + * Konfiguriert das visuelle Erscheinungsbild des Buttons basierend auf dem globalen Theme. + * + *

+ * Es werden Schriftarten, Farben, Rahmen und ein Hover-Effekt über den {@link HoverListener} hinzugefügt. + *

+ * + */ + private void styleCalcButton() { + calcButton.setFont(Theme.FONT_BUTTON); + calcButton.setForeground(Color.WHITE); + calcButton.setBackground(Theme.ACCENT); + calcButton.setOpaque(true); + + calcButton.setFocusPainted(false); + calcButton.setBorderPainted(false); + + calcButton.setCursor(new Cursor(Cursor.HAND_CURSOR)); + calcButton.setBorder(new EmptyBorder(8, 28, 8, 28)); + + calcButton.addMouseListener(new HoverListener()); + calcButton.addActionListener(this); + } + + /** + * Verarbeitet den Klick auf den Berechnungs-Button. + * + *

+ * Die Methode liest alle Eingabefelder aus, validiert die Inhalte auf Vollständigkeit und Korrektheit und stößt bei + * Erfolg die Notenberechnung an. + *

+ * + * @param event Das auslösende ActionEvent. + */ + @Override + public void actionPerformed(ActionEvent event) { + saveCurrentInput(); + + try { + ExamRecord record = buildExamRecord(); + showResult(new Grading(record).calculate(), Theme.SUCCESS_FG); + } catch (IllegalArgumentException exception) { + showResult(exception.getMessage(), Theme.ERROR_FG); + } + } + + private void saveCurrentInput() { + Properties properties = new Properties(); + + for (Map.Entry entry : examInputFields.entrySet()) { + String text = entry.getValue().getText(); + + if (text != null && !text.isBlank()) { + properties.setProperty(entry.getKey().name(), text); + } + } + + LocalStorage.save(properties); + } + + /** + * Erstellt ein {@link ExamRecord}-Objekt, indem die Eingabefelder aller {@link Exam}-Einträge geparst und validiert + * werden. + * + * @return ein vollständig befülltes {@link ExamRecord} + * @throws IllegalArgumentException wenn eine Eingabe fehlt, kein gültiges Format hat oder außerhalb des gültigen + * Bereichs [1.0, 6.0] liegt + */ + private ExamRecord buildExamRecord() { + ExamRecord record = ExamRecord.create(); + + for (Exam exam : Exam.values()) { + double grade = parseGradeOrThrow(examInputFields.get(exam).getText()); + record.add(new ExamResult(exam, grade)); + } + + return record; + } + + /** + * Parst und validiert eine Note aus dem gegebenen Texteingabe-String. + * + * @param text der Rohtext, der als Note interpretiert werden soll + * @return die geparste Note als {@code double} + * @throws IllegalArgumentException wenn der Text leer oder keine gültige Zahl ist, oder die Note außerhalb des + * gültigen Bereichs [1.0, 6.0] liegt + */ + private double parseGradeOrThrow(String text) { + double grade; + + try { + grade = parseGrade(text); + } catch (NumberFormatException exception) { + throw new IllegalArgumentException(exception.getMessage().equals("empty") ? ERROR_MISSING : ERROR_FORMAT); + } + + if (grade < 1.0 || grade > 6.0) { + throw new IllegalArgumentException(ERROR_RANGE); + } + + return grade; + } + + /** + * Wandelt einen Text-String in einen numerischen Notenwert um. + * + * @param text Der eingegebene Text aus einem Textfeld. + * @return Die Note als {@code double}. + * @throws NumberFormatException Wenn der Text leer ist oder kein gültiges Zahlenformat aufweist. + */ + private double parseGrade(String text) throws NumberFormatException { + if (text == null || text.isBlank()) { + throw new NumberFormatException("empty"); + } + + return Double.parseDouble(text.replace(",", ".").trim()); + } + + /** + * Zeigt eine Nachricht im Ergebnisfeld der Anwendung an. + * + * @param message Der anzuzeigende Text. + * @param color Die Farbe, in der der Text dargestellt werden soll. + */ + private void showResult(String message, Color color) { + resultTextArea.setForeground(color); + resultTextArea.setText(message); + } + + /** + * Interne Hilfsklasse zur Umsetzung von Hover-Effekten auf dem Button. + * + *

+ * Beim Betreten des Buttons mit der Maus wird die Hintergrundfarbe auf {@code ACCENT_HOVER} gesetzt und beim + * Verlassen auf {@code ACCENT} zurückgesetzt. + *

+ * + */ + private static class HoverListener extends MouseAdapter { + + /** + * Ändert die Hintergrundfarbe beim Mouse-Over. + * + * @param event Das Maus-Ereignis. + */ + @Override + public void mouseEntered(MouseEvent event) { + if (!(event.getSource() instanceof JButton button)) { + return; + } + + button.setBackground(Theme.ACCENT_HOVER); + } + + /** + * Setzt die Hintergrundfarbe beim Verlassen des Buttons zurück. + * + * @param event Das Maus-Ereignis. + */ + @Override + public void mouseExited(MouseEvent event) { + if (!(event.getSource() instanceof JButton button)) { + return; + } + + button.setBackground(Theme.ACCENT); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/victorpyra/calc/ui/component/CardComponent.java b/src/main/java/com/github/victorpyra/calc/ui/component/CardComponent.java new file mode 100644 index 0000000..bb958ba --- /dev/null +++ b/src/main/java/com/github/victorpyra/calc/ui/component/CardComponent.java @@ -0,0 +1,69 @@ +package com.github.victorpyra.calc.ui.component; + +import com.github.victorpyra.calc.ui.theme.Theme; +import java.awt.BorderLayout; +import javax.swing.BorderFactory; +import javax.swing.JPanel; +import javax.swing.border.CompoundBorder; +import javax.swing.border.EmptyBorder; +import javax.swing.border.LineBorder; +import javax.swing.border.TitledBorder; + +/** + * Die Klasse {@code CardComponent} ist eine Utility-Klasse zur Erstellung von UI-Containern. + * Die Klasse ist als 'final' markiert, um anzuzeigen, dass diese nicht weiter mit 'extends' vererbt werden kann. + * + *

+ * Sie stellt eine statische Methode bereit, um Inhalte in einem visuell abgegrenzten + * Panel (einer "Card") mit Rahmen und Titel darzustellen. Das Design orientiert sich + * dabei strikt an den Vorgaben der {@link Theme}-Klasse. + *

+ * + * @author Victor Pyra + * @version 1.0 + */ +public final class CardComponent { + /** + * Erstellt ein {@link JPanel}, das als dekorative Karte fungiert. + * + *

+ * Die Karte erhält einen abgerundeten Rahmen mit einem Titel, der über die + * {@code TitledBorder} konfiguriert wird. Innerhalb der Karte wird das übergebene + * Content-Panel platziert und farblich angepasst. + *

+ * + * @param title Der Text, der als Überschrift im Rahmen der Karte angezeigt wird. + * @param content Das {@code JPanel}, welches im Inneren der Karte angezeigt werden soll. + * @return Ein fertig konfiguriertes {@code JPanel} im Card-Design. + */ + public static JPanel create(String title, JPanel content) { + JPanel card = new JPanel(new BorderLayout()); + card.setBackground(Theme.PANEL_BG); + + TitledBorder titled = BorderFactory.createTitledBorder( + new LineBorder(Theme.BORDER_COL, 1, true), + title, + TitledBorder.LEFT, + TitledBorder.TOP, + Theme.FONT_TITLE, + Theme.TEXT_MUTED + ); + + card.setBorder(new CompoundBorder( + titled, + new EmptyBorder(8, 10, 10, 10) + )); + + content.setBackground(Theme.PANEL_BG); + card.add(content, BorderLayout.CENTER); + + return card; + } + + /** + * Privater Konstruktor, um eine Instanziierung dieser Utility-Klasse zu verhindern. + */ + private CardComponent() { + throw new UnsupportedOperationException("Dies ist eine Utility-Klasse und kann nicht instanziiert werden."); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/victorpyra/calc/ui/component/InputComponent.java b/src/main/java/com/github/victorpyra/calc/ui/component/InputComponent.java new file mode 100644 index 0000000..62772c2 --- /dev/null +++ b/src/main/java/com/github/victorpyra/calc/ui/component/InputComponent.java @@ -0,0 +1,103 @@ +package com.github.victorpyra.calc.ui.component; + +import com.github.victorpyra.calc.ui.theme.Theme; +import java.awt.Color; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; +import javax.swing.JTextField; +import javax.swing.border.Border; +import javax.swing.border.CompoundBorder; +import javax.swing.border.EmptyBorder; +import javax.swing.border.LineBorder; + +/** + * Die Klasse {@code InputComponent} ist eine spezialisierte {@link JTextField}-Komponente. + * Die Klasse ist als 'final' markiert, um anzuzeigen, dass diese nicht weiter mit 'extends' vererbt werden kann. + * + *

+ * Sie erweitert das Standard-Textfeld um das globale Design-System der Anwendung. + * Dies umfasst die automatische Anwendung von Farben, Schriftarten und einen + * dynamischen Rahmen, der auf Fokus-Ereignisse reagiert. + *

+ * + * @author Victor Pyra + * @version 1.0 + */ +public final class InputComponent extends JTextField { + /** + * Der Standard-Rahmen der Komponente im inaktiven Zustand. + */ + private static final Border DEFAULT_BORDER = new CompoundBorder( + new LineBorder(Theme.BORDER_COL, 2, true), + new EmptyBorder(6, 10, 6, 10) + ); + + /** + * Der Rahmen der Komponente, wenn sie den Tastaturfokus erhält. + */ + private static final Border FOCUS_BORDER = new CompoundBorder( + new LineBorder(Theme.ACCENT, 2, true), + new EmptyBorder(6, 10, 6, 10) + ); + + /** + * Erstellt eine neue {@code InputComponent} und initialisiert das visuelle Erscheinungsbild. + * + *

+ * Es werden die Schriftarten, Farben für Text, Hintergrund und Cursor sowie der + * initiale Rahmen aus der {@link Theme}-Klasse geladen. Zudem wird ein + * {@link FocusHandler} registriert. + *

+ * + */ + public InputComponent() { + super(); + + setFont(Theme.FONT_FIELD); + setForeground(Theme.TEXT_MAIN); + setBackground(Color.WHITE); + setCaretColor(Theme.ACCENT); + setBorder(DEFAULT_BORDER); + + addFocusListener(new FocusHandler()); + } + + /** + * Interne Hilfsklasse zur Handhabung von Fokus-Ereignissen. + * + *

+ * Diese Klasse wechselt den Rahmen der Komponente zwischen {@code DEFAULT_BORDER} + * und {@code FOCUS_BORDER}, um dem Benutzer visuelles Feedback zu geben. + *

+ * + */ + private static class FocusHandler extends FocusAdapter { + /** + * Wird aufgerufen, wenn die Komponente den Fokus erhält. + * + * @param event Das entsprechende Fokus-Ereignis. + */ + @Override + public void focusGained(FocusEvent event) { + if (!(event.getSource() instanceof JTextField field)) { + return; + } + + field.setBorder(FOCUS_BORDER); + } + + /** + * Wird aufgerufen, wenn die Komponente den Fokus verliert. + * + * @param event Das entsprechende Fokus-Ereignis. + */ + @Override + public void focusLost(FocusEvent event) { + if (!(event.getSource() instanceof JTextField field)) { + return; + } + + field.setBorder(DEFAULT_BORDER); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/victorpyra/calc/ui/component/WindowClosingComponent.java b/src/main/java/com/github/victorpyra/calc/ui/component/WindowClosingComponent.java new file mode 100644 index 0000000..00a708c --- /dev/null +++ b/src/main/java/com/github/victorpyra/calc/ui/component/WindowClosingComponent.java @@ -0,0 +1,62 @@ +package com.github.victorpyra.calc.ui.component; + +import com.github.victorpyra.calc.result.Exam; +import com.github.victorpyra.calc.storage.LocalStorage; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.util.Map; +import java.util.Properties; +import javax.swing.JTextField; + +/** + * Die Klasse {@code WindowClosingComponent} überwacht das Schließen des Anwendungsfensters. + * + *

+ * Sie implementiert einen {@link WindowAdapter}, um beim Beenden der Anwendung automatisch + * alle aktuellen Eingaben aus den Textfeldern zu sammeln und über den {@link LocalStorage} + * persistent zu speichern. + *

+ * + * @author Victor Pyra + * @version 1.0 + */ +public class WindowClosingComponent extends WindowAdapter { + /** + * Die Map mit den Eingabefeldern, deren Inhalte gespeichert werden sollen. + */ + private final Map examInputFields; + + /** + * Erstellt eine neue {@code WindowClosingComponent}. + * + * @param examInputFields Die Map, die den Zugriff auf die aktuellen Noteneingaben ermöglicht. + */ + public WindowClosingComponent(Map examInputFields) { + this.examInputFields = examInputFields; + } + + /** + * Wird aufgerufen, wenn das Fenster geschlossen wird. + * + *

+ * Die Methode iteriert über alle bekannten Prüfungsteile, extrahiert die Texte aus den + * Eingabefeldern und stößt den Speichervorgang an. + *

+ * + * @param event Das Fenster-Ereignis. + */ + @Override + public void windowClosing(WindowEvent event) { + Properties property = new Properties(); + + for (Map.Entry entry : examInputFields.entrySet()) { + String text = entry.getValue().getText(); + + if (text != null && !text.isBlank()) { + property.setProperty(entry.getKey().name(), text); + } + } + + LocalStorage.save(property); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/victorpyra/calc/ui/theme/Theme.java b/src/main/java/com/github/victorpyra/calc/ui/theme/Theme.java new file mode 100644 index 0000000..c4fddfa --- /dev/null +++ b/src/main/java/com/github/victorpyra/calc/ui/theme/Theme.java @@ -0,0 +1,87 @@ +package com.github.victorpyra.calc.ui.theme; + +import java.awt.Color; +import java.awt.Font; + +/** + * Die Klasse {@code Theme} dient als zentraler Speicherort für alle visuellen Stilvorgaben der Benutzeroberfläche. + * Die Klasse ist als 'final' markiert, um anzuzeigen, dass diese nicht weiter mit 'extends' vererbt werden kann. + * + *

+ * Sie enthält Definitionen für Farben und Schriftarten, um eine konsistente Gestaltung der + * Taschenrechner-Applikation sicherzustellen. Durch die Verwendung dieser Konstanten kann das Design der gesamten + * Anwendung an einer einzigen Stelle angepasst werden. + *

+ * + * @author Victor Pyra + * @version 1.0 + */ +public class Theme { + /** + * Die Haupt-Hintergrundfarbe für das Anwendungsfenster (helles Grau). + */ + public static final Color BG = new Color(245, 246, 248); + /** + * Die Hintergrundfarbe für Panels und Inhaltsbereiche (reines Weiß). + */ + public static final Color PANEL_BG = Color.WHITE; + /** + * Die primäre Akzentfarbe für interaktive Elemente wie Buttons oder Highlights. + */ + public static final Color ACCENT = new Color(41, 98, 200); + /** + * Eine dunklere Variante der Akzentfarbe, die für Mouse-Over-Effekte (Hover) genutzt wird. + */ + public static final Color ACCENT_HOVER = new Color(26, 76, 160); + /** + * Die Standardfarbe für Rahmen und Trennlinien zwischen Komponenten. + */ + public static final Color BORDER_COL = new Color(220, 222, 226); + /** + * Die Haupt-Textfarbe für maximale Lesbarkeit auf hellem Hintergrund. + */ + public static final Color TEXT_MAIN = new Color(30, 30, 35); + /** + * Eine abgeschwächte Textfarbe für sekundäre Informationen oder Platzhalter. + */ + public static final Color TEXT_MUTED = new Color(110, 115, 125); + /** + * Die Hintergrundfarbe speziell für das Display- bzw. Ergebnisfeld. + */ + public static final Color RESULT_BG = new Color(250, 251, 253); + /** + * Die Signalfarbe (Rot) für Fehlermeldungen oder ungültige Eingaben. + */ + public static final Color ERROR_FG = new Color(190, 40, 40); + /** + * Die Signalfarbe (Grün) für erfolgreiche Operationen oder positive Statusmeldungen. + */ + public static final Color SUCCESS_FG = new Color(25, 120, 60); + /** + * Die Standardschriftart für allgemeine Beschriftungen (Labels). + */ + public static final Font FONT_LABEL = new Font("Segoe UI", Font.PLAIN, 13); + /** + * Die Schriftart für Texteingabefelder. + */ + public static final Font FONT_FIELD = new Font("Segoe UI", Font.PLAIN, 13); + /** + * Die fettgedruckte Schriftart für Schaltflächen, um sie visuell hervorzuheben. + */ + public static final Font FONT_BUTTON = new Font("Segoe UI", Font.BOLD, 13); + /** + * Die Schriftart für die Anzeige der berechneten Ergebnisse. + */ + public static final Font FONT_RESULT = new Font("Segoe UI", Font.PLAIN, 13); + /** + * Eine kleinere, fettgedruckte Schriftart für Überschriften oder Sektionstitel. + */ + public static final Font FONT_TITLE = new Font("Segoe UI", Font.BOLD, 11); + + /** + * Privater Konstruktor, um eine Instanziierung dieser Utility-Klasse zu verhindern. + */ + private Theme() { + throw new UnsupportedOperationException("Dies ist eine Utility-Klasse und kann nicht instanziiert werden."); + } +} \ No newline at end of file diff --git a/src/main/resources/calculator-taskbar.png b/src/main/resources/calculator-taskbar.png new file mode 100644 index 0000000000000000000000000000000000000000..a7a60710e40672bb382fdc209cec9b095cc9761b GIT binary patch literal 22926 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 VQr$|PJNVH8bTkarYi>J4{}0y`jqCsb literal 0 HcmV?d00001 diff --git a/src/test/java/com/github/victorpyra/calc/grading/ExamRecordTest.java b/src/test/java/com/github/victorpyra/calc/grading/ExamRecordTest.java new file mode 100644 index 0000000..c4e887f --- /dev/null +++ b/src/test/java/com/github/victorpyra/calc/grading/ExamRecordTest.java @@ -0,0 +1,116 @@ +package com.github.victorpyra.calc.grading; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.github.victorpyra.calc.result.Exam; +import com.github.victorpyra.calc.result.ExamRecord; +import com.github.victorpyra.calc.result.ExamResult; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Die Klasse {@code ExamRecordTest} validiert die Funktionalität der Ergebnisverwaltung. + * + *

+ * Geprüft wird insbesondere die Integrität des Datensatzes, das Verhindern von Duplikaten sowie die korrekte + * Zustandsermittlung für die Notenberechnung. + *

+ * + * @author Victor Pyra + * @version 1.0 + */ +class ExamRecordTest { + /** + * Die zu testende Instanz des Datensatzes. + */ + private ExamRecord record; + + /** + * Initialisiert vor jedem Testlauf einen neuen, leeren {@link ExamRecord}. + */ + @BeforeEach + void setUp() { + record = ExamRecord.create(); + } + + /** + * Überprüft, ob ein Ergebnis korrekt hinzugefügt werden kann und danach auffindbar ist. + */ + @Test + void shouldAddAndFindResult() { + ExamResult result = new ExamResult(Exam.AP1, 2.0); + record.add(result); + + assertNotNull(record.find(Exam.AP1)); + assertEquals(2.0, record.find(Exam.AP1).grade()); + } + + /** + * Stellt sicher, dass für denselben Prüfungsteil keine zwei Ergebnisse hinterlegt werden können. + */ + @Test + void shouldNotAddDuplicateExams() { + record.add(new ExamResult(Exam.AP1, 2.0)); + record.add(new ExamResult(Exam.AP1, 3.0)); + + assertEquals(1, record.results().size()); + assertEquals(2.0, record.find(Exam.AP1).grade()); + } + + /** + * Validiert die Logik der {@code gradable}-Methode. + * + *

+ * Ein Datensatz darf erst dann als berechenbar markiert werden, wenn die in {@link Grading#NEEDED_EXAM_RESULTS} + * definierte Anzahl an Prüfungen vorliegt. + *

+ * + */ + @Test + void shouldBeGradableOnlyWhenComplete() { + assertFalse(record.gradable()); + + for (Exam exam : Exam.values()) { + record.add(new ExamResult(exam, 3.0)); + } + + assertTrue(record.gradable()); + assertEquals(Grading.NEEDED_EXAM_RESULTS, record.results().size()); + } + + /** + * Prüft, ob das Erreichen der Note 6.0 korrekt als Abbruchkriterium (terminated) erkannt wird. + */ + @Test + void shouldIdentifyTerminatedRecord() { + record.add(new ExamResult(Exam.AP1, 6.0)); + assertTrue(record.terminated()); + } + + /** + * Stellt sicher, dass bei der Suche nach einem nicht vorhandenen Prüfungsteil {@code null} zurückgegeben wird, + * anstatt eine Exception zu werfen. + */ + @Test + void shouldReturnNullWhenExamNotFound() { + assertNull(record.find(Exam.AP1)); + } + + /** + * Überprüft, ob nach Erreichen der maximalen Anzahl an Ergebnissen keine weiteren Einträge mehr akzeptiert werden. + */ + @Test + void shouldNotAddBeyondLimit() { + for (Exam exam : Exam.values()) { + record.add(new ExamResult(exam, 3.0)); + } + + // Simulierter Versuch, ein zusätzliches theoretisches Ergebnis zu addieren + record.add(new ExamResult(Exam.AP1, 1.0)); + assertEquals(Grading.NEEDED_EXAM_RESULTS, record.results().size()); + } +} \ No newline at end of file diff --git a/src/test/java/com/github/victorpyra/calc/grading/GradingTest.java b/src/test/java/com/github/victorpyra/calc/grading/GradingTest.java new file mode 100644 index 0000000..8dc02dd --- /dev/null +++ b/src/test/java/com/github/victorpyra/calc/grading/GradingTest.java @@ -0,0 +1,56 @@ +package com.github.victorpyra.calc.grading; + +import com.github.victorpyra.calc.result.Exam; +import com.github.victorpyra.calc.result.ExamRecord; +import com.github.victorpyra.calc.result.ExamResult; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Die Klasse {@code GradingTest} enthält Unit-Tests für die Notenberechnung. + * + *

+ * Hier werden verschiedene Szenarien geprüft, wie zum Beispiel das Bestehen + * bei Mindestpunktzahl, das Durchfallen durch eine Note 6.0 oder die + * korrekte Gewichtung der einzelnen Prüfungsteile. + *

+ * + */ +class GradingTest { + private ExamRecord record; + /** + * Bereitet die Testumgebung vor jedem einzelnen Testlauf vor. + */ + @BeforeEach + void setUp() { + record = ExamRecord.create(); + } + + /** + * Prüft, ob die Berechnung korrekt erkennt, wenn Prüfungsteile fehlen. + */ + @Test + void shouldReturnErrorMessageWhenExamsMissing() { + Grading grading = new Grading(record); + String result = grading.calculate(); + + assertTrue(result.contains("Nicht berechenbar")); + } + + /** + * Validiert das Ausschlusskriterium einer ungenügenden Leistung (Note 6.0). + * + *

+ * Auch wenn der Gesamtschnitt rechnerisch ausreichen würde, führt eine 6.0 + * in einem Teilbereich sofort zum Nichtbestehen. + *

+ * + */ + @Test + void shouldFailWhenAnyGradeIsSix() { + // Hier würden alle 7 Prüfungen hinzugefügt, eine davon mit 6.0 + record.add(new ExamResult(Exam.AP1, 6.0)); + assertTrue(record.terminated()); + } +} \ No newline at end of file