From 7f6d8a4a08879c1e132ae3925958611183032eef Mon Sep 17 00:00:00 2001 From: Claudecio Martins Date: Sun, 11 Oct 2026 10:00:46 -0300 Subject: [PATCH] feat(pdf): centraliza configuracao de selo padrao e adiciona armazenamento seguro de PIN --- .../signer/core/pdf/PdfSigningService.java | 17 + .../core/security/SecurePinStorage.java | 269 ++++ .../core/security/SecurePinStorageTest.java | 77 ++ .../ui/CyberCoreDesktopApplication.java | 25 +- .../signer/ui/components/SvgIcons.java | 99 ++ .../ui/view/CertificateDetailsDialog.java | 5 +- .../signer/ui/view/LogsViewerDialog.java | 9 +- .../ui/view/PdfCertificateSignDialog.java | 534 ++++++++ .../signer/ui/view/PdfSignDialog.java | 1186 ++++++++--------- .../signer/ui/view/SealManagementDialog.java | 5 +- .../signer/ui/view/SettingsView.java | 39 +- .../signer/ui/view/TutorialDialog.java | 5 +- .../signer/ui/view/XmlSignDialog.java | 17 +- .../src/main/resources/styles/theme.css | 163 +++ 14 files changed, 1766 insertions(+), 684 deletions(-) create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/SecurePinStorage.java create mode 100644 modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/security/SecurePinStorageTest.java create mode 100644 modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/PdfCertificateSignDialog.java 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; +} +