diff --git a/src/main/java/org/cryptomator/common/EventMap.java b/src/main/java/org/cryptomator/common/EventMap.java deleted file mode 100644 index 2e8dbf035..000000000 --- a/src/main/java/org/cryptomator/common/EventMap.java +++ /dev/null @@ -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 - *

- * Use {@link EventMap#put(VaultEvent)} to add an element and {@link EventMap#remove(VaultEvent)} to remove it. - *

- * 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 { - - private static final int MAX_SIZE = 300; - - public record EventKey(Path ciphertextPath, Class c) {} - - private final ObservableMap delegate; - - @Inject - public EventMap() { - delegate = FXCollections.observableHashMap(); - } - - @Override - public void addListener(MapChangeListener mapChangeListener) { - delegate.addListener(mapChangeListener); - } - - @Override - public void removeListener(MapChangeListener 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 m) { - delegate.putAll(m); - } - - @Override - public void clear() { - delegate.clear(); - } - - @Override - public @NotNull Set keySet() { - return delegate.keySet(); - } - - @Override - public @NotNull Collection values() { - return delegate.values(); - } - - @Override - public @NotNull Set> 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()); - } -} diff --git a/src/main/java/org/cryptomator/event/NotificationManager.java b/src/main/java/org/cryptomator/event/NotificationManager.java new file mode 100644 index 000000000..36b9eb655 --- /dev/null +++ b/src/main/java/org/cryptomator/event/NotificationManager.java @@ -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. + *

+ * 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 + *

  • + * + * + *
  • + * + */ +@Singleton +public class NotificationManager { + + private static final int DEBOUNCE_THRESHOLD_SECONDS = 5; + + Cache eventCache; + Queue 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 target) { + synchronized (this) { + var result = target.addAll(eventsRequiringNotification); + eventsRequiringNotification.clear(); + return result; + } + } + + +} diff --git a/src/main/java/org/cryptomator/ipc/HandleNotificationCallbackMessage.java b/src/main/java/org/cryptomator/ipc/HandleNotificationCallbackMessage.java deleted file mode 100644 index cfd8cfdb2..000000000 --- a/src/main/java/org/cryptomator/ipc/HandleNotificationCallbackMessage.java +++ /dev/null @@ -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); - } -} diff --git a/src/main/java/org/cryptomator/ipc/IpcMessage.java b/src/main/java/org/cryptomator/ipc/IpcMessage.java index 928f79c69..7f76da295 100644 --- a/src/main/java/org/cryptomator/ipc/IpcMessage.java +++ b/src/main/java/org/cryptomator/ipc/IpcMessage.java @@ -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 decoder; diff --git a/src/main/java/org/cryptomator/ipc/IpcMessageListener.java b/src/main/java/org/cryptomator/ipc/IpcMessageListener.java index bab4c2e8e..756305cbe 100644 --- a/src/main/java/org/cryptomator/ipc/IpcMessageListener.java +++ b/src/main/java/org/cryptomator/ipc/IpcMessageListener.java @@ -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 args); - void handleNotificationCallback(String content); - } diff --git a/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java b/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java index 348b3a26b..b479a7f14 100644 --- a/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java +++ b/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java @@ -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"), // diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java index ebc7cb635..ae9e60388 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java @@ -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 showNotification(Runnable action) { - return CompletableFuture.supplyAsync(() -> notificationWindow.create(message, description, action).showNotification(), Platform::runLater).whenComplete(this::reportErrors); + public CompletionStage showNotification() { + return CompletableFuture.supplyAsync(() -> notificationWindow.create().showNotification(), Platform::runLater).whenComplete(this::reportErrors); } /** diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxNotificationRadar.java b/src/main/java/org/cryptomator/ui/fxapp/FxNotificationRadar.java index 66bd08488..d7aadb103 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxNotificationRadar.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxNotificationRadar.java @@ -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 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 getEventsRequiringNotification() { + return eventsRequiringNotification; + } } diff --git a/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java b/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java index dfdc8f338..5a375e9b1 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java @@ -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 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) { diff --git a/src/main/java/org/cryptomator/ui/notification/NotificationComponent.java b/src/main/java/org/cryptomator/ui/notification/NotificationComponent.java index 7a9d2c0b9..3ebcdf4ab 100644 --- a/src/main/java/org/cryptomator/ui/notification/NotificationComponent.java +++ b/src/main/java/org/cryptomator/ui/notification/NotificationComponent.java @@ -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(); + + 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(); } } diff --git a/src/main/java/org/cryptomator/ui/notification/NotificationController.java b/src/main/java/org/cryptomator/ui/notification/NotificationController.java index c35a754f6..9e60ceaa7 100644 --- a/src/main/java/org/cryptomator/ui/notification/NotificationController.java +++ b/src/main/java/org/cryptomator/ui/notification/NotificationController.java @@ -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 notificationsProp; + private final IntegerProperty selectionIndex; + private final ObjectProperty 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 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 messageProperty() { return message; } - public String getDescription() { + public String getMessage() { + return message.get(); + } + + public ObservableValue descriptionProperty() { return description; } - public String getButtonText() { - return callback == null? "" : callback.label(); + public String getDescription() { + return description.get(); + } + + public ObservableValue 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(); + } + } diff --git a/src/main/java/org/cryptomator/ui/notification/NotificationModule.java b/src/main/java/org/cryptomator/ui/notification/NotificationModule.java index 3540e7636..68508f8b6 100644 --- a/src/main/java/org/cryptomator/ui/notification/NotificationModule.java +++ b/src/main/java/org/cryptomator/ui/notification/NotificationModule.java @@ -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, Provider> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) { return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle); } diff --git a/src/main/resources/css/light_theme.css b/src/main/resources/css/light_theme.css index 39e2892ac..a2e88c29a 100644 --- a/src/main/resources/css/light_theme.css +++ b/src/main/resources/css/light_theme.css @@ -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; +} \ No newline at end of file diff --git a/src/main/resources/fxml/notification.fxml b/src/main/resources/fxml/notification.fxml index f291d0c79..5f24eb25b 100644 --- a/src/main/resources/fxml/notification.fxml +++ b/src/main/resources/fxml/notification.fxml @@ -1,39 +1,53 @@ + + - - - - - - + + - + - - + + + - - - - - - + maxHeight="200.0" maxWidth="400.0" styleClass="notification-window" spacing="6"> + + + + + - - - - + +