Zentriere ersten Zoom-Schritt mittels AnimationTimer-Single-Shot

ChangeListener auf hvalueProperty feuert nicht zuverlässig: wenn hvalue
im Fit-Modus bereits 0.5 (oder identisch zum Reset-Wert) ist, gibt es
keine Wertänderung beim setFitToWidth(false), und der Listener läuft
nie an – der spätere JavaFX-eigene Reset auf 0.0 bleibt unkontrolliert.

AnimationTimer.handle() läuft einmal pro JavaFX-Frame, nach allen
Layout-, CSS- und Pulse-Passes des aktuellen Frames. Das ist der einzige
in JavaFX zuverlässige Mechanismus, um nach allem zu feuern, was JavaFX
in diesem Frame noch erledigt. stop() im ersten handle() macht den Timer
zum Single-Shot.

Folge-Zoom-Schritte (wasInFitMode == false) bleiben unverändert mit
einfachem Platform.runLater.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-05 16:03:29 +02:00
parent 0651fcb6eb
commit 661894f1ec
@@ -17,8 +17,8 @@ import org.apache.pdfbox.pdmodel.encryption.InvalidPasswordException;
import org.apache.pdfbox.rendering.ImageType; import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.rendering.PDFRenderer; import org.apache.pdfbox.rendering.PDFRenderer;
import javafx.animation.AnimationTimer;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.embed.swing.SwingFXUtils; import javafx.embed.swing.SwingFXUtils;
import javafx.geometry.Bounds; import javafx.geometry.Bounds;
import javafx.geometry.Insets; import javafx.geometry.Insets;
@@ -729,24 +729,20 @@ public final class PdfPreviewPane {
if (wasInFitMode) { if (wasInFitMode) {
// Erster Zoom-Schritt nach Verlassen des Fit-Modus: setFitToWidth(false) // Erster Zoom-Schritt nach Verlassen des Fit-Modus: setFitToWidth(false)
// löst einen system-bedingten Reset von hvalue auf 0.0 aus. Statt // löst Layout-, CSS- und Property-Reset-Passes innerhalb desselben
// diesen Reset per Timing zu umgehen (was nicht zuverlässig ist), // JavaFX-Frames aus. Ein AnimationTimer feuert handle() nach allen
// wird er aktiv abgefangen: Ein einmaliger ChangeListener auf // diesen Passes des aktuellen Frames; das ist der einzige Punkt,
// hvalueProperty feuert beim Reset, entfernt sich sofort selbst // an dem setHvalue(0.5)/setVvalue(0.5) garantiert nicht mehr von
// (Single-Shot) und postet anschließend die Zentrierung via // einem nachträglichen Reset überschrieben werden. stop() im ersten
// Platform.runLater. So wirkt setHvalue(0.5)/setVvalue(0.5) // handle()-Aufruf macht den Timer zum Single-Shot.
// garantiert nach dem Reset ohne mehrfaches runLater oder new AnimationTimer() {
// layout()-Hacks. @Override
@SuppressWarnings("unchecked") public void handle(long now) {
final ChangeListener<Number>[] holder = new ChangeListener[1]; stop();
holder[0] = (obs, oldVal, newVal) -> {
scrollPane.hvalueProperty().removeListener(holder[0]);
Platform.runLater(() -> {
scrollPane.setHvalue(0.5); scrollPane.setHvalue(0.5);
scrollPane.setVvalue(0.5); scrollPane.setVvalue(0.5);
}); }
}; }.start();
scrollPane.hvalueProperty().addListener(holder[0]);
} else { } else {
// Folge-Schritte: aktuelle Scroll-Position bewahren // Folge-Schritte: aktuelle Scroll-Position bewahren
double hval = scrollPane.getHvalue(); double hval = scrollPane.getHvalue();