Compare commits

...

27 Commits

Author SHA1 Message Date
Armin Schrenk
c0f3facab8 correct typos 2025-03-21 15:45:04 +01:00
Armin Schrenk
b9512c9e93 cleanup 2025-03-21 15:37:04 +01:00
Armin Schrenk
dd724220f8 enable clear() again 2025-03-21 15:31:58 +01:00
Armin Schrenk
94410f1839 more renaming 2025-03-21 15:24:40 +01:00
Armin Schrenk
cc8aa00326 Renamed classes 2025-03-21 15:03:31 +01:00
Armin Schrenk
e8e2fcb0b3 rename EventRegistry to Aggregator 2025-03-21 14:51:53 +01:00
Armin Schrenk
151b35355d no size restriction/lru needed 2025-03-21 14:49:19 +01:00
Armin Schrenk
37d5353f77 run the eventUpdate from app start 2025-03-21 13:46:20 +01:00
Armin Schrenk
7b6953445f remove comments 2025-03-21 13:45:59 +01:00
Armin Schrenk
fcd3db63ce use a preemptive update status to not miss updates 2025-03-21 13:38:00 +01:00
Armin Schrenk
d67085d57d doodles 2025-03-20 16:41:30 +01:00
Armin Schrenk
1f60d9f5e8 remove notification, if eventView is focused 2025-03-19 15:13:51 +01:00
Armin Schrenk
5378769467 adjust update-indicator 2025-03-19 15:12:40 +01:00
Armin Schrenk
913ed5e109 removed unused class 2025-03-19 14:53:24 +01:00
Armin Schrenk
f4cfe19fdc more cleanup 2025-03-19 12:41:45 +01:00
Armin Schrenk
a001dfd8a8 add equals to FileSystemEventBucket to ensure correct removal in collections 2025-03-19 12:30:23 +01:00
Armin Schrenk
893a4bcae9 the great rename 2025-03-19 12:25:26 +01:00
Armin Schrenk
e61fb74367 removed unused classes 2025-03-19 12:06:02 +01:00
Armin Schrenk
fbbbc1cb40 cleanup 2025-03-19 11:58:04 +01:00
Armin Schrenk
f27f3c46c1 decouple eventMap insertion from event happening (to not spam FX thread) 2025-03-19 11:55:29 +01:00
Armin Schrenk
83b557b6be removed dead code 2025-03-18 18:02:55 +01:00
Armin Schrenk
32a0df06d0 move eventMap to event package 2025-03-18 18:02:46 +01:00
Armin Schrenk
5350e07f62 renamed VaultEventsMap.Key parameters for clarity 2025-03-18 17:57:15 +01:00
Armin Schrenk
cc5c46743b Refactor EventMap:
* renamed to VaultEventsMap
* split between boilerplate and buissness logic (ObservableMapDecorator)
* add LRU cache
* fixed VaultEvent compareTo method
2025-03-18 17:55:32 +01:00
Armin Schrenk
bc7f3fe7db more renaming 2025-03-18 12:17:39 +01:00
Armin Schrenk
750ca3f39c also key the eventMap for vault 2025-03-18 12:13:25 +01:00
Armin Schrenk
2bbffc3623 rename eventMap to make purpose clearer 2025-03-18 12:10:44 +01:00
21 changed files with 292 additions and 328 deletions

View File

@@ -59,7 +59,6 @@ open module org.cryptomator.desktop {
uses org.cryptomator.common.locationpresets.LocationPresetsProvider;
uses SSLContextProvider;
uses org.cryptomator.event.NotificationHandler;
provides TrayMenuController with AwtTrayMenuController;
provides Configurator with LogbackConfiguratorFactory;

View File

@@ -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());
}
}

View File

