Forráskód Böngészése

- minimizes to tray when vaults are still unlocked (i.e. webdav shares still mounted)

Sebastian Stenzel 10 éve
szülő
commit
b6546f24d5

+ 19 - 6
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) {

+ 18 - 1
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<Directory> getDirectories() {
+		return directoryList.getItems();
+	}
+
+	public Collection<Directory> getUnlockedDirectories() {
+		return getDirectories().stream().filter(d -> d.isUnlocked()).collect(Collectors.toSet());
 	}
 
 	/* public Getter/Setter */

+ 17 - 0
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 {

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

+ 11 - 1
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
+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.

BIN
main/ui/src/main/resources/tray_icon.png