Add initial implementation of ExamCalc application with UI components and grading logic
This commit is contained in:
55
build.gradle.kts
Normal file
55
build.gradle.kts
Normal file
@@ -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")
|
||||
}
|
||||
9
grades.storage
Normal file
9
grades.storage
Normal file
@@ -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
|
||||
1
settings.gradle.kts
Normal file
1
settings.gradle.kts
Normal file
@@ -0,0 +1 @@
|
||||
rootProject.name = "pruefungsrechner"
|
||||
32
src/main/java/com/github/victorpyra/calc/ExamCalcApp.java
Normal file
32
src/main/java/com/github/victorpyra/calc/ExamCalcApp.java
Normal file
@@ -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.
|
||||
*
|
||||
* <p>
|
||||
* Sie ist für die Initialisierung und den Start der grafischen Benutzeroberfläche zuständig. Hier wird das Hauptfenster
|
||||
* instanziiert und dem Benutzer angezeigt.
|
||||
* </p>
|
||||
*
|
||||
* @author Victor Pyra
|
||||
* @version 1.0.0
|
||||
*/
|
||||
public final class ExamCalcApp {
|
||||
/**
|
||||
* Die Main-Methode startet die Anwendung.
|
||||
*
|
||||
* <p>
|
||||
* Sie erzeugt eine Instanz des Hauptfensters mit dem Titel "IHK-Notenberechner" und ruft die Methode zum Öffnen des
|
||||
* Frames auf.
|
||||
* </p>
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
138
src/main/java/com/github/victorpyra/calc/grading/Grading.java
Normal file
138
src/main/java/com/github/victorpyra/calc/grading/Grading.java
Normal file
@@ -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.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
* @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();
|
||||
}
|
||||
}
|
||||
83
src/main/java/com/github/victorpyra/calc/result/Exam.java
Normal file
83
src/main/java/com/github/victorpyra/calc/result/Exam.java
Normal file
@@ -0,0 +1,83 @@
|
||||
package com.github.victorpyra.calc.result;
|
||||
|
||||
/**
|
||||
* Das Enum {@code Exam} repräsentiert die verschiedenen Prüfungsbestandteile der Abschlussprüfung.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
106
src/main/java/com/github/victorpyra/calc/result/ExamRecord.java
Normal file
106
src/main/java/com/github/victorpyra/calc/result/ExamRecord.java
Normal file
@@ -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.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
* @author Victor Pyra
|
||||
* @version 1.0
|
||||
*/
|
||||
public final class ExamRecord {
|
||||
private final List<ExamResult> 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<ExamResult> results() {
|
||||
return results;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>
|
||||
* Er verknüpft einen {@link Exam}-Typ mit der erreichten Note und bietet Hilfsmittel
|
||||
* zur einheitlichen Formatierung von numerischen Notenwerten nach deutschem Standard.
|
||||
* </p>
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* <p>
|
||||
* Es verwendet deutsche Lokalisierungseinstellungen, um sicherzustellen, dass
|
||||
* Dezimaltrenner den regionalen Standards entsprechen.
|
||||
* </p>
|
||||
*
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* <p>
|
||||
* Dabei werden maximal zwei Nachkommastellen berücksichtigt und die
|
||||
* Formatierungsregeln des deutschen Lokals (z. B. Komma als Dezimaltrenner) angewandt.
|
||||
* </p>
|
||||
*
|
||||
* @param grade Der zu formatierende Notenwert.
|
||||
* @return Die formatierte Note als {@code String}.
|
||||
*/
|
||||
public String format(double grade) {
|
||||
return DECIMAL_FORMAT.format(grade);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>
|
||||
* Sie fungiert als einfacher lokaler Speicher (ähnlich dem LocalStorage im Browser), um Notenwerte
|
||||
* zwischen Programmstarts in einer Konfigurationsdatei zu erhalten.
|
||||
* </p>
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* <p>
|
||||
* Bestehende Daten werden dabei überschrieben. Tritt ein Fehler beim Schreibvorgang auf,
|
||||
* wird dieser in die Standard-Fehlerausgabe geschrieben.
|
||||
* </p>
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* <p>
|
||||
* Falls die Datei noch nicht existiert, wird ein leeres {@link Properties}-Objekt zurückgegeben.
|
||||
* </p>
|
||||
*
|
||||
* @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.");
|
||||
}
|
||||
}
|
||||
251
src/main/java/com/github/victorpyra/calc/ui/MainFrame.java
Normal file
251
src/main/java/com/github/victorpyra/calc/ui/MainFrame.java
Normal file
@@ -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.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
* @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<Exam, JTextField> 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.
|
||||
*
|
||||
* <p>
|
||||
* Der Konstruktor konfiguriert die grundlegenden Fenstereigenschaften, setzt das Anwendungs-Icon und initialisiert
|
||||
* den Layout-Aufbau.
|
||||
* </p>
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* <p>
|
||||
* Der Konstruktor konfiguriert die grundlegenden Fenstereigenschaften, setzt das Anwendungs-Icon und initialisiert
|
||||
* den Layout-Aufbau.
|
||||
* </p>
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* <p>
|
||||
* Die Ressource wird aus dem Klassenpfad geladen. Falls die Datei nicht gefunden wird, erfolgt eine Fehlermeldung
|
||||
* in der Konsole.
|
||||
* </p>
|
||||
*
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* <p>
|
||||
* Hier werden die Eingabefelder in Karten-Komponenten verpackt, Scroll-Panels für die Ergebnisse erstellt und das
|
||||
* BoxLayout für die vertikale Anordnung konfiguriert.
|
||||
* </p>
|
||||
*
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
* @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<Exam, JTextField> 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<Exam, JTextField> examInputFields
|
||||
) {
|
||||
this.calcButton = calcButton;
|
||||
this.resultTextArea = resultTextArea;
|
||||
this.examInputFields = examInputFields;
|
||||
|
||||
styleCalcButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Konfiguriert das visuelle Erscheinungsbild des Buttons basierend auf dem globalen Theme.
|
||||
*
|
||||
* <p>
|
||||
* Es werden Schriftarten, Farben, Rahmen und ein Hover-Effekt über den {@link HoverListener} hinzugefügt.
|
||||
* </p>
|
||||
*
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* <p>
|
||||
* Die Methode liest alle Eingabefelder aus, validiert die Inhalte auf Vollständigkeit und Korrektheit und stößt bei
|
||||
* Erfolg die Notenberechnung an.
|
||||
* </p>
|
||||
*
|
||||
* @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<Exam, JTextField> 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.
|
||||
*
|
||||
* <p>
|
||||
* Beim Betreten des Buttons mit der Maus wird die Hintergrundfarbe auf {@code ACCENT_HOVER} gesetzt und beim
|
||||
* Verlassen auf {@code ACCENT} zurückgesetzt.
|
||||
* </p>
|
||||
*
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
* @author Victor Pyra
|
||||
* @version 1.0
|
||||
*/
|
||||
public final class CardComponent {
|
||||
/**
|
||||
* Erstellt ein {@link JPanel}, das als dekorative Karte fungiert.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
* @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.");
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* <p>
|
||||
* Diese Klasse wechselt den Rahmen der Komponente zwischen {@code DEFAULT_BORDER}
|
||||
* und {@code FOCUS_BORDER}, um dem Benutzer visuelles Feedback zu geben.
|
||||
* </p>
|
||||
*
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
* @author Victor Pyra
|
||||
* @version 1.0
|
||||
*/
|
||||
public class WindowClosingComponent extends WindowAdapter {
|
||||
/**
|
||||
* Die Map mit den Eingabefeldern, deren Inhalte gespeichert werden sollen.
|
||||
*/
|
||||
private final Map<Exam, JTextField> examInputFields;
|
||||
|
||||
/**
|
||||
* Erstellt eine neue {@code WindowClosingComponent}.
|
||||
*
|
||||
* @param examInputFields Die Map, die den Zugriff auf die aktuellen Noteneingaben ermöglicht.
|
||||
*/
|
||||
public WindowClosingComponent(Map<Exam, JTextField> examInputFields) {
|
||||
this.examInputFields = examInputFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wird aufgerufen, wenn das Fenster geschlossen wird.
|
||||
*
|
||||
* <p>
|
||||
* Die Methode iteriert über alle bekannten Prüfungsteile, extrahiert die Texte aus den
|
||||
* Eingabefeldern und stößt den Speichervorgang an.
|
||||
* </p>
|
||||
*
|
||||
* @param event Das Fenster-Ereignis.
|
||||
*/
|
||||
@Override
|
||||
public void windowClosing(WindowEvent event) {
|
||||
Properties property = new Properties();
|
||||
|
||||
for (Map.Entry<Exam, JTextField> entry : examInputFields.entrySet()) {
|
||||
String text = entry.getValue().getText();
|
||||
|
||||
if (text != null && !text.isBlank()) {
|
||||
property.setProperty(entry.getKey().name(), text);
|
||||
}
|
||||
}
|
||||
|
||||
LocalStorage.save(property);
|
||||
}
|
||||
}
|
||||
87
src/main/java/com/github/victorpyra/calc/ui/theme/Theme.java
Normal file
87
src/main/java/com/github/victorpyra/calc/ui/theme/Theme.java
Normal file
@@ -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.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
* @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.");
|
||||
}
|
||||
}
|
||||
BIN
src/main/resources/calculator-taskbar.png
Normal file
BIN
src/main/resources/calculator-taskbar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -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.
|
||||
*
|
||||
* <p>
|
||||
* Geprüft wird insbesondere die Integrität des Datensatzes, das Verhindern von Duplikaten sowie die korrekte
|
||||
* Zustandsermittlung für die Notenberechnung.
|
||||
* </p>
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* <p>
|
||||
* Ein Datensatz darf erst dann als berechenbar markiert werden, wenn die in {@link Grading#NEEDED_EXAM_RESULTS}
|
||||
* definierte Anzahl an Prüfungen vorliegt.
|
||||
* </p>
|
||||
*
|
||||
*/
|
||||
@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());
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
*/
|
||||
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).
|
||||
*
|
||||
* <p>
|
||||
* Auch wenn der Gesamtschnitt rechnerisch ausreichen würde, führt eine 6.0
|
||||
* in einem Teilbereich sofort zum Nichtbestehen.
|
||||
* </p>
|
||||
*
|
||||
*/
|
||||
@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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user