From c997afbfd46756976395bb552926092b5f87fe5b Mon Sep 17 00:00:00 2001 From: Claudecio Martins Date: Sun, 11 Oct 2026 10:17:44 -0300 Subject: [PATCH] fix(pdf): estabiliza drag do selo, remove razao/local e elimina reticencias em botoes e avisos --- .../ui/view/PdfCertificateSignDialog.java | 58 +++++-- .../signer/ui/view/PdfSignDialog.java | 152 +++++++++++------- .../src/main/resources/styles/theme.css | 6 +- 3 files changed, 143 insertions(+), 73 deletions(-) diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/PdfCertificateSignDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/PdfCertificateSignDialog.java index 3bd2575..909e04a 100644 --- a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/PdfCertificateSignDialog.java +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/PdfCertificateSignDialog.java @@ -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"); diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/PdfSignDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/PdfSignDialog.java index c852b37..98518ed 100644 --- a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/PdfSignDialog.java +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/PdfSignDialog.java @@ -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 positionCombo; private ComboBox 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; diff --git a/modules/cybercore-ui/src/main/resources/styles/theme.css b/modules/cybercore-ui/src/main/resources/styles/theme.css index dcb802a..0588ffd 100644 --- a/modules/cybercore-ui/src/main/resources/styles/theme.css +++ b/modules/cybercore-ui/src/main/resources/styles/theme.css @@ -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; }