#51: Einheitliche Status-Darstellung mit Icon, Farbe und Tooltip

Neue zentrale Klasse ProcessingStatusPresentation als einzige autoritative
Quelle fuer Icons, CSS-Farben, Tooltip-Texte und Summary-Kategorielabels
aller DocumentCompletionStatus-Werte. GuiBatchRunResultRow delegiert
statusIcon() und statusColor() an diese Klasse und stellt neue Methode
statusTooltip() bereit. In GuiBatchRunTab erhalten Status-Icons Tooltips
per CellFactory; die duplizierte private statusColor()-Methode entfaellt.
Fuer FAILED_PERMANENT wird im Detailbereich ein erweiterter Erklaerungstext
gemaess Spezifikation #51 angezeigt. Unit-Tests fuer ProcessingStatusPresentation
(alle Status, Eindeutigkeit, korrekte Mapping-Werte) und statusTooltip() in
GuiBatchRunResultRowTest ergaenzt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-30 11:55:11 +02:00
parent 732d00c4ad
commit 563d9f52db
5 changed files with 620 additions and 27 deletions
@@ -197,6 +197,8 @@ public record GuiBatchRunResultRow(
* <p>
* Wenn {@code resetPending} den Wert {@code true} hat, wird unabhängig vom
* eigentlichen Status das Reset-Icon zurückgegeben.
* <p>
* Die Icon-Werte stammen aus {@link ProcessingStatusPresentation}.
*
* @return das entsprechende Status-Zeichen
*/
@@ -204,13 +206,7 @@ public record GuiBatchRunResultRow(
if (resetPending) {
return RESET_PENDING_ICON;
}
return switch (status) {
case SUCCESS -> ""; // ✓ CHECK MARK
case FAILED_RETRYABLE -> ""; // ↻ CLOCKWISE OPEN CIRCLE ARROW
case FAILED_PERMANENT -> "×"; // × MULTIPLICATION SIGN
case SKIPPED_ALREADY_PROCESSED -> ""; // ≡ IDENTICAL TO
case SKIPPED_FINAL_FAILURE -> ""; // ⊘ CIRCLED DIVISION SLASH
};
return ProcessingStatusPresentation.iconFor(status);
}
/**
@@ -218,20 +214,36 @@ public record GuiBatchRunResultRow(
* <p>
* Wenn {@code resetPending} den Wert {@code true} hat, wird unabhängig vom
* eigentlichen Status die Reset-Farbe zurückgegeben.
* <p>
* Farbe ist niemals das einzige Unterscheidungsmerkmal {@link #statusIcon()} und
* {@link #statusTooltip()} beschreiben den Status auch ohne Farbwahrnehmung eindeutig.
* Die Farbwerte stammen aus {@link ProcessingStatusPresentation}.
*
* @return die entsprechende CSS-Hex-Farbe (z.B. "#2e7d32")
* @return die entsprechende CSS-Hex-Farbe (z. B. {@code "#2e7d32"})
*/
public String statusColor() {
if (resetPending) {
return "#757575"; // Grau für Reset-pending
}
return switch (status) {
case SUCCESS -> "#2e7d32"; // Grün
case FAILED_RETRYABLE -> "#d98200"; // Orange
case FAILED_PERMANENT -> "#c62828"; // Rot
case SKIPPED_ALREADY_PROCESSED -> "#1565c0"; // Blau-Grau
case SKIPPED_FINAL_FAILURE -> "#757575"; // Grau
};
return ProcessingStatusPresentation.cssColorFor(status);
}
/**
* Gibt den deutschsprachigen Tooltip-Text für den Verarbeitungsstatus dieser Zeile zurück.
* <p>
* Wenn {@code resetPending} den Wert {@code true} hat, wird ein Tooltip für den
* Reset-Zustand zurückgegeben.
* <p>
* Der Tooltip-Text beschreibt den Status vollständig ohne Farbe. Die Texte stammen
* aus {@link ProcessingStatusPresentation}.
*
* @return der Tooltip-Text; nie leer
*/
public String statusTooltip() {
if (resetPending) {
return RESET_PENDING_LABEL;
}
return ProcessingStatusPresentation.tooltipFor(status);
}
/**
@@ -249,7 +261,7 @@ public record GuiBatchRunResultRow(
return switch (status) {
case SUCCESS -> "Erfolgreich";
case FAILED_RETRYABLE -> "Fehlgeschlagen (wiederholbar)";
case FAILED_PERMANENT -> "Fehlgeschlagen (permanent)";
case FAILED_PERMANENT -> "Fehlgeschlagen (dauerhaft)";
case SKIPPED_ALREADY_PROCESSED -> "Übersprungen (bereits verarbeitet)";
case SKIPPED_FINAL_FAILURE -> "Übersprungen (endgültig fehlgeschlagen)";
};
@@ -67,6 +67,7 @@ import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.TextArea;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
@@ -605,6 +606,7 @@ public final class GuiBatchRunTab {
if (empty || icon == null) {
setText(null);
setStyle(null);
setTooltip(null);
return;
}
setText(icon);
@@ -612,9 +614,15 @@ public final class GuiBatchRunTab {
GuiBatchRunResultRow data = tableRow != null ? tableRow.getItem() : null;
if (data != null && data.resetPending()) {
setStyle("-fx-text-fill: #1565c0; -fx-alignment: CENTER; -fx-font-size: 14;");
} else {
String color = data != null ? statusColor(data.status()) : "#000000";
setTooltip(new Tooltip(data.statusTooltip()));
} else if (data != null) {
// Farbe aus zentralem Mapping nie alleiniges Unterscheidungsmerkmal
String color = ProcessingStatusPresentation.cssColorFor(data.status());
setStyle("-fx-text-fill: " + color + "; -fx-alignment: CENTER; -fx-font-size: 14;");
setTooltip(new Tooltip(data.statusTooltip()));
} else {
setStyle("-fx-alignment: CENTER; -fx-font-size: 14;");
setTooltip(null);
}
}
});
@@ -1419,15 +1427,7 @@ public final class GuiBatchRunTab {
// Statische Helfer
// -------------------------------------------------------------------------
private static String statusColor(DocumentCompletionStatus status) {
return switch (status) {
case SUCCESS -> "#2e7d32";
case FAILED_RETRYABLE -> "#e65100";
case FAILED_PERMANENT -> "#c62828";
case SKIPPED_ALREADY_PROCESSED -> "#1565c0";
case SKIPPED_FINAL_FAILURE -> "#757575";
};
}
// statusColor() wurde zugunsten von ProcessingStatusPresentation.cssColorFor() entfernt.
private static String formatDuration(Duration duration) {
double seconds = duration.toMillis() / 1000.0;
@@ -1475,6 +1475,14 @@ public final class GuiBatchRunTab {
"Endg\u00fcltig fehlgeschlagen. Erneute Verarbeitung nur nach Reset m\u00f6glich."));
return builder.toString();
}
if (row.status() == DocumentCompletionStatus.FAILED_PERMANENT) {
// Erweiterter Erkl\u00e4rungstext gem\u00e4\u00df Spezifikation #51 \u2013 dauerhaft fehlgeschlagen
builder.append('\n').append(ProcessingStatusPresentation.DETAIL_TEXT_FAILED_PERMANENT);
row.aiFailureMessage().ifPresent(msg ->
builder.append("\n\nFehlerdetail: ")
.append(AiFailureMessageTranslator.translate(msg)));
return builder.toString();
}
row.effectiveFileName()
.ifPresent(name -> builder.append("Neuer Dateiname: ").append(name).append('\n'));
row.resolvedDate()
@@ -0,0 +1,257 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun;
import java.util.Objects;
import de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionStatus;
/**
* Zentrale Mapping-Klasse für die visuelle Darstellung von Verarbeitungsstatus in der GUI.
* <p>
* Diese Klasse ist die einzige autoritative Quelle für Status-Icons, CSS-Farben,
* Tooltip-Texte und Summary-Kategorielabels aller {@link DocumentCompletionStatus}-Werte.
* Alle Anzeigeorte im GUI-Adapter (Ergebnistabelle, Detailbereich, Summary-Banner)
* beziehen ihre Darstellungsinformationen ausschließlich über diese Klasse.
* <p>
* Farbe ist niemals das einzige Unterscheidungsmerkmal: Icon und Tooltip-Text beschreiben
* den Status vollständig auch ohne Farb­wahrnehmung.
* <p>
* Diese Klasse enthält keine JavaFX-Typen; sie ist rein datenhaltend und zustandslos.
* Alle Methoden sind statisch.
*/
public final class ProcessingStatusPresentation {
// -------------------------------------------------------------------------
// Icons (Unicode-Zeichen, zuverlässig darstellbar unter Windows 10+)
// -------------------------------------------------------------------------
/** Icon für {@link DocumentCompletionStatus#SUCCESS}. */
public static final String ICON_SUCCESS = ""; // CHECK MARK
/** Icon für {@link DocumentCompletionStatus#FAILED_RETRYABLE}. */
public static final String ICON_FAILED_RETRYABLE = ""; // CLOCKWISE OPEN CIRCLE ARROW
/** Icon für {@link DocumentCompletionStatus#FAILED_PERMANENT}. */
public static final String ICON_FAILED_PERMANENT = "×"; // MULTIPLICATION SIGN
/** Icon für {@link DocumentCompletionStatus#SKIPPED_ALREADY_PROCESSED}. */
public static final String ICON_SKIPPED_ALREADY_PROCESSED = ""; // IDENTICAL TO
/** Icon für {@link DocumentCompletionStatus#SKIPPED_FINAL_FAILURE}. */
public static final String ICON_SKIPPED_FINAL_FAILURE = ""; // CIRCLED DIVISION SLASH
// -------------------------------------------------------------------------
// CSS-Farben (Hex-Strings für JavaFX setStyle)
// -------------------------------------------------------------------------
/** CSS-Farbe für {@link DocumentCompletionStatus#SUCCESS}. */
public static final String COLOR_SUCCESS = "#2e7d32"; // Grün
/** CSS-Farbe für {@link DocumentCompletionStatus#FAILED_RETRYABLE}. */
public static final String COLOR_FAILED_RETRYABLE = "#d98200"; // Orange
/** CSS-Farbe für {@link DocumentCompletionStatus#FAILED_PERMANENT}. */
public static final String COLOR_FAILED_PERMANENT = "#c62828"; // Rot
/** CSS-Farbe für {@link DocumentCompletionStatus#SKIPPED_ALREADY_PROCESSED}. */
public static final String COLOR_SKIPPED_ALREADY_PROCESSED = "#757575"; // Grau
/** CSS-Farbe für {@link DocumentCompletionStatus#SKIPPED_FINAL_FAILURE}. */
public static final String COLOR_SKIPPED_FINAL_FAILURE = "#424242"; // Dunkelgrau
// -------------------------------------------------------------------------
// Tooltip-Texte (deutsche Benutzertexte, gemäß Spezifikation)
// -------------------------------------------------------------------------
/** Tooltip für {@link DocumentCompletionStatus#SUCCESS}. */
public static final String TOOLTIP_SUCCESS =
"Erfolgreich verarbeitet und umbenannt.";
/** Tooltip für {@link DocumentCompletionStatus#FAILED_RETRYABLE}. */
public static final String TOOLTIP_FAILED_RETRYABLE =
"Temporärer Fehler wird beim nächsten Lauf automatisch erneut versucht.";
/** Tooltip für {@link DocumentCompletionStatus#FAILED_PERMANENT}. */
public static final String TOOLTIP_FAILED_PERMANENT =
"Dauerhaft nicht verarbeitbar z. B. kein Textinhalt (Foto-PDF), Passwortschutz "
+ "oder beschädigte Datei. Kein weiterer automatischer Versuch.";
/** Tooltip für {@link DocumentCompletionStatus#SKIPPED_ALREADY_PROCESSED}. */
public static final String TOOLTIP_SKIPPED_ALREADY_PROCESSED =
"Übersprungen wurde bereits in einem früheren Lauf erfolgreich verarbeitet.";
/** Tooltip für {@link DocumentCompletionStatus#SKIPPED_FINAL_FAILURE}. */
public static final String TOOLTIP_SKIPPED_FINAL_FAILURE =
"Endgültig übersprungen nach wiederholten Fehlern.";
// -------------------------------------------------------------------------
// Detailtext für FAILED_PERMANENT (Erklärung im Detailbereich)
// -------------------------------------------------------------------------
/**
* Erweiterter Erklärungstext, der im Detailbereich bei dauerhaft fehlgeschlagenen
* Dokumenten angezeigt wird.
*/
public static final String DETAIL_TEXT_FAILED_PERMANENT =
"Diese Datei kann nicht verarbeitet werden. Mögliche Ursachen: "
+ "kein lesbarer Text (z. B. gescanntes Foto ohne OCR), Passwortschutz "
+ "oder beschädigte Datei. "
+ "Sie können den Status manuell zurücksetzen, wenn Sie die Ursache behoben haben.";
// -------------------------------------------------------------------------
// Summary-Kategorielabels
// -------------------------------------------------------------------------
/** Summary-Kategorie für {@link DocumentCompletionStatus#SUCCESS}. */
public static final String SUMMARY_CATEGORY_SUCCESS = "erfolgreich";
/** Summary-Kategorie für {@link DocumentCompletionStatus#FAILED_RETRYABLE}. */
public static final String SUMMARY_CATEGORY_FAILED_RETRYABLE = "wird wiederholt";
/** Summary-Kategorie für {@link DocumentCompletionStatus#FAILED_PERMANENT}. */
public static final String SUMMARY_CATEGORY_FAILED_PERMANENT = "fehlgeschlagen";
/** Summary-Kategorie für {@link DocumentCompletionStatus#SKIPPED_ALREADY_PROCESSED}. */
public static final String SUMMARY_CATEGORY_SKIPPED_ALREADY_PROCESSED = "übersprungen";
/** Summary-Kategorie für {@link DocumentCompletionStatus#SKIPPED_FINAL_FAILURE}. */
public static final String SUMMARY_CATEGORY_SKIPPED_FINAL_FAILURE = "endgültig übersprungen";
// -------------------------------------------------------------------------
// Record-Typ für gebündelte Darstellungsinformationen
// -------------------------------------------------------------------------
/**
* Gebündelte visuelle Darstellungsinformationen für einen Verarbeitungsstatus.
*
* @param icon Unicode-Zeichen als Status-Icon; nie leer
* @param cssColor CSS-Hex-Farbe für das Icon, z. B. {@code "#2e7d32"}; nie leer
* @param tooltipText Deutschsprachiger Tooltip-Text; nie leer
* @param summaryCategoryLabel Kategorie-Bezeichnung für das Summary-Banner; nie leer
*/
public record StatusVisuals(
String icon,
String cssColor,
String tooltipText,
String summaryCategoryLabel) {
/**
* Kompakter Konstruktor zur Pflichtfeld-Validierung.
*
* @throws NullPointerException wenn ein Feld {@code null} ist
* @throws IllegalArgumentException wenn ein String-Feld leer ist
*/
public StatusVisuals {
Objects.requireNonNull(icon, "icon muss gesetzt sein");
Objects.requireNonNull(cssColor, "cssColor muss gesetzt sein");
Objects.requireNonNull(tooltipText, "tooltipText muss gesetzt sein");
Objects.requireNonNull(summaryCategoryLabel, "summaryCategoryLabel muss gesetzt sein");
if (icon.isBlank()) throw new IllegalArgumentException("icon darf nicht leer sein");
if (cssColor.isBlank()) throw new IllegalArgumentException("cssColor darf nicht leer sein");
if (tooltipText.isBlank()) throw new IllegalArgumentException("tooltipText darf nicht leer sein");
if (summaryCategoryLabel.isBlank())
throw new IllegalArgumentException("summaryCategoryLabel darf nicht leer sein");
}
}
// -------------------------------------------------------------------------
// Zentrale Mapping-Methoden
// -------------------------------------------------------------------------
/**
* Liefert das Status-Icon für den angegebenen Verarbeitungsstatus.
*
* @param status der Verarbeitungsstatus; darf nicht {@code null} sein
* @return das zugehörige Unicode-Zeichen; nie leer
* @throws NullPointerException wenn {@code status} {@code null} ist
*/
public static String iconFor(DocumentCompletionStatus status) {
Objects.requireNonNull(status, "status darf nicht null sein");
return switch (status) {
case SUCCESS -> ICON_SUCCESS;
case FAILED_RETRYABLE -> ICON_FAILED_RETRYABLE;
case FAILED_PERMANENT -> ICON_FAILED_PERMANENT;
case SKIPPED_ALREADY_PROCESSED -> ICON_SKIPPED_ALREADY_PROCESSED;
case SKIPPED_FINAL_FAILURE -> ICON_SKIPPED_FINAL_FAILURE;
};
}
/**
* Liefert die CSS-Hex-Farbe für das Status-Icon des angegebenen Verarbeitungsstatus.
* <p>
* Die Farbe ist nie das einzige Unterscheidungsmerkmal Icon und Tooltip-Text
* beschreiben den Status unabhängig von der Farbe eindeutig.
*
* @param status der Verarbeitungsstatus; darf nicht {@code null} sein
* @return die CSS-Hex-Farbe (z. B. {@code "#2e7d32"}); nie leer
* @throws NullPointerException wenn {@code status} {@code null} ist
*/
public static String cssColorFor(DocumentCompletionStatus status) {
Objects.requireNonNull(status, "status darf nicht null sein");
return switch (status) {
case SUCCESS -> COLOR_SUCCESS;
case FAILED_RETRYABLE -> COLOR_FAILED_RETRYABLE;
case FAILED_PERMANENT -> COLOR_FAILED_PERMANENT;
case SKIPPED_ALREADY_PROCESSED -> COLOR_SKIPPED_ALREADY_PROCESSED;
case SKIPPED_FINAL_FAILURE -> COLOR_SKIPPED_FINAL_FAILURE;
};
}
/**
* Liefert den deutschsprachigen Tooltip-Text für den angegebenen Verarbeitungsstatus.
*
* @param status der Verarbeitungsstatus; darf nicht {@code null} sein
* @return der Tooltip-Text; nie leer
* @throws NullPointerException wenn {@code status} {@code null} ist
*/
public static String tooltipFor(DocumentCompletionStatus status) {
Objects.requireNonNull(status, "status darf nicht null sein");
return switch (status) {
case SUCCESS -> TOOLTIP_SUCCESS;
case FAILED_RETRYABLE -> TOOLTIP_FAILED_RETRYABLE;
case FAILED_PERMANENT -> TOOLTIP_FAILED_PERMANENT;
case SKIPPED_ALREADY_PROCESSED -> TOOLTIP_SKIPPED_ALREADY_PROCESSED;
case SKIPPED_FINAL_FAILURE -> TOOLTIP_SKIPPED_FINAL_FAILURE;
};
}
/**
* Liefert die Summary-Kategorie-Bezeichnung für den angegebenen Verarbeitungsstatus.
* Diese Kategorie wird im Summary-Banner nach einem Lauf angezeigt.
*
* @param status der Verarbeitungsstatus; darf nicht {@code null} sein
* @return die Kategorienbezeichnung; nie leer
* @throws NullPointerException wenn {@code status} {@code null} ist
*/
public static String summaryCategoryFor(DocumentCompletionStatus status) {
Objects.requireNonNull(status, "status darf nicht null sein");
return switch (status) {
case SUCCESS -> SUMMARY_CATEGORY_SUCCESS;
case FAILED_RETRYABLE -> SUMMARY_CATEGORY_FAILED_RETRYABLE;
case FAILED_PERMANENT -> SUMMARY_CATEGORY_FAILED_PERMANENT;
case SKIPPED_ALREADY_PROCESSED -> SUMMARY_CATEGORY_SKIPPED_ALREADY_PROCESSED;
case SKIPPED_FINAL_FAILURE -> SUMMARY_CATEGORY_SKIPPED_FINAL_FAILURE;
};
}
/**
* Liefert alle gebündelten visuellen Darstellungsinformationen für den angegebenen
* Verarbeitungsstatus in einem einzigen Objekt.
*
* @param status der Verarbeitungsstatus; darf nicht {@code null} sein
* @return ein befülltes {@link StatusVisuals}-Record; nie {@code null}
* @throws NullPointerException wenn {@code status} {@code null} ist
*/
public static StatusVisuals visualsFor(DocumentCompletionStatus status) {
Objects.requireNonNull(status, "status darf nicht null sein");
return new StatusVisuals(
iconFor(status),
cssColorFor(status),
tooltipFor(status),
summaryCategoryFor(status));
}
/** Nicht instanziierbar reine Utility-Klasse. */
private ProcessingStatusPresentation() {
throw new UnsupportedOperationException("Nicht instanziierbar");
}
}