mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-14 08:41:28 +00:00
Pimp notification dialog
Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
75
src/main/java/org/cryptomator/event/NotificationManager.java
Normal file
75
src/main/java/org/cryptomator/event/NotificationManager.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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"), //
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user