diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index be52dae3..6f009a35 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -92,6 +92,9 @@ public class AppController implements Initializable { @FXML private Menu fileMenu; + @FXML + private Menu viewMenu; + @FXML private Menu toolsMenu; @@ -116,6 +119,9 @@ public class AppController implements Initializable { @FXML private CheckMenuItem showTxHex; + @FXML + private MenuItem minimizeToTray; + @FXML private MenuItem refreshWallet; @@ -285,6 +291,10 @@ public class AppController implements Initializable { } else if(platform == org.controlsfx.tools.Platform.WINDOWS) { toolsMenu.getItems().removeIf(item -> item.getStyleClass().contains("windowsHide")); } + + if(platform == org.controlsfx.tools.Platform.UNIX || !TrayManager.isSupported()) { + viewMenu.getItems().remove(minimizeToTray); + } } public void showIntroduction(ActionEvent event) { @@ -953,6 +963,10 @@ public class AppController implements Initializable { messageSignDialog.showAndWait(); } + public void minimizeToTray(ActionEvent event) { + AppServices.get().minimizeStage((Stage)tabs.getScene().getWindow()); + } + public void refreshWallet(ActionEvent event) { Tab selectedTab = tabs.getSelectionModel().getSelectedItem(); TabData tabData = (TabData)selectedTab.getUserData(); diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java index 75d4ddf2..c374f7d5 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java @@ -9,6 +9,7 @@ import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.uri.BitcoinURI; import com.sparrowwallet.drongo.wallet.KeystoreSource; import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.control.TrayManager; import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.Device; @@ -64,6 +65,8 @@ public class AppServices { private final Map> walletWindows = new LinkedHashMap<>(); + private TrayManager trayManager; + private static final BooleanProperty onlineProperty = new SimpleBooleanProperty(false); private ExchangeSource.RatesService ratesService; @@ -401,6 +404,15 @@ public class AppServices { return application; } + public void minimizeStage(Stage stage) { + if(trayManager == null) { + trayManager = new TrayManager(); + } + + trayManager.addStage(stage); + stage.hide(); + } + public Map getOpenWallets() { Map openWallets = new LinkedHashMap<>(); for(List walletTabDataList : walletWindows.values()) { diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TrayManager.java b/src/main/java/com/sparrowwallet/sparrow/control/TrayManager.java new file mode 100644 index 00000000..ed1118ec --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/TrayManager.java @@ -0,0 +1,95 @@ +package com.sparrowwallet.sparrow.control; + +import javafx.application.Platform; +import javafx.stage.Stage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.imageio.ImageIO; +import javax.swing.*; +import java.awt.*; +import java.awt.image.BaseMultiResolutionImage; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class TrayManager { + private static final Logger log = LoggerFactory.getLogger(TrayManager.class); + + private final SystemTray tray; + private final TrayIcon trayIcon; + private final PopupMenu popupMenu = new PopupMenu(); + + public TrayManager() { + if(!SystemTray.isSupported()) { + throw new UnsupportedOperationException("SystemTray icons are not supported by the current desktop environment."); + } + + tray = SystemTray.getSystemTray(); + + try { + List imgList = new ArrayList<>(); + imgList.add(ImageIO.read(getClass().getResource("/image/sparrow-white-small.png"))); + imgList.add(ImageIO.read(getClass().getResource("/image/sparrow-white-small@2x.png"))); + imgList.add(ImageIO.read(getClass().getResource("/image/sparrow-white-small@3x.png"))); + BaseMultiResolutionImage mrImage = new BaseMultiResolutionImage(imgList.toArray(new Image[0])); + + this.trayIcon = new TrayIcon(mrImage, "Sparrow", popupMenu); + + MenuItem miExit = new MenuItem("Quit Sparrow"); + miExit.addActionListener(e -> { + SwingUtilities.invokeLater(() -> { tray.remove(this.trayIcon); }); + Platform.exit(); + }); + this.popupMenu.add(miExit); + } catch(IOException e) { + log.error("Could not load system tray image", e); + throw new IllegalStateException(e); + } + } + + public void addStage(Stage stage) { + EventQueue.invokeLater(() -> { + MenuItem miStage = new MenuItem(stage.getTitle()); + miStage.setFont(Font.decode(null).deriveFont(Font.BOLD)); + miStage.addActionListener(e -> Platform.runLater(() -> { + stage.show(); + EventQueue.invokeLater(() -> { + popupMenu.remove(miStage); + + if(popupMenu.getItemCount() == 1) { + Platform.setImplicitExit(true); + SwingUtilities.invokeLater(() -> tray.remove(trayIcon)); + } + }); + })); + //Make sure it's always at the top + this.popupMenu.insert(miStage,popupMenu.getItemCount() - 1); + + if(!isShowing()) { + // Keeps the JVM running even if there are no + // visible JavaFX Stages, otherwise JVM would + // exit and we lose the TrayIcon + Platform.setImplicitExit(false); + + SwingUtilities.invokeLater(() -> { + try { + tray.add(this.trayIcon); + } catch(AWTException e) { + log.error("Unable to add system tray icon", e); + } + }); + } + }); + } + + public boolean isShowing() { + return Arrays.stream(tray.getTrayIcons()).collect(Collectors.toList()).contains(trayIcon); + } + + public static boolean isSupported() { + return Desktop.isDesktopSupported() && SystemTray.isSupported(); + } +} diff --git a/src/main/resources/com/sparrowwallet/sparrow/app.fxml b/src/main/resources/com/sparrowwallet/sparrow/app.fxml index edaa9964..658e5419 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/app.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/app.fxml @@ -47,7 +47,7 @@ - + @@ -87,6 +87,7 @@ + diff --git a/src/main/resources/image/sparrow-white-small.png b/src/main/resources/image/sparrow-white-small.png new file mode 100644 index 00000000..3f1d9530 Binary files /dev/null and b/src/main/resources/image/sparrow-white-small.png differ diff --git a/src/main/resources/image/sparrow-white-small@2x.png b/src/main/resources/image/sparrow-white-small@2x.png new file mode 100644 index 00000000..eba0be2f Binary files /dev/null and b/src/main/resources/image/sparrow-white-small@2x.png differ diff --git a/src/main/resources/image/sparrow-white-small@3x.png b/src/main/resources/image/sparrow-white-small@3x.png new file mode 100644 index 00000000..9cb1fe37 Binary files /dev/null and b/src/main/resources/image/sparrow-white-small@3x.png differ