瀏覽代碼

Add minimize button and tray menu refactoring

* Add minimize button and change close button behavior
  * close main window (minimize to tray) if system tray available,
    quit application otherwise.
  * show minimize button if system tray unavailable
* Move some codes from TrayMenuController to FxApplication, includes:
  * Desktop integrations (shortcut, quit handlers...)
  * vaults change listener for sudden termination
  * public method showPreferenceWindow change to showPerferenceTab
    due to name conflict
  * public method quitApplication for both main window and system tray
  * shutdown hook for unmounting vaults on shutdown
* Add a new i18n string: main.minimizeBtn.tooltip
En-Jan Chou 5 年之前
父節點
當前提交
3369401f1c

+ 1 - 0
main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java

@@ -33,6 +33,7 @@ public enum FontAwesome5Icon {
 	TIMES("\uF00D"), //
 	USER_CROWN("\uF6A4"), //
 	WRENCH("\uF0AD"), //
+	WINDOW_MINIMIZE("\uF2D1"), //
 	;
 
 	private final String unicode;

+ 94 - 3
main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java

@@ -3,16 +3,21 @@ package org.cryptomator.ui.fxapp;
 import dagger.Lazy;
 import javafx.application.Application;
 import javafx.application.Platform;
+import javafx.beans.Observable;
 import javafx.beans.binding.Bindings;
 import javafx.beans.binding.BooleanBinding;
 import javafx.beans.value.ObservableValue;
 import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
 import javafx.collections.ObservableSet;
 import javafx.stage.Stage;
 import org.cryptomator.common.LicenseHolder;
+import org.cryptomator.common.ShutdownHook;
 import org.cryptomator.common.settings.Settings;
 import org.cryptomator.common.settings.UiTheme;
 import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.common.vaults.VaultState;
+import org.cryptomator.common.vaults.Volume;
 import org.cryptomator.jni.JniException;
 import org.cryptomator.jni.MacApplicationUiAppearance;
 import org.cryptomator.jni.MacApplicationUiState;
@@ -27,13 +32,21 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javax.inject.Inject;
+import javax.inject.Named;
+import java.awt.Desktop;
 import java.awt.desktop.QuitResponse;
+import java.util.EnumSet;
+import java.util.EventObject;
 import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 @FxApplicationScoped
 public class FxApplication extends Application {
 
 	private static final Logger LOG = LoggerFactory.getLogger(FxApplication.class);
+	private static final Set<VaultState> STATES_ALLOWING_TERMINATION = EnumSet.of(VaultState.LOCKED, VaultState.NEEDS_MIGRATION, VaultState.MISSING, VaultState.ERROR);
 
 	private final Settings settings;
 	private final Lazy<MainWindowComponent> mainWindow;
@@ -45,9 +58,13 @@ public class FxApplication extends Application {
 	private final LicenseHolder licenseHolder;
 	private final ObservableSet<Stage> visibleStages = FXCollections.observableSet();
 	private final BooleanBinding hasVisibleStages = Bindings.isNotEmpty(visibleStages);
+	private final AtomicBoolean allowSuddenTermination;
+	private final CountDownLatch shutdownLatch;
+	private final ShutdownHook shutdownHook;
+	private final ObservableList<Vault> vaults;
 
 	@Inject
-	FxApplication(Settings settings, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, UnlockComponent.Builder unlockWindowBuilder, QuitComponent.Builder quitWindowBuilder, Optional<MacFunctions> macFunctions, VaultService vaultService, LicenseHolder licenseHolder) {
+	FxApplication(Settings settings, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, UnlockComponent.Builder unlockWindowBuilder, QuitComponent.Builder quitWindowBuilder, Optional<MacFunctions> macFunctions, VaultService vaultService, LicenseHolder licenseHolder, ObservableList<Vault> vaults, @Named("shutdownLatch") CountDownLatch shutdownLatch, ShutdownHook shutdownHook) {
 		this.settings = settings;
 		this.mainWindow = mainWindow;
 		this.preferencesWindow = preferencesWindow;
@@ -56,6 +73,10 @@ public class FxApplication extends Application {
 		this.macFunctions = macFunctions;
 		this.vaultService = vaultService;
 		this.licenseHolder = licenseHolder;
+		this.vaults = vaults;
+		this.shutdownLatch = shutdownLatch;
+		this.shutdownHook = shutdownHook;
+		this.allowSuddenTermination = new AtomicBoolean(true);
 	}
 
 	public void start() {
@@ -66,6 +87,25 @@ public class FxApplication extends Application {
 
 		settings.theme().addListener(this::themeChanged);
 		loadSelectedStyleSheet(settings.theme().get());
+
+		vaults.addListener(this::vaultsChanged);
+
+		shutdownHook.runOnShutdown(this::forceUnmountRemainingVaults);
+
+		// register preferences shortcut
+		if (Desktop.getDesktop().isSupported(Desktop.Action.APP_PREFERENCES)) {
+			Desktop.getDesktop().setPreferencesHandler(this::showPreferencesWindow);
+		}
+
+		// register quit handler
+		if (Desktop.getDesktop().isSupported(Desktop.Action.APP_QUIT_HANDLER)) {
+			Desktop.getDesktop().setQuitHandler(this::handleQuitRequest);
+		}
+
+		// allow sudden termination
+		if (Desktop.getDesktop().isSupported(Desktop.Action.APP_SUDDEN_TERMINATION)) {
+			Desktop.getDesktop().enableSuddenTermination();
+		}
 	}
 
 	@Override
@@ -86,7 +126,7 @@ public class FxApplication extends Application {
 		}
 	}
 
-	public void showPreferencesWindow(SelectedPreferencesTab selectedTab) {
+	public void showPreferencesTab(SelectedPreferencesTab selectedTab) {
 		Platform.runLater(() -> {
 			Stage stage = preferencesWindow.get().showPreferencesWindow(selectedTab);
 			addVisibleStage(stage);
@@ -110,7 +150,7 @@ public class FxApplication extends Application {
 		});
 	}
 
-	public void showQuitWindow(QuitResponse response) {
+	private void showQuitWindow(@SuppressWarnings("unused") EventObject actionEvent, QuitResponse response) {
 		Platform.runLater(() -> {
 			Stage stage = quitWindowBuilder.quitResponse(response).build().showQuitWindow();
 			addVisibleStage(stage);
@@ -145,4 +185,55 @@ public class FxApplication extends Application {
 		}
 	}
 
+	public void quitApplication() {
+		handleQuitRequest(null, new QuitResponse() {
+			@Override
+			public void performQuit() {
+				shutdownLatch.countDown();
+			}
+
+			@Override
+			public void cancelQuit() {
+				// no-op
+			}
+		});
+	}
+
+	private void showPreferencesWindow(@SuppressWarnings("unused") EventObject actionEvent) {
+		showPreferencesTab(SelectedPreferencesTab.ANY);
+	}
+
+	private void handleQuitRequest(EventObject e, QuitResponse response) {
+		if (allowSuddenTermination.get()) {
+			response.performQuit(); // really?
+		} else {
+			showQuitWindow(e, response);
+		}
+	}
+
+	private void vaultsChanged(@SuppressWarnings("unused") Observable observable) {
+		boolean allVaultsAllowTermination = vaults.stream().map(Vault::getState).allMatch(STATES_ALLOWING_TERMINATION::contains);
+		boolean suddenTerminationChanged = allowSuddenTermination.compareAndSet(!allVaultsAllowTermination, allVaultsAllowTermination);
+		if (suddenTerminationChanged && Desktop.getDesktop().isSupported(Desktop.Action.APP_SUDDEN_TERMINATION)) {
+			if (allVaultsAllowTermination) {
+				Desktop.getDesktop().enableSuddenTermination();
+				LOG.debug("sudden termination enabled");
+			} else {
+				Desktop.getDesktop().disableSuddenTermination();
+				LOG.debug("sudden termination disabled");
+			}
+		}
+	}
+
+	private void forceUnmountRemainingVaults() {
+		for (Vault vault : vaults) {
+			if (vault.isUnlocked()) {
+				try {
+					vault.lock(true);
+				} catch (Volume.VolumeException e) {
+					LOG.error("Failed to unmount vault " + vault.getPath(), e);
+				}
+			}
+		}
+	}
 }

+ 11 - 6
main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowTitleController.java

@@ -5,12 +5,10 @@ import javafx.fxml.FXML;
 import javafx.scene.layout.HBox;
 import javafx.stage.Stage;
 import org.cryptomator.common.LicenseHolder;
-import org.cryptomator.common.vaults.VaultListManager;
 import org.cryptomator.ui.common.FxController;
 import org.cryptomator.ui.fxapp.FxApplication;
 import org.cryptomator.ui.fxapp.UpdateChecker;
 import org.cryptomator.ui.preferences.SelectedPreferencesTab;
-import org.cryptomator.ui.wrongfilealert.WrongFileAlertComponent;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -63,18 +61,23 @@ public class MainWindowTitleController implements FxController {
 		if (minimizeToSysTray) {
 			window.close();
 		} else {
-			window.setIconified(true);
+			application.quitApplication();
 		}
 	}
 
+	@FXML
+	public void minimize() {
+		window.setIconified(true);
+	}
+
 	@FXML
 	public void showPreferences() {
-		application.showPreferencesWindow(SelectedPreferencesTab.ANY);
+		application.showPreferencesTab(SelectedPreferencesTab.ANY);
 	}
 
 	@FXML
 	public void showDonationKeyPreferences() {
-		application.showPreferencesWindow(SelectedPreferencesTab.DONATION_KEY);
+		application.showPreferencesTab(SelectedPreferencesTab.DONATION_KEY);
 	}
 
 	/* Getter/Setter */
@@ -91,5 +94,7 @@ public class MainWindowTitleController implements FxController {
 		return updateAvailable.get();
 	}
 
-
+	public boolean isMinimizeToSysTray() {
+		return minimizeToSysTray;
+	}
 }

+ 5 - 86
main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java

@@ -3,57 +3,33 @@ package org.cryptomator.ui.traymenu;
 import javafx.application.Platform;
 import javafx.beans.Observable;
 import javafx.collections.ObservableList;
-import org.cryptomator.common.ShutdownHook;
-import org.cryptomator.common.settings.Settings;
 import org.cryptomator.common.vaults.Vault;
-import org.cryptomator.common.vaults.VaultState;
-import org.cryptomator.common.vaults.Volume;
-import org.cryptomator.ui.fxapp.FxApplication;
 import org.cryptomator.ui.launcher.FxApplicationStarter;
 import org.cryptomator.ui.preferences.SelectedPreferencesTab;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import javax.inject.Inject;
-import javax.inject.Named;
-import java.awt.Desktop;
 import java.awt.Menu;
 import java.awt.MenuItem;
 import java.awt.PopupMenu;
-import java.awt.desktop.QuitResponse;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
-import java.util.EnumSet;
 import java.util.EventObject;
 import java.util.ResourceBundle;
-import java.util.Set;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Consumer;
 
 @TrayMenuScoped
 class TrayMenuController {
-
-	private static final Logger LOG = LoggerFactory.getLogger(TrayMenuController.class);
-	public static final Set<VaultState> STATES_ALLOWING_TERMINATION = EnumSet.of(VaultState.LOCKED, VaultState.NEEDS_MIGRATION, VaultState.MISSING, VaultState.ERROR);
-
 	private final ResourceBundle resourceBundle;
 	private final FxApplicationStarter fxApplicationStarter;
-	private final CountDownLatch shutdownLatch;
-	private final ShutdownHook shutdownHook;
 	private final ObservableList<Vault> vaults;
 	private final PopupMenu menu;
-	private final AtomicBoolean allowSuddenTermination;
 
 	@Inject
-	TrayMenuController(ResourceBundle resourceBundle, FxApplicationStarter fxApplicationStarter, @Named("shutdownLatch") CountDownLatch shutdownLatch, ShutdownHook shutdownHook, ObservableList<Vault> vaults) {
+	TrayMenuController(ResourceBundle resourceBundle, FxApplicationStarter fxApplicationStarter, ObservableList<Vault> vaults) {
 		this.resourceBundle = resourceBundle;
 		this.fxApplicationStarter = fxApplicationStarter;
-		this.shutdownLatch = shutdownLatch;
-		this.shutdownHook = shutdownHook;
 		this.vaults = vaults;
 		this.menu = new PopupMenu();
-		this.allowSuddenTermination = new AtomicBoolean(true);
 	}
 
 	public PopupMenu getMenu() {
@@ -64,38 +40,11 @@ class TrayMenuController {
 		vaults.addListener(this::vaultListChanged);
 
 		rebuildMenu();
-
-		// register preferences shortcut
-		if (Desktop.getDesktop().isSupported(Desktop.Action.APP_PREFERENCES)) {
-			Desktop.getDesktop().setPreferencesHandler(this::showPreferencesWindow);
-		}
-
-		// register quit handler
-		if (Desktop.getDesktop().isSupported(Desktop.Action.APP_QUIT_HANDLER)) {
-			Desktop.getDesktop().setQuitHandler(this::handleQuitRequest);
-		}
-		shutdownHook.runOnShutdown(this::forceUnmountRemainingVaults);
-
-		// allow sudden termination
-		if (Desktop.getDesktop().isSupported(Desktop.Action.APP_SUDDEN_TERMINATION)) {
-			Desktop.getDesktop().enableSuddenTermination();
-		}
 	}
 
 	private void vaultListChanged(@SuppressWarnings("unused") Observable observable) {
 		assert Platform.isFxApplicationThread();
 		rebuildMenu();
-		boolean allVaultsAllowTermination = vaults.stream().map(Vault::getState).allMatch(STATES_ALLOWING_TERMINATION::contains);
-		boolean suddenTerminationChanged = allowSuddenTermination.compareAndSet(!allVaultsAllowTermination, allVaultsAllowTermination);
-		if (suddenTerminationChanged && Desktop.getDesktop().isSupported(Desktop.Action.APP_SUDDEN_TERMINATION)) {
-			if (allVaultsAllowTermination) {
-				Desktop.getDesktop().enableSuddenTermination();
-				LOG.debug("sudden termination enabled");
-			} else {
-				Desktop.getDesktop().disableSuddenTermination();
-				LOG.debug("sudden termination disabled");
-			}
-		}
 	}
 
 	private void rebuildMenu() {
@@ -166,45 +115,15 @@ class TrayMenuController {
 		fxApplicationStarter.get(true).thenAccept(app -> app.getVaultService().reveal(vault));
 	}
 
-	void showMainWindow(@SuppressWarnings("unused") ActionEvent actionEvent) {
+	public void showMainWindow(@SuppressWarnings("unused") ActionEvent actionEvent) {
 		fxApplicationStarter.get(true).thenAccept(app -> app.showMainWindow());
 	}
 
 	private void showPreferencesWindow(@SuppressWarnings("unused") EventObject actionEvent) {
-		fxApplicationStarter.get(true).thenAccept(app -> app.showPreferencesWindow(SelectedPreferencesTab.ANY));
-	}
-
-	private void handleQuitRequest(EventObject e, QuitResponse response) {
-		if (allowSuddenTermination.get()) {
-			response.performQuit(); // really?
-		} else {
-			fxApplicationStarter.get(true).thenAccept(app -> app.showQuitWindow(response));
-		}
+		fxApplicationStarter.get(true).thenAccept(app -> app.showPreferencesTab(SelectedPreferencesTab.ANY));
 	}
 
-	private void quitApplication(EventObject actionEvent) {
-		handleQuitRequest(actionEvent, new QuitResponse() {
-			@Override
-			public void performQuit() {
-				shutdownLatch.countDown();
-			}
-
-			@Override
-			public void cancelQuit() {
-				// no-op
-			}
-		});
-	}
-
-	private void forceUnmountRemainingVaults() {
-		for (Vault vault : vaults) {
-			if (vault.isUnlocked()) {
-				try {
-					vault.lock(true);
-				} catch (Volume.VolumeException e) {
-					LOG.error("Failed to unmount vault " + vault.getPath(), e);
-				}
-			}
-		}
+	private void quitApplication(@SuppressWarnings("unused") EventObject actionEvent) {
+		fxApplicationStarter.get(true).thenAccept(app -> app.quitApplication());
 	}
 }

+ 8 - 0
main/ui/src/main/resources/fxml/main_window_title.fxml

@@ -43,6 +43,14 @@
 				<Tooltip text="%main.preferencesBtn.tooltip"/>
 			</tooltip>
 		</Button>
+		<Button contentDisplay="GRAPHIC_ONLY" mnemonicParsing="false" onAction="#minimize" focusTraversable="false" visible="${!controller.minimizeToSysTray}" managed="${!controller.minimizeToSysTray}">
+			<graphic>
+				<FontAwesome5IconView glyph="WINDOW_MINIMIZE" glyphSize="12"/>
+			</graphic>
+			<tooltip>
+				<Tooltip text="%main.minimizeBtn.tooltip"/>
+			</tooltip>
+		</Button>
 		<Button contentDisplay="GRAPHIC_ONLY" mnemonicParsing="false" onAction="#close" focusTraversable="false">
 			<graphic>
 				<FontAwesome5IconView glyph="TIMES" glyphSize="16"/>

+ 1 - 0
main/ui/src/main/resources/i18n/strings.properties

@@ -135,6 +135,7 @@ preferences.donationKey.getDonationKey=Get a donation key
 
 # Main Window
 main.closeBtn.tooltip=Close
+main.minimizeBtn.tooltip=Minimize
 main.preferencesBtn.tooltip=Preferences
 main.donationKeyMissing.tooltip=Please consider donating
 ## Drag 'n' Drop