Feature #21/#20: Anwendungs-Icon und System-Tray einbinden
Schließt Issue #21: Alle vier Icon-Größen (16/32/64/128 px) werden beim Start am primären Stage gesetzt; JavaFX wählt automatisch die passende Größe je nach Kontext (Titelleiste, Taskleiste, Alt+Tab). Schließt Issue #20: Beim Klick auf den X-Button wird das Fenster in den Windows System-Tray minimiert (stage.hide()) statt die Anwendung zu beenden. Platform.setImplicitExit(false) hält die JavaFX-Runtime aktiv. Das Tray-Icon zeigt ein Kontextmenü mit "Öffnen" und "Beenden"; Doppelklick öffnet das Fenster ebenfalls. Beim Beenden über das Tray-Menü wird das Icon sauber entfernt. Die gesamte AWT-Tray-Logik ist in SystemTrayManager gekapselt. Der Headless-Betrieb bleibt unberührt. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+58
-3
@@ -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.
|
||||
*
|
||||
* <p>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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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<WindowEvent> 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+137
@@ -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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user