Pimp notification dialog

Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
This commit is contained in:
Armin Schrenk
2025-12-04 15:13:31 +01:00
parent b4528f825e
commit 9e631a78f2
14 changed files with 330 additions and 273 deletions

View File

@@ -1,160 +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.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;
};
return new EventKey(p, e.getClass());
}
}

View File

@@ -0,0 +1,75 @@
package org.cryptomator.event;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.cryptomator.cryptofs.event.BrokenFileNodeEvent;
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.ArrayDeque;
import java.util.List;
import java.util.Queue;
/**
* Manager for notifications.
* <p>
* To add (filesystem) events, use method {@link #tryAddEvent(FilesystemEvent)}. If the input event is eligible, it is added to an internal queue.
* An event is eligible, if
* <li>
* <ul>the event should trigger a notification and</ul>
* <ul>it is not added within the last {@value DEBOUNCE_THRESHOLD_SECONDS} seconds</ul>
* </li>
*
*/
@Singleton
public class NotificationManager {
private static final int DEBOUNCE_THRESHOLD_SECONDS = 5;
Cache<Path, FilesystemEvent> eventCache;
Queue<FilesystemEvent> eventsRequiringNotification;
@Inject
public NotificationManager() {
eventCache = Caffeine.newBuilder().expireAfterWrite(Duration.ofSeconds(DEBOUNCE_THRESHOLD_SECONDS)).build();
eventsRequiringNotification = new ArrayDeque<>();
}
public boolean tryAddEvent(FilesystemEvent e) {
var notRecentlyAdded = switch (e) {
case BrokenFileNodeEvent bfne -> isRecent(bfne.ciphertextPath(), bfne);
default -> false;
};
if(notRecentlyAdded) {
synchronized (this) {
eventsRequiringNotification.add(e);
}
}
return notRecentlyAdded;
}
boolean isRecent(Path key, FilesystemEvent e) {
var cacheElement = eventCache.get(key, _ -> e);
return cacheElement == e;
}
/**
* Clones all events requiring a notification to the target list and clears afterward the notification manager queue
* @param target list the queue is cloned to
* @return {@code true}, if elements were copied
*/
public boolean cloneTo(List<FilesystemEvent> target) {
synchronized (this) {
var result = target.addAll(eventsRequiringNotification);
eventsRequiringNotification.clear();
return result;
}
}
}

View File

@@ -1,22 +0,0 @@
package org.cryptomator.ipc;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
record HandleNotificationCallbackMessage(String content) implements IpcMessage {
@Override
public MessageType getMessageType() {
return MessageType.HANDLE_NOTIFICATION_CALLBACK;
}
@Override
public ByteBuffer encodePayload() {
return ByteBuffer.wrap(content.getBytes(StandardCharsets.UTF_8));
}
public static IpcMessage decode(ByteBuffer byteBuffer) {
var content = StandardCharsets.UTF_8.decode(byteBuffer).toString();
return new HandleNotificationCallbackMessage(content);
}
}

View File

