feat: improve UI startup performance, window focus handling, and system tray interaction with tests

This commit is contained in:
Claudecio Martins
2026-10-12 10:37:36 -03:00
parent 64ca13b3a1
commit d1cfab95c2
4 changed files with 81 additions and 9 deletions
@@ -130,6 +130,10 @@ public class CyberCoreDesktopApplication extends Application {
mainAppView.setOnCertificates(() -> new CertificatesManagerDialog(primaryStage, certificateManager).show()); mainAppView.setOnCertificates(() -> new CertificatesManagerDialog(primaryStage, certificateManager).show());
mainAppView.setOnAbout(() -> openAboutDialog(primaryStage)); 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 // Inicializa o servidor local do protocolo CCSP/1 para atender requisições do navegador
initCcspServer(primaryStage); initCcspServer(primaryStage);
@@ -140,11 +144,8 @@ public class CyberCoreDesktopApplication extends Application {
this.systemTrayManager = new SystemTrayManager(primaryStage, this::refreshCertificates); this.systemTrayManager = new SystemTrayManager(primaryStage, this::refreshCertificates);
this.systemTrayManager.setup(); this.systemTrayManager.setup();
// Carrega os certificados instalados na máquina do usuário // Carrega os certificados instalados na máquina em segundo plano para não congelar a interface
refreshCertificates(); CompletableFuture.runAsync(this::refreshCertificates);
mainAppView.show();
logger.info("Janela principal JavaFX exibida.");
} }
private void refreshCertificates() { private void refreshCertificates() {
@@ -15,6 +15,8 @@ import java.awt.PopupMenu;
import java.awt.RenderingHints; import java.awt.RenderingHints;
import java.awt.SystemTray; import java.awt.SystemTray;
import java.awt.TrayIcon; import java.awt.TrayIcon;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.util.Objects; import java.util.Objects;
@@ -76,7 +78,17 @@ public class SystemTrayManager {
this.trayIcon = new TrayIcon(iconImage, "CyberCore Signer (CCSP/1 Ativo)", popup); this.trayIcon = new TrayIcon(iconImage, "CyberCore Signer (CCSP/1 Ativo)", popup);
this.trayIcon.setImageAutoSize(true); 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)); this.trayIcon.addActionListener(e -> Platform.runLater(this::showStage));
tray.add(this.trayIcon); tray.add(this.trayIcon);
@@ -104,10 +116,31 @@ public class SystemTrayManager {
} }
public void showStage() { public void showStage() {
if (stage != null) { if (stage == null) {
return;
}
try {
if (!stage.isShowing()) {
stage.show(); stage.show();
}
if (stage.isIconified()) {
stage.setIconified(false); stage.setIconified(false);
}
stage.toFront(); 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());
} }
} }
@@ -297,8 +297,17 @@ public final class MainAppView {
public void show() { public void show() {
stage.centerOnScreen(); stage.centerOnScreen();
if (stage.isIconified()) {
stage.setIconified(false);
}
stage.show(); stage.show();
stage.toFront(); stage.toFront();
stage.requestFocus();
try {
stage.setAlwaysOnTop(true);
stage.setAlwaysOnTop(false);
} catch (Throwable ignored) {
}
} }
public void hide() { public void hide() {
@@ -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);
}
}