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.setResizable(true);
this.stage.setMinWidth(620);
this.stage.setMinHeight(460);
this.stage.setMinWidth(700);
this.stage.setMinHeight(490);
this.stage.setTitle("Seleção de Certificado Digital — CyberCore Signer");
initLayout();
}
private HBox windowsStoreBox;
private HBox pinRow;
private void initLayout() {
VBox root = new VBox(14);
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;");
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.setWrapText(true);
Label lblSealTitle = new Label("Selo Visual:");
lblSealTitle.setStyle("-fx-font-weight: bold; -fx-text-fill: -fx-text-secondary; -fx-font-size: 11.5px;");
Label lblSealValue = new Label(sealSummaryText);
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(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);
lblPinTitle = new Label("PIN / Senha:");
lblPinTitle.getStyleClass().add("dialog-field-title");
@@ -232,6 +249,8 @@ public final class PdfCertificateSignDialog {
lblPinNotice = new Label();
lblPinNotice.setStyle("-fx-font-size: 11.5px;");
lblPinNotice.setWrapText(true);
HBox.setHgrow(lblPinNotice, Priority.ALWAYS);
pinRow.getChildren().addAll(lblPinTitle, pinField, btnClearRememberedPin, lblPinNotice);
chkRememberPin = new CheckBox("Lembrar PIN/senha com segurança neste computador");
@@ -240,12 +259,14 @@ public final class PdfCertificateSignDialog {
VBox tipBox = new VBox(2);
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.");
lblTip.setStyle("-fx-font-size: 11px; -fx-text-fill: -fx-text-secondary;");
lblTip.setWrapText(true);
lblTip.setMaxWidth(Double.MAX_VALUE);
tipBox.getChildren().add(lblTip);
certSection.getChildren().addAll(certHeader, certCombo, pinRow, chkRememberPin, tipBox);
certSection.getChildren().addAll(certHeader, certCombo, windowsStoreBox, pinRow, chkRememberPin, tipBox);
// 3. Rodapé
HBox footer = new HBox(12);
@@ -266,8 +287,9 @@ public final class PdfCertificateSignDialog {
btnCancel.setOnAction(e -> stage.close());
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.setPrefWidth(Region.USE_COMPUTED_SIZE);
btnSign.setDefaultButton(true);
btnSign.setOnAction(e -> handleSign());
@@ -275,7 +297,7 @@ public final class PdfCertificateSignDialog {
root.getChildren().addAll(header, summaryBox, certSection, footer);
Scene scene = new Scene(root, 650, 480);
Scene scene = new Scene(root, 720, 500);
ThemeManager.applyTheme(scene);
stage.setScene(scene);
@@ -300,31 +322,40 @@ public final class PdfCertificateSignDialog {
pinField.setDisable(true);
lblPinNotice.setText("");
chkRememberPin.setVisible(false);
chkRememberPin.setManaged(false);
btnClearRememberedPin.setVisible(false);
windowsStoreBox.setVisible(false);
windowsStoreBox.setManaged(false);
pinRow.setVisible(true);
pinRow.setManaged(true);
return;
}
boolean isWindowsStore = "provider-win-store".equalsIgnoreCase(cert.providerId());
if (isWindowsStore) {
// Certificado instalado no repositório do Windows (não exige PIN no aplicativo)
pinField.setVisible(false);
lblPinTitle.setVisible(false);
windowsStoreBox.setVisible(true);
windowsStoreBox.setManaged(true);
pinRow.setVisible(false);
pinRow.setManaged(false);
chkRememberPin.setVisible(false);
chkRememberPin.setManaged(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;
}
// Token USB (PKCS#11) ou Arquivo A1 (PKCS#12)
pinField.setVisible(true);
lblPinTitle.setVisible(true);
windowsStoreBox.setVisible(false);
windowsStoreBox.setManaged(false);
pinRow.setVisible(true);
pinRow.setManaged(true);
char[] storedPin = SecurePinStorage.getInstance().getPin(cert.id());
if (storedPin != null && storedPin.length > 0) {
pinField.setText(new String(storedPin));
pinField.setDisable(true);
chkRememberPin.setVisible(false);
chkRememberPin.setManaged(false);
btnClearRememberedPin.setVisible(true);
lblPinNotice.setText("✓ PIN lembrado com segurança neste computador");
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.setDisable(false);
chkRememberPin.setVisible(true);
chkRememberPin.setManaged(true);
btnClearRememberedPin.setVisible(false);
boolean isA3 = cert.type() == CertificateType.A3;
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.concurrent.Task;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.Scene;
@@ -67,6 +68,9 @@ public final class PdfSignDialog {
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 ownerStage;
private final CertificateManager certManager;
@@ -94,11 +98,10 @@ public final class PdfSignDialog {
private float currentPagePdfWidth = 595.28f;
private float currentPagePdfHeight = 841.89f;
// Controle de arraste suave (drag & drop) sem jitter/flicker
private double dragStartSceneX;
private double dragStartSceneY;
private double initialSealLayoutX;
private double initialSealLayoutY;
// Controle de arraste suave, milimétrico e estável (drag & drop)
private double dragStartOffsetInSealX;
private double dragStartOffsetInSealY;
private boolean sealDragOccurred = false;
// Coordenadas PDF (pontos tipográficos nativos do PDF)
private Float customPdfX = null;
@@ -122,8 +125,6 @@ public final class PdfSignDialog {
private CheckBox chkVisibleSeal;
private ComboBox<PositionOption> positionCombo;
private ComboBox<PageOption> pageCombo;
private TextField txtReason;
private TextField txtLocation;
private ImageView sealPreviewImageView;
// Visualizador Interativo da Folha
@@ -335,21 +336,21 @@ public final class PdfSignDialog {
sealPreviewImageView.setFitHeight(75);
previewBox.getChildren().add(sealPreviewImageView);
// Opções Adicionais Rápidas
// Opções Rápidas de Posição e Página
GridPane grid = new GridPane();
grid.setHgap(8);
grid.setVgap(8);
ColumnConstraints colLabel = new ColumnConstraints();
colLabel.setMinWidth(90);
colLabel.setPrefWidth(95);
colLabel.setMinWidth(75);
colLabel.setPrefWidth(80);
colLabel.setHgrow(Priority.NEVER);
ColumnConstraints colField = new ColumnConstraints();
colField.setHgrow(Priority.ALWAYS);
grid.getColumnConstraints().addAll(colLabel, colField);
Label lblPosition = new Label("Posição Fixa:");
Label lblPosition = new Label("Posição:");
lblPosition.getStyleClass().add("dialog-field-title");
positionCombo = new ComboBox<>(FXCollections.observableArrayList(PositionOption.values()));
positionCombo.setMaxWidth(Double.MAX_VALUE);
@@ -362,28 +363,23 @@ public final class PdfSignDialog {
pageCombo.setMaxWidth(Double.MAX_VALUE);
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(positionCombo, 1, 0);
grid.add(lblPage, 0, 1);
grid.add(pageCombo, 1, 1);
grid.add(lblReason, 0, 2);
grid.add(txtReason, 1, 2);
grid.add(lblLocation, 0, 3);
grid.add(txtLocation, 1, 3);
// Dica amigável de posicionamento livre
VBox dragTipBox = new VBox(2);
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();
sealSection.getChildren().addAll(sealHeader, defaultSealCard, previewBox, grid);
sealSection.getChildren().addAll(sealHeader, defaultSealCard, previewBox, grid, dragTipBox);
return sealSection;
}
@@ -511,7 +507,15 @@ public final class PdfSignDialog {
interactiveOverlayPane = new Pane();
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.setPreserveRatio(true);
@@ -520,20 +524,33 @@ public final class PdfSignDialog {
sealStampOverlayView.setEffect(sealShadow);
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 -> {
dragStartSceneX = event.getSceneX();
dragStartSceneY = event.getSceneY();
initialSealLayoutX = sealStampOverlayView.getLayoutX();
initialSealLayoutY = sealStampOverlayView.getLayoutY();
dragStartOffsetInSealX = event.getX();
dragStartOffsetInSealY = event.getY();
sealDragOccurred = false;
event.consume();
});
sealStampOverlayView.setOnMouseDragged(event -> {
if (!chkVisibleSeal.isSelected() || selectedPdfBytes == null) return;
double deltaX = event.getSceneX() - dragStartSceneX;
double deltaY = event.getSceneY() - dragStartSceneY;
updateSealPositionFromDrag(initialSealLayoutX + deltaX, initialSealLayoutY + deltaY);
sealDragOccurred = true;
// 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();
});
@@ -752,15 +769,17 @@ public final class PdfSignDialog {
sealStampOverlayView.setLayoutY(clampedY);
sealStampOverlayView.setVisible(true);
this.sealRelativeX = clampedX / sheetW;
this.sealRelativeY = clampedY / sheetH;
this.sealRelativeX = (sheetW > 0) ? (clampedX / sheetW) : 0;
this.sealRelativeY = (sheetH > 0) ? (clampedY / sheetH) : 0;
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 calculatedPdfX = (float) (sealRelativeX * currentPagePdfWidth);
float calculatedPdfY = (float) (currentPagePdfHeight - (sealRelativeY * currentPagePdfHeight) - sealPdfH);
// Conversão direta e exata da escala visual na tela para os pontos do PDF
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.customPdfY = Math.max(0, Math.min(calculatedPdfY, currentPagePdfHeight - sealPdfH));
@@ -779,29 +798,50 @@ public final class PdfSignDialog {
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) {
case BOTTOM_LEFT -> {
this.sealRelativeX = 0.05;
this.sealRelativeY = 0.88;
case TOP_LEFT -> {
targetPdfX = margin;
targetPdfY = currentPagePdfHeight - sealPdfH - margin;
}
case TOP_RIGHT -> {
this.sealRelativeX = 0.70;
this.sealRelativeY = 0.05;
targetPdfX = currentPagePdfWidth - sealPdfW - margin;
targetPdfY = currentPagePdfHeight - sealPdfH - margin;
}
case TOP_LEFT -> {
this.sealRelativeX = 0.05;
this.sealRelativeY = 0.05;
case BOTTOM_LEFT -> {
targetPdfX = margin;
targetPdfY = margin;
}
default -> { // BOTTOM_RIGHT
this.sealRelativeX = 0.70;
this.sealRelativeY = 0.88;
targetPdfX = currentPagePdfWidth - sealPdfW - margin;
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();
lblPositionFeedback.setText("Posição fixa: " + option.label);
lblPositionFeedback.setText("📍 " + option.label + " (Pág. " + (currentPageIndex + 1) + ")");
}
private void handlePositionComboSelection() {
@@ -821,8 +861,6 @@ public final class PdfSignDialog {
private void toggleVisibleSeal(boolean active) {
positionCombo.setDisable(!active);
pageCombo.setDisable(!active);
txtReason.setDisable(!active);
txtLocation.setDisable(!active);
sealPreviewImageView.setOpacity(active ? 1.0 : 0.3);
sealStampOverlayView.setVisible(active && selectedPdfBytes != null);
}
@@ -831,8 +869,8 @@ public final class PdfSignDialog {
try {
PdfSealTheme theme = activeSealTheme != null ? activeSealTheme : PdfSealThemeRegistry.getDefaultTheme();
String reason = (txtReason != null && !txtReason.getText().isBlank()) ? txtReason.getText() : "Assinatura Digital ICP-Brasil";
String location = (txtLocation != null && !txtLocation.getText().isBlank()) ? txtLocation.getText() : "Brasil";
String reason = DEFAULT_REASON;
String location = DEFAULT_LOCATION;
CpfDisplayMode cpfMode = theme.defaultCpfMode() != null ? theme.defaultCpfMode() : CpfDisplayMode.FULL;
boolean includeDt = theme.defaultIncludeDateTime();
String customInfo = theme.formatCustomField();
@@ -888,8 +926,8 @@ public final class PdfSignDialog {
customPdfX,
customPdfY,
customPageNumber,
txtReason.getText(),
txtLocation.getText()
DEFAULT_REASON,
DEFAULT_LOCATION
);
String sealSummary;
@@ -933,14 +933,14 @@
/* Botões Padronizados para Diálogos e Modais */
.btn-dialog {
-fx-min-width: 110px;
-fx-pref-width: 110px;
-fx-pref-width: -fx-pref-size;
-fx-pref-height: 34px;
-fx-alignment: center;
}
.btn-dialog-wide {
-fx-min-width: 230px;
-fx-pref-width: 230px;
-fx-min-width: 220px;
-fx-pref-width: -fx-pref-size;
-fx-pref-height: 36px;
-fx-alignment: center;
}