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 <noreply@anthropic.com>
This commit is contained in:
2026-04-24 13:50:43 +02:00
parent 7e31057bfa
commit ca16855e81
@@ -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.
*
* <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.
*/
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);