diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/PdfUmbenennerGuiApplication.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/PdfUmbenennerGuiApplication.java index 375f342..332f659 100644 --- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/PdfUmbenennerGuiApplication.java +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/PdfUmbenennerGuiApplication.java @@ -4,8 +4,12 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import javafx.application.Application; +import javafx.application.Platform; +import javafx.event.EventHandler; import javafx.scene.Scene; +import javafx.scene.image.Image; import javafx.stage.Stage; +import javafx.stage.WindowEvent; /** * JavaFX application entry point for the PDF-Umbenenner GUI inbound adapter. @@ -18,6 +22,9 @@ import javafx.stage.Stage; * {@code titleUpdateListener} hook. The close-request handler is installed through * {@link GuiConfigurationEditorWorkspace#installCloseRequestHandler(Stage)} so that * unsaved changes are protected when the user tries to close the window. + * + *
Beim Schließen des Fensters wird die Anwendung in den Windows System-Tray minimiert. + * Über das Tray-Kontextmenü kann das Fenster wieder geöffnet oder die Anwendung beendet werden. */ public class PdfUmbenennerGuiApplication extends Application { @@ -25,6 +32,8 @@ public class PdfUmbenennerGuiApplication extends Application { private static final double DEFAULT_WIDTH = 1100; private static final double DEFAULT_HEIGHT = 800; + private SystemTrayManager trayManager; + /** * Creates a new instance of the JavaFX application. */ @@ -35,9 +44,10 @@ public class PdfUmbenennerGuiApplication extends Application { /** * Initializes and shows the primary stage. *
+ * Lädt die Anwendungs-Icons in allen verfügbaren Größen und setzt sie am Fenster. * Wires the workspace title-update listener to the stage title so any dirty-state change - * causes an immediate window-title refresh. Also installs the close-request handler that - * guards unsaved changes before the window is closed. + * causes an immediate window-title refresh. Installs the close-request handler that + * guards unsaved changes and minimizes the window to the system tray instead of closing. * * @param primaryStage the primary stage provided by the JavaFX runtime; never {@code null} */ @@ -45,6 +55,14 @@ public class PdfUmbenennerGuiApplication extends Application { public void start(Stage primaryStage) { LOG.info("GUI: JavaFX-Oberfläche wird initialisiert."); + // Anwendungs-Icons laden; JavaFX wählt je nach Kontext automatisch die passende Größe + primaryStage.getIcons().addAll( + new Image(getClass().getResourceAsStream("/icons/Icon16.png")), + new Image(getClass().getResourceAsStream("/icons/Icon32.png")), + new Image(getClass().getResourceAsStream("/icons/Icon64.png")), + new Image(getClass().getResourceAsStream("/icons/Icon128.png")) + ); + GuiStartupContext startupContext = GuiStartupContextHolder.currentOrBlank(); GuiConfigurationEditorWorkspace workspace = new GuiConfigurationEditorWorkspace(startupContext); @@ -58,6 +76,13 @@ public class PdfUmbenennerGuiApplication extends Application { // Install the close-request handler that protects unsaved changes. workspace.installCloseRequestHandler(primaryStage); + // System-Tray aktivieren: JavaFX-Runtime nicht beenden wenn Fenster versteckt wird + Platform.setImplicitExit(false); + trayManager = new SystemTrayManager(primaryStage); + if (trayManager.install()) { + installTrayCloseHandler(primaryStage, workspace); + } + primaryStage.setMaximized(true); primaryStage.show(); @@ -70,10 +95,40 @@ public class PdfUmbenennerGuiApplication extends Application { /** * Called by the JavaFX runtime when the application is stopping. *
- * Logs the GUI shutdown event. No additional cleanup is required. + * Entfernt das System-Tray-Icon und loggt das Beenden. */ @Override public void stop() { LOG.info("GUI: JavaFX-Anwendung wird beendet."); + if (trayManager != null) { + trayManager.remove(); + } + } + + /** + * Legt einen Close-Request-Handler an, der bei sauberem Zustand das Fenster in den + * System-Tray minimiert statt es zu schließen. + *
+ * Der vom Workspace installierte Handler wird dabei vorrangig aufgerufen. Nur wenn
+ * er das Event nicht konsumiert (sauberer Zustand, keine laufenden Operationen),
+ * greift dieser Handler und versteckt das Fenster.
+ *
+ * @param stage das primäre Fenster
+ * @param workspace der Workspace-Handler, der bereits installiert wurde
+ */
+ private void installTrayCloseHandler(Stage stage, GuiConfigurationEditorWorkspace workspace) {
+ EventHandler
+ * Wird das Hauptfenster geschlossen, bleibt die Anwendung im Hintergrund aktiv und zeigt
+ * ein Tray-Icon in der Windows-Taskleiste. Über das Kontextmenü kann das Fenster wieder
+ * geöffnet oder die Anwendung vollständig beendet werden.
+ *
+ * Alle Stage-Operationen werden auf dem JavaFX Application Thread ausgeführt, da AWT-Events
+ * auf dem AWT Event Dispatch Thread eintreffen.
+ */
+class SystemTrayManager {
+
+ private static final Logger LOG = LogManager.getLogger(SystemTrayManager.class);
+
+ private final Stage stage;
+ private TrayIcon trayIcon;
+ private boolean installed;
+
+ /**
+ * Erstellt einen neuen {@code SystemTrayManager} für die angegebene Stage.
+ *
+ * @param stage das primäre Fenster; darf nicht {@code null} sein
+ */
+ SystemTrayManager(Stage stage) {
+ this.stage = stage;
+ }
+
+ /**
+ * Installiert das System-Tray-Icon.
+ *
+ * Schlägt die Installation fehl (System-Tray nicht unterstützt oder Icon-Bild nicht ladbar),
+ * wird {@code false} zurückgegeben und kein Tray-Icon angezeigt.
+ *
+ * @return {@code true} wenn das Icon erfolgreich installiert wurde, sonst {@code false}
+ */
+ boolean install() {
+ if (!SystemTray.isSupported()) {
+ LOG.warn("GUI: System-Tray wird auf diesem System nicht unterstützt.");
+ return false;
+ }
+ BufferedImage image = loadTrayImage();
+ if (image == null) {
+ return false;
+ }
+ PopupMenu menu = buildContextMenu();
+ trayIcon = new TrayIcon(image, "PDF-Umbenenner", menu);
+ trayIcon.setImageAutoSize(true);
+ // Doppelklick öffnet das Fenster
+ trayIcon.addActionListener(e -> Platform.runLater(this::showWindow));
+ try {
+ SystemTray.getSystemTray().add(trayIcon);
+ installed = true;
+ LOG.info("GUI: System-Tray-Icon erfolgreich installiert.");
+ return true;
+ } catch (AWTException e) {
+ LOG.warn("GUI: System-Tray-Icon konnte nicht installiert werden: {}", e.getMessage(), e);
+ return false;
+ }
+ }
+
+ /**
+ * Entfernt das Tray-Icon aus dem System-Tray.
+ * Ist kein Icon installiert, wird der Aufruf ignoriert.
+ */
+ void remove() {
+ if (installed && trayIcon != null) {
+ SystemTray.getSystemTray().remove(trayIcon);
+ installed = false;
+ LOG.info("GUI: System-Tray-Icon entfernt.");
+ }
+ }
+
+ /**
+ * Gibt an, ob das Tray-Icon aktiv installiert ist.
+ *
+ * @return {@code true} wenn das Icon im System-Tray sichtbar ist
+ */
+ boolean isInstalled() {
+ return installed;
+ }
+
+ private BufferedImage loadTrayImage() {
+ try (InputStream stream = getClass().getResourceAsStream("/icons/Icon16.png")) {
+ if (stream == null) {
+ LOG.warn("GUI: Tray-Icon-Ressource '/icons/Icon16.png' nicht gefunden.");
+ return null;
+ }
+ return ImageIO.read(stream);
+ } catch (IOException e) {
+ LOG.warn("GUI: Tray-Icon-Bild konnte nicht geladen werden: {}", e.getMessage(), e);
+ return null;
+ }
+ }
+
+ private PopupMenu buildContextMenu() {
+ PopupMenu menu = new PopupMenu();
+
+ MenuItem openItem = new MenuItem("Öffnen");
+ openItem.addActionListener(e -> Platform.runLater(this::showWindow));
+
+ MenuItem exitItem = new MenuItem("Beenden");
+ exitItem.addActionListener(e -> {
+ remove();
+ Platform.exit();
+ System.exit(0);
+ });
+
+ menu.add(openItem);
+ menu.addSeparator();
+ menu.add(exitItem);
+ return menu;
+ }
+
+ private void showWindow() {
+ stage.show();
+ stage.toFront();
+ }
+}