diff --git a/src/main/java/org/cryptomator/event/NotificationManager.java b/src/main/java/org/cryptomator/event/NotificationManager.java index e9e8e99d7..fe6020f7d 100644 --- a/src/main/java/org/cryptomator/event/NotificationManager.java +++ b/src/main/java/org/cryptomator/event/NotificationManager.java @@ -23,19 +23,20 @@ import java.util.concurrent.atomic.AtomicBoolean; *
  • it is not added within the last {@value DEBOUNCE_THRESHOLD_SECONDS} seconds
  • * * + * @see org.cryptomator.ui.fxapp.FxNotificationManager */ @Singleton public class NotificationManager { private static final int DEBOUNCE_THRESHOLD_SECONDS = 5; - Cache eventCache; - ConcurrentLinkedQueue eventsRequiringNotification; + private final Cache debounceCache; + private final ConcurrentLinkedQueue pendingEvents; @Inject public NotificationManager() { - eventCache = Caffeine.newBuilder().expireAfterWrite(Duration.ofSeconds(DEBOUNCE_THRESHOLD_SECONDS)).build(); - eventsRequiringNotification = new ConcurrentLinkedQueue<>(); + debounceCache = Caffeine.newBuilder().expireAfterWrite(Duration.ofSeconds(DEBOUNCE_THRESHOLD_SECONDS)).build(); + pendingEvents = new ConcurrentLinkedQueue<>(); } /** @@ -55,9 +56,9 @@ public class NotificationManager { boolean addEvent(Vault v, Path keyPath, FilesystemEvent e) { var key = new FSEventBucket(v, keyPath, e.getClass()); var isAdded = new AtomicBoolean(false); - eventCache.asMap().computeIfAbsent(key, _ -> { + debounceCache.asMap().computeIfAbsent(key, _ -> { synchronized (this) { - eventsRequiringNotification.add(new VaultEvent(v, e)); + pendingEvents.add(new VaultEvent(v, e)); isAdded.set(true); } return e; @@ -66,15 +67,15 @@ public class NotificationManager { } /** - * Clones all events requiring a notification to the target list and clears afterward the notification manager queue + * Adds all events to the target list and clears afterward the pending-event-queue * - * @param target list the queue is cloned to + * @param target list where the filesystem events are copied to * @return {@code true}, if elements were copied */ - public boolean cloneTo(List target) { + public boolean addTo(List target) { synchronized (this) { - var result = target.addAll(eventsRequiringNotification); - eventsRequiringNotification.clear(); + var result = target.addAll(pendingEvents); + pendingEvents.clear(); return result; } } diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java index 35b18bdc4..f33436a86 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java @@ -30,7 +30,7 @@ public class FxApplication { private final FxApplicationTerminator applicationTerminator; private final AutoUnlocker autoUnlocker; private final FxFSEventList fxFSEventList; - private final FxNotificationRadar notificationRadar; + private final FxNotificationManager notificationRadar; @Inject FxApplication(@Named("startupTime") long startupTime, // @@ -43,7 +43,7 @@ public class FxApplication { FxApplicationTerminator applicationTerminator, // AutoUnlocker autoUnlocker, // FxFSEventList fxFSEventList, // - FxNotificationRadar notificationRadar) { + FxNotificationManager notificationRadar) { this.startupTime = startupTime; this.environment = environment; this.settings = settings; diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxNotificationRadar.java b/src/main/java/org/cryptomator/ui/fxapp/FxNotificationManager.java similarity index 61% rename from src/main/java/org/cryptomator/ui/fxapp/FxNotificationRadar.java rename to src/main/java/org/cryptomator/ui/fxapp/FxNotificationManager.java index e63ebbf4f..7be86fc50 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxNotificationRadar.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxNotificationManager.java @@ -11,10 +11,18 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; /** - * Sends notifications + * Notification manager inside the UI domain. + *

    + * 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 FxNotificationRadar { +public class FxNotificationManager { + + private static final int POLL_INTERVAL_SECONDS = 1; private final NotificationManager notificationManager; private final ScheduledExecutorService scheduler; @@ -22,7 +30,7 @@ public class FxNotificationRadar { private final ObservableList eventsRequiringNotification; @Inject - public FxNotificationRadar(NotificationManager notificationManager, ScheduledExecutorService scheduler, FxApplicationWindows applicationWindows) { + public FxNotificationManager(NotificationManager notificationManager, ScheduledExecutorService scheduler, FxApplicationWindows applicationWindows) { this.notificationManager = notificationManager; this.scheduler = scheduler; this.applicationWindows = applicationWindows; @@ -30,15 +38,12 @@ public class FxNotificationRadar { } public void schedulePollForUpdates() { - scheduler.scheduleAtFixedRate(this::checkForPendingNotifications, 0, 1000, TimeUnit.MILLISECONDS); + scheduler.scheduleAtFixedRate(this::checkForPendingNotifications, 0, POLL_INTERVAL_SECONDS, TimeUnit.SECONDS); } - /** - * TODO - */ private void checkForPendingNotifications() { Platform.runLater(() -> { - if (notificationManager.cloneTo(eventsRequiringNotification)) { + if (notificationManager.addTo(eventsRequiringNotification)) { applicationWindows.showNotification(); } }); diff --git a/src/main/java/org/cryptomator/ui/notification/NotificationController.java b/src/main/java/org/cryptomator/ui/notification/NotificationController.java index 7bdf52541..47a807070 100644 --- a/src/main/java/org/cryptomator/ui/notification/NotificationController.java +++ b/src/main/java/org/cryptomator/ui/notification/NotificationController.java @@ -2,7 +2,7 @@ package org.cryptomator.ui.notification; import org.cryptomator.event.VaultEvent; import org.cryptomator.ui.common.FxController; -import org.cryptomator.ui.fxapp.FxNotificationRadar; +import org.cryptomator.ui.fxapp.FxNotificationManager; import javax.inject.Inject; import javafx.beans.binding.Bindings; @@ -27,7 +27,7 @@ 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 notificationsProp; + private final SimpleListProperty events; private final IntegerProperty selectionIndex; private final ObservableStringValue paging; private final ObjectProperty selectedEvent; @@ -37,18 +37,12 @@ public class NotificationController implements FxController { private final ExecutorService executorService; @Inject - public NotificationController(@NotificationWindow Stage window, FxNotificationRadar notificationRadar, ExecutorService executorService) { + public NotificationController(@NotificationWindow Stage window, FxNotificationManager notificationManager, ExecutorService executorService) { this.window = window; - this.notificationsProp = new SimpleListProperty<>(notificationRadar.getEventsRequiringNotification()); - this.selectionIndex = new SimpleIntegerProperty(0); + this.events = new SimpleListProperty<>(notificationManager.getEventsRequiringNotification()); + this.selectionIndex = new SimpleIntegerProperty(-1); this.selectedEvent = new SimpleObjectProperty<>(); - selectionIndex.addListener((_, _, n) -> { - if (!notificationsProp.isEmpty()) { - selectedEvent.setValue(notificationsProp.get(n.intValue())); - } - }); - selectedEvent.addListener(this::adjustTexts); - this.paging = Bindings.createStringBinding(() -> selectionIndex.get() + 1 + "/" + notificationsProp.size(), selectionIndex, notificationsProp); + this.paging = Bindings.createStringBinding(() -> selectionIndex.get() + 1 + "/" + events.size(), selectionIndex, events); this.message = new SimpleStringProperty(); this.description = new SimpleStringProperty(); this.actionText = new SimpleStringProperty(); @@ -57,11 +51,18 @@ public class NotificationController implements FxController { @FXML public void initialize() { - selectedEvent.setValue(notificationsProp.get(selectionIndex.get())); + selectionIndex.addListener((_, _, n) -> { + if (!events.isEmpty()) { + selectedEvent.setValue(events.get(n.intValue())); + } + }); + selectedEvent.addListener(this::selectTexts); + + selectionIndex.setValue(0); } //TODO: Translations! - private void adjustTexts(ObservableValue observable, VaultEvent oldEvent, VaultEvent newEvent) { + private void selectTexts(ObservableValue observable, VaultEvent oldEvent, VaultEvent newEvent) { if (newEvent == null) { message.set("NO CONTENT"); description.set(BUG_MSG); @@ -80,7 +81,7 @@ public class NotificationController implements FxController { @FXML - public void handleButtonAction() { + public void processEvent() { try { var ev = selectedEvent.get(); switch (ev.actualEvent()) { @@ -91,14 +92,14 @@ public class NotificationController implements FxController { } finally { //remove processed event int i = selectionIndex.get(); - notificationsProp.remove(i); - if (notificationsProp.isEmpty()) { + events.remove(i); + if (events.isEmpty()) { close(); //no more events - } else if (notificationsProp.size() == i) { + } else if (events.size() == i) { i = i - 1; selectionIndex.set(i); //triggers event update } else { - selectedEvent.set(notificationsProp.get(i)); + selectedEvent.set(events.get(i)); } } } @@ -114,14 +115,14 @@ public class NotificationController implements FxController { @FXML public void nextNotification() { int i = selectionIndex.get(); - if (i != notificationsProp.size() - 1) { + if (i != events.size() - 1) { selectionIndex.set(i + 1); } } @FXML public void close() { - notificationsProp.clear(); + events.clear(); window.close(); } diff --git a/src/main/java/org/cryptomator/ui/notification/NotificationModule.java b/src/main/java/org/cryptomator/ui/notification/NotificationModule.java index c6e8d6dd1..64895a40c 100644 --- a/src/main/java/org/cryptomator/ui/notification/NotificationModule.java +++ b/src/main/java/org/cryptomator/ui/notification/NotificationModule.java @@ -40,7 +40,21 @@ abstract class NotificationModule { return stage; } - //TODO: TEST + /** + * Places the notification window on the screen according to some heuristic based on operating system and system bar placement. + *

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

      + *
    • If the system bar is at the top, the window is centered horizontally at the top of the screen.
    • + *
    • Otherwise (e.g., system bar at the bottom or elsewhere), the window is placed in the bottom-right corner.
    • + *
    + *

    + * 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(); diff --git a/src/main/resources/fxml/notification.fxml b/src/main/resources/fxml/notification.fxml index cd65d3579..646441e56 100644 --- a/src/main/resources/fxml/notification.fxml +++ b/src/main/resources/fxml/notification.fxml @@ -42,7 +42,7 @@ -