fix(pdf): estabiliza drag do selo, remove razao/local e elimina reticencias em botoes e avisos

This commit is contained in:
Claudecio Martins
2026-10-11 10:17:44 -03:00
parent 7f6d8a4a08
commit c997afbfd4
3 changed files with 143 additions and 73 deletions
@@ -105,13 +105,16 @@ public final class PdfCertificateSignDialog {
this.stage.initOwner(owner); this.stage.initOwner(owner);
} }
this.stage.setResizable(true); this.stage.setResizable(true);
this.stage.setMinWidth(620); this.stage.setMinWidth(700);
this.stage.setMinHeight(460); this.stage.setMinHeight(490);
this.stage.setTitle("Seleção de Certificado Digital — CyberCore Signer"); this.stage.setTitle("Seleção de Certificado Digital — CyberCore Signer");
initLayout(); initLayout();
} }
private HBox windowsStoreBox;
private HBox pinRow;
private void initLayout() { private void initLayout() {
VBox root = new VBox(14); VBox root = new VBox(14);
root.setPadding(new Insets(18, 20, 18, 20)); root.setPadding(new Insets(18, 20, 18, 20));
@@ -146,11 +149,13 @@ public final class PdfCertificateSignDialog {
lblDocTitle.setStyle("-fx-font-weight: bold; -fx-text-fill: -fx-text-secondary; -fx-font-size: 11.5px;"); lblDocTitle.setStyle("-fx-font-weight: bold; -fx-text-fill: -fx-text-secondary; -fx-font-size: 11.5px;");
Label lblDocValue = new Label(sourcePdfFile.getName() + " (" + (sourcePdfFile.length() / 1024) + " KB)"); Label lblDocValue = new Label(sourcePdfFile.getName() + " (" + (sourcePdfFile.length() / 1024) + " KB)");
lblDocValue.setStyle("-fx-font-weight: bold; -fx-text-fill: -fx-text-primary; -fx-font-size: 11.5px;"); lblDocValue.setStyle("-fx-font-weight: bold; -fx-text-fill: -fx-text-primary; -fx-font-size: 11.5px;");
lblDocValue.setWrapText(true);
Label lblSealTitle = new Label("Selo Visual:"); Label lblSealTitle = new Label("Selo Visual:");
lblSealTitle.setStyle("-fx-font-weight: bold; -fx-text-fill: -fx-text-secondary; -fx-font-size: 11.5px;"); lblSealTitle.setStyle("-fx-font-weight: bold; -fx-text-fill: -fx-text-secondary; -fx-font-size: 11.5px;");
Label lblSealValue = new Label(sealSummaryText); Label lblSealValue = new Label(sealSummaryText);
lblSealValue.setStyle("-fx-text-fill: #0284c7; -fx-font-weight: bold; -fx-font-size: 11.5px;"); lblSealValue.setStyle("-fx-text-fill: #0284c7; -fx-font-weight: bold; -fx-font-size: 11.5px;");
lblSealValue.setWrapText(true);
summaryGrid.add(lblDocTitle, 0, 0); summaryGrid.add(lblDocTitle, 0, 0);
summaryGrid.add(lblDocValue, 1, 0); summaryGrid.add(lblDocValue, 1, 0);
@@ -209,7 +214,19 @@ public final class PdfCertificateSignDialog {
} }
}); });
HBox pinRow = new HBox(10); // Banner informativo para certificados da Loja do Windows (sem PIN)
windowsStoreBox = new HBox(8);
windowsStoreBox.setAlignment(Pos.CENTER_LEFT);
windowsStoreBox.setPadding(new Insets(10, 12, 10, 12));
windowsStoreBox.setStyle("-fx-background-color: #ecfdf5; -fx-border-color: #a7f3d0; -fx-border-radius: 6px; -fx-background-radius: 6px;");
Label lblWinNotice = new Label("✓ Certificado instalado no computador — Gerenciado nativamente pelo Windows (PIN não necessário)");
lblWinNotice.setStyle("-fx-text-fill: #065f46; -fx-font-weight: bold; -fx-font-size: 11.5px;");
lblWinNotice.setWrapText(true);
HBox.setHgrow(lblWinNotice, Priority.ALWAYS);
windowsStoreBox.getChildren().add(lblWinNotice);
// Linha de PIN / Senha para Tokens A3 e Arquivos A1
pinRow = new HBox(10);
pinRow.setAlignment(Pos.CENTER_LEFT); pinRow.setAlignment(Pos.CENTER_LEFT);
lblPinTitle = new Label("PIN / Senha:"); lblPinTitle = new Label("PIN / Senha:");
lblPinTitle.getStyleClass().add("dialog-field-title"); lblPinTitle.getStyleClass().add("dialog-field-title");
@@ -232,6 +249,8 @@ public final class PdfCertificateSignDialog {
lblPinNotice = new Label(); lblPinNotice = new Label();
lblPinNotice.setStyle("-fx-font-size: 11.5px;"); lblPinNotice.setStyle("-fx-font-size: 11.5px;");
lblPinNotice.setWrapText(true);
HBox.setHgrow(lblPinNotice, Priority.ALWAYS);
pinRow.getChildren().addAll(lblPinTitle, pinField, btnClearRememberedPin, lblPinNotice); pinRow.getChildren().addAll(lblPinTitle, pinField, btnClearRememberedPin, lblPinNotice);
chkRememberPin = new CheckBox("Lembrar PIN/senha com segurança neste computador"); chkRememberPin = new CheckBox("Lembrar PIN/senha com segurança neste computador");
@@ -240,12 +259,14 @@ public final class PdfCertificateSignDialog {
VBox tipBox = new VBox(2); VBox tipBox = new VBox(2);
tipBox.setPadding(new Insets(8, 10, 8, 10)); tipBox.setPadding(new Insets(8, 10, 8, 10));
tipBox.setStyle("-fx-background-color: -fx-bg-primary; -fx-border-color: -fx-border-light; -fx-border-radius: 6px;"); tipBox.setStyle("-fx-background-color: -fx-bg-primary; -fx-border-color: -fx-border-light; -fx-border-radius: 6px; -fx-background-radius: 6px;");
Label lblTip = new Label("🔒 Segurança: O PIN é criptografado localmente com AES-256-GCM vinculado à máquina e nunca é transmitido na rede."); Label lblTip = new Label("🔒 Segurança: O PIN é criptografado localmente com AES-256-GCM vinculado à máquina e nunca é transmitido na rede.");
lblTip.setStyle("-fx-font-size: 11px; -fx-text-fill: -fx-text-secondary;"); lblTip.setStyle("-fx-font-size: 11px; -fx-text-fill: -fx-text-secondary;");
lblTip.setWrapText(true);
lblTip.setMaxWidth(Double.MAX_VALUE);
tipBox.getChildren().add(lblTip); tipBox.getChildren().add(lblTip);
certSection.getChildren().addAll(certHeader, certCombo, pinRow, chkRememberPin, tipBox); certSection.getChildren().addAll(certHeader, certCombo, windowsStoreBox, pinRow, chkRememberPin, tipBox);
// 3. Rodapé // 3. Rodapé
HBox footer = new HBox(12); HBox footer = new HBox(12);
@@ -266,8 +287,9 @@ public final class PdfCertificateSignDialog {
btnCancel.setOnAction(e -> stage.close()); btnCancel.setOnAction(e -> stage.close());
btnSign = new Button("Assinar Documento PDF"); btnSign = new Button("Assinar Documento PDF");
btnSign.getStyleClass().addAll("btn-primary", "btn-dialog"); btnSign.getStyleClass().addAll("btn-primary", "btn-dialog-wide");
btnSign.setMinWidth(220); btnSign.setMinWidth(220);
btnSign.setPrefWidth(Region.USE_COMPUTED_SIZE);
btnSign.setDefaultButton(true); btnSign.setDefaultButton(true);
btnSign.setOnAction(e -> handleSign()); btnSign.setOnAction(e -> handleSign());
@@ -275,7 +297,7 @@ public final class PdfCertificateSignDialog {
root.getChildren().addAll(header, summaryBox, certSection, footer); root.getChildren().addAll(header, summaryBox, certSection, footer);
Scene scene = new Scene(root, 650, 480); Scene scene = new Scene(root, 720, 500);
ThemeManager.applyTheme(scene); ThemeManager.applyTheme(scene);
stage.setScene(scene); stage.setScene(scene);
@@ -300,31 +322,40 @@ public final class PdfCertificateSignDialog {
pinField.setDisable(true); pinField.setDisable(true);
lblPinNotice.setText(""); lblPinNotice.setText("");
chkRememberPin.setVisible(false); chkRememberPin.setVisible(false);
chkRememberPin.setManaged(false);
btnClearRememberedPin.setVisible(false); btnClearRememberedPin.setVisible(false);
windowsStoreBox.setVisible(false);
windowsStoreBox.setManaged(false);
pinRow.setVisible(true);
pinRow.setManaged(true);
return; return;
} }
boolean isWindowsStore = "provider-win-store".equalsIgnoreCase(cert.providerId()); boolean isWindowsStore = "provider-win-store".equalsIgnoreCase(cert.providerId());
if (isWindowsStore) { if (isWindowsStore) {
// Certificado instalado no repositório do Windows (não exige PIN no aplicativo) // Certificado instalado no repositório do Windows (não exige PIN no aplicativo)
pinField.setVisible(false); windowsStoreBox.setVisible(true);
lblPinTitle.setVisible(false); windowsStoreBox.setManaged(true);
pinRow.setVisible(false);
pinRow.setManaged(false);
chkRememberPin.setVisible(false); chkRememberPin.setVisible(false);
chkRememberPin.setManaged(false);
btnClearRememberedPin.setVisible(false); btnClearRememberedPin.setVisible(false);
lblPinNotice.setText("✓ Certificado instalado no computador — Gerenciado pelo Windows (PIN não necessário)");
lblPinNotice.setStyle("-fx-text-fill: #10b981; -fx-font-weight: bold; -fx-font-size: 11.5px;");
return; return;
} }
// Token USB (PKCS#11) ou Arquivo A1 (PKCS#12) // Token USB (PKCS#11) ou Arquivo A1 (PKCS#12)
pinField.setVisible(true); windowsStoreBox.setVisible(false);
lblPinTitle.setVisible(true); windowsStoreBox.setManaged(false);
pinRow.setVisible(true);
pinRow.setManaged(true);
char[] storedPin = SecurePinStorage.getInstance().getPin(cert.id()); char[] storedPin = SecurePinStorage.getInstance().getPin(cert.id());
if (storedPin != null && storedPin.length > 0) { if (storedPin != null && storedPin.length > 0) {
pinField.setText(new String(storedPin)); pinField.setText(new String(storedPin));
pinField.setDisable(true); pinField.setDisable(true);
chkRememberPin.setVisible(false); chkRememberPin.setVisible(false);
chkRememberPin.setManaged(false);
btnClearRememberedPin.setVisible(true); btnClearRememberedPin.setVisible(true);
lblPinNotice.setText("✓ PIN lembrado com segurança neste computador"); lblPinNotice.setText("✓ PIN lembrado com segurança neste computador");
lblPinNotice.setStyle("-fx-text-fill: #10b981; -fx-font-weight: bold; -fx-font-size: 11.5px;"); lblPinNotice.setStyle("-fx-text-fill: #10b981; -fx-font-weight: bold; -fx-font-size: 11.5px;");
@@ -332,6 +363,7 @@ public final class PdfCertificateSignDialog {
pinField.clear(); pinField.clear();
pinField.setDisable(false); pinField.setDisable(false);
chkRememberPin.setVisible(true); chkRememberPin.setVisible(true);
chkRememberPin.setManaged(true);
btnClearRememberedPin.setVisible(false); btnClearRememberedPin.setVisible(false);
boolean isA3 = cert.type() == CertificateType.A3; boolean isA3 = cert.type() == CertificateType.A3;
lblPinNotice.setText(isA3 ? "Requer PIN do token" : "Requer senha do certificado"); lblPinNotice.setText(isA3 ? "Requer PIN do token" : "Requer senha do certificado");
@@ -18,6 +18,7 @@ import javafx.application.Platform;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.concurrent.Task; import javafx.concurrent.Task;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Cursor; import javafx.scene.Cursor;
import javafx.scene.Scene; import javafx.scene.Scene;
@@ -67,6 +68,9 @@ public final class PdfSignDialog {
private static final Logger logger = LoggerFactory.getLogger(PdfSignDialog.class); private static final Logger logger = LoggerFactory.getLogger(PdfSignDialog.class);
private static final String DEFAULT_REASON = "Assinatura Digital ICP-Brasil";
private static final String DEFAULT_LOCATION = "Brasil";
private final Stage stage; private final Stage stage;
private final Stage ownerStage; private final Stage ownerStage;
private final CertificateManager certManager; private final CertificateManager certManager;
@@ -94,11 +98,10 @@ public final class PdfSignDialog {
private float currentPagePdfWidth = 595.28f; private float currentPagePdfWidth = 595.28f;
private float currentPagePdfHeight = 841.89f; private float currentPagePdfHeight = 841.89f;
// Controle de arraste suave (drag & drop) sem jitter/flicker // Controle de arraste suave, milimétrico e estável (drag & drop)
private double dragStartSceneX; private double dragStartOffsetInSealX;
private double dragStartSceneY; private double dragStartOffsetInSealY;
private double initialSealLayoutX; private boolean sealDragOccurred = false;
private double initialSealLayoutY;
// Coordenadas PDF (pontos tipográficos nativos do PDF) // Coordenadas PDF (pontos tipográficos nativos do PDF)
private Float customPdfX = null; private Float customPdfX = null;
@@ -122,8 +125,6 @@ public final class PdfSignDialog {
private CheckBox chkVisibleSeal; private CheckBox chkVisibleSeal;
private ComboBox<PositionOption> positionCombo; private ComboBox<PositionOption> positionCombo;
private ComboBox<PageOption> pageCombo; private ComboBox<PageOption> pageCombo;
private TextField txtReason;
private TextField txtLocation;
private ImageView sealPreviewImageView; private ImageView sealPreviewImageView;
// Visualizador Interativo da Folha // Visualizador Interativo da Folha
@@ -335,21 +336,21 @@ public final class PdfSignDialog {
sealPreviewImageView.setFitHeight(75); sealPreviewImageView.setFitHeight(75);
previewBox.getChildren().add(sealPreviewImageView); previewBox.getChildren().add(sealPreviewImageView);
// Opções Adicionais Rápidas // Opções Rápidas de Posição e Página
GridPane grid = new GridPane(); GridPane grid = new GridPane();
grid.setHgap(8); grid.setHgap(8);
grid.setVgap(8); grid.setVgap(8);
ColumnConstraints colLabel = new ColumnConstraints(); ColumnConstraints colLabel = new ColumnConstraints();
colLabel.setMinWidth(90); colLabel.setMinWidth(75);
colLabel.setPrefWidth(95); colLabel.setPrefWidth(80);
colLabel.setHgrow(Priority.NEVER); colLabel.setHgrow(Priority.NEVER);
ColumnConstraints colField = new ColumnConstraints(); ColumnConstraints colField = new ColumnConstraints();
colField.setHgrow(Priority.ALWAYS); colField.setHgrow(Priority.ALWAYS);
grid.getColumnConstraints().addAll(colLabel, colField); grid.getColumnConstraints().addAll(colLabel, colField);
Label lblPosition = new Label("Posição Fixa:"); Label lblPosition = new Label("Posição:");
lblPosition.getStyleClass().add("dialog-field-title"); lblPosition.getStyleClass().add("dialog-field-title");
positionCombo = new ComboBox<>(FXCollections.observableArrayList(PositionOption.values())); positionCombo = new ComboBox<>(FXCollections.observableArrayList(PositionOption.values()));
positionCombo.setMaxWidth(Double.MAX_VALUE); positionCombo.setMaxWidth(Double.MAX_VALUE);
@@ -362,28 +363,23 @@ public final class PdfSignDialog {
pageCombo.setMaxWidth(Double.MAX_VALUE); pageCombo.setMaxWidth(Double.MAX_VALUE);
pageCombo.setValue(PageOption.LAST_PAGE); pageCombo.setValue(PageOption.LAST_PAGE);
Label lblReason = new Label("Razão:");
lblReason.getStyleClass().add("dialog-field-title");
txtReason = new TextField("Assinatura Digital ICP-Brasil");
txtReason.textProperty().addListener((obs, oldV, newV) -> updateSealPreview());
Label lblLocation = new Label("Local:");
lblLocation.getStyleClass().add("dialog-field-title");
txtLocation = new TextField("Brasil");
txtLocation.textProperty().addListener((obs, oldV, newV) -> updateSealPreview());
grid.add(lblPosition, 0, 0); grid.add(lblPosition, 0, 0);
grid.add(positionCombo, 1, 0); grid.add(positionCombo, 1, 0);
grid.add(lblPage, 0, 1); grid.add(lblPage, 0, 1);
grid.add(pageCombo, 1, 1); grid.add(pageCombo, 1, 1);
grid.add(lblReason, 0, 2);
grid.add(txtReason, 1, 2); // Dica amigável de posicionamento livre
grid.add(lblLocation, 0, 3); VBox dragTipBox = new VBox(2);
grid.add(txtLocation, 1, 3); dragTipBox.setPadding(new Insets(8, 10, 8, 10));
dragTipBox.setStyle("-fx-background-color: -fx-bg-primary; -fx-border-color: -fx-border-light; -fx-border-radius: 6px; -fx-background-radius: 6px;");
Label lblDragTip = new Label("💡 Arraste o selo livremente sobre a folha para posicioná-lo onde desejar.");
lblDragTip.setStyle("-fx-font-size: 11px; -fx-text-fill: -fx-text-secondary;");
lblDragTip.setWrapText(true);
dragTipBox.getChildren().add(lblDragTip);
reloadActiveDefaultTheme(); reloadActiveDefaultTheme();
sealSection.getChildren().addAll(sealHeader, defaultSealCard, previewBox, grid); sealSection.getChildren().addAll(sealHeader, defaultSealCard, previewBox, grid, dragTipBox);
return sealSection; return sealSection;
} }
@@ -511,7 +507,15 @@ public final class PdfSignDialog {
interactiveOverlayPane = new Pane(); interactiveOverlayPane = new Pane();
interactiveOverlayPane.setCursor(Cursor.CROSSHAIR); interactiveOverlayPane.setCursor(Cursor.CROSSHAIR);
interactiveOverlayPane.setOnMouseClicked(this::handleSheetClick); interactiveOverlayPane.setOnMouseClicked(event -> {
if (sealDragOccurred) {
// Previne que a soltura do mouse no término do arraste cause clique residual
sealDragOccurred = false;
event.consume();
return;
}
handleSheetClick(event);
});
sealStampOverlayView = new ImageView(); sealStampOverlayView = new ImageView();
sealStampOverlayView.setPreserveRatio(true); sealStampOverlayView.setPreserveRatio(true);
@@ -520,20 +524,33 @@ public final class PdfSignDialog {
sealStampOverlayView.setEffect(sealShadow); sealStampOverlayView.setEffect(sealShadow);
sealStampOverlayView.setVisible(false); sealStampOverlayView.setVisible(false);
// Arraste 100% suave com âncora fixa na cena (elimina totalmente o tremor/flicker) // Arraste com ancoragem precisa no ponto exato do clique dentro do selo (sem salto e sem drift)
sealStampOverlayView.setOnMousePressed(event -> { sealStampOverlayView.setOnMousePressed(event -> {
dragStartSceneX = event.getSceneX(); dragStartOffsetInSealX = event.getX();
dragStartSceneY = event.getSceneY(); dragStartOffsetInSealY = event.getY();
initialSealLayoutX = sealStampOverlayView.getLayoutX(); sealDragOccurred = false;
initialSealLayoutY = sealStampOverlayView.getLayoutY();
event.consume(); event.consume();
}); });
sealStampOverlayView.setOnMouseDragged(event -> { sealStampOverlayView.setOnMouseDragged(event -> {
if (!chkVisibleSeal.isSelected() || selectedPdfBytes == null) return; if (!chkVisibleSeal.isSelected() || selectedPdfBytes == null) return;
double deltaX = event.getSceneX() - dragStartSceneX; sealDragOccurred = true;
double deltaY = event.getSceneY() - dragStartSceneY;
updateSealPositionFromDrag(initialSealLayoutX + deltaX, initialSealLayoutY + deltaY); // Converte coordenada da cena para coordenadas locais do painel da folha
Point2D localInSheet = interactiveOverlayPane.sceneToLocal(event.getSceneX(), event.getSceneY());
double targetX = localInSheet.getX() - dragStartOffsetInSealX;
double targetY = localInSheet.getY() - dragStartOffsetInSealY;
updateSealPositionFromDrag(targetX, targetY);
event.consume();
});
sealStampOverlayView.setOnMouseReleased(event -> {
// Garante que o evento de soltura do mouse não borbulhe para a folha
event.consume();
});
sealStampOverlayView.setOnMouseClicked(event -> {
sealDragOccurred = false;
event.consume(); event.consume();
}); });
@@ -752,15 +769,17 @@ public final class PdfSignDialog {
sealStampOverlayView.setLayoutY(clampedY); sealStampOverlayView.setLayoutY(clampedY);
sealStampOverlayView.setVisible(true); sealStampOverlayView.setVisible(true);
this.sealRelativeX = clampedX / sheetW; this.sealRelativeX = (sheetW > 0) ? (clampedX / sheetW) : 0;
this.sealRelativeY = clampedY / sheetH; this.sealRelativeY = (sheetH > 0) ? (clampedY / sheetH) : 0;
PdfSealTheme theme = activeSealTheme != null ? activeSealTheme : PdfSealThemeRegistry.getDefaultTheme(); PdfSealTheme theme = activeSealTheme != null ? activeSealTheme : PdfSealThemeRegistry.getDefaultTheme();
float sealPdfW = (float) ((theme != null) ? theme.width() * 0.72 : 144.0); float sealPdfW = (float) ((theme != null) ? theme.width() * 0.72 : 144.0);
float sealPdfH = (float) ((theme != null) ? theme.height() * 0.72 : 44.0); float sealPdfH = (float) ((theme != null) ? theme.height() * 0.72 : 44.0);
float calculatedPdfX = (float) (sealRelativeX * currentPagePdfWidth); // Conversão direta e exata da escala visual na tela para os pontos do PDF
float calculatedPdfY = (float) (currentPagePdfHeight - (sealRelativeY * currentPagePdfHeight) - sealPdfH); double scale = (currentPagePdfWidth > 0) ? (sheetW / currentPagePdfWidth) : 1.0;
float calculatedPdfX = (float) (clampedX / scale);
float calculatedPdfY = (float) (currentPagePdfHeight - (clampedY / scale) - sealPdfH);
this.customPdfX = Math.max(0, Math.min(calculatedPdfX, currentPagePdfWidth - sealPdfW)); this.customPdfX = Math.max(0, Math.min(calculatedPdfX, currentPagePdfWidth - sealPdfW));
this.customPdfY = Math.max(0, Math.min(calculatedPdfY, currentPagePdfHeight - sealPdfH)); this.customPdfY = Math.max(0, Math.min(calculatedPdfY, currentPagePdfHeight - sealPdfH));
@@ -779,29 +798,50 @@ public final class PdfSignDialog {
return; return;
} }
isCustomPositionActive = false; PdfSealTheme theme = activeSealTheme != null ? activeSealTheme : PdfSealThemeRegistry.getDefaultTheme();
float sealPdfW = (float) ((theme != null) ? theme.width() * 0.72 : 144.0);
float sealPdfH = (float) ((theme != null) ? theme.height() * 0.72 : 44.0);
float margin = 20f;
float targetPdfX;
float targetPdfY;
switch (option) { switch (option) {
case BOTTOM_LEFT -> { case TOP_LEFT -> {
this.sealRelativeX = 0.05; targetPdfX = margin;
this.sealRelativeY = 0.88; targetPdfY = currentPagePdfHeight - sealPdfH - margin;
} }
case TOP_RIGHT -> { case TOP_RIGHT -> {
this.sealRelativeX = 0.70; targetPdfX = currentPagePdfWidth - sealPdfW - margin;
this.sealRelativeY = 0.05; targetPdfY = currentPagePdfHeight - sealPdfH - margin;
} }
case TOP_LEFT -> { case BOTTOM_LEFT -> {
this.sealRelativeX = 0.05; targetPdfX = margin;
this.sealRelativeY = 0.05; targetPdfY = margin;
} }
default -> { // BOTTOM_RIGHT default -> { // BOTTOM_RIGHT
this.sealRelativeX = 0.70; targetPdfX = currentPagePdfWidth - sealPdfW - margin;
this.sealRelativeY = 0.88; targetPdfY = margin;
} }
} }
this.customPdfX = Math.max(0, Math.min(targetPdfX, currentPagePdfWidth - sealPdfW));
this.customPdfY = Math.max(0, Math.min(targetPdfY, currentPagePdfHeight - sealPdfH));
this.customPageNumber = currentPageIndex + 1;
this.isCustomPositionActive = true;
// Projeta as coordenadas calculadas na tela de visualização
double sheetW = currentBaseSheetWidth * zoomFactor;
double sheetH = currentBaseSheetHeight * zoomFactor;
double scale = (currentPagePdfWidth > 0) ? (sheetW / currentPagePdfWidth) : 1.0;
double screenX = customPdfX * scale;
double screenY = (currentPagePdfHeight - customPdfY - sealPdfH) * scale;
this.sealRelativeX = (sheetW > 0) ? (screenX / sheetW) : 0;
this.sealRelativeY = (sheetH > 0) ? (screenY / sheetH) : 0;
repositionSealByRelativeCoords(); repositionSealByRelativeCoords();
lblPositionFeedback.setText("Posição fixa: " + option.label); lblPositionFeedback.setText("📍 " + option.label + " (Pág. " + (currentPageIndex + 1) + ")");
} }
private void handlePositionComboSelection() { private void handlePositionComboSelection() {
@@ -821,8 +861,6 @@ public final class PdfSignDialog {
private void toggleVisibleSeal(boolean active) { private void toggleVisibleSeal(boolean active) {
positionCombo.setDisable(!active); positionCombo.setDisable(!active);
pageCombo.setDisable(!active); pageCombo.setDisable(!active);
txtReason.setDisable(!active);
txtLocation.setDisable(!active);
sealPreviewImageView.setOpacity(active ? 1.0 : 0.3); sealPreviewImageView.setOpacity(active ? 1.0 : 0.3);
sealStampOverlayView.setVisible(active && selectedPdfBytes != null); sealStampOverlayView.setVisible(active && selectedPdfBytes != null);
} }
@@ -831,8 +869,8 @@ public final class PdfSignDialog {
try { try {
PdfSealTheme theme = activeSealTheme != null ? activeSealTheme : PdfSealThemeRegistry.getDefaultTheme(); PdfSealTheme theme = activeSealTheme != null ? activeSealTheme : PdfSealThemeRegistry.getDefaultTheme();
String reason = (txtReason != null && !txtReason.getText().isBlank()) ? txtReason.getText() : "Assinatura Digital ICP-Brasil"; String reason = DEFAULT_REASON;
String location = (txtLocation != null && !txtLocation.getText().isBlank()) ? txtLocation.getText() : "Brasil"; String location = DEFAULT_LOCATION;
CpfDisplayMode cpfMode = theme.defaultCpfMode() != null ? theme.defaultCpfMode() : CpfDisplayMode.FULL; CpfDisplayMode cpfMode = theme.defaultCpfMode() != null ? theme.defaultCpfMode() : CpfDisplayMode.FULL;
boolean includeDt = theme.defaultIncludeDateTime(); boolean includeDt = theme.defaultIncludeDateTime();
String customInfo = theme.formatCustomField(); String customInfo = theme.formatCustomField();
@@ -888,8 +926,8 @@ public final class PdfSignDialog {
customPdfX, customPdfX,
customPdfY, customPdfY,
customPageNumber, customPageNumber,
txtReason.getText(), DEFAULT_REASON,
txtLocation.getText() DEFAULT_LOCATION
); );
String sealSummary; String sealSummary;
@@ -933,14 +933,14 @@
/* Botões Padronizados para Diálogos e Modais */ /* Botões Padronizados para Diálogos e Modais */
.btn-dialog { .btn-dialog {
-fx-min-width: 110px; -fx-min-width: 110px;
-fx-pref-width: 110px; -fx-pref-width: -fx-pref-size;
-fx-pref-height: 34px; -fx-pref-height: 34px;
-fx-alignment: center; -fx-alignment: center;
} }
.btn-dialog-wide { .btn-dialog-wide {
-fx-min-width: 230px; -fx-min-width: 220px;
-fx-pref-width: 230px; -fx-pref-width: -fx-pref-size;
-fx-pref-height: 36px; -fx-pref-height: 36px;
-fx-alignment: center; -fx-alignment: center;
} }