Implementiere PDF-Vorschau: Zoom-Verbesserungen und Grab & Pan
32c: ScrollPane.setPrefSize(0,0) und StackPane.setMinSize(0,0) verhindern, dass der Vorschaubereich beim manuellen Zoom mitwächst. 32a: Zoom-Akkumulator nutzt if statt while – pro Mausrad-Raste wird genau eine Zoom-Stufe (10 %) angewendet, auch bei großen deltaY-Werten. 32b: Beim ersten Zoom-Einstieg wird die Ansicht auf die Bildmitte zentriert (H/V = 0.5). scrollPane.layout() vor der Scroll-Wert- Restaurierung stellt sicher, dass die neuen Inhaltsgrenzen bekannt sind. 32d: Grab & Pan – im manuellen Zoom-Modus kann die Vorschau mit der Maus verschoben werden. OPEN_HAND-Cursor signalisiert den Zoom-Modus, CLOSED_HAND die aktive Pan-Geste. 32e: resetToFitView() setzt Pan-Zustand und Mauszeiger zurück, sodass beim Laden einer neuen Datei der Fit-to-View-Modus vollständig wiederhergestellt wird. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+119
-8
@@ -23,6 +23,7 @@ import javafx.geometry.Bounds;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiTooltipTexts;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
@@ -31,6 +32,7 @@ import javafx.scene.control.Tooltip;
|
||||
import javafx.util.Duration;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.input.ScrollEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
@@ -53,6 +55,11 @@ import javafx.scene.layout.VBox;
|
||||
* {@link ScrollPane} übernimmt das Scrollen. Das Laden einer neuen Datei setzt den
|
||||
* Zoom automatisch auf Fit-to-View zurück.
|
||||
*
|
||||
* <p><strong>Grab & Pan:</strong> Im manuellen Zoom-Modus kann die Vorschau durch
|
||||
* Klicken und Ziehen (linke Maustaste) verschoben werden. Der Mauszeiger wechselt im
|
||||
* Zoom-Modus auf {@link Cursor#OPEN_HAND} und während der Geste auf
|
||||
* {@link Cursor#CLOSED_HAND}.
|
||||
*
|
||||
* <p>Das Laden der PDF-Datei und das Rendering einzelner Seiten erfolgt auf einem
|
||||
* dedizierten Worker-Thread. UI-Updates laufen ausschließlich über den JavaFX
|
||||
* Application Thread. Bereits gerenderte Seiten werden in einem In-Memory-Cache
|
||||
@@ -124,6 +131,18 @@ public final class PdfPreviewPane {
|
||||
*/
|
||||
private double naturalViewportWidth = 0.0;
|
||||
|
||||
/** X-Startposition der laufenden Pan-Geste in Bildschirmkoordinaten; -1 wenn inaktiv. */
|
||||
private double panStartX = -1;
|
||||
|
||||
/** Y-Startposition der laufenden Pan-Geste in Bildschirmkoordinaten; -1 wenn inaktiv. */
|
||||
private double panStartY = -1;
|
||||
|
||||
/** Horizontaler Scroll-Wert zu Beginn der laufenden Pan-Geste. */
|
||||
private double panStartHvalue = 0.0;
|
||||
|
||||
/** Vertikaler Scroll-Wert zu Beginn der laufenden Pan-Geste. */
|
||||
private double panStartVvalue = 0.0;
|
||||
|
||||
/**
|
||||
* Sequenznummer der aktuell angeforderten Vorschau. Jede neue Anforderung
|
||||
* (Laden oder Seitenwechsel) erhöht diesen Zähler. Lade-/Rendering-Ergebnisse
|
||||
@@ -205,6 +224,9 @@ public final class PdfPreviewPane {
|
||||
scrollPane.setFitToHeight(true);
|
||||
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
|
||||
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
|
||||
// 32c: Verhindert, dass ScrollPane und StackPane beim manuellen Zoom mitwachsen
|
||||
scrollPane.setPrefSize(0, 0);
|
||||
viewStack.setMinSize(0, 0);
|
||||
VBox.setVgrow(scrollPane, Priority.ALWAYS);
|
||||
// Strg + Mausrad → Zoom; ohne Strg → normales Scrollen
|
||||
scrollPane.addEventFilter(ScrollEvent.SCROLL, event -> {
|
||||
@@ -213,6 +235,10 @@ public final class PdfPreviewPane {
|
||||
event.consume();
|
||||
}
|
||||
});
|
||||
// Grab & Pan – im manuellen Zoom-Modus mit Maus verschiebbar
|
||||
viewStack.addEventHandler(MouseEvent.MOUSE_PRESSED, this::onPanMousePressed);
|
||||
viewStack.addEventHandler(MouseEvent.MOUSE_DRAGGED, this::onPanMouseDragged);
|
||||
viewStack.addEventHandler(MouseEvent.MOUSE_RELEASED, this::onPanMouseReleased);
|
||||
|
||||
prevButton.setId("pdf-preview-prev-button");
|
||||
prevButton.setOnAction(e -> navigateToPreviousPage());
|
||||
@@ -531,22 +557,90 @@ public final class PdfPreviewPane {
|
||||
});
|
||||
}
|
||||
|
||||
// --- Grab & Pan -----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Startet die Pan-Geste. Speichert die Startposition und den aktuellen Scroll-Zustand.
|
||||
* Nur aktiv wenn der manuelle Zoom-Modus eingeschaltet ist.
|
||||
*
|
||||
* @param event das Maus-Pressed-Ereignis
|
||||
*/
|
||||
private void onPanMousePressed(MouseEvent event) {
|
||||
if (scrollPane.isFitToWidth()) {
|
||||
return; // Im Fit-Modus kein Pan nötig
|
||||
}
|
||||
panStartX = event.getScreenX();
|
||||
panStartY = event.getScreenY();
|
||||
panStartHvalue = scrollPane.getHvalue();
|
||||
panStartVvalue = scrollPane.getVvalue();
|
||||
viewStack.setCursor(Cursor.CLOSED_HAND);
|
||||
event.consume();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verschiebt den Viewport relativ zur Startposition der Pan-Geste.
|
||||
* Die Scrolldelta wird auf die scrollbaren Bereiche des Inhalts normiert.
|
||||
*
|
||||
* @param event das Maus-Dragged-Ereignis
|
||||
*/
|
||||
private void onPanMouseDragged(MouseEvent event) {
|
||||
if (panStartX < 0 || scrollPane.isFitToWidth()) {
|
||||
return;
|
||||
}
|
||||
double dx = event.getScreenX() - panStartX;
|
||||
double dy = event.getScreenY() - panStartY;
|
||||
|
||||
Bounds viewport = scrollPane.getViewportBounds();
|
||||
double contentWidth = viewStack.getWidth();
|
||||
double contentHeight = viewStack.getHeight();
|
||||
double viewportWidth = viewport != null ? viewport.getWidth() : 0;
|
||||
double viewportHeight = viewport != null ? viewport.getHeight() : 0;
|
||||
|
||||
double scrollableWidth = contentWidth - viewportWidth;
|
||||
double scrollableHeight = contentHeight - viewportHeight;
|
||||
|
||||
if (scrollableWidth > 0) {
|
||||
double newHval = panStartHvalue - dx / scrollableWidth;
|
||||
scrollPane.setHvalue(Math.max(0, Math.min(1, newHval)));
|
||||
}
|
||||
if (scrollableHeight > 0) {
|
||||
double newVval = panStartVvalue - dy / scrollableHeight;
|
||||
scrollPane.setVvalue(Math.max(0, Math.min(1, newVval)));
|
||||
}
|
||||
event.consume();
|
||||
}
|
||||
|
||||
/**
|
||||
* Beendet die Pan-Geste und stellt den OPEN_HAND-Mauszeiger wieder her.
|
||||
*
|
||||
* @param event das Maus-Released-Ereignis
|
||||
*/
|
||||
private void onPanMouseReleased(MouseEvent event) {
|
||||
panStartX = -1;
|
||||
panStartY = -1;
|
||||
if (!scrollPane.isFitToWidth()) {
|
||||
viewStack.setCursor(Cursor.OPEN_HAND);
|
||||
}
|
||||
event.consume();
|
||||
}
|
||||
|
||||
// --- Zoom -----------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Akkumuliert den Scroll-Delta und wendet den Zoom schrittweise an.
|
||||
* Pro Raste (ca. {@value #ZOOM_NOTCH_THRESHOLD} Einheiten) ändert sich der Zoom
|
||||
* um {@value #ZOOM_STEP}.
|
||||
* um {@value #ZOOM_STEP}. Pro ScrollEvent wird maximal eine Zoom-Stufe angewendet.
|
||||
*
|
||||
* @param deltaY vertikaler Scroll-Delta des {@link ScrollEvent}
|
||||
*/
|
||||
private void accumulateAndApplyZoomDelta(double deltaY) {
|
||||
zoomAccumulator += deltaY;
|
||||
while (zoomAccumulator >= ZOOM_NOTCH_THRESHOLD) {
|
||||
// 32a: Pro Mausrad-Raste genau eine Zoom-Stufe (if statt while verhindert
|
||||
// mehrfaches Springen bei großen deltaY-Werten)
|
||||
if (zoomAccumulator >= ZOOM_NOTCH_THRESHOLD) {
|
||||
zoomAccumulator -= ZOOM_NOTCH_THRESHOLD;
|
||||
applyZoom(Math.min(ZOOM_MAX, zoomLevel + ZOOM_STEP));
|
||||
}
|
||||
while (zoomAccumulator <= -ZOOM_NOTCH_THRESHOLD) {
|
||||
} else if (zoomAccumulator <= -ZOOM_NOTCH_THRESHOLD) {
|
||||
zoomAccumulator += ZOOM_NOTCH_THRESHOLD;
|
||||
applyZoom(Math.max(ZOOM_MIN, zoomLevel - ZOOM_STEP));
|
||||
}
|
||||
@@ -555,13 +649,19 @@ public final class PdfPreviewPane {
|
||||
/**
|
||||
* Setzt den Zoomfaktor und verlässt beim ersten Aufruf den Fit-to-View-Modus.
|
||||
* Die Referenzbreite wird einmalig aus dem Viewport übernommen.
|
||||
* <p>
|
||||
* Beim Wechsel aus dem Fit-to-View-Modus wird die Ansicht auf die Bildmitte
|
||||
* zentriert (H/V = 0.5). Bei weiteren Zoom-Schritten bleibt die aktuelle
|
||||
* Scrollposition erhalten. Ein {@code layout()}-Aufruf vor der Positionswiederherstellung
|
||||
* stellt sicher, dass die neuen Inhaltsgrenzen bereits berechnet sind.
|
||||
*
|
||||
* @param newZoom gewünschter Zoomfaktor, wird auf [{@link #ZOOM_MIN}, {@link #ZOOM_MAX}] begrenzt
|
||||
*/
|
||||
private void applyZoom(double newZoom) {
|
||||
double effective = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, newZoom));
|
||||
|
||||
if (scrollPane.isFitToWidth()) {
|
||||
boolean wasInFitMode = scrollPane.isFitToWidth();
|
||||
if (wasInFitMode) {
|
||||
Bounds viewport = scrollPane.getViewportBounds();
|
||||
double vpWidth = viewport != null ? viewport.getWidth() : viewStack.getWidth();
|
||||
if (vpWidth <= 0) {
|
||||
@@ -572,32 +672,43 @@ public final class PdfPreviewPane {
|
||||
scrollPane.setFitToHeight(false);
|
||||
imageView.fitWidthProperty().unbind();
|
||||
imageView.fitHeightProperty().unbind();
|
||||
// 32d: Mauszeiger signalisiert Pan-Modus
|
||||
viewStack.setCursor(Cursor.OPEN_HAND);
|
||||
}
|
||||
|
||||
if (effective == zoomLevel) {
|
||||
return;
|
||||
}
|
||||
|
||||
double hval = scrollPane.getHvalue();
|
||||
double vval = scrollPane.getVvalue();
|
||||
// 32b: Beim ersten Zoom Mitte beibehalten; danach aktuelle Position bewahren
|
||||
double hval = wasInFitMode ? 0.5 : scrollPane.getHvalue();
|
||||
double vval = wasInFitMode ? 0.5 : scrollPane.getVvalue();
|
||||
|
||||
zoomLevel = effective;
|
||||
imageView.setFitWidth(naturalViewportWidth * zoomLevel);
|
||||
imageView.setFitHeight(0);
|
||||
|
||||
// 32b: layout() stellt sicher, dass die neuen Inhaltsgrenzen bekannt sind,
|
||||
// bevor die Scroll-Werte restauriert werden
|
||||
Platform.runLater(() -> {
|
||||
scrollPane.layout();
|
||||
scrollPane.setHvalue(hval);
|
||||
scrollPane.setVvalue(vval);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt Zoom und Akkumulator zurück und reaktiviert den Fit-to-View-Modus.
|
||||
* Setzt Zoom, Akkumulator und Pan-Zustand zurück und reaktiviert den Fit-to-View-Modus.
|
||||
* Wird beim Laden einer neuen Datei und beim Leeren der Komponente aufgerufen.
|
||||
*/
|
||||
private void resetToFitView() {
|
||||
zoomLevel = 1.0;
|
||||
zoomAccumulator = 0.0;
|
||||
naturalViewportWidth = 0.0;
|
||||
// 32e: Pan-Zustand und Mauszeiger zurücksetzen
|
||||
panStartX = -1;
|
||||
panStartY = -1;
|
||||
viewStack.setCursor(null);
|
||||
if (!scrollPane.isFitToWidth()) {
|
||||
imageView.fitWidthProperty().bind(viewStack.widthProperty());
|
||||
imageView.fitHeightProperty().bind(viewStack.heightProperty());
|
||||
|
||||
Reference in New Issue
Block a user