diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiConfigurationEditorWorkspace.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiConfigurationEditorWorkspace.java index e360e11..fe8e026 100644 --- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiConfigurationEditorWorkspace.java +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiConfigurationEditorWorkspace.java @@ -415,11 +415,12 @@ public final class GuiConfigurationEditorWorkspace { private final GuiHistoricalDocumentContextPort historicalDocumentContextPort; /** - * Bridge-Port zum Prompt-Editor-Use-Case. Wird vom {@link GuiPromptEditorTab} genutzt, - * um den Prompt-Inhalt zu laden, zu speichern und eine Standard-Prompt-Datei anzulegen. + * Fabrik, die für einen gegebenen Prompt-Dateipfad einen {@link GuiPromptEditorPort} + * erzeugt. Wird verwendet, wenn eine neue Konfiguration geladen oder gespeichert wird, + * um den {@link GuiPromptEditorTab} mit einem aktualisierten Port zu versorgen. * Supplied by Bootstrap via the startup context. */ - private final GuiPromptEditorPort promptEditorPort; + private final GuiPromptEditorPortFactory promptEditorPortFactory; /** * Second main tab of the window that drives the live processing-run view. Created @@ -510,7 +511,7 @@ public final class GuiConfigurationEditorWorkspace { this.manualFileRenamePort = effectiveContext.manualFileRenamePort(); this.manualFileCopyPort = effectiveContext.manualFileCopyPort(); this.historicalDocumentContextPort = effectiveContext.historicalDocumentContextPort(); - this.promptEditorPort = effectiveContext.promptEditorPort(); + this.promptEditorPortFactory = effectiveContext.promptEditorPortFactory(); this.batchRunTab = new GuiBatchRunTab( () -> this.batchRunLauncher, () -> this.miniRunLauncher, @@ -541,7 +542,7 @@ public final class GuiConfigurationEditorWorkspace { maxTitleLength = 60; } this.promptEditorTab = new GuiPromptEditorTab( - this.promptEditorPort, configuredPromptPath, maxTitleLength); + effectiveContext.promptEditorPort(), configuredPromptPath, maxTitleLength); configureRoot(); configureHeader(effectiveContext.startupNotice()); @@ -1145,6 +1146,8 @@ public final class GuiConfigurationEditorWorkspace { refreshHeader(); // Statuszeile nach erfolgreichem Speichern aktualisieren (Konfigurationspfad kann neu sein) statusBarStateListener.accept(this.editorState); + // Prompt-Tab über neuen Prompt-Pfad informieren (kann sich durch Speichern geändert haben) + notifyPromptTabConfigChanged(this.editorState); if (result.hasApiKeyPreservationNote()) { LOG.info("GUI-Editor: API-Key fuer Provider '{}' wurde beibehalten (Feld war leer, " @@ -1242,6 +1245,61 @@ public final class GuiConfigurationEditorWorkspace { runEditorValidation(); // Statuszeile über den neuen Zustand informieren statusBarStateListener.accept(newState); + // Prompt-Tab mit neuem Pfad und Port versorgen + notifyPromptTabConfigChanged(newState); + } + + /** + * Benachrichtigt den Prompt-Editor-Tab über eine geänderte Konfiguration. + *

+ * Liest {@code prompt.template.file} und {@code max.title.length} aus dem neuen + * Zustand, erzeugt über die Factory einen passenden Port und übergibt beides an den + * Tab. Ist der Prompt-Pfad leer, wird ein No-Op-Port verwendet. + *

+ * Muss auf dem JavaFX Application Thread aufgerufen werden. + * + * @param state der neue Editor-Zustand; darf nicht {@code null} sein + */ + private void notifyPromptTabConfigChanged(de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorState state) { + String promptPath = state.values().promptTemplateFile(); + int maxTitle; + try { + maxTitle = Integer.parseInt(state.values().maxTitleLength().trim()); + } catch (NumberFormatException e) { + maxTitle = 60; + } + GuiPromptEditorPort newPort = (promptPath != null && !promptPath.isBlank()) + ? promptEditorPortFactory.create(promptPath) + : noOpPromptEditorPort(); + promptEditorTab.notifyConfigurationChanged( + newPort, + promptPath != null ? promptPath : "", + maxTitle); + } + + private static GuiPromptEditorPort noOpPromptEditorPort() { + return new GuiPromptEditorPort() { + @Override + public de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingResult loadCurrentPrompt() { + return new de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingFailure( + "NO_PATH", "Kein Prompt-Pfad konfiguriert."); + } + + @Override + public de.gecheckt.pdf.umbenenner.application.port.out.PromptSaveResult save(String content) { + return new de.gecheckt.pdf.umbenenner.application.port.out.PromptSaveResult.WriteFailed( + "Kein Prompt-Pfad konfiguriert.", null); + } + + @Override + public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome + createDefaultPromptIfMissing( + de.gecheckt.pdf.umbenenner.application.validation.technicaltest + .CorrectionSuggestion.CreatePromptFile suggestion) { + return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest + .CorrectionOutcome.NotAttempted(suggestion, "Kein Prompt-Pfad konfiguriert."); + } + }; } private void configureRoot() { diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiPromptEditorPortFactory.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiPromptEditorPortFactory.java new file mode 100644 index 0000000..243a768 --- /dev/null +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiPromptEditorPortFactory.java @@ -0,0 +1,22 @@ +package de.gecheckt.pdf.umbenenner.adapter.in.gui; + +/** + * Fabrik, die für einen gegebenen Prompt-Dateipfad einen {@link GuiPromptEditorPort} erzeugt. + *

+ * Wird vom {@link GuiConfigurationEditorWorkspace} genutzt, um nach einem Konfigurations-Laden + * oder -Speichern einen neuen Port für den {@link GuiPromptEditorTab} zu erstellen, ohne dass + * der GUI-Adapter direkt von Bootstrap-internen Klassen abhängen muss. + *

+ * Alle Implementierungen liegen in {@code pdf-umbenenner-bootstrap}. + */ +@FunctionalInterface +public interface GuiPromptEditorPortFactory { + + /** + * Erzeugt einen {@link GuiPromptEditorPort} für den angegebenen Prompt-Dateipfad. + * + * @param promptFilePath konfigurierter Pfad zur Prompt-Datei; darf nicht {@code null} sein + * @return vollständig verdrahteter Port; nie {@code null} + */ + GuiPromptEditorPort create(String promptFilePath); +} diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiPromptEditorTab.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiPromptEditorTab.java index 94560e7..ac64404 100644 --- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiPromptEditorTab.java +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiPromptEditorTab.java @@ -58,11 +58,11 @@ public class GuiPromptEditorTab { private static final String TAB_TITLE = "Prompt"; private static final String TAB_TITLE_DIRTY = "Prompt *"; - private final GuiPromptEditorPort promptEditorPort; + private GuiPromptEditorPort promptEditorPort; /** Konfigurierter Prompt-Dateipfad – wird für CreatePromptFile-Vorschläge benötigt. */ - private final String configuredPromptPath; + private String configuredPromptPath; /** Konfigurierte maximale Titellänge – für den Default-Prompt-Inhalt. */ - private final int maxTitleLength; + private int maxTitleLength; // Thread-Strategie (injizierbar für Tests ohne JavaFX-Runtime) /** Erzeugt Worker-Threads für blockierende Operationen. */ @@ -125,6 +125,35 @@ public class GuiPromptEditorTab { return dirty; } + /** + * Aktualisiert den Tab auf eine neue Konfiguration. + *

+ * Setzt Port, Prompt-Dateipfad und maximale Titellänge auf die neuen Werte. + * Der bisherige Lade-Baseline wird verworfen und der Dirty-State zurückgesetzt. + * Ist der Tab zum Zeitpunkt des Aufrufs sichtbar, wird ein erneutes Laden sofort + * ausgelöst; andernfalls erfolgt das Laden beim nächsten Öffnen des Tabs. + *

+ * Muss auf dem JavaFX Application Thread aufgerufen werden. + * + * @param newPort neuer Port für Prompt-Operationen; darf nicht {@code null} sein + * @param newPromptPath neuer konfigurierter Prompt-Dateipfad; darf nicht {@code null} sein + * @param newMaxTitleLength neue konfigurierte maximale Titellänge + */ + public void notifyConfigurationChanged(GuiPromptEditorPort newPort, + String newPromptPath, + int newMaxTitleLength) { + this.promptEditorPort = Objects.requireNonNull(newPort, "newPort must not be null"); + this.configuredPromptPath = Objects.requireNonNull(newPromptPath, "newPromptPath must not be null"); + this.maxTitleLength = newMaxTitleLength; + this.loadedContent = null; + this.dirty = false; + this.tab.setText(TAB_TITLE); + this.saveButton.setDisable(true); + if (tab.isSelected()) { + loadPromptAsync(); + } + } + /** * Zeigt einen Bestätigungsdialog, wenn ungespeicherte Änderungen vorhanden sind. * Gibt {@code true} zurück, wenn die Änderungen verworfen werden dürfen. diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiStartupContext.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiStartupContext.java index b51976e..5bd7e7f 100644 --- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiStartupContext.java +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiStartupContext.java @@ -76,7 +76,8 @@ public record GuiStartupContext( GuiHistoryOverviewPort historyOverviewPort, GuiHistoryDetailsPort historyDetailsPort, GuiHistoryResetDocumentStatusPort historyResetDocumentStatusPort, - GuiDeleteDocumentHistoryPort deleteDocumentHistoryPort) { + GuiDeleteDocumentHistoryPort deleteDocumentHistoryPort, + GuiPromptEditorPortFactory promptEditorPortFactory) { /** * Creates a fully wired startup context. @@ -107,6 +108,8 @@ public record GuiStartupContext( * bar; {@code null} defaults to {@code "dev"} * @param promptEditorPort bridge zum Prompt-Editor-Use-Case; darf nicht * {@code null} sein + * @param promptEditorPortFactory Fabrik für Prompt-Editor-Ports bei Konfigurationswechsel; + * darf nicht {@code null} sein */ public GuiStartupContext { initialState = Objects.requireNonNull(initialState, "initialState must not be null"); @@ -150,6 +153,8 @@ public record GuiStartupContext( "historyResetDocumentStatusPort must not be null"); deleteDocumentHistoryPort = Objects.requireNonNull(deleteDocumentHistoryPort, "deleteDocumentHistoryPort must not be null"); + promptEditorPortFactory = Objects.requireNonNull(promptEditorPortFactory, + "promptEditorPortFactory must not be null"); } /** @@ -193,7 +198,7 @@ public record GuiStartupContext( rejectingManualFileCopyPort(), noOpHistoricalDocumentContextPort(), "dev", noOpPromptEditorPort(), noOpHistoryOverviewPort(), noOpHistoryDetailsPort(), - noOpHistoryResetPort(), noOpDeleteHistoryPort()); + noOpHistoryResetPort(), noOpDeleteHistoryPort(), noOpPromptEditorPortFactory()); } /** @@ -231,7 +236,7 @@ public record GuiStartupContext( rejectingManualFileCopyPort(), noOpHistoricalDocumentContextPort(), "dev", noOpPromptEditorPort(), noOpHistoryOverviewPort(), noOpHistoryDetailsPort(), - noOpHistoryResetPort(), noOpDeleteHistoryPort()); + noOpHistoryResetPort(), noOpDeleteHistoryPort(), noOpPromptEditorPortFactory()); } /** @@ -269,7 +274,7 @@ public record GuiStartupContext( rejectingManualFileRenamePort(), rejectingManualFileCopyPort(), noOpHistoricalDocumentContextPort(), "dev", noOpPromptEditorPort(), noOpHistoryOverviewPort(), noOpHistoryDetailsPort(), - noOpHistoryResetPort(), noOpDeleteHistoryPort()); + noOpHistoryResetPort(), noOpDeleteHistoryPort(), noOpPromptEditorPortFactory()); } private static GuiBatchRunLauncher rejectingBatchRunLauncher() { @@ -389,7 +394,12 @@ public record GuiStartupContext( noOpHistoryOverviewPort(), noOpHistoryDetailsPort(), noOpHistoryResetPort(), - noOpDeleteHistoryPort()); + noOpDeleteHistoryPort(), + noOpPromptEditorPortFactory()); + } + + private static GuiPromptEditorPortFactory noOpPromptEditorPortFactory() { + return path -> noOpPromptEditorPort(); } private static GuiPromptEditorPort noOpPromptEditorPort() { diff --git a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiPromptEditorTabSmokeTest.java b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiPromptEditorTabSmokeTest.java index 49d5e6a..d0c9c86 100644 --- a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiPromptEditorTabSmokeTest.java +++ b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiPromptEditorTabSmokeTest.java @@ -252,6 +252,47 @@ class GuiPromptEditorTabSmokeTest { assertFalse(dirtyRef.get(), "Dirty-State muss false sein wenn Datei nicht gefunden wurde"); } + @Test + void notifyConfigurationChanged_shouldResetDirtyStateAndTitle() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + AtomicReference fxError = new AtomicReference<>(); + AtomicBoolean dirtyRef = new AtomicBoolean(true); + AtomicReference titleRef = new AtomicReference<>(); + + Platform.runLater(() -> { + try { + SyncPromptEditorPort port = new SyncPromptEditorPort(); + GuiPromptEditorTab editorTab = buildSyncTab(port); + // Laden und anschliessend Inhalt aendern, um Dirty-State zu erzeugen + editorTab.loadPromptAsync(); + editorTab.resetToDefault(); + // Vorbedingung: Dirty-State muss aktiv sein + assertTrue(editorTab.hasDirtyContent(), + "Vorbedingung: Dirty-State muss nach resetToDefault aktiv sein"); + + // Konfiguration wechseln – Dirty-State und Titel sollen zurueckgesetzt werden + editorTab.notifyConfigurationChanged(new SyncPromptEditorPort(), "/new/prompt.txt", 80); + + dirtyRef.set(editorTab.hasDirtyContent()); + titleRef.set(editorTab.tab().getText()); + } catch (Throwable t) { + fxError.set(t); + } finally { + latch.countDown(); + } + }); + + assertTrue(latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS)); + if (fxError.get() != null) { + throw new AssertionError("FX-Thread hat eine Ausnahme geworfen", fxError.get()); + } + assertFalse(dirtyRef.get(), + "Dirty-State muss nach notifyConfigurationChanged false sein"); + assertFalse(titleRef.get().contains("*"), + "Tab-Titel darf nach notifyConfigurationChanged keinen Asterisk enthalten; Titel war: " + + titleRef.get()); + } + @Test void tabTitle_shouldContainAsterisk_afterEditWithLoadedBaseline() throws Exception { CountDownLatch latch = new CountDownLatch(1); diff --git a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java index 9c3a3ca..15bfa96 100644 --- a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java +++ b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java @@ -849,7 +849,8 @@ public class BootstrapRunner { historyOverviewPort, historyDetailsPort, historyResetPort, - deleteHistoryPort); + deleteHistoryPort, + this::buildGuiPromptEditorPort); } Path configPath = Paths.get(configPathOverride.get()); @@ -879,7 +880,8 @@ public class BootstrapRunner { historyOverviewPort, historyDetailsPort, historyResetPort, - deleteHistoryPort); + deleteHistoryPort, + this::buildGuiPromptEditorPort); } LOG.info("GUI startup: configuration file confirmed at: {}", configPath.toAbsolutePath()); @@ -892,7 +894,8 @@ public class BootstrapRunner { technicalTestOrchestrator, correctionExecutionService, batchRunLauncher, miniRunLauncher, resetPort, manualRenamePort, manualCopyPort, historicalDocumentContextPort, applicationVersion, promptEditorPort, - historyOverviewPort, historyDetailsPort, historyResetPort, deleteHistoryPort); + historyOverviewPort, historyDetailsPort, historyResetPort, deleteHistoryPort, + this::buildGuiPromptEditorPort); } catch (GuiConfigurationLoadException e) { LOG.error("GUI startup: configuration could not be loaded, starting without it: {}", e.getMessage(), e); @@ -918,7 +921,8 @@ public class BootstrapRunner { historyOverviewPort, historyDetailsPort, historyResetPort, - deleteHistoryPort); + deleteHistoryPort, + this::buildGuiPromptEditorPort); } }