diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/MainWindowController.java b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/MainWindowController.java index 0e243bb4..17415c6a 100644 --- a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/MainWindowController.java +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/MainWindowController.java @@ -8,6 +8,8 @@ import javafx.animation.PauseTransition; import javafx.application.Platform; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.fxml.FXML; import javafx.geometry.Insets; import javafx.scene.Node; @@ -116,6 +118,10 @@ public class MainWindowController { private SignatureFieldInfo selectedSigField; /** Marker rectangle of {@link #selectedSigField}, relative to the displayed page (see PdfExtraInfo). */ private float[] selectedSigFieldMarker; + /** Mirrors {@link #selectedSigField} for the bindings that lock the visible-signature toggles. */ + private final BooleanProperty sigFieldSelected = new SimpleBooleanProperty(false); + /** True while no document is loaded, i.e. there is nothing to place a visible signature on. */ + private final BooleanProperty noDocument = new SimpleBooleanProperty(true); private PdfPageView pdfPageView; private SignatureOverlay signatureOverlay; /** Holds the side panel node while it's detached from the SplitPane (hidden). */ @@ -305,6 +311,13 @@ private void initialize() { btnVisibleSig.selectedProperty().bindBidirectional(signingVM.visibleProperty()); btnTsa.selectedProperty().bindBidirectional(signingVM.tsaEnabledProperty()); + // Signing into an existing field always draws the appearance into that field's rectangle - SignerLogic + // forces visible=true for it regardless of this flag - so the menu item and the toolbar toggle lock + // together with the side-panel checkbox. Left switchable they would only make the UI disagree with the + // signature that comes out. "(create new field)" is the way back to an invisible signature. + menuVisibleSig.disableProperty().bind(noDocument.or(sigFieldSelected)); + btnVisibleSig.disableProperty().bind(noDocument.or(sigFieldSelected)); + // When TSA is turned on but no URL is configured yet, jump the side-panel // accordion to the TSA section so the user can fill the required field. signingVM.tsaEnabledProperty().addListener((obs, was, on) -> { @@ -742,14 +755,15 @@ private void setDocumentControlsDisabled(boolean disabled) { txtPageNumber.setDisable(disabled); btnNextPage.setDisable(disabled); btnSign.setDisable(disabled); - btnVisibleSig.setDisable(disabled); menuSign.setDisable(disabled); menuClose.setDisable(disabled); menuSaveAs.setDisable(disabled); - menuVisibleSig.setDisable(disabled); menuZoomIn.setDisable(disabled); menuZoomOut.setDisable(disabled); menuZoomFit.setDisable(disabled); + // The visible-signature toggles (menu item + toolbar button) are bound to this property instead of being + // disabled here, so that a selected signature field can lock them as well. + noDocument.set(disabled); if (signatureSettingsController != null) { signatureSettingsController.setVisibleSigCheckBoxDisabled(disabled); } @@ -792,9 +806,13 @@ private void updateSigCoordsBadge() { * coordinates persisted in the ViewModel — if they form a valid rectangle * that fits the current page — and falls back to a safe bottom-right * default otherwise. Always re-targets the current page. + *

+ * Does nothing while an existing signature field is selected: that field's own {@code /Rect} decides where the + * signature goes, so a placement rectangle would only promise a position the signing path ignores. */ private void autoPlaceVisibleSignature() { - if (!documentVM.isDocumentLoaded() || placementVM.isPlaced() || options == null) { + if (!documentVM.isDocumentLoaded() || placementVM.isPlaced() || options == null + || selectedSigField != null) { return; } PageInfo pageInfo = new PdfExtraInfo(options).getPageInfo(documentVM.getCurrentPage()); @@ -809,11 +827,7 @@ private void autoPlaceVisibleSignature() { float urx = signingVM.positionURXProperty().get(); float ury = signingVM.positionURYProperty().get(); - boolean fits = urx - llx > 1f && ury - lly > 1f - && llx >= 0f && lly >= 0f - && urx <= pw && ury <= ph; - - if (fits) { + if (VisibleSignatureCoordinator.hasUsablePosition(llx, lly, urx, ury, pw, ph)) { placementVM.fromPdfCoordinates(llx, lly, urx, ury, pw, ph); } else { // Safe default: bottom-right, 15% × 8% of the page with ~5% margins. @@ -1495,11 +1509,19 @@ private void refreshSignatureFields() { * @param field the selected field, or {@code null} for "create a new field" */ private void onSigFieldSelected(SignatureFieldInfo field) { + final boolean hadField = selectedSigField != null; selectedSigField = field; selectedSigFieldMarker = null; + sigFieldSelected.set(field != null); if (field == null) { signatureOverlay.clearFieldHighlight(); signatureOverlay.setMouseTransparent(false); + if (hadField && signingVM.visibleProperty().get()) { + // The visible flag the field forced on survives its deselection, so the rectangle has to come + // back with it - otherwise signing would place the appearance at coordinates nothing on screen + // shows. The position options were reset with the selection, so this lands on the default spot. + autoPlaceVisibleSignature(); + } updateSigStateBadge(); return; } diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/viewmodel/VisibleSignatureCoordinator.java b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/viewmodel/VisibleSignatureCoordinator.java index b47b99bb..c677a14e 100644 --- a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/viewmodel/VisibleSignatureCoordinator.java +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/viewmodel/VisibleSignatureCoordinator.java @@ -1,5 +1,7 @@ package net.sf.jsignpdf.fx.viewmodel; +import net.sf.jsignpdf.Constants; + /** * Static helpers for moving the visible-signature rectangle between the placement overlay (relative page coords in * {@link SignaturePlacementViewModel}) and the signing configuration (PDF coords in {@link SigningOptionsViewModel}). @@ -44,9 +46,9 @@ public static void pushPlacementToSigning(SignaturePlacementViewModel placementV * {@code MainWindowController#autoPlaceVisibleSignature()}, this always replaces the existing placement rather than * bailing out when one is already present. *

- * No-op if the signing VM has {@code visible=false} or the coordinates do not describe a meaningful rectangle - * (non-positive dimensions or out-of-page bounds). The latter guard means callers can pass stale default coordinates - * without corrupting the placement — the user just has to place the rectangle themselves. + * No-op if the signing VM has {@code visible=false} or {@link #hasUsablePosition} rejects the coordinates. The + * latter guard means callers can pass stale default coordinates without corrupting the placement — the user just + * has to place the rectangle themselves. */ public static void pushSigningToPlacement(SigningOptionsViewModel signingVM, SignaturePlacementViewModel placementVM, @@ -58,12 +60,37 @@ public static void pushSigningToPlacement(SigningOptionsViewModel signingVM, float lly = signingVM.positionLLYProperty().get(); float urx = signingVM.positionURXProperty().get(); float ury = signingVM.positionURYProperty().get(); - boolean fits = urx - llx > 1f && ury - lly > 1f - && llx >= 0f && lly >= 0f - && urx <= pageWidth && ury <= pageHeight; - if (!fits) { + if (!hasUsablePosition(llx, lly, urx, ury, pageWidth, pageHeight)) { return; } placementVM.fromPdfCoordinates(llx, lly, urx, ury, pageWidth, pageHeight); } + + /** + * Decides whether the given PDF coordinates are a position worth restoring on screen, i.e. a rectangle with + * positive dimensions that sits inside the page. + *

+ * The untouched default rectangle ({@code DEFVAL_LLX..DEFVAL_URY}) is rejected even though it technically fits: + * it is what a fresh profile starts with, and what selecting an existing signature field writes back, so it means + * "no position chosen yet" rather than "put a 100×100 box in the lower-left corner". Callers fall back to their + * own default placement instead. + * + * @param llx lower-left X in PDF points + * @param lly lower-left Y in PDF points + * @param urx upper-right X in PDF points + * @param ury upper-right Y in PDF points + * @param pageWidth width of the target page in PDF units + * @param pageHeight height of the target page in PDF units + * @return true when the rectangle can be shown as-is + */ + public static boolean hasUsablePosition(float llx, float lly, float urx, float ury, + float pageWidth, float pageHeight) { + if (llx == Constants.DEFVAL_LLX && lly == Constants.DEFVAL_LLY + && urx == Constants.DEFVAL_URX && ury == Constants.DEFVAL_URY) { + return false; + } + return urx - llx > 1f && ury - lly > 1f + && llx >= 0f && lly >= 0f + && urx <= pageWidth && ury <= pageHeight; + } } diff --git a/jsignpdf/src/test/java/net/sf/jsignpdf/fx/viewmodel/VisibleSignatureCoordinatorTest.java b/jsignpdf/src/test/java/net/sf/jsignpdf/fx/viewmodel/VisibleSignatureCoordinatorTest.java index e03d9ddf..ee4bd175 100644 --- a/jsignpdf/src/test/java/net/sf/jsignpdf/fx/viewmodel/VisibleSignatureCoordinatorTest.java +++ b/jsignpdf/src/test/java/net/sf/jsignpdf/fx/viewmodel/VisibleSignatureCoordinatorTest.java @@ -6,6 +6,8 @@ import org.junit.Test; +import net.sf.jsignpdf.Constants; + /** * Tests for {@link VisibleSignatureCoordinator} — the placement↔signing view-model sync used by the preset save/load flows * and the sign/close flows. @@ -128,6 +130,54 @@ public void pushSigning_noOp_whenCoordsAreDegenerate() { assertFalse(pvm.isPlaced()); } + /** + * The default rectangle fits any page, but it is what a fresh profile carries and what selecting an existing + * signature field writes back - restoring it would drop a 100×100 box in the lower-left corner of the page. + */ + @Test + public void pushSigning_noOp_whenCoordsAreTheUntouchedDefaults() { + SigningOptionsViewModel svm = new SigningOptionsViewModel(); + svm.visibleProperty().set(true); + svm.positionLLXProperty().set(Constants.DEFVAL_LLX); + svm.positionLLYProperty().set(Constants.DEFVAL_LLY); + svm.positionURXProperty().set(Constants.DEFVAL_URX); + svm.positionURYProperty().set(Constants.DEFVAL_URY); + SignaturePlacementViewModel pvm = new SignaturePlacementViewModel(); + + VisibleSignatureCoordinator.pushSigningToPlacement(svm, pvm, PAGE_W, PAGE_H); + + assertFalse("the default rectangle means 'no position chosen yet'", pvm.isPlaced()); + } + + // ---- hasUsablePosition ---- + + @Test + public void hasUsablePosition_acceptsARectangleInsideThePage() { + assertTrue(VisibleSignatureCoordinator.hasUsablePosition(120f, 560f, 180f, 640f, PAGE_W, PAGE_H)); + } + + @Test + public void hasUsablePosition_rejectsTheDefaultRectangle() { + assertFalse(VisibleSignatureCoordinator.hasUsablePosition(Constants.DEFVAL_LLX, Constants.DEFVAL_LLY, + Constants.DEFVAL_URX, Constants.DEFVAL_URY, PAGE_W, PAGE_H)); + } + + /** Only the untouched defaults are special - the same box moved anywhere else is a real user choice. */ + @Test + public void hasUsablePosition_acceptsTheDefaultSizedRectangleElsewhere() { + assertTrue(VisibleSignatureCoordinator.hasUsablePosition(Constants.DEFVAL_LLX, Constants.DEFVAL_LLY + 10f, + Constants.DEFVAL_URX, Constants.DEFVAL_URY + 10f, PAGE_W, PAGE_H)); + } + + @Test + public void hasUsablePosition_rejectsDegenerateAndOutOfPageRectangles() { + assertFalse("zero width", VisibleSignatureCoordinator.hasUsablePosition(10f, 10f, 10f, 80f, PAGE_W, PAGE_H)); + assertFalse("beyond the page", VisibleSignatureCoordinator.hasUsablePosition(10f, 10f, PAGE_W + 1f, 80f, + PAGE_W, PAGE_H)); + assertFalse("negative origin", VisibleSignatureCoordinator.hasUsablePosition(-5f, 10f, 80f, 80f, + PAGE_W, PAGE_H)); + } + @Test public void pushSigning_replacesExistingPlacement() { // Load-preset semantics: even if a rectangle is already placed, it gets replaced with the preset's coords. diff --git a/website/docs/JSignPdf.adoc b/website/docs/JSignPdf.adoc index 89cb8d80..3a08869a 100644 --- a/website/docs/JSignPdf.adoc +++ b/website/docs/JSignPdf.adoc @@ -882,8 +882,11 @@ fills their own box. JSignPdf can sign into such a field instead of creating a n In the JavaFX UI, the _Signature field_ combo box at the top of the _Signature Appearance_ panel lists the empty signature fields of the opened document. Pick one and the signature goes there; the field is marked on the page preview and the position controls are switched off, because the field's own rectangle decides where -the signature lands. The combo stays on _(create new field)_ for documents without empty fields, and the -selection is reset whenever you open another document. +the signature lands. _Visible signature_ is turned on and locked (in the panel, the toolbar and the menu +alike) for as long as a field is selected, because the appearance is always drawn into the field -- switch +back to _(create new field)_ if you want an invisible signature, or pick a field whose rectangle has zero +size, which gives one anyway. The combo stays on _(create new field)_ for documents without empty fields, and +the selection is reset whenever you open another document. On the command line, `-lsf` shows what a document offers: