From ca16855e81f76b416c76b0eadd278d1033c5d687 Mon Sep 17 00:00:00 2001 From: Marcus van Elst Date: Fri, 24 Apr 2026 13:50:43 +0200 Subject: [PATCH] Fix #27 und #29: Gezielter Scroll-Schutz und zuverlaessiger Seitenanfang Bug #27: Den zu aggressiven ScrollEvent::consume-Filter durch einen gezielten Filter auf dem internen ScrollPane der PDFView-Skin ersetzt. Der Filter konsumiert nur dann, wenn die Seite keinen ueberlaufenden Inhalt hat oder der Scroll-Inhalt an der oberen bzw. unteren Grenze angekommen ist. Dadurch bleibt Inhalts-Scrolling innerhalb einer Seite weiterhin moeglich; nur der Seitenwechsel per Mausrad wird verhindert. Bug #29: Platform.runLater() durch eine PauseTransition (100 ms) ersetzt, die nach dem vollstaendigen Rendering-Durchlauf der PDFView-Skin den internen ScrollPane explizit auf vValue=0 zuruecksetzt. So wird der Seitenanfang zuverlaessig angezeigt, ohne dass die Skin die Position nachtraeglich ueberschreibt. Co-Authored-By: Claude Sonnet 4.6 --- .../in/gui/batchrun/PdfPreviewPane.java | 108 ++++++++++++++++-- 1 file changed, 97 insertions(+), 11 deletions(-) diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/PdfPreviewPane.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/PdfPreviewPane.java index fbe4526..6ef7909 100644 --- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/PdfPreviewPane.java +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/PdfPreviewPane.java @@ -10,18 +10,21 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import com.dlsc.pdfviewfx.PDFView; +import javafx.animation.PauseTransition; import javafx.application.Platform; import javafx.geometry.Insets; import javafx.geometry.Pos; -import javafx.scene.input.ScrollEvent; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ProgressIndicator; +import javafx.scene.control.ScrollPane; +import javafx.scene.input.ScrollEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; +import javafx.util.Duration; /** * Detailbereich-Komponente zur asynchronen Anzeige von Seiten einer Quelldatei. @@ -94,6 +97,13 @@ public final class PdfPreviewPane { /** Gibt an ob die Navigation bedienbar ist. */ private boolean enabled = true; + /** + * Interner ScrollPane der PDFView-Skin. Wird nach der Skin-Installation + * per Lookup gesetzt und für den Scroll-Schutz (Bug #27) sowie das + * Zurücksetzen auf den Seitenanfang (Bug #29) verwendet. + */ + private ScrollPane pdfViewScrollPane = null; + /** * Erstellt die Komponente im deaktivierten Platzhalter-Zustand. */ @@ -105,10 +115,15 @@ public final class PdfPreviewPane { pdfView.setShowToolBar(false); pdfView.setId("pdf-preview-view"); - // Bug #27: Mausrad-Scrollevents abfangen, damit PDFView keinen Seitenwechsel auslöst. - // Das Mausrad soll ausschließlich innerhalb der aktuellen Seite scrollen. - // Seitenwechsel sind nur über die Navigations-Buttons erlaubt. - pdfView.addEventFilter(ScrollEvent.SCROLL, ScrollEvent::consume); + // Bug #27: Nach Skin-Installation den internen ScrollPane suchen und + // dort einen gezielten Filter registrieren, der nur an den Scroll-Grenzen + // konsumiert. So wird Mausrad-Seitenwechsel verhindert, ohne das + // Inhalts-Scrolling innerhalb einer Seite zu blockieren. + pdfView.skinProperty().addListener((obs, oldSkin, newSkin) -> { + if (newSkin != null) { + Platform.runLater(this::installInternalScrollFilter); + } + }); overlayLabel.setId("pdf-preview-overlay-label"); overlayLabel.setStyle("-fx-text-fill: #555555;"); @@ -246,11 +261,12 @@ public final class PdfPreviewPane { return; } int targetPage = currentPage - 1; + pdfView.setPage(targetPage - 1); currentPage = targetPage; updatePageLabel(); updateNavigationButtons(); - // Bug #29: Seite nach dem Layout-Pass von oben anzeigen (0-basierter Index) - Platform.runLater(() -> pdfView.setPage(targetPage - 1)); + // Bug #29: Nach dem Rendering-Durchlauf zum Seitenanfang scrollen + scrollToTopAfterRender(); } private void navigateToNextPage() { @@ -258,11 +274,81 @@ public final class PdfPreviewPane { return; } int targetPage = currentPage + 1; + pdfView.setPage(targetPage - 1); currentPage = targetPage; updatePageLabel(); updateNavigationButtons(); - // Bug #29: Seite nach dem Layout-Pass von oben anzeigen (0-basierter Index) - Platform.runLater(() -> pdfView.setPage(targetPage - 1)); + // Bug #29: Nach dem Rendering-Durchlauf zum Seitenanfang scrollen + scrollToTopAfterRender(); + } + + // --- Interne Scroll-Hilfsmethoden ---------------------------------------- + + /** + * Sucht nach der Skin-Installation den internen ScrollPane der PDFView-Skin + * und registriert dort einen EventFilter für Bug #27. + * + *

