Fix #27 und #29: pdfviewfx 3.3.2 und zuverlaessiger Seitenanfang via ImageView-Listener

pdfviewfx wird von 3.1.1 auf 3.3.2 aktualisiert. Version 3.3.1 behebt
'Do not interrupt rendering', wodurch ClosedByInterruptException bei
schnellem Seitenwechsel (#27 Folge-Bug) und das Ausbleiben weiterer
Renderings ab Seite 3+ (#29 Folge-Bug) nicht mehr auftreten.

Das 100-ms-PauseTransition-Workaround fuer den Seitenanfang wird ersetzt
durch einen Listener auf die imageProperty des internen ImageView der
PDFView-Skin. Der Listener scrollt erst dann zum Seitenanfang, wenn
das Rendering tatsaechlich abgeschlossen ist und pendingScrollToTop
gesetzt wurde (bei loadSource und Seitenwechsel-Buttons). Dadurch wird
der Seitenanfang zuverlaessig angezeigt, unabhaengig von der Renderzeit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-24 14:30:40 +02:00
parent ca16855e81
commit 0387be0e96
2 changed files with 95 additions and 42 deletions
+1 -1
View File
@@ -51,7 +51,7 @@
<dependency>
<groupId>com.dlsc.pdfviewfx</groupId>
<artifactId>pdfviewfx</artifactId>
<version>3.1.1</version>
<version>3.3.2</version>
</dependency>
<!-- JBIG2-Codec für PDF-Bilddecodierung -->
<dependency>
@@ -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.
*
* <p>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:
* <ol>
* <li>Einen EventFilter gegen Mausrad-Seitenwechsel (verhindert, dass das
* Mausrad an den Scroll-Grenzen die Seite wechselt).</li>
* <li>Einen ImageView-Listener, der nach abgeschlossenem Rendering zum
* Seitenanfang scrollt, sofern {@code pendingScrollToTop} gesetzt ist.</li>
* </ol>
*/
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);