diff --git a/pdf-umbenenner-adapter-in-gui/pom.xml b/pdf-umbenenner-adapter-in-gui/pom.xml index db406d9..186406a 100644 --- a/pdf-umbenenner-adapter-in-gui/pom.xml +++ b/pdf-umbenenner-adapter-in-gui/pom.xml @@ -51,7 +51,7 @@ com.dlsc.pdfviewfx pdfviewfx - 3.1.1 + 3.3.2 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 6ef7909..588be97 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,21 +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.Node; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ProgressIndicator; import javafx.scene.control.ScrollPane; +import javafx.scene.image.ImageView; 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. @@ -99,11 +99,18 @@ public final class PdfPreviewPane { /** * 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. + * per Lookup gesetzt und für den Scroll-Schutz verwendet. */ private ScrollPane pdfViewScrollPane = null; + /** + * Signalisiert, dass nach dem nächsten abgeschlossenen Rendering-Vorgang + * zum Seitenanfang gescrollt werden soll. Wird gesetzt beim Laden einer + * neuen Datei und bei jedem Seitenwechsel; wird vom ImageView-Listener + * nach dem Scrollen zurückgesetzt. + */ + private boolean pendingScrollToTop = false; + /** * Erstellt die Komponente im deaktivierten Platzhalter-Zustand. */ @@ -115,13 +122,11 @@ public final class PdfPreviewPane { pdfView.setShowToolBar(false); pdfView.setId("pdf-preview-view"); - // 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. + // Nach der Skin-Installation den internen ScrollPane suchen und + // die Scroll-Filter sowie den Seitenanfang-Listener installieren. pdfView.skinProperty().addListener((obs, oldSkin, newSkin) -> { if (newSkin != null) { - Platform.runLater(this::installInternalScrollFilter); + Platform.runLater(this::installScrollHandlers); } }); @@ -188,6 +193,7 @@ public final class PdfPreviewPane { currentSourceFile = sourceFile; currentPage = 0; totalPages = -1; + pendingScrollToTop = true; requestLoad(sourceFile); } @@ -261,12 +267,11 @@ public final class PdfPreviewPane { return; } int targetPage = currentPage - 1; + pendingScrollToTop = true; pdfView.setPage(targetPage - 1); currentPage = targetPage; updatePageLabel(); updateNavigationButtons(); - // Bug #29: Nach dem Rendering-Durchlauf zum Seitenanfang scrollen - scrollToTopAfterRender(); } private void navigateToNextPage() { @@ -274,41 +279,52 @@ public final class PdfPreviewPane { return; } int targetPage = currentPage + 1; + pendingScrollToTop = true; pdfView.setPage(targetPage - 1); currentPage = targetPage; updatePageLabel(); updateNavigationButtons(); - // 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. + * und installiert dort beide Scroll-Handler: + *

    + *
  1. Einen EventFilter gegen Mausrad-Seitenwechsel (verhindert, dass das + * Mausrad an den Scroll-Grenzen die Seite wechselt).
  2. + *
  3. Einen ImageView-Listener, der nach abgeschlossenem Rendering zum + * Seitenanfang scrollt, sofern {@code pendingScrollToTop} gesetzt ist.
  4. + *
*/ - private void installInternalScrollFilter() { - javafx.scene.Node found = pdfView.lookup(".scroll-pane"); + private void installScrollHandlers() { + Node found = pdfView.lookup(".scroll-pane"); if (!(found instanceof ScrollPane sp)) { - LOG.warn("PDF-Vorschau: Interner ScrollPane nicht gefunden – Mausrad-Schutz nicht aktiv"); + LOG.warn("PDF-Vorschau: Interner ScrollPane nicht gefunden – Scroll-Handler nicht aktiv"); return; } pdfViewScrollPane = sp; - LOG.debug("PDF-Vorschau: Interner ScrollPane gefunden, Mausrad-Schutz wird installiert"); + LOG.debug("PDF-Vorschau: Interner ScrollPane gefunden, Scroll-Handler werden installiert"); + installMouseWheelPageChangeFilter(sp); + installScrollToTopOnRenderComplete(sp); + } + + /** + * Registriert einen EventFilter am internen ScrollPane, der Mausrad-Ereignisse + * an den Scroll-Grenzen konsumiert. Dadurch wird verhindert, dass die PDFView-Skin + * beim Erreichen des Seitenanfangs oder -endes automatisch die Seite wechselt, + * ohne dass der Benutzer die Navigations-Buttons betätigt. + * + * @param sp der interne ScrollPane der PDFView-Skin + */ + private void installMouseWheelPageChangeFilter(ScrollPane sp) { sp.addEventFilter(ScrollEvent.SCROLL, event -> { if (event.isInertia()) { return; } - // Prüfen ob die Seite überhaupt scrollbaren Inhalt hat - javafx.scene.Node content = sp.getContent(); + Node content = sp.getContent(); if (content == null) { event.consume(); return; @@ -334,21 +350,59 @@ public final class PdfPreviewPane { } /** - * 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). + * Sucht den {@link ImageView} innerhalb des internen ScrollPane und registriert + * einen Listener auf dessen {@code imageProperty}. Sobald ein neues Seitenbild + * gerendert wurde und {@code pendingScrollToTop} gesetzt ist, scrollt die + * Komponente zum Seitenanfang. Der Listener stellt sicher, dass der Skin + * seinen eigenen Scroll-Zustand erst abgeschlossen hat, bevor die Position + * zurückgesetzt wird. + * + * @param sp der interne ScrollPane der PDFView-Skin */ - private void scrollToTopAfterRender() { - if (pdfViewScrollPane == null) { + private void installScrollToTopOnRenderComplete(ScrollPane sp) { + // CSS anwenden, damit Style-Klassen für den Lookup verfügbar sind + pdfView.applyCss(); + // Suche den ImageView über die bekannte CSS-Klasse des Bild-Wrappers + Node wrapperNode = sp.lookup(".image-view-wrapper"); + ImageView targetImageView = null; + if (wrapperNode instanceof javafx.scene.layout.Pane wrapper) { + for (Node child : wrapper.getChildren()) { + if (child instanceof ImageView iv) { + targetImageView = iv; + break; + } + } + } + if (targetImageView == null) { + // Fallback: alle ImageViews im ScrollPane durchsuchen (da Thumbnails + // deaktiviert sind, gibt es im Hauptbereich genau einen) + for (Node n : sp.lookupAll(".image-view")) { + if (n instanceof ImageView iv) { + targetImageView = iv; + break; + } + } + } + if (targetImageView == null) { + LOG.warn("PDF-Vorschau: ImageView nicht gefunden – Scrollen zum Seitenanfang nicht aktiv"); return; } - PauseTransition pause = new PauseTransition(Duration.millis(100)); - pause.setOnFinished(e -> { - pdfViewScrollPane.setVvalue(0.0); - pdfViewScrollPane.setHvalue(0.0); + + final ImageView imageView = targetImageView; + imageView.imageProperty().addListener((obs, oldImg, newImg) -> { + // Nur scrollen, wenn ein neues Bild geliefert wurde und ein Seitenwechsel + // bzw. Neuladung angefordert war + if (newImg != null && pendingScrollToTop) { + pendingScrollToTop = false; + // Ein Platform.runLater() stellt sicher, dass der Skin seinen + // internen Scroll-Zustand zuerst abgeschlossen hat + Platform.runLater(() -> { + sp.setVvalue(0.0); + sp.setHvalue(0.0); + }); + } }); - pause.play(); + LOG.debug("PDF-Vorschau: ImageView-Listener für Seitenanfang installiert"); } // --- Asynchrones Laden --------------------------------------------------- @@ -403,13 +457,12 @@ public final class PdfPreviewPane { int pages = (doc != null) ? doc.getNumberOfPages() : 1; totalPages = Math.max(1, pages); currentPage = 1; - // PDFView zeigt nach load() bereits Seite 0 (= Seite 1) + // PDFView zeigt nach load() bereits Seite 0 (= Seite 1); das Scrollen + // zum Seitenanfang übernimmt der ImageView-Listener sobald das Bild vorliegt showContent(); updateNavigationButtons(); updatePageLabel(); - // Bug #29: Nach abgeschlossenem Rendering-Durchlauf zum Seitenanfang scrollen - scrollToTopAfterRender(); - LOG.debug("PDF-Vorschau: Rendering erfolgreich – {} Seite(n)", totalPages); + LOG.debug("PDF-Vorschau: Rendering angestoßen – {} Seite(n)", totalPages); } catch (Exception e) { String msg = classifyLoadException(e); LOG.warn("PDF-Vorschau: Rendering fehlgeschlagen – {}", msg, e);