Fix #29: Layout-Umbau und fit-to-view PDF-Vorschau ohne Scrollbalken
GuiBatchRunTab: Buttons "Erneut verarbeiten" / "Status zurücksetzen" und Meldungsbereich in die linke SplitPane-Spalte unterhalb der Tabelle verschoben. Detailbereich (rechte Spalte) erstreckt sich dadurch vollständig von oben bis unten – mehr Platz für die PDF-Vorschau. PdfPreviewPane: Gesamten suppressScrollReset / ChangeListener-Code entfernt. Seite wird jetzt immer fit-to-view ohne Scrollbalken angezeigt: Seitenverhältnis wird asynchron per renderPage(0.05f) ermittelt, Zoom über setZoomFactor() gesetzt und bei Größenänderungen der Anzeigefläche automatisch neu berechnet. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+29
-29
@@ -84,14 +84,11 @@ import javafx.scene.layout.VBox;
|
|||||||
* │ Ergebnisliste (60%) │ Detailbereich (40%) │
|
* │ Ergebnisliste (60%) │ Detailbereich (40%) │
|
||||||
* │ (TableView + Checkboxen) │ KI-Begründung (kompakt) │
|
* │ (TableView + Checkboxen) │ KI-Begründung (kompakt) │
|
||||||
* │ │ Dateiname-Editor │
|
* │ │ Dateiname-Editor │
|
||||||
* │ │ PDF-Vorschau (Restplatz) │
|
* ├───────────────────────────┤ PDF-Vorschau (Restplatz) │
|
||||||
* ├───────────────────────────┴──────────────────────────────┤
|
* │ [Erneut ver.] [Zurückset.]│ │
|
||||||
* │ [Erneut verarbeiten] [Status zurücksetzen] │
|
* │ Meldungsbereich │ │
|
||||||
* ├──────────────────────────────────────────────────────────┤
|
* └───────────────────────────┴──────────────────────────────┘
|
||||||
* │ Meldungs- und Zusammenfassungsbereich │
|
* [Starten] [Abbrechen]
|
||||||
* ├──────────────────────────────────────────────────────────┤
|
|
||||||
* │ [Starten] [Abbrechen] │
|
|
||||||
* └──────────────────────────────────────────────────────────┘
|
|
||||||
* </pre>
|
* </pre>
|
||||||
*
|
*
|
||||||
* <h2>Threading</h2>
|
* <h2>Threading</h2>
|
||||||
@@ -490,11 +487,31 @@ public final class GuiBatchRunTab {
|
|||||||
tableScroll.setId("batch-run-result-scroll");
|
tableScroll.setId("batch-run-result-scroll");
|
||||||
resultTable.setMinHeight(LIST_MIN_HEIGHT);
|
resultTable.setMinHeight(LIST_MIN_HEIGHT);
|
||||||
|
|
||||||
// Detailbereich: KI-Begründung oben (kompakt), darunter Dateiname-Editor,
|
// Selektions-Aktions-Buttons unterhalb der Tabelle (linke Spalte)
|
||||||
// darunter PDF-Vorschau (nimmt verbleibenden Platz)
|
reprocessButton.setId("batch-run-reprocess");
|
||||||
|
reprocessButton.setOnAction(event -> handleReprocessSelected());
|
||||||
|
|
||||||
|
resetStatusButton.setId("batch-run-reset-status");
|
||||||
|
resetStatusButton.setOnAction(event -> handleResetSelected());
|
||||||
|
|
||||||
|
HBox selectionButtonBar = new HBox(SECONDARY_SPACING, reprocessButton, resetStatusButton);
|
||||||
|
selectionButtonBar.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
selectionButtonBar.setPadding(new Insets(SECONDARY_SPACING / 2, 0, 0, 0));
|
||||||
|
|
||||||
|
// Meldungsbereich unterhalb der Selektions-Buttons (linke Spalte)
|
||||||
|
messageArea.setId("batch-run-message-area");
|
||||||
|
messageArea.setEditable(false);
|
||||||
|
messageArea.setWrapText(true);
|
||||||
|
messageArea.setPrefRowCount(3);
|
||||||
|
|
||||||
|
// Linke Spalte: Tabelle wächst, Buttons und Meldungsbereich darunter
|
||||||
|
VBox leftColumn = new VBox(0, tableScroll, selectionButtonBar, messageArea);
|
||||||
|
VBox.setVgrow(tableScroll, Priority.ALWAYS);
|
||||||
|
|
||||||
|
// Detailbereich rechts erstreckt sich vollständig von oben bis unten
|
||||||
VBox detailBox = buildDetailPane();
|
VBox detailBox = buildDetailPane();
|
||||||
|
|
||||||
SplitPane splitPane = new SplitPane(tableScroll, detailBox);
|
SplitPane splitPane = new SplitPane(leftColumn, detailBox);
|
||||||
splitPane.setId("batch-run-split-pane");
|
splitPane.setId("batch-run-split-pane");
|
||||||
splitPane.setDividerPositions(SPLIT_DIVIDER_POSITION);
|
splitPane.setDividerPositions(SPLIT_DIVIDER_POSITION);
|
||||||
SplitPane.setResizableWithParent(detailBox, true);
|
SplitPane.setResizableWithParent(detailBox, true);
|
||||||
@@ -920,22 +937,6 @@ public final class GuiBatchRunTab {
|
|||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
private Region buildFooter() {
|
private Region buildFooter() {
|
||||||
messageArea.setId("batch-run-message-area");
|
|
||||||
messageArea.setEditable(false);
|
|
||||||
messageArea.setWrapText(true);
|
|
||||||
messageArea.setPrefRowCount(3);
|
|
||||||
|
|
||||||
// Selektions-Aktions-Buttons
|
|
||||||
reprocessButton.setId("batch-run-reprocess");
|
|
||||||
reprocessButton.setOnAction(event -> handleReprocessSelected());
|
|
||||||
|
|
||||||
resetStatusButton.setId("batch-run-reset-status");
|
|
||||||
resetStatusButton.setOnAction(event -> handleResetSelected());
|
|
||||||
|
|
||||||
HBox selectionButtonBar = new HBox(SECONDARY_SPACING, reprocessButton, resetStatusButton);
|
|
||||||
selectionButtonBar.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
selectionButtonBar.setPadding(new Insets(SECONDARY_SPACING, 0, 0, 0));
|
|
||||||
|
|
||||||
// Lauf-Steuerungs-Buttons
|
// Lauf-Steuerungs-Buttons
|
||||||
startButton.setId("batch-run-start");
|
startButton.setId("batch-run-start");
|
||||||
startButton.setOnAction(event -> handleStart());
|
startButton.setOnAction(event -> handleStart());
|
||||||
@@ -948,8 +949,7 @@ public final class GuiBatchRunTab {
|
|||||||
runButtonBar.setAlignment(Pos.CENTER_LEFT);
|
runButtonBar.setAlignment(Pos.CENTER_LEFT);
|
||||||
runButtonBar.setPadding(new Insets(SECONDARY_SPACING / 2, 0, 0, 0));
|
runButtonBar.setPadding(new Insets(SECONDARY_SPACING / 2, 0, 0, 0));
|
||||||
|
|
||||||
VBox footer = new VBox(SECONDARY_SPACING / 2, selectionButtonBar, messageArea, runButtonBar);
|
return runButtonBar;
|
||||||
return footer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|||||||
+81
-119
@@ -11,8 +11,6 @@ import org.apache.logging.log4j.Logger;
|
|||||||
|
|
||||||
import com.dlsc.pdfviewfx.PDFView;
|
import com.dlsc.pdfviewfx.PDFView;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.value.ChangeListener;
|
|
||||||
import javafx.beans.value.ObservableValue;
|
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
@@ -20,7 +18,6 @@ import javafx.scene.control.Button;
|
|||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.ProgressIndicator;
|
import javafx.scene.control.ProgressIndicator;
|
||||||
import javafx.scene.control.ScrollPane;
|
import javafx.scene.control.ScrollPane;
|
||||||
import javafx.scene.input.ScrollEvent;
|
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
import javafx.scene.layout.Region;
|
import javafx.scene.layout.Region;
|
||||||
@@ -30,16 +27,15 @@ import javafx.scene.layout.VBox;
|
|||||||
/**
|
/**
|
||||||
* Detailbereich-Komponente zur asynchronen Anzeige von Seiten einer Quelldatei.
|
* Detailbereich-Komponente zur asynchronen Anzeige von Seiten einer Quelldatei.
|
||||||
*
|
*
|
||||||
* <p>Die Komponente zeigt die Seiten einer PDF-Datei mit Seitennavigation an.
|
* <p>Die Komponente zeigt eine einzelne Seite der PDF-Datei vollständig eingepasst
|
||||||
* Das Laden erfolgt auf einem Hintergrund-Worker-Thread; UI-Updates laufen
|
* (fit-to-view) an. Das Laden erfolgt auf einem Hintergrund-Worker-Thread; UI-Updates
|
||||||
* ausschließlich über den JavaFX Application Thread.
|
* 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.
|
||||||
*
|
*
|
||||||
* <p>PDFView übernimmt intern das Rendern und die Darstellung. Diese Komponente
|
* <p>Es gilt das Prinzip „Latest Preview Request Wins": Veraltete Lade-Ergebnisse
|
||||||
* steuert Laden, Fehlerbehandlung und den Ladeindikator.
|
* werden verworfen, sobald eine neue Anforderung eingeht.
|
||||||
*
|
|
||||||
* <p>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.
|
|
||||||
*
|
*
|
||||||
* <h2>Fehlerfälle</h2>
|
* <h2>Fehlerfälle</h2>
|
||||||
* <ul>
|
* <ul>
|
||||||
@@ -50,7 +46,8 @@ import javafx.scene.layout.VBox;
|
|||||||
*
|
*
|
||||||
* <h2>Threading</h2>
|
* <h2>Threading</h2>
|
||||||
* <p>Alle öffentlichen Methoden müssen auf dem JavaFX Application Thread aufgerufen
|
* <p>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 {
|
public final class PdfPreviewPane {
|
||||||
|
|
||||||
@@ -99,17 +96,10 @@ public final class PdfPreviewPane {
|
|||||||
private boolean enabled = true;
|
private boolean enabled = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interner ScrollPane der PDFView-Skin. Wird nach der Skin-Installation
|
* Zuletzt ermitteltes Seitenverhältnis (Höhe/Breite) der gerenderten Seite.
|
||||||
* per Lookup gesetzt und für den Scroll-Schutz und den Seitenanfang-Listener verwendet.
|
* Wird asynchron nach dem Laden gesetzt; -1.0 wenn noch nicht bekannt.
|
||||||
*/
|
*/
|
||||||
private ScrollPane pdfViewScrollPane = null;
|
private double lastKnownAspectRatio = -1.0;
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<Number> activeScrollToTopListener = null;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Erstellt die Komponente im deaktivierten Platzhalter-Zustand.
|
* Erstellt die Komponente im deaktivierten Platzhalter-Zustand.
|
||||||
@@ -122,11 +112,16 @@ public final class PdfPreviewPane {
|
|||||||
pdfView.setShowToolBar(false);
|
pdfView.setShowToolBar(false);
|
||||||
pdfView.setId("pdf-preview-view");
|
pdfView.setId("pdf-preview-view");
|
||||||
|
|
||||||
// Nach der Skin-Installation den internen ScrollPane suchen und
|
// Nach der Skin-Installation Scrollbalken im internen ScrollPane ausblenden.
|
||||||
// die Scroll-Handler installieren.
|
|
||||||
pdfView.skinProperty().addListener((obs, oldSkin, newSkin) -> {
|
pdfView.skinProperty().addListener((obs, oldSkin, newSkin) -> {
|
||||||
if (newSkin != null) {
|
if (newSkin != null) {
|
||||||
Platform.runLater(this::installScrollHandlers);
|
Platform.runLater(() -> {
|
||||||
|
Node found = pdfView.lookup(".scroll-pane");
|
||||||
|
if (found instanceof ScrollPane sp) {
|
||||||
|
sp.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||||
|
sp.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -148,6 +143,10 @@ public final class PdfPreviewPane {
|
|||||||
StackPane.setAlignment(progressIndicator, Pos.CENTER);
|
StackPane.setAlignment(progressIndicator, Pos.CENTER);
|
||||||
VBox.setVgrow(viewStack, Priority.ALWAYS);
|
VBox.setVgrow(viewStack, Priority.ALWAYS);
|
||||||
|
|
||||||
|
// Bei Größenänderungen der Anzeigefläche Zoom neu berechnen
|
||||||
|
viewStack.widthProperty().addListener((obs, ov, nv) -> fitToView());
|
||||||
|
viewStack.heightProperty().addListener((obs, ov, nv) -> fitToView());
|
||||||
|
|
||||||
prevButton.setId("pdf-preview-prev-button");
|
prevButton.setId("pdf-preview-prev-button");
|
||||||
prevButton.setOnAction(e -> navigateToPreviousPage());
|
prevButton.setOnAction(e -> navigateToPreviousPage());
|
||||||
|
|
||||||
@@ -205,9 +204,9 @@ public final class PdfPreviewPane {
|
|||||||
currentSourceFile = null;
|
currentSourceFile = null;
|
||||||
currentPage = 0;
|
currentPage = 0;
|
||||||
totalPages = -1;
|
totalPages = -1;
|
||||||
|
lastKnownAspectRatio = -1.0;
|
||||||
// Neue Sequenznummer: laufende Requests werden verworfen
|
// Neue Sequenznummer: laufende Requests werden verworfen
|
||||||
currentRequestSequence.incrementAndGet();
|
currentRequestSequence.incrementAndGet();
|
||||||
cancelScrollToTopListener();
|
|
||||||
pdfView.unload();
|
pdfView.unload();
|
||||||
showPlaceholder();
|
showPlaceholder();
|
||||||
updateNavigationButtons();
|
updateNavigationButtons();
|
||||||
@@ -271,9 +270,7 @@ public final class PdfPreviewPane {
|
|||||||
currentPage = targetPage;
|
currentPage = targetPage;
|
||||||
updatePageLabel();
|
updatePageLabel();
|
||||||
updateNavigationButtons();
|
updateNavigationButtons();
|
||||||
// Nach dem Seitenwechsel stellt PDFViewFX intern die Scroll-Position ein.
|
fitToView();
|
||||||
// Der Einmal-Listener korrigiert sie bei Bedarf auf den Seitenanfang.
|
|
||||||
scheduleScrollToTop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void navigateToNextPage() {
|
private void navigateToNextPage() {
|
||||||
@@ -285,106 +282,72 @@ public final class PdfPreviewPane {
|
|||||||
currentPage = targetPage;
|
currentPage = targetPage;
|
||||||
updatePageLabel();
|
updatePageLabel();
|
||||||
updateNavigationButtons();
|
updateNavigationButtons();
|
||||||
// Nach dem Seitenwechsel stellt PDFViewFX intern die Scroll-Position ein.
|
fitToView();
|
||||||
// Der Einmal-Listener korrigiert sie bei Bedarf auf den Seitenanfang.
|
|
||||||
scheduleScrollToTop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Scroll-Hilfsmethoden ------------------------------------------------
|
// --- Zoom-Berechnung (fit-to-view) ----------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sucht nach der Skin-Installation den internen ScrollPane der PDFView-Skin
|
* Berechnet den optimalen Zoomfaktor anhand des bekannten Seitenverhältnisses und
|
||||||
* und registriert dort den Mausrad-Seitenwechsel-Filter.
|
* 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
|
||||||
* <p>Der Filter verhindert, dass das Mausrad an den Scroll-Grenzen automatisch
|
* Anzeigefläche noch keine Größe hat.
|
||||||
* die Seite wechselt; das Inhalts-Scrolling innerhalb einer Seite bleibt
|
|
||||||
* unberührt.
|
|
||||||
*/
|
*/
|
||||||
private void installScrollHandlers() {
|
private void fitToView() {
|
||||||
Node found = pdfView.lookup(".scroll-pane");
|
if (lastKnownAspectRatio <= 0.0) {
|
||||||
if (!(found instanceof ScrollPane sp)) {
|
|
||||||
LOG.warn("PDF-Vorschau: Interner ScrollPane nicht gefunden – Scroll-Handler nicht aktiv");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pdfViewScrollPane = sp;
|
double vw = viewStack.getWidth();
|
||||||
LOG.debug("PDF-Vorschau: Interner ScrollPane gefunden, Scroll-Handler werden installiert");
|
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()) {
|
* 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;
|
return;
|
||||||
}
|
}
|
||||||
Node content = sp.getContent();
|
int pageIdx = Math.max(0, currentPage - 1);
|
||||||
if (content == null) {
|
executor.submit(() -> {
|
||||||
event.consume();
|
try {
|
||||||
return;
|
// Seite bei sehr kleiner Skala rendern, um das Seitenverhältnis zu ermitteln
|
||||||
}
|
var img = doc.renderPage(pageIdx, 0.05f);
|
||||||
double contentH = content.getBoundsInLocal().getHeight();
|
if (img != null && img.getWidth() > 0) {
|
||||||
double viewportH = sp.getViewportBounds().getHeight();
|
double ratio = (double) img.getHeight() / img.getWidth();
|
||||||
boolean hatUeberlauf = contentH > viewportH + 1.0;
|
Platform.runLater(() -> {
|
||||||
|
if (currentRequestSequence.get() == seq) {
|
||||||
if (!hatUeberlauf) {
|
lastKnownAspectRatio = ratio;
|
||||||
// Seite passt vollständig in den Viewport: kein Inhalts-Scrolling möglich,
|
fitToView();
|
||||||
// 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();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} 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.
|
|
||||||
*
|
|
||||||
* <p>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 ---------------------------------------------------
|
// --- Asynchrones Laden ---------------------------------------------------
|
||||||
@@ -442,9 +405,8 @@ public final class PdfPreviewPane {
|
|||||||
showContent();
|
showContent();
|
||||||
updateNavigationButtons();
|
updateNavigationButtons();
|
||||||
updatePageLabel();
|
updatePageLabel();
|
||||||
// PDFViewFX setzt nach dem Rendering intern die Scroll-Position.
|
// Seitenverhältnis asynchron ermitteln und Zoom anpassen
|
||||||
// Der Einmal-Listener korrigiert sie bei Bedarf auf den Seitenanfang.
|
fetchAspectRatioAsync(seq);
|
||||||
scheduleScrollToTop();
|
|
||||||
LOG.debug("PDF-Vorschau: Rendering angestoßen – {} Seite(n)", totalPages);
|
LOG.debug("PDF-Vorschau: Rendering angestoßen – {} Seite(n)", totalPages);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
String msg = classifyLoadException(e);
|
String msg = classifyLoadException(e);
|
||||||
|
|||||||
Reference in New Issue
Block a user