Armin Schrenk 5 месяцев назад
Родитель
Сommit
d67085d57d

+ 1 - 1
src/main/java/org/cryptomator/common/vaults/Vault.java

@@ -259,7 +259,7 @@ public class Vault {
 
 
 	private void consumeVaultEvent(FilesystemEvent e) {
-		fileSystemEventRegistry.enque(this, e);
+		fileSystemEventRegistry.enqueue(this, e);
 	}
 
 	// ******************************************************************************

+ 0 - 29
src/main/java/org/cryptomator/event/FileSystemEventBucket.java

@@ -1,29 +0,0 @@
-package org.cryptomator.event;
-
-import org.cryptomator.common.vaults.Vault;
-import org.cryptomator.cryptofs.event.FilesystemEvent;
-
-public record FileSystemEventBucket(Vault vault, FilesystemEvent mostRecent, int count) implements Comparable<FileSystemEventBucket> {
-
-	@Override
-	public boolean equals(Object other) {
-		if (other instanceof FileSystemEventBucket(Vault v2, FilesystemEvent e2, _)) {
-			return vault.equals(v2) && mostRecent.getClass().equals(e2.getClass());
-		}
-		return false;
-	}
-
-	@Override
-	public int compareTo(FileSystemEventBucket other) {
-		var timeResult = mostRecent.getTimestamp().compareTo(other.mostRecent().getTimestamp());
-		if (timeResult != 0) {
-			return timeResult;
-		}
-		var vaultIdResult = vault.getId().compareTo(other.vault.getId());
-		if (vaultIdResult != 0) {
-			return vaultIdResult;
-		}
-		return this.mostRecent.getClass().getName().compareTo(other.mostRecent.getClass().getName());
-	}
-
-}

+ 50 - 153
src/main/java/org/cryptomator/event/FileSystemEventRegistry.java

@@ -7,61 +7,51 @@ 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.jetbrains.annotations.NotNull;
 
 import javax.inject.Inject;
 import javax.inject.Singleton;
-import javafx.application.Platform;
-import javafx.collections.FXCollections;
-import javafx.collections.MapChangeListener;
-import javafx.collections.ObservableMap;
 import java.nio.file.Path;
-import java.util.List;
-import java.util.TreeSet;
+import java.time.Instant;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Objects;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ConcurrentSkipListMap;
 import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 
+/**
+ * Angenommen:
+ * Datenstruktur die
+ * 1. Thread-Safe ist
+ * ??
+ *
+ *
+ *
+ *
+ * 1. Wenn ein Set verwendet wird, dann können wir nach Timestamp sortieren, aber wir können einen Eintrag nur durch entfernen und hinzufügen updaten
+ * 2. Wenn eine Map verwendet wird, dann können wir Einträge updaten. Aber
+ *
+ */
+//TODO: Rename to aggregator
+//TODO: lru cache
 @Singleton
 public class FileSystemEventRegistry {
 
 	private static final int MAX_MAP_SIZE = 400;
 
-	public record Key(Vault vault, Path idPath, Class<? extends FilesystemEvent> c) {}
+	public record Key(Vault vault, Path idPath, Class<? extends FilesystemEvent> c) {};
 
 	public record Value(FilesystemEvent mostRecentEvent, int count) {}
 
-	/**
-	 * Queue of elements to be inserted into the map
-	 */
-	private final ConcurrentMap<Key, Value> queue;
-	/**
-	 * Least-recently-used cache.
-	 */
-	private final TreeSet<Key> lruCache;
-	/**
-	 * Actual map
-	 */
-	private final ObservableMap<Key, Value> map;
-
-	private final ScheduledExecutorService scheduler;
-
-	private final AtomicBoolean queueHasElements;
+	private final ConcurrentHashMap<Key, Value> map;
+	private final AtomicBoolean hasUpdates;
 
 	@Inject
 	public FileSystemEventRegistry(ScheduledExecutorService scheduledExecutorService) {
-		this.queue = new ConcurrentHashMap<>();
-		this.lruCache = new TreeSet<>(this::compareKeys);
-		this.map = FXCollections.observableHashMap();
-		this.scheduler = scheduledExecutorService;
-		this.queueHasElements = new AtomicBoolean(false);
-		scheduler.scheduleWithFixedDelay(() -> {
-			if (queueHasElements.get()) {
-				flush();
-			}
-		}, 1000, 1000, TimeUnit.MILLISECONDS);
+		this.map = new ConcurrentHashMap<>();
+		this.hasUpdates = new AtomicBoolean(false);
 	}
 
 	/**
@@ -70,9 +60,9 @@ public class FileSystemEventRegistry {
 	 * @param v Vault where the event occurred
 	 * @param e Actual {@link FilesystemEvent}
 	 */
-	public synchronized void enque(Vault v, FilesystemEvent e) {
+	public synchronized void enqueue(Vault v, FilesystemEvent e) {
 		var key = computeKey(v, e);
-		queue.compute(key, (k, val) -> {
+		map.compute(key, (k, val) -> {
 			if (val == null) {
 				return new Value(e, 1);
 			} else {
@@ -80,45 +70,21 @@ public class FileSystemEventRegistry {
 			}
 		});
 
-		queueHasElements.set(true);
-	}
-
-
-	/**
-	 * Lists all entries in this map as {@link FileSystemEventBucket}. The list is sorted ascending by the timestamp of event occurral (and more if it is the same timestamp).
-	 * Must be executed on the JavaFX application thread
-	 *
-	 * @return a list of vault events, mainly sorted ascending by the event timestamp
-	 * @implNote Method is not synchronized, because it is only executed if executed by JavaFX application thread
-	 */
-	public List<FileSystemEventBucket> listAll() {
-		if (!Platform.isFxApplicationThread()) {
-			throw new IllegalStateException("Listing map entries must be performed on JavaFX application thread");
-		}
-		return lruCache.stream().map(key -> {
-			var value = map.get(key);
-			return new FileSystemEventBucket(key.vault(), value.mostRecentEvent(), value.count());
-		}).toList();
+		hasUpdates.set(true);
 	}
 
 	/**
 	 * Removes an event from the map.
 	 * <p>
 	 * To identify the event, a similar event (in the sense of map key) is given.
-	 * Must be executed on the JavaFX application thread
 	 *
-	 * @param v Vault where the event occurred
-	 * @param similar A similar {@link FilesystemEvent} (same class, same idPath)
 	 * @return the removed {@link Value}
 	 * @implNote Method is not synchronized, because it is only executed if executed by JavaFX application thread
 	 */
-	public Value remove(Vault v, FilesystemEvent similar) {
-		if (!Platform.isFxApplicationThread()) {
-			throw new IllegalStateException("Map removal must be performed on JavaFX application thread");
-		}
-		var key = computeKey(v, similar);
-		lruCache.remove(key);
-		return map.remove(key);
+	public Value remove(Key key) {
+		var result = map.remove(key);
+		hasUpdates.set(true);
+		return result;
 	}
 
 	/**
@@ -129,93 +95,8 @@ public class FileSystemEventRegistry {
 	 * @implNote Method is not synchronized, because it is only executed if executed by JavaFX application thread
 	 */
 	public void clear() {
-		if (!Platform.isFxApplicationThread()) {
-			throw new IllegalStateException("Map removal must be performed on JavaFX application thread");
-		}
-		lruCache.clear();
 		map.clear();
-	}
-
-	/**
-	 * Flushes all changes from the queue into the map
-	 */
-	private synchronized void flush() {
-		//Lock queue
-		var latch = new CountDownLatch(1);
-		Platform.runLater(() -> {
-			queue.forEach(this::updateMap);
-			queue.clear();
-			latch.countDown();
-		});
-		try {
-			latch.await();
-			queueHasElements.set(false);
-		} catch (InterruptedException e) {
-			Thread.currentThread().interrupt();
-		}
-	}
-
-	/**
-	 * Updates a single map entry
-	 *
-	 * @param k Key of the entry to update
-	 * @param v Value of the entry to update
-	 * @implNote Method is not synchronized, because it is only called on the (one-and-only) JavaFX application thread
-	 */
-	private void updateMap(Key k, Value v) {
-		var entry = map.get(k);
-		if (entry == null) {
-			if (map.size() == MAX_MAP_SIZE) {
-				var toRemove = lruCache.first();
-				lruCache.remove(toRemove);
-				map.remove(toRemove);
-			}
-			map.put(k, v);
-			lruCache.add(k);
-		} else {
-			lruCache.remove(k);
-			map.put(k, new Value(v.mostRecentEvent, entry.count + v.count));
-			lruCache.add(k); //correct, because cache-sorting uses the map in comparsionMethod
-		}
-	}
-
-	/* Observability */
-
-	public void addListener(MapChangeListener<? super Key, ? super Value> mapChangeListener) {
-		map.addListener(mapChangeListener);
-	}
-
-	public void removeListener(MapChangeListener<? super Key, ? super Value> mapChangeListener) {
-		map.removeListener(mapChangeListener);
-	}
-
-
-	/* Internal stuff */
-
-	/**
-	 * Comparsion method for the lru cache. During comparsion the map is accessed.
-	 * First the entries are compared by the event timestamp, then vaultId, then identifying path and lastly by class name.
-	 *
-	 * @param left a {@link Key} object
-	 * @param right another {@link Key} object, compared to {@code left}
-	 * @return a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater than the second.
-	 */
-	private int compareKeys(Key left, Key right) {
-		var t1 = map.get(left).mostRecentEvent.getTimestamp();
-		var t2 = map.get(right).mostRecentEvent.getTimestamp();
-		var timeComparsion = t1.compareTo(t2);
-		if (timeComparsion != 0) {
-			return timeComparsion;
-		}
-		var vaultIdComparsion = left.vault.getId().compareTo(right.vault.getId());
-		if (vaultIdComparsion != 0) {
-			return vaultIdComparsion;
-		}
-		var pathComparsion = left.idPath.compareTo(right.idPath);
-		if (pathComparsion != 0) {
-			return pathComparsion;
-		}
-		return left.c.getName().compareTo(right.c.getName());
+		hasUpdates.set(true);
 	}
 
 	/**
@@ -236,4 +117,20 @@ public class FileSystemEventRegistry {
 		return new Key(v, p, event.getClass());
 	}
 
+	public boolean hasUpdates() {
+		return hasUpdates.get();
+	}
+
+	/**
+	 * Clones the map entries into a set.
+	 * <p>
+	 * The set is first cleared, then all map entries are added in one bulk operation. Sets the updates status of the event registry.
+	 *
+	 * @param targetCollection
+	 */
+	public void cloneTo(Collection<Map.Entry<Key, Value>> targetCollection) {
+		targetCollection.clear();
+		targetCollection.addAll(map.entrySet());
+		hasUpdates.set(false);
+	}
 }

+ 18 - 16
src/main/java/org/cryptomator/ui/eventview/EventListCellController.java

@@ -9,7 +9,6 @@ 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.event.FileSystemEventBucket;
 import org.cryptomator.integrations.revealpath.RevealFailedException;
 import org.cryptomator.integrations.revealpath.RevealPathService;
 import org.cryptomator.ui.common.FxController;
@@ -41,6 +40,7 @@ import java.nio.file.Path;
 import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
 import java.time.format.FormatStyle;
+import java.util.Map;
 import java.util.Optional;
 import java.util.ResourceBundle;
 import java.util.function.Function;
@@ -55,7 +55,7 @@ public class EventListCellController implements FxController {
 	@Nullable
 	private final RevealPathService revealService;
 	private final ResourceBundle resourceBundle;
-	private final ObjectProperty<FileSystemEventBucket> event;
+	private final ObjectProperty<Map.Entry<FileSystemEventRegistry.Key, FileSystemEventRegistry.Value>> eventEntry;
 	private final StringProperty eventMessage;
 	private final StringProperty eventDescription;
 	private final ObjectProperty<FontAwesome5Icon> eventIcon;
@@ -81,14 +81,14 @@ public class EventListCellController implements FxController {
 		this.fileSystemEventRegistry = fileSystemEventRegistry;
 		this.revealService = revealService.orElseGet(() -> null);
 		this.resourceBundle = resourceBundle;
-		this.event = new SimpleObjectProperty<>(null);
+		this.eventEntry = new SimpleObjectProperty<>(null);
 		this.eventMessage = new SimpleStringProperty();
 		this.eventDescription = new SimpleStringProperty();
 		this.eventIcon = new SimpleObjectProperty<>();
-		this.eventCount = ObservableUtil.mapWithDefault(event, e -> e.count() == 1? "" : "("+ e.count() +")", "");
-		this.vaultUnlocked = ObservableUtil.mapWithDefault(event.flatMap(e -> e.vault().unlockedProperty()), Function.identity(), false);
-		this.readableTime = ObservableUtil.mapWithDefault(event, e -> LOCAL_TIME_FORMATTER.format(e.mostRecent().getTimestamp()), "");
-		this.readableDate = ObservableUtil.mapWithDefault(event, e -> LOCAL_DATE_FORMATTER.format(e.mostRecent().getTimestamp()), "");
+		this.eventCount = ObservableUtil.mapWithDefault(eventEntry, e -> e.getValue().count() == 1? "" : "("+ e.getValue().count() +")", "");
+		this.vaultUnlocked = ObservableUtil.mapWithDefault(eventEntry.flatMap(e -> e.getKey().vault().unlockedProperty()), Function.identity(), false);
+		this.readableTime = ObservableUtil.mapWithDefault(eventEntry, e -> LOCAL_TIME_FORMATTER.format(e.getValue().mostRecentEvent().getTimestamp()), "");
+		this.readableDate = ObservableUtil.mapWithDefault(eventEntry, e -> LOCAL_DATE_FORMATTER.format(e.getValue().mostRecentEvent().getTimestamp()), "");
 		this.message = Bindings.createStringBinding(this::selectMessage, vaultUnlocked, eventMessage);
 		this.description = Bindings.createStringBinding(this::selectDescription, vaultUnlocked, eventDescription);
 		this.icon = Bindings.createObjectBinding(this::selectIcon, vaultUnlocked, eventIcon);
@@ -108,13 +108,13 @@ public class EventListCellController implements FxController {
 		return vaultUnlocked.getValue() && (eventActionsMenu.isShowing() || root.isHover());
 	}
 
-	public void setEvent(@NotNull FileSystemEventBucket item) {
-		event.set(item);
+	public void setEventEntry(@NotNull Map.Entry<FileSystemEventRegistry.Key, FileSystemEventRegistry.Value> item) {
+		eventEntry.set(item);
 		eventActionsMenu.hide();
 		eventActionsMenu.getItems().clear();
-		eventTooltip.setText(item.vault().getDisplayName());
-		addAction("generic.action.dismiss", () -> fileSystemEventRegistry.remove(item.vault(),item.mostRecent()));
-		switch (item.mostRecent()) {
+		eventTooltip.setText(item.getKey().vault().getDisplayName());
+		addAction("generic.action.dismiss", () -> fileSystemEventRegistry.remove(item.getKey()));
+		switch (item.getValue().mostRecentEvent()) {
 			case ConflictResolvedEvent fse -> this.adjustToConflictResolvedEvent(fse);
 			case ConflictResolutionFailedEvent fse -> this.adjustToConflictEvent(fse);
 			case DecryptionFailedEvent fse -> this.adjustToDecryptionFailedEvent(fse);
@@ -209,16 +209,18 @@ public class EventListCellController implements FxController {
 	private String selectDescription() {
 		if (vaultUnlocked.getValue()) {
 			return eventDescription.getValue();
-		} else {
-			var e = event.getValue();
+		} else if (eventEntry.getValue() != null) {
+			var e = eventEntry.getValue().getKey();
 			return resourceBundle.getString("eventView.entry.vaultLocked.description").formatted(e != null ? e.vault().getDisplayName() : "");
+		} else {
+			return "";
 		}
 	}
 
 
 	@FXML
 	public void toggleEventActionsMenu() {
-		var e = event.get();
+		var e = eventEntry.get();
 		if (e != null) {
 			if (eventActionsMenu.isShowing()) {
 				eventActionsMenu.hide();
@@ -232,7 +234,7 @@ public class EventListCellController implements FxController {
 		if (!(p instanceof CryptoPath)) {
 			throw new IllegalArgumentException("Path " + p + " is not a vault path");
 		}
-		var v = event.getValue().vault();
+		var v = eventEntry.getValue().getKey().vault();
 		if (!v.isUnlocked()) {
 			return Path.of(System.getProperty("user.home"));
 		}

+ 7 - 6
src/main/java/org/cryptomator/ui/eventview/EventListCellFactory.java

@@ -1,6 +1,6 @@
 package org.cryptomator.ui.eventview;
 
-import org.cryptomator.event.FileSystemEventBucket;
+import org.cryptomator.event.FileSystemEventRegistry;
 import org.cryptomator.ui.common.FxmlLoaderFactory;
 
 import javax.inject.Inject;
@@ -12,9 +12,10 @@ import javafx.scene.control.ListView;
 import javafx.util.Callback;
 import java.io.IOException;
 import java.io.UncheckedIOException;
+import java.util.Map;
 
 @EventViewScoped
-public class EventListCellFactory implements Callback<ListView<FileSystemEventBucket>, ListCell<FileSystemEventBucket>> {
+public class EventListCellFactory implements Callback<ListView<Map.Entry<FileSystemEventRegistry.Key, FileSystemEventRegistry.Value>>, ListCell<Map.Entry<FileSystemEventRegistry.Key, FileSystemEventRegistry.Value>>> {
 
 	private static final String FXML_PATH = "/fxml/eventview_cell.fxml";
 
@@ -27,7 +28,7 @@ public class EventListCellFactory implements Callback<ListView<FileSystemEventBu
 
 
 	@Override
-	public ListCell<FileSystemEventBucket> call(ListView<FileSystemEventBucket> eventListView) {
+	public ListCell<Map.Entry<FileSystemEventRegistry.Key, FileSystemEventRegistry.Value>> call(ListView<Map.Entry<FileSystemEventRegistry.Key, FileSystemEventRegistry.Value>> eventListView) {
 		try {
 			FXMLLoader fxmlLoader = fxmlLoaders.load(FXML_PATH);
 			return new Cell(fxmlLoader.getRoot(), fxmlLoader.getController());
@@ -36,7 +37,7 @@ public class EventListCellFactory implements Callback<ListView<FileSystemEventBu
 		}
 	}
 
-	private static class Cell extends ListCell<FileSystemEventBucket> {
+	private static class Cell extends ListCell<Map.Entry<FileSystemEventRegistry.Key, FileSystemEventRegistry.Value>> {
 
 		private final Parent root;
 		private final EventListCellController controller;
@@ -47,7 +48,7 @@ public class EventListCellFactory implements Callback<ListView<FileSystemEventBu
 		}
 
 		@Override
-		protected void updateItem(FileSystemEventBucket item, boolean empty) {
+		protected void updateItem(Map.Entry<FileSystemEventRegistry.Key, FileSystemEventRegistry.Value> item, boolean empty) {
 			super.updateItem(item, empty);
 
 			if (empty || item == null) {
@@ -57,7 +58,7 @@ public class EventListCellFactory implements Callback<ListView<FileSystemEventBu
 				this.getStyleClass().addLast("list-cell");
 				setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
 				setGraphic(root);
-				controller.setEvent(item);
+				controller.setEventEntry(item);
 			}
 		}
 	}

+ 37 - 28
src/main/java/org/cryptomator/ui/eventview/EventViewController.java

@@ -1,15 +1,14 @@
 package org.cryptomator.ui.eventview;
 
 import org.cryptomator.common.vaults.Vault;
-import org.cryptomator.event.FileSystemEventBucket;
 import org.cryptomator.event.FileSystemEventRegistry;
 import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.fxapp.EventsUpdateCheck;
 
 import javax.inject.Inject;
 import javafx.beans.value.ObservableValue;
 import javafx.collections.FXCollections;
 import javafx.collections.ListChangeListener;
-import javafx.collections.MapChangeListener;
 import javafx.collections.ObservableList;
 import javafx.collections.transformation.FilteredList;
 import javafx.collections.transformation.SortedList;
@@ -17,17 +16,15 @@ import javafx.fxml.FXML;
 import javafx.scene.control.ChoiceBox;
 import javafx.scene.control.ListView;
 import javafx.util.StringConverter;
-import java.util.Comparator;
+import java.util.Map;
 import java.util.ResourceBundle;
 
 @EventViewScoped
 public class EventViewController implements FxController {
 
-	private final FileSystemEventRegistry fileSystemEventRegistry;
-	private final ObservableList<FileSystemEventBucket> eventList;
-	private final FilteredList<FileSystemEventBucket> filteredEventList;
+	private final FilteredList<Map.Entry<FileSystemEventRegistry.Key, FileSystemEventRegistry.Value>> filteredEventList;
 	private final ObservableList<Vault> vaults;
-	private final SortedList<FileSystemEventBucket> reversedEventList;
+	private final SortedList<Map.Entry<FileSystemEventRegistry.Key, FileSystemEventRegistry.Value>> sortedEventList;
 	private final ObservableList<Vault> choiceBoxEntries;
 	private final ResourceBundle resourceBundle;
 	private final EventListCellFactory cellFactory;
@@ -35,20 +32,44 @@ public class EventViewController implements FxController {
 	@FXML
 	ChoiceBox<Vault> vaultFilterChoiceBox;
 	@FXML
-	ListView<FileSystemEventBucket> eventListView;
+	ListView<Map.Entry<FileSystemEventRegistry.Key, FileSystemEventRegistry.Value>> eventListView;
 
 	@Inject
-	public EventViewController(FileSystemEventRegistry fileSystemEventRegistry, ObservableList<Vault> vaults, ResourceBundle resourceBundle, EventListCellFactory cellFactory) {
-		this.fileSystemEventRegistry = fileSystemEventRegistry;
-		this.eventList = FXCollections.observableArrayList();
-		this.filteredEventList = eventList.filtered(_ -> true);
+	public EventViewController(EventsUpdateCheck eventsUpdateCheck, ObservableList<Vault> vaults, ResourceBundle resourceBundle, EventListCellFactory cellFactory) {
+		this.filteredEventList = eventsUpdateCheck.getList().filtered(_ -> true);
 		this.vaults = vaults;
-		this.reversedEventList = new SortedList<>(filteredEventList, Comparator.reverseOrder());
+		this.sortedEventList = new SortedList<>(filteredEventList, this::compareBuckets);
 		this.choiceBoxEntries = FXCollections.observableArrayList();
 		this.resourceBundle = resourceBundle;
 		this.cellFactory = cellFactory;
 	}
 
+	/**
+	 * Comparsion method for the lru cache. During comparsion the map is accessed.
+	 * First the entries are compared by the event timestamp, then vaultId, then identifying path and lastly by class name.
+	 *
+	 * @param left a {@link FileSystemEventRegistry.Key} object
+	 * @param right another {@link FileSystemEventRegistry.Key} object, compared to {@code left}
+	 * @return a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater than the second.
+	 */
+	private int compareBuckets(Map.Entry<FileSystemEventRegistry.Key, FileSystemEventRegistry.Value> left, Map.Entry<FileSystemEventRegistry.Key, FileSystemEventRegistry.Value> right) {
+		var t1 = left.getValue().mostRecentEvent().getTimestamp();
+		var t2 = right.getValue().mostRecentEvent().getTimestamp();
+		var timeComparison = t1.compareTo(t2);
+		if (timeComparison != 0) {
+			return -timeComparison; //we need the reverse timesorting
+		}
+		var vaultIdComparsion = left.getKey().vault().getId().compareTo(right.getKey().vault().getId());
+		if (vaultIdComparsion != 0) {
+			return vaultIdComparsion;
+		}
+		var pathComparsion = left.getKey().idPath().compareTo(right.getKey().idPath());
+		if (pathComparsion != 0) {
+			return pathComparsion;
+		}
+		return left.getKey().c().getName().compareTo(right.getKey().c().getName());
+	}
+
 	@FXML
 	public void initialize() {
 		choiceBoxEntries.add(null);
@@ -60,37 +81,25 @@ public class EventViewController implements FxController {
 			}
 		});
 
-		eventList.addAll(fileSystemEventRegistry.listAll());
-		fileSystemEventRegistry.addListener((MapChangeListener<? super FileSystemEventRegistry.Key, ? super FileSystemEventRegistry.Value>) this::updateList);
 		eventListView.setCellFactory(cellFactory);
-		eventListView.setItems(reversedEventList);
+		eventListView.setItems(sortedEventList);
 
 		vaultFilterChoiceBox.setItems(choiceBoxEntries);
 		vaultFilterChoiceBox.valueProperty().addListener(this::applyVaultFilter);
 		vaultFilterChoiceBox.setConverter(new VaultConverter(resourceBundle));
 	}
 
-	private void updateList(MapChangeListener.Change<? extends FileSystemEventRegistry.Key, ? extends FileSystemEventRegistry.Value> change) {
-		var vault = change.getKey().vault();
-		if (change.wasRemoved()) {
-			eventList.remove(new FileSystemEventBucket(vault, change.getValueRemoved().mostRecentEvent(), 0));
-		}
-		if (change.wasAdded()) {
-			eventList.addLast(new FileSystemEventBucket(vault, change.getValueAdded().mostRecentEvent(), change.getValueAdded().count()));
-		}
-	}
-
 	private void applyVaultFilter(ObservableValue<? extends Vault> v, Vault oldV, Vault newV) {
 		if (newV == null) {
 			filteredEventList.setPredicate(_ -> true);
 		} else {
-			filteredEventList.setPredicate(e -> e.vault().equals(newV));
+			filteredEventList.setPredicate(e -> e.getKey().vault().equals(newV));
 		}
 	}
 
 	@FXML
 	void clearEvents() {
-		fileSystemEventRegistry.clear();
+		//fileSystemEventRegistry.clear();
 	}
 
 	private static class VaultConverter extends StringConverter<Vault> {

+ 62 - 0
src/main/java/org/cryptomator/ui/fxapp/EventsUpdateCheck.java

@@ -0,0 +1,62 @@
+package org.cryptomator.ui.fxapp;
+
+import org.cryptomator.event.FileSystemEventRegistry;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+import javafx.application.Platform;
+import javafx.beans.property.BooleanProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+@FxApplicationScoped
+public class EventsUpdateCheck {
+
+	private final ObservableList<Map.Entry<FileSystemEventRegistry.Key, FileSystemEventRegistry.Value>> events;
+	private final FileSystemEventRegistry eventRegistry;
+	private final ScheduledFuture<?> scheduledTask;
+	private final BooleanProperty unreadEvents;
+
+	@Inject
+	public EventsUpdateCheck(FileSystemEventRegistry eventRegistry, ScheduledExecutorService scheduler, @Named("unreadEventsAvailable") BooleanProperty unreadEvents) {
+		this.events = FXCollections.observableArrayList();
+		this.eventRegistry = eventRegistry;
+		this.unreadEvents = unreadEvents;
+		this.scheduledTask = scheduler.scheduleWithFixedDelay(() -> {
+			if (eventRegistry.hasUpdates()) {
+				flush();
+			}
+		}, 1000, 1000, TimeUnit.MILLISECONDS);
+		//TODO: allow the task to be canceled (to enable ui actions, e.g. when the contextMenu is open, the list should not be updated
+	}
+
+	public ObservableList<Map.Entry<FileSystemEventRegistry.Key, FileSystemEventRegistry.Value>> getList() {
+		return events;
+	}
+
+	/**
+	 * Clones the registry into the observable list
+	 */
+	private void flush() {
+		var latch = new CountDownLatch(1);
+		Platform.runLater(() -> {
+			eventRegistry.cloneTo(events);
+			unreadEvents.setValue(true);
+			latch.countDown();
+		});
+		try {
+			latch.await();
+		} catch (InterruptedException e) {
+			Thread.currentThread().interrupt();
+		}
+	}
+
+
+
+}

+ 1 - 0
src/main/java/org/cryptomator/ui/fxapp/FxApplication.java

@@ -43,6 +43,7 @@ public class FxApplication {
 		this.autoUnlocker = autoUnlocker;
 	}
 
+	//TODO: eventUpdater muss hier starten
 	public void start() {
 		LOG.trace("FxApplication.start()");
 		applicationStyle.initialize();

+ 0 - 8
src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java

@@ -69,7 +69,6 @@ public class VaultListController implements FxController {
 	private final VaultListCellFactory cellFactory;
 	private final AddVaultWizardComponent.Builder addVaultWizard;
 	private final BooleanBinding emptyVaultList;
-	private final FileSystemEventRegistry fileSystemEventRegistry;
 	private final BooleanProperty newEventsPresent;
 	private final VaultListManager vaultListManager;
 	private final BooleanProperty draggingVaultOver = new SimpleBooleanProperty();
@@ -97,7 +96,6 @@ public class VaultListController implements FxController {
 						FxApplicationWindows appWindows, //
 						Settings settings, //
 						Dialogs dialogs, //
-						FileSystemEventRegistry fileSystemEventRegistry, //
 						@Named("unreadEventsAvailable") BooleanProperty unreadEvents) {
 		this.mainWindow = mainWindow;
 		this.vaults = vaults;
@@ -111,13 +109,7 @@ public class VaultListController implements FxController {
 		this.dialogs = dialogs;
 
 		this.emptyVaultList = Bindings.isEmpty(vaults);
-		this.fileSystemEventRegistry = fileSystemEventRegistry;
 		this.newEventsPresent = unreadEvents;
-		fileSystemEventRegistry.addListener((MapChangeListener<? super FileSystemEventRegistry.Key, ? super FileSystemEventRegistry.Value>) change -> {
-			if (change.wasAdded()) {
-				newEventsPresent.setValue(true);
-			}
-		});
 
 		selectedVault.addListener(this::selectedVaultDidChange);
 		cellSize = settings.compactMode.map(compact -> compact ? 30.0 : 60.0);