mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-14 08:41:28 +00:00
Merge pull request #4069 from cryptomator/feature/notify-fallback
Feature: Notification Window with JavaFX
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import ch.qos.logback.classic.spi.Configurator;
|
||||
import org.cryptomator.networking.SSLContextWithPKCS12TrustStore;
|
||||
import org.cryptomator.common.locationpresets.DropboxLinuxLocationPresetsProvider;
|
||||
import org.cryptomator.common.locationpresets.DropboxMacLocationPresetsProvider;
|
||||
import org.cryptomator.common.locationpresets.DropboxWindowsLocationPresetsProvider;
|
||||
@@ -14,11 +13,12 @@ import org.cryptomator.common.locationpresets.OneDriveLinuxLocationPresetsProvid
|
||||
import org.cryptomator.common.locationpresets.OneDriveMacLocationPresetsProvider;
|
||||
import org.cryptomator.common.locationpresets.OneDriveWindowsLocationPresetsProvider;
|
||||
import org.cryptomator.common.locationpresets.PCloudLocationPresetsProvider;
|
||||
import org.cryptomator.networking.SSLContextWithMacKeychain;
|
||||
import org.cryptomator.networking.SSLContextProvider;
|
||||
import org.cryptomator.networking.SSLContextWithWindowsCertStore;
|
||||
import org.cryptomator.integrations.tray.TrayMenuController;
|
||||
import org.cryptomator.logging.LogbackConfiguratorFactory;
|
||||
import org.cryptomator.networking.SSLContextProvider;
|
||||
import org.cryptomator.networking.SSLContextWithMacKeychain;
|
||||
import org.cryptomator.networking.SSLContextWithPKCS12TrustStore;
|
||||
import org.cryptomator.networking.SSLContextWithWindowsCertStore;
|
||||
import org.cryptomator.ui.traymenu.AwtTrayMenuController;
|
||||
|
||||
open module org.cryptomator.desktop {
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
package org.cryptomator.common;
|
||||
|
||||
import org.cryptomator.cryptofs.event.BrokenDirFileEvent;
|
||||
import org.cryptomator.cryptofs.event.BrokenFileNodeEvent;
|
||||
import org.cryptomator.cryptofs.event.ConflictResolutionFailedEvent;
|
||||
import org.cryptomator.cryptofs.event.ConflictResolvedEvent;
|
||||
import org.cryptomator.cryptofs.event.DecryptionFailedEvent;
|
||||
import org.cryptomator.cryptofs.event.FileIsInUseEvent;
|
||||
import org.cryptomator.cryptofs.event.FilesystemEvent;
|
||||
import org.cryptomator.event.VaultEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.MapChangeListener;
|
||||
import javafx.collections.ObservableMap;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Map containing {@link VaultEvent}s.
|
||||
* The map is keyed by the ciphertext path of the affected resource _and_ the {@link FilesystemEvent}s class in order to group same events
|
||||
* <p>
|
||||
* Use {@link EventMap#put(VaultEvent)} to add an element and {@link EventMap#remove(VaultEvent)} to remove it.
|
||||
* <p>
|
||||
* The map is size restricted to {@value MAX_SIZE} elements. If a _new_ element (i.e. not already present) is added, the least recently added is removed.
|
||||
*/
|
||||
@Singleton
|
||||
public class EventMap implements ObservableMap<EventMap.EventKey, VaultEvent> {
|
||||
|
||||
private static final int MAX_SIZE = 300;
|
||||
|
||||
public record EventKey(Path ciphertextPath, Class<? extends FilesystemEvent> c) {}
|
||||
|
||||
private final ObservableMap<EventMap.EventKey, VaultEvent> delegate;
|
||||
|
||||
@Inject
|
||||
public EventMap() {
|
||||
delegate = FXCollections.observableHashMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addListener(MapChangeListener<? super EventKey, ? super VaultEvent> mapChangeListener) {
|
||||
delegate.addListener(mapChangeListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener(MapChangeListener<? super EventKey, ? super VaultEvent> mapChangeListener) {
|
||||
delegate.removeListener(mapChangeListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return delegate.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return delegate.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsKey(Object key) {
|
||||
return delegate.containsKey(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsValue(Object value) {
|
||||
return delegate.containsValue(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VaultEvent get(Object key) {
|
||||
return delegate.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable VaultEvent put(EventKey key, VaultEvent value) {
|
||||
return delegate.put(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VaultEvent remove(Object key) {
|
||||
return delegate.remove(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putAll(@NotNull Map<? extends EventKey, ? extends VaultEvent> m) {
|
||||
delegate.putAll(m);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
delegate.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Set<EventKey> keySet() {
|
||||
return delegate.keySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Collection<VaultEvent> values() {
|
||||
return delegate.values();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Set<Entry<EventKey, VaultEvent>> entrySet() {
|
||||
return delegate.entrySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addListener(InvalidationListener invalidationListener) {
|
||||
delegate.addListener(invalidationListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener(InvalidationListener invalidationListener) {
|
||||
delegate.removeListener(invalidationListener);
|
||||
}
|
||||
|
||||
public synchronized void put(VaultEvent e) {
|
||||
//compute key
|
||||
var key = computeKey(e.actualEvent());
|
||||
//if-else
|
||||
var nullOrEntry = delegate.get(key);
|
||||
if (nullOrEntry == null) {
|
||||
if (size() == MAX_SIZE) {
|
||||
delegate.entrySet().stream() //
|
||||
.min(Comparator.comparing(entry -> entry.getValue().actualEvent().getTimestamp())) //
|
||||
.ifPresent(oldestEntry -> delegate.remove(oldestEntry.getKey()));
|
||||
}
|
||||
delegate.put(key, e);
|
||||
} else {
|
||||
delegate.put(key, nullOrEntry.incrementCount(e.actualEvent()));
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized VaultEvent remove(VaultEvent similar) {
|
||||
//compute key
|
||||
var key = computeKey(similar.actualEvent());
|
||||
return this.remove(key);
|
||||
}
|
||||
|
||||
private EventKey computeKey(FilesystemEvent e) {
|
||||
var p = switch (e) {
|
||||
case DecryptionFailedEvent(_, Path ciphertextPath, _) -> ciphertextPath;
|
||||
case ConflictResolvedEvent(_, _, _, _, Path resolvedCiphertext) -> resolvedCiphertext;
|
||||
case ConflictResolutionFailedEvent(_, _, Path conflictingCiphertext, _) -> conflictingCiphertext;
|
||||
case BrokenDirFileEvent(_, Path ciphertext) -> ciphertext;
|
||||
case BrokenFileNodeEvent(_, _, Path ciphertext) -> ciphertext;
|
||||
case FileIsInUseEvent(_, _, Path ciphertext, _, _, _) -> ciphertext;
|
||||
};
|
||||
return new EventKey(p, e.getClass());
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import org.cryptomator.cryptolib.api.CryptoException;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoader;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||
import org.cryptomator.event.FileSystemEventAggregator;
|
||||
import org.cryptomator.event.NotificationManager;
|
||||
import org.cryptomator.integrations.mount.MountFailedException;
|
||||
import org.cryptomator.integrations.mount.Mountpoint;
|
||||
import org.cryptomator.integrations.mount.UnmountFailedException;
|
||||
@@ -79,6 +80,7 @@ public class Vault {
|
||||
private final Mounter mounter;
|
||||
private final Settings settings;
|
||||
private final FileSystemEventAggregator fileSystemEventAggregator;
|
||||
private final NotificationManager notificationManager;
|
||||
private final BooleanProperty showingStats;
|
||||
|
||||
private final AtomicReference<Mounter.MountHandle> mountHandle = new AtomicReference<>(null);
|
||||
@@ -91,7 +93,8 @@ public class Vault {
|
||||
@Named("lastKnownException") ObjectProperty<Exception> lastKnownException, //
|
||||
VaultStats stats, //
|
||||
Mounter mounter, Settings settings, //
|
||||
FileSystemEventAggregator fileSystemEventAggregator) {
|
||||
FileSystemEventAggregator fileSystemEventAggregator, //
|
||||
NotificationManager notificationManager) {
|
||||
this.vaultSettings = vaultSettings;
|
||||
this.configCache = configCache;
|
||||
this.cryptoFileSystem = cryptoFileSystem;
|
||||
@@ -110,6 +113,7 @@ public class Vault {
|
||||
this.mounter = mounter;
|
||||
this.settings = settings;
|
||||
this.fileSystemEventAggregator = fileSystemEventAggregator;
|
||||
this.notificationManager = notificationManager;
|
||||
this.showingStats = new SimpleBooleanProperty(false);
|
||||
this.quickAccessEntry = new AtomicReference<>(null);
|
||||
}
|
||||
@@ -266,6 +270,7 @@ public class Vault {
|
||||
|
||||
private void consumeVaultEvent(FilesystemEvent e) {
|
||||
fileSystemEventAggregator.put(this, e);
|
||||
notificationManager.offer(this, e);
|
||||
}
|
||||
|
||||
// ******************************************************************************
|
||||
|
||||
82
src/main/java/org/cryptomator/event/NotificationManager.java
Normal file
82
src/main/java/org/cryptomator/event/NotificationManager.java
Normal file
@@ -0,0 +1,82 @@
|
||||
package org.cryptomator.event;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.cryptofs.event.FilesystemEvent;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Manager for notifications.
|
||||
* <p>
|
||||
* To add (filesystem) events, use method {@link #offer(Vault, FilesystemEvent)}. If the input event is eligible, it is added to an internal queue.
|
||||
* An event is eligible, if
|
||||
* <ul>
|
||||
* <li>the event should trigger a notification and</li>
|
||||
* <li>it is not added within the last {@value DEBOUNCE_THRESHOLD_SECONDS} seconds</li>
|
||||
* </ul>
|
||||
*
|
||||
* @see org.cryptomator.ui.fxapp.FxNotificationManager
|
||||
*/
|
||||
@Singleton
|
||||
public class NotificationManager {
|
||||
|
||||
private static final int DEBOUNCE_THRESHOLD_SECONDS = 5;
|
||||
|
||||
private final Cache<FSEventBucket, FilesystemEvent> debounceCache;
|
||||
private final ConcurrentLinkedQueue<VaultEvent> pendingEvents;
|
||||
|
||||
@Inject
|
||||
public NotificationManager() {
|
||||
debounceCache = Caffeine.newBuilder().expireAfterWrite(Duration.ofSeconds(DEBOUNCE_THRESHOLD_SECONDS)).build();
|
||||
pendingEvents = new ConcurrentLinkedQueue<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Offers the given filesystem event to the notification manager.
|
||||
*
|
||||
* @param v The vault where the filesystem event happened
|
||||
* @param e the actual filesystem event
|
||||
* @return {@code true} if the filesystem event is accepted, otherwise {@code false}.
|
||||
*/
|
||||
public boolean offer(Vault v, FilesystemEvent e) {
|
||||
return switch (e) {
|
||||
//example: case BrokenFileNodeEvent bfne -> addEvent(v, bfne.ciphertextPath(), bfne);
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
boolean addEvent(Vault v, Path keyPath, FilesystemEvent e) {
|
||||
var key = new FSEventBucket(v, keyPath, e.getClass());
|
||||
var isAdded = new AtomicBoolean(false);
|
||||
debounceCache.asMap().computeIfAbsent(key, _ -> {
|
||||
synchronized (this) {
|
||||
pendingEvents.add(new VaultEvent(v, e));
|
||||
isAdded.set(true);
|
||||
}
|
||||
return e;
|
||||
});
|
||||
return isAdded.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds all events to the target list and clears afterward the pending-event-queue
|
||||
*
|
||||
* @param target list where the filesystem events are copied to
|
||||
* @return {@code true}, if elements were copied
|
||||
*/
|
||||
public boolean addTo(List<VaultEvent> target) {
|
||||
synchronized (this) {
|
||||
var result = target.addAll(pendingEvents);
|
||||
pendingEvents.clear();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,25 +3,6 @@ package org.cryptomator.event;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.cryptofs.event.FilesystemEvent;
|
||||
|
||||
import java.time.Instant;
|
||||
public record VaultEvent(Vault v, FilesystemEvent actualEvent) {
|
||||
|
||||
public record VaultEvent(Vault v, FilesystemEvent actualEvent, int count) implements Comparable<VaultEvent> {
|
||||
|
||||
public VaultEvent(Vault v, FilesystemEvent actualEvent) {
|
||||
this(v, actualEvent, 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(VaultEvent other) {
|
||||
var timeResult = actualEvent.getTimestamp().compareTo(other.actualEvent().getTimestamp());
|
||||
if(timeResult != 0) {
|
||||
return timeResult;
|
||||
} else {
|
||||
return this.equals(other) ? 0 : this.actualEvent.getClass().getName().compareTo(other.actualEvent.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
public VaultEvent incrementCount(FilesystemEvent update) {
|
||||
return new VaultEvent(v, update, count+1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,8 @@ public enum FxmlFile {
|
||||
UNLOCK_SUCCESS("/fxml/unlock_success.fxml"), //
|
||||
VAULT_OPTIONS("/fxml/vault_options.fxml"), //
|
||||
VAULT_STATISTICS("/fxml/stats.fxml"), //
|
||||
WRONGFILEALERT("/fxml/wrongfilealert.fxml");
|
||||
WRONGFILEALERT("/fxml/wrongfilealert.fxml"),
|
||||
NOTIFICATION("/fxml/notification.fxml");
|
||||
|
||||
private final String ressourcePathString;
|
||||
|
||||
|
||||
56
src/main/java/org/cryptomator/ui/common/SystemBarUtil.java
Normal file
56
src/main/java/org/cryptomator/ui/common/SystemBarUtil.java
Normal file
@@ -0,0 +1,56 @@
|
||||
package org.cryptomator.ui.common;
|
||||
|
||||
import javafx.stage.Screen;
|
||||
|
||||
/**
|
||||
* Utility class providing methods regarding the OS bar.
|
||||
*/
|
||||
public class SystemBarUtil {
|
||||
|
||||
public enum Placement {
|
||||
/**
|
||||
* OS Bar placed at the left screen edge
|
||||
*/
|
||||
LEFT,
|
||||
/**
|
||||
* OS Bar placed at the top screen edge
|
||||
*/
|
||||
TOP,
|
||||
/**
|
||||
* OS Bar placed at the right screen edge
|
||||
*/
|
||||
RIGHT,
|
||||
/**
|
||||
* OS Bar placed at the bottom screen edge
|
||||
*/
|
||||
BOTTOM;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the placement of the OS bar on the given screen.
|
||||
* <p>
|
||||
* <b>Assuming the OS bar fills one screen edge completely</b>,
|
||||
* this method determines that screen edge by comparing the actual screen bounds with the visual ones.
|
||||
* <p>
|
||||
* If the screen does not have a system bar, the bottom placement is returned.
|
||||
* If the screen does have multiple system bars, the first in following priority is returned:
|
||||
* LEFT, TOP, RIGHT, BOTTOM.
|
||||
*
|
||||
* @param screen a {@link Screen} where an OS bar exists
|
||||
* @return {@link Placement} indicating the screen edge.
|
||||
*/
|
||||
public static Placement getPlacementOfSystembar(Screen screen) {
|
||||
var bounds = screen.getBounds();
|
||||
var vBounds = screen.getVisualBounds();
|
||||
//assumption: the system bar fills a whole screen side
|
||||
if (bounds.getMinX() != vBounds.getMinX()) {
|
||||
return Placement.LEFT;
|
||||
} else if (bounds.getMinY() != vBounds.getMinY()) {
|
||||
return Placement.TOP;
|
||||
} else if (bounds.getMaxX() != vBounds.getMaxX()) {
|
||||
return Placement.RIGHT;
|
||||
} else {
|
||||
return Placement.BOTTOM;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ public enum FontAwesome5Icon {
|
||||
CARET_DOWN("\uF0D7"), //
|
||||
CARET_RIGHT("\uF0Da"), //
|
||||
CHECK("\uF00C"), //
|
||||
CHEVRON_LEFT("\uF053"), //
|
||||
CHEVRON_RIGHT("\uF054"), //
|
||||
CLOCK("\uF017"), //
|
||||
CLIPBOARD("\uF328"), //
|
||||
COG("\uF013"), //
|
||||
|
||||
@@ -30,9 +30,20 @@ public class FxApplication {
|
||||
private final FxApplicationTerminator applicationTerminator;
|
||||
private final AutoUnlocker autoUnlocker;
|
||||
private final FxFSEventList fxFSEventList;
|
||||
private final FxNotificationManager notificationManager;
|
||||
|
||||
@Inject
|
||||
FxApplication(@Named("startupTime") long startupTime, Environment environment, Settings settings, AppLaunchEventHandler launchEventHandler, Lazy<TrayMenuComponent> trayMenu, FxApplicationWindows appWindows, FxApplicationStyle applicationStyle, FxApplicationTerminator applicationTerminator, AutoUnlocker autoUnlocker, FxFSEventList fxFSEventList) {
|
||||
FxApplication(@Named("startupTime") long startupTime, //
|
||||
Environment environment, //
|
||||
Settings settings, //
|
||||
AppLaunchEventHandler launchEventHandler, //
|
||||
Lazy<TrayMenuComponent> trayMenu, //
|
||||
FxApplicationWindows appWindows, //
|
||||
FxApplicationStyle applicationStyle, //
|
||||
FxApplicationTerminator applicationTerminator, //
|
||||
AutoUnlocker autoUnlocker, //
|
||||
FxFSEventList fxFSEventList, //
|
||||
FxNotificationManager notificationManager) {
|
||||
this.startupTime = startupTime;
|
||||
this.environment = environment;
|
||||
this.settings = settings;
|
||||
@@ -43,6 +54,7 @@ public class FxApplication {
|
||||
this.applicationTerminator = applicationTerminator;
|
||||
this.autoUnlocker = autoUnlocker;
|
||||
this.fxFSEventList = fxFSEventList;
|
||||
this.notificationManager = notificationManager;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
@@ -88,6 +100,7 @@ public class FxApplication {
|
||||
|
||||
launchEventHandler.startHandlingLaunchEvents();
|
||||
fxFSEventList.schedulePollForUpdates();
|
||||
notificationManager.schedulePollForUpdates();
|
||||
autoUnlocker.tryUnlockForTimespan(2, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.cryptomator.ui.eventview.EventViewComponent;
|
||||
import org.cryptomator.ui.health.HealthCheckComponent;
|
||||
import org.cryptomator.ui.lock.LockComponent;
|
||||
import org.cryptomator.ui.mainwindow.MainWindowComponent;
|
||||
import org.cryptomator.ui.notification.NotificationComponent;
|
||||
import org.cryptomator.ui.preferences.PreferencesComponent;
|
||||
import org.cryptomator.ui.quit.QuitComponent;
|
||||
import org.cryptomator.ui.recoverykey.RecoveryKeyComponent;
|
||||
@@ -39,7 +40,8 @@ import java.io.InputStream;
|
||||
UpdateReminderComponent.class, //
|
||||
ShareVaultComponent.class, //
|
||||
EventViewComponent.class, //
|
||||
RecoveryKeyComponent.class})
|
||||
RecoveryKeyComponent.class, //
|
||||
NotificationComponent.class })
|
||||
abstract class FxApplicationModule {
|
||||
|
||||
private static Image createImageFromResource(String resourceName) throws IOException {
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.cryptomator.ui.error.ErrorComponent;
|
||||
import org.cryptomator.ui.eventview.EventViewComponent;
|
||||
import org.cryptomator.ui.lock.LockComponent;
|
||||
import org.cryptomator.ui.mainwindow.MainWindowComponent;
|
||||
import org.cryptomator.ui.notification.NotificationComponent;
|
||||
import org.cryptomator.ui.preferences.PreferencesComponent;
|
||||
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
|
||||
import org.cryptomator.ui.quit.QuitComponent;
|
||||
@@ -54,6 +55,7 @@ public class FxApplicationWindows {
|
||||
private final LockComponent.Factory lockWorkflowFactory;
|
||||
private final ErrorComponent.Factory errorWindowFactory;
|
||||
private final Lazy<EventViewComponent> eventViewWindow;
|
||||
private final NotificationComponent.Factory notificationWindow;
|
||||
private final ExecutorService executor;
|
||||
private final VaultOptionsComponent.Factory vaultOptionsWindow;
|
||||
private final ShareVaultComponent.Factory shareVaultWindow;
|
||||
@@ -73,6 +75,7 @@ public class FxApplicationWindows {
|
||||
VaultOptionsComponent.Factory vaultOptionsWindow, //
|
||||
ShareVaultComponent.Factory shareVaultWindow, //
|
||||
Lazy<EventViewComponent> eventViewWindow, //
|
||||
NotificationComponent.Factory notificationWindow,
|
||||
ExecutorService executor, //
|
||||
Dialogs dialogs) {
|
||||
this.primaryStage = primaryStage;
|
||||
@@ -85,6 +88,7 @@ public class FxApplicationWindows {
|
||||
this.lockWorkflowFactory = lockWorkflowFactory;
|
||||
this.errorWindowFactory = errorWindowFactory;
|
||||
this.eventViewWindow = eventViewWindow;
|
||||
this.notificationWindow = notificationWindow;
|
||||
this.executor = executor;
|
||||
this.vaultOptionsWindow = vaultOptionsWindow;
|
||||
this.shareVaultWindow = shareVaultWindow;
|
||||
@@ -193,6 +197,10 @@ public class FxApplicationWindows {
|
||||
return CompletableFuture.supplyAsync(() -> eventViewWindow.get().showEventViewerWindow(), Platform::runLater).whenComplete(this::reportErrors);
|
||||
}
|
||||
|
||||
public CompletionStage<Stage> showNotification() {
|
||||
return CompletableFuture.supplyAsync(() -> notificationWindow.create().showNotification(), Platform::runLater).whenComplete(this::reportErrors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the generic error scene in the given window.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.cryptomator.ui.fxapp;
|
||||
|
||||
import org.cryptomator.event.NotificationManager;
|
||||
import org.cryptomator.event.VaultEvent;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Notification manager inside the UI domain.
|
||||
* <p>
|
||||
* Polls the {@link NotificationManager} for pending events every {@value POLL_INTERVAL_SECONDS } seconds and
|
||||
* triggers the notification window display when events are available.
|
||||
* Returns an observable list of events requiring a user notification with {@link #getEventsRequiringNotification()}.
|
||||
*
|
||||
* @see NotificationManager
|
||||
*/
|
||||
@FxApplicationScoped
|
||||
public class FxNotificationManager {
|
||||
|
||||
private static final int POLL_INTERVAL_SECONDS = 1;
|
||||
|
||||
private final NotificationManager notificationManager;
|
||||
private final ScheduledExecutorService scheduler;
|
||||
private final FxApplicationWindows applicationWindows;
|
||||
private final ObservableList<VaultEvent> eventsRequiringNotification;
|
||||
|
||||
@Inject
|
||||
public FxNotificationManager(NotificationManager notificationManager, ScheduledExecutorService scheduler, FxApplicationWindows applicationWindows) {
|
||||
this.notificationManager = notificationManager;
|
||||
this.scheduler = scheduler;
|
||||
this.applicationWindows = applicationWindows;
|
||||
this.eventsRequiringNotification = FXCollections.observableArrayList();
|
||||
}
|
||||
|
||||
public void schedulePollForUpdates() {
|
||||
scheduler.scheduleAtFixedRate(this::checkForPendingNotifications, 0, POLL_INTERVAL_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private void checkForPendingNotifications() {
|
||||
Platform.runLater(() -> {
|
||||
if (notificationManager.addTo(eventsRequiringNotification)) {
|
||||
applicationWindows.showNotification();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public ObservableList<VaultEvent> getEventsRequiringNotification() {
|
||||
return eventsRequiringNotification;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,8 +15,8 @@ import org.cryptomator.ui.addvaultwizard.AddVaultWizardComponent;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.VaultService;
|
||||
import org.cryptomator.ui.dialogs.Dialogs;
|
||||
import org.cryptomator.ui.fxapp.FxFSEventList;
|
||||
import org.cryptomator.ui.fxapp.FxApplicationWindows;
|
||||
import org.cryptomator.ui.fxapp.FxFSEventList;
|
||||
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
|
||||
import org.cryptomator.ui.recoverykey.RecoveryKeyComponent;
|
||||
import org.slf4j.Logger;
|
||||
@@ -139,8 +139,8 @@ public class VaultListController implements FxController {
|
||||
vaultList.setItems(vaults);
|
||||
vaultList.setCellFactory(cellFactory);
|
||||
|
||||
vaultList.prefHeightProperty().bind(
|
||||
vaultList.fixedCellSizeProperty().multiply(Bindings.size(vaultList.getItems()))
|
||||
vaultList.prefHeightProperty().bind( //
|
||||
vaultList.fixedCellSizeProperty().multiply(Bindings.size(vaultList.getItems())) //
|
||||
);
|
||||
|
||||
selectedVault.bind(vaultList.getSelectionModel().selectedItemProperty());
|
||||
@@ -157,11 +157,11 @@ public class VaultListController implements FxController {
|
||||
//unlock vault on double click
|
||||
vaultList.addEventFilter(MouseEvent.MOUSE_CLICKED, click -> {
|
||||
if (click.getClickCount() >= 2) {
|
||||
Optional.ofNullable(selectedVault.get())
|
||||
.filter(Vault::isLocked)
|
||||
Optional.ofNullable(selectedVault.get()) //
|
||||
.filter(Vault::isLocked) //
|
||||
.ifPresent(vault -> appWindows.startUnlockWorkflow(vault, mainWindow));
|
||||
Optional.ofNullable(selectedVault.get())
|
||||
.filter(Vault::isUnlocked)
|
||||
Optional.ofNullable(selectedVault.get()) //
|
||||
.filter(Vault::isUnlocked) //
|
||||
.ifPresent(vaultService::reveal);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.cryptomator.ui.notification;
|
||||
|
||||
import dagger.Lazy;
|
||||
import dagger.Subcomponent;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
@NotificationScoped
|
||||
@Subcomponent(modules = {NotificationModule.class})
|
||||
public interface NotificationComponent {
|
||||
|
||||
@NotificationWindow
|
||||
Stage window();
|
||||
|
||||
@FxmlScene(FxmlFile.NOTIFICATION)
|
||||
Lazy<Scene> scene();
|
||||
|
||||
default Stage showNotification() {
|
||||
var window = window();
|
||||
window.setScene(scene().get());
|
||||
window.sizeToScene();
|
||||
window.show();
|
||||
window.requestFocus();
|
||||
return window;
|
||||
}
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
NotificationComponent create();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package org.cryptomator.ui.notification;
|
||||
|
||||
import org.cryptomator.event.VaultEvent;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.fxapp.FxNotificationManager;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleListProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.value.ObservableStringValue;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.stage.Stage;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
@NotificationScoped
|
||||
public class NotificationController implements FxController {
|
||||
|
||||
private static final String BUG_MSG = "IF YOU SEE THIS MESSAGE, PLEASE CONTACT THE DEVELOPERS OF CRYPTOMATOR ABOUT A BUG IN THE NOTIFICATION DISPLAY";
|
||||
|
||||
private final Stage window;
|
||||
private final SimpleListProperty<VaultEvent> events;
|
||||
private final IntegerProperty selectionIndex;
|
||||
private final ObservableStringValue paging;
|
||||
private final ObjectProperty<VaultEvent> selectedEvent;
|
||||
private final ObservableValue<Boolean> singleEvent;
|
||||
private final StringProperty vaultName;
|
||||
private final StringProperty message;
|
||||
private final StringProperty description;
|
||||
private final StringProperty actionText;
|
||||
private final ExecutorService executorService;
|
||||
|
||||
@Inject
|
||||
public NotificationController(@NotificationWindow Stage window, FxNotificationManager notificationManager, ExecutorService executorService) {
|
||||
this.window = window;
|
||||
this.events = new SimpleListProperty<>(notificationManager.getEventsRequiringNotification());
|
||||
this.selectionIndex = new SimpleIntegerProperty(-1);
|
||||
this.selectedEvent = new SimpleObjectProperty<>();
|
||||
this.singleEvent = events.sizeProperty().map(size -> size.intValue() == 1);
|
||||
this.paging = Bindings.createStringBinding(() -> selectionIndex.get() + 1 + "/" + events.size(), selectionIndex, events);
|
||||
this.vaultName = new SimpleStringProperty();
|
||||
this.message = new SimpleStringProperty();
|
||||
this.description = new SimpleStringProperty();
|
||||
this.actionText = new SimpleStringProperty();
|
||||
this.executorService = executorService;
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
selectionIndex.addListener((_, _, n) -> {
|
||||
if (!events.isEmpty()) {
|
||||
selectedEvent.setValue(events.get(n.intValue()));
|
||||
}
|
||||
});
|
||||
selectedEvent.addListener(this::selectTexts);
|
||||
|
||||
selectionIndex.setValue(0);
|
||||
}
|
||||
|
||||
//TODO: Translations!
|
||||
private void selectTexts(ObservableValue<? extends VaultEvent> observable, VaultEvent oldEvent, VaultEvent newEvent) {
|
||||
if (newEvent == null) {
|
||||
vaultName.set("");
|
||||
message.set("NO CONTENT");
|
||||
description.set(BUG_MSG);
|
||||
actionText.set(null);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (newEvent.actualEvent()) {
|
||||
default -> {
|
||||
vaultName.set(newEvent.v().getDisplayName());
|
||||
message.set("NO CONTENT");
|
||||
description.set(BUG_MSG);
|
||||
actionText.set(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@FXML
|
||||
public void processSelectedEvent() {
|
||||
try {
|
||||
var ev = selectedEvent.get();
|
||||
switch (ev.actualEvent()) {
|
||||
//TODO: executorService.submit(callback.action());
|
||||
default -> {
|
||||
} //normally nothing
|
||||
}
|
||||
} finally {
|
||||
removeSelectedEvent();
|
||||
}
|
||||
}
|
||||
|
||||
private void removeSelectedEvent() {
|
||||
int i = selectionIndex.get();
|
||||
events.remove(i);
|
||||
if (events.isEmpty()) {
|
||||
close(); //no more events
|
||||
} else if (events.size() == i) {
|
||||
selectionIndex.set(i - 1); //triggers event update
|
||||
} else {
|
||||
selectedEvent.set(events.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void previousNotification() {
|
||||
int i = selectionIndex.get();
|
||||
if (i != 0) {
|
||||
selectionIndex.set(i - 1);
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void nextNotification() {
|
||||
int i = selectionIndex.get();
|
||||
if (i != events.size() - 1) {
|
||||
selectionIndex.set(i + 1);
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void close() {
|
||||
events.clear();
|
||||
window.close();
|
||||
}
|
||||
|
||||
|
||||
//FXML bindings
|
||||
public ObservableValue<String> vaultNameProperty() {
|
||||
return vaultName;
|
||||
}
|
||||
|
||||
public String getVaultName() {
|
||||
return vaultName.get();
|
||||
}
|
||||
|
||||
public ObservableValue<String> messageProperty() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message.get();
|
||||
}
|
||||
|
||||
public ObservableValue<String> descriptionProperty() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description.get();
|
||||
}
|
||||
|
||||
public StringProperty actionTextProperty() {
|
||||
return actionText;
|
||||
}
|
||||
|
||||
public String getActionText() {
|
||||
return Objects.requireNonNullElse(actionText.get(), "");
|
||||
}
|
||||
|
||||
public ObservableStringValue pagingProperty() {
|
||||
return paging;
|
||||
}
|
||||
|
||||
public String getPaging() {
|
||||
return paging.get();
|
||||
}
|
||||
|
||||
public ObservableValue<Boolean> singleEventProperty() {
|
||||
return singleEvent;
|
||||
}
|
||||
|
||||
public boolean isSingleEvent() {
|
||||
return singleEvent.getValue();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package org.cryptomator.ui.notification;
|
||||
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.multibindings.IntoMap;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.ui.common.DefaultSceneFactory;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxControllerKey;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.common.StageInitializer;
|
||||
import org.cryptomator.ui.common.SystemBarUtil;
|
||||
|
||||
import javax.inject.Provider;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Screen;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
import java.util.Map;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
@Module
|
||||
abstract class NotificationModule {
|
||||
|
||||
@Provides
|
||||
@NotificationWindow
|
||||
@NotificationScoped
|
||||
static Stage provideStage(StageInitializer initializer) {
|
||||
Stage stage = new Stage(StageStyle.TRANSPARENT);
|
||||
stage.setTitle("Filesystem notification"); //TODO: translate
|
||||
stage.setResizable(false);
|
||||
stage.initModality(Modality.NONE);
|
||||
stage.setAlwaysOnTop(true);
|
||||
initializer.accept(stage);
|
||||
stage.setOnShown(_ -> placeWindow(stage));
|
||||
return stage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Places the notification window on the screen according to some heuristic based on operating system and system bar placement.
|
||||
* <p>
|
||||
* On macOS, the window is placed in the top-right corner of the primary screen, following platform conventions.
|
||||
* On other operating systems, the window placement depends on the location of the system bar:
|
||||
* <ul>
|
||||
* <li>If the system bar is at the top, the window is centered horizontally at the top of the screen.</li>
|
||||
* <li>Otherwise (e.g., system bar at the bottom or elsewhere), the window is placed in the bottom-right corner.</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* The method uses the visual bounds of the primary screen to avoid overlapping with system UI elements.
|
||||
* Assumes the window size has already been set before calling this method.
|
||||
*
|
||||
* @param window the Stage representing the notification window to be placed
|
||||
*/
|
||||
static void placeWindow(Stage window) {
|
||||
var screen = Screen.getPrimary();
|
||||
var vBounds = screen.getVisualBounds();
|
||||
if (SystemUtils.IS_OS_MAC) { //place to right top
|
||||
window.setX(vBounds.getMaxX() - window.getWidth());
|
||||
window.setY(vBounds.getMinY());
|
||||
} else {
|
||||
switch (SystemBarUtil.getPlacementOfSystembar(screen)) {
|
||||
case TOP -> { //place to middle top
|
||||
window.setX(vBounds.getMinX() + (vBounds.getWidth() - window.getWidth()) / 2.0);
|
||||
window.setY(vBounds.getMinY());
|
||||
}
|
||||
default -> { //place to right bottom
|
||||
window.setX(vBounds.getMaxX() - window.getWidth());
|
||||
window.setY(vBounds.getMaxY() - window.getHeight());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// javafx setup
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.NOTIFICATION)
|
||||
@NotificationScoped
|
||||
static Scene provideNotificationScene(@NotificationWindow FxmlLoaderFactory fxmlLoaders) {
|
||||
return fxmlLoaders.createScene(FxmlFile.NOTIFICATION);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@NotificationScoped
|
||||
@NotificationWindow
|
||||
static FxmlLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
|
||||
return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
|
||||
}
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(NotificationController.class)
|
||||
abstract FxController bindNotificationController(NotificationController controller);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.cryptomator.ui.notification;
|
||||
|
||||
import javax.inject.Scope;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
@Scope
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface NotificationScoped {}
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.cryptomator.ui.notification;
|
||||
|
||||
import javax.inject.Qualifier;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
@Qualifier
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
public @interface NotificationWindow {}
|
||||
@@ -113,6 +113,11 @@
|
||||
-fx-font-size: 1.2em;
|
||||
}
|
||||
|
||||
.label-window-title {
|
||||
-fx-font-family: 'Open Sans SemiBold';
|
||||
-fx-font-size: 1.0em;
|
||||
}
|
||||
|
||||
.label-small {
|
||||
-fx-font-size: 0.8em;
|
||||
}
|
||||
@@ -1186,3 +1191,60 @@
|
||||
-fx-background-color: MAIN_BG;
|
||||
-fx-font-size: 1.166667em; /* 14pt - 2 more than the default font */
|
||||
}
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Notification Window
|
||||
* *
|
||||
******************************************************************************/
|
||||
.notification-window {
|
||||
-fx-background-color: MAIN_BG;
|
||||
-fx-background-radius: 8px;
|
||||
-fx-border-radius: 8px;
|
||||
-fx-background-insets: 0;
|
||||
-fx-border-color: MUTED_BG;
|
||||
-fx-border-width: 1px;
|
||||
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.2), 28, 0.35, 0, 6);
|
||||
}
|
||||
|
||||
.notification-window .dialog-header {
|
||||
-fx-alignment: center-left;
|
||||
}
|
||||
|
||||
.notification-window .close-button {
|
||||
-fx-background-color: transparent;
|
||||
-fx-border-color: transparent;
|
||||
-fx-padding: 6 8 6 8;
|
||||
}
|
||||
|
||||
.notification-window .close-button:hover {
|
||||
-fx-background-color: CONTROL_BG_HOVER;
|
||||
-fx-background-radius: 8;
|
||||
}
|
||||
|
||||
.notification-window .close-button:pressed {
|
||||
-fx-background-color: CONTROL_BG_ARMED;
|
||||
}
|
||||
|
||||
.notification-window .action-button:hover {
|
||||
-fx-background-color: CONTROL_BG_HOVER;
|
||||
-fx-border-color: CONTROL_BORDER_FOCUSED;
|
||||
}
|
||||
|
||||
.notification-window .action-button:pressed {
|
||||
-fx-background-color: CONTROL_BG_ARMED;
|
||||
}
|
||||
|
||||
.notification-window .nav-button {
|
||||
-fx-background-color: transparent;
|
||||
-fx-border-color: transparent;
|
||||
-fx-padding: 6 8 6 8;
|
||||
}
|
||||
|
||||
.notification-window .nav-button:hover {
|
||||
-fx-background-color: CONTROL_BG_HOVER;
|
||||
-fx-background-radius: 8;
|
||||
}
|
||||
|
||||
.notification-window .nav-button:pressed {
|
||||
-fx-background-color: CONTROL_BG_ARMED;
|
||||
}
|
||||
|
||||
@@ -113,6 +113,11 @@
|
||||
-fx-font-size: 1.2em;
|
||||
}
|
||||
|
||||
.label-window-title {
|
||||
-fx-font-family: 'Open Sans SemiBold';
|
||||
-fx-font-size: 1.0em;
|
||||
}
|
||||
|
||||
.label-small {
|
||||
-fx-font-size: 0.8em;
|
||||
}
|
||||
@@ -1186,3 +1191,61 @@
|
||||
-fx-background-color: MAIN_BG;
|
||||
-fx-font-size: 1.166667em; /* 14pt - 2 more than the default font */
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Notification Window
|
||||
* *
|
||||
******************************************************************************/
|
||||
.notification-window {
|
||||
-fx-background-color: MAIN_BG;
|
||||
-fx-background-radius: 8px;
|
||||
-fx-border-radius: 8px;
|
||||
-fx-background-insets: 0;
|
||||
-fx-border-color: MUTED_BG;
|
||||
-fx-border-width: 1px;
|
||||
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.2), 28, 0.35, 0, 6);
|
||||
}
|
||||
|
||||
.notification-window .dialog-header {
|
||||
-fx-alignment: center-left;
|
||||
}
|
||||
|
||||
.notification-window .close-button {
|
||||
-fx-background-color: transparent;
|
||||
-fx-border-color: transparent;
|
||||
-fx-padding: 6 8 6 8;
|
||||
}
|
||||
|
||||
.notification-window .close-button:hover {
|
||||
-fx-background-color: CONTROL_BG_HOVER;
|
||||
-fx-background-radius: 8;
|
||||
}
|
||||
|
||||
.notification-window .close-button:pressed {
|
||||
-fx-background-color: CONTROL_BG_ARMED;
|
||||
}
|
||||
|
||||
.notification-window .action-button:hover {
|
||||
-fx-background-color: CONTROL_BG_HOVER;
|
||||
-fx-border-color: CONTROL_BORDER_FOCUSED;
|
||||
}
|
||||
|
||||
.notification-window .action-button:pressed {
|
||||
-fx-background-color: CONTROL_BG_ARMED;
|
||||
}
|
||||
|
||||
.notification-window .nav-button {
|
||||
-fx-background-color: transparent;
|
||||
-fx-border-color: transparent;
|
||||
-fx-padding: 6 8 6 8;
|
||||
}
|
||||
|
||||
.notification-window .nav-button:hover {
|
||||
-fx-background-color: CONTROL_BG_HOVER;
|
||||
-fx-background-radius: 8;
|
||||
}
|
||||
|
||||
.notification-window .nav-button:pressed {
|
||||
-fx-background-color: CONTROL_BG_ARMED;
|
||||
}
|
||||
67
src/main/resources/fxml/notification.fxml
Normal file
67
src/main/resources/fxml/notification.fxml
Normal file
@@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.Separator?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.image.Image?>
|
||||
<?import javafx.scene.image.ImageView?>
|
||||
<?import javafx.scene.layout.BorderPane?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.Region?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<BorderPane xmlns="http://javafx.com/javafx"
|
||||
xmlns:fx="http://javafx.com/fxml"
|
||||
fx:controller="org.cryptomator.ui.notification.NotificationController"
|
||||
prefHeight="200.0" prefWidth="400.0" maxHeight="200.0" maxWidth="400.0"
|
||||
styleClass="notification-window">
|
||||
<padding>
|
||||
<Insets top="12" right="12" bottom="12" left="12"/>
|
||||
</padding>
|
||||
<top>
|
||||
<VBox >
|
||||
<HBox spacing="6" styleClass="dialog-header" alignment="CENTER_LEFT">
|
||||
<ImageView fitHeight="12" preserveRatio="true" cache="true">
|
||||
<Image url="@../img/logo64.png"/>
|
||||
</ImageView>
|
||||
<Label text="Cryptomator" styleClass="label-window-title"/>
|
||||
<Region HBox.hgrow="ALWAYS"/>
|
||||
<HBox styleClass="dialog-header" alignment="CENTER_LEFT" visible="${!controller.singleEvent}">
|
||||
<Button contentDisplay="GRAPHIC_ONLY" styleClass="nav-button" onAction="#previousNotification" accessibleText="%generic.button.previous">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="CHEVRON_LEFT" glyphSize="12"/>
|
||||
</graphic>
|
||||
</Button>
|
||||
<Label text="${controller.paging}" styleClass="label-window-title"/>
|
||||
<Button contentDisplay="GRAPHIC_ONLY" styleClass="nav-button" onAction="#nextNotification" accessibleText="%generic.button.next">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="CHEVRON_RIGHT" glyphSize="12"/>
|
||||
</graphic>
|
||||
</Button>
|
||||
</HBox>
|
||||
<Button contentDisplay="GRAPHIC_ONLY" onAction="#close" styleClass="close-button" accessibleText="%generic.button.close">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="TIMES" glyphSize="12"/>
|
||||
</graphic>
|
||||
</Button>
|
||||
</HBox>
|
||||
<Separator orientation="HORIZONTAL"/>
|
||||
</VBox>
|
||||
</top>
|
||||
|
||||
<center>
|
||||
<VBox>
|
||||
<padding>
|
||||
<Insets top="6"/>
|
||||
</padding>
|
||||
<Label text="${controller.message}" styleClass="label-large" wrapText="true"/>
|
||||
<Label text="${controller.vaultName}" styleClass="label-small" wrapText="true"/>
|
||||
<Region minHeight="6"/>
|
||||
<Label text="${controller.description}" styleClass="label" wrapText="true"/>
|
||||
<Region VBox.vgrow="ALWAYS"/>
|
||||
<Button text="${controller.actionText}" onAction="#processSelectedEvent"
|
||||
visible="${!controller.actionText.empty}" managed="${!controller.actionText.empty}"/>
|
||||
</VBox>
|
||||
</center>
|
||||
</BorderPane>
|
||||
@@ -13,6 +13,7 @@ generic.button.close=Close
|
||||
generic.button.copy=Copy
|
||||
generic.button.copied=Copied!
|
||||
generic.button.done=Done
|
||||
generic.button.previous=Previous
|
||||
generic.button.next=Next
|
||||
generic.button.print=Print
|
||||
generic.button.remove=Remove
|
||||
|
||||
Reference in New Issue
Block a user