#50: Statuszeile mit Version, Provider und Konfigurationsdateipfad

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-30 12:35:21 +02:00
parent dc17824e84
commit 4f5ce4c750
6 changed files with 585 additions and 11 deletions
@@ -277,6 +277,15 @@ public final class GuiConfigurationEditorWorkspace {
*/
Consumer<String> titleUpdateListener = title -> { };
/**
* Listener der bei jedem Zustandswechsel des Editor-Zustands aufgerufen wird und
* den neuen Zustand an die Statuszeile weiterleitet.
* <p>
* Package-private, damit {@link PdfUmbenennerGuiApplication} die Statuszeile verdrahten kann.
* Standard ist ein No-Op, damit der Workspace auch ohne Statuszeile funktioniert.
*/
Consumer<GuiConfigurationEditorState> statusBarStateListener = state -> { };
/**
* Per-provider {@link GuiModelFieldContainer} instances, one for each known provider family.
* Populated in {@link #createProviderBlock(String, AiProviderFamily)} and registered with
@@ -1095,6 +1104,8 @@ public final class GuiConfigurationEditorWorkspace {
this.editorState = completion.newState();
refreshHeader();
// Statuszeile nach erfolgreichem Speichern aktualisieren (Konfigurationspfad kann neu sein)
statusBarStateListener.accept(this.editorState);
if (result.hasApiKeyPreservationNote()) {
LOG.info("GUI-Editor: API-Key fuer Provider '{}' wurde beibehalten (Feld war leer, "
@@ -1190,6 +1201,8 @@ public final class GuiConfigurationEditorWorkspace {
pendingMessages.clear();
refreshView();
runEditorValidation();
// Statuszeile über den neuen Zustand informieren
statusBarStateListener.accept(newState);
}
private void configureRoot() {
@@ -44,7 +44,8 @@ import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
* the {@link GuiManualFileCopyPort} used to manually copy a source file to the target
* folder for documents that have not yet been successfully processed, and
* the {@link GuiHistoricalDocumentContextPort} used to retrieve the historical processing
* context for documents that were skipped in the current run.
* context for documents that were skipped in the current run, and the resolved application
* version string that the status bar displays at the bottom of the main window.
* <p>
* All ports and services are supplied by Bootstrap so that the GUI adapter does not need to
* know about provider-specific HTTP details or adapter wiring.
@@ -65,7 +66,8 @@ public record GuiStartupContext(
GuiResetDocumentStatusPort resetDocumentStatusPort,
GuiManualFileRenamePort manualFileRenamePort,
GuiManualFileCopyPort manualFileCopyPort,
GuiHistoricalDocumentContextPort historicalDocumentContextPort) {
GuiHistoricalDocumentContextPort historicalDocumentContextPort,
String applicationVersion) {
/**
* Creates a fully wired startup context.
@@ -92,6 +94,8 @@ public record GuiStartupContext(
* must not be {@code null}
* @param historicalDocumentContextPort bridge that resolves the historical processing context
* for skipped documents; must not be {@code null}
* @param applicationVersion resolved application version string shown in the status
* bar; {@code null} defaults to {@code "dev"}
*/
public GuiStartupContext {
initialState = Objects.requireNonNull(initialState, "initialState must not be null");
@@ -124,6 +128,8 @@ public record GuiStartupContext(
"manualFileCopyPort must not be null");
historicalDocumentContextPort = Objects.requireNonNull(historicalDocumentContextPort,
"historicalDocumentContextPort must not be null");
// Null-Fallback für Testumgebungen ohne gepacktes JAR
applicationVersion = applicationVersion == null ? "dev" : applicationVersion;
}
/**
@@ -165,7 +171,7 @@ public record GuiStartupContext(
technicalTestOrchestrator, correctionExecutionService, batchRunLauncher,
miniRunLauncher, resetDocumentStatusPort, rejectingManualFileRenamePort(),
rejectingManualFileCopyPort(),
noOpHistoricalDocumentContextPort());
noOpHistoricalDocumentContextPort(), "dev");
}
/**
@@ -201,7 +207,7 @@ public record GuiStartupContext(
technicalTestOrchestrator, correctionExecutionService, batchRunLauncher,
rejectingMiniRunLauncher(), rejectingResetPort(), rejectingManualFileRenamePort(),
rejectingManualFileCopyPort(),
noOpHistoricalDocumentContextPort());
noOpHistoricalDocumentContextPort(), "dev");
}
/**
@@ -237,7 +243,7 @@ public record GuiStartupContext(
technicalTestOrchestrator, correctionExecutionService,
rejectingBatchRunLauncher(), rejectingMiniRunLauncher(), rejectingResetPort(),
rejectingManualFileRenamePort(), rejectingManualFileCopyPort(),
noOpHistoricalDocumentContextPort());
noOpHistoricalDocumentContextPort(), "dev");
}
private static GuiBatchRunLauncher rejectingBatchRunLauncher() {
@@ -351,6 +357,7 @@ public record GuiStartupContext(
rejectingResetPort(),
rejectingManualFileRenamePort(),
rejectingManualFileCopyPort(),
noOpHistoricalDocumentContextPort());
noOpHistoricalDocumentContextPort(),
"dev");
}
}
@@ -0,0 +1,196 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.AiProviderFamilyStringConverter;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorState;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiProviderConfigurationState;
import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
/**
* Permanente Statuszeile am unteren Rand des Hauptfensters.
* <p>
* Die Statuszeile zeigt immer drei Segmente:
* <ul>
* <li><b>Links:</b> Anwendungsversion im Format {@code V<version>}, z.&nbsp;B. {@code Vdev}.</li>
* <li><b>Mitte:</b> Aktiver Provider und Modellname aus der geladenen Konfiguration.</li>
* <li><b>Rechts:</b> Pfad der geladenen Konfigurationsdatei.</li>
* </ul>
* Wenn keine Konfiguration geladen ist, zeigen Mitte und Rechts den Text
* {@value #KEIN_PROFIL_TEXT}. Die Versionsanzeige ist stets sichtbar.
* <p>
* Alle Aktualisierungen dieser Komponente müssen auf dem JavaFX Application Thread erfolgen.
* Die Klasse selbst erzwingt dies nicht; der Aufrufer trägt die Verantwortung.
*/
public final class GuiStatusBar {
/** Anzeigetext wenn keine Konfiguration geladen ist. */
static final String KEIN_PROFIL_TEXT = "Kein Profil geladen";
/** Präfix vor der Versionsnummer in der linken Statuszeilen-Zelle. */
private static final String VERSION_PREFIX = "V";
private static final AiProviderFamilyStringConverter PROVIDER_CONVERTER =
new AiProviderFamilyStringConverter();
private final String applicationVersion;
private final BorderPane root;
private final Label versionLabel;
private final Label providerLabel;
private final Label configPathLabel;
/**
* Erstellt eine neue Statuszeile mit der angegebenen Anwendungsversion.
*
* @param applicationVersion die aufgelöste Versionsnummer; {@code null} oder leer führt zum
* Fallback {@code "dev"}
*/
public GuiStatusBar(String applicationVersion) {
this.applicationVersion = (applicationVersion == null || applicationVersion.isBlank())
? "dev"
: applicationVersion;
// Linkes Segment: Versionsanzeige
this.versionLabel = new Label(VERSION_PREFIX + this.applicationVersion);
this.versionLabel.setStyle("-fx-font-size: 11px; -fx-text-fill: #555555;");
// Mittleres Segment: Provider und Modell
this.providerLabel = new Label(KEIN_PROFIL_TEXT);
this.providerLabel.setStyle("-fx-font-size: 11px; -fx-text-fill: #555555;");
this.providerLabel.setAlignment(Pos.CENTER);
// Rechtes Segment: Konfigurationspfad
this.configPathLabel = new Label(KEIN_PROFIL_TEXT);
this.configPathLabel.setStyle("-fx-font-size: 11px; -fx-text-fill: #555555;");
this.configPathLabel.setAlignment(Pos.CENTER_RIGHT);
// Abstandhalter zwischen den Segmenten
Region leftSpacer = new Region();
Region rightSpacer = new Region();
HBox.setHgrow(leftSpacer, Priority.ALWAYS);
HBox.setHgrow(rightSpacer, Priority.ALWAYS);
HBox content = new HBox(16,
versionLabel, leftSpacer,
providerLabel, rightSpacer,
configPathLabel);
content.setAlignment(Pos.CENTER_LEFT);
content.setPadding(new Insets(4, 12, 4, 12));
content.setStyle("-fx-background-color: #f5f5f5;");
Separator topSeparator = new Separator();
this.root = new BorderPane();
this.root.setTop(topSeparator);
this.root.setCenter(content);
}
/**
* Gibt den Wurzelknoten der Statuszeile zurück, der in das Hauptfenster eingebettet wird.
*
* @return der Wurzelknoten; nie {@code null}
*/
public BorderPane root() {
return root;
}
/**
* Aktualisiert die Statuszeile anhand des aktuellen Editor-Zustands.
* <p>
* Ist kein Dateisnapshot vorhanden, wird {@link #clearConfiguration()} ausgeführt.
* Andernfalls werden Provider, Modell und Konfigurationspfad aus dem Zustand ermittelt
* und angezeigt.
* <p>
* Muss auf dem JavaFX Application Thread aufgerufen werden.
*
* @param state der aktuelle Editor-Zustand; darf nicht {@code null} sein
*/
public void applyEditorState(GuiConfigurationEditorState state) {
if (state == null || !state.hasLoadedFileSnapshot()) {
clearConfiguration();
return;
}
String configPath = state.configurationPathText();
String providerText = resolveProviderText(state);
providerLabel.setText(providerText);
configPathLabel.setText(configPath.isBlank() ? KEIN_PROFIL_TEXT : configPath);
}
/**
* Setzt Mitte und Rechts der Statuszeile auf den Text {@link #KEIN_PROFIL_TEXT} zurück.
* <p>
* Die Versionsanzeige bleibt unverändert.
* <p>
* Muss auf dem JavaFX Application Thread aufgerufen werden.
*/
public void clearConfiguration() {
providerLabel.setText(KEIN_PROFIL_TEXT);
configPathLabel.setText(KEIN_PROFIL_TEXT);
}
/**
* Gibt den aktuell angezeigten Versionstext zurück (inkl. Präfix {@code V}).
* <p>
* Für Tests zugänglich.
*
* @return der angezeigte Versionstext; nie {@code null}
*/
String versionText() {
return versionLabel.getText();
}
/**
* Gibt den aktuell angezeigten Provider-Text zurück.
* <p>
* Für Tests zugänglich.
*
* @return der angezeigte Provider-Text; nie {@code null}
*/
String providerText() {
return providerLabel.getText();
}
/**
* Gibt den aktuell angezeigten Konfigurationspfad-Text zurück.
* <p>
* Für Tests zugänglich.
*
* @return der angezeigte Konfigurationspfad-Text; nie {@code null}
*/
String configPathText() {
return configPathLabel.getText();
}
/**
* Ermittelt den anzuzeigenden Provider-Text aus dem Editor-Zustand.
* <p>
* Das Format ist: {@code Provider: <AnzeigeName> · <Modellname>}, wobei der Modellname
* weggelassen wird, wenn er leer ist.
*
* @param state der Editor-Zustand; darf nicht {@code null} sein
* @return der formatierte Provider-Text; nie {@code null}
*/
private static String resolveProviderText(GuiConfigurationEditorState state) {
String activeIdentifier = state.values().activeProviderFamily();
if (activeIdentifier == null || activeIdentifier.isBlank()) {
return KEIN_PROFIL_TEXT;
}
AiProviderFamily family = AiProviderFamily.fromIdentifier(activeIdentifier).orElse(null);
if (family == null) {
return KEIN_PROFIL_TEXT;
}
String displayName = PROVIDER_CONVERTER.toString(family);
GuiProviderConfigurationState providerState = state.values().providerConfiguration(family);
String model = providerState != null ? providerState.model() : "";
if (model == null || model.isBlank()) {
return "Provider: " + displayName;
}
return "Provider: " + displayName + " · " + model;
}
}
@@ -8,6 +8,7 @@ import javafx.application.Platform;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
@@ -69,7 +70,16 @@ public class PdfUmbenennerGuiApplication extends Application {
// Wire the title-update listener so the stage title stays in sync with the dirty state.
workspace.titleUpdateListener = primaryStage::setTitle;
Scene scene = new Scene(workspace.root(), DEFAULT_WIDTH, DEFAULT_HEIGHT);
// Statuszeile anlegen und mit dem Workspace verdrahten
GuiStatusBar statusBar = new GuiStatusBar(startupContext.applicationVersion());
workspace.statusBarStateListener = statusBar::applyEditorState;
// Statuszeile unterhalb des Workspace-Inhalts einbetten
BorderPane outerLayout = new BorderPane();
outerLayout.setCenter(workspace.root());
outerLayout.setBottom(statusBar.root());
Scene scene = new Scene(outerLayout, DEFAULT_WIDTH, DEFAULT_HEIGHT);
primaryStage.setTitle(GuiWindowTitleFormatter.format(workspace.editorState()));
primaryStage.setScene(scene);