Der Filter konsumiert ScrollEvents ausschließlich dann, wenn kein + * überlaufender Seiteninhalt vorhanden ist oder der Scroll-Inhalt an der + * Grenze (oben/unten) angekommen ist. Dadurch wird der interne + * Seitenwechsel-Handler der Skin blockiert, ohne normales Inhalts-Scrolling + * zu unterbinden. + */ + private void installInternalScrollFilter() { + javafx.scene.Node found = pdfView.lookup(".scroll-pane"); + if (!(found instanceof ScrollPane sp)) { + LOG.warn("PDF-Vorschau: Interner ScrollPane nicht gefunden – Mausrad-Schutz nicht aktiv"); + return; + } + pdfViewScrollPane = sp; + LOG.debug("PDF-Vorschau: Interner ScrollPane gefunden, Mausrad-Schutz wird installiert"); + + sp.addEventFilter(ScrollEvent.SCROLL, event -> { + if (event.isInertia()) { + return; + } + // Prüfen ob die Seite überhaupt scrollbaren Inhalt hat + javafx.scene.Node content = sp.getContent(); + if (content == null) { + event.consume(); + return; + } + double contentH = content.getBoundsInLocal().getHeight(); + double viewportH = sp.getViewportBounds().getHeight(); + boolean hatUeberlauf = contentH > viewportH + 1.0; + + if (!hatUeberlauf) { + // Seite passt vollständig in den Viewport: kein Inhalts-Scrolling möglich, + // daher Event konsumieren, damit kein Seitenwechsel ausgelöst wird + event.consume(); + return; + } + // Seite hat überlaufenden Inhalt: Event nur an den Scroll-Grenzen konsumieren + boolean scrolltHoch = event.getDeltaY() > 0; + double vVal = sp.getVvalue(); + boolean anGrenze = scrolltHoch ? (vVal <= 0.0) : (vVal >= 1.0); + if (anGrenze) { + event.consume(); + } + }); + } + + /** + * Scrollt den internen ScrollPane nach einer kurzen Pause (100 ms) zum + * Seitenanfang. Die Verzögerung stellt sicher, dass der PDFView-interne + * Rendering-Durchlauf und etwaige nachgelagerte Scroll-Anpassungen der + * Skin abgeschlossen sind, bevor die Position zurückgesetzt wird (Bug #29). + */ + private void scrollToTopAfterRender() { + if (pdfViewScrollPane == null) { + return; + } + PauseTransition pause = new PauseTransition(Duration.millis(100)); + pause.setOnFinished(e -> { + pdfViewScrollPane.setVvalue(0.0); + pdfViewScrollPane.setHvalue(0.0); + }); + pause.play(); } // --- Asynchrones Laden --------------------------------------------------- @@ -321,8 +407,8 @@ public final class PdfPreviewPane { showContent(); updateNavigationButtons(); updatePageLabel(); - // Bug #29: Zum Seitenanfang scrollen, nachdem der Layout-Pass abgeschlossen ist - Platform.runLater(() -> pdfView.setPage(0)); + // Bug #29: Nach abgeschlossenem Rendering-Durchlauf zum Seitenanfang scrollen + scrollToTopAfterRender(); LOG.debug("PDF-Vorschau: Rendering erfolgreich – {} Seite(n)", totalPages); } catch (Exception e) { String msg = classifyLoadException(e);