Feature/simple spinning (#1728)

* Define discrete rotation animation
* New class FontAwesome5Spinner extending FontAwesome5IconView, animated
* New class AutoAnimator to play Animation on a Node conditionally
* Replace all occurences of Progress Indicator with FontAwesome5Spinner
* Spin spinner icon in processing vault state
* remove undocumented progress indicator styling
This commit is contained in:
Armin Schrenk
2021-07-26 18:40:55 +02:00
committed by GitHub
parent 2aa17edd8c
commit c81ef1c109
21 changed files with 232 additions and 152 deletions

View File

@@ -76,6 +76,7 @@ public class CreateNewVaultPasswordController implements FxController {
private final BooleanProperty readyToCreateVault;
private final ObjectBinding<ContentDisplay> createVaultButtonState;
/* FXML */
public ToggleGroup recoveryKeyChoice;
public Toggle showRecoveryKey;
public Toggle skipRecoveryKey;

View File

@@ -1,11 +1,17 @@
package org.cryptomator.ui.common;
import javafx.animation.Animation;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.RotateTransition;
import javafx.animation.SequentialTransition;
import javafx.animation.Timeline;
import javafx.beans.value.WritableValue;
import javafx.scene.Node;
import javafx.stage.Window;
import javafx.util.Duration;
import java.util.stream.IntStream;
public class Animations {
@@ -33,4 +39,19 @@ public class Animations {
);
}
public static SequentialTransition createDiscrete360Rotation(Node toAnimate) {
var animation = new SequentialTransition(IntStream.range(0, 8).mapToObj(i -> Animations.createDiscrete45Rotation()).toArray(Animation[]::new));
animation.setCycleCount(Animation.INDEFINITE);
animation.setNode(toAnimate);
return animation;
}
private static RotateTransition createDiscrete45Rotation() {
var animation = new RotateTransition(Duration.millis(100));
animation.setInterpolator(Interpolator.DISCRETE);
animation.setByAngle(45);
animation.setCycleCount(1);
return animation;
}
}

View File

@@ -0,0 +1,108 @@
package org.cryptomator.ui.common;
import com.tobiasdiez.easybind.EasyBind;
import com.tobiasdiez.easybind.Subscription;
import javafx.animation.Animation;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
/**
* Animation which starts and stops automatically based on an observable condition.
* <p>
* During creation the consumer can optionally define actions to be executed everytime before the animation starts and after it stops.
* The automatic playback of the animation based on the condition can be stopped by calling {@link #deactivateCondition()}. To reactivate it, {@link #activateCondition()} must be called.
*/
public class AutoAnimator<T extends Animation> {
private final T animation;
private final ObservableValue<Boolean> condition;
private final Runnable beforeStart;
private final Runnable afterStop;
private Subscription sub;
AutoAnimator(T animation, ObservableValue<Boolean> condition, Runnable beforeStart, Runnable afterStop) {
this.animation = animation;
this.condition = condition;
this.beforeStart = beforeStart;
this.afterStop = afterStop;
activateCondition();
}
public void playFromStart() {
beforeStart.run();
animation.playFromStart();
}
public void stop() {
animation.stop();
afterStop.run();
}
/**
* Deactivates activation on the condition.
* No-op if condition is already deactivated.
*/
public void deactivateCondition() {
if (sub != null) {
sub.unsubscribe();
}
}
/**
* Activates the condition
* No-op if condition is already activated.
*/
public void activateCondition() {
if (sub == null) {
this.sub = EasyBind.subscribe(condition, this::togglePlay);
}
}
private void togglePlay(boolean play) {
if (play) {
this.playFromStart();
} else {
this.stop();
}
}
public static Builder animate(Animation animation) {
return new Builder(animation);
}
public static class Builder {
private Animation animation;
private ObservableValue<Boolean> condition = new SimpleBooleanProperty(true);
private Runnable beforeStart = () -> {};
private Runnable afterStop = () -> {};
private Builder(Animation animation) {
this.animation = animation;
}
public Builder onCondition(ObservableValue<Boolean> condition) {
this.condition = condition;
return this;
}
public Builder beforeStart(Runnable beforeStart) {
this.beforeStart = beforeStart;
return this;
}
public Builder afterStop(Runnable afterStop) {
this.afterStop = afterStop;
return this;
}
public AutoAnimator build() {
return new AutoAnimator(animation, condition, beforeStart, afterStop);
}
}
}

View File

@@ -0,0 +1,36 @@
package org.cryptomator.ui.controls;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.AutoAnimator;
import javafx.beans.NamedArg;
import javafx.beans.value.ObservableValue;
import java.util.Optional;
/**
* An animated progress spinner using the {@link FontAwesome5IconView} with the spinner glyph.
* <p>
* Using the default constructor, the animation is always played if the icon is visible. To animate on other conditions, use the constructor with the "spinning" property.
*/
public class FontAwesome5Spinner extends FontAwesome5IconView {
private AutoAnimator animator;
public FontAwesome5Spinner() {
new FontAwesome5Spinner(Optional.empty());
}
public FontAwesome5Spinner(@NamedArg("spinning") ObservableValue<Boolean> spinning) {
new FontAwesome5Spinner(Optional.of(spinning));
}
private FontAwesome5Spinner(Optional<ObservableValue<Boolean>> animateCondition) {
setGlyph(FontAwesome5Icon.SPINNER);
var animation = Animations.createDiscrete360Rotation(this);
this.animator = AutoAnimator.animate(animation) //
.afterStop(() -> setRotate(0)) //
.onCondition(animateCondition.orElse(visibleProperty())) //
.build();
}
}

View File

@@ -5,6 +5,7 @@ import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.common.WeakBindings;
import org.cryptomator.ui.controls.FontAwesome5IconView;
import org.cryptomator.ui.controls.NiceSecurePasswordField;
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
import org.cryptomator.ui.keyloading.KeyLoading;
@@ -59,8 +60,10 @@ public class PassphraseEntryController implements FxController {
private final BooleanProperty unlockButtonDisabled;
private final StringBinding vaultName;
/* FXML */
public NiceSecurePasswordField passwordField;
public CheckBox savePasswordCheckbox;
public FontAwesome5IconView unlockInProgressView;
public ImageView face;
public ImageView leftArm;
public ImageView rightArm;

View File

@@ -1,10 +1,14 @@
package org.cryptomator.ui.mainwindow;
import com.tobiasdiez.easybind.EasyBind;
import com.tobiasdiez.easybind.Subscription;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.AutoAnimator;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.controls.FontAwesome5Icon;
import org.cryptomator.ui.controls.FontAwesome5IconView;
import org.cryptomator.ui.fxapp.FxApplication;
import javax.inject.Inject;
@@ -22,6 +26,12 @@ public class VaultDetailController implements FxController {
private final Binding<FontAwesome5Icon> glyph;
private final BooleanBinding anyVaultSelected;
private AutoAnimator spinAnimation;
/* FXML */
public FontAwesome5IconView vaultStateView;
@Inject
VaultDetailController(ObjectProperty<Vault> vault, FxApplication application) {
this.vault = vault;
@@ -32,6 +42,13 @@ public class VaultDetailController implements FxController {
this.anyVaultSelected = vault.isNotNull();
}
public void initialize() {
this.spinAnimation = AutoAnimator.animate(Animations.createDiscrete360Rotation(vaultStateView)) //
.onCondition(EasyBind.select(vault).selectObject(Vault::stateProperty).map(VaultState.Value.PROCESSING::equals)) //
.afterStop(() -> vaultStateView.setRotate(0)) //
.build();
}
// TODO deduplicate w/ VaultListCellController
private FontAwesome5Icon getGlyphForVaultState(VaultState.Value state) {
if (state != null) {

View File

@@ -3,8 +3,11 @@ package org.cryptomator.ui.mainwindow;
import com.tobiasdiez.easybind.EasyBind;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.AutoAnimator;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.controls.FontAwesome5Icon;
import org.cryptomator.ui.controls.FontAwesome5IconView;
import javax.inject.Inject;
import javafx.beans.binding.Binding;
@@ -17,6 +20,11 @@ public class VaultListCellController implements FxController {
private final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
private final Binding<FontAwesome5Icon> glyph;
private AutoAnimator spinAnimation;
/* FXML */
public FontAwesome5IconView vaultStateView;
@Inject
VaultListCellController() {
this.glyph = EasyBind.select(vault) //
@@ -24,6 +32,13 @@ public class VaultListCellController implements FxController {
.map(this::getGlyphForVaultState);
}
public void initialize() {
this.spinAnimation = AutoAnimator.animate(Animations.createDiscrete360Rotation(vaultStateView)) //
.onCondition(EasyBind.select(vault).selectObject(Vault::stateProperty).map(VaultState.Value.PROCESSING::equals)) //
.afterStop(() -> vaultStateView.setRotate(0)) //
.build();
}
// TODO deduplicate w/ VaultDetailController
private FontAwesome5Icon getGlyphForVaultState(VaultState.Value state) {
if (state != null) {
@@ -59,4 +74,5 @@ public class VaultListCellController implements FxController {
public void setVault(Vault value) {
vault.set(value);
}
}

View File

@@ -66,7 +66,10 @@ public class MigrationRunController implements FxController {
private final Lazy<Scene> capabilityErrorScene;
private final BooleanProperty migrationButtonDisabled;
private final DoubleProperty migrationProgress;
private volatile double volatileMigrationProgress = -1.0;
/* FXML */
public NiceSecurePasswordField passwordField;
@Inject

View File

@@ -26,6 +26,8 @@ public class UpdatesPreferencesController implements FxController {
private final ReadOnlyStringProperty latestVersion;
private final ReadOnlyStringProperty currentVersion;
private final BooleanBinding updateAvailable;
/* FXML */
public CheckBox checkForUpdatesCheckbox;
@Inject

View File

@@ -30,6 +30,8 @@ public class QuitController implements FxController {
private final ExecutorService executorService;
private final VaultService vaultService;
private final AtomicReference<QuitResponse> quitResponse = new AtomicReference<>();
/* FXML */
public Button lockAndQuitButton;
@Inject

View File

@@ -32,6 +32,7 @@ public class UnlockSuccessController implements FxController {
private final ObjectProperty<ContentDisplay> revealButtonState;
private final BooleanProperty revealButtonDisabled;
/* FXML */
public CheckBox rememberChoiceCheckbox;
@Inject