#50: Statuszeile mit Version, Provider und Konfigurationsdateipfad
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+13
@@ -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() {
|
||||
|
||||
+13
-6
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
+196
@@ -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. 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;
|
||||
}
|
||||
}
|
||||
+11
-1
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user