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 588be97..7e0fde0 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 @@ -11,6 +11,8 @@ import org.apache.logging.log4j.Logger; import com.dlsc.pdfviewfx.PDFView; import javafx.application.Platform; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; @@ -18,7 +20,6 @@ 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; @@ -99,17 +100,16 @@ public final class PdfPreviewPane { /** * Interner ScrollPane der PDFView-Skin. Wird nach der Skin-Installation - * per Lookup gesetzt und für den Scroll-Schutz verwendet. + * per Lookup gesetzt und für den Scroll-Schutz und den Seitenanfang-Listener 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. + * Aktiver Einmal-Listener auf die {@code vvalueProperty} des internen ScrollPane. + * Wird nach dem ersten Auslösen immer entfernt. Nie null wenn ein Seitenwechsel + * oder Neuladung noch aussteht, null sobald kein Eingriff mehr erwartet wird. */ - private boolean pendingScrollToTop = false; + private ChangeListener activeScrollToTopListener = null; /** * Erstellt die Komponente im deaktivierten Platzhalter-Zustand. @@ -123,7 +123,7 @@ public final class PdfPreviewPane { pdfView.setId("pdf-preview-view"); // Nach der Skin-Installation den internen ScrollPane suchen und - // die Scroll-Filter sowie den Seitenanfang-Listener installieren. + // die Scroll-Handler installieren. pdfView.skinProperty().addListener((obs, oldSkin, newSkin) -> { if (newSkin != null) { Platform.runLater(this::installScrollHandlers); @@ -193,7 +193,6 @@ public final class PdfPreviewPane { currentSourceFile = sourceFile; currentPage = 0; totalPages = -1; - pendingScrollToTop = true; requestLoad(sourceFile); } @@ -208,6 +207,7 @@ public final class PdfPreviewPane { totalPages = -1; // Neue Sequenznummer: laufende Requests werden verworfen currentRequestSequence.incrementAndGet(); + cancelScrollToTopListener(); pdfView.unload(); showPlaceholder(); updateNavigationButtons(); @@ -267,11 +267,13 @@ public final class PdfPreviewPane { return; } int targetPage = currentPage - 1; - pendingScrollToTop = true; pdfView.setPage(targetPage - 1); currentPage = targetPage; updatePageLabel(); updateNavigationButtons(); + // Nach dem Seitenwechsel stellt PDFViewFX intern die Scroll-Position ein. + // Der Einmal-Listener korrigiert sie bei Bedarf auf den Seitenanfang. + scheduleScrollToTop(); } private void navigateToNextPage() { @@ -279,24 +281,24 @@ public final class PdfPreviewPane { return; } int targetPage = currentPage + 1; - pendingScrollToTop = true; pdfView.setPage(targetPage - 1); currentPage = targetPage; updatePageLabel(); updateNavigationButtons(); + // Nach dem Seitenwechsel stellt PDFViewFX intern die Scroll-Position ein. + // Der Einmal-Listener korrigiert sie bei Bedarf auf den Seitenanfang. + scheduleScrollToTop(); } - // --- Interne Scroll-Hilfsmethoden ---------------------------------------- + // --- Scroll-Hilfsmethoden ------------------------------------------------ /** * Sucht nach der Skin-Installation den internen ScrollPane der PDFView-Skin - * 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. - *
+ * und registriert dort den Mausrad-Seitenwechsel-Filter. + * + *

Der Filter verhindert, dass das Mausrad an den Scroll-Grenzen automatisch + * die Seite wechselt; das Inhalts-Scrolling innerhalb einer Seite bleibt + * unberührt. */ private void installScrollHandlers() { Node found = pdfView.lookup(".scroll-pane"); @@ -307,19 +309,6 @@ public final class PdfPreviewPane { pdfViewScrollPane = sp; 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; @@ -350,59 +339,52 @@ public final class PdfPreviewPane { } /** - * 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. + * Registriert einen einmaligen {@link ChangeListener} auf die {@code vvalueProperty} + * des internen ScrollPane. PDFViewFX setzt nach dem Rendern intern die Scroll-Position; + * dieser Listener greift genau dann ein, wenn der Wert von 0 abweicht, und setzt ihn + * sofort auf den Seitenanfang zurück. Nach dem ersten Auslösen entfernt sich der + * Listener selbst, um normales Inhalts-Scrolling nicht zu behindern. * - * @param sp der interne ScrollPane der PDFView-Skin + *

Ein zuvor registrierter, noch nicht ausgelöster Listener wird vor der + * Neuregistrierung entfernt (Rapid-Page-Change-Schutz). */ - 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"); + private void scheduleScrollToTop() { + if (pdfViewScrollPane == null) { return; } + // Vorherigen Listener entfernen, falls ein schneller Seitenwechsel ihn + // noch nicht ausgelöst hat + cancelScrollToTopListener(); - 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(() -> { + final ScrollPane sp = pdfViewScrollPane; + activeScrollToTopListener = new ChangeListener<>() { + @Override + public void changed(ObservableValue obs, + Number old, Number newVal) { + // Einmal-Listener: immer sofort entfernen + sp.vvalueProperty().removeListener(this); + if (activeScrollToTopListener == this) { + activeScrollToTopListener = null; + } + // Nur eingreifen, wenn PDFViewFX die Position nicht bereits auf 0 gesetzt hat + if (newVal.doubleValue() > 0.0) { sp.setVvalue(0.0); sp.setHvalue(0.0); - }); + } } - }); - LOG.debug("PDF-Vorschau: ImageView-Listener für Seitenanfang installiert"); + }; + sp.vvalueProperty().addListener(activeScrollToTopListener); + } + + /** + * Entfernt einen eventuell noch aktiven Einmal-Listener auf die {@code vvalueProperty}. + * Wird beim Verwerfen einer Anforderung (clear, Rapid-Page-Change) aufgerufen. + */ + private void cancelScrollToTopListener() { + if (activeScrollToTopListener != null && pdfViewScrollPane != null) { + pdfViewScrollPane.vvalueProperty().removeListener(activeScrollToTopListener); + activeScrollToTopListener = null; + } } // --- Asynchrones Laden --------------------------------------------------- @@ -457,11 +439,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); das Scrollen - // zum Seitenanfang übernimmt der ImageView-Listener sobald das Bild vorliegt showContent(); updateNavigationButtons(); updatePageLabel(); + // PDFViewFX setzt nach dem Rendering intern die Scroll-Position. + // Der Einmal-Listener korrigiert sie bei Bedarf auf den Seitenanfang. + scheduleScrollToTop(); LOG.debug("PDF-Vorschau: Rendering angestoßen – {} Seite(n)", totalPages); } catch (Exception e) { String msg = classifyLoadException(e);