Fix #29: Seitenanfang zuverlaessig via vvalue-Einmal-Listener erzwingen
Der bisherige ImageView-imageProperty-Listener mit Platform.runLater() wurde von PDFViewFX nach dem Rendering noch einmal ueberschrieben, weil die interne Scroll-Korrektur ebenfalls asynchron laeuft und spaeter ausgefuehrt wird. Neuer Ansatz: Nach jedem pdfView.load() und pdfView.setPage()-Aufruf wird ein einmaliger ChangeListener auf die vvalueProperty des internen ScrollPane registriert (scheduleScrollToTop). Sobald PDFViewFX seine interne Scroll-Position durchschreibt und der Wert von 0 abweicht, korrigiert der Listener ihn sofort auf 0 und entfernt sich danach selbst. Damit greift der Eingriff immer nach dem internen PDFViewFX-Scroll, unabhaengig von der Renderzeit. Zusaetzlich wird ein aktiver Listener bei schnellen Seitenwechseln (cancelScrollToTopListener) und bei clear() sauber aufgeraeumt. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+62
-79
@@ -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<Number> 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:
|
||||
* <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>
|
||||
* und registriert dort den Mausrad-Seitenwechsel-Filter.
|
||||
*
|
||||
* <p>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
|
||||
* <p>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<? extends Number> 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);
|
||||
|
||||
Reference in New Issue
Block a user