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 aeaf492..397a28e 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 @@ -130,6 +130,10 @@ 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."); + // Inicializa o servidor local do protocolo CCSP/1 para atender requisições do navegador initCcspServer(primaryStage); @@ -140,11 +144,8 @@ public class CyberCoreDesktopApplication extends Application { this.systemTrayManager = new SystemTrayManager(primaryStage, this::refreshCertificates); this.systemTrayManager.setup(); - // Carrega os certificados instalados na máquina do usuário - refreshCertificates(); - - mainAppView.show(); - logger.info("Janela principal JavaFX exibida."); + // Carrega os certificados instalados na máquina em segundo plano para não congelar a interface + CompletableFuture.runAsync(this::refreshCertificates); } private void refreshCertificates() { diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/tray/SystemTrayManager.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/tray/SystemTrayManager.java index 07869b0..fc4e557 100644 --- a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/tray/SystemTrayManager.java +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/tray/SystemTrayManager.java @@ -15,6 +15,8 @@ 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; @@ -76,7 +78,17 @@ public class SystemTrayManager { this.trayIcon = new TrayIcon(iconImage, "CyberCore Signer (CCSP/1 Ativo)", popup); this.trayIcon.setImageAutoSize(true); - // Clique no ícone restaura a janela + // Clique com o botão esquerdo (single-click) restaura a janela imediatamente + this.trayIcon.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1) { + 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); @@ -104,10 +116,31 @@ public class SystemTrayManager { } public void showStage() { - if (stage != null) { - stage.show(); - stage.setIconified(false); + if (stage == null) { + return; + } + + try { + if (!stage.isShowing()) { + stage.show(); + } + + if (stage.isIconified()) { + 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()); } } diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/MainAppView.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/MainAppView.java index e6c4d4f..4b15621 100644 --- a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/MainAppView.java +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/MainAppView.java @@ -297,8 +297,17 @@ 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() { diff --git a/modules/cybercore-ui/src/test/java/br/com/cybercore/signer/ui/tray/SystemTrayManagerTest.java b/modules/cybercore-ui/src/test/java/br/com/cybercore/signer/ui/tray/SystemTrayManagerTest.java new file mode 100644 index 0000000..644b0e3 --- /dev/null +++ b/modules/cybercore-ui/src/test/java/br/com/cybercore/signer/ui/tray/SystemTrayManagerTest.java @@ -0,0 +1,29 @@ +package br.com.cybercore.signer.ui.tray; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.awt.SystemTray; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayName("Testes do Gerenciador de Bandeja (SystemTrayManager)") +class SystemTrayManagerTest { + + @Test + @DisplayName("Deve rejeitar Stage nulo no construtor") + void shouldRejectNullStage() { + assertThatThrownBy(() -> new SystemTrayManager(null, () -> {})) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("stage must not be null"); + } + + @Test + @DisplayName("Deve reportar suporte do ambiente ou lidar graciosamente sem UI nativa") + void shouldReportSupportOrHandleGracefully() { + boolean supported = SystemTray.isSupported(); + // Verifica que chamada não lança exceção fatal + assertThat(supported).isIn(true, false); + } +}