#79: GuiPromptEditorTab erhaelt Konfigurationsaenderungen via notifyConfigurationChanged
Einfuehren von GuiPromptEditorPortFactory als funktionalem Interface, damit GuiConfigurationEditorWorkspace bei jedem Laden oder Speichern einer Konfiguration einen passenden Port fuer den Prompt-Tab erzeugen kann. GuiPromptEditorTab.notifyConfigurationChanged() aktualisiert Port, Pfad und maxTitleLength und setzt Dirty-State sowie Tab-Titel zurueck. BootstrapRunner uebergibt die Factory an GuiStartupContext. Damit werden alle vier Symptome aus #79 behoben: leerer Tab, gesperrte Textarea, fehlgeschlagenes Speichern und fehlender Dirty-State-Indikator. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+63
-5
@@ -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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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() {
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package de.gecheckt.pdf.umbenenner.adapter.in.gui;
|
||||
|
||||
/**
|
||||
* Fabrik, die für einen gegebenen Prompt-Dateipfad einen {@link GuiPromptEditorPort} erzeugt.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
+32
-3
@@ -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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
|
||||
+15
-5
@@ -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() {
|
||||
|
||||
+41
@@ -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<Throwable> fxError = new AtomicReference<>();
|
||||
AtomicBoolean dirtyRef = new AtomicBoolean(true);
|
||||
AtomicReference<String> 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);
|
||||
|
||||
+8
-4
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user