From 8fc971c07cc344d05af323fd0685c665e544ff60 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Wed, 7 Apr 2021 09:11:59 +0200 Subject: [PATCH] add minimize to system tray functionality --- .../sparrowwallet/sparrow/AppController.java | 14 +++ .../sparrowwallet/sparrow/AppServices.java | 12 +++ .../sparrow/control/TrayManager.java | 95 ++++++++++++++++++ .../com/sparrowwallet/sparrow/app.fxml | 3 +- .../resources/image/sparrow-white-small.png | Bin 0 -> 544 bytes .../image/sparrow-white-small@2x.png | Bin 0 -> 913 bytes .../image/sparrow-white-small@3x.png | Bin 0 -> 1841 bytes 7 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/sparrowwallet/sparrow/control/TrayManager.java create mode 100644 src/main/resources/image/sparrow-white-small.png create mode 100644 src/main/resources/image/sparrow-white-small@2x.png create mode 100644 src/main/resources/image/sparrow-white-small@3x.png 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 0000000000000000000000000000000000000000..3f1d9530c1413f096ed0f9b982f17454ec4fd01a GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uuz(rC1}QWNE&K$eWHMd+Lx40B zNDK%#fLH@cgV^uW7~bs$(z`uf978MwTP_%K9dZzGxOn7lNKiF5zrqrsnExzgvWk-$-P6j-ZrrVi^Oi`iap8!%b=RAxZ{u;U zz~?Npo%VKG9hDWz=6U$qR&TkWLi-%632gT{wQg8UP|5R9H~DqrVI%J|iG?n_(%iPs zi*_ldTsSuS36JGfyXGq*Ck&2$w+u>XQ~uI9#lCQ1)g!i@sps!H35g};6dv7QanT~N z^V#uwpzHttwySIjIObaM7OUpS`ZS~R&~y2nK2 zCtQ|I<^tThTYPu#W7fQr>cuWsmi{B{>gPE|$5Jvr9P7y4YQmoGn$Icr!tA#3_wIRN z%UZPm{Nv}DV}8Wzik#r5Lf3vy|0^03Z)7f47I@sp8YI8O;&ID`$c@oV?lvEea_>~J rQ2tRC^-_i9C-=`)HewnZYu&XIckT&WOW literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..eba0be2f4b41e2b05b5189b82ee5e8a29295b74a GIT binary patch literal 913 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}E~ycoX}-P; zT0k}j17mw80}DtA5K93u0|WB{Mh0de%?J`(zyz07Sip>6gA}f5OZp6?WHMd+Lx40B zNDK%BfLH@c1KEu0PH?)I0_iGG7srqY&MQ~;dQT0MU?}j_=kiRLAn(y(VQFzkD#%el zIK7#TU3Q79_BM_jc84mujBPhXHx7OZ&Wu2huN6~_QU*2}lwOHMic`tPCH;#Cih z?|oi5|8wDa%lB(n{kpF-`Q(xcAK#J7q1DC65bTHVKN&m}7muWQAhg zK_mUjqU9b=|1^zdChFT&eb5R2zsFKGtMYjIxtWWnn`Ldb&MN=V>Gbi~;Y;O?g>Me9 z6mH)9c=a)hyIUr79sTjqy5;?zu0t+DCBm_YF?#c1)_7yEj|4 z_WH`Hb7Pd%iVP2m&F=i;Yg}gN8*@y2&!ftVt{Pq26MEyP2v4{27yAA}{r4JUr-#Xb z0^Jp-^`5%KJV}}3v84N*i-?BIGw%8y(*>>X99_JKJ5VHDV*PEor^mK2+HE+Xu-*Nm zI0M7~|8dj%W&mTs7ZeLmuWa;o3KThBXnOXP$RR}|uEpDWICx~{NJ*Vudsb7h&^M!` z^H^KkKL+vX+ZLQ%V6Tv|cCCYyZ1gqn%)Fsi<2Y*)_jxRmzc1kz&rtb%_cORKI+gVgL+8A=C z&H>6w+B}kO-u3u_)bSfGjL$8c&g3>NoxMS4-o?WUp7>WZsh+Uk*J1i(WBUr#p#81^ zeCt147v?_Qk-+|Go`nn3Cb292l9xOkI6j@(e>gr~Cr0Up`uh%%NgTYL#qmlRjH+86 zu8$VGl&1B3a&5A)PoJv5=Nk=oUQ3;t5G{1Pg#DNP0h{y)iOnPx#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91K%fHv1ONa40RR91KmY&$07g+lumAu69BD*PQ~&?~0ssI200000AOHXW zC;$KeAOHXWAOHXZu1wwaWB>pIdPzhdt?s9Unk-Y=iJK**m2!%pE48;KCxm};9%BREmsKPZo#B>yT zs*!FshR#kxBM#vcK4L41k?UsNo+z76&cSymenq~*pXd0jK;N7abSxL8sDYk%7h>ZH zCnlipO$j2I`iv>F35(r~9Lr2~5U8=xoO zjpuj=vvGig<@mo-f(kWs_0~4T?#-YKErjb3#1B{vO^K-}u-g{X`Ke5e-h<<)L_gD+ z6C@=6L>$8-Xe3yTXJnXpto*j%ec5N0EGx@;q3|8)D8G9QH- zQ=o<8-M9&}3)g$np1@jY>l%$g(UtOdKGwj@jU`!JEar@RKcOjHi{K1&K%t+pU&2S$ zK9sK&d$9=G3jOe57|y_IXf3LVzM-xiQ_)J?gLVP4BY>ny;}uuVV>6aQHz0a(<mBO*TUB4E9EXF_$Q)5RE1)_DZrhG{l*8fr7L>YHz=d&*&|o zZ8%^&NZa2OR)Jl(1^q(n4WY$x7A*aU>+xn$*jTi{tD#YeD&rJ2xWy!DiSCcOXuC zOXuBC8PO%bYINKQ_u(_lw!Nam-B9A^g-2~VFN4*v6%N8vm<^ktlN}JB^SNQ=wNAz;I=Hq#$?q#ku<{S(Ygy>D6 zpV%LM6r7z5Jy+^QikB7j=-cwoYeqqi1OaST;KJ4MW@^ds?9m(A_(CfFb+QlcMxV8~f~=5w`$zIQa^$_Kkw)VMZqBH_iJJSOd$T393vv9f(1g=HoEPp?vHCg;xaa{OQVj z9i0^jd!KwD7;*Z`D_@(W!YPu3KWPo zP#}~K^lz}v?PG8o20+P8$5rgf+00000NkvXXu0mjf89GAF literal 0 HcmV?d00001