diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/PdfSigningService.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/PdfSigningService.java
index d895faa..dcc2e01 100644
--- a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/PdfSigningService.java
+++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/PdfSigningService.java
@@ -66,6 +66,8 @@ public class PdfSigningService {
this.certificateManager = Objects.requireNonNull(certificateManager, "CertificateManager não pode ser nulo.");
}
+ public record PageDimensions(float width, float height) {}
+
/**
* Retorna a quantidade total de páginas contidas no documento PDF.
*/
@@ -76,6 +78,21 @@ public class PdfSigningService {
}
}
+ /**
+ * Retorna as dimensões em pontos (pt) da página informada do documento PDF.
+ */
+ public static PageDimensions getPageDimensions(byte[] pdfBytes, int pageIndex0Based) throws IOException {
+ Objects.requireNonNull(pdfBytes, "Bytes do PDF não podem ser nulos.");
+ try (PDDocument doc = Loader.loadPDF(pdfBytes)) {
+ if (pageIndex0Based < 0 || pageIndex0Based >= doc.getNumberOfPages()) {
+ throw new IllegalArgumentException("Índice de página inválido: " + pageIndex0Based + ". Total: " + doc.getNumberOfPages());
+ }
+ PDPage page = doc.getPage(pageIndex0Based);
+ PDRectangle mediaBox = page.getMediaBox();
+ return new PageDimensions(mediaBox.getWidth(), mediaBox.getHeight());
+ }
+ }
+
/**
* Renderiza uma página específica do PDF para BufferedImage na resolução DPI informada.
*/
diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/SecurePinStorage.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/SecurePinStorage.java
new file mode 100644
index 0000000..12f1b0b
--- /dev/null
+++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/SecurePinStorage.java
@@ -0,0 +1,269 @@
+package br.com.cybercore.signer.core.security;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.SecureRandom;
+import java.security.spec.KeySpec;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Armazenamento local seguro e criptografado de PINs/senhas de certificados.
+ *
+ * Utiliza criptografia simétrica AES-256-GCM com chave derivada exclusivamente
+ * a partir de atributos do usuário e da máquina local (PBKDF2WithHmacSHA256).
+ * Os dados são gravados com permissões restritas em {@code ~/.cybercore/secure-pins.dat}.
+ */
+public final class SecurePinStorage {
+
+ private static final Logger logger = LoggerFactory.getLogger(SecurePinStorage.class);
+
+ private static final String FILE_NAME = "secure-pins.dat";
+ private static final String SALT_FILE_NAME = "secure-pins.salt";
+ private static final int GCM_IV_LENGTH = 12;
+ private static final int GCM_TAG_LENGTH = 128;
+ private static final int PBKDF2_ITERATIONS = 65536;
+ private static final int KEY_LENGTH_BITS = 256;
+
+ private static final SecurePinStorage INSTANCE = new SecurePinStorage();
+
+ private final Path storageDirectory;
+ private final Path dataFile;
+ private final Path saltFile;
+ private final ObjectMapper objectMapper;
+ private final Map memoryCache = new ConcurrentHashMap<>();
+
+ public static SecurePinStorage getInstance() {
+ return INSTANCE;
+ }
+
+ public SecurePinStorage() {
+ this(resolveDefaultDirectory());
+ }
+
+ public SecurePinStorage(Path storageDirectory) {
+ this.storageDirectory = Objects.requireNonNull(storageDirectory);
+ this.dataFile = storageDirectory.resolve(FILE_NAME);
+ this.saltFile = storageDirectory.resolve(SALT_FILE_NAME);
+ this.objectMapper = new ObjectMapper();
+ loadFromDisk();
+ }
+
+ private static Path resolveDefaultDirectory() {
+ String userHome = System.getProperty("user.home", ".");
+ return Path.of(userHome, ".cybercore");
+ }
+
+ /**
+ * Armazena de forma persistente e criptografada o PIN associado ao ID do certificado.
+ */
+ public synchronized void savePin(String certId, char[] pin) {
+ Objects.requireNonNull(certId, "certId não pode ser nulo");
+ if (pin == null || pin.length == 0) {
+ removePin(certId);
+ return;
+ }
+
+ memoryCache.put(certId, pin.clone());
+ PinSessionManager.getInstance().savePin(certId, pin);
+ persistToDisk();
+ logger.info("PIN do certificado '{}' armazenado com segurança neste computador.", certId);
+ }
+
+ /**
+ * Retorna o PIN armazenado para o certificado ou {@code null} se não houver.
+ */
+ public synchronized char[] getPin(String certId) {
+ if (certId == null) {
+ return null;
+ }
+
+ // 1. Tenta cache em memória
+ char[] cached = memoryCache.get(certId);
+ if (cached != null) {
+ return cached.clone();
+ }
+
+ // 2. Tenta carregar do PinSessionManager
+ char[] sessionPin = PinSessionManager.getInstance().getPin(certId);
+ if (sessionPin != null) {
+ memoryCache.put(certId, sessionPin.clone());
+ return sessionPin.clone();
+ }
+
+ return null;
+ }
+
+ /**
+ * Verifica se existe um PIN armazenado para o certificado.
+ */
+ public synchronized boolean hasPin(String certId) {
+ if (certId == null) {
+ return false;
+ }
+ return memoryCache.containsKey(certId) || PinSessionManager.getInstance().hasPin(certId);
+ }
+
+ /**
+ * Remove o PIN salvo para o certificado e zera a memória.
+ */
+ public synchronized void removePin(String certId) {
+ if (certId == null) {
+ return;
+ }
+
+ char[] old = memoryCache.remove(certId);
+ if (old != null) {
+ Arrays.fill(old, ' ');
+ }
+ PinSessionManager.getInstance().clearPin(certId);
+ persistToDisk();
+ logger.info("PIN do certificado '{}' removido do armazenamento seguro.", certId);
+ }
+
+ /**
+ * Remove todos os PINs armazenados em disco e zera todas as memórias.
+ */
+ public synchronized void clearAll() {
+ for (char[] pin : memoryCache.values()) {
+ if (pin != null) {
+ Arrays.fill(pin, ' ');
+ }
+ }
+ memoryCache.clear();
+ PinSessionManager.getInstance().clearAll();
+
+ try {
+ Files.deleteIfExists(dataFile);
+ logger.info("Arquivo de credenciais criptografadas removido com sucesso.");
+ } catch (Exception e) {
+ logger.warn("Falha ao remover arquivo de credenciais: {}", e.getMessage());
+ }
+ }
+
+ public synchronized int getStoredCount() {
+ return memoryCache.size();
+ }
+
+ // =========================================================================
+ // CRIPTOGRAFIA AES-256-GCM E DERIVAÇÃO DE CHAVE
+ // =========================================================================
+
+ private void persistToDisk() {
+ try {
+ Files.createDirectories(storageDirectory);
+
+ if (memoryCache.isEmpty()) {
+ Files.deleteIfExists(dataFile);
+ return;
+ }
+
+ Map plainMap = new HashMap<>();
+ for (Map.Entry entry : memoryCache.entrySet()) {
+ plainMap.put(entry.getKey(), new String(entry.getValue()));
+ }
+
+ byte[] jsonBytes = objectMapper.writeValueAsBytes(plainMap);
+ byte[] salt = getOrCreateSalt();
+ SecretKey secretKey = deriveKey(salt);
+
+ byte[] iv = new byte[GCM_IV_LENGTH];
+ new SecureRandom().nextBytes(iv);
+
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
+ GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey, spec);
+
+ byte[] ciphertext = cipher.doFinal(jsonBytes);
+
+ // Grava: [12 bytes IV] + [ciphertext + tag]
+ try (FileOutputStream fos = new FileOutputStream(dataFile.toFile())) {
+ fos.write(iv);
+ fos.write(ciphertext);
+ }
+ } catch (Exception e) {
+ logger.error("Erro ao persistir PINs criptografados no disco: {}", e.getMessage(), e);
+ }
+ }
+
+ private void loadFromDisk() {
+ if (!Files.exists(dataFile) || !Files.exists(saltFile)) {
+ return;
+ }
+
+ try {
+ byte[] allBytes = Files.readAllBytes(dataFile);
+ if (allBytes.length <= GCM_IV_LENGTH) {
+ return;
+ }
+
+ byte[] iv = Arrays.copyOfRange(allBytes, 0, GCM_IV_LENGTH);
+ byte[] ciphertext = Arrays.copyOfRange(allBytes, GCM_IV_LENGTH, allBytes.length);
+
+ byte[] salt = Files.readAllBytes(saltFile);
+ SecretKey secretKey = deriveKey(salt);
+
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
+ GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
+ cipher.init(Cipher.DECRYPT_MODE, secretKey, spec);
+
+ byte[] plainBytes = cipher.doFinal(ciphertext);
+ Map plainMap = objectMapper.readValue(plainBytes, new TypeReference<>() {});
+
+ memoryCache.clear();
+ for (Map.Entry entry : plainMap.entrySet()) {
+ char[] pin = entry.getValue().toCharArray();
+ memoryCache.put(entry.getKey(), pin);
+ PinSessionManager.getInstance().savePin(entry.getKey(), pin);
+ }
+ logger.info("Carregados {} PIN(s) do armazenamento seguro local.", memoryCache.size());
+ } catch (Exception e) {
+ logger.warn("Não foi possível carregar credenciais salvas em disco: {}", e.getMessage());
+ }
+ }
+
+ private byte[] getOrCreateSalt() throws Exception {
+ if (Files.exists(saltFile)) {
+ return Files.readAllBytes(saltFile);
+ }
+ byte[] salt = new byte[32];
+ new SecureRandom().nextBytes(salt);
+ Files.write(saltFile, salt);
+ return salt;
+ }
+
+ private SecretKey deriveKey(byte[] salt) throws Exception {
+ // Combina identificadores estáveis exclusivos da máquina e usuário
+ String hostIdentity = String.join(":",
+ System.getProperty("user.name", "unknown"),
+ System.getProperty("user.home", "unknown"),
+ System.getProperty("os.name", "unknown"),
+ System.getProperty("os.arch", "unknown"),
+ "cybercore-signer-hardware-pin-binding"
+ );
+
+ KeySpec spec = new PBEKeySpec(hostIdentity.toCharArray(), salt, PBKDF2_ITERATIONS, KEY_LENGTH_BITS);
+ SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
+ byte[] keyBytes = factory.generateSecret(spec).getEncoded();
+ return new SecretKeySpec(keyBytes, "AES");
+ }
+}
diff --git a/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/security/SecurePinStorageTest.java b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/security/SecurePinStorageTest.java
new file mode 100644
index 0000000..7fafe75
--- /dev/null
+++ b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/security/SecurePinStorageTest.java
@@ -0,0 +1,77 @@
+package br.com.cybercore.signer.core.security;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.nio.file.Path;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class SecurePinStorageTest {
+
+ @TempDir
+ Path tempDir;
+
+ private SecurePinStorage storage;
+
+ @BeforeEach
+ void setUp() {
+ storage = new SecurePinStorage(tempDir);
+ }
+
+ @AfterEach
+ void tearDown() {
+ storage.clearAll();
+ }
+
+ @Test
+ void shouldSaveAndRetrievePinCorrectly() {
+ String certId = "cert-test-123";
+ char[] pin = "123456".toCharArray();
+
+ storage.savePin(certId, pin);
+
+ assertThat(storage.hasPin(certId)).isTrue();
+ assertThat(storage.getPin(certId)).isEqualTo(pin);
+ }
+
+ @Test
+ void shouldPersistAndReloadFromDisk() {
+ String certId = "token-a3-999";
+ char[] pin = "senhaForte987".toCharArray();
+
+ storage.savePin(certId, pin);
+
+ // Cria nova instância apontando para o mesmo diretório
+ SecurePinStorage reloaded = new SecurePinStorage(tempDir);
+
+ assertThat(reloaded.hasPin(certId)).isTrue();
+ assertThat(reloaded.getPin(certId)).isEqualTo(pin);
+ }
+
+ @Test
+ void shouldRemoveSpecificPin() {
+ storage.savePin("cert-1", "1111".toCharArray());
+ storage.savePin("cert-2", "2222".toCharArray());
+
+ storage.removePin("cert-1");
+
+ assertThat(storage.hasPin("cert-1")).isFalse();
+ assertThat(storage.getPin("cert-1")).isNull();
+ assertThat(storage.hasPin("cert-2")).isTrue();
+ }
+
+ @Test
+ void shouldClearAllPins() {
+ storage.savePin("cert-1", "1111".toCharArray());
+ storage.savePin("cert-2", "2222".toCharArray());
+
+ storage.clearAll();
+
+ assertThat(storage.hasPin("cert-1")).isFalse();
+ assertThat(storage.hasPin("cert-2")).isFalse();
+ assertThat(storage.getStoredCount()).isEqualTo(0);
+ }
+}
diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/CyberCoreDesktopApplication.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/CyberCoreDesktopApplication.java
index 9314f21..081e0c3 100644
--- a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/CyberCoreDesktopApplication.java
+++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/CyberCoreDesktopApplication.java
@@ -104,7 +104,7 @@ public class CyberCoreDesktopApplication extends Application {
mainAppView = new MainAppView(primaryStage);
// Ações da Grade de Assinatura e Validação
- mainAppView.setOnSignPdf(() -> new PdfSignDialog(primaryStage, certificateManager).show());
+ mainAppView.setOnSignPdf(() -> handleStartPdfSigning(primaryStage));
mainAppView.setOnSignXml(() -> new XmlSignDialog(primaryStage, certificateManager).show());
mainAppView.setOnSignFile(() -> new FileSignDialog(primaryStage, "Assinar Arquivo", "*.*", SvgIcons.fileIcon(), certificateManager, signingService).show());
mainAppView.setOnBatchSign(() -> new BatchSigningDialog(primaryStage, certificateManager, signingService).show());
@@ -155,6 +155,19 @@ public class CyberCoreDesktopApplication extends Application {
}
}
+ private void handleStartPdfSigning(Stage ownerStage) {
+ FileChooser chooser = new FileChooser();
+ chooser.setTitle("Selecionar Documento PDF para Assinar");
+ chooser.getExtensionFilters().addAll(
+ new FileChooser.ExtensionFilter("Documentos PDF (*.pdf)", "*.pdf"),
+ new FileChooser.ExtensionFilter("Todos os Arquivos (*.*)", "*.*")
+ );
+ File selectedFile = chooser.showOpenDialog(ownerStage);
+ if (selectedFile != null && selectedFile.exists()) {
+ new PdfSignDialog(ownerStage, certificateManager, selectedFile).show();
+ }
+ }
+
private void openSettingsDialog(Stage owner) {
Stage dlg = new Stage();
dlg.initModality(Modality.APPLICATION_MODAL);
@@ -178,8 +191,9 @@ public class CyberCoreDesktopApplication extends Application {
VBox.setVgrow(scrollPane, Priority.ALWAYS);
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
- btnClose.getStyleClass().add("btn-secondary");
- btnClose.setPrefWidth(95);
+ btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
+ btnClose.setPrefWidth(110);
+ btnClose.setMinWidth(110);
btnClose.setOnAction(e -> dlg.close());
HBox footer = new HBox(btnClose);
@@ -206,8 +220,9 @@ public class CyberCoreDesktopApplication extends Application {
AboutView aboutView = new AboutView();
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
- btnClose.getStyleClass().add("btn-primary");
- btnClose.setPrefWidth(95);
+ btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
+ btnClose.setPrefWidth(110);
+ btnClose.setMinWidth(110);
btnClose.setOnAction(e -> dlg.close());
HBox footer = new HBox(btnClose);
diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/components/SvgIcons.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/components/SvgIcons.java
index acb5e1d..12061e0 100644
--- a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/components/SvgIcons.java
+++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/components/SvgIcons.java
@@ -729,4 +729,103 @@ public final class SvgIcons {
box.getChildren().add(cross);
return box;
}
+
+ /**
+ * Ícone vetorial de Seta para a Direita / Avançar.
+ */
+ public static Node arrowRightIcon(double size) {
+ return arrowRightIcon(size, "#ffffff");
+ }
+
+ public static Node arrowRightIcon(double size, String colorHex) {
+ StackPane box = new StackPane();
+ box.setPrefSize(size, size);
+ box.setMaxSize(size, size);
+ box.setMinSize(size, size);
+ box.setAlignment(Pos.CENTER);
+
+ SVGPath arrow = new SVGPath();
+ arrow.setContent("M 5 3 L 10.5 8 L 5 13");
+ arrow.setStroke(Color.web(colorHex));
+ arrow.setStrokeWidth(2.2);
+ arrow.setStrokeLineCap(javafx.scene.shape.StrokeLineCap.ROUND);
+ arrow.setStrokeLineJoin(javafx.scene.shape.StrokeLineJoin.ROUND);
+ arrow.setFill(Color.TRANSPARENT);
+
+ double scale = size / 16.0;
+ arrow.setScaleX(scale);
+ arrow.setScaleY(scale);
+
+ box.getChildren().add(arrow);
+ return box;
+ }
+
+ /**
+ * Ícones vetoriais de Zoom In (+), Zoom Out (-) e Ajustar à Tela.
+ */
+ public static Node zoomInIcon(double size) {
+ StackPane box = new StackPane();
+ box.setPrefSize(size, size);
+ box.setMaxSize(size, size);
+ box.setMinSize(size, size);
+ box.setAlignment(Pos.CENTER);
+
+ SVGPath path = new SVGPath();
+ path.setContent("M 6.5 2 A 4.5 4.5 0 1 0 9.7 9.7 L 13.5 13.5 M 4.5 6.5 L 8.5 6.5 M 6.5 4.5 L 6.5 8.5");
+ path.setStroke(Color.web("#0284c7"));
+ path.setStrokeWidth(1.8);
+ path.setStrokeLineCap(javafx.scene.shape.StrokeLineCap.ROUND);
+ path.setFill(Color.TRANSPARENT);
+
+ double scale = size / 16.0;
+ path.setScaleX(scale);
+ path.setScaleY(scale);
+
+ box.getChildren().add(path);
+ return box;
+ }
+
+ public static Node zoomOutIcon(double size) {
+ StackPane box = new StackPane();
+ box.setPrefSize(size, size);
+ box.setMaxSize(size, size);
+ box.setMinSize(size, size);
+ box.setAlignment(Pos.CENTER);
+
+ SVGPath path = new SVGPath();
+ path.setContent("M 6.5 2 A 4.5 4.5 0 1 0 9.7 9.7 L 13.5 13.5 M 4.5 6.5 L 8.5 6.5");
+ path.setStroke(Color.web("#0284c7"));
+ path.setStrokeWidth(1.8);
+ path.setStrokeLineCap(javafx.scene.shape.StrokeLineCap.ROUND);
+ path.setFill(Color.TRANSPARENT);
+
+ double scale = size / 16.0;
+ path.setScaleX(scale);
+ path.setScaleY(scale);
+
+ box.getChildren().add(path);
+ return box;
+ }
+
+ public static Node zoomFitIcon(double size) {
+ StackPane box = new StackPane();
+ box.setPrefSize(size, size);
+ box.setMaxSize(size, size);
+ box.setMinSize(size, size);
+ box.setAlignment(Pos.CENTER);
+
+ SVGPath path = new SVGPath();
+ path.setContent("M 2 5 L 2 2 L 5 2 M 11 2 L 14 2 L 14 5 M 14 11 L 14 14 L 11 14 M 5 14 L 2 14 L 2 11");
+ path.setStroke(Color.web("#0284c7"));
+ path.setStrokeWidth(1.8);
+ path.setStrokeLineCap(javafx.scene.shape.StrokeLineCap.ROUND);
+ path.setFill(Color.TRANSPARENT);
+
+ double scale = size / 16.0;
+ path.setScaleX(scale);
+ path.setScaleY(scale);
+
+ box.getChildren().add(path);
+ return box;
+ }
}
diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/CertificateDetailsDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/CertificateDetailsDialog.java
index e3c3f50..fc7c311 100644
--- a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/CertificateDetailsDialog.java
+++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/CertificateDetailsDialog.java
@@ -129,8 +129,9 @@ public class CertificateDetailsDialog {
actionsRow.setAlignment(Pos.CENTER_RIGHT);
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
- btnClose.getStyleClass().add("btn-secondary");
- btnClose.setPrefWidth(95);
+ btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
+ btnClose.setPrefWidth(110);
+ btnClose.setMinWidth(110);
btnClose.setOnAction(e -> stage.close());
actionsRow.getChildren().add(btnClose);
diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/LogsViewerDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/LogsViewerDialog.java
index 9059132..dcfd313 100644
--- a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/LogsViewerDialog.java
+++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/LogsViewerDialog.java
@@ -63,12 +63,15 @@ public final class LogsViewerDialog {
footer.setAlignment(Pos.CENTER_RIGHT);
Button btnRefresh = new Button("Atualizar");
- btnRefresh.getStyleClass().add("btn-secondary");
+ btnRefresh.getStyleClass().addAll("btn-secondary", "btn-dialog");
+ btnRefresh.setPrefWidth(110);
+ btnRefresh.setMinWidth(110);
btnRefresh.setOnAction(e -> loadInitialLogs());
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
- btnClose.getStyleClass().add("btn-primary");
- btnClose.setPrefWidth(95);
+ btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
+ btnClose.setPrefWidth(110);
+ btnClose.setMinWidth(110);
btnClose.setOnAction(e -> stage.close());
footer.getChildren().addAll(btnRefresh, btnClose);
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
new file mode 100644
index 0000000..3bd2575
--- /dev/null
+++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/PdfCertificateSignDialog.java
@@ -0,0 +1,534 @@
+package br.com.cybercore.signer.ui.view;
+
+import br.com.cybercore.signer.core.domain.CertificateType;
+import br.com.cybercore.signer.core.domain.ManagedCertificate;
+import br.com.cybercore.signer.core.pdf.PdfSignatureOptions;
+import br.com.cybercore.signer.core.pdf.PdfSigningService;
+import br.com.cybercore.signer.core.pdf.seal.CpfDisplayMode;
+import br.com.cybercore.signer.core.pdf.seal.PdfSealTheme;
+import br.com.cybercore.signer.core.provider.CertificateManager;
+import br.com.cybercore.signer.core.security.PinSessionManager;
+import br.com.cybercore.signer.core.security.SecurePinStorage;
+import br.com.cybercore.signer.ui.components.SvgIcons;
+import br.com.cybercore.signer.ui.theme.ThemeManager;
+import javafx.application.Platform;
+import javafx.collections.FXCollections;
+import javafx.concurrent.Task;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.Scene;
+import javafx.scene.control.Alert;
+import javafx.scene.control.Button;
+import javafx.scene.control.ButtonBar;
+import javafx.scene.control.ButtonType;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.Label;
+import javafx.scene.control.ListCell;
+import javafx.scene.control.PasswordField;
+import javafx.scene.control.ProgressIndicator;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.VBox;
+import javafx.stage.FileChooser;
+import javafx.stage.Modality;
+import javafx.stage.Stage;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.awt.Desktop;
+import java.io.File;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Diálogo dedicado e compacto para Seleção de Certificado Digital ICP-Brasil, inserção de PIN/senha,
+ * definição do arquivo de destino e conclusão da Assinatura Digital PAdES.
+ */
+@SuppressWarnings("this-escape")
+public final class PdfCertificateSignDialog {
+
+ private static final Logger logger = LoggerFactory.getLogger(PdfCertificateSignDialog.class);
+
+ public record SealConfig(
+ boolean visibleSeal,
+ PdfSealTheme sealTheme,
+ CpfDisplayMode cpfDisplayMode,
+ boolean includeDateTime,
+ String customExtraInfo,
+ PdfSignDialog.PositionOption positionOption,
+ PdfSignDialog.PageOption pageOption,
+ boolean isCustomPosition,
+ Float customX,
+ Float customY,
+ Integer customPageNumber,
+ String reason,
+ String location
+ ) {}
+
+ private final Stage stage;
+ private final CertificateManager certManager;
+ private final PdfSigningService pdfSigningService;
+ private final File sourcePdfFile;
+ private final SealConfig sealConfig;
+ private final String sealSummaryText;
+
+ private ComboBox certCombo;
+ private Label lblPinTitle;
+ private PasswordField pinField;
+ private Button btnClearRememberedPin;
+ private Label lblPinNotice;
+ private CheckBox chkRememberPin;
+ private ProgressIndicator progressIndicator;
+ private Label lblStatus;
+ private Button btnCancel;
+ private Button btnSign;
+
+ public PdfCertificateSignDialog(Stage owner,
+ CertificateManager certManager,
+ PdfSigningService pdfSigningService,
+ File sourcePdfFile,
+ SealConfig sealConfig,
+ String sealSummaryText) {
+ this.certManager = Objects.requireNonNull(certManager, "CertificateManager não pode ser nulo.");
+ this.pdfSigningService = Objects.requireNonNull(pdfSigningService, "PdfSigningService não pode ser nulo.");
+ this.sourcePdfFile = Objects.requireNonNull(sourcePdfFile, "sourcePdfFile não pode ser nulo.");
+ this.sealConfig = Objects.requireNonNull(sealConfig, "sealConfig não pode ser nulo.");
+ this.sealSummaryText = sealSummaryText != null ? sealSummaryText : "Selo Visual PAdES";
+
+ this.stage = new Stage();
+ this.stage.initModality(Modality.APPLICATION_MODAL);
+ if (owner != null) {
+ this.stage.initOwner(owner);
+ }
+ this.stage.setResizable(true);
+ this.stage.setMinWidth(620);
+ this.stage.setMinHeight(460);
+ this.stage.setTitle("Seleção de Certificado Digital — CyberCore Signer");
+
+ initLayout();
+ }
+
+ private void initLayout() {
+ VBox root = new VBox(14);
+ root.setPadding(new Insets(18, 20, 18, 20));
+ root.getStyleClass().add("dialog-container");
+
+ // Topo / Cabeçalho
+ HBox header = new HBox(12);
+ header.setAlignment(Pos.CENTER_LEFT);
+
+ VBox titleBox = new VBox(2);
+ Label lblTitle = new Label("SELEÇÃO DE CERTIFICADO DIGITAL");
+ lblTitle.getStyleClass().add("dialog-header");
+ Label lblSub = new Label("Escolha o certificado ICP-Brasil e informe o PIN para concluir a assinatura digital.");
+ lblSub.getStyleClass().add("dialog-subtitle");
+ titleBox.getChildren().addAll(lblTitle, lblSub);
+
+ header.getChildren().addAll(SvgIcons.certificatesIcon(), titleBox);
+
+ // 1. Resumo do Documento e do Selo
+ VBox summaryBox = new VBox(8);
+ summaryBox.getStyleClass().addAll("dialog-box", "document-summary-box");
+ summaryBox.setPadding(new Insets(12, 14, 12, 14));
+
+ Label lblSummaryHeader = new Label("DOCUMENTO CONFIGURADO");
+ lblSummaryHeader.getStyleClass().add("dialog-field-title");
+
+ GridPane summaryGrid = new GridPane();
+ summaryGrid.setHgap(12);
+ summaryGrid.setVgap(6);
+
+ Label lblDocTitle = new Label("Arquivo:");
+ 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;");
+
+ 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;");
+
+ summaryGrid.add(lblDocTitle, 0, 0);
+ summaryGrid.add(lblDocValue, 1, 0);
+ summaryGrid.add(lblSealTitle, 0, 1);
+ summaryGrid.add(lblSealValue, 1, 1);
+
+ summaryBox.getChildren().addAll(lblSummaryHeader, summaryGrid);
+
+ // 2. Card de Certificado ICP-Brasil
+ VBox certSection = new VBox(10);
+ certSection.getStyleClass().add("dialog-box");
+ certSection.setPadding(new Insets(14));
+ VBox.setVgrow(certSection, Priority.ALWAYS);
+
+ HBox certHeader = new HBox(10);
+ certHeader.setAlignment(Pos.CENTER_LEFT);
+ Label lblCertTitle = new Label("CERTIFICADO DIGITAL ICP-BRASIL");
+ lblCertTitle.getStyleClass().add("dialog-field-title");
+ HBox.setHgrow(lblCertTitle, Priority.ALWAYS);
+
+ Button btnRefresh = new Button("↻ Atualizar Lista");
+ btnRefresh.getStyleClass().addAll("btn-secondary", "btn-compact");
+ btnRefresh.setOnAction(e -> {
+ populateCertificates();
+ updatePinFieldVisibility();
+ });
+ certHeader.getChildren().addAll(lblCertTitle, btnRefresh);
+
+ certCombo = new ComboBox<>();
+ certCombo.setMaxWidth(Double.MAX_VALUE);
+ populateCertificates();
+ certCombo.setOnAction(e -> updatePinFieldVisibility());
+
+ certCombo.setCellFactory(lv -> new ListCell<>() {
+ @Override
+ protected void updateItem(ManagedCertificate item, boolean empty) {
+ super.updateItem(item, empty);
+ if (empty || item == null) {
+ setText(null);
+ } else {
+ String doc = item.cpf() != null ? " [CPF: " + item.cpf() + "]" : (item.cnpj() != null ? " [CNPJ: " + item.cnpj() + "]" : "");
+ setText(item.commonName() + doc + " (" + item.type() + ")");
+ }
+ }
+ });
+ certCombo.setButtonCell(new ListCell<>() {
+ @Override
+ protected void updateItem(ManagedCertificate item, boolean empty) {
+ super.updateItem(item, empty);
+ if (empty || item == null) {
+ setText(null);
+ } else {
+ String doc = item.cpf() != null ? " [CPF: " + item.cpf() + "]" : (item.cnpj() != null ? " [CNPJ: " + item.cnpj() + "]" : "");
+ setText(item.commonName() + doc + " (" + item.type() + ")");
+ }
+ }
+ });
+
+ HBox pinRow = new HBox(10);
+ pinRow.setAlignment(Pos.CENTER_LEFT);
+ lblPinTitle = new Label("PIN / Senha:");
+ lblPinTitle.getStyleClass().add("dialog-field-title");
+
+ pinField = new PasswordField();
+ pinField.setPromptText("PIN ou senha do certificado...");
+ pinField.setPrefWidth(220);
+
+ btnClearRememberedPin = new Button("Alterar PIN");
+ btnClearRememberedPin.getStyleClass().addAll("btn-secondary", "btn-compact");
+ btnClearRememberedPin.setStyle("-fx-font-size: 11px;");
+ btnClearRememberedPin.setVisible(false);
+ btnClearRememberedPin.setOnAction(e -> {
+ ManagedCertificate sel = certCombo.getValue();
+ if (sel != null) {
+ SecurePinStorage.getInstance().removePin(sel.id());
+ updatePinFieldVisibility();
+ }
+ });
+
+ lblPinNotice = new Label();
+ lblPinNotice.setStyle("-fx-font-size: 11.5px;");
+ pinRow.getChildren().addAll(lblPinTitle, pinField, btnClearRememberedPin, lblPinNotice);
+
+ chkRememberPin = new CheckBox("Lembrar PIN/senha com segurança neste computador");
+ chkRememberPin.setStyle("-fx-font-size: 11.5px; -fx-text-fill: -fx-text-primary;");
+ chkRememberPin.setSelected(true);
+
+ 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;");
+ 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;");
+ tipBox.getChildren().add(lblTip);
+
+ certSection.getChildren().addAll(certHeader, certCombo, pinRow, chkRememberPin, tipBox);
+
+ // 3. Rodapé
+ HBox footer = new HBox(12);
+ footer.setAlignment(Pos.CENTER_RIGHT);
+ footer.setPadding(new Insets(4, 0, 0, 0));
+
+ progressIndicator = new ProgressIndicator();
+ progressIndicator.setPrefSize(22, 22);
+ progressIndicator.setVisible(false);
+
+ lblStatus = new Label();
+ lblStatus.setStyle("-fx-font-size: 12px;");
+ HBox.setHgrow(lblStatus, Priority.ALWAYS);
+
+ btnCancel = new Button("Cancelar");
+ btnCancel.getStyleClass().addAll("btn-secondary", "btn-dialog");
+ btnCancel.setMinWidth(110);
+ btnCancel.setOnAction(e -> stage.close());
+
+ btnSign = new Button("Assinar Documento PDF");
+ btnSign.getStyleClass().addAll("btn-primary", "btn-dialog");
+ btnSign.setMinWidth(220);
+ btnSign.setDefaultButton(true);
+ btnSign.setOnAction(e -> handleSign());
+
+ footer.getChildren().addAll(progressIndicator, lblStatus, btnCancel, btnSign);
+
+ root.getChildren().addAll(header, summaryBox, certSection, footer);
+
+ Scene scene = new Scene(root, 650, 480);
+ ThemeManager.applyTheme(scene);
+ stage.setScene(scene);
+
+ updatePinFieldVisibility();
+ }
+
+ private void populateCertificates() {
+ try {
+ List certs = certManager.getAvailableCertificates();
+ certCombo.setItems(FXCollections.observableArrayList(certs));
+ if (!certs.isEmpty()) {
+ certCombo.setValue(certs.get(0));
+ }
+ } catch (Exception e) {
+ logger.error("Erro ao listar certificados", e);
+ }
+ }
+
+ private void updatePinFieldVisibility() {
+ ManagedCertificate cert = certCombo.getValue();
+ if (cert == null) {
+ pinField.setDisable(true);
+ lblPinNotice.setText("");
+ chkRememberPin.setVisible(false);
+ btnClearRememberedPin.setVisible(false);
+ 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);
+ chkRememberPin.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;
+ }
+
+ // Token USB (PKCS#11) ou Arquivo A1 (PKCS#12)
+ pinField.setVisible(true);
+ lblPinTitle.setVisible(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);
+ 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;");
+ } else {
+ pinField.clear();
+ pinField.setDisable(false);
+ chkRememberPin.setVisible(true);
+ btnClearRememberedPin.setVisible(false);
+ boolean isA3 = cert.type() == CertificateType.A3;
+ lblPinNotice.setText(isA3 ? "Requer PIN do token" : "Requer senha do certificado");
+ lblPinNotice.setStyle("-fx-text-fill: -fx-text-secondary; -fx-font-size: 11.5px;");
+ }
+ }
+
+ private void handleSign() {
+ ManagedCertificate cert = certCombo.getValue();
+ if (cert == null) {
+ showAlert(Alert.AlertType.WARNING, "Certificado Não Selecionado", "Por favor, selecione um certificado digital.");
+ return;
+ }
+
+ boolean isWindowsStore = "provider-win-store".equalsIgnoreCase(cert.providerId());
+ char[] storedPin = !isWindowsStore ? SecurePinStorage.getInstance().getPin(cert.id()) : null;
+
+ if (!isWindowsStore && (storedPin == null || storedPin.length == 0) && (pinField.getText() == null || pinField.getText().trim().isEmpty())) {
+ showAlert(Alert.AlertType.WARNING, "PIN / Senha Obrigatório",
+ cert.type() == CertificateType.A3
+ ? "Por favor, informe o PIN do seu dispositivo A3 para autorizar a assinatura."
+ : "Por favor, informe a senha do certificado A1 para autorizar a assinatura.");
+ pinField.requestFocus();
+ return;
+ }
+
+ FileChooser chooser = new FileChooser();
+ chooser.setTitle("Salvar PDF Assinado Como...");
+ chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Documentos PDF (*.pdf)", "*.pdf"));
+
+ String originalName = sourcePdfFile.getName();
+ String baseName = originalName.toLowerCase().endsWith(".pdf")
+ ? originalName.substring(0, originalName.length() - 4)
+ : originalName;
+ chooser.setInitialFileName(baseName + "_assinado.pdf");
+ if (sourcePdfFile.getParentFile() != null && sourcePdfFile.getParentFile().exists()) {
+ chooser.setInitialDirectory(sourcePdfFile.getParentFile());
+ }
+
+ File targetFile = chooser.showSaveDialog(stage);
+ if (targetFile == null) {
+ return;
+ }
+
+ char[] pinToUse = null;
+ if (!isWindowsStore) {
+ if (storedPin != null && storedPin.length > 0) {
+ pinToUse = storedPin;
+ } else if (!pinField.getText().isEmpty()) {
+ pinToUse = pinField.getText().toCharArray();
+ }
+ }
+
+ final char[] enteredPin = (!isWindowsStore && pinField.getText() != null && !pinField.getText().isEmpty())
+ ? pinField.getText().toCharArray()
+ : null;
+
+ PdfSignatureOptions.Builder b = PdfSignatureOptions.builder(cert.id())
+ .pinOrPassword(pinToUse)
+ .visibleSeal(sealConfig.visibleSeal())
+ .sealTheme(sealConfig.sealTheme())
+ .cpfDisplayMode(sealConfig.cpfDisplayMode())
+ .includeDateTime(sealConfig.includeDateTime())
+ .customExtraInfo(sealConfig.customExtraInfo())
+ .reason(sealConfig.reason())
+ .location(sealConfig.location());
+
+ if (sealConfig.isCustomPosition() && sealConfig.customX() != null && sealConfig.customY() != null) {
+ b.customPosition(sealConfig.customX(), sealConfig.customY(), sealConfig.customPageNumber());
+ } else if (sealConfig.positionOption() != null) {
+ b.position(sealConfig.positionOption().toPosition());
+ if (sealConfig.pageOption() != null) {
+ b.pageChoice(sealConfig.pageOption().toPageChoice());
+ }
+ }
+
+ PdfSignatureOptions options = b.build();
+
+ setFormDisabled(true);
+ progressIndicator.setVisible(true);
+ lblStatus.setText("Assinando documento PDF com envelope PAdES...");
+ lblStatus.setStyle("-fx-text-fill: -fx-accent;");
+
+ Task signTask = new Task<>() {
+ @Override
+ protected File call() throws Exception {
+ pdfSigningService.signPdfFile(sourcePdfFile, targetFile, options);
+ return targetFile;
+ }
+ };
+
+ signTask.setOnSucceeded(e -> {
+ setFormDisabled(false);
+ progressIndicator.setVisible(false);
+ lblStatus.setText("✓ Documento assinado com sucesso!");
+ lblStatus.setStyle("-fx-text-fill: #10b981; -fx-font-weight: bold;");
+
+ // Salva o PIN de forma segura se solicitado pelo usuário
+ if (!isWindowsStore && chkRememberPin.isSelected() && enteredPin != null && enteredPin.length > 0) {
+ SecurePinStorage.getInstance().savePin(cert.id(), enteredPin);
+ }
+
+ showSuccessDialog(targetFile);
+ stage.close();
+ });
+
+ signTask.setOnFailed(e -> {
+ setFormDisabled(false);
+ progressIndicator.setVisible(false);
+ Throwable ex = signTask.getException();
+ logger.error("Falha ao assinar PDF", ex);
+
+ // Se o PIN estava incorreto, remove do armazenamento seguro
+ String msg = ex != null ? ex.getMessage() : "";
+ if (msg.contains("PIN_INCORRECT") || msg.contains("senha") || msg.contains("PIN")) {
+ SecurePinStorage.getInstance().removePin(cert.id());
+ updatePinFieldVisibility();
+ }
+
+ lblStatus.setText("✕ Falha na assinatura");
+ lblStatus.setStyle("-fx-text-fill: #ef4444; -fx-font-weight: bold;");
+
+ showAlert(Alert.AlertType.ERROR, "Erro na Assinatura do PDF",
+ "Não foi possível concluir a assinatura digital:\n" + (ex != null ? ex.getMessage() : "Erro desconhecido"));
+ });
+
+ Thread signThread = new Thread(signTask, "cybercore-pdf-cert-signer");
+ signThread.setDaemon(true);
+ signThread.start();
+ }
+
+ private void showSuccessDialog(File targetFile) {
+ Alert alert = new Alert(Alert.AlertType.INFORMATION);
+ alert.setTitle("Assinatura Concluída");
+ alert.setHeaderText("PDF Assinado com Sucesso!");
+ alert.setContentText("O documento foi digitalmente assinado no padrão PAdES ICP-Brasil e salvo no destino solicitado.\n\n"
+ + "Arquivo gerado:\n" + targetFile.getAbsolutePath());
+
+ ButtonType btnOpen = new ButtonType("Abrir PDF");
+ ButtonType btnFolder = new ButtonType("Abrir Pasta");
+ ButtonType btnClose = new ButtonType("Fechar", ButtonBar.ButtonData.CANCEL_CLOSE);
+
+ alert.getButtonTypes().setAll(btnOpen, btnFolder, btnClose);
+ ThemeManager.applyTheme(alert.getDialogPane().getScene());
+
+ Optional result = alert.showAndWait();
+ if (result.isPresent()) {
+ if (result.get() == btnOpen) {
+ openFile(targetFile);
+ } else if (result.get() == btnFolder) {
+ openFolder(targetFile);
+ }
+ }
+ }
+
+ private void openFile(File file) {
+ try {
+ if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) {
+ Desktop.getDesktop().open(file);
+ }
+ } catch (Exception ex) {
+ logger.warn("Não foi possível abrir o arquivo automaticamente: {}", ex.getMessage());
+ }
+ }
+
+ private void openFolder(File file) {
+ try {
+ if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) {
+ File parent = file.getParentFile();
+ if (parent != null) {
+ Desktop.getDesktop().open(parent);
+ }
+ }
+ } catch (Exception ex) {
+ logger.warn("Não foi possível abrir a pasta do arquivo: {}", ex.getMessage());
+ }
+ }
+
+ private void setFormDisabled(boolean disabled) {
+ btnSign.setDisable(disabled);
+ btnCancel.setDisable(disabled);
+ certCombo.setDisable(disabled);
+ pinField.setDisable(disabled);
+ }
+
+ private void showAlert(Alert.AlertType type, String title, String message) {
+ Alert alert = new Alert(type);
+ alert.setTitle(title);
+ alert.setHeaderText(null);
+ alert.setContentText(message);
+ ThemeManager.applyTheme(alert.getDialogPane().getScene());
+ alert.showAndWait();
+ }
+
+ public void show() {
+ stage.centerOnScreen();
+ stage.show();
+ stage.toFront();
+ }
+}
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 17a94ad..c852b37 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
@@ -2,8 +2,6 @@ package br.com.cybercore.signer.ui.view;
import br.com.cybercore.signer.core.config.AgentConfiguration;
import br.com.cybercore.signer.core.config.AgentConfigurationService;
-import br.com.cybercore.signer.core.domain.CertificateType;
-import br.com.cybercore.signer.core.domain.ManagedCertificate;
import br.com.cybercore.signer.core.pdf.PdfSealPageChoice;
import br.com.cybercore.signer.core.pdf.PdfSealPosition;
import br.com.cybercore.signer.core.pdf.PdfSignatureOptions;
@@ -14,7 +12,6 @@ import br.com.cybercore.signer.core.pdf.seal.PdfSealRenderer;
import br.com.cybercore.signer.core.pdf.seal.PdfSealTheme;
import br.com.cybercore.signer.core.pdf.seal.PdfSealThemeRegistry;
import br.com.cybercore.signer.core.provider.CertificateManager;
-import br.com.cybercore.signer.core.security.PinSessionManager;
import br.com.cybercore.signer.ui.components.SvgIcons;
import br.com.cybercore.signer.ui.theme.ThemeManager;
import javafx.application.Platform;
@@ -26,24 +23,25 @@ import javafx.scene.Cursor;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
-import javafx.scene.control.ButtonBar;
-import javafx.scene.control.ButtonType;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
-import javafx.scene.control.PasswordField;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextField;
+import javafx.scene.control.Tooltip;
import javafx.scene.effect.DropShadow;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
+import javafx.scene.input.ScrollEvent;
+import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
@@ -53,18 +51,16 @@ import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.awt.Desktop;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.nio.file.Files;
-import java.time.Instant;
-import java.util.List;
import java.util.Objects;
-import java.util.Optional;
/**
- * Diálogo especializado para Assinatura Digital de Documentos PDF no padrão PAdES,
- * com pré-visualização interativa de páginas do PDF e posicionamento livre do selo visual por clique ou arrasto.
+ * Diálogo interativo para pré-visualização do documento PDF, controle de zoom avançado,
+ * customização do selo visual e posicionamento na folha.
+ *
+ * Ao avançar, fecha esta janela e abre uma nova janela dedicada à seleção do certificado ICP-Brasil e PIN.
*/
@SuppressWarnings("this-escape")
public final class PdfSignDialog {
@@ -72,41 +68,66 @@ public final class PdfSignDialog {
private static final Logger logger = LoggerFactory.getLogger(PdfSignDialog.class);
private final Stage stage;
+ private final Stage ownerStage;
private final CertificateManager certManager;
private final PdfSigningService pdfSigningService;
- // Documento PDF em memória para visualização e assinatura
- private TextField txtSourcePdf;
+ // Documento PDF em memória
private File selectedSourceFile;
private byte[] selectedPdfBytes;
private int totalPages = 0;
private int currentPageIndex = 0;
- // Coordenadas personalizadas capturadas da interação na folha
+ // Controle de Zoom da Folha (1.0 = 100%)
+ private double zoomFactor = 1.0;
+ private static final double MIN_ZOOM = 0.5;
+ private static final double MAX_ZOOM = 2.5;
+ private static final double ZOOM_STEP = 0.25;
+
+ // Dimensões base da folha para renderização com zoom (atualizadas dinamicamente pelo PDF carregado)
+ private double currentBaseSheetWidth = 500.0;
+ private double currentBaseSheetHeight = 707.0;
+ private double currentBaseSealWidth = 120.0;
+ private double currentBaseSealHeight = 36.7;
+
+ // Tamanho real da página PDF em pontos tipográficos
+ 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;
+
+ // Coordenadas PDF (pontos tipográficos nativos do PDF)
private Float customPdfX = null;
private Float customPdfY = null;
private Integer customPageNumber = null;
private boolean isCustomPositionActive = false;
- // Controles de Certificado
- private ComboBox certCombo;
- private PasswordField pinField;
- private Label lblPinNotice;
+ // Posição percentual normalizada do selo na folha [0.0 - 1.0]
+ private double sealRelativeX = 0.70;
+ private double sealRelativeY = 0.88;
- // Controles de Selo Visual
+ // Componentes do Card do Documento
+ private Label lblDocName;
+ private Label lblDocMeta;
+ private Button btnChangeDoc;
+
+ // Controles de Selo Visual (utiliza o selo padrão das configurações)
+ private PdfSealTheme activeSealTheme;
+ private Label lblSealThemeName;
+ private Label lblSealThemeDesc;
private CheckBox chkVisibleSeal;
- private ComboBox themeCombo;
- private ComboBox cpfModeCombo;
- private CheckBox chkDateTime;
- private TextField txtCustomInfo;
private ComboBox positionCombo;
private ComboBox pageCombo;
private TextField txtReason;
private TextField txtLocation;
private ImageView sealPreviewImageView;
- // Visualizador Interativo da Página do PDF
- private VBox pageViewerColumn;
+ // Visualizador Interativo da Folha
+ private ScrollPane sheetScroll;
private StackPane pageSheetContainer;
private ImageView pageImageView;
private Pane interactiveOverlayPane;
@@ -117,17 +138,30 @@ public final class PdfSignDialog {
private Button btnNextPage;
private ProgressIndicator pageLoadingIndicator;
+ // Controles de Zoom
+ private Button btnZoomOut;
+ private Button btnZoomIn;
+ private Button btnZoomFit;
+ private Label lblZoomValue;
+
// Rodapé de Ações
- private ProgressIndicator progressIndicator;
- private Label lblStatus;
- private Button btnSign;
private Button btnCancel;
+ private Button btnNextToCert;
public PdfSignDialog(Stage owner, CertificateManager certManager) {
- this(owner, certManager, new PdfSigningService(certManager));
+ this(owner, certManager, new PdfSigningService(certManager), null);
+ }
+
+ public PdfSignDialog(Stage owner, CertificateManager certManager, File initialSourceFile) {
+ this(owner, certManager, new PdfSigningService(certManager), initialSourceFile);
}
public PdfSignDialog(Stage owner, CertificateManager certManager, PdfSigningService pdfSigningService) {
+ this(owner, certManager, pdfSigningService, null);
+ }
+
+ public PdfSignDialog(Stage owner, CertificateManager certManager, PdfSigningService pdfSigningService, File initialSourceFile) {
+ this.ownerStage = owner;
this.certManager = Objects.requireNonNull(certManager, "CertificateManager não pode ser nulo.");
this.pdfSigningService = Objects.requireNonNull(pdfSigningService, "PdfSigningService não pode ser nulo.");
@@ -137,123 +171,123 @@ public final class PdfSignDialog {
this.stage.initOwner(owner);
}
this.stage.setResizable(true);
- this.stage.setMinWidth(880);
- this.stage.setMinHeight(560);
+ this.stage.setMinWidth(960);
+ this.stage.setMinHeight(640);
+ this.stage.setTitle("Posicionamento do Selo de Assinatura — CyberCore Signer");
initLayout();
+
+ if (initialSourceFile != null && initialSourceFile.exists()) {
+ loadPdfDocument(initialSourceFile);
+ }
}
private void initLayout() {
- VBox root = new VBox(14);
- root.setPadding(new Insets(18));
+ VBox root = new VBox(12);
+ root.setPadding(new Insets(16, 18, 16, 18));
root.getStyleClass().add("dialog-container");
- // Topo / Cabeçalho
- HBox header = new HBox(14);
+ // 1. Cabeçalho
+ HBox header = new HBox(12);
header.setAlignment(Pos.CENTER_LEFT);
+
VBox titleBox = new VBox(3);
Label lblTitle = new Label("ASSINATURA DE DOCUMENTO PDF (PADES)");
lblTitle.getStyleClass().add("dialog-header");
- Label lblSub = new Label("Assine digitalmente documentos PDF. Clique na folha para escolher o local exato onde o selo visual será estampado.");
- lblSub.getStyleClass().add("dialog-subtitle");
- titleBox.getChildren().addAll(lblTitle, lblSub);
+ Label lblSubtitle = new Label("Configure o selo visual e defina o local exato onde ele será estampado na folha.");
+ lblSubtitle.getStyleClass().add("dialog-subtitle");
+ titleBox.getChildren().addAll(lblTitle, lblSubtitle);
+
header.getChildren().addAll(SvgIcons.pdfIcon(), titleBox);
- // Corpo Principal dividido em 2 colunas: Configuração à Esquerda e Visualizador Interativo à Direita
- HBox body = new HBox(16);
+ // 2. Barra informativa do Documento
+ HBox docBar = new HBox(12);
+ docBar.setAlignment(Pos.CENTER_LEFT);
+ docBar.getStyleClass().addAll("document-summary-box");
+
+ VBox docDetails = new VBox(2);
+ lblDocName = new Label("Nenhum arquivo selecionado");
+ lblDocName.setStyle("-fx-font-weight: bold; -fx-font-size: 12.5px; -fx-text-fill: -fx-text-primary;");
+ lblDocMeta = new Label("Selecione um arquivo PDF para pré-visualização");
+ lblDocMeta.setStyle("-fx-font-size: 11px; -fx-text-fill: -fx-text-secondary;");
+ docDetails.getChildren().addAll(lblDocName, lblDocMeta);
+
+ Region docSpacer = new Region();
+ HBox.setHgrow(docSpacer, Priority.ALWAYS);
+
+ btnChangeDoc = new Button("Trocar PDF...");
+ btnChangeDoc.getStyleClass().addAll("btn-secondary", "btn-compact");
+ btnChangeDoc.setOnAction(e -> handleBrowseSourcePdf());
+
+ docBar.getChildren().addAll(SvgIcons.fileIcon(), docDetails, docSpacer, btnChangeDoc);
+
+ // 3. Área Central (Duas Colunas: Configurações do Selo à Esquerda e Visualizador à Direita)
+ HBox body = new HBox(14);
VBox.setVgrow(body, Priority.ALWAYS);
- // =========================================================================
- // COLUNA DA ESQUERDA: Formulário de Configurações
- // =========================================================================
+ // Coluna Esquerda: Informações do Selo Padrão e Opções
ScrollPane leftScroll = new ScrollPane();
leftScroll.setFitToWidth(true);
- leftScroll.setPrefWidth(470);
- leftScroll.setMinWidth(440);
+ leftScroll.setPrefWidth(330);
+ leftScroll.setMinWidth(310);
+ leftScroll.setMaxWidth(360);
leftScroll.setStyle("-fx-background-color: transparent; -fx-background: transparent; -fx-border-color: transparent;");
- VBox formContent = new VBox(14);
- formContent.setPadding(new Insets(2, 6, 2, 0));
+ VBox sealSection = buildSealOptionsSection();
+ leftScroll.setContent(sealSection);
- // 1. Arquivo PDF
- VBox fileSection = new VBox(8);
- fileSection.getStyleClass().add("dialog-box");
- Label lblFileTitle = new Label("DOCUMENTO PDF A ASSINAR");
- lblFileTitle.getStyleClass().add("dialog-field-title");
+ // Coluna Direita: Visualizador Interativo com Zoom
+ VBox pageViewerColumn = buildInteractiveViewerColumn();
+ HBox.setHgrow(pageViewerColumn, Priority.ALWAYS);
- HBox fileRow = new HBox(10);
- fileRow.setAlignment(Pos.CENTER_LEFT);
- txtSourcePdf = new TextField();
- txtSourcePdf.setPromptText("Selecione o arquivo PDF...");
- txtSourcePdf.setEditable(false);
- HBox.setHgrow(txtSourcePdf, Priority.ALWAYS);
+ body.getChildren().addAll(leftScroll, pageViewerColumn);
- Button btnBrowse = new Button("Procurar PDF...");
- btnBrowse.getStyleClass().add("btn-secondary");
- btnBrowse.setOnAction(e -> handleBrowseSourcePdf());
- fileRow.getChildren().addAll(txtSourcePdf, btnBrowse);
- fileSection.getChildren().addAll(lblFileTitle, fileRow);
+ // 4. Rodapé
+ HBox footer = new HBox(12);
+ footer.setAlignment(Pos.CENTER_RIGHT);
+ footer.setPadding(new Insets(6, 0, 0, 0));
- // 2. Certificado Digital
- VBox certSection = new VBox(8);
- certSection.getStyleClass().add("dialog-box");
- Label lblCertTitle = new Label("CERTIFICADO DIGITAL ICP-BRASIL");
- lblCertTitle.getStyleClass().add("dialog-field-title");
+ btnCancel = new Button("Cancelar");
+ btnCancel.getStyleClass().addAll("btn-secondary", "btn-dialog");
+ btnCancel.setMinWidth(110);
+ btnCancel.setOnAction(e -> stage.close());
- certCombo = new ComboBox<>();
- certCombo.setMaxWidth(Double.MAX_VALUE);
- populateCertificates();
- certCombo.setOnAction(e -> {
- updatePinFieldVisibility();
- updateSealPreview();
- });
+ HBox btnNextContent = new HBox(8);
+ btnNextContent.setAlignment(Pos.CENTER);
+ Label lblBtnNextText = new Label("Avançar");
+ lblBtnNextText.setStyle("-fx-font-weight: bold; -fx-font-size: 12px; -fx-text-fill: #ffffff;");
+ btnNextContent.getChildren().addAll(lblBtnNextText, SvgIcons.arrowRightIcon(12));
- certCombo.setCellFactory(lv -> new ListCell<>() {
- @Override
- protected void updateItem(ManagedCertificate item, boolean empty) {
- super.updateItem(item, empty);
- if (empty || item == null) {
- setText(null);
- } else {
- String doc = item.cpf() != null ? " [CPF: " + item.cpf() + "]" : (item.cnpj() != null ? " [CNPJ: " + item.cnpj() + "]" : "");
- setText(item.commonName() + doc + " (" + item.type() + ")");
- }
- }
- });
- certCombo.setButtonCell(new ListCell<>() {
- @Override
- protected void updateItem(ManagedCertificate item, boolean empty) {
- super.updateItem(item, empty);
- if (empty || item == null) {
- setText(null);
- } else {
- String doc = item.cpf() != null ? " [CPF: " + item.cpf() + "]" : (item.cnpj() != null ? " [CNPJ: " + item.cnpj() + "]" : "");
- setText(item.commonName() + doc + " (" + item.type() + ")");
- }
- }
- });
+ btnNextToCert = new Button();
+ btnNextToCert.setGraphic(btnNextContent);
+ btnNextToCert.getStyleClass().addAll("btn-primary", "btn-dialog-wide");
+ btnNextToCert.setMinWidth(240);
+ btnNextToCert.setPrefWidth(250);
+ btnNextToCert.setDefaultButton(true);
+ btnNextToCert.setOnAction(e -> handleAdvanceToCertificate());
- HBox pinRow = new HBox(10);
- pinRow.setAlignment(Pos.CENTER_LEFT);
- Label lblPin = new Label("PIN / Senha:");
- lblPin.getStyleClass().add("dialog-field-title");
- pinField = new PasswordField();
- pinField.setPromptText("PIN ou senha do certificado...");
- pinField.setPrefWidth(200);
+ Region footerSpacer = new Region();
+ HBox.setHgrow(footerSpacer, Priority.ALWAYS);
- lblPinNotice = new Label();
- lblPinNotice.setStyle("-fx-font-size: 11px;");
- pinRow.getChildren().addAll(lblPin, pinField, lblPinNotice);
+ footer.getChildren().addAll(footerSpacer, btnCancel, btnNextToCert);
- certSection.getChildren().addAll(lblCertTitle, certCombo, pinRow);
+ root.getChildren().addAll(header, docBar, body, footer);
- // 3. Configurações de Selo Visual
- VBox sealSection = new VBox(10);
+ Scene scene = new Scene(root, 1020, 680);
+ ThemeManager.applyTheme(scene);
+ stage.setScene(scene);
+
+ updateSealPreview();
+ }
+
+ private VBox buildSealOptionsSection() {
+ VBox sealSection = new VBox(12);
sealSection.getStyleClass().add("dialog-box");
+ sealSection.setPadding(new Insets(12));
HBox sealHeader = new HBox(10);
sealHeader.setAlignment(Pos.CENTER_LEFT);
- Label lblSealTitle = new Label("OPÇÕES DO SELO VISUAL (SVG)");
+ Label lblSealTitle = new Label("SELO VISUAL");
lblSealTitle.getStyleClass().add("dialog-field-title");
HBox.setHgrow(lblSealTitle, Priority.ALWAYS);
@@ -262,134 +296,67 @@ public final class PdfSignDialog {
chkVisibleSeal.setOnAction(e -> toggleVisibleSeal(chkVisibleSeal.isSelected()));
sealHeader.getChildren().addAll(lblSealTitle, chkVisibleSeal);
+ // Card do Selo Padrão das Configurações
+ VBox defaultSealCard = new VBox(6);
+ defaultSealCard.getStyleClass().add("diagnostic-item");
+ defaultSealCard.setPadding(new Insets(10));
+
+ HBox themeHeader = new HBox(8);
+ themeHeader.setAlignment(Pos.CENTER_LEFT);
+ Label lblDefaultBadge = new Label("★ PADRÃO");
+ lblDefaultBadge.setStyle("-fx-background-color: #0284c7; -fx-text-fill: #ffffff; -fx-padding: 2px 6px; -fx-background-radius: 4px; -fx-font-weight: bold; -fx-font-size: 10px;");
+
+ lblSealThemeName = new Label();
+ lblSealThemeName.getStyleClass().add("cert-title");
+ lblSealThemeName.setStyle("-fx-font-size: 12.5px; -fx-font-weight: bold;");
+ themeHeader.getChildren().addAll(lblDefaultBadge, lblSealThemeName);
+
+ lblSealThemeDesc = new Label();
+ lblSealThemeDesc.getStyleClass().add("cert-detail");
+ lblSealThemeDesc.setWrapText(true);
+
+ Button btnManageInSettings = new Button("⚙ Mudar Selo nas Configurações");
+ btnManageInSettings.getStyleClass().addAll("btn-secondary", "btn-compact");
+ btnManageInSettings.setStyle("-fx-font-size: 11px;");
+ btnManageInSettings.setOnAction(e -> {
+ new SealManagementDialog(stage).showAndWait();
+ reloadActiveDefaultTheme();
+ });
+
+ defaultSealCard.getChildren().addAll(themeHeader, lblSealThemeDesc, btnManageInSettings);
+
+ // Preview em miniatura do SVG
+ VBox previewBox = new VBox(4);
+ previewBox.setAlignment(Pos.CENTER);
+ previewBox.setStyle("-fx-background-color: -fx-bg-primary; -fx-border-color: -fx-border-light; -fx-border-radius: 6px; -fx-padding: 8px;");
+ sealPreviewImageView = new ImageView();
+ sealPreviewImageView.setPreserveRatio(true);
+ sealPreviewImageView.setFitWidth(260);
+ sealPreviewImageView.setFitHeight(75);
+ previewBox.getChildren().add(sealPreviewImageView);
+
+ // Opções Adicionais Rápidas
GridPane grid = new GridPane();
- grid.setHgap(10);
+ grid.setHgap(8);
grid.setVgap(8);
- Label lblTheme = new Label("Tema SVG:");
- lblTheme.getStyleClass().add("dialog-field-title");
+ ColumnConstraints colLabel = new ColumnConstraints();
+ colLabel.setMinWidth(90);
+ colLabel.setPrefWidth(95);
+ colLabel.setHgrow(Priority.NEVER);
- HBox themeRow = new HBox(8);
- themeRow.setAlignment(Pos.CENTER_LEFT);
+ ColumnConstraints colField = new ColumnConstraints();
+ colField.setHgrow(Priority.ALWAYS);
+ grid.getColumnConstraints().addAll(colLabel, colField);
- AgentConfiguration agentConfig = new AgentConfigurationService().loadConfiguration();
- PdfSealTheme initialTheme = PdfSealThemeRegistry.getTheme(agentConfig.defaultSealThemeId())
- .orElse(PdfSealThemeRegistry.getDefaultTheme());
-
- themeCombo = new ComboBox<>(FXCollections.observableArrayList(PdfSealThemeRegistry.listThemes()));
- themeCombo.setMaxWidth(Double.MAX_VALUE);
- themeCombo.setValue(initialTheme);
- themeCombo.setOnAction(e -> {
- PdfSealTheme th = themeCombo.getValue();
- if (th != null) {
- if (th.defaultCpfMode() != null) {
- cpfModeCombo.setValue(th.defaultCpfMode());
- }
- chkDateTime.setSelected(th.defaultIncludeDateTime());
- if (txtCustomInfo != null) {
- txtCustomInfo.setText(th.formatCustomField());
- }
- }
- updateSealPreview();
- });
- HBox.setHgrow(themeCombo, Priority.ALWAYS);
-
- Button btnManageSeals = new Button("⚙ Selos");
- btnManageSeals.getStyleClass().add("btn-secondary");
- btnManageSeals.setStyle("-fx-padding: 4px 8px; -fx-font-size: 11px;");
- btnManageSeals.setOnAction(e -> {
- new SealManagementDialog(stage).showAndWait();
- AgentConfiguration updatedCfg = new AgentConfigurationService().loadConfiguration();
- themeCombo.setItems(FXCollections.observableArrayList(PdfSealThemeRegistry.listThemes()));
- PdfSealTheme activeTheme = PdfSealThemeRegistry.getTheme(updatedCfg.defaultSealThemeId())
- .orElse(PdfSealThemeRegistry.getDefaultTheme());
- themeCombo.setValue(activeTheme);
- if (txtCustomInfo != null) {
- txtCustomInfo.setText(activeTheme.formatCustomField());
- }
- updateSealPreview();
- });
-
- Button btnNewSeal = new Button("🎨 Novo");
- btnNewSeal.getStyleClass().add("btn-secondary");
- btnNewSeal.setStyle("-fx-padding: 4px 8px; -fx-font-size: 11px;");
- btnNewSeal.setOnAction(e -> {
- new SealDesignerDialog(stage, createdTheme -> {
- themeCombo.setItems(FXCollections.observableArrayList(PdfSealThemeRegistry.listThemes()));
- themeCombo.setValue(createdTheme);
- if (txtCustomInfo != null) {
- txtCustomInfo.setText(createdTheme.formatCustomField());
- }
- updateSealPreview();
- }).show();
- });
-
- themeRow.getChildren().addAll(themeCombo, btnManageSeals, btnNewSeal);
-
- themeCombo.setCellFactory(lv -> new ListCell<>() {
- @Override
- protected void updateItem(PdfSealTheme item, boolean empty) {
- super.updateItem(item, empty);
- if (empty || item == null) {
- setText(null);
- } else {
- setText(item.name() + " (" + item.description() + ")");
- }
- }
- });
- themeCombo.setButtonCell(new ListCell<>() {
- @Override
- protected void updateItem(PdfSealTheme item, boolean empty) {
- super.updateItem(item, empty);
- if (empty || item == null) {
- setText(null);
- } else {
- setText(item.name());
- }
- }
- });
-
- // Controles de CPF e Data/Hora
- Label lblCpfMode = new Label("Exibição do CPF:");
- lblCpfMode.getStyleClass().add("dialog-field-title");
-
- cpfModeCombo = new ComboBox<>(FXCollections.observableArrayList(CpfDisplayMode.values()));
- cpfModeCombo.setMaxWidth(Double.MAX_VALUE);
- cpfModeCombo.setValue(initialTheme.defaultCpfMode() != null ? initialTheme.defaultCpfMode() : CpfDisplayMode.FULL);
- cpfModeCombo.setCellFactory(lv -> new ListCell<>() {
- @Override
- protected void updateItem(CpfDisplayMode item, boolean empty) {
- super.updateItem(item, empty);
- setText((empty || item == null) ? null : item.getDescription());
- }
- });
- cpfModeCombo.setButtonCell(new ListCell<>() {
- @Override
- protected void updateItem(CpfDisplayMode item, boolean empty) {
- super.updateItem(item, empty);
- setText((empty || item == null) ? null : item.getDescription());
- }
- });
- cpfModeCombo.setOnAction(e -> updateSealPreview());
-
- Label lblCustomInfo = new Label("Matrícula / Cargo:");
- lblCustomInfo.getStyleClass().add("dialog-field-title");
- txtCustomInfo = new TextField(initialTheme.formatCustomField());
- txtCustomInfo.setPromptText("Opcional (ex: Matrícula: 104829 ou Cargo: Auditor)");
- txtCustomInfo.textProperty().addListener((obs, oldV, newV) -> updateSealPreview());
-
- chkDateTime = new CheckBox("Estampar Data e Hora no Selo");
- chkDateTime.setSelected(initialTheme.defaultIncludeDateTime());
- chkDateTime.setOnAction(e -> updateSealPreview());
-
- Label lblPosition = new Label("Posicionamento:");
+ Label lblPosition = new Label("Posição Fixa:");
lblPosition.getStyleClass().add("dialog-field-title");
positionCombo = new ComboBox<>(FXCollections.observableArrayList(PositionOption.values()));
positionCombo.setMaxWidth(Double.MAX_VALUE);
positionCombo.setValue(PositionOption.BOTTOM_RIGHT);
positionCombo.setOnAction(e -> handlePositionComboSelection());
- Label lblPage = new Label("Página Padrão:");
+ Label lblPage = new Label("Página:");
lblPage.getStyleClass().add("dialog-field-title");
pageCombo = new ComboBox<>(FXCollections.observableArrayList(PageOption.values()));
pageCombo.setMaxWidth(Double.MAX_VALUE);
@@ -405,248 +372,290 @@ public final class PdfSignDialog {
txtLocation = new TextField("Brasil");
txtLocation.textProperty().addListener((obs, oldV, newV) -> updateSealPreview());
- grid.add(lblTheme, 0, 0);
- grid.add(themeRow, 1, 0);
- grid.add(lblCpfMode, 0, 1);
- grid.add(cpfModeCombo, 1, 1);
- grid.add(lblCustomInfo, 0, 2);
- grid.add(txtCustomInfo, 1, 2);
- Label lblDt = new Label("Data/Hora:");
- lblDt.getStyleClass().add("dialog-field-title");
- grid.add(lblDt, 0, 3);
- grid.add(chkDateTime, 1, 3);
- grid.add(lblPosition, 0, 4);
- grid.add(positionCombo, 1, 4);
- grid.add(lblPage, 0, 5);
- grid.add(pageCombo, 1, 5);
- grid.add(lblReason, 0, 6);
- grid.add(txtReason, 1, 6);
- grid.add(lblLocation, 0, 7);
- grid.add(txtLocation, 1, 7);
+ 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);
- // Preview em miniatura do SVG
- VBox previewBox = new VBox(4);
- previewBox.setAlignment(Pos.CENTER);
- previewBox.setStyle("-fx-background-color: -fx-bg-primary; -fx-border-color: -fx-border-light; -fx-border-radius: 6px; -fx-padding: 8px;");
- sealPreviewImageView = new ImageView();
- sealPreviewImageView.setPreserveRatio(true);
- sealPreviewImageView.setFitWidth(280);
- sealPreviewImageView.setFitHeight(90);
- previewBox.getChildren().add(sealPreviewImageView);
+ reloadActiveDefaultTheme();
- sealSection.getChildren().addAll(sealHeader, grid, previewBox);
+ sealSection.getChildren().addAll(sealHeader, defaultSealCard, previewBox, grid);
+ return sealSection;
+ }
- formContent.getChildren().addAll(fileSection, certSection, sealSection);
- leftScroll.setContent(formContent);
+ private void reloadActiveDefaultTheme() {
+ AgentConfiguration cfg = new AgentConfigurationService().loadConfiguration();
+ this.activeSealTheme = PdfSealThemeRegistry.getTheme(cfg.defaultSealThemeId())
+ .orElse(PdfSealThemeRegistry.getDefaultTheme());
+ if (lblSealThemeName != null) {
+ lblSealThemeName.setText(activeSealTheme.name());
+ }
+ if (lblSealThemeDesc != null) {
+ lblSealThemeDesc.setText(activeSealTheme.description() != null ? activeSealTheme.description() : "Tema padrão");
+ }
+ if (selectedPdfBytes != null) {
+ recalculateBaseDimensions(currentPagePdfWidth, currentPagePdfHeight);
+ updateContainerZoomDimensions();
+ repositionSealByRelativeCoords();
+ }
+ updateSealPreview();
+ }
- // =========================================================================
- // COLUNA DA DIREITA: Visualizador Interativo da Página do PDF (Folha)
- // =========================================================================
- pageViewerColumn = new VBox(10);
- pageViewerColumn.getStyleClass().add("dialog-box");
- HBox.setHgrow(pageViewerColumn, Priority.ALWAYS);
+ private VBox buildInteractiveViewerColumn() {
+ VBox column = new VBox(10);
+ column.getStyleClass().add("dialog-box");
+ column.setPadding(new Insets(10));
- // Barra superior do visualizador
- HBox viewerBar = new HBox(12);
- viewerBar.setAlignment(Pos.CENTER_LEFT);
+ // Barra de Ferramentas Superior: Navegação de Páginas + Controles de Zoom + Posição
+ HBox toolbar = new HBox(12);
+ toolbar.setAlignment(Pos.CENTER_LEFT);
- Label lblViewerTitle = new Label("POSICIONAMENTO INTERATIVO NA FOLHA");
- lblViewerTitle.getStyleClass().add("dialog-field-title");
-
- HBox pageNavBox = new HBox(8);
+ // Grupo de Navegação
+ HBox pageNavBox = new HBox(6);
pageNavBox.setAlignment(Pos.CENTER_LEFT);
- btnPrevPage = new Button("◀ Anterior");
- btnPrevPage.getStyleClass().add("btn-secondary");
+ btnPrevPage = new Button("◀");
+ btnPrevPage.getStyleClass().addAll("btn-secondary", "btn-compact");
+ btnPrevPage.setTooltip(new Tooltip("Página Anterior"));
btnPrevPage.setDisable(true);
btnPrevPage.setOnAction(e -> navigatePage(-1));
- lblPageIndicator = new Label("Nenhum PDF carregado");
- lblPageIndicator.setStyle("-fx-font-weight: bold; -fx-font-size: 12px;");
+ lblPageIndicator = new Label("Nenhum PDF");
+ lblPageIndicator.setStyle("-fx-font-weight: bold; -fx-font-size: 11.5px;");
- btnNextPage = new Button("Próxima ▶");
- btnNextPage.getStyleClass().add("btn-secondary");
+ btnNextPage = new Button("▶");
+ btnNextPage.getStyleClass().addAll("btn-secondary", "btn-compact");
+ btnNextPage.setTooltip(new Tooltip("Próxima Página"));
btnNextPage.setDisable(true);
btnNextPage.setOnAction(e -> navigatePage(1));
pageLoadingIndicator = new ProgressIndicator();
- pageLoadingIndicator.setPrefSize(18, 18);
+ pageLoadingIndicator.setPrefSize(16, 16);
pageLoadingIndicator.setVisible(false);
pageNavBox.getChildren().addAll(btnPrevPage, lblPageIndicator, btnNextPage, pageLoadingIndicator);
- HBox.setHgrow(pageNavBox, Priority.ALWAYS);
+ // Grupo de Zoom da Folha
+ HBox zoomBox = new HBox(5);
+ zoomBox.setAlignment(Pos.CENTER_LEFT);
+ zoomBox.setStyle("-fx-background-color: -fx-bg-primary; -fx-border-color: -fx-border-light; -fx-border-radius: 6px; -fx-padding: 2px 6px;");
+
+ btnZoomOut = new Button();
+ btnZoomOut.setGraphic(SvgIcons.zoomOutIcon(13));
+ btnZoomOut.getStyleClass().addAll("btn-secondary", "btn-compact");
+ btnZoomOut.setTooltip(new Tooltip("Diminuir Zoom"));
+ btnZoomOut.setOnAction(e -> adjustZoom(-ZOOM_STEP));
+
+ lblZoomValue = new Label("100%");
+ lblZoomValue.setStyle("-fx-font-size: 11px; -fx-font-weight: bold; -fx-min-width: 38px; -fx-alignment: center;");
+
+ btnZoomIn = new Button();
+ btnZoomIn.setGraphic(SvgIcons.zoomInIcon(13));
+ btnZoomIn.getStyleClass().addAll("btn-secondary", "btn-compact");
+ btnZoomIn.setTooltip(new Tooltip("Aumentar Zoom"));
+ btnZoomIn.setOnAction(e -> adjustZoom(ZOOM_STEP));
+
+ btnZoomFit = new Button("Ajustar");
+ btnZoomFit.setGraphic(SvgIcons.zoomFitIcon(12));
+ btnZoomFit.getStyleClass().addAll("btn-secondary", "btn-compact");
+ btnZoomFit.setTooltip(new Tooltip("Ajustar à janela"));
+ btnZoomFit.setOnAction(e -> setZoom(1.0));
+
+ zoomBox.getChildren().addAll(btnZoomOut, lblZoomValue, btnZoomIn, btnZoomFit);
+
+ Region tbSpacer = new Region();
+ HBox.setHgrow(tbSpacer, Priority.ALWAYS);
lblPositionFeedback = new Label("👉 Clique na folha para posicionar");
lblPositionFeedback.setStyle("-fx-font-size: 11.5px; -fx-text-fill: -fx-accent; -fx-font-weight: bold;");
- viewerBar.getChildren().addAll(lblViewerTitle, pageNavBox, lblPositionFeedback);
+ toolbar.getChildren().addAll(pageNavBox, zoomBox, tbSpacer, lblPositionFeedback);
- // Área da Folha de Papel Centralizada com ScrollPane
- ScrollPane sheetScroll = new ScrollPane();
- sheetScroll.setFitToWidth(true);
- sheetScroll.setFitToHeight(true);
+ // ScrollPane da Folha de Papel
+ sheetScroll = new ScrollPane();
+ sheetScroll.setFitToWidth(false);
+ sheetScroll.setFitToHeight(false);
+ sheetScroll.setPannable(false);
sheetScroll.setStyle("-fx-background-color: transparent; -fx-background: transparent; -fx-border-color: transparent;");
VBox.setVgrow(sheetScroll, Priority.ALWAYS);
+ // Permite atalho de Zoom com Ctrl + Rolagem do Mouse
+ sheetScroll.addEventFilter(ScrollEvent.SCROLL, event -> {
+ if (event.isControlDown()) {
+ event.consume();
+ if (event.getDeltaY() > 0) {
+ adjustZoom(ZOOM_STEP);
+ } else if (event.getDeltaY() < 0) {
+ adjustZoom(-ZOOM_STEP);
+ }
+ }
+ });
+
StackPane sheetWrapper = new StackPane();
sheetWrapper.setAlignment(Pos.CENTER);
- sheetWrapper.setPadding(new Insets(10));
+ sheetWrapper.setPadding(new Insets(14));
pageSheetContainer = new StackPane();
pageSheetContainer.setAlignment(Pos.TOP_LEFT);
pageSheetContainer.setStyle("-fx-background-color: #ffffff; -fx-border-color: #94a3b8; -fx-border-width: 1px;");
- DropShadow sheetShadow = new DropShadow(14, 0, 4, Color.rgb(0, 0, 0, 0.28));
+ DropShadow sheetShadow = new DropShadow(16, 0, 4, Color.rgb(0, 0, 0, 0.30));
pageSheetContainer.setEffect(sheetShadow);
- pageSheetContainer.setMaxWidth(RegionConstant.PREVIEW_WIDTH);
- pageSheetContainer.setMaxHeight(RegionConstant.PREVIEW_HEIGHT);
+ updateContainerZoomDimensions();
- // Imagem da página do PDF
pageImageView = new ImageView();
pageImageView.setPreserveRatio(true);
- pageImageView.setFitWidth(RegionConstant.PREVIEW_WIDTH);
- pageImageView.setFitHeight(RegionConstant.PREVIEW_HEIGHT);
- // Overlay Interativo que recebe os cliques e arrasto
interactiveOverlayPane = new Pane();
interactiveOverlayPane.setCursor(Cursor.CROSSHAIR);
interactiveOverlayPane.setOnMouseClicked(this::handleSheetClick);
- interactiveOverlayPane.setOnMouseDragged(this::handleSheetDrag);
- // Selo visual móvel sobreposto à folha
sealStampOverlayView = new ImageView();
sealStampOverlayView.setPreserveRatio(true);
- sealStampOverlayView.setFitWidth(RegionConstant.SEAL_WIDTH);
- sealStampOverlayView.setFitHeight(RegionConstant.SEAL_HEIGHT);
sealStampOverlayView.setCursor(Cursor.MOVE);
- DropShadow sealShadow = new DropShadow(8, 0, 2, Color.rgb(2, 132, 199, 0.6));
+ DropShadow sealShadow = new DropShadow(8, 0, 2, Color.rgb(2, 132, 199, 0.65));
sealStampOverlayView.setEffect(sealShadow);
sealStampOverlayView.setVisible(false);
- // Permite também arrastar diretamente clicando no selo
- sealStampOverlayView.setOnMouseDragged(this::handleSealDrag);
+ // Arraste 100% suave com âncora fixa na cena (elimina totalmente o tremor/flicker)
+ sealStampOverlayView.setOnMousePressed(event -> {
+ dragStartSceneX = event.getSceneX();
+ dragStartSceneY = event.getSceneY();
+ initialSealLayoutX = sealStampOverlayView.getLayoutX();
+ initialSealLayoutY = sealStampOverlayView.getLayoutY();
+ 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);
+ event.consume();
+ });
interactiveOverlayPane.getChildren().add(sealStampOverlayView);
pageSheetContainer.getChildren().addAll(pageImageView, interactiveOverlayPane);
sheetWrapper.getChildren().add(pageSheetContainer);
sheetScroll.setContent(sheetWrapper);
- pageViewerColumn.getChildren().addAll(viewerBar, sheetScroll);
-
- body.getChildren().addAll(leftScroll, pageViewerColumn);
-
- // =========================================================================
- // RODAPÉ: Status e Botões de Ação
- // =========================================================================
- HBox footer = new HBox(12);
- footer.setAlignment(Pos.CENTER_RIGHT);
-
- progressIndicator = new ProgressIndicator();
- progressIndicator.setPrefSize(22, 22);
- progressIndicator.setVisible(false);
-
- lblStatus = new Label();
- lblStatus.setStyle("-fx-font-size: 12px;");
- HBox.setHgrow(lblStatus, Priority.ALWAYS);
-
- btnCancel = new Button("Cancelar");
- btnCancel.getStyleClass().add("btn-secondary");
- btnCancel.setOnAction(e -> stage.close());
-
- btnSign = new Button("Assinar Documento PDF");
- btnSign.getStyleClass().add("btn-primary");
- btnSign.setDefaultButton(true);
- btnSign.setOnAction(e -> executePdfSignature());
-
- footer.getChildren().addAll(progressIndicator, lblStatus, btnCancel, btnSign);
-
- root.getChildren().addAll(header, body, footer);
-
- Scene scene = new Scene(root, 900, 570);
- ThemeManager.applyTheme(scene);
- stage.setScene(scene);
-
- updatePinFieldVisibility();
- updateSealPreview();
+ column.getChildren().addAll(toolbar, sheetScroll);
+ return column;
}
- private static class RegionConstant {
- static final double PREVIEW_WIDTH = 360.0;
- static final double PREVIEW_HEIGHT = 510.0;
- static final double SEAL_WIDTH = 150.0;
- static final double SEAL_HEIGHT = 45.0;
- static final float PDF_DEFAULT_PAGE_WIDTH = 595.28f;
- static final float PDF_DEFAULT_PAGE_HEIGHT = 841.89f;
- static final float PDF_SEAL_WIDTH = 144.0f;
- static final float PDF_SEAL_HEIGHT = 42.0f;
+ private void adjustZoom(double delta) {
+ setZoom(Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, this.zoomFactor + delta)));
}
+ private void setZoom(double newZoom) {
+ this.zoomFactor = Math.round(newZoom * 100.0) / 100.0;
+ lblZoomValue.setText(String.format("%.0f%%", zoomFactor * 100));
+
+ updateContainerZoomDimensions();
+ repositionSealByRelativeCoords();
+ }
+
+ private void updateContainerZoomDimensions() {
+ double currentW = currentBaseSheetWidth * zoomFactor;
+ double currentH = currentBaseSheetHeight * zoomFactor;
+
+ pageSheetContainer.setPrefSize(currentW, currentH);
+ pageSheetContainer.setMaxSize(currentW, currentH);
+ pageSheetContainer.setMinSize(currentW, currentH);
+
+ if (interactiveOverlayPane != null) {
+ interactiveOverlayPane.setPrefSize(currentW, currentH);
+ interactiveOverlayPane.setMaxSize(currentW, currentH);
+ }
+ if (pageImageView != null) {
+ pageImageView.setFitWidth(currentW);
+ pageImageView.setFitHeight(currentH);
+ }
+ if (sealStampOverlayView != null) {
+ sealStampOverlayView.setFitWidth(currentBaseSealWidth * zoomFactor);
+ sealStampOverlayView.setFitHeight(currentBaseSealHeight * zoomFactor);
+ }
+ }
+
+ private void repositionSealByRelativeCoords() {
+ if (!chkVisibleSeal.isSelected() || selectedPdfBytes == null) return;
+
+ double sheetW = currentBaseSheetWidth * zoomFactor;
+ double sheetH = currentBaseSheetHeight * zoomFactor;
+ double sealW = currentBaseSealWidth * zoomFactor;
+ double sealH = currentBaseSealHeight * zoomFactor;
+
+ double posX = Math.max(0, Math.min(sealRelativeX * sheetW, sheetW - sealW));
+ double posY = Math.max(0, Math.min(sealRelativeY * sheetH, sheetH - sealH));
+
+ sealStampOverlayView.setLayoutX(posX);
+ sealStampOverlayView.setLayoutY(posY);
+ sealStampOverlayView.setVisible(true);
+ }
+
+ // =========================================================================
+ // CARREGAMENTO DO PDF
+ // =========================================================================
private void handleBrowseSourcePdf() {
FileChooser chooser = new FileChooser();
chooser.setTitle("Selecionar Documento PDF");
- chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Documentos PDF (*.pdf)", "*.pdf"));
+ chooser.getExtensionFilters().addAll(
+ new FileChooser.ExtensionFilter("Documentos PDF (*.pdf)", "*.pdf"),
+ new FileChooser.ExtensionFilter("Todos os Arquivos (*.*)", "*.*")
+ );
+
+ if (selectedSourceFile != null && selectedSourceFile.getParentFile() != null) {
+ chooser.setInitialDirectory(selectedSourceFile.getParentFile());
+ }
File file = chooser.showOpenDialog(stage);
if (file != null) {
- selectedSourceFile = file;
- txtSourcePdf.setText(file.getAbsolutePath());
loadPdfDocument(file);
}
}
- private void loadPdfDocument(File file) {
+ public void loadPdfDocument(File file) {
try {
+ selectedSourceFile = file;
selectedPdfBytes = Files.readAllBytes(file.toPath());
totalPages = PdfSigningService.getPageCount(selectedPdfBytes);
currentPageIndex = 0;
+ lblDocName.setText(file.getName());
+ long kb = file.length() / 1024;
+ lblDocMeta.setText(String.format("%s | %d KB | %d página(s)", file.getAbsolutePath(), kb, totalPages));
+ btnChangeDoc.setText("Trocar PDF...");
+
btnPrevPage.setDisable(true);
btnNextPage.setDisable(totalPages <= 1);
- lblPageIndicator.setText(String.format("Página 1 de %d", totalPages));
+ lblPageIndicator.setText(String.format("Pág. 1 de %d", totalPages));
- // Renderiza primeira página
renderCurrentPage();
- // Ativa o selo visual na folha
if (chkVisibleSeal.isSelected()) {
sealStampOverlayView.setVisible(true);
- // Posiciona no canto inferior direito inicialmente
resetSealToCornerPosition(PositionOption.BOTTOM_RIGHT);
}
} catch (Exception ex) {
- logger.error("Erro ao carregar documento PDF para pré-visualização", ex);
+ logger.error("Erro ao carregar documento PDF", ex);
showAlert(Alert.AlertType.ERROR, "Erro ao Ler PDF", "Não foi possível carregar o arquivo PDF: " + ex.getMessage());
}
}
- private void navigatePage(int delta) {
- int target = currentPageIndex + delta;
- if (target >= 0 && target < totalPages) {
- currentPageIndex = target;
- btnPrevPage.setDisable(currentPageIndex == 0);
- btnNextPage.setDisable(currentPageIndex == totalPages - 1);
- lblPageIndicator.setText(String.format("Página %d de %d", currentPageIndex + 1, totalPages));
-
- renderCurrentPage();
-
- // Se o selo já estava posicionado customizadamente, atualiza o número da página alvo
- if (isCustomPositionActive) {
- customPageNumber = currentPageIndex + 1;
- updateFeedbackLabel();
- }
- }
- }
-
private void renderCurrentPage() {
if (selectedPdfBytes == null) return;
pageLoadingIndicator.setVisible(true);
int pageToRender = currentPageIndex;
+ // Renderiza em alta resolução (140 DPI) para texto nítido mesmo em zoom alto
Task renderTask = new Task<>() {
@Override
protected Image call() throws Exception {
- byte[] pngBytes = PdfSigningService.renderPagePngBytes(selectedPdfBytes, pageToRender, 96f);
+ byte[] pngBytes = PdfSigningService.renderPagePngBytes(selectedPdfBytes, pageToRender, 140f);
return new Image(new ByteArrayInputStream(pngBytes));
}
};
@@ -656,13 +665,19 @@ public final class PdfSignDialog {
Image img = renderTask.getValue();
pageImageView.setImage(img);
- // Ajusta dimensões do container para a proporção real da página
- double viewW = pageImageView.getBoundsInParent().getWidth();
- double viewH = pageImageView.getBoundsInParent().getHeight();
- if (viewW > 0 && viewH > 0) {
- interactiveOverlayPane.setPrefSize(viewW, viewH);
- pageSheetContainer.setPrefSize(viewW, viewH);
+ try {
+ PdfSigningService.PageDimensions dims = PdfSigningService.getPageDimensions(selectedPdfBytes, pageToRender);
+ currentPagePdfWidth = dims.width();
+ currentPagePdfHeight = dims.height();
+ } catch (Exception ex) {
+ logger.warn("Dimensões da página {} não puderam ser lidas via PDFBox, usando imagem", pageToRender);
+ currentPagePdfWidth = (float) img.getWidth();
+ currentPagePdfHeight = (float) img.getHeight();
}
+
+ recalculateBaseDimensions(currentPagePdfWidth, currentPagePdfHeight);
+ updateContainerZoomDimensions();
+ repositionSealByRelativeCoords();
});
renderTask.setOnFailed(e -> {
@@ -675,56 +690,83 @@ public final class PdfSignDialog {
th.start();
}
+ private void recalculateBaseDimensions(float pdfW, float pdfH) {
+ double ratio = (double) pdfW / (double) pdfH;
+ if (ratio >= 1.0) {
+ // Documento em Orientação Paisagem (Landscape)
+ currentBaseSheetWidth = 640.0;
+ currentBaseSheetHeight = currentBaseSheetWidth / ratio;
+ } else {
+ // Documento em Orientação Retrato (Portrait)
+ currentBaseSheetWidth = 490.0;
+ currentBaseSheetHeight = currentBaseSheetWidth / ratio;
+ }
+
+ PdfSealTheme activeTheme = activeSealTheme != null ? activeSealTheme : PdfSealThemeRegistry.getDefaultTheme();
+ double sealPdfW = activeTheme.width() * 0.72;
+ double sealPdfH = activeTheme.height() * 0.72;
+
+ // Proporção exata do selo em relação à página PDF nativa
+ double sealFractionOfPage = sealPdfW / pdfW;
+ currentBaseSealWidth = currentBaseSheetWidth * sealFractionOfPage;
+ currentBaseSealHeight = currentBaseSealWidth * (sealPdfH / sealPdfW);
+ }
+
+ private void navigatePage(int delta) {
+ int target = currentPageIndex + delta;
+ if (target >= 0 && target < totalPages) {
+ currentPageIndex = target;
+ btnPrevPage.setDisable(currentPageIndex == 0);
+ btnNextPage.setDisable(currentPageIndex == totalPages - 1);
+ lblPageIndicator.setText(String.format("Pág. %d de %d", currentPageIndex + 1, totalPages));
+
+ renderCurrentPage();
+
+ if (isCustomPositionActive) {
+ customPageNumber = currentPageIndex + 1;
+ updateFeedbackLabel();
+ }
+ }
+ }
+
+ // =========================================================================
+ // INTERAÇÃO E POSICIONAMENTO DO SELO
+ // =========================================================================
private void handleSheetClick(MouseEvent event) {
if (!chkVisibleSeal.isSelected() || selectedPdfBytes == null) return;
- placeSealAtViewCoordinates(event.getX(), event.getY());
+ double sealW = currentBaseSealWidth * zoomFactor;
+ double sealH = currentBaseSealHeight * zoomFactor;
+ updateSealPositionFromDrag(event.getX() - sealW / 2.0, event.getY() - sealH / 2.0);
}
- private void handleSheetDrag(MouseEvent event) {
- if (!chkVisibleSeal.isSelected() || selectedPdfBytes == null) return;
- placeSealAtViewCoordinates(event.getX(), event.getY());
- }
+ private void updateSealPositionFromDrag(double newLayoutX, double newLayoutY) {
+ double sheetW = currentBaseSheetWidth * zoomFactor;
+ double sheetH = currentBaseSheetHeight * zoomFactor;
+ double sealW = currentBaseSealWidth * zoomFactor;
+ double sealH = currentBaseSealHeight * zoomFactor;
- private void handleSealDrag(MouseEvent event) {
- if (!chkVisibleSeal.isSelected() || selectedPdfBytes == null) return;
- double currentX = sealStampOverlayView.getLayoutX() + event.getX();
- double currentY = sealStampOverlayView.getLayoutY() + event.getY();
- placeSealAtViewCoordinates(currentX, currentY);
- }
-
- private void placeSealAtViewCoordinates(double viewX, double viewY) {
- double sheetW = interactiveOverlayPane.getWidth() > 0 ? interactiveOverlayPane.getWidth() : RegionConstant.PREVIEW_WIDTH;
- double sheetH = interactiveOverlayPane.getHeight() > 0 ? interactiveOverlayPane.getHeight() : RegionConstant.PREVIEW_HEIGHT;
-
- double sealW = RegionConstant.SEAL_WIDTH;
- double sealH = RegionConstant.SEAL_HEIGHT;
-
- // Centraliza o selo na ponta do cursor, sem deixar transbordar da folha
- double clampedX = Math.max(0, Math.min(viewX - sealW / 2.0, sheetW - sealW));
- double clampedY = Math.max(0, Math.min(viewY - sealH / 2.0, sheetH - sealH));
+ double clampedX = Math.max(0, Math.min(newLayoutX, sheetW - sealW));
+ double clampedY = Math.max(0, Math.min(newLayoutY, sheetH - sealH));
sealStampOverlayView.setLayoutX(clampedX);
sealStampOverlayView.setLayoutY(clampedY);
sealStampOverlayView.setVisible(true);
- // Conversão matemática precisa para coordenadas do sistema PDF (origem inferior-esquerda em pontos)
- double pctX = clampedX / sheetW;
- double pctY = clampedY / sheetH;
+ this.sealRelativeX = clampedX / sheetW;
+ this.sealRelativeY = clampedY / sheetH;
- float pdfW = RegionConstant.PDF_DEFAULT_PAGE_WIDTH;
- float pdfH = RegionConstant.PDF_DEFAULT_PAGE_HEIGHT;
- float sealPdfW = RegionConstant.PDF_SEAL_WIDTH;
- float sealPdfH = RegionConstant.PDF_SEAL_HEIGHT;
+ 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) (pctX * pdfW);
- float calculatedPdfY = (float) (pdfH - (pctY * pdfH) - sealPdfH);
+ float calculatedPdfX = (float) (sealRelativeX * currentPagePdfWidth);
+ float calculatedPdfY = (float) (currentPagePdfHeight - (sealRelativeY * currentPagePdfHeight) - sealPdfH);
- this.customPdfX = Math.max(0, Math.min(calculatedPdfX, pdfW - sealPdfW));
- this.customPdfY = Math.max(0, Math.min(calculatedPdfY, pdfH - sealPdfH));
+ this.customPdfX = Math.max(0, Math.min(calculatedPdfX, currentPagePdfWidth - sealPdfW));
+ this.customPdfY = Math.max(0, Math.min(calculatedPdfY, currentPagePdfHeight - sealPdfH));
this.customPageNumber = currentPageIndex + 1;
this.isCustomPositionActive = true;
- // Seleciona a opção CUSTOM no combo
if (positionCombo.getValue() != PositionOption.CUSTOM) {
positionCombo.setValue(PositionOption.CUSTOM);
}
@@ -738,39 +780,27 @@ public final class PdfSignDialog {
}
isCustomPositionActive = false;
- double sheetW = interactiveOverlayPane.getWidth() > 0 ? interactiveOverlayPane.getWidth() : RegionConstant.PREVIEW_WIDTH;
- double sheetH = interactiveOverlayPane.getHeight() > 0 ? interactiveOverlayPane.getHeight() : RegionConstant.PREVIEW_HEIGHT;
-
- double sealW = RegionConstant.SEAL_WIDTH;
- double sealH = RegionConstant.SEAL_HEIGHT;
- double margin = 16.0;
-
- double targetX;
- double targetY;
switch (option) {
case BOTTOM_LEFT -> {
- targetX = margin;
- targetY = sheetH - sealH - margin;
+ this.sealRelativeX = 0.05;
+ this.sealRelativeY = 0.88;
}
case TOP_RIGHT -> {
- targetX = sheetW - sealW - margin;
- targetY = margin;
+ this.sealRelativeX = 0.70;
+ this.sealRelativeY = 0.05;
}
case TOP_LEFT -> {
- targetX = margin;
- targetY = margin;
+ this.sealRelativeX = 0.05;
+ this.sealRelativeY = 0.05;
}
default -> { // BOTTOM_RIGHT
- targetX = sheetW - sealW - margin;
- targetY = sheetH - sealH - margin;
+ this.sealRelativeX = 0.70;
+ this.sealRelativeY = 0.88;
}
}
- sealStampOverlayView.setLayoutX(targetX);
- sealStampOverlayView.setLayoutY(targetY);
- sealStampOverlayView.setVisible(true);
-
+ repositionSealByRelativeCoords();
lblPositionFeedback.setText("Posição fixa: " + option.label);
}
@@ -789,85 +819,29 @@ public final class PdfSignDialog {
}
private void toggleVisibleSeal(boolean active) {
- themeCombo.setDisable(!active);
positionCombo.setDisable(!active);
pageCombo.setDisable(!active);
txtReason.setDisable(!active);
txtLocation.setDisable(!active);
- if (txtCustomInfo != null) {
- txtCustomInfo.setDisable(!active);
- }
sealPreviewImageView.setOpacity(active ? 1.0 : 0.3);
sealStampOverlayView.setVisible(active && selectedPdfBytes != null);
}
- private void populateCertificates() {
- try {
- List certs = certManager.getAvailableCertificates();
- certCombo.setItems(FXCollections.observableArrayList(certs));
- if (!certs.isEmpty()) {
- certCombo.setValue(certs.get(0));
- }
- } catch (Exception e) {
- logger.error("Erro ao listar certificados para assinatura de PDF", e);
- }
- }
-
- private void updatePinFieldVisibility() {
- ManagedCertificate cert = certCombo.getValue();
- if (cert == null) {
- pinField.setDisable(true);
- lblPinNotice.setText("");
- return;
- }
-
- boolean isA3 = cert.type() == CertificateType.A3;
- boolean hasPinInSession = isA3 && PinSessionManager.getInstance().hasPin(cert.id());
-
- if (hasPinInSession) {
- pinField.setDisable(true);
- pinField.clear();
- lblPinNotice.setText("✓ PIN memorizado");
- lblPinNotice.setStyle("-fx-text-fill: #10b981; -fx-font-weight: bold;");
- } else if (isA3 && "provider-win-store".equalsIgnoreCase(cert.providerId())) {
- pinField.setDisable(true);
- pinField.clear();
- lblPinNotice.setText("Driver nativo");
- lblPinNotice.setStyle("-fx-text-fill: -fx-accent;");
- } else {
- pinField.setDisable(false);
- lblPinNotice.setText(isA3 ? "Requer PIN" : "Requer senha");
- lblPinNotice.setStyle("-fx-text-fill: -fx-text-secondary;");
- }
- }
-
private void updateSealPreview() {
try {
- PdfSealTheme theme = themeCombo.getValue();
- if (theme == null) {
- theme = PdfSealThemeRegistry.getDefaultTheme();
- }
+ PdfSealTheme theme = activeSealTheme != null ? activeSealTheme : PdfSealThemeRegistry.getDefaultTheme();
- ManagedCertificate cert = certCombo.getValue();
- String name = (cert != null && cert.commonName() != null) ? cert.commonName() : "NOME DO TITULAR ICP-BRASIL";
- String doc = (cert != null && cert.cpf() != null) ? cert.cpf() : (cert != null && cert.cnpj() != null ? cert.cnpj() : "000.000.000-00");
- String issuer = (cert != null && cert.issuerDn() != null) ? cert.issuerDn() : "AC ICP-Brasil Multipla v5";
String reason = (txtReason != null && !txtReason.getText().isBlank()) ? txtReason.getText() : "Assinatura Digital ICP-Brasil";
String location = (txtLocation != null && !txtLocation.getText().isBlank()) ? txtLocation.getText() : "Brasil";
-
- CpfDisplayMode cpfMode = (cpfModeCombo != null && cpfModeCombo.getValue() != null)
- ? cpfModeCombo.getValue()
- : CpfDisplayMode.FULL;
- boolean includeDt = (chkDateTime != null) ? chkDateTime.isSelected() : true;
- String customInfo = (txtCustomInfo != null && !txtCustomInfo.getText().isBlank())
- ? txtCustomInfo.getText().trim()
- : null;
+ CpfDisplayMode cpfMode = theme.defaultCpfMode() != null ? theme.defaultCpfMode() : CpfDisplayMode.FULL;
+ boolean includeDt = theme.defaultIncludeDateTime();
+ String customInfo = theme.formatCustomField();
PdfSealData data = new PdfSealData(
- name,
- doc,
+ "NOME DO TITULAR ICP-BRASIL",
+ "000.000.000-00",
null,
- issuer,
+ "AC ICP-Brasil Multipla v5",
"7A3F9C01B4E8D2E5...",
reason,
location,
@@ -879,185 +853,71 @@ public final class PdfSignDialog {
byte[] pngBytes = PdfSealRenderer.renderToPngBytes(theme, data);
Image image = new Image(new ByteArrayInputStream(pngBytes));
sealPreviewImageView.setImage(image);
-
- // Atualiza também a estampa que fica sobreposta na folha do PDF
sealStampOverlayView.setImage(image);
} catch (Exception e) {
logger.warn("Falha ao atualizar pré-visualização do selo: {}", e.getMessage());
}
}
- private void executePdfSignature() {
- if (selectedSourceFile == null || !selectedSourceFile.exists()) {
- showAlert(Alert.AlertType.WARNING, "Documento Não Selecionado", "Por favor, selecione um arquivo PDF válido para assinar.");
+ // =========================================================================
+ // TRANSIÇÃO PARA A JANELA DE CERTIFICADO E ASSINATURA
+ // =========================================================================
+ private void handleAdvanceToCertificate() {
+ if (selectedSourceFile == null || selectedPdfBytes == null) {
+ showAlert(Alert.AlertType.WARNING, "Documento Não Selecionado",
+ "Por favor, selecione um arquivo PDF antes de avançar para a assinatura.");
return;
}
- ManagedCertificate cert = certCombo.getValue();
- if (cert == null) {
- showAlert(Alert.AlertType.WARNING, "Certificado Não Selecionado", "Por favor, selecione um certificado digital.");
- return;
- }
+ PdfSealTheme theme = activeSealTheme != null ? activeSealTheme : PdfSealThemeRegistry.getDefaultTheme();
+ CpfDisplayMode selectedCpfMode = theme.defaultCpfMode() != null ? theme.defaultCpfMode() : CpfDisplayMode.FULL;
+ boolean selectedIncludeDt = theme.defaultIncludeDateTime();
+ String customInfo = theme.formatCustomField();
+ PositionOption pos = positionCombo.getValue();
+ PageOption pg = pageCombo.getValue();
- FileChooser chooser = new FileChooser();
- chooser.setTitle("Salvar PDF Assinado Como...");
- chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Documentos PDF (*.pdf)", "*.pdf"));
+ PdfCertificateSignDialog.SealConfig sealConfig = new PdfCertificateSignDialog.SealConfig(
+ chkVisibleSeal.isSelected(),
+ theme,
+ selectedCpfMode,
+ selectedIncludeDt,
+ customInfo,
+ pos,
+ pg,
+ isCustomPositionActive,
+ customPdfX,
+ customPdfY,
+ customPageNumber,
+ txtReason.getText(),
+ txtLocation.getText()
+ );
- String originalName = selectedSourceFile.getName();
- String baseName = originalName.toLowerCase().endsWith(".pdf")
- ? originalName.substring(0, originalName.length() - 4)
- : originalName;
- chooser.setInitialFileName(baseName + "_assinado.pdf");
- if (selectedSourceFile.getParentFile() != null && selectedSourceFile.getParentFile().exists()) {
- chooser.setInitialDirectory(selectedSourceFile.getParentFile());
- }
+ String sealSummary;
+ if (chkVisibleSeal.isSelected()) {
+ String themeName = theme.name();
- File targetFile = chooser.showSaveDialog(stage);
- if (targetFile == null) {
- return;
- }
-
- char[] pin = null;
- if (!pinField.isDisable() && !pinField.getText().isEmpty()) {
- pin = pinField.getText().toCharArray();
- }
-
- CpfDisplayMode selectedCpfMode = (cpfModeCombo != null && cpfModeCombo.getValue() != null)
- ? cpfModeCombo.getValue()
- : CpfDisplayMode.FULL;
- boolean selectedIncludeDt = (chkDateTime != null) ? chkDateTime.isSelected() : true;
- String customInfo = (txtCustomInfo != null && !txtCustomInfo.getText().isBlank())
- ? txtCustomInfo.getText().trim()
- : null;
-
- PdfSignatureOptions options;
- if (isCustomPositionActive && customPdfX != null && customPdfY != null) {
- options = PdfSignatureOptions.builder(cert.id())
- .pinOrPassword(pin)
- .visibleSeal(chkVisibleSeal.isSelected())
- .sealTheme(themeCombo.getValue())
- .cpfDisplayMode(selectedCpfMode)
- .includeDateTime(selectedIncludeDt)
- .customExtraInfo(customInfo)
- .customPosition(customPdfX, customPdfY, customPageNumber)
- .reason(txtReason.getText())
- .location(txtLocation.getText())
- .build();
+ if (isCustomPositionActive && customPageNumber != null && customPdfX != null && customPdfY != null) {
+ sealSummary = String.format("Selo Estampado (%s) — Pág. %d [X: %.0f pt, Y: %.0f pt]",
+ themeName, customPageNumber, customPdfX, customPdfY);
+ } else {
+ sealSummary = String.format("Selo Estampado (%s) — %s (%s)",
+ themeName, (pos != null ? pos.label : "Canto Inferior Direito"), (pg != null ? pg.label : "Última Página"));
+ }
} else {
- options = PdfSignatureOptions.builder(cert.id())
- .pinOrPassword(pin)
- .visibleSeal(chkVisibleSeal.isSelected())
- .sealTheme(themeCombo.getValue())
- .cpfDisplayMode(selectedCpfMode)
- .includeDateTime(selectedIncludeDt)
- .customExtraInfo(customInfo)
- .position(positionCombo.getValue().toPosition())
- .pageChoice(pageCombo.getValue().toPageChoice())
- .reason(txtReason.getText())
- .location(txtLocation.getText())
- .build();
+ sealSummary = "Sem Selo Visual (Assinatura PAdES Invisível)";
}
- setFormDisabled(true);
- progressIndicator.setVisible(true);
- lblStatus.setText("Assinando documento PDF com envelope PAdES...");
- lblStatus.setStyle("-fx-text-fill: -fx-accent;");
+ // Fecha a janela do PDF e abre a nova janela dedicada de certificado
+ stage.close();
- Task signTask = new Task<>() {
- @Override
- protected File call() throws Exception {
- pdfSigningService.signPdfFile(selectedSourceFile, targetFile, options);
- return targetFile;
- }
- };
-
- signTask.setOnSucceeded(e -> {
- setFormDisabled(false);
- progressIndicator.setVisible(false);
- lblStatus.setText("✓ Documento assinado com sucesso!");
- lblStatus.setStyle("-fx-text-fill: #10b981; -fx-font-weight: bold;");
-
- showSuccessDialog(targetFile);
- });
-
- signTask.setOnFailed(e -> {
- setFormDisabled(false);
- progressIndicator.setVisible(false);
- Throwable ex = signTask.getException();
- logger.error("Falha ao assinar PDF", ex);
- lblStatus.setText("✕ Falha na assinatura");
- lblStatus.setStyle("-fx-text-fill: #ef4444; -fx-font-weight: bold;");
-
- showAlert(Alert.AlertType.ERROR, "Erro na Assinatura do PDF",
- "Não foi possível concluir a assinatura digital:\n" + (ex != null ? ex.getMessage() : "Erro desconhecido"));
- });
-
- Thread signThread = new Thread(signTask, "cybercore-pdf-signer");
- signThread.setDaemon(true);
- signThread.start();
- }
-
- private void showSuccessDialog(File targetFile) {
- Alert alert = new Alert(Alert.AlertType.INFORMATION);
- alert.setTitle("Assinatura Concluída");
- alert.setHeaderText("PDF Assinado com Sucesso!");
- alert.setContentText("O documento foi digitalmente assinado no padrão PAdES e o selo visual foi aplicado na posição exata solicitada.\n\n"
- + "Arquivo gerado:\n" + targetFile.getAbsolutePath());
-
- ButtonType btnOpen = new ButtonType("Abrir PDF");
- ButtonType btnFolder = new ButtonType("Abrir Pasta");
- ButtonType btnClose = new ButtonType("Fechar", ButtonBar.ButtonData.CANCEL_CLOSE);
-
- alert.getButtonTypes().setAll(btnOpen, btnFolder, btnClose);
- ThemeManager.applyTheme(alert.getDialogPane().getScene());
-
- Optional result = alert.showAndWait();
- if (result.isPresent()) {
- if (result.get() == btnOpen) {
- openFile(targetFile);
- } else if (result.get() == btnFolder) {
- openFolder(targetFile);
- }
- }
- }
-
- private void openFile(File file) {
- try {
- if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) {
- Desktop.getDesktop().open(file);
- }
- } catch (Exception ex) {
- logger.warn("Não foi possível abrir o arquivo automaticamente: {}", ex.getMessage());
- }
- }
-
- private void openFolder(File file) {
- try {
- if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) {
- File parent = file.getParentFile();
- if (parent != null) {
- Desktop.getDesktop().open(parent);
- }
- }
- } catch (Exception ex) {
- logger.warn("Não foi possível abrir a pasta do arquivo: {}", ex.getMessage());
- }
- }
-
- private void setFormDisabled(boolean disabled) {
- btnSign.setDisable(disabled);
- btnCancel.setDisable(disabled);
- certCombo.setDisable(disabled);
- pinField.setDisable(disabled);
- chkVisibleSeal.setDisable(disabled);
- themeCombo.setDisable(disabled);
- positionCombo.setDisable(disabled);
- pageCombo.setDisable(disabled);
- txtReason.setDisable(disabled);
- txtLocation.setDisable(disabled);
- interactiveOverlayPane.setDisable(disabled);
- btnPrevPage.setDisable(disabled || currentPageIndex == 0);
- btnNextPage.setDisable(disabled || currentPageIndex >= totalPages - 1);
+ new PdfCertificateSignDialog(
+ ownerStage,
+ certManager,
+ pdfSigningService,
+ selectedSourceFile,
+ sealConfig,
+ sealSummary
+ ).show();
}
private void showAlert(Alert.AlertType type, String title, String message) {
diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SealManagementDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SealManagementDialog.java
index 71b347b..47fccba 100644
--- a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SealManagementDialog.java
+++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SealManagementDialog.java
@@ -222,8 +222,9 @@ public final class SealManagementDialog {
footer.setAlignment(Pos.CENTER_RIGHT);
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
- btnClose.getStyleClass().add("btn-secondary");
- btnClose.setPrefWidth(95);
+ btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
+ btnClose.setPrefWidth(110);
+ btnClose.setMinWidth(110);
btnClose.setOnAction(e -> stage.close());
footer.getChildren().add(btnClose);
diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SettingsView.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SettingsView.java
index 946f3ed..30f3da9 100644
--- a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SettingsView.java
+++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SettingsView.java
@@ -124,7 +124,7 @@ public final class SettingsView extends VBox {
HBox pinActionsBox = new HBox(12);
pinActionsBox.setAlignment(Pos.CENTER_LEFT);
- Button btnClearPins = new Button("Limpar PINs Salvos na Sessão");
+ Button btnClearPins = new Button("Limpar PINs Salvos (Sessão e Computador)");
btnClearPins.getStyleClass().add("btn-secondary");
btnClearPins.setStyle("-fx-padding: 6px 14px; -fx-font-size: 12px;");
@@ -133,8 +133,9 @@ public final class SettingsView extends VBox {
btnClearPins.setOnAction(e -> {
PinSessionManager.getInstance().clearAll();
+ br.com.cybercore.signer.core.security.SecurePinStorage.getInstance().clearAll();
SmartCardSessionManager.resetCardSessions();
- pinStatusLabel.setText("✓ Todos os PINs e sessões de tokens foram zerados.");
+ pinStatusLabel.setText("✓ Todos os PINs salvos em memória e neste computador foram zerados.");
});
pinActionsBox.getChildren().addAll(btnClearPins, pinStatusLabel);
@@ -193,6 +194,8 @@ public final class SettingsView extends VBox {
private void refreshSealsList() {
sealsContainer.getChildren().clear();
List themes = PdfSealThemeRegistry.listThemes();
+ AgentConfigurationService configService = AgentConfigurationService.getInstance();
+ String currentDefaultId = configService.getConfig().defaultSealThemeId();
for (PdfSealTheme theme : themes) {
HBox item = new HBox(10);
@@ -200,6 +203,8 @@ public final class SettingsView extends VBox {
item.getStyleClass().add("diagnostic-item");
item.setPadding(new Insets(6, 12, 6, 12));
+ boolean isDefault = theme.id().equalsIgnoreCase(currentDefaultId);
+
VBox info = new VBox(2);
Label lblName = new Label(theme.name() + " (" + theme.id() + ")");
lblName.getStyleClass().add("cert-title");
@@ -211,10 +216,34 @@ public final class SettingsView extends VBox {
info.getChildren().addAll(lblName, lblDesc);
HBox.setHgrow(info, Priority.ALWAYS);
- Label badge = new Label(theme.isCustom() ? "Personalizado" : "Sistema");
- badge.getStyleClass().add(theme.isCustom() ? "diagnostic-value-warn" : "diagnostic-value-ok");
+ Label badgeType = new Label(theme.isCustom() ? "Personalizado" : "Sistema");
+ badgeType.getStyleClass().add(theme.isCustom() ? "diagnostic-value-warn" : "diagnostic-value-ok");
+ item.getChildren().addAll(info, badgeType);
- item.getChildren().addAll(info, badge);
+ if (isDefault) {
+ Label badgeDefault = new Label("★ PADRÃO");
+ badgeDefault.setStyle("-fx-background-color: #0284c7; -fx-text-fill: #ffffff; -fx-padding: 3px 8px; -fx-background-radius: 4px; -fx-font-weight: bold; -fx-font-size: 11px;");
+ item.getChildren().add(badgeDefault);
+ } else {
+ Button btnSetDefault = new Button("★ Definir Padrão");
+ btnSetDefault.getStyleClass().addAll("btn-secondary", "btn-compact");
+ btnSetDefault.setStyle("-fx-padding: 3px 8px; -fx-font-size: 11px;");
+ btnSetDefault.setOnAction(e -> {
+ AgentConfiguration current = configService.getConfig();
+ configService.updateConfig(new AgentConfiguration(
+ current.requireConfirmation(),
+ current.alwaysPromptPin(),
+ current.autoStart(),
+ theme.id(),
+ current.defaultCpfDisplayMode(),
+ current.defaultIncludeDateTime(),
+ current.serverPort()
+ ));
+ refreshSealsList();
+ notifySave("Selo '" + theme.name() + "' definido como o padrão para assinaturas.");
+ });
+ item.getChildren().add(btnSetDefault);
+ }
if (theme.isCustom()) {
Button btnDelete = new Button("Excluir");
diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/TutorialDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/TutorialDialog.java
index dfd3af2..0792d5c 100644
--- a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/TutorialDialog.java
+++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/TutorialDialog.java
@@ -77,8 +77,9 @@ public final class TutorialDialog {
HBox footer = new HBox();
footer.setAlignment(Pos.CENTER_RIGHT);
Button btnClose = new Button(" Entendi", SvgIcons.closeIcon(13));
- btnClose.getStyleClass().add("btn-primary");
- btnClose.setPrefWidth(95);
+ btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
+ btnClose.setPrefWidth(110);
+ btnClose.setMinWidth(110);
btnClose.setOnAction(e -> stage.close());
footer.getChildren().add(btnClose);
diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/XmlSignDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/XmlSignDialog.java
index a67cd10..bb9d1c2 100644
--- a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/XmlSignDialog.java
+++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/XmlSignDialog.java
@@ -27,9 +27,11 @@ import javafx.scene.control.RadioButton;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextField;
import javafx.scene.control.ToggleGroup;
+import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Modality;
@@ -214,6 +216,16 @@ public final class XmlSignDialog {
elementGrid.setHgap(12);
elementGrid.setVgap(8);
+ ColumnConstraints colTagLabel = new ColumnConstraints();
+ colTagLabel.setMinWidth(140);
+ colTagLabel.setPrefWidth(145);
+ colTagLabel.setHgrow(Priority.NEVER);
+
+ ColumnConstraints colTagField = new ColumnConstraints();
+ colTagField.setHgrow(Priority.ALWAYS);
+
+ elementGrid.getColumnConstraints().addAll(colTagLabel, colTagField);
+
Label lblTag = new Label("Tag do Elemento:");
lblTag.getStyleClass().add("dialog-field-title");
txtElementTag = new TextField("infNFe");
@@ -254,11 +266,12 @@ public final class XmlSignDialog {
HBox.setHgrow(lblStatus, Priority.ALWAYS);
btnCancel = new Button("Cancelar");
- btnCancel.getStyleClass().add("btn-secondary");
+ btnCancel.getStyleClass().addAll("btn-secondary", "btn-dialog");
btnCancel.setOnAction(e -> stage.close());
btnSign = new Button("Assinar Documento XML");
- btnSign.getStyleClass().add("btn-primary");
+ btnSign.getStyleClass().addAll("btn-primary", "btn-dialog");
+ btnSign.setMinWidth(Region.USE_PREF_SIZE);
btnSign.setDefaultButton(true);
btnSign.setOnAction(e -> executeXmlSignature());
diff --git a/modules/cybercore-ui/src/main/resources/styles/theme.css b/modules/cybercore-ui/src/main/resources/styles/theme.css
index 8047762..dcb802a 100644
--- a/modules/cybercore-ui/src/main/resources/styles/theme.css
+++ b/modules/cybercore-ui/src/main/resources/styles/theme.css
@@ -853,6 +853,11 @@
3. BOTÕES E COMPONENTES COMUNS (DUAL THEME)
========================================================================== */
+/* Botões Base e Proteção contra Truncamento com Reticências */
+.button {
+ -fx-min-width: -fx-pref-size;
+}
+
.btn-primary {
-fx-background-color: #0284c7;
-fx-text-fill: #ffffff;
@@ -860,6 +865,8 @@
-fx-padding: 8px 18px;
-fx-background-radius: 6px;
-fx-cursor: hand;
+ -fx-min-width: -fx-pref-size;
+ -fx-alignment: center;
-fx-effect: dropshadow(three-pass-box, rgba(2, 132, 199, 0.25), 4, 0, 0, 1);
}
@@ -873,6 +880,8 @@
-fx-font-weight: bold;
-fx-padding: 8px 18px;
-fx-background-radius: 6px;
+ -fx-min-width: -fx-pref-size;
+ -fx-alignment: center;
-fx-cursor: hand;
}
@@ -901,6 +910,8 @@
-fx-text-fill: #475569;
-fx-font-weight: bold;
-fx-padding: 8px 18px;
+ -fx-min-width: -fx-pref-size;
+ -fx-alignment: center;
-fx-cursor: hand;
}
@@ -919,6 +930,29 @@
-fx-text-fill: #f87171;
}
+/* Botões Padronizados para Diálogos e Modais */
+.btn-dialog {
+ -fx-min-width: 110px;
+ -fx-pref-width: 110px;
+ -fx-pref-height: 34px;
+ -fx-alignment: center;
+}
+
+.btn-dialog-wide {
+ -fx-min-width: 230px;
+ -fx-pref-width: 230px;
+ -fx-pref-height: 36px;
+ -fx-alignment: center;
+}
+
+/* Botões Compactos (barras de ferramentas e atalhos em linha) */
+.btn-compact {
+ -fx-padding: 5px 12px;
+ -fx-font-size: 11.5px;
+ -fx-min-width: -fx-pref-size;
+ -fx-alignment: center;
+}
+
/* Botão de Perigo / Exclusão / Remoção */
.btn-danger {
-fx-background-color: #fee2e2;
@@ -931,6 +965,7 @@
-fx-font-weight: bold;
-fx-padding: 6px 14px;
-fx-alignment: center;
+ -fx-min-width: -fx-pref-size;
-fx-cursor: hand;
}
@@ -1063,3 +1098,131 @@
-fx-background-color: #0284c7;
-fx-background-radius: 4px;
}
+
+/* =========================================================================
+ WIZARD STEPPER (FLUXO MULTI-ETAPAS)
+ ========================================================================= */
+.wizard-stepper {
+ -fx-alignment: center-left;
+ -fx-padding: 2px 0 6px 0;
+}
+
+.wizard-step-pill {
+ -fx-alignment: center;
+ -fx-padding: 4px 12px;
+ -fx-background-radius: 14px;
+ -fx-border-radius: 14px;
+ -fx-border-width: 1px;
+}
+
+.theme-light .wizard-step-pill {
+ -fx-background-color: #f1f5f9;
+ -fx-border-color: #cbd5e1;
+}
+
+.theme-dark .wizard-step-pill {
+ -fx-background-color: #1e293b;
+ -fx-border-color: #334155;
+}
+
+.theme-light .wizard-step-pill-active {
+ -fx-background-color: #e0f2fe;
+ -fx-border-color: #0284c7;
+}
+
+.theme-dark .wizard-step-pill-active {
+ -fx-background-color: #0c4a6e;
+ -fx-border-color: #38bdf8;
+}
+
+.wizard-step-badge {
+ -fx-font-size: 11px;
+ -fx-font-weight: bold;
+ -fx-alignment: center;
+ -fx-min-width: 20px;
+ -fx-min-height: 20px;
+ -fx-background-radius: 10px;
+ -fx-text-fill: #ffffff;
+}
+
+.theme-light .wizard-step-badge-active {
+ -fx-background-color: #0284c7;
+ -fx-text-fill: #ffffff;
+}
+
+.theme-dark .wizard-step-badge-active {
+ -fx-background-color: #38bdf8;
+ -fx-text-fill: #0f172a;
+}
+
+.theme-light .wizard-step-badge-inactive {
+ -fx-background-color: #94a3b8;
+ -fx-text-fill: #ffffff;
+}
+
+.theme-dark .wizard-step-badge-inactive {
+ -fx-background-color: #475569;
+ -fx-text-fill: #e2e8f0;
+}
+
+.wizard-step-text {
+ -fx-font-size: 11.5px;
+ -fx-font-weight: bold;
+}
+
+.theme-light .wizard-step-text-active {
+ -fx-text-fill: #0369a1;
+}
+
+.theme-dark .wizard-step-text-active {
+ -fx-text-fill: #38bdf8;
+}
+
+.theme-light .wizard-step-text-inactive {
+ -fx-text-fill: #64748b;
+}
+
+.theme-dark .wizard-step-text-inactive {
+ -fx-text-fill: #94a3b8;
+}
+
+.wizard-step-separator {
+ -fx-pref-width: 28px;
+ -fx-min-width: 20px;
+ -fx-pref-height: 2px;
+}
+
+.theme-light .wizard-step-separator {
+ -fx-background-color: #cbd5e1;
+}
+
+.theme-dark .wizard-step-separator {
+ -fx-background-color: #334155;
+}
+
+.theme-light .wizard-step-separator-active {
+ -fx-background-color: #0284c7;
+}
+
+.theme-dark .wizard-step-separator-active {
+ -fx-background-color: #38bdf8;
+}
+
+.document-summary-box {
+ -fx-padding: 10px 14px;
+ -fx-border-radius: 8px;
+ -fx-background-radius: 8px;
+ -fx-border-width: 1px;
+ -fx-alignment: center-left;
+}
+
+.theme-light .document-summary-box {
+ -fx-background-color: #f8fafc;
+ -fx-border-color: #e2e8f0;
+}
+
+.theme-dark .document-summary-box {
+ -fx-background-color: #0f172a;
+ -fx-border-color: #1e293b;
+}
+