Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
This commit is contained in:
Armin Schrenk
2025-12-09 17:49:07 +01:00
parent 5b9e70da33
commit 2521cd2bfb
6 changed files with 65 additions and 44 deletions

View File

@@ -23,19 +23,20 @@ import java.util.concurrent.atomic.AtomicBoolean;
* <li>it is not added within the last {@value DEBOUNCE_THRESHOLD_SECONDS} seconds</li>
* </ul>
*
* @see org.cryptomator.ui.fxapp.FxNotificationManager
*/
@Singleton
public class NotificationManager {
private static final int DEBOUNCE_THRESHOLD_SECONDS = 5;
Cache<FSEventBucket, FilesystemEvent> eventCache;
ConcurrentLinkedQueue<VaultEvent> eventsRequiringNotification;
private final Cache<FSEventBucket, FilesystemEvent> debounceCache;
private final ConcurrentLinkedQueue<VaultEvent> 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<VaultEvent> target) {
public boolean addTo(List<VaultEvent> target) {
synchronized (this) {
var result = target.addAll(eventsRequiringNotification);
eventsRequiringNotification.clear();
var result = target.addAll(pendingEvents);
pendingEvents.clear();
return result;
}
}

View File

@@ -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;

View File

@@ -11,10 +11,18 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* Sends notifications
* Notification manager inside the UI domain.
* <p>
* Polls the {@link NotificationManager} for pending events every {@value POLL_INTERVAL_SECONDS } seconds and
* triggers the notification window display when events are available.
* Returns an observable list of events requiring a user notification with {@link #getEventsRequiringNotification()}.
*
* @see NotificationManager
*/
@FxApplicationScoped
public class 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<VaultEvent> 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();
}
});

View File

@@ -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<VaultEvent> notificationsProp;
private final SimpleListProperty<VaultEvent> events;
private final IntegerProperty selectionIndex;
private final ObservableStringValue paging;
private final ObjectProperty<VaultEvent> 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<? extends VaultEvent> observable, VaultEvent oldEvent, VaultEvent newEvent) {
private void selectTexts(ObservableValue<? extends VaultEvent> 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();
}

View File

@@ -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.
* <p>
* On macOS, the window is placed in the top-right corner of the primary screen, following platform conventions.
* On other operating systems, the window placement depends on the location of the system bar:
* <ul>
* <li>If the system bar is at the top, the window is centered horizontally at the top of the screen.</li>
* <li>Otherwise (e.g., system bar at the bottom or elsewhere), the window is placed in the bottom-right corner.</li>
* </ul>
* <p>
* The method uses the visual bounds of the primary screen to avoid overlapping with system UI elements.
* Assumes the window size has already been set before calling this method.
*
* @param window the Stage representing the notification window to be placed
*/
static void placeWindow(Stage window) {
var screen = Screen.getPrimary();
var vBounds = screen.getVisualBounds();