mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-08-20 06:06:02 +00:00
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
This commit is contained in:
@@ -33,6 +33,7 @@ public enum FontAwesome5Icon {
|
||||
TIMES("\uF00D"), //
|
||||
USER_CROWN("\uF6A4"), //
|
||||
WRENCH("\uF0AD"), //
|
||||
WINDOW_MINIMIZE("\uF2D1"), //
|
||||
;
|
||||
|
||||
private final String unicode;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
fxApplicationStarter.get(true).thenAccept(app -> app.showPreferencesTab(SelectedPreferencesTab.ANY));
|
||||
}
|
||||
|
||||
private void handleQuitRequest(EventObject e, QuitResponse response) {
|
||||
if (allowSuddenTermination.get()) {
|
||||
response.performQuit(); // really?
|
||||
} else {
|
||||
fxApplicationStarter.get(true).thenAccept(app -> app.showQuitWindow(response));
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user