@@ -10,12 +10,11 @@ import java.nio.channels.WritableByteChannel;
import java.util.function.Function;
//TODO can the enum be removed?
sealed interface IpcMessage permits HandleLaunchArgsMessage, RevealRunningAppMessage, HandleNotificationCallbackMessage {
sealed interface IpcMessage permits HandleLaunchArgsMessage, RevealRunningAppMessage {
enum MessageType {
REVEAL_RUNNING_APP(RevealRunningAppMessage::decode),
HANDLE_LAUNCH_ARGS(HandleLaunchArgsMessage::decode),
HANDLE_NOTIFICATION_CALLBACK(HandleNotificationCallbackMessage::decode);
HANDLE_LAUNCH_ARGS(HandleLaunchArgsMessage::decode);
private final Function<ByteBuffer, IpcMessage> decoder;

View File

@@ -8,7 +8,6 @@ public interface IpcMessageListener {
switch (message) {
case RevealRunningAppMessage m -> revealRunningApp(); // TODO: rename to _ with JEP 443
case HandleLaunchArgsMessage m -> handleLaunchArgs(m.args());
case HandleNotificationCallbackMessage m -> handleNotificationCallback(m.content());
}
}
@@ -16,6 +15,4 @@ public interface IpcMessageListener {
void handleLaunchArgs(List<String> args);
void handleNotificationCallback(String content);
}

View File

@@ -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"), //

View File

@@ -4,7 +4,6 @@ import com.google.common.base.Preconditions;
import dagger.Lazy;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.integrations.notify.NotifyService;
import org.cryptomator.integrations.tray.TrayIntegrationProvider;
import org.cryptomator.ui.dialogs.Dialogs;
import org.cryptomator.ui.dialogs.SimpleDialog;
@@ -198,8 +197,8 @@ public class FxApplicationWindows {
return CompletableFuture.supplyAsync(() -> eventViewWindow.get().showEventViewerWindow(), Platform::runLater).whenComplete(this::reportErrors);
}
public CompletionStage<Stage> showNotification(Runnable action) {
return CompletableFuture.supplyAsync(() -> notificationWindow.create(message, description, action).showNotification(), Platform::runLater).whenComplete(this::reportErrors);
public CompletionStage<Stage> showNotification() {
return CompletableFuture.supplyAsync(() -> notificationWindow.create().showNotification(), Platform::runLater).whenComplete(this::reportErrors);
}
/**

View File

@@ -1,21 +1,52 @@
package org.cryptomator.ui.fxapp;
import org.cryptomator.cryptofs.event.FilesystemEvent;
import org.cryptomator.event.NotificationManager;
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;
/**
* Scans the event list for events requiring a notification
* Sends notifications
*/
@FxApplicationScoped
public class FxNotificationRadar {
private final FxFSEventList eventList;
private final NotificationManager notificationManager;
private final ScheduledExecutorService scheduler;
private final FxApplicationWindows applicationWindows;
private final ObservableList<FilesystemEvent> eventsRequiringNotification;
@Inject
FxNotificationRadar(FxFSEventList eventList) {
this.eventList = eventList;
eventList.getObservableList()
public FxNotificationRadar(NotificationManager notificationManager, ScheduledExecutorService scheduler, FxApplicationWindows applicationWindows) {
this.notificationManager = notificationManager;
this.scheduler = scheduler;
this.applicationWindows = applicationWindows;
this.eventsRequiringNotification = FXCollections.observableArrayList();
}
public void schedulePollForUpdates() {
scheduler.schedule(this::checkForPendingNotifications, 1000, TimeUnit.MILLISECONDS);
}
/**
* TODO
*/
private void checkForPendingNotifications() {
Platform.runLater(() -> {
if (notificationManager.cloneTo(eventsRequiringNotification)) {
applicationWindows.showNotification();
}
});
}
public ObservableList<FilesystemEvent> getEventsRequiringNotification() {
return eventsRequiringNotification;
}
}

View File

@@ -10,15 +10,15 @@ import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptofs.DirStructure;
import org.cryptomator.cryptofs.common.Constants;
import org.cryptomator.cryptofs.event.BrokenDirFileEvent;
import org.cryptomator.integrations.mount.MountService;
import org.cryptomator.integrations.notify.NotifyService;
import org.cryptomator.integrations.notify.NotifyServiceException;
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.fxapp.FxNotificationRadar;
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
import org.cryptomator.ui.recoverykey.RecoveryKeyComponent;
import org.slf4j.Logger;
@@ -35,7 +35,6 @@ import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.geometry.Side;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.ListView;
@@ -82,6 +81,7 @@ public class VaultListController implements FxController {
private final AddVaultWizardComponent.Builder addVaultWizard;
private final BooleanBinding emptyVaultList;
private final BooleanProperty unreadEvents;
private final FxNotificationRadar notificationRadar;
private final VaultListManager vaultListManager;
private final BooleanProperty draggingVaultOver = new SimpleBooleanProperty();
private final ResourceBundle resourceBundle;
@@ -115,7 +115,9 @@ public class VaultListController implements FxController {
RecoveryKeyComponent.Factory recoveryKeyWindow, //
VaultComponent.Factory vaultComponentFactory, //
List<MountService> mountServices, //
FxFSEventList fxFSEventList) {
FxFSEventList fxFSEventList,
FxNotificationRadar notificationRadar
) {
this.mainWindow = mainWindow;
this.vaults = vaults;
this.selectedVault = selectedVault;
@@ -132,6 +134,7 @@ public class VaultListController implements FxController {
this.emptyVaultList = Bindings.isEmpty(vaults);
this.unreadEvents = fxFSEventList.unreadEventsProperty();
this.notificationRadar = notificationRadar;
selectedVault.addListener(this::selectedVaultDidChange);
cellSize = settings.compactMode.map(compact -> compact ? 30.0 : 60.0);
@@ -207,6 +210,8 @@ public class VaultListController implements FxController {
@FXML
private void toggleMenu() {
notificationRadar.getEventsRequiringNotification().add(new BrokenDirFileEvent(Path.of("C:\\Your\\Momma\\Does\\Things")));
appWindows.showNotification();
/*
if (addVaultContextMenu.isShowing()) {
addVaultContextMenu.hide();
@@ -214,15 +219,6 @@ public class VaultListController implements FxController {
addVaultContextMenu.show(addVaultButton, Side.BOTTOM, 0.0, 0.0);
}
*/
NotifyService.loadAll().findFirst().ifPresent(
s -> {
try {
s.sendNotification("Hello", "Lindsay");
} catch (NotifyServiceException e) {
throw new RuntimeException(e);
}
}
);
}
private void deselect(MouseEvent released) {

View File

@@ -1,8 +1,11 @@
package org.cryptomator.ui.notification;
import dagger.BindsInstance;
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
@@ -10,17 +13,23 @@ import javafx.stage.Stage;
public interface NotificationComponent {
@NotificationWindow
Stage notificationWindow();
Stage window();
default Stage showNotification(){
var window = notificationWindow();
@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(@BindsInstance Runnable action);
NotificationComponent create();
}
}

View File

@@ -1,56 +1,146 @@
package org.cryptomator.ui.notification;
import org.cryptomator.common.Nullable;
import org.cryptomator.integrations.notify.NotifyService;
import org.cryptomator.cryptofs.event.FilesystemEvent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.fxapp.FxNotificationRadar;
import javax.inject.Inject;
import javax.inject.Named;
import javafx.event.ActionEvent;
import javafx.beans.binding.BooleanBinding;
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.ObservableBooleanValue;
import javafx.beans.value.ObservableIntegerValue;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.stage.Stage;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
@NotificationScoped
public class NotificationController implements FxController {
private final String message;
private final String description;
private final NotifyAction callback;
private static final String LOREM_IPSUM = """
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam""";
private final Stage window;
private final SimpleListProperty<FilesystemEvent> notificationsProp;
private final IntegerProperty selectionIndex;
private final ObjectProperty<FilesystemEvent> selectedEvent;
private final StringProperty message;
private final StringProperty description;
private final StringProperty actionText;
private final ExecutorService executorService;
@FXML
Button button;
@Inject
public NotificationController(@Named("Message") String message, @Named("Description") String description, @Nullable NotifyAction callback, ExecutorService executorService) {
this.message = message;
this.description = description;
this.callback = callback;
public NotificationController(@NotificationWindow Stage window, FxNotificationRadar notificationRadar, ExecutorService executorService) {
this.window = window;
this.notificationsProp = new SimpleListProperty<>(notificationRadar.getEventsRequiringNotification());
this.selectionIndex = new SimpleIntegerProperty(0);
this.selectedEvent = new SimpleObjectProperty<>();
selectionIndex.addListener((obs, o, n) -> selectedEvent.setValue(notificationsProp.get(n.intValue())));
selectedEvent.addListener(this::adjustTexts);
this.message = new SimpleStringProperty();
this.description = new SimpleStringProperty();
this.actionText = new SimpleStringProperty();
this.executorService = executorService;
}
@FXML
public void initialize() {
selectedEvent.setValue(notificationsProp.get(selectionIndex.get()));
}
private void adjustTexts(ObservableValue<? extends FilesystemEvent> observable, FilesystemEvent oldEvent, FilesystemEvent newEvent) {
switch (newEvent) {
default -> {
message.set("BABA");
description.set(LOREM_IPSUM);
actionText.set("ACTION");
}
}
}
@FXML
public void handleButtonAction() {
var ev = selectedEvent.get();
switch (ev) {
default -> {
} //normally nothing
}
//executorService.submit(callback.action());
int i = selectionIndex.get();
notificationsProp.remove(i); //remove processed event
if (notificationsProp.isEmpty()) {
close(); //no more events
} else if (notificationsProp.size() == i) {
i = i - 1;
selectionIndex.set(i); //triggers event update
} else {
selectedEvent.set(notificationsProp.get(i));
}
}
@FXML
public void close() {
notificationsProp.clear();
window.close();
}
//FXML bindings
public String getMessage() {
public ObservableValue<String> messageProperty() {
return message;
}
public String getDescription() {
public String getMessage() {
return message.get();
}
public ObservableValue<String> descriptionProperty() {
return description;
}
public String getButtonText() {
return callback == null? "" : callback.label();
public String getDescription() {
return description.get();
}
public ObservableValue<String> actionTextProperty() {
return actionText;
}
public String getActionText() {
return Objects.requireNonNullElse(actionText.get(), "");
}
public ObservableBooleanValue buttonVisibleProperty() {
return actionText.isEmpty();
}
public boolean isButtonVisible() {
return callback != null;
return actionText.get() != null;
}
@FXML
public void handleButtonAction(ActionEvent actionEvent) {
executorService.submit(callback.action());
public ObservableIntegerValue selectionIndexProperty() {
return selectionIndex;
}
public int getSelectionIndex() {
return selectionIndex.get();
}
public ObservableIntegerValue numberOfNotificationsProperty() {
return notificationsProp.sizeProperty();
}
public int getNumberOfNotifications() {
return notificationsProp.size();
}
}

View File

@@ -4,6 +4,7 @@ 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;
@@ -11,11 +12,11 @@ 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.quit.QuitForcedController;
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;
@@ -27,29 +28,46 @@ abstract class NotificationModule {
@Provides
@NotificationWindow
@NotificationScoped
static Stage provideStage(StageInitializer initializer, @FxmlScene(FxmlFile.NOTIFICATION) Scene notificationScene) {
Stage stage = new Stage(StageStyle.UTILITY);
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.setScene(notificationScene);
stage.sizeToScene();
stage.setOnShown(_ -> placeWindow(stage) );
return stage;
}
static void placeWindow(Stage window) {
if(SystemUtils.IS_OS_WINDOWS) { //place to right bottom
var screenBounds = Screen.getPrimary().getVisualBounds();
window.setX(screenBounds.getMaxX() - window.getWidth());
window.setY(screenBounds.getMaxY() - window.getHeight());
} else if(SystemUtils.IS_OS_MAC) { //place to right top
var screenBounds = Screen.getPrimary().getVisualBounds(); //TODO: TEST
window.setX(screenBounds.getMaxX() - window.getWidth());
window.setY(screenBounds.getMinY() - window.getHeight());
} else { //place to middle top
//GNOME; KDE; etc...
var screenBounds = Screen.getPrimary().getVisualBounds(); //TODO: TEST
window.setX(screenBounds.getMaxX() / 2.0);
window.setY(screenBounds.getMinY() - window.getHeight());
}
}
// javafx setup
@Provides
@FxmlScene(FxmlFile.NOTIFICATION)
@NotificationScoped
static Scene provideNotificationScene(FxmlLoaderFactory fxmlLoaders) {
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);
}

View File

@@ -1186,3 +1186,12 @@
-fx-background-color: MAIN_BG;
-fx-font-size: 1.166667em; /* 14pt - 2 more than the default font */
}
/**
Notification Window
**/
.notification-window {
-fx-background-radius: 18 18 18 18;
-fx-border-radius: 18 18 18 18;
-fx-background-color: MAIN_BG;
}

View File

@@ -1,39 +1,53 @@
<?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.Label?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.Group?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.shape.Circle?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import javafx.scene.control.Separator?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.ButtonBar?>
<HBox xmlns="http://javafx.com/javafx"
<?import javafx.scene.text.TextFlow?>
<?import com.sun.javafx.scene.control.IntegerField?>
<?import org.cryptomator.ui.controls.FormattedLabel?>
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.cryptomator.ui.notification.NotificationController"
prefHeight="100.0" prefWidth="200.0">
<children>
<StackPane>
<padding>
<Insets topRightBottomLeft="6"/>
</padding>
<ImageView fitHeight="64" preserveRatio="true" cache="true">
maxHeight="200.0" maxWidth="400.0" styleClass="notification-window" spacing="6">
<padding>
<Insets topRightBottomLeft="6"/>
</padding>
<HBox spacing="6">
<ImageView fitHeight="16" preserveRatio="true" cache="true">
<Image url="@../img/logo64.png"/>
</ImageView>
</StackPane>
<VBox HBox.hgrow="ALWAYS">
<Label text="${controller.message}" styleClass="label-large" wrapText="true">
<padding>
<Insets bottom="6" top="6"/>
</padding>
</Label>
<Label text="${controller.description}" wrapText="true"/>
<Button text="${controller.buttonText}" ButtonBar.buttonData="CANCEL_CLOSE" onAction="#handleButtonAction" visible="${controller.buttonVisible}" managed="${controller.buttonVisible}"/>
</VBox>
</children>
</HBox>
<Label text="Vault Notification" />
<Region HBox.hgrow="ALWAYS"/>
<Button contentDisplay="GRAPHIC_ONLY" onAction="#close">
<graphic>
<FontAwesome5IconView glyph="TIMES" glyphSize="16"/>
</graphic>
</Button>
</HBox>
<Separator orientation="HORIZONTAL" />
<Label text="${controller.message}" styleClass="label-large" wrapText="true"/>
<Label text="${controller.description}" wrapText="true"/>
<HBox>
<Button text="${controller.actionText}" onAction="#handleButtonAction" visible="${controller.buttonVisible}" managed="${controller.buttonVisible}"/>
<Region HBox.hgrow="ALWAYS"/>
<Button contentDisplay="GRAPHIC_ONLY">
<graphic>
<FontAwesome5IconView glyph="CHEVRON_LEFT" glyphSize="6"/>
</graphic>
</Button>
<FormattedLabel format="a%d/%d" arg1="${controller.selectionIndex}" arg2="${controller.numberOfNotifications}"/>
<Button contentDisplay="GRAPHIC_ONLY">
<graphic>
<FontAwesome5IconView glyph="CHEVRON_RIGHT" glyphSize="6"/>
</graphic>
</Button>
</HBox>
</VBox>