From b6546f24d5189769263870fff6ea8088731a7d95 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 11 Dec 2014 00:27:44 +0100 Subject: [PATCH] - minimizes to tray when vaults are still unlocked (i.e. webdav shares still mounted) --- .../org/cryptomator/ui/MainApplication.java | 25 +++- .../org/cryptomator/ui/MainController.java | 19 ++- .../org/cryptomator/ui/model/Directory.java | 17 +++ .../org/cryptomator/ui/util/TrayIconUtil.java | 119 ++++++++++++++++++ .../main/resources/localization.properties | 12 +- main/ui/src/main/resources/tray_icon.png | Bin 0 -> 2038 bytes 6 files changed, 184 insertions(+), 8 deletions(-) create mode 100644 main/ui/src/main/java/org/cryptomator/ui/util/TrayIconUtil.java create mode 100644 main/ui/src/main/resources/tray_icon.png diff --git a/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java b/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java index 21db728b3..aad82c792 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java +++ b/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java @@ -13,12 +13,14 @@ import java.util.ResourceBundle; import java.util.Set; import javafx.application.Application; +import javafx.application.Platform; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage; import org.cryptomator.ui.settings.Settings; +import org.cryptomator.ui.util.TrayIconUtil; import org.eclipse.jetty.util.ConcurrentHashSet; public class MainApplication extends Application { @@ -27,30 +29,41 @@ public class MainApplication extends Application { private static final CleanShutdownPerformer CLEAN_SHUTDOWN_PERFORMER = new CleanShutdownPerformer(); public static void main(String[] args) { - launch(args); + Application.launch(args); Runtime.getRuntime().addShutdownHook(CLEAN_SHUTDOWN_PERFORMER); } @Override public void start(final Stage primaryStage) throws IOException { - final ResourceBundle localizations = ResourceBundle.getBundle("localization"); - final FXMLLoader loader = new FXMLLoader(getClass().getResource("/main.fxml"), localizations); + final ResourceBundle rb = ResourceBundle.getBundle("localization"); + final FXMLLoader loader = new FXMLLoader(getClass().getResource("/main.fxml"), rb); final Parent root = loader.load(); final MainController ctrl = loader.getController(); ctrl.setStage(primaryStage); final Scene scene = new Scene(root); - primaryStage.setTitle("Cryptomator"); + primaryStage.setTitle(rb.getString("app.name")); primaryStage.setScene(scene); primaryStage.sizeToScene(); primaryStage.setResizable(false); primaryStage.show(); + TrayIconUtil.init(primaryStage, rb, () -> { + quit(); + }); + } + + private void quit() { + Platform.runLater(() -> { + CLEAN_SHUTDOWN_PERFORMER.run(); + Settings.save(); + Platform.exit(); + System.exit(0); + }); } @Override - public void stop() throws Exception { + public void stop() { CLEAN_SHUTDOWN_PERFORMER.run(); Settings.save(); - super.stop(); } public static void addShutdownTask(Runnable r) { diff --git a/main/ui/src/main/java/org/cryptomator/ui/MainController.java b/main/ui/src/main/java/org/cryptomator/ui/MainController.java index 1f6310830..ebc244700 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/MainController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/MainController.java @@ -11,8 +11,11 @@ package org.cryptomator.ui; import java.io.File; import java.io.IOException; import java.net.URL; +import java.util.Collection; import java.util.ResourceBundle; +import java.util.stream.Collectors; +import javafx.application.Platform; import javafx.collections.ListChangeListener; import javafx.event.ActionEvent; import javafx.fxml.FXML; @@ -82,7 +85,7 @@ public class MainController implements Initializable, InitializationListener, Un stage.setTitle(selectedDir.getName()); showDirectory(selectedDir); } - + private void showDirectory(Directory directory) { try { if (directory.isUnlocked()) { @@ -133,6 +136,7 @@ public class MainController implements Initializable, InitializationListener, Un @Override public void didUnlock(UnlockController ctrl) { showUnlockedView(ctrl.getDirectory()); + Platform.setImplicitExit(false); } private void showUnlockedView(Directory directory) { @@ -144,6 +148,19 @@ public class MainController implements Initializable, InitializationListener, Un @Override public void didLock(UnlockedController ctrl) { showUnlockView(ctrl.getDirectory()); + if (getUnlockedDirectories().isEmpty()) { + Platform.setImplicitExit(true); + } + } + + /* Convenience */ + + public Collection getDirectories() { + return directoryList.getItems(); + } + + public Collection getUnlockedDirectories() { + return getDirectories().stream().filter(d -> d.isUnlocked()).collect(Collectors.toSet()); } /* public Getter/Setter */ diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/Directory.java b/main/ui/src/main/java/org/cryptomator/ui/model/Directory.java index 0ace7a4d9..de7056ddd 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/Directory.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/Directory.java @@ -113,6 +113,23 @@ public class Directory implements Serializable { return server; } + /* hashcode/equals */ + + @Override + public int hashCode() { + return path.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Directory) { + final Directory other = (Directory) obj; + return this.path.equals(other.path); + } else { + return false; + } + } + /* graceful shutdown */ private class ShutdownTask implements Runnable { diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/TrayIconUtil.java b/main/ui/src/main/java/org/cryptomator/ui/util/TrayIconUtil.java new file mode 100644 index 000000000..9cf25d28b --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/util/TrayIconUtil.java @@ -0,0 +1,119 @@ +package org.cryptomator.ui.util; + +import java.awt.AWTException; +import java.awt.Image; +import java.awt.MenuItem; +import java.awt.PopupMenu; +import java.awt.SystemTray; +import java.awt.Toolkit; +import java.awt.TrayIcon; +import java.awt.TrayIcon.MessageType; +import java.awt.event.ActionEvent; +import java.io.IOException; +import java.util.ResourceBundle; + +import javafx.application.Platform; +import javafx.stage.Stage; + +import javax.swing.SwingUtilities; + +import org.apache.commons.lang3.SystemUtils; + +public final class TrayIconUtil { + + private static TrayIconUtil INSTANCE; + + private final Stage mainApplicationWindow; + private final ResourceBundle rb; + private final Runnable exitCommand; + + /** + * This will add an icon to the system tray and modify the application shutdown procedure. Depending on + * {@link Platform#isImplicitExit()} the application may still be running, allowing shutdown using the tray menu. + */ + public synchronized static void init(Stage mainApplicationWindow, ResourceBundle rb, Runnable exitCommand) { + if (INSTANCE == null && SystemTray.isSupported()) { + INSTANCE = new TrayIconUtil(mainApplicationWindow, rb, exitCommand); + } + } + + private TrayIconUtil(Stage mainApplicationWindow, ResourceBundle rb, Runnable exitCommand) { + this.mainApplicationWindow = mainApplicationWindow; + this.rb = rb; + this.exitCommand = exitCommand; + + initTrayIcon(); + } + + private void initTrayIcon() { + final TrayIcon trayIcon = createTrayIcon(); + try { + SystemTray.getSystemTray().add(trayIcon); + mainApplicationWindow.setOnCloseRequest((e) -> { + if (Platform.isImplicitExit()) { + exitCommand.run(); + } else { + mainApplicationWindow.close(); + this.showTrayNotification(trayIcon); + } + }); + } catch (SecurityException | AWTException ex) { + // not working? then just go ahead and close the app + mainApplicationWindow.setOnCloseRequest((ev) -> { + exitCommand.run(); + }); + } + } + + private TrayIcon createTrayIcon() { + final PopupMenu popup = new PopupMenu(); + + final MenuItem showItem = new MenuItem(rb.getString("tray.menu.open")); + showItem.addActionListener(this::restoreFromTray); + popup.add(showItem); + + final MenuItem exitItem = new MenuItem(rb.getString("tray.menu.quit")); + exitItem.addActionListener(this::quitFromTray); + popup.add(exitItem); + + final Image image = Toolkit.getDefaultToolkit().getImage(TrayIconUtil.class.getResource("/tray_icon.png")); + return new TrayIcon(image, rb.getString("app.name"), popup); + } + + private void showTrayNotification(TrayIcon trayIcon) { + final Runnable notificationCmd; + if (SystemUtils.IS_OS_MAC_OSX) { + final String title = rb.getString("tray.infoMsg.title"); + final String msg = rb.getString("tray.infoMsg.msg.osx"); + final String notificationCenterAppleScript = String.format("display notification \"%s\" with title \"%s\"", msg, title); + notificationCmd = () -> { + try { + Runtime.getRuntime().exec(new String[] {"/usr/bin/osascript", "-e", notificationCenterAppleScript}); + } catch (IOException e) { + // ignore, user will notice the tray icon anyway. + } + }; + } else { + final String title = rb.getString("tray.infoMsg.title"); + final String msg = rb.getString("tray.infoMsg.msg"); + notificationCmd = () -> { + trayIcon.displayMessage(title, msg, MessageType.INFO); + }; + } + SwingUtilities.invokeLater(() -> { + notificationCmd.run(); + }); + } + + private void restoreFromTray(ActionEvent event) { + Platform.runLater(() -> { + mainApplicationWindow.show(); + mainApplicationWindow.requestFocus(); + }); + } + + private void quitFromTray(ActionEvent event) { + exitCommand.run(); + } + +} diff --git a/main/ui/src/main/resources/localization.properties b/main/ui/src/main/resources/localization.properties index 165304d4f..e631f86e8 100644 --- a/main/ui/src/main/resources/localization.properties +++ b/main/ui/src/main/resources/localization.properties @@ -7,6 +7,8 @@ # Sebastian Stenzel - initial API and implementation #------------------------------------------------------------------------------- +app.name=Cryptomator + # welcome.fxml welcome.welcomeLabel=Welcome to Cryptomator @@ -34,4 +36,12 @@ unlock.messageLabel.startServerFailed=Starting WebDAV server failed. # unlocked.fxml unlocked.messageLabel.runningOnPort=Vault is accessible via WebDAV on local port %d. -unlocked.button.lock=Lock vault \ No newline at end of file +unlocked.button.lock=Lock vault + + +# tray icon +tray.menu.open=Open +tray.menu.quit=Quit +tray.infoMsg.title=Still running +tray.infoMsg.msg=Cryptomator is still alive. Quit it from the tray icon. +tray.infoMsg.msg.osx=Cryptomator is still alive. Quit it from the menu bar icon. diff --git a/main/ui/src/main/resources/tray_icon.png b/main/ui/src/main/resources/tray_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0f3bf126c0f3a41c0503c0c51d91b52910fa14e5 GIT binary patch literal 2038 zcmZ`)2~ZPf6b?rnh=D4gf}jmCMaq#)0)()J1R-EJqJac~Q7O5^m0V3WBp_%#I+dzb zu+)m2QmRmaVp@YD6&$Puszs40WdIq_k)w`PiX4jVZa_^L+?hT8_rCA{-uHIjYz}Yp zCOhnEECz$Ii{!B5kh?Q_+AKk?1G?ZT69}l#n`5p{7~1(( zNuimyg%||T3_u}~fyHQ2J@gJ5nt3NpB7WmarB%yJo+M&GDwE2k3Rr{iD2vJ=y?sOc zUy*vDYEI15#5r#av~n;`s!__cXba*M5X_`9OokSQ-*#k<6{eJ{)KZNGp)mvIDdw?- z_{6s|%)r+g=D~%8EoumHDn#pIQsxEBV+-*~iwT$q%?N1gK^~-+$`aWS(omBNsy_o* zG_jBvrj#kwe3eivMGG@g%t8y%CYhvn$(SX~h!QcV(Ta9U#jx4iLf)C_+Yc3{0%luou!XRcNhMSKeJNC53RysN*UAS zLXIx+ek8v+rg?sz$^g(QGMlQoku^n;nPp-T=Ii-qVoOdJPGB%r@<{eZ0mrUO5S_-~ z=u~{BmsZ=~9b%Q2;^3Hu^{pt)-W7WxzlY>=cHlazv|j6NL)z|1+J0qO-|*I6r`8Bw zRIC$)=lNMdoDZ?%TbjJUL#}xFGvv|#UBqx`ht;Fv(|(VpZ=LpTVMVy#E1#_{=(Txi z@5bU-&TjdoudlCq@3m_y1YDv+f^g50EQ8TFX+O(YdpyX`kN@C`OMqOb*LRb-<15_6 z=?#qqF&-P9OYmezUcgsEmn%-23%zn^M<7#1BJ{Q?VvMWWQyO+OlqA4C|>8I8tkTrRi5 z)oxNtrqR-!B91rrhnA%@#r`uf(VCJU6+XiU-R$cwUL+i79#{ugl$~gxUtMqKQ<81# z&ppG|p6X@_E!cTwtGaf_Q@w%wD8e$EWwc|qpY%&}wtd`ors@fKNcD{bAMg7|OhlxJ z5VC#)KBm{teb~94(w}^zrG@Qu`=A&8!=lzhQFV8p777e3DhQ6A#I~PXezv`-lkc-K z+~Z^}`MdBz<&zn(gT{8q$lWr{w^nwyMi(-FizMA-SA;p{Ay-%B7{?@FOg> z_;R1ZqTYFoPH)wOI~a0va~+$W(dtj`KkQx9)6+xjZ`n?{e?Zie3*D>?ojmCEL*7hB zN5^~C*;ZMPoqx)#(1%!xy}aYQ_<0vp?&k-}hZ+Z7I(<2<>n^hK`HRe4O20zxd?llK z(*gsN;%#-jCQq=sIX-2o%Y6`Z$obX&M9c8VNCuKkC@q$3OHSTf-2PBe7k@jrEM%az zes~hcIqLf4)6BfQEJxzh3~yk?<;v^_Yj8MR{oqr5!0KaIszdgtovXIif~AdEPNnmt zP~;r5vYAk~kIZDQt-P@cOfGk=?z>r=@G2>2j1@xWRJP?meL|F}>l?gye;f7?X_qct zB3*xO`&jF|YkYhhh#g52U|bt)rwJ>M>=1{4A-M5X)%OMP(C0{nyz1Io){ewPjwh?= zRL-ri3f|qZi6wM*ToJ3`4mvSuIE zg}kr)H#W3`pAb_KS7LD|lITeYdOUg5>-oQwS!-NYAZs^YRfztKBf~edYeG{F{0CIo B^fLee literal 0 HcmV?d00001