commit 8918de42192aed07d8eaf44b6dc219f104f7606d Author: tiko Date: Thu Feb 26 09:55:26 2026 +0100 Add initial implementation of ExamCalc application with UI components and grading logic 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 0000000..a7a6071 Binary files /dev/null and b/src/main/resources/calculator-taskbar.png differ 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