优化 SpinnerPane (#5533)

This commit is contained in:
Glavo
2026-02-14 20:45:21 +08:00
committed by GitHub
parent eea072cea5
commit fb4f0be899

View File

@@ -18,8 +18,6 @@
package org.jackhuang.hmcl.ui.construct; package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXSpinner; import com.jfoenix.controls.JFXSpinner;
import javafx.beans.DefaultProperty;
import javafx.beans.InvalidationListener;
import javafx.beans.property.*; import javafx.beans.property.*;
import javafx.event.Event; import javafx.event.Event;
import javafx.event.EventHandler; import javafx.event.EventHandler;
@@ -33,14 +31,12 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations; import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.animation.TransitionPane;
@DefaultProperty("content") /// A spinner pane that can show spinner, failed reason, or content.
public class SpinnerPane extends Control { public class SpinnerPane extends Control {
private final ObjectProperty<Node> content = new SimpleObjectProperty<>(this, "content"); private static final String DEFAULT_STYLE_CLASS = "spinner-pane";
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading");
private final StringProperty failedReason = new SimpleStringProperty(this, "failedReason");
public SpinnerPane() { public SpinnerPane() {
getStyleClass().add("spinner-pane"); getStyleClass().add(DEFAULT_STYLE_CLASS);
} }
public void showSpinner() { public void showSpinner() {
@@ -52,60 +48,136 @@ public class SpinnerPane extends Control {
setLoading(false); setLoading(false);
} }
public Node getContent() { private void updateContent() {
return content.get(); if (getSkin() instanceof Skin skin) {
skin.updateContent();
}
} }
private ObjectProperty<Node> content;
public ObjectProperty<Node> contentProperty() { public ObjectProperty<Node> contentProperty() {
if (content == null)
content = new ObjectPropertyBase<>() {
@Override
public Object getBean() {
return SpinnerPane.this;
}
@Override
public String getName() {
return "content";
}
@Override
protected void invalidated() {
updateContent();
}
};
return content; return content;
} }
public void setContent(Node content) { public Node getContent() {
this.content.set(content); return contentProperty().get();
} }
public boolean isLoading() { public void setContent(Node content) {
return loading.get(); contentProperty().set(content);
} }
private BooleanProperty loading;
public BooleanProperty loadingProperty() { public BooleanProperty loadingProperty() {
if (loading == null)
loading = new BooleanPropertyBase() {
@Override
public Object getBean() {
return SpinnerPane.this;
}
@Override
public String getName() {
return "loading";
}
@Override
protected void invalidated() {
updateContent();
}
};
return loading; return loading;
} }
public void setLoading(boolean loading) { public boolean isLoading() {
this.loading.set(loading); return loading != null && loading.get();
} }
public String getFailedReason() { public void setLoading(boolean loading) {
return failedReason.get(); loadingProperty().set(loading);
} }
private StringProperty failedReason;
public StringProperty failedReasonProperty() { public StringProperty failedReasonProperty() {
if (failedReason == null)
failedReason = new StringPropertyBase() {
@Override
public Object getBean() {
return SpinnerPane.this;
}
@Override
public String getName() {
return "failedReason";
}
@Override
protected void invalidated() {
updateContent();
}
};
return failedReason; return failedReason;
} }
public void setFailedReason(String failedReason) { public String getFailedReason() {
this.failedReason.set(failedReason); return failedReason != null ? failedReason.get() : null;
} }
public void setFailedReason(String failedReason) {
failedReasonProperty().set(failedReason);
}
private ObjectProperty<EventHandler<Event>> onFailedAction;
public final ObjectProperty<EventHandler<Event>> onFailedActionProperty() { public final ObjectProperty<EventHandler<Event>> onFailedActionProperty() {
return onFailedAction; if (onFailedAction == null) {
onFailedAction = new ObjectPropertyBase<>() {
@Override
public Object getBean() {
return SpinnerPane.this;
} }
public final void setOnFailedAction(EventHandler<Event> value) { @Override
onFailedActionProperty().set(value); public String getName() {
return "onFailedAction";
} }
public final EventHandler<Event> getOnFailedAction() {
return onFailedActionProperty().get();
}
private final ObjectProperty<EventHandler<Event>> onFailedAction = new SimpleObjectProperty<EventHandler<Event>>(this, "onFailedAction") {
@Override @Override
protected void invalidated() { protected void invalidated() {
setEventHandler(FAILED_ACTION, get()); setEventHandler(FAILED_ACTION, get());
} }
}; };
}
return onFailedAction;
}
public final EventHandler<Event> getOnFailedAction() {
return onFailedAction != null ? onFailedAction.get() : null;
}
public final void setOnFailedAction(EventHandler<Event> value) {
onFailedActionProperty().set(value);
}
@Override @Override
protected SkinBase<SpinnerPane> createDefaultSkin() { protected SkinBase<SpinnerPane> createDefaultSkin() {
@@ -113,47 +185,58 @@ public class SpinnerPane extends Control {
} }
private static final class Skin extends SkinBase<SpinnerPane> { private static final class Skin extends SkinBase<SpinnerPane> {
private final JFXSpinner spinner = new JFXSpinner();
private final StackPane contentPane = new StackPane();
private final StackPane topPane = new StackPane();
private final TransitionPane root = new TransitionPane(); private final TransitionPane root = new TransitionPane();
private final StackPane failedPane = new StackPane();
private final Label failedReasonLabel = new Label();
@SuppressWarnings("FieldCanBeLocal") // prevent from gc.
private final InvalidationListener observer;
Skin(SpinnerPane control) { Skin(SpinnerPane control) {
super(control); super(control);
topPane.getChildren().setAll(spinner); updateContent();
topPane.getStyleClass().add("notice-pane"); this.getChildren().setAll(root);
}
private StackPane contentPane;
private StackPane spinnerPane;
private StackPane failedPane;
private Label failedReasonLabel;
void updateContent() {
SpinnerPane control = getSkinnable();
Node nextContent;
if (control.isLoading()) {
if (spinnerPane == null) {
spinnerPane = new StackPane(new JFXSpinner());
spinnerPane.getStyleClass().add("notice-pane");
}
nextContent = spinnerPane;
} else if (control.getFailedReason() != null) {
if (failedPane == null) {
failedReasonLabel = new Label();
failedPane = new StackPane(failedReasonLabel);
failedPane.getStyleClass().add("notice-pane"); failedPane.getStyleClass().add("notice-pane");
failedPane.getChildren().setAll(failedReasonLabel); FXUtils.onClicked(failedPane, () -> control.fireEvent(new Event(SpinnerPane.FAILED_ACTION)));
FXUtils.onClicked(failedPane, () -> { }
EventHandler<Event> action = control.getOnFailedAction(); failedReasonLabel.setText(control.getFailedReason());
if (action != null) nextContent = failedPane;
action.handle(new Event(FAILED_ACTION)); } else {
}); if (contentPane == null) {
contentPane = new StackPane();
}
FXUtils.onChangeAndOperate(getSkinnable().content, newValue -> { Node content = control.getContent();
if (newValue == null) { if (content != null)
contentPane.getChildren().setAll(content);
else
contentPane.getChildren().clear(); contentPane.getChildren().clear();
} else {
contentPane.getChildren().setAll(newValue);
}
});
getChildren().setAll(root);
observer = FXUtils.observeWeak(() -> { nextContent = contentPane;
if (getSkinnable().getFailedReason() != null) {
root.setContent(failedPane, ContainerAnimations.FADE);
failedReasonLabel.setText(getSkinnable().getFailedReason());
} else if (getSkinnable().isLoading()) {
root.setContent(topPane, ContainerAnimations.FADE);
} else {
root.setContent(contentPane, ContainerAnimations.FADE);
} }
}, getSkinnable().loadingProperty(), getSkinnable().failedReasonProperty());
if (nextContent != failedPane && failedReasonLabel != null) {
failedReasonLabel.setText(null);
}
root.setContent(nextContent, ContainerAnimations.FADE);
} }
} }