M10 bis AP-003

This commit is contained in:
2026-04-20 13:07:19 +02:00
parent 20b847d821
commit 01414fc732
16 changed files with 1139 additions and 184 deletions
@@ -34,9 +34,9 @@ import org.apache.logging.log4j.Logger;
*
* <h2>Current scope</h2>
* <p>
* The adapter launches a minimal GUI shell that proves the GUI startup path works.
* It does not provide a configuration editor, file operations, validation, provider
* controls, or any other functionality beyond the technical startup proof.
* The adapter launches the editor shell with the unloaded start state, an optional startup
* notice, and a file-loading callback supplied by Bootstrap. File I/O and save behavior remain
* outside the current GUI step.
*/
public class GuiAdapter {
@@ -84,13 +84,22 @@ public class GuiAdapter {
* or if the platform is not supported
*/
public void start(Optional<String> startupNotice) {
start(GuiStartupContext.blank(startupNotice));
}
/**
* Starts the JavaFX GUI with the supplied startup context.
*
* @param startupContext startup data for the GUI; must not be {@code null}
*/
public void start(GuiStartupContext startupContext) {
LOG.info("GUI-Adapter: JavaFX-Start wird eingeleitet.");
if (startupNotice.isPresent()) {
Application.launch(PdfUmbenennerGuiApplication.class,
PdfUmbenennerGuiApplication.STARTUP_NOTICE_ARG_PREFIX + startupNotice.get());
} else {
GuiStartupContextHolder.install(startupContext);
try {
Application.launch(PdfUmbenennerGuiApplication.class);
} finally {
GuiStartupContextHolder.clear();
LOG.info("GUI-Adapter: JavaFX-Anwendung wurde beendet.");
}
LOG.info("GUI-Adapter: JavaFX-Anwendung wurde beendet.");
}
}
@@ -0,0 +1,449 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorState;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationTemplateFactory;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Separator;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Window;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Builds the editor workspace shown after the JavaFX application starts.
* <p>
* The workspace owns the unloaded start state, the optional startup notice, the file-loading
* callback and the visible section scaffold for the single-tab shell. It performs no save
* operations and no validation logic.
*/
public final class GuiConfigurationEditorWorkspace {
private static final Logger LOG = LogManager.getLogger(GuiConfigurationEditorWorkspace.class);
private static final String WELCOME_TEXT =
"Willkommen. Legen Sie mit „Neu“ eine Standardvorlage an oder öffnen Sie eine bestehende Konfiguration.";
private final BorderPane root = new BorderPane();
private final Label statusLabel = new Label();
private final Label configurationPathValueLabel = new Label();
private final Label welcomeTitleLabel = new Label("Willkommen");
private final Label welcomeTextLabel = new Label(WELCOME_TEXT);
private final TabPane tabPane = new TabPane();
private final VBox sectionsBox = new VBox(12);
private final Button newButton = new Button("Neu");
private final Button openButton = new Button("Öffnen");
private final Button saveButton = new Button("Speichern");
private final Button saveAsButton = new Button("Speichern unter");
private final GuiConfigurationFileLoader configurationFileLoader;
private GuiConfigurationEditorState editorState;
private boolean welcomeGuidanceVisible;
/**
* Creates a new workspace with the unloaded start state.
*
* @param startupNotice optional startup notice to show in the header area
*/
public GuiConfigurationEditorWorkspace(Optional<String> startupNotice) {
this(GuiStartupContext.blank(startupNotice));
}
/**
* Creates a new workspace with the supplied startup context.
*
* @param startupContext startup data from Bootstrap; must not be {@code null}
*/
public GuiConfigurationEditorWorkspace(GuiStartupContext startupContext) {
GuiStartupContext effectiveContext = startupContext == null
? GuiStartupContext.blank(Optional.empty())
: startupContext;
this.configurationFileLoader = effectiveContext.configurationFileLoader();
this.editorState = effectiveContext.initialState();
this.welcomeGuidanceVisible = editorState.isNewConfiguration();
configureRoot();
configureHeader(effectiveContext.startupNotice());
configureTabs();
configureActionBar();
configureActions();
refreshView();
}
/**
* Returns the root node used by the JavaFX scene.
*
* @return the root pane for the workspace
*/
public Parent root() {
return root;
}
/**
* Returns the currently active editor state.
*
* @return the current editor state
*/
public GuiConfigurationEditorState editorState() {
return editorState;
}
/**
* Returns the currently displayed configuration-path text.
*
* @return the header text for the loaded configuration path
*/
public String configurationPathText() {
return configurationPathValueLabel.getText();
}
/**
* Returns the welcome text shown in the editor start state.
*
* @return the German welcome text
*/
public String welcomeText() {
return welcomeTextLabel.getText();
}
/**
* Returns whether the welcome guidance is currently visible.
*
* @return {@code true} when the welcome text is shown, otherwise {@code false}
*/
public boolean isWelcomeGuidanceVisible() {
return welcomeGuidanceVisible;
}
/**
* Returns the "Neu" action button.
*
* @return the button used to switch to the standard template
*/
public Button newButton() {
return newButton;
}
/**
* Returns the "Öffnen" action button.
*
* @return the button used for the open-file flow
*/
public Button openButton() {
return openButton;
}
/**
* Returns the "Speichern" action button.
*
* @return the button used for future save wiring
*/
public Button saveButton() {
return saveButton;
}
/**
* Returns the "Speichern unter" action button.
*
* @return the button used for future save-as wiring
*/
public Button saveAsButton() {
return saveAsButton;
}
/**
* Returns the tab pane containing the single editor tab.
*
* @return the tab pane shown in the center area
*/
public TabPane tabPane() {
return tabPane;
}
/**
* Returns the fixed section titles shown inside the single configuration tab.
*
* @return the section titles in visual order
*/
List<String> sectionTitles() {
return List.of("Pfade", "Provider", "Verarbeitungslimits", "Tests", "Meldungen");
}
/**
* Handles the explicit "Neu" action.
* <p>
* The workspace switches to the standard template and hides the welcome guidance.
*/
public void requestNewConfiguration() {
LOG.info("GUI-Editor: Neue Standardvorlage wird angezeigt.");
applyEditorState(GuiConfigurationTemplateFactory.createStandardTemplate());
}
/**
* Handles the explicit "Öffnen" action.
* <p>
* The file chooser is native to the platform and filters for {@code *.properties} files.
*/
public void requestOpenConfiguration() {
Window owner = root.getScene() == null ? null : root.getScene().getWindow();
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Konfiguration öffnen");
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Properties-Dateien", "*.properties"));
if (owner != null && editorState.hasLoadedFileSnapshot()) {
Path currentPath = editorState.loadedFileSnapshot().orElseThrow().filePath();
Path parent = currentPath.getParent();
if (parent != null) {
fileChooser.setInitialDirectory(parent.toFile());
}
}
java.io.File selectedFile = fileChooser.showOpenDialog(owner);
if (selectedFile != null) {
openConfigurationFile(selectedFile.toPath());
}
}
/**
* Opens the selected configuration file on a background thread.
*
* @param configFilePath the selected file path; must not be {@code null}
*/
void openConfigurationFile(Path configFilePath) {
if (configurationFileLoader == null) {
showError("Öffnen ist derzeit nicht verfügbar.");
return;
}
Thread worker = new Thread(() -> {
try {
GuiConfigurationEditorState loadedState = configurationFileLoader.load(configFilePath);
Platform.runLater(() -> applyEditorState(loadedState));
} catch (Exception exception) {
Platform.runLater(() -> showError("Konfiguration konnte nicht geladen werden: "
+ safeMessage(exception)));
}
}, "gui-config-loader");
worker.setDaemon(true);
worker.start();
}
/**
* Handles the explicit "Speichern" action.
* <p>
* File writing is intentionally not implemented yet.
*/
public void requestSaveConfiguration() {
LOG.info("GUI-Editor: Speichern-Aktion wurde ausgelöst, ist aber noch nicht implementiert.");
}
/**
* Handles the explicit "Speichern unter" action.
* <p>
* File writing is intentionally not implemented yet.
*/
public void requestSaveConfigurationAs() {
LOG.info("GUI-Editor: Speichern-unter-Aktion wurde ausgelöst, ist aber noch nicht implementiert.");
}
private void applyEditorState(GuiConfigurationEditorState newState) {
this.editorState = newState;
this.welcomeGuidanceVisible = false;
statusLabel.setVisible(false);
statusLabel.setManaged(false);
refreshView();
}
private void configureRoot() {
root.getStyleClass().add("gui-editor-root");
root.setPadding(new Insets(16));
}
private void configureHeader(Optional<String> startupNotice) {
Label titleLabel = new Label("PDF-Umbenenner");
titleLabel.setStyle("-fx-font-size: 24px; -fx-font-weight: bold;");
Label subtitleLabel = new Label("Konfigurationseditor");
subtitleLabel.setStyle("-fx-font-size: 12px; -fx-text-fill: #666666;");
VBox titleBox = new VBox(2, titleLabel, subtitleLabel);
Label pathCaption = new Label("Konfigurationspfad:");
pathCaption.setStyle("-fx-font-weight: bold;");
HBox pathRow = new HBox(8, pathCaption, configurationPathValueLabel);
pathRow.setAlignment(Pos.CENTER_LEFT);
statusLabel.setWrapText(true);
statusLabel.setVisible(false);
statusLabel.setManaged(false);
statusLabel.setStyle(
"-fx-padding: 8px 10px; -fx-background-color: #fff2cc; -fx-text-fill: #5c3b00; -fx-border-color: #e0b95b;");
VBox headerBox = new VBox(8, titleBox, pathRow, statusLabel, new Separator());
headerBox.setPadding(new Insets(0, 0, 16, 0));
root.setTop(headerBox);
startupNotice.ifPresent(this::showStatusMessage);
}
private void configureTabs() {
Tab editorTab = new Tab("Konfiguration");
editorTab.setClosable(false);
sectionsBox.setSpacing(12);
sectionsBox.setFillWidth(true);
ScrollPane scrollPane = new ScrollPane(sectionsBox);
scrollPane.setFitToWidth(true);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
scrollPane.setPadding(new Insets(0));
editorTab.setContent(scrollPane);
tabPane.getTabs().add(editorTab);
root.setCenter(tabPane);
}
private void configureActionBar() {
HBox actionBar = new HBox(10, newButton, openButton, saveButton, saveAsButton);
actionBar.setAlignment(Pos.CENTER_LEFT);
actionBar.setPadding(new Insets(16, 0, 0, 0));
root.setBottom(actionBar);
}
private void configureActions() {
newButton.setOnAction(event -> requestNewConfiguration());
openButton.setOnAction(event -> requestOpenConfiguration());
saveButton.setOnAction(event -> requestSaveConfiguration());
saveAsButton.setOnAction(event -> requestSaveConfigurationAs());
}
private void refreshView() {
refreshHeader();
refreshSections();
}
private void refreshHeader() {
configurationPathValueLabel.setText(editorState.configurationPathText());
}
private void refreshSections() {
sectionsBox.getChildren().setAll(
createWelcomeCard(),
createPathsSection(),
createProviderSection(),
createProcessingLimitsSection(),
createTestsSection(),
createMessagesSection());
}
private Node createWelcomeCard() {
VBox card = createCardContainer();
welcomeTitleLabel.setStyle("-fx-font-size: 16px; -fx-font-weight: bold;");
welcomeTextLabel.setWrapText(true);
welcomeTextLabel.setStyle("-fx-font-size: 13px;");
card.getChildren().addAll(welcomeTitleLabel, welcomeTextLabel);
card.setVisible(welcomeGuidanceVisible);
card.setManaged(welcomeGuidanceVisible);
return card;
}
private Node createPathsSection() {
VBox card = createCardContainer();
card.getChildren().addAll(
sectionTitle("Pfade"),
textLabel("Der Bereich ist vorbereitet. Geladene Pfade werden in einem späteren Schritt editierbar ergänzt."));
return card;
}
private Node createProviderSection() {
VBox card = createCardContainer();
card.getChildren().addAll(
sectionTitle("Provider"),
textLabel("Der Bereich ist vorbereitet. Provider-Auswahl und zugehörige Eingaben folgen in einem späteren Schritt."));
return card;
}
private Node createProcessingLimitsSection() {
VBox card = createCardContainer();
card.getChildren().addAll(
sectionTitle("Verarbeitungslimits"),
textLabel("Der Bereich ist vorbereitet. Eingaben für Limits werden in einem späteren Schritt ergänzt."));
return card;
}
private Node createTestsSection() {
VBox card = createCardContainer();
card.getChildren().addAll(
sectionTitle("Tests"),
textLabel("Der Bereich ist vorbereitet. Test- und Diagnoseaktionen werden in einem späteren Schritt ergänzt."));
return card;
}
private Node createMessagesSection() {
VBox card = createCardContainer();
card.getChildren().addAll(
sectionTitle("Meldungen"),
textLabel("Der Bereich ist vorbereitet. Sichtbare Meldungen und technische Hinweise folgen in einem späteren Schritt."));
return card;
}
private VBox createCardContainer() {
VBox card = new VBox(8);
card.setStyle(
"-fx-padding: 12px; -fx-border-color: #d8d8d8; -fx-border-radius: 8px; -fx-background-radius: 8px; -fx-background-color: white;");
card.setMaxWidth(Double.MAX_VALUE);
VBox.setVgrow(card, Priority.NEVER);
return card;
}
private Label sectionTitle(String title) {
Label label = new Label(title);
label.setStyle("-fx-font-size: 14px; -fx-font-weight: bold;");
return label;
}
private Label textLabel(String text) {
Label label = new Label(text);
label.setWrapText(true);
return label;
}
private void showStatusMessage(String message) {
statusLabel.setText(message);
statusLabel.setVisible(true);
statusLabel.setManaged(true);
}
private void showError(String message) {
LOG.warn("GUI-Editor: {}", message);
showStatusMessage(message);
Alert alert = new Alert(Alert.AlertType.ERROR, message, ButtonType.OK);
alert.setHeaderText("Konfiguration konnte nicht geladen werden");
alert.show();
}
private String safeMessage(Throwable exception) {
return exception == null ? "unbekannter Fehler" : exception.getMessage() == null
? exception.getClass().getSimpleName()
: exception.getMessage();
}
}
@@ -0,0 +1,23 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui;
import java.nio.file.Path;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorState;
/**
* Loads a configuration file into a GUI editor state.
* <p>
* The interface allows Bootstrap to provide the concrete file-loading and migration logic
* while the GUI only deals with editor states.
*/
@FunctionalInterface
public interface GuiConfigurationFileLoader {
/**
* Loads the configuration file at the given path.
*
* @param configFilePath the file to load; must not be {@code null}
* @return the loaded editor state; never {@code null}
*/
GuiConfigurationEditorState load(Path configFilePath);
}
@@ -0,0 +1,26 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui;
/**
* Runtime exception thrown when the GUI configuration cannot be loaded.
*/
public class GuiConfigurationLoadException extends RuntimeException {
/**
* Creates a new load exception.
*
* @param message the exception message
* @param cause the underlying cause
*/
public GuiConfigurationLoadException(String message, Throwable cause) {
super(message, cause);
}
/**
* Creates a new load exception.
*
* @param message the exception message
*/
public GuiConfigurationLoadException(String message) {
super(message);
}
}
@@ -0,0 +1,46 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui;
import java.util.Objects;
import java.util.Optional;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorState;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorStateFactory;
/**
* Immutable startup data for the GUI adapter.
* <p>
* Carries the initial editor state, the optional startup notice and the file-loading callback
* that the workspace uses for native open actions.
*/
public record GuiStartupContext(
GuiConfigurationEditorState initialState,
Optional<String> startupNotice,
GuiConfigurationFileLoader configurationFileLoader) {
/**
* Creates a startup context.
*
* @param initialState initial editor state; must not be {@code null}
* @param startupNotice optional startup notice; {@code null} becomes empty
* @param configurationFileLoader file-loading callback; must not be {@code null}
*/
public GuiStartupContext {
initialState = Objects.requireNonNull(initialState, "initialState must not be null");
startupNotice = startupNotice == null ? Optional.empty() : startupNotice;
configurationFileLoader = Objects.requireNonNull(configurationFileLoader,
"configurationFileLoader must not be null");
}
/**
* Creates a blank startup context with no loader side effects.
*
* @param startupNotice optional startup notice; {@code null} becomes empty
* @return a startup context for the unloaded editor start
*/
public static GuiStartupContext blank(Optional<String> startupNotice) {
return new GuiStartupContext(
GuiConfigurationEditorStateFactory.createBlankStartState(),
startupNotice,
configFilePath -> GuiConfigurationEditorStateFactory.createBlankStartState());
}
}
@@ -0,0 +1,31 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
/**
* Process-local holder for the next GUI startup context.
* <p>
* JavaFX creates the application class reflectively, so Bootstrap installs the context here
* immediately before the JavaFX launch call.
*/
final class GuiStartupContextHolder {
private static final AtomicReference<GuiStartupContext> CONTEXT = new AtomicReference<>();
private GuiStartupContextHolder() {
// Utility class.
}
static void install(GuiStartupContext context) {
CONTEXT.set(context);
}
static GuiStartupContext currentOrBlank() {
return Optional.ofNullable(CONTEXT.get()).orElseGet(() -> GuiStartupContext.blank(Optional.empty()));
}
static void clear() {
CONTEXT.set(null);
}
}
@@ -1,153 +1,61 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui;
import java.util.List;
import java.util.Optional;
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Minimal JavaFX {@link Application} subclass that establishes the GUI shell.
* JavaFX application entry point for the PDF-Umbenenner GUI inbound adapter.
* <p>
* This class is the JavaFX lifecycle entry point launched by
* {@link GuiAdapter#start(java.util.Optional)}. It creates the primary stage with a
* minimal, neutral layout that proves the GUI startup path is technically functional.
* The layout is deliberately structured as a {@link BorderPane} so that later milestones
* can populate individual regions (header, center content, bottom message area) without
* requiring an architectural restructuring.
*
* <h2>Startup notice</h2>
* <p>
* When Bootstrap forwards a startup notice (e.g. because the supplied {@code --config}
* path was not found), the notice is passed as an application parameter prefixed with
* {@link #STARTUP_NOTICE_ARG_PREFIX}. This class reads the parameter via
* {@link Application#getParameters()} and displays it prominently in the center region
* instead of the default placeholder.
*
* <h2>Current scope</h2>
* <p>
* The shell displays the application window with a neutral title and either a static
* placeholder label or a startup notice. It contains no configuration editor, no file
* operations, no validation, no provider controls, and no message area. These are the
* responsibility of later milestones.
*
* <h2>Explicit non-goals</h2>
* <ul>
* <li>No configuration editor or editable input fields</li>
* <li>No file operations (Neu, Oeffnen, Speichern, Speichern unter)</li>
* <li>No validation or provider controls</li>
* <li>No message area or technical tests</li>
* <li>No welcome text in the final product sense</li>
* </ul>
*
* <h2>Threading</h2>
* <p>
* The {@link #start(Stage)} method is called by the JavaFX runtime on the
* JavaFX Application Thread. No blocking operations are performed during
* stage setup.
* The application starts the editor shell in a clean, unloaded state unless Bootstrap
* has provided a preloaded startup context. The visible editor surface is delegated to
* {@link GuiConfigurationEditorWorkspace}.
*/
public class PdfUmbenennerGuiApplication extends Application {
private static final Logger LOG = LogManager.getLogger(PdfUmbenennerGuiApplication.class);
/**
* Argument prefix used to forward a startup notice from {@link GuiAdapter} to this
* application via {@link Application#launch(Class, String[])}.
* <p>
* When an argument beginning with this prefix is present in the raw parameter list,
* the remainder of the argument string is treated as the notice text to display.
*/
static final String STARTUP_NOTICE_ARG_PREFIX = "--startup-notice=";
private static final String WINDOW_TITLE = "PDF-Umbenenner";
private static final double DEFAULT_WIDTH = 800;
private static final double DEFAULT_HEIGHT = 600;
private static final double DEFAULT_WIDTH = 1100;
private static final double DEFAULT_HEIGHT = 800;
/**
* Creates a new instance of the JavaFX application.
* <p>
* This no-argument constructor is required by the JavaFX runtime, which
* instantiates the {@link Application} subclass reflectively.
*/
public PdfUmbenennerGuiApplication() {
// Required by JavaFX runtime for reflective instantiation.
}
/**
* Initializes and shows the primary stage with a minimal GUI shell.
* <p>
* The stage is set up with a {@link BorderPane} root layout. When a startup notice is
* present (forwarded via application parameters by {@link GuiAdapter}), the notice is
* displayed prominently in red in the center region. Otherwise a neutral placeholder
* label is shown. The layout structure is chosen to allow incremental extension in later
* milestones without requiring a root-level restructuring.
* <p>
* Start and shutdown events are logged via Log4j2 to satisfy the GUI logging requirements.
* Initializes and shows the primary stage.
*
* @param primaryStage the primary stage provided by the JavaFX runtime; never {@code null}
*/
@Override
public void start(Stage primaryStage) {
LOG.info("GUI-Shell: JavaFX-Oberfläche wird initialisiert.");
LOG.info("GUI: JavaFX-Oberfläche wird initialisiert.");
BorderPane root = new BorderPane();
GuiStartupContext startupContext = GuiStartupContextHolder.currentOrBlank();
GuiConfigurationEditorWorkspace workspace = new GuiConfigurationEditorWorkspace(startupContext);
Scene scene = new Scene(workspace.root(), DEFAULT_WIDTH, DEFAULT_HEIGHT);
Optional<String> startupNotice = extractStartupNotice();
if (startupNotice.isPresent()) {
Label noticeLabel = new Label(startupNotice.get());
noticeLabel.setWrapText(true);
noticeLabel.setAlignment(Pos.CENTER);
noticeLabel.setStyle("-fx-font-size: 13px; -fx-text-fill: #cc0000;");
root.setCenter(noticeLabel);
LOG.info("GUI-Shell: Starthinweis wird angezeigt.");
} else {
Label placeholderLabel = new Label("PDF-Umbenenner GUI wird vorbereitet …");
placeholderLabel.setStyle("-fx-font-size: 14px; -fx-text-fill: #666666;");
root.setCenter(placeholderLabel);
}
Scene scene = new Scene(root, DEFAULT_WIDTH, DEFAULT_HEIGHT);
primaryStage.setTitle(WINDOW_TITLE);
primaryStage.setScene(scene);
primaryStage.setOnCloseRequest(event ->
LOG.info("GUI-Shell: Fenster wird vom Benutzer geschlossen."));
primaryStage.setOnCloseRequest(event -> LOG.info("GUI: Fenster wird vom Benutzer geschlossen."));
primaryStage.show();
LOG.info("GUI-Shell: Hauptfenster erfolgreich angezeigt.");
}
/**
* Extracts the startup notice from the application parameters, if present.
* <p>
* Searches the raw parameter list for an argument beginning with
* {@link #STARTUP_NOTICE_ARG_PREFIX} and returns the remainder as the notice text.
* Returns an empty Optional when no such argument is present.
*
* @return the startup notice text, or an empty Optional if no notice was forwarded
*/
private Optional<String> extractStartupNotice() {
List<String> rawParams = getParameters().getRaw();
return rawParams.stream()
.filter(p -> p.startsWith(STARTUP_NOTICE_ARG_PREFIX))
.map(p -> p.substring(STARTUP_NOTICE_ARG_PREFIX.length()))
.findFirst();
LOG.info("GUI: Hauptfenster erfolgreich angezeigt.");
}
/**
* Called by the JavaFX runtime when the application is stopping.
* <p>
* Logs the GUI shutdown event. No additional cleanup is required
* for the minimal shell.
* Logs the GUI shutdown event. No additional cleanup is required.
*/
@Override
public void stop() {
LOG.info("GUI-Shell: JavaFX-Anwendung wird beendet.");
LOG.info("GUI: JavaFX-Anwendung wird beendet.");
}
}
@@ -18,7 +18,8 @@ import java.util.Optional;
public record GuiConfigurationEditorState(
Optional<GuiConfigurationFileSnapshot> loadedFileSnapshot,
GuiConfigurationValues baselineValues,
GuiConfigurationValues values) {
GuiConfigurationValues values,
Optional<String> pendingMigrationMessage) {
/**
* Creates a new editor state.
@@ -31,6 +32,7 @@ public record GuiConfigurationEditorState(
loadedFileSnapshot = loadedFileSnapshot == null ? Optional.empty() : loadedFileSnapshot;
baselineValues = Objects.requireNonNull(baselineValues, "baselineValues must not be null");
values = Objects.requireNonNull(values, "values must not be null");
pendingMigrationMessage = pendingMigrationMessage == null ? Optional.empty() : pendingMigrationMessage;
}
/**
@@ -69,6 +71,36 @@ public record GuiConfigurationEditorState(
return loadedFileSnapshot.isEmpty();
}
/**
* Returns the configuration-path text shown in the GUI header.
* <p>
* When no file snapshot is loaded, the header must remain empty so the GUI does not
* suggest a file is already associated with the editor state.
*
* @return the loaded configuration path as text, or an empty string when no file is loaded
*/
public String configurationPathText() {
return loadedFileSnapshot.map(snapshot -> snapshot.filePath().toString()).orElse("");
}
/**
* Returns whether the editor holds a migration note for later display.
*
* @return {@code true} when a migration note is stored, otherwise {@code false}
*/
public boolean hasPendingMigrationMessage() {
return pendingMigrationMessage.isPresent();
}
/**
* Returns the stored migration note text.
*
* @return the migration note text, or an empty string when no note is stored
*/
public String pendingMigrationMessageText() {
return pendingMigrationMessage.orElse("");
}
/**
* Returns a copy with different current editable values.
*
@@ -76,7 +108,7 @@ public record GuiConfigurationEditorState(
* @return a new editor state containing the supplied values
*/
public GuiConfigurationEditorState withValues(GuiConfigurationValues values) {
return new GuiConfigurationEditorState(loadedFileSnapshot, baselineValues, values);
return new GuiConfigurationEditorState(loadedFileSnapshot, baselineValues, values, pendingMigrationMessage);
}
/**
@@ -86,7 +118,7 @@ public record GuiConfigurationEditorState(
* @return a new editor state using the supplied baseline
*/
public GuiConfigurationEditorState withBaselineValues(GuiConfigurationValues baselineValues) {
return new GuiConfigurationEditorState(loadedFileSnapshot, baselineValues, values);
return new GuiConfigurationEditorState(loadedFileSnapshot, baselineValues, values, pendingMigrationMessage);
}
/**
@@ -96,7 +128,7 @@ public record GuiConfigurationEditorState(
* @return a new editor state linked to the supplied snapshot
*/
public GuiConfigurationEditorState withLoadedFileSnapshot(GuiConfigurationFileSnapshot snapshot) {
return new GuiConfigurationEditorState(Optional.of(snapshot), baselineValues, values);
return new GuiConfigurationEditorState(Optional.of(snapshot), baselineValues, values, pendingMigrationMessage);
}
/**
@@ -105,7 +137,18 @@ public record GuiConfigurationEditorState(
* @return a new editor state without a loaded file snapshot
*/
public GuiConfigurationEditorState withoutLoadedFileSnapshot() {
return new GuiConfigurationEditorState(Optional.empty(), baselineValues, values);
return new GuiConfigurationEditorState(Optional.empty(), baselineValues, values, pendingMigrationMessage);
}
/**
* Returns a copy with a migration note that is kept for later display.
*
* @param migrationMessage the migration note; must not be {@code null}
* @return a new editor state carrying the supplied migration note
*/
public GuiConfigurationEditorState withPendingMigrationMessage(String migrationMessage) {
return new GuiConfigurationEditorState(loadedFileSnapshot, baselineValues, values,
Optional.of(migrationMessage));
}
/**
@@ -116,6 +159,7 @@ public record GuiConfigurationEditorState(
public GuiConfigurationEditorState markClean() {
return baselineValues.equals(values)
? this
: new GuiConfigurationEditorState(loadedFileSnapshot, baselineValues, baselineValues);
: new GuiConfigurationEditorState(loadedFileSnapshot, baselineValues, baselineValues,
pendingMigrationMessage);
}
}
@@ -0,0 +1,96 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui.editor;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
/**
* Creates editor states from loaded properties snapshots or from the empty start state.
* <p>
* The factory maps the raw `.properties` document into the GUI-side editor model without
* introducing validation or effective-value resolution into the file load path.
*/
public final class GuiConfigurationEditorStateFactory {
private static final String PROP_SOURCE_FOLDER = "source.folder";
private static final String PROP_TARGET_FOLDER = "target.folder";
private static final String PROP_SQLITE_FILE = "sqlite.file";
private static final String PROP_PROMPT_TEMPLATE_FILE = "prompt.template.file";
private static final String PROP_RUNTIME_LOCK_FILE = "runtime.lock.file";
private static final String PROP_LOG_DIRECTORY = "log.directory";
private static final String PROP_LOG_LEVEL = "log.level";
private static final String PROP_MAX_RETRIES_TRANSIENT = "max.retries.transient";
private static final String PROP_MAX_PAGES = "max.pages";
private static final String PROP_MAX_TEXT_CHARACTERS = "max.text.characters";
private static final String PROP_LOG_AI_SENSITIVE = "log.ai.sensitive";
private static final String PROP_ACTIVE_PROVIDER = "ai.provider.active";
private static final String PROP_CLAUDE_BASE_URL = "ai.provider.claude.baseUrl";
private static final String PROP_CLAUDE_MODEL = "ai.provider.claude.model";
private static final String PROP_CLAUDE_TIMEOUT = "ai.provider.claude.timeoutSeconds";
private static final String PROP_CLAUDE_API_KEY = "ai.provider.claude.apiKey";
private static final String PROP_OPENAI_BASE_URL = "ai.provider.openai-compatible.baseUrl";
private static final String PROP_OPENAI_MODEL = "ai.provider.openai-compatible.model";
private static final String PROP_OPENAI_TIMEOUT = "ai.provider.openai-compatible.timeoutSeconds";
private static final String PROP_OPENAI_API_KEY = "ai.provider.openai-compatible.apiKey";
private GuiConfigurationEditorStateFactory() {
// Utility class.
}
/**
* Creates an editor state from a loaded properties snapshot.
*
* @param snapshot the loaded file snapshot; must not be {@code null}
* @param pendingMigrationMessage optional migration note to retain for later display
* @return an editor state representing the loaded configuration file
*/
public static GuiConfigurationEditorState fromPropertiesSnapshot(
GuiConfigurationFileSnapshot snapshot,
Optional<String> pendingMigrationMessage) {
Properties properties = snapshot.properties();
Map<AiProviderFamily, GuiProviderConfigurationState> providerConfigurations = new LinkedHashMap<>();
providerConfigurations.put(AiProviderFamily.CLAUDE, new GuiProviderConfigurationState(
propertyOrBlank(properties, PROP_CLAUDE_BASE_URL),
propertyOrBlank(properties, PROP_CLAUDE_MODEL),
propertyOrBlank(properties, PROP_CLAUDE_TIMEOUT),
GuiProviderApiKeyState.unresolved(propertyOrBlank(properties, PROP_CLAUDE_API_KEY))));
providerConfigurations.put(AiProviderFamily.OPENAI_COMPATIBLE, new GuiProviderConfigurationState(
propertyOrBlank(properties, PROP_OPENAI_BASE_URL),
propertyOrBlank(properties, PROP_OPENAI_MODEL),
propertyOrBlank(properties, PROP_OPENAI_TIMEOUT),
GuiProviderApiKeyState.unresolved(propertyOrBlank(properties, PROP_OPENAI_API_KEY))));
GuiConfigurationValues values = new GuiConfigurationValues(
propertyOrBlank(properties, PROP_SOURCE_FOLDER),
propertyOrBlank(properties, PROP_TARGET_FOLDER),
propertyOrBlank(properties, PROP_SQLITE_FILE),
propertyOrBlank(properties, PROP_PROMPT_TEMPLATE_FILE),
propertyOrBlank(properties, PROP_RUNTIME_LOCK_FILE),
propertyOrBlank(properties, PROP_LOG_DIRECTORY),
propertyOrBlank(properties, PROP_LOG_LEVEL),
propertyOrBlank(properties, PROP_MAX_RETRIES_TRANSIENT),
propertyOrBlank(properties, PROP_MAX_PAGES),
propertyOrBlank(properties, PROP_MAX_TEXT_CHARACTERS),
propertyOrBlank(properties, PROP_LOG_AI_SENSITIVE),
propertyOrBlank(properties, PROP_ACTIVE_PROVIDER),
providerConfigurations);
return new GuiConfigurationEditorState(Optional.of(snapshot), values, values, pendingMigrationMessage);
}
/**
* Creates the empty editor state used at application startup when no configuration is loaded.
*
* @return a blank editor state
*/
public static GuiConfigurationEditorState createBlankStartState() {
return GuiConfigurationTemplateFactory.createBlankStartState();
}
private static String propertyOrBlank(Properties properties, String key) {
String value = properties.getProperty(key);
return value == null ? "" : value.trim();
}
}
@@ -44,7 +44,34 @@ public final class GuiConfigurationTemplateFactory {
*/
public static GuiConfigurationEditorState createStandardTemplate() {
GuiConfigurationValues standardValues = createStandardValues();
return new GuiConfigurationEditorState(Optional.empty(), standardValues, standardValues);
return new GuiConfigurationEditorState(Optional.empty(), standardValues, standardValues, Optional.empty());
}
/**
* Creates the empty editor state used when the GUI starts without a loaded configuration.
* <p>
* This start state intentionally does not show the standard template yet. The template
* is reserved for the explicit {@code Neu} action so the GUI starts without an implicit
* draft and only shows the welcome guidance until the user requests a new configuration.
*
* @return a clean editor state without a loaded file snapshot and without template values
*/
public static GuiConfigurationEditorState createBlankStartState() {
GuiConfigurationValues blankValues = new GuiConfigurationValues(
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
Map.of());
return new GuiConfigurationEditorState(Optional.empty(), blankValues, blankValues, Optional.empty());
}
/**