@@ -10,7 +10,7 @@ package org.cryptomator.common.vaults;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.Constants;
import org.cryptomator.common.EventMap;
import org.cryptomator.event.FileSystemEventAggregator;
import org.cryptomator.common.mount.Mounter;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.VaultSettings;
@@ -23,7 +23,6 @@ import org.cryptomator.cryptofs.event.FilesystemEvent;
import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.MasterkeyLoader;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
import org.cryptomator.event.VaultEvent;
import org.cryptomator.integrations.mount.MountFailedException;
import org.cryptomator.integrations.mount.Mountpoint;
import org.cryptomator.integrations.mount.UnmountFailedException;
@@ -35,7 +34,6 @@ import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
@@ -78,7 +76,7 @@ public class Vault {
private final ObjectBinding<Mountpoint> mountPoint;
private final Mounter mounter;
private final Settings settings;
private final EventMap eventMap;
private final FileSystemEventAggregator fileSystemEventAggregator;
private final BooleanProperty showingStats;
private final AtomicReference<Mounter.MountHandle> mountHandle = new AtomicReference<>(null);
@@ -91,7 +89,7 @@ public class Vault {
@Named("lastKnownException") ObjectProperty<Exception> lastKnownException, //
VaultStats stats, //
Mounter mounter, Settings settings, //
EventMap eventMap) {
FileSystemEventAggregator fileSystemEventAggregator) {
this.vaultSettings = vaultSettings;
this.configCache = configCache;
this.cryptoFileSystem = cryptoFileSystem;
@@ -108,7 +106,7 @@ public class Vault {
this.mountPoint = Bindings.createObjectBinding(this::getMountPoint, state);
this.mounter = mounter;
this.settings = settings;
this.eventMap = eventMap;
this.fileSystemEventAggregator = fileSystemEventAggregator;
this.showingStats = new SimpleBooleanProperty(false);
this.quickAccessEntry = new AtomicReference<>(null);
}
@@ -261,10 +259,7 @@ public class Vault {
private void consumeVaultEvent(FilesystemEvent e) {
var wrapper = new VaultEvent(this, e);
Platform.runLater(() -> {
eventMap.put(wrapper);
});
fileSystemEventAggregator.put(this, e);
}
// ******************************************************************************

View File

@@ -1,14 +0,0 @@
package org.cryptomator.event;
public sealed interface Answer permits Answer.DoNothing, Answer.DoSomething {
record DoNothing() implements Answer {}
record DoSomething(Runnable action) implements Answer {
void run() {
action.run();
}
}
}

View File

@@ -0,0 +1,8 @@
package org.cryptomator.event;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.event.FilesystemEvent;
import java.nio.file.Path;
public record FSEventBucket(Vault vault, Path idPath, Class<? extends FilesystemEvent> c) {}

View File

@@ -0,0 +1,5 @@
package org.cryptomator.event;
import org.cryptomator.cryptofs.event.FilesystemEvent;
public record FSEventBucketContent(FilesystemEvent mostRecentEvent, int count) {}

View File

@@ -0,0 +1,101 @@
package org.cryptomator.event;
import org.cryptomator.common.vaults.Vault;
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 javax.inject.Inject;
import javax.inject.Singleton;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
@Singleton
public class FileSystemEventAggregator {
private final ConcurrentHashMap<FSEventBucket, FSEventBucketContent> map;
private final AtomicBoolean hasUpdates;
@Inject
public FileSystemEventAggregator() {
this.map = new ConcurrentHashMap<>();
this.hasUpdates = new AtomicBoolean(false);
}
/**
* Adds the given event to the map. If a bucket for this event already exists, only the count is updated and the event set as the most recent one.
*
* @param v Vault where the event occurred
* @param e Actual {@link FilesystemEvent}
*/
public void put(Vault v, FilesystemEvent e) {
var key = computeKey(v, e);
hasUpdates.set(true);
map.compute(key, (k, val) -> {
if (val == null) {
return new FSEventBucketContent(e, 1);
} else {
return new FSEventBucketContent(e, val.count() + 1);
}
});
}
/**
* Removes an event bucket from the map.
*/
public FSEventBucketContent remove(FSEventBucket key) {
hasUpdates.set(true);
return map.remove(key);
}
/**
* Clears the event map.
*/
public void clear() {
hasUpdates.set(true);
map.clear();
}
public boolean hasUpdates() {
return hasUpdates.get();
}
/**
* Clones the map entries into a collection.
* <p>
* The collection is first cleared, then all map entries are added in one bulk operation. Cleans the hasUpdates status.
*
* @param target collection which is first cleared and then the EntrySet copied to.
*/
public void cloneTo(Collection<Map.Entry<FSEventBucket, FSEventBucketContent>> target) {
hasUpdates.set(false);
target.clear();
target.addAll(map.entrySet());
}
/**
* Method to compute the identifying key for a given filesystem event
*
* @param v Vault where the event occurred
* @param event Actual {@link FilesystemEvent}
* @return a {@link FSEventBucket} used in the map and lru cache
*/
private static FSEventBucket computeKey(Vault v, FilesystemEvent event) {
var p = switch (event) {
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 FSEventBucket(v, p, event.getClass());
}
}

View File

@@ -1,15 +0,0 @@
package org.cryptomator.event;
import org.cryptomator.integrations.common.IntegrationsLoader;
import java.util.ServiceLoader;
import java.util.stream.Stream;
public interface NotificationHandler {
Answer handle(VaultEvent e);
static Stream<NotificationHandler> loadAll() {
return IntegrationsLoader.loadAll(ServiceLoader.load(NotificationHandler.class), NotificationHandler.class);
}
}

View File

@@ -1,27 +0,0 @@
package org.cryptomator.event;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.event.FilesystemEvent;
import java.time.Instant;
public record VaultEvent(Vault v, FilesystemEvent actualEvent, int count) implements Comparable<VaultEvent> {
public VaultEvent(Vault v, FilesystemEvent actualEvent) {
this(v, actualEvent, 1);
}
@Override
public int compareTo(VaultEvent other) {
var timeResult = actualEvent.getTimestamp().compareTo(other.actualEvent().getTimestamp());
if(timeResult != 0) {
return timeResult;
} else {
return this.equals(other) ? 0 : this.actualEvent.getClass().getName().compareTo(other.actualEvent.getClass().getName());
}
}
public VaultEvent incrementCount(FilesystemEvent update) {
return new VaultEvent(v, update, count+1);
}
}

View File

@@ -1,6 +1,8 @@
package org.cryptomator.ui.eventview;
import org.cryptomator.common.EventMap;
import org.cryptomator.event.FSEventBucket;
import org.cryptomator.event.FSEventBucketContent;
import org.cryptomator.event.FileSystemEventAggregator;
import org.cryptomator.common.Nullable;
import org.cryptomator.common.ObservableUtil;
import org.cryptomator.cryptofs.CryptoPath;
@@ -9,7 +11,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.VaultEvent;
import org.cryptomator.integrations.revealpath.RevealFailedException;
import org.cryptomator.integrations.revealpath.RevealPathService;
import org.cryptomator.ui.common.FxController;
@@ -41,6 +42,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;
@@ -51,11 +53,11 @@ public class EventListCellController implements FxController {
private static final DateTimeFormatter LOCAL_DATE_FORMATTER = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).withZone(ZoneId.systemDefault());
private static final DateTimeFormatter LOCAL_TIME_FORMATTER = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withZone(ZoneId.systemDefault());
private final EventMap eventMap;
private final FileSystemEventAggregator fileSystemEventAggregator;
@Nullable
private final RevealPathService revealService;
private final ResourceBundle resourceBundle;
private final ObjectProperty<VaultEvent> event;
private final ObjectProperty<Map.Entry<FSEventBucket, FSEventBucketContent>> eventEntry;
private final StringProperty eventMessage;
private final StringProperty eventDescription;
private final ObjectProperty<FontAwesome5Icon> eventIcon;
@@ -77,18 +79,18 @@ public class EventListCellController implements FxController {
Button eventActionsButton;
@Inject
public EventListCellController(EventMap eventMap, Optional<RevealPathService> revealService, ResourceBundle resourceBundle) {
this.eventMap = eventMap;
public EventListCellController(FileSystemEventAggregator fileSystemEventAggregator, Optional<RevealPathService> revealService, ResourceBundle resourceBundle) {
this.fileSystemEventAggregator = fileSystemEventAggregator;
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.v().unlockedProperty()), Function.identity(), false);
this.readableTime = ObservableUtil.mapWithDefault(event, e -> LOCAL_TIME_FORMATTER.format(e.actualEvent().getTimestamp()), "");
this.readableDate = ObservableUtil.mapWithDefault(event, e -> LOCAL_DATE_FORMATTER.format(e.actualEvent().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 +110,15 @@ public class EventListCellController implements FxController {
return vaultUnlocked.getValue() && (eventActionsMenu.isShowing() || root.isHover());
}
public void setEvent(@NotNull VaultEvent item) {
event.set(item);
public void setEventEntry(@NotNull Map.Entry<FSEventBucket, FSEventBucketContent> item) {
eventEntry.set(item);
eventActionsMenu.hide();
eventActionsMenu.getItems().clear();
eventTooltip.setText(item.v().getDisplayName());
addAction("generic.action.dismiss", () -> eventMap.remove(item));
switch (item.actualEvent()) {
eventTooltip.setText(item.getKey().vault().getDisplayName());
addAction("generic.action.dismiss", () -> {
fileSystemEventAggregator.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 +213,18 @@ public class EventListCellController implements FxController {
private String selectDescription() {
if (vaultUnlocked.getValue()) {
return eventDescription.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 {
var e = event.getValue();
return resourceBundle.getString("eventView.entry.vaultLocked.description").formatted(e != null ? e.v().getDisplayName() : "");
return "";
}
}
@FXML
public void toggleEventActionsMenu() {
var e = event.get();
var e = eventEntry.get();
if (e != null) {
if (eventActionsMenu.isShowing()) {
eventActionsMenu.hide();
@@ -232,7 +238,7 @@ public class EventListCellController implements FxController {
if (!(p instanceof CryptoPath)) {
throw new IllegalArgumentException("Path " + p + " is not a vault path");
}
var v = event.getValue().v();
var v = eventEntry.getValue().getKey().vault();
if (!v.isUnlocked()) {
return Path.of(System.getProperty("user.home"));
}

View File

@@ -1,6 +1,7 @@
package org.cryptomator.ui.eventview;
import org.cryptomator.event.VaultEvent;
import org.cryptomator.event.FSEventBucket;
import org.cryptomator.event.FSEventBucketContent;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import javax.inject.Inject;
@@ -12,9 +13,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<VaultEvent>, ListCell<VaultEvent>> {
public class EventListCellFactory implements Callback<ListView<Map.Entry<FSEventBucket, FSEventBucketContent>>, ListCell<Map.Entry<FSEventBucket, FSEventBucketContent>>> {
private static final String FXML_PATH = "/fxml/eventview_cell.fxml";
@@ -27,7 +29,7 @@ public class EventListCellFactory implements Callback<ListView<VaultEvent>, List
@Override
public ListCell<VaultEvent> call(ListView<VaultEvent> eventListView) {
public ListCell<Map.Entry<FSEventBucket, FSEventBucketContent>> call(ListView<Map.Entry<FSEventBucket, FSEventBucketContent>> eventListView) {
try {
FXMLLoader fxmlLoader = fxmlLoaders.load(FXML_PATH);
return new Cell(fxmlLoader.getRoot(), fxmlLoader.getController());
@@ -36,7 +38,7 @@ public class EventListCellFactory implements Callback<ListView<VaultEvent>, List
}
}
private static class Cell extends ListCell<VaultEvent> {
private static class Cell extends ListCell<Map.Entry<FSEventBucket, FSEventBucketContent>> {
private final Parent root;
private final EventListCellController controller;
@@ -47,7 +49,7 @@ public class EventListCellFactory implements Callback<ListView<VaultEvent>, List
}
@Override
protected void updateItem(VaultEvent item, boolean empty) {
protected void updateItem(Map.Entry<FSEventBucket, FSEventBucketContent> item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
@@ -57,7 +59,7 @@ public class EventListCellFactory implements Callback<ListView<VaultEvent>, List
this.getStyleClass().addLast("list-cell");
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
setGraphic(root);
controller.setEvent(item);
controller.setEventEntry(item);
}
}
}

View File

@@ -1,15 +1,16 @@
package org.cryptomator.ui.eventview;
import org.cryptomator.common.EventMap;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.event.VaultEvent;
import org.cryptomator.event.FSEventBucket;
import org.cryptomator.event.FSEventBucketContent;
import org.cryptomator.event.FileSystemEventAggregator;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.fxapp.FxFSEventList;
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 +18,16 @@ 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 EventMap eventMap;
private final ObservableList<VaultEvent> eventList;
private final FilteredList<VaultEvent> filteredEventList;
private final FilteredList<Map.Entry<FSEventBucket, FSEventBucketContent>> filteredEventList;
private final ObservableList<Vault> vaults;
private final SortedList<VaultEvent> reversedEventList;
private final FileSystemEventAggregator aggregator;
private final SortedList<Map.Entry<FSEventBucket, FSEventBucketContent>> sortedEventList;
private final ObservableList<Vault> choiceBoxEntries;
private final ResourceBundle resourceBundle;
private final EventListCellFactory cellFactory;
@@ -35,20 +35,45 @@ public class EventViewController implements FxController {
@FXML
ChoiceBox<Vault> vaultFilterChoiceBox;
@FXML
ListView<VaultEvent> eventListView;
ListView<Map.Entry<FSEventBucket, FSEventBucketContent>> eventListView;
@Inject
public EventViewController(EventMap eventMap, ObservableList<Vault> vaults, ResourceBundle resourceBundle, EventListCellFactory cellFactory) {
this.eventMap = eventMap;
this.eventList = FXCollections.observableArrayList();
this.filteredEventList = eventList.filtered(_ -> true);
public EventViewController(FxFSEventList fxFSEventList, ObservableList<Vault> vaults, ResourceBundle resourceBundle, EventListCellFactory cellFactory, FileSystemEventAggregator aggregator) {
this.filteredEventList = fxFSEventList.getObservableList().filtered(_ -> true);
this.vaults = vaults;
this.reversedEventList = new SortedList<>(filteredEventList, Comparator.reverseOrder());
this.aggregator = aggregator;
this.sortedEventList = new SortedList<>(filteredEventList, this::compareBuckets);
this.choiceBoxEntries = FXCollections.observableArrayList();
this.resourceBundle = resourceBundle;
this.cellFactory = cellFactory;
}
/**
* Comparison 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 an entry of a {@link FSEventBucket} and its content
* @param right another entry of a {@link FSEventBucket} plus content, 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<FSEventBucket, FSEventBucketContent> left, Map.Entry<FSEventBucket, FSEventBucketContent> 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 vaultIdComparison = left.getKey().vault().getId().compareTo(right.getKey().vault().getId());
if (vaultIdComparison != 0) {
return vaultIdComparison;
}
var pathComparison = left.getKey().idPath().compareTo(right.getKey().idPath());
if (pathComparison != 0) {
return pathComparison;
}
return left.getKey().c().getName().compareTo(right.getKey().c().getName());
}
@FXML
public void initialize() {
choiceBoxEntries.add(null);
@@ -60,39 +85,25 @@ public class EventViewController implements FxController {
}
});
eventList.addAll(eventMap.values());
eventMap.addListener((MapChangeListener<? super EventMap.EventKey, ? super VaultEvent>) 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 EventMap.EventKey, ? extends VaultEvent> change) {
if (change.wasAdded() && change.wasRemoved()) {
//entry updated
eventList.remove(change.getValueRemoved());
eventList.addLast(change.getValueAdded());
} else if (change.wasAdded()) {
eventList.addLast(change.getValueAdded());
} else { //removed
eventList.remove(change.getValueRemoved());
}
}
private void applyVaultFilter(ObservableValue<? extends Vault> v, Vault oldV, Vault newV) {
if (newV == null) {
filteredEventList.setPredicate(_ -> true);
} else {
filteredEventList.setPredicate(e -> e.v().equals(newV));
filteredEventList.setPredicate(e -> e.getKey().vault().equals(newV));
}
}
@FXML
void clearEvents() {
eventMap.clear();
aggregator.clear();
}
private static class VaultConverter extends StringConverter<Vault> {

View File

@@ -11,6 +11,7 @@ import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.fxapp.FxFSEventList;
import javax.inject.Provider;
import javafx.scene.Scene;
@@ -25,12 +26,17 @@ abstract class EventViewModule {
@Provides
@EventViewScoped
@EventViewWindow
static Stage provideStage(StageFactory factory, ResourceBundle resourceBundle) {
static Stage provideStage(StageFactory factory, ResourceBundle resourceBundle, FxFSEventList fxFSEventList) {
Stage stage = factory.create();
stage.setHeight(498);
stage.setTitle(resourceBundle.getString("eventView.title"));
stage.setResizable(true);
stage.initModality(Modality.NONE);
stage.focusedProperty().addListener((_,_,isFocused) -> {
if(isFocused) {
fxFSEventList.unreadEventsProperty().setValue(false);
}
});
return stage;
}

View File

@@ -1,14 +0,0 @@
package org.cryptomator.ui.eventview;
import org.cryptomator.ui.common.FxController;
import javax.inject.Inject;
@EventViewScoped
public class UpdateEventViewController implements FxController {
@Inject
public UpdateEventViewController() {
}
}

View File

@@ -29,9 +29,10 @@ public class FxApplication {
private final FxApplicationStyle applicationStyle;
private final FxApplicationTerminator applicationTerminator;
private final AutoUnlocker autoUnlocker;
private final FxFSEventList fxFSEventList; //not unused! By injecting it here, the object gets initiated the service starts
@Inject
FxApplication(@Named("startupTime") long startupTime, Environment environment, Settings settings, AppLaunchEventHandler launchEventHandler, Lazy<TrayMenuComponent> trayMenu, FxApplicationWindows appWindows, FxApplicationStyle applicationStyle, FxApplicationTerminator applicationTerminator, AutoUnlocker autoUnlocker) {
FxApplication(@Named("startupTime") long startupTime, Environment environment, Settings settings, AppLaunchEventHandler launchEventHandler, Lazy<TrayMenuComponent> trayMenu, FxApplicationWindows appWindows, FxApplicationStyle applicationStyle, FxApplicationTerminator applicationTerminator, AutoUnlocker autoUnlocker, FxFSEventList fxFSEventList) {
this.startupTime = startupTime;
this.environment = environment;
this.settings = settings;
@@ -41,6 +42,7 @@ public class FxApplication {
this.applicationStyle = applicationStyle;
this.applicationTerminator = applicationTerminator;
this.autoUnlocker = autoUnlocker;
this.fxFSEventList = fxFSEventList;
}
public void start() {

View File

@@ -20,6 +20,9 @@ import org.cryptomator.ui.unlock.UnlockComponent;
import org.cryptomator.ui.updatereminder.UpdateReminderComponent;
import org.cryptomator.ui.vaultoptions.VaultOptionsComponent;
import javax.inject.Named;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.scene.image.Image;
import java.io.IOException;
import java.io.InputStream;

View File

@@ -0,0 +1,65 @@
package org.cryptomator.ui.fxapp;
import org.cryptomator.event.FSEventBucket;
import org.cryptomator.event.FSEventBucketContent;
import org.cryptomator.event.FileSystemEventAggregator;
import javax.inject.Inject;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
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 FxFSEventList {
private final ObservableList<Map.Entry<FSEventBucket, FSEventBucketContent>> events;
private final FileSystemEventAggregator eventAggregator;
private final ScheduledFuture<?> scheduledTask;
private final BooleanProperty unreadEvents;
@Inject
public FxFSEventList(FileSystemEventAggregator fsEventAggregator, ScheduledExecutorService scheduler) {
this.events = FXCollections.observableArrayList();
this.eventAggregator = fsEventAggregator;
this.unreadEvents = new SimpleBooleanProperty(false);
this.scheduledTask = scheduler.scheduleWithFixedDelay(() -> {
if (fsEventAggregator.hasUpdates()) {
flush();
}
}, 1000, 1000, TimeUnit.MILLISECONDS);
}
/**
* Starts the clone task on the FX thread and wait till it is completed
*/
private void flush() {
var latch = new CountDownLatch(1);
Platform.runLater(() -> {
eventAggregator.cloneTo(events);
unreadEvents.setValue(true);
latch.countDown();
});
try {
latch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public ObservableList<Map.Entry<FSEventBucket, FSEventBucketContent>> getObservableList() {
return events;
}
public BooleanProperty unreadEventsProperty() {
return unreadEvents;
}
}

View File

@@ -1,17 +1,16 @@
package org.cryptomator.ui.mainwindow;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.EventMap;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptofs.DirStructure;
import org.cryptomator.event.VaultEvent;
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.preferences.SelectedPreferencesTab;
import org.slf4j.Logger;
@@ -25,7 +24,6 @@ import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.collections.MapChangeListener;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.geometry.Side;
@@ -69,8 +67,7 @@ public class VaultListController implements FxController {
private final VaultListCellFactory cellFactory;
private final AddVaultWizardComponent.Builder addVaultWizard;
private final BooleanBinding emptyVaultList;
private final EventMap eventMap;
private final BooleanProperty newEventsPresent;
private final BooleanProperty unreadEvents;
private final VaultListManager vaultListManager;
private final BooleanProperty draggingVaultOver = new SimpleBooleanProperty();
private final ResourceBundle resourceBundle;
@@ -97,7 +94,7 @@ public class VaultListController implements FxController {
FxApplicationWindows appWindows, //
Settings settings, //
Dialogs dialogs, //
EventMap eventMap) {
FxFSEventList fxFSEventList) {
this.mainWindow = mainWindow;
this.vaults = vaults;
this.selectedVault = selectedVault;
@@ -110,13 +107,7 @@ public class VaultListController implements FxController {
this.dialogs = dialogs;
this.emptyVaultList = Bindings.isEmpty(vaults);
this.eventMap = eventMap;
this.newEventsPresent = new SimpleBooleanProperty(false);
eventMap.addListener((MapChangeListener<? super EventMap.EventKey, ? super VaultEvent>) change -> {
if (change.wasAdded()) {
newEventsPresent.setValue(true);
}
});
this.unreadEvents = fxFSEventList.unreadEventsProperty();
selectedVault.addListener(this::selectedVaultDidChange);
cellSize = settings.compactMode.map(compact -> compact ? 30.0 : 60.0);
@@ -279,7 +270,7 @@ public class VaultListController implements FxController {
@FXML
public void showEventViewer() {
appWindows.showEventViewer();
newEventsPresent.setValue(false);
unreadEvents.setValue(false);
}
// Getter and Setter
@@ -307,11 +298,11 @@ public class VaultListController implements FxController {
return cellSize.getValue();
}
public ObservableValue<Boolean> newEventsPresentProperty() {
return newEventsPresent;
public ObservableValue<Boolean> unreadEventsPresentProperty() {
return unreadEvents;
}
public boolean getNewEventsPresent() {
return newEventsPresent.getValue();
public boolean getUnreadEventsPresent() {
return unreadEvents.getValue();
}
}

View File

@@ -642,10 +642,7 @@
******************************************************************************/
.update-indicator {
-fx-background-color: white, RED_5;
-fx-background-insets: 1px, 2px;
-fx-background-radius: 6px, 5px;
-fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.8), 2, 0, 0, 0);
-fx-fill: RED_5;
}
/*******************************************************************************

View File

@@ -644,11 +644,8 @@
* *
******************************************************************************/
.update-indicator {
-fx-background-color: white, RED_5;
-fx-background-insets: 1px, 2px;
-fx-background-radius: 6px, 5px;
-fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.8), 2, 0, 0, 0);
.icon-update-indicator {
-fx-fill: RED_5;
}
/*******************************************************************************

View File

@@ -12,6 +12,10 @@
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Arc?>
<?import javafx.scene.shape.Circle?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.shape.Rectangle?>
<?import javafx.scene.layout.AnchorPane?>
<StackPane xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:id="root"
@@ -51,7 +55,9 @@
<Tooltip text="%main.vaultlist.showEventsButton.tooltip"/>
</tooltip>
</Button>
<Region styleClass="update-indicator" visible="${controller.newEventsPresent}" mouseTransparent="true" StackPane.alignment="TOP_RIGHT" prefWidth="12" prefHeight="12" maxWidth="-Infinity" maxHeight="-Infinity"/>
<AnchorPane mouseTransparent="true" minWidth="12" maxWidth="12" minHeight="12" maxHeight="12" StackPane.alignment="CENTER">
<Circle radius="4" styleClass="icon-update-indicator" AnchorPane.topAnchor="-8" AnchorPane.rightAnchor="-6" visible="${controller.unreadEventsPresent}" />
</AnchorPane>
</StackPane>
<Button onMouseClicked="#showPreferences" styleClass="button-right" alignment="CENTER" minWidth="20" contentDisplay="GRAPHIC_ONLY">
<graphic>