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 workspaceHandler = stage.getOnCloseRequest(); + stage.setOnCloseRequest(event -> { + // Workspace-Handler zuerst: prüft Dirty-State, laufende Operationen usw. + if (workspaceHandler != null) { + workspaceHandler.handle(event); + } + // Wurde das Event nicht konsumiert, ist der Zustand sauber: Fenster in Tray verstecken + if (!event.isConsumed()) { + event.consume(); + LOG.info("GUI: Fenster wird in den System-Tray minimiert."); + stage.hide(); + } + }); } } diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/SystemTrayManager.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/SystemTrayManager.java new file mode 100644 index 0000000..85a9d52 --- /dev/null +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/SystemTrayManager.java @@ -0,0 +1,137 @@ +package de.gecheckt.pdf.umbenenner.adapter.in.gui; + +import java.awt.AWTException; +import java.awt.MenuItem; +import java.awt.PopupMenu; +import java.awt.SystemTray; +import java.awt.TrayIcon; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; + +import javax.imageio.ImageIO; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import javafx.application.Platform; +import javafx.stage.Stage; + +/** + * Verwaltet das Windows System-Tray-Icon für den PDF-Umbenenner. + *

+ * 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(); + } +}