feat: add exit confirmation dialog, system tray enhancements, and diagnostic options

This commit is contained in:
Claudecio Martins
2026-10-12 12:32:35 -03:00
parent 16212702cb
commit edccfff7ff
10 changed files with 527 additions and 68 deletions
@@ -36,6 +36,17 @@ public class CyberCoreSignerApplication {
}
public static void main(String[] args) {
// Previne conflitos nativos de UI Automation/Acessibilidade do Glass no Windows 11
if (System.getProperty("javafx.accessibility") == null) {
System.setProperty("javafx.accessibility", "false");
}
if (System.getProperty("glass.accessible.force") == null) {
System.setProperty("glass.accessible.force", "false");
}
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
logger.error("Exceção não tratada na thread '{}': {}", thread.getName(), throwable.getMessage(), throwable);
});
parseArguments(args);
printBanner();
@@ -67,6 +78,9 @@ public class CyberCoreSignerApplication {
try {
logger.info("Iniciando interface gráfica desktop...");
CyberCoreDesktopApplication.setDevelopmentMode(developmentMode);
Runtime.getRuntime().addShutdownHook(new Thread(() ->
logger.warn("JVM em encerramento (sinal externo, System.exit ou fim do aplicativo)."),
"cybercore-shutdown-diagnostic"));
Application.launch(CyberCoreDesktopApplication.class, args);
} catch (Throwable t) {
logger.warn("Não foi possível inicializar ambiente gráfico ({}). Continuando em modo console.", t.getMessage());
@@ -33,12 +33,27 @@ public class CertificateManager {
/**
* Cria e inicializa uma instância padrão do CertificateManager com os provedores nativos da plataforma.
* Suporta chaves de diagnóstico para isolamento de problemas de middleware:
* -Dcybercore.diag.no-wincert=true (não inicializa Windows Certificate Store)
* -Dcybercore.diag.no-pkcs11=true (não inicializa PKCS#11 / DLLs nativas)
*/
public static CertificateManager createDefault() {
CertificateManager manager = new CertificateManager();
manager.registerProvider(new WindowsCertificateStoreProvider());
if (Boolean.getBoolean("cybercore.diag.no-wincert")) {
logger.warn("Diagnóstico: Provedor Windows Certificate Store DESATIVADO via -Dcybercore.diag.no-wincert=true");
} else {
manager.registerProvider(new WindowsCertificateStoreProvider());
}
manager.registerProvider(new Pkcs12CertificateProvider());
manager.registerProvider(new Pkcs11CertificateProvider());
if (Boolean.getBoolean("cybercore.diag.no-pkcs11")) {
logger.warn("Diagnóstico: Provedor PKCS#11 DESATIVADO via -Dcybercore.diag.no-pkcs11=true");
} else {
manager.registerProvider(new Pkcs11CertificateProvider());
}
return manager;
}
@@ -63,4 +63,34 @@ class CertificateManagerTest {
assertThat(byCnpj).isPresent();
assertThat(byCnpj.get().id()).isEqualTo("cert-1");
}
@Test
@DisplayName("Deve desativar provider PKCS#11 quando chave de diagnóstico estiver ativa")
void shouldRespectDiagnosticsNoPkcs11() {
System.setProperty("cybercore.diag.no-pkcs11", "true");
try {
CertificateManager manager = CertificateManager.createDefault();
assertThat(manager.getProviders())
.extracting(CertificateProvider::getProviderId)
.doesNotContain("provider-pkcs11")
.contains("provider-a1");
} finally {
System.clearProperty("cybercore.diag.no-pkcs11");
}
}
@Test
@DisplayName("Deve desativar provider Windows Certificate Store quando chave de diagnóstico estiver ativa")
void shouldRespectDiagnosticsNoWinCert() {
System.setProperty("cybercore.diag.no-wincert", "true");
try {
CertificateManager manager = CertificateManager.createDefault();
assertThat(manager.getProviders())
.extracting(CertificateProvider::getProviderId)
.doesNotContain("provider-win-store")
.contains("provider-a1");
} finally {
System.clearProperty("cybercore.diag.no-wincert");
}
}
}
@@ -117,10 +117,14 @@ public class CyberCoreDesktopApplication extends Application {
mainAppView.setOnValidateTimestamp(() -> new SignatureValidationDialog(primaryStage, true).show());
mainAppView.setOnManageSeals(() -> new SealManagementDialog(primaryStage).show());
mainAppView.setOnExit(() -> {
logger.info("Encerrando CyberCore Signer pela interface gráfica.");
primaryStage.close();
Platform.exit();
System.exit(0);
logger.info("Ação 'Sair' acionada na interface gráfica.");
if (systemTrayManager != null) {
systemTrayManager.handleWindowCloseRequest();
} else {
primaryStage.close();
Platform.exit();
System.exit(0);
}
});
// Ações do Painel Lateral
@@ -130,9 +134,21 @@ public class CyberCoreDesktopApplication extends Application {
mainAppView.setOnCertificates(() -> new CertificatesManagerDialog(primaryStage, certificateManager).show());
mainAppView.setOnAbout(() -> openAboutDialog(primaryStage));
// Exibe imediatamente a janela principal em primeiro plano para o usuário
mainAppView.show();
logger.info("Janela principal JavaFX exibida.");
// Observabilidade do ciclo de vida da janela
primaryStage.showingProperty().addListener((obs, oldVal, newVal) ->
logger.info("Visibilidade da janela principal JavaFX alterada: visível={}", newVal));
primaryStage.iconifiedProperty().addListener((obs, oldVal, newVal) ->
logger.info("Estado de minimização da janela JavaFX alterado: minimizada={}", newVal));
primaryStage.focusedProperty().addListener((obs, oldVal, newVal) ->
logger.info("Foco da janela principal alterado: focada={}", newVal));
primaryStage.xProperty().addListener((obs, oldVal, newVal) ->
logger.debug("Janela principal movida: x={}, y={}", newVal, primaryStage.getY()));
primaryStage.yProperty().addListener((obs, oldVal, newVal) ->
logger.debug("Janela principal movida: x={}, y={}", primaryStage.getX(), newVal));
primaryStage.widthProperty().addListener((obs, oldVal, newVal) ->
logger.debug("Janela principal redimensionada: largura={}, altura={}", newVal, primaryStage.getHeight()));
primaryStage.heightProperty().addListener((obs, oldVal, newVal) ->
logger.debug("Janela principal redimensionada: largura={}, altura={}", primaryStage.getWidth(), newVal));
// Inicializa o servidor local do protocolo CCSP/1 para atender requisições do navegador
initCcspServer(primaryStage);
@@ -140,12 +156,22 @@ public class CyberCoreDesktopApplication extends Application {
// Monitoramento em tempo real de conexão/remoção de Smart Cards e Tokens USB
initSmartCardWatcher();
// Configura bandeja do sistema (System Tray) para operação contínua em segundo plano
this.systemTrayManager = new SystemTrayManager(primaryStage, this::refreshCertificates);
this.systemTrayManager.setup();
// Carrega os certificados instalados na máquina em segundo plano para não congelar a interface
CompletableFuture.runAsync(this::refreshCertificates);
// Carrega os certificados instalados na máquina do usuário de forma estável
refreshCertificates();
// Exibe a janela principal em primeiro plano após os serviços estarem prontos
mainAppView.show();
logger.info("Janela principal JavaFX exibida.");
// Configura a bandeja (System Tray) somente depois que a janela JavaFX está visível,
// para não inicializar o toolkit AWT concorrentemente com a criação da janela.
if (Boolean.getBoolean("cybercore.no.tray")) {
logger.warn("System Tray desativado via -Dcybercore.no.tray=true (diagnóstico).");
} else {
Platform.runLater(() -> this.systemTrayManager.setup());
}
}
private void refreshCertificates() {
@@ -473,12 +499,23 @@ public class CyberCoreDesktopApplication extends Application {
this.ccspServer = new CcspServer(this.certificateManager, this.signingService, promptListener);
this.ccspServer.start();
logger.info("Servidor local CCSP/1 inicializado na porta {}", this.ccspServer.getPort());
} catch (java.net.BindException be) {
logger.warn("Porta 21325 já está ocupada por outra instância do CyberCore Signer em execução. O agente continuará ativo na interface gráfica.");
} catch (Exception e) {
logger.error("Falha ao inicializar servidor local CCSP/1: {}", e.getMessage(), e);
if (e.getCause() instanceof java.net.BindException || (e.getMessage() != null && e.getMessage().contains("Address already in use"))) {
logger.warn("Porta 21325 já está ocupada por outra instância do CyberCore Signer em execução. O agente continuará ativo na interface gráfica.");
} else {
logger.error("Falha ao inicializar servidor local CCSP/1: {}", e.getMessage(), e);
}
}
}
private void initSmartCardWatcher() {
if (Boolean.getBoolean("cybercore.diag.no-smartcard-watcher")) {
logger.warn("Diagnóstico: Monitor dinâmico de smart cards DESATIVADO via -Dcybercore.diag.no-smartcard-watcher=true");
return;
}
try {
this.smartCardWatcher = new SmartCardWatcher();
this.smartCardWatcher.addListener(new SmartCardWatcher.CardEventListener() {
@@ -1,5 +1,6 @@
package br.com.cybercore.signer.ui.tray;
import br.com.cybercore.signer.ui.view.ExitConfirmationDialog;
import javafx.application.Platform;
import javafx.stage.Stage;
import org.slf4j.Logger;
@@ -15,8 +16,6 @@ import java.awt.PopupMenu;
import java.awt.RenderingHints;
import java.awt.SystemTray;
import java.awt.TrayIcon;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.util.Objects;
@@ -42,12 +41,19 @@ public class SystemTrayManager {
/**
* Inicializa o ícone e menu de contexto na bandeja do sistema, caso haja suporte.
*/
public synchronized boolean setup() {
public boolean setup() {
if (!SystemTray.isSupported()) {
logger.info("SystemTray não é suportado pelo ambiente atual ou sistema operacional.");
return false;
}
// O TrayIcon AWT é criado na thread de eventos do AWT, nunca na thread do JavaFX,
// para evitar conflito entre os dois loops de mensagens nativos no Windows.
java.awt.EventQueue.invokeLater(this::setupOnAwtThread);
return true;
}
private synchronized void setupOnAwtThread() {
try {
SystemTray tray = SystemTray.getSystemTray();
Image iconImage = createTrayIconImage();
@@ -78,69 +84,78 @@ public class SystemTrayManager {
this.trayIcon = new TrayIcon(iconImage, "CyberCore Signer (CCSP/1 Ativo)", popup);
this.trayIcon.setImageAutoSize(true);
// Clique com o botão esquerdo (single-click) restaura a janela imediatamente
this.trayIcon.addMouseListener(new MouseAdapter() {
// Duplo clique (ActionListener) e clique simples (MouseListener) restauram a janela
this.trayIcon.addActionListener(e -> {
logger.info("Ícone da bandeja acionado (duplo clique).");
Platform.runLater(this::showStage);
});
this.trayIcon.addMouseListener(new java.awt.event.MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON1) {
public void mouseClicked(java.awt.event.MouseEvent e) {
if (e.getButton() == java.awt.event.MouseEvent.BUTTON1) {
logger.info("Ícone da bandeja acionado (clique simples).");
Platform.runLater(SystemTrayManager.this::showStage);
}
}
});
// Duplo clique ou acionamento nativo padrão também restaura a janela
this.trayIcon.addActionListener(e -> Platform.runLater(this::showStage));
tray.add(this.trayIcon);
this.initialized = true;
// Intercepta fechamento da janela para ocultar em vez de encerrar
// Intercepta fechamento da janela (botão X) para confirmar ação com o usuário
Platform.runLater(() -> {
stage.setOnCloseRequest(event -> {
event.consume();
stage.hide();
showMessage("CyberCore Signer", "O agente continua ativo na bandeja atendendo aplicações web.", TrayIcon.MessageType.INFO);
handleWindowCloseRequest();
});
});
logger.info("System Tray configurado com sucesso.");
return true;
} catch (AWTException e) {
logger.warn("Falha ao registrar ícone no System Tray: {}", e.getMessage());
return false;
} catch (Throwable t) {
logger.warn("Erro ao inicializar System Tray: {}", t.getMessage());
return false;
logger.warn("Erro ao inicializar System Tray: {}", t.getMessage(), t);
}
}
/**
* Exibe o diálogo de confirmação ao clicar no 'X' da janela principal,
* permitindo minimizar para a bandeja, encerrar definitivamente ou cancelar.
*/
public void handleWindowCloseRequest() {
try {
ExitConfirmationDialog dialog = new ExitConfirmationDialog(stage);
ExitConfirmationDialog.ExitChoice choice = dialog.showAndGetChoice();
switch (choice) {
case MINIMIZE_TO_TRAY -> {
logger.info("Usuário optou por minimizar o CyberCore Signer para a bandeja.");
stage.hide();
showMessage("CyberCore Signer", "O agente continua ativo na bandeja atendendo aplicações web.", TrayIcon.MessageType.INFO);
}
case EXIT_COMPLETELY -> {
logger.info("Usuário optou por encerrar definitivamente o CyberCore Signer.");
shutdown();
}
case CANCEL -> {
logger.debug("Fechamento da janela cancelado pelo usuário.");
}
}
} catch (Throwable t) {
logger.warn("Erro ao exibir diálogo de fechamento, minimizando para bandeja por segurança: {}", t.getMessage());
stage.hide();
}
}
public void showStage() {
if (stage == null) {
return;
}
try {
if (!stage.isShowing()) {
stage.show();
}
if (stage.isIconified()) {
stage.setIconified(false);
}
if (stage != null) {
logger.info("Restaurando janela principal (visível={}, minimizada={}).",
stage.isShowing(), stage.isIconified());
stage.show();
stage.setIconified(false);
stage.toFront();
stage.requestFocus();
// No Windows (Win32 / DWM), chamadas de toFront() partindo da bandeja do sistema
// podem ser suprimidas por políticas de foco do SO (Foreground Lock Timeout).
// A elevação momentânea via setAlwaysOnTop força o gerenciador de janelas a trazer
// o assinador diretamente para o primeiro plano.
stage.setAlwaysOnTop(true);
stage.setAlwaysOnTop(false);
} catch (Throwable t) {
logger.debug("Falha secundária ao restaurar stage da bandeja: {}", t.getMessage());
}
}
@@ -0,0 +1,111 @@
package br.com.cybercore.signer.ui.view;
import br.com.cybercore.signer.ui.components.SvgIcons;
import br.com.cybercore.signer.ui.theme.ThemeManager;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Modality;
import javafx.stage.Stage;
/**
* Diálogo modal de confirmação ao fechar a janela principal do CyberCore Signer.
* Permite ao usuário escolher entre manter o agente ativo na bandeja do sistema
* ou encerrar completamente a aplicação.
*/
public final class ExitConfirmationDialog {
public enum ExitChoice {
MINIMIZE_TO_TRAY,
EXIT_COMPLETELY,
CANCEL
}
private final Stage stage;
private ExitChoice chosenAction = ExitChoice.CANCEL;
public ExitConfirmationDialog(Stage owner) {
this.stage = new Stage();
this.stage.initModality(Modality.APPLICATION_MODAL);
if (owner != null) {
this.stage.initOwner(owner);
}
this.stage.setTitle("CyberCore Signer — Encerrar ou Manter na Bandeja");
this.stage.setResizable(false);
this.stage.setMinWidth(520);
this.stage.setMinHeight(190);
initLayout();
}
private void initLayout() {
VBox root = new VBox(22);
root.setPadding(new Insets(24));
root.getStyleClass().add("dialog-container");
// Cabeçalho com ícone e texto explicativo
HBox header = new HBox(16);
header.setAlignment(Pos.CENTER_LEFT);
VBox titleBox = new VBox(4);
Label title = new Label("Como você deseja proceder?");
title.setStyle("-fx-font-size: 15px; -fx-font-weight: bold;");
Label subtitle = new Label(
"Você pode manter o CyberCore Signer ativo na bandeja do sistema\n" +
"para atender assinaturas de aplicações web, ou encerrá-lo por completo."
);
subtitle.setStyle("-fx-text-fill: -fx-text-secondary; -fx-font-size: 12px;");
titleBox.getChildren().addAll(title, subtitle);
header.getChildren().addAll(SvgIcons.cyberShieldLogo(38), titleBox);
// Barra de Ações com os botões
HBox actionsRow = new HBox(10);
actionsRow.setAlignment(Pos.CENTER_RIGHT);
Button btnCancel = new Button("Cancelar");
btnCancel.getStyleClass().addAll("btn-cancel", "btn-dialog");
btnCancel.setOnAction(e -> {
chosenAction = ExitChoice.CANCEL;
stage.close();
});
HBox spacer = new HBox();
HBox.setHgrow(spacer, Priority.ALWAYS);
Button btnExit = new Button("Encerrar Definitivamente");
btnExit.getStyleClass().addAll("btn-danger", "btn-dialog");
btnExit.setOnAction(e -> {
chosenAction = ExitChoice.EXIT_COMPLETELY;
stage.close();
});
Button btnMinimize = new Button("Minimizar para a Bandeja");
btnMinimize.getStyleClass().addAll("btn-primary", "btn-dialog");
btnMinimize.setDefaultButton(true);
btnMinimize.setOnAction(e -> {
chosenAction = ExitChoice.MINIMIZE_TO_TRAY;
stage.close();
});
actionsRow.getChildren().addAll(btnCancel, spacer, btnExit, btnMinimize);
root.getChildren().addAll(header, actionsRow);
Scene scene = new Scene(root);
ThemeManager.applyTheme(scene);
stage.setScene(scene);
stage.setOnCloseRequest(e -> chosenAction = ExitChoice.CANCEL);
}
public ExitChoice showAndGetChoice() {
stage.showAndWait();
return chosenAction;
}
}
@@ -16,6 +16,8 @@ import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Objects;
@@ -27,6 +29,8 @@ import java.util.Objects;
@SuppressWarnings("this-escape")
public final class MainAppView {
private static final Logger logger = LoggerFactory.getLogger(MainAppView.class);
private final Stage stage;
private final VBox root = new VBox(16);
private final Button themeToggleBtn = new Button();
@@ -136,6 +140,14 @@ public final class MainAppView {
getClass().getResource(CyberCoreUiConstants.MAIN_CSS_PATH)
).toExternalForm());
if (Boolean.getBoolean("cybercore.diag.no-effects")) {
logger.warn("Diagnóstico: Efeitos/sombras visuais DESATIVADOS via -Dcybercore.diag.no-effects=true");
var noEffectsUrl = getClass().getResource("/styles/no-effects.css");
if (noEffectsUrl != null) {
scene.getStylesheets().add(noEffectsUrl.toExternalForm());
}
}
// Configuração e inicialização do Tema
ThemeManager.applyTheme(scene);
updateThemeToggleButton();
@@ -144,11 +156,17 @@ public final class MainAppView {
stage.setTitle(CyberCoreUiConstants.APP_NAME);
stage.setScene(scene);
stage.setResizable(false);
stage.setMinWidth(780);
stage.setMinHeight(480);
stage.setMaxWidth(820);
stage.setMaxHeight(520);
if (Boolean.getBoolean("cybercore.diag.plain-stage")) {
logger.warn("Diagnóstico: Plain Stage ATIVADO via -Dcybercore.diag.plain-stage=true (janela redimensionável, sem restrições min/max)");
stage.setResizable(true);
} else {
stage.setResizable(false);
stage.setMinWidth(780);
stage.setMinHeight(480);
stage.setMaxWidth(820);
stage.setMaxHeight(520);
}
}
private HBox createHeaderBar() {
@@ -297,17 +315,8 @@ public final class MainAppView {
public void show() {
stage.centerOnScreen();
if (stage.isIconified()) {
stage.setIconified(false);
}
stage.show();
stage.toFront();
stage.requestFocus();
try {
stage.setAlwaysOnTop(true);
stage.setAlwaysOnTop(false);
} catch (Throwable ignored) {
}
}
public void hide() {
@@ -0,0 +1,21 @@
/* CyberCore Signer - Modo Diagnóstico: Remoção de sombras/efeitos visuais dinâmicos */
.cyber-action-card,
.serpro-action-btn,
.cyber-side-card,
.serpro-side-btn,
.cyber-header-bar,
.theme-toggle-btn,
.cyber-status-badge {
-fx-effect: null !important;
}
.theme-light .cyber-action-card:hover,
.theme-light .serpro-action-btn:hover,
.theme-dark .cyber-action-card:hover,
.theme-dark .serpro-action-btn:hover,
.cyber-side-card:hover,
.serpro-side-btn:hover,
.theme-toggle-btn:hover {
-fx-effect: null !important;
}