diff --git a/src/main/java/org/cryptomator/ui/eventview/EventListCellController.java b/src/main/java/org/cryptomator/ui/eventview/EventListCellController.java index 8f546a97f..36b602f5e 100644 --- a/src/main/java/org/cryptomator/ui/eventview/EventListCellController.java +++ b/src/main/java/org/cryptomator/ui/eventview/EventListCellController.java @@ -1,30 +1,35 @@ package org.cryptomator.ui.eventview; import org.cryptomator.common.ObservableUtil; -import org.cryptomator.common.vaults.Vault; +import org.cryptomator.cryptofs.event.ConflictResolutionFailedEvent; import org.cryptomator.cryptofs.event.ConflictResolvedEvent; -import org.cryptomator.cryptofs.event.FilesystemEvent; +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; import org.cryptomator.ui.controls.FontAwesome5Icon; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; -import javafx.beans.Observable; +import javafx.beans.binding.Bindings; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; import javafx.beans.value.ObservableValue; 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.MenuItem; import java.nio.file.Path; import java.util.Optional; import java.util.ResourceBundle; +import java.util.function.Function; public class EventListCellController implements FxController { @@ -34,14 +39,16 @@ public class EventListCellController implements FxController { private final Optional revealService; private final ResourceBundle resourceBundle; private final ObjectProperty event; + private final StringProperty eventMessage; + private final StringProperty eventDescription; + private final ObjectProperty eventIcon; + private final ObservableValue vaultUnlocked; private final ObservableValue message; private final ObservableValue description; private final ObservableValue icon; @FXML - ContextMenu basicEventActions; - @FXML - ContextMenu conflictResoledEventActions; + ContextMenu eventActions; @FXML Button eventActionsButton; @@ -51,72 +58,118 @@ public class EventListCellController implements FxController { this.revealService = revealService; this.resourceBundle = resourceBundle; this.event = new SimpleObjectProperty<>(null); - this.message = ObservableUtil.mapWithDefault(event, e -> e.getClass().getName(), ""); - this.description = ObservableUtil.mapWithDefault(event, this::selectDescription, ""); - this.icon = ObservableUtil.mapWithDefault(event, this::selectIcon, FontAwesome5Icon.BELL); - event.addListener(this::hideContextMenus); + this.eventMessage = new SimpleStringProperty(); + this.eventDescription = new SimpleStringProperty(); + this.eventIcon = new SimpleObjectProperty<>(); + this.vaultUnlocked = ObservableUtil.mapWithDefault(event.flatMap(e -> e.v().unlockedProperty()), Function.identity(), false); + this.message = Bindings.createStringBinding(this::selectMessage, vaultUnlocked, eventMessage); + this.description = Bindings.createStringBinding(this::selectDescription, vaultUnlocked, eventDescription); + this.icon = Bindings.createObjectBinding(this::selectIcon, vaultUnlocked, eventIcon); } - - private void hideContextMenus(Observable observable, VaultEvent oldValue, VaultEvent newValue) { - basicEventActions.hide(); - conflictResoledEventActions.hide(); - } - - public void setEvent(VaultEvent item) { + public void setEvent(@NotNull VaultEvent item) { event.set(item); + eventDescription.setValue("Vault " + item.v().getDisplayName()); + eventActions.hide(); + eventActions.getItems().clear(); + addAction("generic.action.dismiss", () -> events.remove(item)); + switch (item.actualEvent()) { + case ConflictResolvedEvent fse -> this.adjustToConflictResolvedEvent(fse); + case ConflictResolutionFailedEvent fse -> this.adjustToConflictEvent(fse); + case DecryptionFailedEvent fse -> this.adjustToDecryptionFailedEvent(fse); + } } - private FontAwesome5Icon selectIcon(VaultEvent e) { - return FontAwesome5Icon.FILE; + private void adjustToConflictResolvedEvent(ConflictResolvedEvent cre) { + eventIcon.setValue(FontAwesome5Icon.FILE); + eventMessage.setValue("Resolved conflict, new file is " + cre.resolvedCleartextPath()); //TODO:localize + if (revealService.isPresent()) { + addAction("event.conflictResolved.showDecrypted", () -> this.showResolvedConflict(cre)); + } } - private String selectDescription(VaultEvent e) { - return switch (e.actualEvent()) { - case ConflictResolvedEvent _-> "A conflict is resolved!"; - default -> "Something happened"; - }; + private void adjustToConflictEvent(ConflictResolutionFailedEvent cfe) { + eventIcon.setValue(FontAwesome5Icon.TIMES); + eventMessage.setValue("Failed to resolve conflict for " + cfe.conflictingCiphertextPath()); //TODO:localize + if (revealService.isPresent()) { + addAction("event.conflictFailed.showEncrypted", () -> reveal(cfe.conflictingCiphertextPath())); + } } + private void adjustToDecryptionFailedEvent(DecryptionFailedEvent dfe) { + eventIcon.setValue(FontAwesome5Icon.BAN); + eventMessage.setValue("Cannot decrypt " + dfe.ciphertextPath()); //TODO:localize + if (revealService.isPresent()) { + addAction("event.decryptionFailed.showEncrypted", () -> reveal(dfe.ciphertextPath())); + } + } + + private void addAction(String localizationKey, Runnable action) { + var entry = new MenuItem(resourceBundle.getString(localizationKey)); + entry.getStyleClass().addLast("add-vault-menu-item"); + entry.setOnAction(_ -> action.run()); + eventActions.getItems().addLast(entry); + } + + + private FontAwesome5Icon selectIcon() { + if (vaultUnlocked.getValue()) { + return eventIcon.getValue(); + } else { + return FontAwesome5Icon.LOCK; + } + } + + private String selectMessage() { + if (vaultUnlocked.getValue()) { + return eventMessage.getValue(); + } else { + var e = event.getValue(); + return "Event for " + (e != null ? e.v().getDisplayName() : ""); //TODO: localize + } + } + + private String selectDescription() { + if (vaultUnlocked.getValue()) { + return eventDescription.getValue(); + } else { + return "Unlock the vault to display details."; //TODO: localize + } + } + + @FXML public void toggleEventActionsMenu() { var e = event.get(); if (e != null) { - var contextMenu = switch (e.actualEvent()) { - case ConflictResolvedEvent _ -> conflictResoledEventActions; - default -> basicEventActions; - }; - if (contextMenu.isShowing()) { - contextMenu.hide(); + if (eventActions.isShowing()) { + eventActions.hide(); } else { - contextMenu.show(eventActionsButton, Side.BOTTOM, 0.0, 0.0); + eventActions.show(eventActionsButton, Side.BOTTOM, 0.0, 0.0); } } } - @FXML - public void dismissEvent() { - events.remove(event.getValue()); - } - - @FXML - public void showResolvedConflict() { - if (event.getValue() instanceof VaultEvent(_, Vault v, FilesystemEvent fse) && fse instanceof ConflictResolvedEvent cre) { - if (v.isUnlocked()) { - var mountUri = v.getMountPoint().uri(); - var internalPath = cre.resolvedCleartextPath().toString().substring(1); - var actualPath = Path.of(mountUri.getPath().concat(internalPath).substring(1)); - var s = revealService.orElseThrow(() -> new IllegalStateException("Function requiring revealService called, but service not available")); - try { - s.reveal(actualPath); - } catch (RevealFailedException e) { - LOG.warn("Failed to show resolved file conflict", e); - } - - } + private void showResolvedConflict(ConflictResolvedEvent cre) { + var v = event.getValue().v(); + if (v.isUnlocked()) { + var mountUri = v.getMountPoint().uri(); + var internalPath = cre.resolvedCleartextPath().toString().substring(1); + var actualPath = Path.of(mountUri.getPath().concat(internalPath).substring(1)); + reveal(actualPath); } } + private void reveal(Path p) { + try { + revealService.orElseThrow(() -> new IllegalStateException("Function requiring revealService called, but service not available")) // + .reveal(p); + } catch (RevealFailedException e) { + LOG.warn("Failed to show path {}",p, e); + } + } + + //-- property accessors -- public ObservableValue messageProperty() { return message; @@ -142,8 +195,4 @@ public class EventListCellController implements FxController { return icon.getValue(); } - public boolean isRevealServicePresent() { - return revealService.isPresent(); - } - } diff --git a/src/main/resources/fxml/eventview_cell.fxml b/src/main/resources/fxml/eventview_cell.fxml index 0cf7694d9..8e3d583db 100644 --- a/src/main/resources/fxml/eventview_cell.fxml +++ b/src/main/resources/fxml/eventview_cell.fxml @@ -33,16 +33,8 @@ - - - - - - - - - - + + diff --git a/src/main/resources/i18n/strings.properties b/src/main/resources/i18n/strings.properties index b03c202bf..14fd0b1e0 100644 --- a/src/main/resources/i18n/strings.properties +++ b/src/main/resources/i18n/strings.properties @@ -1,7 +1,13 @@ # Locale Specific CSS files such as CJK, RTL,... additionalStyleSheets= +#Test +event.conflictResolved.showDecrypted=Show decrypted file +event.conflictFailed.showEncrypted=Show encrypted file +event.decryptionFailed.showEncrypted=Show encrypted file + # Generics +generic.action.dismiss=Dismiss ## Button generic.button.apply=Apply generic.button.back=Back