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:
+ *
+ * - Einen EventFilter gegen Mausrad-Seitenwechsel (verhindert, dass das
+ * Mausrad an den Scroll-Grenzen die Seite wechselt).
+ * - Einen ImageView-Listener, der nach abgeschlossenem Rendering zum
+ * Seitenanfang scrollt, sofern {@code pendingScrollToTop} gesetzt ist.
+ *
*/
- 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);