diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunTab.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunTab.java index 889ce1f..af9523b 100644 --- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunTab.java +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunTab.java @@ -84,14 +84,11 @@ import javafx.scene.layout.VBox; * │ Ergebnisliste (60%) │ Detailbereich (40%) │ * │ (TableView + Checkboxen) │ KI-Begründung (kompakt) │ * │ │ Dateiname-Editor │ - * │ │ PDF-Vorschau (Restplatz) │ - * ├───────────────────────────┴──────────────────────────────┤ - * │ [Erneut verarbeiten] [Status zurücksetzen] │ - * ├──────────────────────────────────────────────────────────┤ - * │ Meldungs- und Zusammenfassungsbereich │ - * ├──────────────────────────────────────────────────────────┤ - * │ [Starten] [Abbrechen] │ - * └──────────────────────────────────────────────────────────┘ + * ├───────────────────────────┤ PDF-Vorschau (Restplatz) │ + * │ [Erneut ver.] [Zurückset.]│ │ + * │ Meldungsbereich │ │ + * └───────────────────────────┴──────────────────────────────┘ + * [Starten] [Abbrechen] * * *
Die Komponente zeigt die Seiten einer PDF-Datei mit Seitennavigation an. - * Das Laden erfolgt auf einem Hintergrund-Worker-Thread; UI-Updates laufen - * ausschließlich über den JavaFX Application Thread. + *
Die Komponente zeigt eine einzelne Seite der PDF-Datei vollständig eingepasst + * (fit-to-view) an. Das Laden erfolgt auf einem Hintergrund-Worker-Thread; UI-Updates + * laufen ausschließlich über den JavaFX Application Thread. Nach dem Laden wird das + * Seitenverhältnis asynchron ermittelt und der Zoom so gesetzt, dass die Seite ohne + * Scrollbalken vollständig sichtbar ist. Bei Größenänderungen der Anzeigefläche wird + * der Zoom automatisch neu berechnet. * - *
PDFView übernimmt intern das Rendern und die Darstellung. Diese Komponente - * steuert Laden, Fehlerbehandlung und den Ladeindikator. - * - *
Beim Selektionswechsel wird eine neue Lade-Anforderung ausgelöst. Es gilt das - * Prinzip „Latest Preview Request Wins": Veraltete Lade-Ergebnisse werden - * verworfen, sobald eine neue Anforderung eingeht. + *
Es gilt das Prinzip „Latest Preview Request Wins": Veraltete Lade-Ergebnisse + * werden verworfen, sobald eine neue Anforderung eingeht. * *
Alle öffentlichen Methoden müssen auf dem JavaFX Application Thread aufgerufen
- * werden. Internes Laden läuft auf einem dedizierten Worker-Thread.
+ * werden. Internes Laden und Seitenverhältnis-Ermittlung laufen auf einem dedizierten
+ * Worker-Thread.
*/
public final class PdfPreviewPane {
@@ -99,17 +96,10 @@ public final class PdfPreviewPane {
private boolean enabled = true;
/**
- * Interner ScrollPane der PDFView-Skin. Wird nach der Skin-Installation
- * per Lookup gesetzt und für den Scroll-Schutz und den Seitenanfang-Listener verwendet.
+ * Zuletzt ermitteltes Seitenverhältnis (Höhe/Breite) der gerenderten Seite.
+ * Wird asynchron nach dem Laden gesetzt; -1.0 wenn noch nicht bekannt.
*/
- private ScrollPane pdfViewScrollPane = null;
-
- /**
- * 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 ChangeListener Der Filter verhindert, dass das Mausrad an den Scroll-Grenzen automatisch
- * die Seite wechselt; das Inhalts-Scrolling innerhalb einer Seite bleibt
- * unberührt.
+ * Berechnet den optimalen Zoomfaktor anhand des bekannten Seitenverhältnisses und
+ * der aktuellen Größe der Anzeigefläche und setzt ihn auf {@code pdfView}.
+ * Hat keinen Effekt wenn das Seitenverhältnis noch nicht bekannt ist oder die
+ * Anzeigefläche noch keine Größe hat.
*/
- private void installScrollHandlers() {
- Node found = pdfView.lookup(".scroll-pane");
- if (!(found instanceof ScrollPane sp)) {
- LOG.warn("PDF-Vorschau: Interner ScrollPane nicht gefunden – Scroll-Handler nicht aktiv");
+ private void fitToView() {
+ if (lastKnownAspectRatio <= 0.0) {
return;
}
- pdfViewScrollPane = sp;
- LOG.debug("PDF-Vorschau: Interner ScrollPane gefunden, Scroll-Handler werden installiert");
+ double vw = viewStack.getWidth();
+ double vh = viewStack.getHeight();
+ if (vw <= 0.0 || vh <= 0.0) {
+ return;
+ }
+ PDFView.Document doc = pdfView.getDocument();
+ if (doc == null || currentPage <= 0) {
+ return;
+ }
+ // Querformat: Zoom=1 → Höhe füllt den Viewport; Breite kann überlaufen.
+ // Hochformat: Zoom=1 → Breite füllt den Viewport; Höhe kann überlaufen.
+ boolean landscape = doc.isLandscape(currentPage - 1);
+ double zoom;
+ if (!landscape) {
+ zoom = Math.min(1.0, vh / (lastKnownAspectRatio * vw));
+ } else {
+ zoom = Math.min(1.0, vw / (lastKnownAspectRatio * vh));
+ }
+ pdfView.setZoomFactor(Math.max(0.05, zoom));
+ }
- sp.addEventFilter(ScrollEvent.SCROLL, event -> {
- if (event.isInertia()) {
- return;
- }
- 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();
+ /**
+ * Ermittelt das Seitenverhältnis der angegebenen Seite asynchron auf dem
+ * Worker-Thread und aktualisiert danach den Zoom auf dem FX-Thread.
+ * Veraltete Ergebnisse werden anhand der Sequenznummer verworfen.
+ *
+ * @param seq die Sequenznummer der aktuellen Lade-Anforderung
+ */
+ private void fetchAspectRatioAsync(long seq) {
+ PDFView.Document doc = pdfView.getDocument();
+ if (doc == null) {
+ return;
+ }
+ int pageIdx = Math.max(0, currentPage - 1);
+ executor.submit(() -> {
+ try {
+ // Seite bei sehr kleiner Skala rendern, um das Seitenverhältnis zu ermitteln
+ var img = doc.renderPage(pageIdx, 0.05f);
+ if (img != null && img.getWidth() > 0) {
+ double ratio = (double) img.getHeight() / img.getWidth();
+ Platform.runLater(() -> {
+ if (currentRequestSequence.get() == seq) {
+ lastKnownAspectRatio = ratio;
+ fitToView();
+ }
+ });
+ }
+ } catch (Exception e) {
+ LOG.debug("PDF-Vorschau: Seitenverhältnis nicht ermittelbar: {}", e.getMessage());
}
});
}
- /**
- * 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.
- *
- * Ein zuvor registrierter, noch nicht ausgelöster Listener wird vor der
- * Neuregistrierung entfernt (Rapid-Page-Change-Schutz).
- */
- private void scheduleScrollToTop() {
- if (pdfViewScrollPane == null) {
- return;
- }
- // Vorherigen Listener entfernen, falls ein schneller Seitenwechsel ihn
- // noch nicht ausgelöst hat
- cancelScrollToTopListener();
-
- 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);
- }
- }
- };
- 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 ---------------------------------------------------
/**
@@ -442,9 +405,8 @@ public final class PdfPreviewPane {
showContent();
updateNavigationButtons();
updatePageLabel();
- // PDFViewFX setzt nach dem Rendering intern die Scroll-Position.
- // Der Einmal-Listener korrigiert sie bei Bedarf auf den Seitenanfang.
- scheduleScrollToTop();
+ // Seitenverhältnis asynchron ermitteln und Zoom anpassen
+ fetchAspectRatioAsync(seq);
LOG.debug("PDF-Vorschau: Rendering angestoßen – {} Seite(n)", totalPages);
} catch (Exception e) {
String msg = classifyLoadException(e);