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 cd6331b..9332248 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 @@ -140,7 +140,8 @@ public final class GuiConfigurationEditorWorkspace { private final Label welcomeTextLabel = new Label(WELCOME_TEXT); /** Package-private to allow node lookups in smoke tests. */ final TabPane tabPane = new TabPane(); - private final VBox sectionsBox = new VBox(12); + // Kompakter Abstand (6px) zwischen Konfigurationssektionen + private final VBox sectionsBox = new VBox(6); private final Button newButton = new Button("Neu"); private final Button openButton = new Button("Öffnen"); private final Button saveButton = new Button("Speichern"); @@ -1226,7 +1227,8 @@ public final class GuiConfigurationEditorWorkspace { configurationLockBanner.setVisible(false); configurationLockBanner.setManaged(false); - VBox tabContent = new VBox(8, configurationLockBanner, sectionsBox); + // Kompakter Abstand (4px) zwischen Lock-Banner und Sektionen + VBox tabContent = new VBox(4, configurationLockBanner, sectionsBox); VBox.setVgrow(sectionsBox, Priority.ALWAYS); ScrollPane scrollPane = new ScrollPane(tabContent); @@ -1636,12 +1638,16 @@ public final class GuiConfigurationEditorWorkspace { * @param family the provider family this block represents * @return the provider block as a styled card node */ + /** + * Erstellt einen kompakten Provider-Konfigurationsblock mit 2-Spalten-Layout. + * Basis-URL + Modell in einer Zeile, Timeout + API-Key in einer Zeile. + */ private VBox createProviderBlock(String displayName, AiProviderFamily family) { String ns = "ai.provider." + family.getIdentifier() + "."; - VBox block = new VBox(8); + VBox block = new VBox(2); block.setStyle( - "-fx-padding: 10px; -fx-border-color: #c8c8c8; -fx-border-radius: 6px;" + "-fx-padding: 6px; -fx-border-color: #c8c8c8; -fx-border-radius: 6px;" + " -fx-background-radius: 6px; -fx-background-color: #f9f9f9;"); Label title = new Label(displayName); @@ -1652,17 +1658,64 @@ public final class GuiConfigurationEditorWorkspace { editorState.values().providerConfiguration(family)) .orElse(GuiProviderConfigurationState.blank()); - VBox fieldRows = new VBox(0); + // 2-Spalten-Layout mit kompaktem Abstand + GridPane fieldGrid = new GridPane(); + fieldGrid.setHgap(12); + fieldGrid.setVgap(4); - // Base-URL field + // Konfiguriere Spalten: 2 Spalten nebeneinander + javafx.scene.layout.ColumnConstraints col1 = new javafx.scene.layout.ColumnConstraints(); + col1.setMinWidth(160); + col1.setPrefWidth(180); + col1.setHgrow(Priority.ALWAYS); + javafx.scene.layout.ColumnConstraints col2 = new javafx.scene.layout.ColumnConstraints(); + col2.setMinWidth(160); + col2.setPrefWidth(180); + col2.setHgrow(Priority.ALWAYS); + fieldGrid.getColumnConstraints().addAll(col1, col2); + + int gridRow = 0; + + // --- Zeile 1: Basis-URL + Modell --- + // Basis-URL (linke Spalte) TextField baseUrlField = boundTextField(pState.baseUrl(), val -> updateProviderField(family, pState2 -> new GuiProviderConfigurationState( val, pState2.model(), pState2.timeoutSeconds(), pState2.apiKey()))); Label baseUrlError = createFieldErrorLabel(); fieldErrorLabels.put(ns + "baseUrl", baseUrlError); - fieldRows.getChildren().add(buildSimpleFieldRow("Basis-URL:", baseUrlField, baseUrlError)); + Label baseUrlLabel = new Label("Basis-URL:"); + HBox baseUrlBox = new HBox(4, baseUrlField); + HBox.setHgrow(baseUrlField, Priority.ALWAYS); + fieldGrid.add(baseUrlLabel, 0, gridRow); + fieldGrid.add(baseUrlBox, 1, gridRow); + gridRow++; + if (baseUrlError != null) { + fieldGrid.add(new Label(), 0, gridRow); + fieldGrid.add(baseUrlError, 1, gridRow); + gridRow++; + } - // Model field — switches between ComboBox and manual text field. + // --- Zeile 2: Timeout (nur linke Spalte, Modell auf nächster Zeile) --- + // Timeout (linke Spalte) + TextField timeoutField = boundTextField(pState.timeoutSeconds(), + val -> updateProviderField(family, pState2 -> new GuiProviderConfigurationState( + pState2.baseUrl(), pState2.model(), val, pState2.apiKey()))); + Label timeoutError = createFieldErrorLabel(); + fieldErrorLabels.put(ns + "timeoutSeconds", timeoutError); + Label timeoutLabel = new Label("Timeout (Sek.):"); + HBox timeoutBox = new HBox(4, timeoutField); + HBox.setHgrow(timeoutField, Priority.ALWAYS); + fieldGrid.add(timeoutLabel, 0, gridRow); + fieldGrid.add(timeoutBox, 1, gridRow); + gridRow++; + if (timeoutError != null) { + fieldGrid.add(new Label(), 0, gridRow); + fieldGrid.add(timeoutError, 1, gridRow); + gridRow++; + } + + // --- Zeile 3: Modell (linke Spalte) + API-Key (rechte Spalte) --- + // Modell (linke Spalte) GuiModelFieldContainer modelContainer = new GuiModelFieldContainer( pState.model(), val -> updateProviderField(family, pState2 -> new GuiProviderConfigurationState( @@ -1671,20 +1724,18 @@ public final class GuiConfigurationEditorWorkspace { modelCatalogCoordinator.registerFieldContainer(family, modelContainer); Label modelError = createFieldErrorLabel(); fieldErrorLabels.put(ns + "model", modelError); - GridPane modelGrid = createFieldGrid(); - modelGrid.add(new Label("Modell:"), 0, 0); - modelGrid.add(modelContainer.asNode(), 1, 0); - fieldRows.getChildren().addAll(modelGrid, modelError); + Label modelLabel = new Label("Modell:"); + fieldGrid.add(modelLabel, 0, gridRow); + fieldGrid.add(modelContainer.asNode(), 1, gridRow); + gridRow++; + if (modelError != null) { + fieldGrid.add(new Label(), 0, gridRow); + fieldGrid.add(modelError, 1, gridRow); + gridRow++; + } - // Timeout field - TextField timeoutField = boundTextField(pState.timeoutSeconds(), - val -> updateProviderField(family, pState2 -> new GuiProviderConfigurationState( - pState2.baseUrl(), pState2.model(), val, pState2.apiKey()))); - Label timeoutError = createFieldErrorLabel(); - fieldErrorLabels.put(ns + "timeoutSeconds", timeoutError); - fieldRows.getChildren().add(buildSimpleFieldRow("Timeout (Sek.):", timeoutField, timeoutError)); - - // API-Key field with origin label below + // --- Zeile 4: API-Key (linke Spalte) --- + // API-Key (linke Spalte) TextField apiKeyField = boundTextField(pState.apiKey().propertyValue(), val -> updateProviderField(family, pState2 -> new GuiProviderConfigurationState( pState2.baseUrl(), pState2.model(), pState2.timeoutSeconds(), @@ -1693,11 +1744,21 @@ public final class GuiConfigurationEditorWorkspace { fieldErrorLabels.put(ns + "apiKey", apiKeyError); Label apiKeyOriginLabel = createApiKeyOriginLabel(); apiKeyOriginLabels.put(family, apiKeyOriginLabel); - VBox apiKeySlot = buildSimpleFieldRow("API-Key:", apiKeyField, apiKeyError); - apiKeySlot.getChildren().add(apiKeyOriginLabel); - fieldRows.getChildren().add(apiKeySlot); + Label apiKeyLabel = new Label("API-Key:"); + HBox apiKeyBox = new HBox(4, apiKeyField); + HBox.setHgrow(apiKeyField, Priority.ALWAYS); + fieldGrid.add(apiKeyLabel, 0, gridRow); + fieldGrid.add(apiKeyBox, 1, gridRow); + gridRow++; + if (apiKeyError != null) { + fieldGrid.add(new Label(), 0, gridRow); + fieldGrid.add(apiKeyError, 1, gridRow); + gridRow++; + } + fieldGrid.add(new Label(), 0, gridRow); + fieldGrid.add(apiKeyOriginLabel, 1, gridRow); - block.getChildren().add(fieldRows); + block.getChildren().add(fieldGrid); return block; } @@ -1706,51 +1767,76 @@ public final class GuiConfigurationEditorWorkspace { // ========================================================================= /** - * Builds the "Verarbeitungslimits" section with text fields for the numeric limit - * parameters and a checkbox for the sensitive-logging flag. + * Erstellt den "Verarbeitungslimits"-Bereich mit kompaktem 2-Spalten-Layout. + * Felder werden paarweise nebeneinander angeordnet für bessere Platznutzung. * - * @return the card node for the "Verarbeitungslimits" section + * @return das Card-Element für den "Verarbeitungslimits"-Bereich */ private Node createProcessingLimitsSection() { VBox card = createCardContainer(); card.getChildren().add(sectionTitle("Verarbeitungslimits")); - GridPane grid = createFieldGrid(); + // 2-Spalten-GridPane für kompaktes Layout + GridPane grid = new GridPane(); + grid.setHgap(12); + grid.setVgap(4); + + // Konfiguriere 2 Spalten nebeneinander + javafx.scene.layout.ColumnConstraints col1 = new javafx.scene.layout.ColumnConstraints(); + col1.setMinWidth(160); + col1.setPrefWidth(180); + col1.setHgrow(Priority.ALWAYS); + javafx.scene.layout.ColumnConstraints col2 = new javafx.scene.layout.ColumnConstraints(); + col2.setMinWidth(160); + col2.setPrefWidth(180); + col2.setHgrow(Priority.ALWAYS); + grid.getColumnConstraints().addAll(col1, col2); + int row = 0; + // Zeile 1: Maximale Seitenzahl (links) + Maximale Zeichenzahl (rechts) TextField maxPagesField = boundTextField( editorState.values().maxPages(), val -> updateValues(editorState.values().withMaxPages(val))); - addSimpleRow(grid, row++, "Maximale Seitenzahl:", maxPagesField); + grid.add(new Label("Max. Seiten:"), 0, row); + grid.add(maxPagesField, 1, row); TextField maxCharsField = boundTextField( editorState.values().maxTextCharacters(), val -> updateValues(editorState.values().withMaxTextCharacters(val))); - addSimpleRow(grid, row++, "Maximale Zeichenzahl:", maxCharsField); + grid.add(new Label("Max. Zeichen:"), 2, row); + grid.add(maxCharsField, 3, row); + row++; + // Zeile 2: Max. Titellänge (links) + Max. transiente Retries (rechts) TextField maxTitleLengthField = boundTextField( editorState.values().maxTitleLength(), val -> updateValues(editorState.values().withMaxTitleLength(val))); - addSimpleRow(grid, row++, "Max. Titellänge (Zeichen):", maxTitleLengthField); + grid.add(new Label("Max. Titellänge:"), 0, row); + grid.add(maxTitleLengthField, 1, row); TextField maxRetriesField = boundTextField( editorState.values().maxRetriesTransient(), val -> updateValues(editorState.values().withMaxRetriesTransient(val))); - addSimpleRow(grid, row++, "Max. transiente Retries:", maxRetriesField); + grid.add(new Label("Max. Retries:"), 2, row); + grid.add(maxRetriesField, 3, row); + row++; + // Zeile 3: Log-Level (links) + Sensible KI-Ausgabe (rechts) TextField logLevelField = boundTextField( editorState.values().logLevel(), val -> updateValues(editorState.values().withLogLevel(val))); - addSimpleRow(grid, row++, "Log-Level:", logLevelField); + grid.add(new Label("Log-Level:"), 0, row); + grid.add(logLevelField, 1, row); - // log.ai.sensitive as a CheckBox. + // log.ai.sensitive als Checkbox boolean sensitive = Boolean.parseBoolean(editorState.values().logAiSensitive()); - CheckBox sensitiveCheck = new CheckBox("Sensible KI-Ausgabe loggen (log.ai.sensitive)"); + CheckBox sensitiveCheck = new CheckBox("Sensible KI-Ausgabe"); sensitiveCheck.setSelected(sensitive); sensitiveCheck.selectedProperty().addListener((obs, oldVal, newVal) -> updateValues(editorState.values().withLogAiSensitive(Boolean.toString(newVal)))); - grid.add(new Label(), 0, row); - grid.add(sensitiveCheck, 1, row); + grid.add(new Label(), 2, row); + grid.add(sensitiveCheck, 3, row); card.getChildren().add(grid); return card; @@ -2676,10 +2762,13 @@ public final class GuiConfigurationEditorWorkspace { grid.add(field, 1, row); } + /** + * Erstellt ein GridPane für Konfigurationsfelder mit kompaktem vertikalen Abstand. + */ private GridPane createFieldGrid() { GridPane grid = new GridPane(); grid.setHgap(12); - grid.setVgap(8); + grid.setVgap(4); javafx.scene.layout.ColumnConstraints labelCol = new javafx.scene.layout.ColumnConstraints(); labelCol.setMinWidth(180); labelCol.setPrefWidth(200); @@ -2690,10 +2779,14 @@ public final class GuiConfigurationEditorWorkspace { return grid; } + /** + * Erstellt einen Card-Container mit kompaktem Layout für Konfigurationsbereiche. + * Spacing und Padding sind reduziert für ein platzsparendes Design. + */ private VBox createCardContainer() { - VBox card = new VBox(8); + VBox card = new VBox(4); card.setStyle( - "-fx-padding: 12px; -fx-border-color: #d8d8d8; -fx-border-radius: 8px; -fx-background-radius: 8px; -fx-background-color: white;"); + "-fx-padding: 8px; -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;