M10 bis AP-003
This commit is contained in:
+17
-8
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
+449
@@ -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();
|
||||
}
|
||||
}
|
||||
+23
@@ -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);
|
||||
}
|
||||
+26
@@ -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);
|
||||
}
|
||||
}
|
||||
+46
@@ -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());
|
||||
}
|
||||
}
|
||||
+31
@@ -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);
|
||||
}
|
||||
}
|
||||
+15
-107
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
+50
-6
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+96
@@ -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();
|
||||
}
|
||||
}
|
||||
+28
-1
@@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user