From 6c2e2efe22cc60dbb57db4e7e0756b6beb11e892 Mon Sep 17 00:00:00 2001 From: Marcus van Elst Date: Tue, 5 May 2026 12:45:23 +0200 Subject: [PATCH] #86: Mehrfachauswahl im Verlauf-Tab (SelectionMode.MULTIPLE) Strg+Klick, Shift+Klick und Strg+A (alle sichtbaren Eintraege) werden durch JavaFX natuerlich unterstuetzt. Aktionsbuttons (Reset, Loeschen) arbeiten nun auf allen selektierten Eintraegen. Bei Status-Reset wird ein Hinweis angezeigt, wenn SUCCESS-Eintraege in der Auswahl enthalten sind (Partial-Success-Dialog). Co-Authored-By: Claude Sonnet 4.6 --- .../adapter/in/gui/history/GuiHistoryTab.java | 188 +++++++++++------- 1 file changed, 119 insertions(+), 69 deletions(-) diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/history/GuiHistoryTab.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/history/GuiHistoryTab.java index abcf6fc..2128b7b 100644 --- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/history/GuiHistoryTab.java +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/history/GuiHistoryTab.java @@ -31,6 +31,7 @@ import javafx.util.Duration; import javafx.util.StringConverter; import javafx.beans.property.SimpleStringProperty; import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.geometry.Insets; import javafx.geometry.Pos; @@ -209,8 +210,7 @@ public final class GuiHistoryTab { * Muss auf dem JavaFX Application Thread aufgerufen werden. */ public void notifyRunEnded() { - DocumentHistoryRow selected = overviewTable.getSelectionModel().getSelectedItem(); - if (selected != null) { + if (!overviewTable.getSelectionModel().getSelectedItems().isEmpty()) { resetButton.setDisable(false); deleteButton.setDisable(false); } @@ -293,7 +293,7 @@ public final class GuiHistoryTab { private void buildOverviewTable() { overviewTable.setItems(overviewItems); - overviewTable.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); + overviewTable.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); overviewTable.setPlaceholder(new Label(EMPTY_DB_TEXT)); overviewTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN); @@ -479,17 +479,26 @@ public final class GuiHistoryTab { statusFilterBox.setOnAction(e -> loadOverview()); - // Detailbereich bei Zeilenselektion - overviewTable.getSelectionModel().selectedItemProperty().addListener( - (obs, old, selected) -> { - if (selected == null) { + // Detailbereich und Buttons bei Selektionsänderung aktualisieren + overviewTable.getSelectionModel().getSelectedItems().addListener( + (ListChangeListener) change -> { + List sel = + List.copyOf(overviewTable.getSelectionModel().getSelectedItems()); + boolean running = runningCheck.getAsBoolean(); + if (sel.isEmpty()) { clearDetailPane(); resetButton.setDisable(true); deleteButton.setDisable(true); + } else if (sel.size() == 1) { + resetButton.setDisable(running); + deleteButton.setDisable(running); + loadDetails(sel.get(0).fingerprint()); } else { - resetButton.setDisable(runningCheck.getAsBoolean()); - deleteButton.setDisable(runningCheck.getAsBoolean()); - loadDetails(selected.fingerprint()); + // Mehrfachauswahl: Detail-Bereich löschen, Buttons aktivieren + clearDetailPane(); + resetButton.setDisable(running); + deleteButton.setDisable(running); + statusBarLabel.setText(sel.size() + " Einträge ausgewählt."); } }); @@ -598,47 +607,72 @@ public final class GuiHistoryTab { return; } - DocumentHistoryRow selected = overviewTable.getSelectionModel().getSelectedItem(); - if (selected == null) return; + List selectedItems = + List.copyOf(overviewTable.getSelectionModel().getSelectedItems()); + if (selectedItems.isEmpty()) return; - Alert confirm = new Alert(Alert.AlertType.CONFIRMATION); - confirm.setTitle("Status zurücksetzen"); - confirm.setHeaderText("Status zurücksetzen?"); - confirm.setContentText( - "Setzt den Status des Dokuments auf READY_FOR_AI zurück.\n" - + "Fehlerzähler und letzter Fehlerzeitpunkt werden gelöscht.\n" - + "Die Versuchshistorie bleibt vollständig erhalten.\n\n" - + "Das Dokument wird beim nächsten Verarbeitungslauf erneut verarbeitet.\n\n" - + "Quelldatei: " + selected.sourceFileName()); - Optional choice = confirm.showAndWait(); - if (choice.isEmpty() || choice.get() != ButtonType.OK) return; - - DocumentFingerprint fp = selected.fingerprint(); Path configPath = configPathSupplier.get(); if (configPath == null) { showInfo("Keine Konfiguration geladen."); return; } + + long successCount = selectedItems.stream() + .filter(r -> r.overallStatus() == ProcessingStatus.SUCCESS) + .count(); + + StringBuilder sb = new StringBuilder(); + sb.append("Setzt den Status auf READY_FOR_AI zurück.\n"); + sb.append("Fehlerzähler und letzter Fehlerzeitpunkt werden gelöscht.\n"); + sb.append("Die Versuchshistorie bleibt vollständig erhalten.\n\n"); + if (selectedItems.size() == 1) { + sb.append("Quelldatei: ").append(selectedItems.get(0).sourceFileName()); + } else { + sb.append(selectedItems.size()).append(" Einträge werden zurückgesetzt."); + } + if (successCount > 0) { + sb.append("\n\nHinweis: ").append(successCount) + .append(" der ausgewählten Einträge ") + .append(successCount == 1 ? "hat" : "haben") + .append(" Status \"Erfolgreich\". ") + .append(successCount == 1 ? "Dieser Eintrag wird" : "Diese Einträge werden") + .append(" erneut verarbeitet."); + } + + Alert confirm = new Alert(Alert.AlertType.CONFIRMATION); + confirm.setTitle("Status zurücksetzen"); + confirm.setHeaderText("Status zurücksetzen?"); + confirm.setContentText(sb.toString()); + Optional choice = confirm.showAndWait(); + if (choice.isEmpty() || choice.get() != ButtonType.OK) return; + resetButton.setDisable(true); deleteButton.setDisable(true); statusBarLabel.setText("Status wird zurückgesetzt …"); workerPool.submit(() -> { - try { - resetPort.resetStatus(configPath, fp); - LOG.info("Status-Reset durchgeführt für Fingerprint: {}", fp.sha256Hex()); - Platform.runLater(() -> { - statusBarLabel.setText("Status erfolgreich zurückgesetzt."); - loadOverview(); - }); - } catch (Exception ex) { - LOG.error("Status-Reset fehlgeschlagen für {}: {}", fp.sha256Hex(), ex.getMessage(), ex); - Platform.runLater(() -> { - statusBarLabel.setText("Fehler beim Status-Reset: " + ex.getMessage()); - resetButton.setDisable(false); - deleteButton.setDisable(false); - }); + int okCount = 0; + int errCount = 0; + for (DocumentHistoryRow row : selectedItems) { + try { + resetPort.resetStatus(configPath, row.fingerprint()); + LOG.info("Status-Reset durchgeführt für Fingerprint: {}", row.fingerprint().sha256Hex()); + okCount++; + } catch (Exception ex) { + LOG.error("Status-Reset fehlgeschlagen für {}: {}", + row.fingerprint().sha256Hex(), ex.getMessage(), ex); + errCount++; + } } + final int ok = okCount, err = errCount; + Platform.runLater(() -> { + if (err == 0) { + statusBarLabel.setText("Status erfolgreich zurückgesetzt: " + ok + " Eintrag/Einträge."); + } else { + statusBarLabel.setText("Status zurückgesetzt: " + ok + " OK, " + err + " Fehler."); + } + loadOverview(); + }); }); } @@ -648,47 +682,63 @@ public final class GuiHistoryTab { return; } - DocumentHistoryRow selected = overviewTable.getSelectionModel().getSelectedItem(); - if (selected == null) return; + List selectedItems = + List.copyOf(overviewTable.getSelectionModel().getSelectedItems()); + if (selectedItems.isEmpty()) return; - Alert confirm = new Alert(Alert.AlertType.WARNING); - confirm.setTitle("Eintrag löschen"); - confirm.setHeaderText("Eintrag vollständig löschen?"); - confirm.setContentText( - "Der Stammsatz und ALLE Verarbeitungsversuche werden unwiderruflich gelöscht.\n" - + "Diese Aktion kann nicht rückgängig gemacht werden.\n\n" - + "Quelldatei: " + selected.sourceFileName()); - confirm.getButtonTypes().setAll(ButtonType.OK, ButtonType.CANCEL); - Optional choice = confirm.showAndWait(); - if (choice.isEmpty() || choice.get() != ButtonType.OK) return; - - DocumentFingerprint fp = selected.fingerprint(); Path configPath = configPathSupplier.get(); if (configPath == null) { showInfo("Keine Konfiguration geladen."); return; } + + String contentText; + if (selectedItems.size() == 1) { + contentText = "Der Stammsatz und ALLE Verarbeitungsversuche werden unwiderruflich gelöscht.\n" + + "Diese Aktion kann nicht rückgängig gemacht werden.\n\n" + + "Quelldatei: " + selectedItems.get(0).sourceFileName(); + } else { + contentText = selectedItems.size() + " Einträge werden mit allen Versuchen unwiderruflich gelöscht.\n" + + "Diese Aktion kann nicht rückgängig gemacht werden."; + } + + Alert confirm = new Alert(Alert.AlertType.WARNING); + confirm.setTitle("Eintrag löschen"); + confirm.setHeaderText(selectedItems.size() == 1 ? "Eintrag vollständig löschen?" + : selectedItems.size() + " Einträge vollständig löschen?"); + confirm.setContentText(contentText); + confirm.getButtonTypes().setAll(ButtonType.OK, ButtonType.CANCEL); + Optional choice = confirm.showAndWait(); + if (choice.isEmpty() || choice.get() != ButtonType.OK) return; + resetButton.setDisable(true); deleteButton.setDisable(true); - statusBarLabel.setText("Eintrag wird gelöscht …"); + statusBarLabel.setText("Einträge werden gelöscht …"); workerPool.submit(() -> { - try { - deletePort.deleteHistory(configPath, fp); - LOG.info("Dokumenteintrag gelöscht für Fingerprint: {}", fp.sha256Hex()); - Platform.runLater(() -> { - statusBarLabel.setText("Eintrag erfolgreich gelöscht."); - clearDetailPane(); - loadOverview(); - }); - } catch (Exception ex) { - LOG.error("Löschen fehlgeschlagen für {}: {}", fp.sha256Hex(), ex.getMessage(), ex); - Platform.runLater(() -> { - statusBarLabel.setText("Fehler beim Löschen: " + ex.getMessage()); - resetButton.setDisable(false); - deleteButton.setDisable(false); - }); + int okCount = 0; + int errCount = 0; + for (DocumentHistoryRow row : selectedItems) { + try { + deletePort.deleteHistory(configPath, row.fingerprint()); + LOG.info("Dokumenteintrag gelöscht für Fingerprint: {}", row.fingerprint().sha256Hex()); + okCount++; + } catch (Exception ex) { + LOG.error("Löschen fehlgeschlagen für {}: {}", + row.fingerprint().sha256Hex(), ex.getMessage(), ex); + errCount++; + } } + final int ok = okCount, err = errCount; + Platform.runLater(() -> { + if (err == 0) { + statusBarLabel.setText("Gelöscht: " + ok + " Eintrag/Einträge."); + } else { + statusBarLabel.setText("Gelöscht: " + ok + " OK, " + err + " Fehler."); + } + clearDetailPane(); + loadOverview(); + }); }); }