Compare commits
92
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c1fd8b1a4 | ||
|
|
77a86ef963 | ||
|
|
32a741e2de | ||
|
|
d1ead74177 | ||
|
|
80ba1ec1a7 | ||
|
|
25fef6b5c2 | ||
|
|
64ce1854f5 | ||
|
|
fb4f0be899 | ||
|
|
eea072cea5 | ||
|
|
b0cd184e22 | ||
|
|
8b3e801a44 | ||
|
|
04bb089f3a | ||
|
|
4bd0cbc88f | ||
|
|
9c4c7bd786 | ||
|
|
f79d560605 | ||
|
|
90e6782193 | ||
|
|
ca071fd924 | ||
|
|
2e26b14322 | ||
|
|
b7fbebae43 | ||
|
|
33dc39e59e | ||
|
|
a70b8d7baa | ||
|
|
64821600c5 | ||
|
|
dc993b871f | ||
|
|
a0bd64ad1f | ||
|
|
10df7a862b | ||
|
|
9ea6a97aef | ||
|
|
dec2e4ec7a | ||
|
|
8d504cd194 | ||
|
|
2918f19482 | ||
|
|
18f3e2870b | ||
|
|
9f2874c12b | ||
|
|
3fdf21fcd5 | ||
|
|
c9e435c129 | ||
|
|
26125f4002 | ||
|
|
93be558e84 | ||
|
|
e1be52f0f0 | ||
|
|
359a90636a | ||
|
|
d9b31ffd18 | ||
|
|
4b72639969 | ||
|
|
aecbf2e476 | ||
|
|
52d8c44320 | ||
|
|
5d57052ea2 | ||
|
|
570e1e2029 | ||
|
|
5b11202a9c | ||
|
|
c2d5d1b5f6 | ||
|
|
ef5849eba1 | ||
|
|
c2c542af3c | ||
|
|
792412b4c7 | ||
|
|
2b8e8306d7 | ||
|
|
84bcb01fe1 | ||
|
|
253bdf974e | ||
|
|
5839382c64 | ||
|
|
ab9c45549b | ||
|
|
cb413fa40a | ||
|
|
982c062a01 | ||
|
|
9f097149b2 | ||
|
|
cb50c89fb9 | ||
|
|
7fb2b9b734 | ||
|
|
b933fbcd6b | ||
|
|
c880e4befb | ||
|
|
575d908dba | ||
|
|
ecf188ad18 | ||
|
|
0b3cb9964a | ||
|
|
c175f1c1b3 | ||
|
|
dbacf096c6 | ||
|
|
c7b9b73292 | ||
|
|
19079ac123 | ||
|
|
2a66880a58 | ||
|
|
8b91251337 | ||
|
|
0c2742a895 | ||
|
|
a8692c3893 | ||
|
|
6cf757e440 | ||
|
|
c0f4143618 | ||
|
|
73b30974bf | ||
|
|
879f6cfeff | ||
|
|
0fc8222e63 | ||
|
|
5c2bb1cc25 | ||
|
|
c1d0bb973a | ||
|
|
c45cd74b67 | ||
|
|
05251cb4e3 | ||
|
|
ffbbde9d8f | ||
|
|
c75bcd97b2 | ||
|
|
0cc0b21d5f | ||
|
|
a524ba8506 | ||
|
|
17be25c036 | ||
|
|
5a518cec03 | ||
|
|
0adf4fcf32 | ||
|
|
c4ed6b065b | ||
|
|
0c4805df21 | ||
|
|
27972b987e | ||
|
|
fe11e5ff1e | ||
|
|
e3231859ea |
No files matched your search
@@ -60,6 +60,7 @@ dependencies {
|
||||
implementation(libs.twelvemonkeys.imageio.webp)
|
||||
implementation(libs.java.info)
|
||||
implementation(libs.monet.fx)
|
||||
implementation(libs.nayuki.qrcodegen)
|
||||
|
||||
if (launcherExe.isBlank()) {
|
||||
implementation(libs.hmclauncher)
|
||||
|
||||
@@ -127,6 +127,10 @@ public class JFXPopup extends PopupControl {
|
||||
/// @param initOffsetX on the x-axis
|
||||
/// @param initOffsetY on the y-axis
|
||||
public void show(Node node, PopupVPosition vAlign, PopupHPosition hAlign, double initOffsetX, double initOffsetY) {
|
||||
show(node, vAlign, hAlign, initOffsetX, initOffsetY, false);
|
||||
}
|
||||
|
||||
public void show(Node node, PopupVPosition vAlign, PopupHPosition hAlign, double initOffsetX, double initOffsetY, boolean attachToNode) {
|
||||
if (!isShowing()) {
|
||||
Scene scene = node.getScene();
|
||||
if (scene == null || scene.getWindow() == null) {
|
||||
@@ -137,10 +141,13 @@ public class JFXPopup extends PopupControl {
|
||||
|
||||
boolean isRTL = node.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT;
|
||||
|
||||
this.show(parent,
|
||||
parent.getX() + scene.getX() + origin.getX() + (hAlign == PopupHPosition.RIGHT ? ((Region) node).getWidth() : 0),
|
||||
parent.getY() + origin.getY() + scene.getY() + (vAlign == PopupVPosition.BOTTOM ? ((Region) node).getHeight() : 0)
|
||||
);
|
||||
double anchorX = parent.getX() + scene.getX() + origin.getX() + (hAlign == PopupHPosition.RIGHT ? ((Region) node).getWidth() : 0);
|
||||
double anchorY = parent.getY() + origin.getY() + scene.getY() + (vAlign == PopupVPosition.BOTTOM ? ((Region) node).getHeight() : 0);
|
||||
|
||||
if (attachToNode)
|
||||
this.show(node, anchorX, anchorY);
|
||||
else
|
||||
this.show(parent, anchorX, anchorY);
|
||||
|
||||
((JFXPopupSkin) getSkin()).reset(vAlign, isRTL ? hAlign.getOpposite() : hAlign, isRTL ? -initOffsetX : initOffsetX, initOffsetY);
|
||||
Platform.runLater(() -> ((JFXPopupSkin) getSkin()).animate());
|
||||
|
||||
@@ -0,0 +1,421 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.controls;
|
||||
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.WeakChangeListener;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.event.Event;
|
||||
import javafx.event.EventType;
|
||||
import javafx.geometry.Bounds;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/// "Snackbars provide brief messages about app processes at the bottom of the screen"
|
||||
/// (<a href="https://material.io/design/components/snackbars.html#">Material Design Guidelines</a>).
|
||||
///
|
||||
/// To show a snackbar you need to
|
||||
/// <ol>
|
||||
/// - Have a [Pane] (snackbarContainer) to show the snackbar on top of. Register it in
|
||||
/// [the JFXSnackbar constructor][#JFXSnackbar(Pane)] or using the [#registerSnackbarContainer(Pane)] method.
|
||||
/// - Have or create a [JFXSnackbar]. - Having one snackbar where you pass all your
|
||||
/// [SnackbarEvents][JFXSnackbar.SnackbarEvent] will ensure that the [enqueuemethod][JFXSnackbar#enqueue(SnackbarEvent)] works as intended.
|
||||
///
|
||||
/// - Have something to show in the snackbar. A [JFXSnackbarLayout] is nice and pretty,
|
||||
/// but any arbitrary [Node] will do.
|
||||
/// - Create a [SnackbarEvent][JFXSnackbar.SnackbarEvent] specifying the contents and the
|
||||
/// duration.
|
||||
/// </ol>
|
||||
///
|
||||
/// Finally, with all those things prepared, show your snackbar using
|
||||
/// [snackbar.enqueue(snackbarEvent);][JFXSnackbar#enqueue(SnackbarEvent)].
|
||||
///
|
||||
/// It's most convenient to create functions to do most of this (creating the layout and event) with the default
|
||||
/// settings; that way all you need to do to show a snackbar is specify the message or just the message and the duration.
|
||||
///
|
||||
/// @see <a href="https://material.io/design/components/snackbars.html#"> The Material Design Snackbar</a>
|
||||
public class JFXSnackbar extends Group {
|
||||
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-snackbar";
|
||||
|
||||
private Pane snackbarContainer;
|
||||
private final ChangeListener<? super Number> sizeListener = (o, oldVal, newVal) -> refreshPopup();
|
||||
private final WeakChangeListener<? super Number> weakSizeListener = new WeakChangeListener<>(sizeListener);
|
||||
|
||||
private final AtomicBoolean processingQueue = new AtomicBoolean(false);
|
||||
private final ConcurrentLinkedQueue<SnackbarEvent> eventQueue = new ConcurrentLinkedQueue<>();
|
||||
private final ConcurrentHashMap.KeySetView<Object, Boolean> eventsSet = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private final Pane content;
|
||||
private PseudoClass activePseudoClass = null;
|
||||
private PauseTransition pauseTransition;
|
||||
|
||||
/// This constructor assumes that you will eventually call the [#registerSnackbarContainer(Pane)] method before
|
||||
/// calling the [#enqueue(SnackbarEvent)] method. Otherwise, how will the snackbar know where to show itself?
|
||||
///
|
||||
///
|
||||
/// "Snackbars provide brief messages about app processes at the bottom of the screen"
|
||||
/// (<a href="https://material.io/design/components/snackbars.html#">Material Design Guidelines</a>).
|
||||
///
|
||||
/// To show a snackbar you need to
|
||||
///
|
||||
/// - Have a [Pane] (snackbarContainer) to show the snackbar on top of. Register it in
|
||||
/// [the JFXSnackbar constructor][#JFXSnackbar(Pane)] or using the [#registerSnackbarContainer(Pane)] method.
|
||||
/// - Have or create a [JFXSnackbar]. - Having one snackbar where you pass all your
|
||||
/// [SnackbarEvents][JFXSnackbar.SnackbarEvent] will ensure that the [enqueuemethod][JFXSnackbar#enqueue(SnackbarEvent)] works as intended.
|
||||
/// - Have something to show in the snackbar. A [JFXSnackbarLayout] is nice and pretty,
|
||||
/// but any arbitrary [Node] will do.
|
||||
/// - Create a [SnackbarEvent][JFXSnackbar.SnackbarEvent] specifying the contents and the
|
||||
/// duration.
|
||||
///
|
||||
/// Finally, with all those things prepared, show your snackbar using
|
||||
/// [snackbar.enqueue(snackbarEvent);][JFXSnackbar#enqueue(SnackbarEvent)].
|
||||
///
|
||||
public JFXSnackbar() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
/// "Snackbars provide brief messages about app processes at the bottom of the screen"
|
||||
/// (<a href="https://material.io/design/components/snackbars.html#">Material Design Guidelines</a>).
|
||||
///
|
||||
/// To show a snackbar you need to
|
||||
///
|
||||
/// - Have a [Pane] (snackbarContainer) to show the snackbar on top of. Register it in
|
||||
/// [the JFXSnackbar constructor][#JFXSnackbar(Pane)] or using the [#registerSnackbarContainer(Pane)] method.
|
||||
/// - Have or create a [JFXSnackbar]. - Having one snackbar where you pass all your
|
||||
/// [SnackbarEvents][JFXSnackbar.SnackbarEvent] will ensure that the [enqueuemethod][JFXSnackbar#enqueue(SnackbarEvent)] works as intended.
|
||||
/// - Have something to show in the snackbar. A [JFXSnackbarLayout] is nice and pretty,
|
||||
/// but any arbitrary [Node] will do.
|
||||
/// - Create a [SnackbarEvent][JFXSnackbar.SnackbarEvent] specifying the contents and the
|
||||
/// duration.
|
||||
///
|
||||
/// Finally, with all those things prepared, show your snackbar using
|
||||
/// [snackbar.enqueue(snackbarEvent);][JFXSnackbar#enqueue(SnackbarEvent)].
|
||||
///
|
||||
/// @param snackbarContainer where the snackbar will appear. Using a single snackbar instead of many, will ensure that
|
||||
/// the [#enqueue(SnackbarEvent)] method works correctly.
|
||||
public JFXSnackbar(Pane snackbarContainer) {
|
||||
initialize();
|
||||
content = new StackPane();
|
||||
content.getStyleClass().add("jfx-snackbar-content");
|
||||
//wrap the content in a group so that the content is managed inside its own container
|
||||
//but the group is not managed in the snackbarContainer so it does not affect any layout calculations
|
||||
getChildren().add(content);
|
||||
setManaged(false);
|
||||
setVisible(false);
|
||||
|
||||
// register the container before resizing it
|
||||
registerSnackbarContainer(snackbarContainer);
|
||||
|
||||
// resize the popup if its layout has been changed
|
||||
layoutBoundsProperty().addListener((o, oldVal, newVal) -> refreshPopup());
|
||||
|
||||
addEventHandler(SnackbarEvent.SNACKBAR, this::enqueue);
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
}
|
||||
|
||||
// Setters / Getters
|
||||
|
||||
public Pane getPopupContainer() {
|
||||
return snackbarContainer;
|
||||
}
|
||||
|
||||
public void setPrefWidth(double width) {
|
||||
content.setPrefWidth(width);
|
||||
}
|
||||
|
||||
public double getPrefWidth() {
|
||||
return content.getPrefWidth();
|
||||
}
|
||||
|
||||
// Public API
|
||||
|
||||
public void registerSnackbarContainer(Pane snackbarContainer) {
|
||||
if (snackbarContainer != null) {
|
||||
if (this.snackbarContainer != null) {
|
||||
//since listeners are added the container should be properly registered/unregistered
|
||||
throw new IllegalArgumentException("Snackbar Container already set");
|
||||
}
|
||||
this.snackbarContainer = snackbarContainer;
|
||||
this.snackbarContainer.getChildren().add(this);
|
||||
this.snackbarContainer.heightProperty().addListener(weakSizeListener);
|
||||
this.snackbarContainer.widthProperty().addListener(weakSizeListener);
|
||||
}
|
||||
}
|
||||
|
||||
public void unregisterSnackbarContainer(Pane snackbarContainer) {
|
||||
if (snackbarContainer != null) {
|
||||
if (this.snackbarContainer == null) {
|
||||
throw new IllegalArgumentException("Snackbar Container not set");
|
||||
}
|
||||
this.snackbarContainer.getChildren().remove(this);
|
||||
this.snackbarContainer.heightProperty().removeListener(weakSizeListener);
|
||||
this.snackbarContainer.widthProperty().removeListener(weakSizeListener);
|
||||
this.snackbarContainer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void show(SnackbarEvent event) {
|
||||
content.getChildren().setAll(event.getContent());
|
||||
openAnimation = getTimeline(event.getTimeout());
|
||||
if (event.getPseudoClass() != null) {
|
||||
activePseudoClass = event.getPseudoClass();
|
||||
content.pseudoClassStateChanged(activePseudoClass, true);
|
||||
}
|
||||
openAnimation.play();
|
||||
}
|
||||
|
||||
private Timeline openAnimation = null;
|
||||
|
||||
private Timeline getTimeline(Duration timeout) {
|
||||
Timeline animation;
|
||||
animation = new Timeline(
|
||||
new KeyFrame(
|
||||
Duration.ZERO,
|
||||
e -> {
|
||||
this.toBack();
|
||||
this.setVisible(false);
|
||||
},
|
||||
new KeyValue(this.translateYProperty(), this.getLayoutBounds().getHeight(), Motion.EASE),
|
||||
new KeyValue(this.opacityProperty(), 0, Motion.EASE)
|
||||
),
|
||||
new KeyFrame(
|
||||
Duration.millis(10),
|
||||
e -> {
|
||||
this.toFront();
|
||||
this.setVisible(true);
|
||||
}
|
||||
),
|
||||
new KeyFrame(Duration.millis(300),
|
||||
new KeyValue(this.opacityProperty(), 1, Motion.EASE),
|
||||
new KeyValue(this.translateYProperty(), 0, Motion.EASE)
|
||||
)
|
||||
);
|
||||
animation.setCycleCount(1);
|
||||
pauseTransition = Duration.INDEFINITE.equals(timeout) ? null : new PauseTransition(timeout);
|
||||
if (pauseTransition != null) {
|
||||
animation.setOnFinished(finish -> {
|
||||
pauseTransition.setOnFinished(done -> {
|
||||
pauseTransition = null;
|
||||
eventsSet.remove(currentEvent);
|
||||
currentEvent = eventQueue.peek();
|
||||
close();
|
||||
});
|
||||
pauseTransition.play();
|
||||
});
|
||||
}
|
||||
return animation;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (openAnimation != null) {
|
||||
openAnimation.stop();
|
||||
}
|
||||
if (this.isVisible()) {
|
||||
Timeline closeAnimation = new Timeline(
|
||||
new KeyFrame(
|
||||
Duration.ZERO,
|
||||
e -> this.toFront(),
|
||||
new KeyValue(this.opacityProperty(), 1, Motion.EASE),
|
||||
new KeyValue(this.translateYProperty(), 0, Motion.EASE)
|
||||
),
|
||||
new KeyFrame(
|
||||
Duration.millis(290),
|
||||
e -> this.setVisible(true)
|
||||
),
|
||||
new KeyFrame(Duration.millis(300),
|
||||
e -> {
|
||||
this.toBack();
|
||||
this.setVisible(false);
|
||||
},
|
||||
new KeyValue(this.translateYProperty(), this.getLayoutBounds().getHeight(), Motion.EASE),
|
||||
new KeyValue(this.opacityProperty(), 0, Motion.EASE)
|
||||
)
|
||||
);
|
||||
closeAnimation.setCycleCount(1);
|
||||
closeAnimation.setOnFinished(e -> {
|
||||
resetPseudoClass();
|
||||
processSnackbar();
|
||||
});
|
||||
closeAnimation.play();
|
||||
}
|
||||
}
|
||||
|
||||
private SnackbarEvent currentEvent = null;
|
||||
|
||||
public SnackbarEvent getCurrentEvent() {
|
||||
return currentEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows {@link SnackbarEvent SnackbarEvents} one by one. The next event will be shown after the current event's duration.
|
||||
*
|
||||
* @param event the {@link SnackbarEvent event} to put in the queue.
|
||||
*/
|
||||
public void enqueue(SnackbarEvent event) {
|
||||
synchronized (this) {
|
||||
if (!eventsSet.contains(event)) {
|
||||
eventsSet.add(event);
|
||||
eventQueue.offer(event);
|
||||
} else if (currentEvent == event && pauseTransition != null) {
|
||||
pauseTransition.playFromStart();
|
||||
}
|
||||
}
|
||||
if (processingQueue.compareAndSet(false, true)) {
|
||||
Platform.runLater(() -> {
|
||||
currentEvent = eventQueue.poll();
|
||||
if (currentEvent != null) {
|
||||
show(currentEvent);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void resetPseudoClass() {
|
||||
if (activePseudoClass != null) {
|
||||
content.pseudoClassStateChanged(activePseudoClass, false);
|
||||
activePseudoClass = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void processSnackbar() {
|
||||
currentEvent = eventQueue.poll();
|
||||
if (currentEvent != null) {
|
||||
eventsSet.remove(currentEvent);
|
||||
show(currentEvent);
|
||||
} else {
|
||||
//The enqueue method and this listener should be executed sequentially on the FX Thread so there
|
||||
//should not be a race condition
|
||||
processingQueue.getAndSet(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshPopup() {
|
||||
if (snackbarContainer == null) {
|
||||
return;
|
||||
}
|
||||
Bounds contentBound = this.getLayoutBounds();
|
||||
double offsetX = Math.ceil(snackbarContainer.getWidth() / 2) - Math.ceil(contentBound.getWidth() / 2);
|
||||
double offsetY = snackbarContainer.getHeight() - contentBound.getHeight();
|
||||
this.setLayoutX(offsetX);
|
||||
this.setLayoutY(offsetY);
|
||||
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Event API
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/// Specifies _what_ and _how long_ to show a [JFXSnackbar].
|
||||
///
|
||||
/// The _what_ can be any arbitrary [Node]; the [JFXSnackbarLayout] is a great choice.
|
||||
///
|
||||
/// The _how long_ is specified in the form of a [javafx.util.Duration][javafx.util.Duration], not to be
|
||||
/// confused with the [java.time.Duration].
|
||||
public static class SnackbarEvent extends Event {
|
||||
|
||||
public static final EventType<SnackbarEvent> SNACKBAR = new EventType<>(Event.ANY, "SNACKBAR");
|
||||
|
||||
/// The amount of time the snackbar will show for, if not otherwise specified.
|
||||
///
|
||||
/// It's 1.5 seconds.
|
||||
public static Duration DEFAULT_DURATION = Duration.seconds(1.5);
|
||||
|
||||
private final Node content;
|
||||
private final PseudoClass pseudoClass;
|
||||
private final Duration timeout;
|
||||
|
||||
/// Creates a [SnackbarEvent] with the [default duration][#DEFAULT_DURATION] and no pseudoClass.
|
||||
///
|
||||
/// @param content what you want shown in the snackbar; a [JFXSnackbarLayout] is a great choice.
|
||||
public SnackbarEvent(Node content) {
|
||||
this(content, DEFAULT_DURATION, null);
|
||||
}
|
||||
|
||||
/// Creates a [SnackbarEvent] with the [default duration][#DEFAULT_DURATION]; you specify the contents and
|
||||
/// pseudoClass.
|
||||
///
|
||||
/// @param content what you want shown in the snackbar; a [JFXSnackbarLayout] is a great choice.
|
||||
public SnackbarEvent(Node content, PseudoClass pseudoClass) {
|
||||
this(content, DEFAULT_DURATION, pseudoClass);
|
||||
}
|
||||
|
||||
/// Creates a SnackbarEvent with no pseudoClass; you specify the contents and duration.
|
||||
/// pseudoClass.
|
||||
///
|
||||
/// @param content what you want shown in the snackbar; a [JFXSnackbarLayout] is a great choice.
|
||||
/// @param timeout the amount of time you want the snackbar to show for.
|
||||
public SnackbarEvent(Node content, Duration timeout) {
|
||||
this(content, timeout, null);
|
||||
}
|
||||
|
||||
/// Creates a SnackbarEvent; you specify the contents, duration and pseudoClass.
|
||||
///
|
||||
/// If you don't need so much customization, try one of the other constructors.
|
||||
///
|
||||
/// @param content what you want shown in the snackbar; a [JFXSnackbarLayout] is a great choice.
|
||||
/// @param timeout the amount of time you want the snackbar to show for.
|
||||
public SnackbarEvent(Node content, Duration timeout, PseudoClass pseudoClass) {
|
||||
super(SNACKBAR);
|
||||
this.content = content;
|
||||
this.pseudoClass = pseudoClass;
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
public Node getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public PseudoClass getPseudoClass() {
|
||||
return pseudoClass;
|
||||
}
|
||||
|
||||
public Duration getTimeout() {
|
||||
return timeout;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public EventType<? extends SnackbarEvent> getEventType() {
|
||||
return (EventType<? extends SnackbarEvent>) super.getEventType();
|
||||
}
|
||||
|
||||
public boolean isPersistent() {
|
||||
return Duration.INDEFINITE.equals(getTimeout());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.controls;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
|
||||
/// JFXSnackbarLayout default layout for snackbar content
|
||||
///
|
||||
/// @author Shadi Shaheen
|
||||
/// @version 1.0
|
||||
/// @since 2018-11-16
|
||||
public class JFXSnackbarLayout extends BorderPane {
|
||||
|
||||
private final Label toast;
|
||||
private JFXButton action;
|
||||
private final StackPane actionContainer;
|
||||
|
||||
public JFXSnackbarLayout(String message) {
|
||||
this(message, null, null);
|
||||
}
|
||||
|
||||
public JFXSnackbarLayout(String message, String actionText, EventHandler<ActionEvent> actionHandler) {
|
||||
initialize();
|
||||
|
||||
toast = new Label();
|
||||
toast.setMinWidth(Control.USE_PREF_SIZE);
|
||||
toast.getStyleClass().add("jfx-snackbar-toast");
|
||||
toast.setWrapText(true);
|
||||
toast.setText(message);
|
||||
StackPane toastContainer = new StackPane(toast);
|
||||
toastContainer.setPadding(new Insets(20));
|
||||
actionContainer = new StackPane();
|
||||
actionContainer.setPadding(new Insets(0, 10, 0, 0));
|
||||
|
||||
toast.prefWidthProperty().bind(Bindings.createDoubleBinding(() -> {
|
||||
if (getPrefWidth() == -1) {
|
||||
return getPrefWidth();
|
||||
}
|
||||
double actionWidth = actionContainer.isVisible() ? actionContainer.getWidth() : 0.0;
|
||||
return prefWidthProperty().get() - actionWidth;
|
||||
}, prefWidthProperty(), actionContainer.widthProperty(), actionContainer.visibleProperty()));
|
||||
|
||||
setLeft(toastContainer);
|
||||
setRight(actionContainer);
|
||||
|
||||
if (actionText != null) {
|
||||
action = new JFXButton();
|
||||
action.setText(actionText);
|
||||
action.setOnAction(actionHandler);
|
||||
action.setMinWidth(Control.USE_PREF_SIZE);
|
||||
action.setButtonType(JFXButton.ButtonType.FLAT);
|
||||
action.getStyleClass().add("jfx-snackbar-action");
|
||||
// actions will be added upon showing the snackbar if needed
|
||||
actionContainer.getChildren().add(action);
|
||||
|
||||
if (!actionText.isEmpty()) {
|
||||
action.setVisible(true);
|
||||
actionContainer.setVisible(true);
|
||||
actionContainer.setManaged(true);
|
||||
// to force updating the layout bounds
|
||||
action.setText("");
|
||||
action.setText(actionText);
|
||||
action.setOnAction(actionHandler);
|
||||
} else {
|
||||
actionContainer.setVisible(false);
|
||||
actionContainer.setManaged(false);
|
||||
action.setVisible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-snackbar-layout";
|
||||
|
||||
public String getToast() {
|
||||
return toast.getText();
|
||||
}
|
||||
|
||||
public void setToast(String toast) {
|
||||
this.toast.setText(toast);
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,14 +34,17 @@ import javafx.geometry.Insets;
|
||||
import javafx.geometry.Rectangle2D;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Tab;
|
||||
import javafx.scene.control.TextFormatter;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.*;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.setting.StyleSheets;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author Shadi Shaheen
|
||||
@@ -95,14 +98,17 @@ public class JFXCustomColorPickerDialog extends StackPane {
|
||||
|
||||
rgbField.getStyleClass().add("custom-color-field");
|
||||
rgbField.setPromptText("RGB Color");
|
||||
rgbField.setTextFormatter(colorCharFormatter());
|
||||
rgbField.textProperty().addListener((o, oldVal, newVal) -> updateColorFromUserInput(newVal));
|
||||
|
||||
hsbField.getStyleClass().add("custom-color-field");
|
||||
hsbField.setPromptText("HSB Color");
|
||||
hsbField.setTextFormatter(colorCharFormatter());
|
||||
hsbField.textProperty().addListener((o, oldVal, newVal) -> updateColorFromUserInput(newVal));
|
||||
|
||||
hexField.getStyleClass().add("custom-color-field");
|
||||
hexField.setPromptText("#HEX Color");
|
||||
hexField.setTextFormatter(colorCharFormatter());
|
||||
hexField.textProperty().addListener((o, oldVal, newVal) -> updateColorFromUserInput(newVal));
|
||||
|
||||
StackPane tabContent = new StackPane();
|
||||
@@ -404,4 +410,20 @@ public class JFXCustomColorPickerDialog extends StackPane {
|
||||
dialog.setMinWidth(minWidth);
|
||||
dialog.setMinHeight(minHeight);
|
||||
}
|
||||
|
||||
private static final Pattern COLOR_CHAR_PATTERN = Pattern.compile("[0-9a-zA-Z#(),%.\\s]*");
|
||||
|
||||
private static TextFormatter<String> colorCharFormatter() {
|
||||
return new TextFormatter<>(change -> {
|
||||
if (!change.isContentChange()) return change;
|
||||
|
||||
String ins = StringUtils.toHalfWidth(change.getText());
|
||||
if (!COLOR_CHAR_PATTERN.matcher(ins).matches()) return null;
|
||||
String full = StringUtils.toHalfWidth(change.getControlNewText());
|
||||
long h = full.chars().filter(c -> c == '#').count();
|
||||
if (h > 1 || (h == 1 && full.indexOf('#') != 0)) return null;
|
||||
change.setText(ins);
|
||||
return change;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.OAuth;
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
import org.jackhuang.hmcl.event.EventManager;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
@@ -37,8 +37,8 @@ import java.util.concurrent.ExecutionException;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||
private final int port;
|
||||
@@ -104,8 +104,11 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||
String html;
|
||||
try {
|
||||
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"))
|
||||
.replace("%style%", Themes.getTheme().toColorScheme().toStyleSheet().replace("-monet", "--monet"))
|
||||
.replace("%lang%", Locale.getDefault().toLanguageTag())
|
||||
.replace("%close-page%", i18n("account.methods.microsoft.close_page"));
|
||||
.replace("%success%", i18n("message.success"))
|
||||
.replace("%ok%", i18n("button.ok"))
|
||||
.replace("%close_page%", i18n("account.methods.microsoft.close_page"));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to load html", e);
|
||||
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_HTML, "");
|
||||
@@ -123,7 +126,8 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||
|
||||
public static class Factory implements OAuth.Callback {
|
||||
public final EventManager<GrantDeviceCodeEvent> onGrantDeviceCode = new EventManager<>();
|
||||
public final EventManager<OpenBrowserEvent> onOpenBrowser = new EventManager<>();
|
||||
public final EventManager<OpenBrowserEvent> onOpenBrowserAuthorizationCode = new EventManager<>();
|
||||
public final EventManager<OpenBrowserEvent> onOpenBrowserDevice = new EventManager<>();
|
||||
|
||||
@Override
|
||||
public OAuth.Session startServer() throws IOException, AuthenticationException {
|
||||
@@ -150,17 +154,13 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void openBrowser(String url) throws IOException {
|
||||
public void openBrowser(OAuth.GrantFlow grantFlow, String url) throws IOException {
|
||||
lastlyOpenedURL = url;
|
||||
|
||||
try {
|
||||
Thread.sleep(1500);
|
||||
} catch (InterruptedException ignored) {
|
||||
switch (grantFlow) {
|
||||
case AUTHORIZATION_CODE -> onOpenBrowserAuthorizationCode.fireEvent(new OpenBrowserEvent(this, url));
|
||||
case DEVICE -> onOpenBrowserDevice.fireEvent(new OpenBrowserEvent(this, url));
|
||||
}
|
||||
|
||||
FXUtils.openLink(url);
|
||||
|
||||
onOpenBrowser.fireEvent(new OpenBrowserEvent(this, url));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.FXThread;
|
||||
import org.jackhuang.hmcl.util.InvocationDispatcher;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
@@ -44,13 +45,15 @@ import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@@ -310,11 +313,18 @@ public final class TerracottaManager {
|
||||
public static TerracottaState.HostScanning setScanning() {
|
||||
TerracottaState state = STATE_V.get();
|
||||
if (state instanceof TerracottaState.PortSpecific portSpecific) {
|
||||
String uri = NetworkUtils.withQuery(String.format("http://127.0.0.1:%d/state/scanning", portSpecific.port), Map.of(
|
||||
"player", getPlayerName()
|
||||
));
|
||||
Task.supplyAsync(Schedulers.io(), TerracottaNodeList::fetch)
|
||||
.thenComposeAsync(nodes -> {
|
||||
List<Pair<String, String>> query = new ArrayList<>(nodes.size() + 1);
|
||||
query.add(pair("player", getPlayerName()));
|
||||
for (URI node : nodes) {
|
||||
query.add(pair("public_nodes", node.toString()));
|
||||
}
|
||||
return new GetTask(NetworkUtils.withQuery(
|
||||
"http://127.0.0.1:%d/state/scanning".formatted(portSpecific.port), query
|
||||
)).setSignificance(Task.TaskSignificance.MINOR);
|
||||
}).start();
|
||||
|
||||
new GetTask(uri).setSignificance(Task.TaskSignificance.MINOR).start();
|
||||
return new TerracottaState.HostScanning(-1, -1, null);
|
||||
}
|
||||
return null;
|
||||
@@ -323,15 +333,19 @@ public final class TerracottaManager {
|
||||
public static Task<TerracottaState.GuestConnecting> setGuesting(String room) {
|
||||
TerracottaState state = STATE_V.get();
|
||||
if (state instanceof TerracottaState.PortSpecific portSpecific) {
|
||||
String uri = NetworkUtils.withQuery(String.format("http://127.0.0.1:%d/state/guesting", portSpecific.port), Map.of(
|
||||
"room", room,
|
||||
"player", getPlayerName()
|
||||
));
|
||||
|
||||
return new GetTask(uri)
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.thenSupplyAsync(() -> new TerracottaState.GuestConnecting(-1, -1, null))
|
||||
.setSignificance(Task.TaskSignificance.MINOR);
|
||||
return Task.supplyAsync(Schedulers.io(), TerracottaNodeList::fetch)
|
||||
.thenComposeAsync(nodes -> {
|
||||
ArrayList<Pair<String, String>> query = new ArrayList<>(nodes.size() + 2);
|
||||
query.add(pair("room", room));
|
||||
query.add(pair("player", getPlayerName()));
|
||||
for (URI node : nodes) {
|
||||
query.add(pair("public_nodes", node.toString()));
|
||||
}
|
||||
return new GetTask(NetworkUtils.withQuery("http://127.0.0.1:%d/state/guesting".formatted(portSpecific.port), query))
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.thenSupplyAsync(() -> new TerracottaState.GuestConnecting(-1, -1, null))
|
||||
.setSignificance(Task.TaskSignificance.MINOR);
|
||||
});
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.terracotta;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSerializable;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.List;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/// @author Glavo
|
||||
public final class TerracottaNodeList {
|
||||
private static final String NODE_LIST_URL = "https://terracotta.glavo.site/nodes";
|
||||
|
||||
@JsonSerializable
|
||||
private record TerracottaNode(String url, @Nullable String region) implements Validation {
|
||||
@Override
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
Validation.requireNonNull(url, "TerracottaNode.url cannot be null");
|
||||
try {
|
||||
new URI(url);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new JsonParseException("Invalid URL: " + url, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static volatile List<URI> list;
|
||||
|
||||
public static List<URI> fetch() {
|
||||
List<URI> list = TerracottaNodeList.list;
|
||||
if (list != null)
|
||||
return list;
|
||||
|
||||
synchronized (TerracottaNodeList.class) {
|
||||
list = TerracottaNodeList.list;
|
||||
if (list != null)
|
||||
return list;
|
||||
|
||||
try {
|
||||
List<TerracottaNode> nodes = HttpRequest.GET(NODE_LIST_URL)
|
||||
.getJson(JsonUtils.listTypeOf(TerracottaNode.class));
|
||||
|
||||
if (nodes == null) {
|
||||
list = List.of();
|
||||
LOG.info("No available Terracotta nodes found");
|
||||
} else {
|
||||
list = nodes.stream()
|
||||
.filter(node -> {
|
||||
if (node == null)
|
||||
return false;
|
||||
|
||||
try {
|
||||
node.validate();
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Invalid terracotta node: " + node, e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return StringUtils.isBlank(node.region) || LocaleUtils.IS_CHINA_MAINLAND == "CN".equalsIgnoreCase(node.region);
|
||||
})
|
||||
.map(it -> URI.create(it.url()))
|
||||
.toList();
|
||||
LOG.info("Terracotta node list: " + list);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to fetch terracotta node list", e);
|
||||
list = List.of();
|
||||
}
|
||||
|
||||
TerracottaNodeList.list = list;
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
private TerracottaNodeList() {
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import org.glavo.monetfx.beans.property.ColorSchemeProperty;
|
||||
import org.glavo.monetfx.beans.property.ReadOnlyColorSchemeProperty;
|
||||
import org.glavo.monetfx.beans.property.SimpleColorSchemeProperty;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.ui.WindowsNativeUtils;
|
||||
import org.jackhuang.hmcl.util.platform.NativeUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OSVersion;
|
||||
@@ -46,9 +47,12 @@ import org.jackhuang.hmcl.util.platform.windows.WinConstants;
|
||||
import org.jackhuang.hmcl.util.platform.windows.WinReg;
|
||||
import org.jackhuang.hmcl.util.platform.windows.WinTypes;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/// @author Glavo
|
||||
public final class Themes {
|
||||
@@ -113,6 +117,7 @@ public final class Themes {
|
||||
if (defaultBrightness != null)
|
||||
return defaultBrightness;
|
||||
|
||||
LOG.info("Detecting system theme brightness");
|
||||
Brightness brightness = Brightness.DEFAULT;
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
WinReg reg = WinReg.INSTANCE;
|
||||
@@ -130,7 +135,38 @@ public final class Themes {
|
||||
// If the key does not exist, it means Light mode is used
|
||||
brightness = Brightness.LIGHT;
|
||||
}
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
|
||||
Path dbusSend = SystemUtils.which("dbus-send");
|
||||
if (dbusSend != null) {
|
||||
try {
|
||||
String[] result = SystemUtils.run(List.of(
|
||||
FileUtils.getAbsolutePath(dbusSend),
|
||||
"--session",
|
||||
"--print-reply=literal",
|
||||
"--reply-timeout=1000",
|
||||
"--dest=org.freedesktop.portal.Desktop",
|
||||
"/org/freedesktop/portal/desktop",
|
||||
"org.freedesktop.portal.Settings.Read",
|
||||
"string:org.freedesktop.appearance",
|
||||
"string:color-scheme"
|
||||
), Duration.ofSeconds(2)).trim().split(" ");
|
||||
|
||||
if (result.length > 0) {
|
||||
String value = result[result.length - 1];
|
||||
// 1: prefer dark
|
||||
// 2: prefer light
|
||||
if ("1".equals(value)) {
|
||||
brightness = Brightness.DARK;
|
||||
} else if ("2".equals(value)) {
|
||||
brightness = Brightness.LIGHT;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to get system theme from D-Bus", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
LOG.info("Detected system theme brightness: " + brightness);
|
||||
|
||||
return defaultBrightness = brightness;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ package org.jackhuang.hmcl.ui;
|
||||
|
||||
import org.jackhuang.hmcl.auth.*;
|
||||
import org.jackhuang.hmcl.ui.account.ClassicAccountLoginDialog;
|
||||
import org.jackhuang.hmcl.ui.account.OAuthAccountLoginDialog;
|
||||
import org.jackhuang.hmcl.ui.account.MicrosoftAccountLoginPane;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CancellationException;
|
||||
@@ -49,10 +49,10 @@ public final class DialogController {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
AtomicReference<AuthInfo> res = new AtomicReference<>(null);
|
||||
runInFX(() -> {
|
||||
OAuthAccountLoginDialog pane = new OAuthAccountLoginDialog((OAuthAccount) account, it -> {
|
||||
MicrosoftAccountLoginPane pane = new MicrosoftAccountLoginPane(account, it -> {
|
||||
res.set(it);
|
||||
latch.countDown();
|
||||
}, latch::countDown);
|
||||
}, latch::countDown, false);
|
||||
Controllers.dialog(pane);
|
||||
});
|
||||
latch.await();
|
||||
|
||||
@@ -312,14 +312,6 @@ public final class FXUtils {
|
||||
});
|
||||
}
|
||||
|
||||
public static Node wrap(Node node) {
|
||||
return limitingSize(node, 30, 20);
|
||||
}
|
||||
|
||||
public static Node wrap(SVG svg) {
|
||||
return wrap(svg.createIcon(20));
|
||||
}
|
||||
|
||||
private static class ListenerPair<T> {
|
||||
private final ObservableValue<T> value;
|
||||
private final ChangeListener<? super T> listener;
|
||||
@@ -1299,8 +1291,8 @@ public final class FXUtils {
|
||||
return button;
|
||||
}
|
||||
|
||||
public static Label newSafeTruncatedLabel(String text) {
|
||||
Label label = new Label(text);
|
||||
public static Label newSafeTruncatedLabel() {
|
||||
Label label = new Label();
|
||||
label.setTextOverrun(OverrunStyle.CENTER_WORD_ELLIPSIS);
|
||||
showTooltipWhenTruncated(label);
|
||||
return label;
|
||||
|
||||
@@ -194,8 +194,9 @@ public class InstallerItem extends Control {
|
||||
mutualIncompatible(incompatibleMap, forge, fabric, quilt, neoForge, cleanroom, legacyfabric);
|
||||
addIncompatibles(incompatibleMap, liteLoader, fabric, quilt, neoForge, cleanroom, legacyfabric);
|
||||
addIncompatibles(incompatibleMap, optiFine, fabric, quilt, neoForge, cleanroom, liteLoader, legacyfabric);
|
||||
addIncompatibles(incompatibleMap, fabricApi, forge, quiltApi, neoForge, liteLoader, optiFine, cleanroom, legacyfabricApi, legacyfabricApi);
|
||||
addIncompatibles(incompatibleMap, fabricApi, forge, quiltApi, neoForge, liteLoader, optiFine, cleanroom, legacyfabric, legacyfabricApi);
|
||||
addIncompatibles(incompatibleMap, quiltApi, forge, fabric, fabricApi, neoForge, liteLoader, optiFine, cleanroom, legacyfabric, legacyfabricApi);
|
||||
addIncompatibles(incompatibleMap, legacyfabricApi, forge, fabric, fabricApi, neoForge, liteLoader, optiFine, cleanroom, quilt, quiltApi);
|
||||
|
||||
for (Map.Entry<InstallerItem, Set<InstallerItem>> entry : incompatibleMap.entrySet()) {
|
||||
InstallerItem item = entry.getKey();
|
||||
@@ -342,6 +343,7 @@ public class InstallerItem extends Control {
|
||||
BorderPane.setAlignment(statusLabel, Pos.CENTER_LEFT);
|
||||
|
||||
HBox buttonsContainer = new HBox();
|
||||
buttonsContainer.setPickOnBounds(false);
|
||||
buttonsContainer.setSpacing(8);
|
||||
buttonsContainer.setAlignment(Pos.CENTER);
|
||||
pane.getChildren().add(buttonsContainer);
|
||||
|
||||
@@ -18,22 +18,17 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.SVGPath;
|
||||
|
||||
/**
|
||||
* All vector icons used in the launcher.
|
||||
* <p>
|
||||
* Unless otherwise stated,
|
||||
* these icons are from <a href="https://fonts.google.com/icons?icon.size=24&icon.color=%235f6368&icon.query=list&icon.set=Material+Symbols">Material Symbols</a>,
|
||||
* with a style of outlined, a weight of 400, a grade of 0, and an optical size of 24 px.
|
||||
* The view boxes of all icons are normalized to {@code 0 0 24 24}.
|
||||
*/
|
||||
/// All vector icons used in the launcher.
|
||||
///
|
||||
/// Unless otherwise stated,
|
||||
/// these icons are from <a href="https://fonts.google.com/icons?icon.size=24&icon.color=%235f6368&icon.query=list&icon.set=Material+Symbols">Material Symbols</a>,
|
||||
/// with a style of outlined, a weight of 400, a grade of 0, and an optical size of 24 px.
|
||||
/// The view boxes of all icons are normalized to `0 0 24 24`.
|
||||
public enum SVG {
|
||||
NONE(""), // Empty Icon
|
||||
ADD("M11 13H5V11H11V5H13V11H19V13H13V19H11V13Z"),
|
||||
ADD_CIRCLE("M11 17H13V13H17V11H13V7H11V11H7V13H11V17ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
|
||||
ALPHA_CIRCLE("M11,7H13A2,2 0 0,1 15,9V17H13V13H11V17H9V9A2,2 0 0,1 11,7M11,9V11H13V9H11M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z"), // Not Material
|
||||
@@ -50,9 +45,9 @@ public enum SVG {
|
||||
CHECKROOM("M3 20Q2.575 20 2.2875 19.7125T2 19Q2 18.75 2.1 18.5375T2.4 18.2L11 11.75V10Q11 9.575 11.3 9.2875T12.025 9Q12.65 9 13.075 8.55T13.5 7.475Q13.5 6.85 13.0625 6.425T12 6Q11.375 6 10.9375 6.4375T10.5 7.5H8.5Q8.5 6.05 9.525 5.025T12 4Q13.45 4 14.475 5.0125T15.5 7.475Q15.5 8.65 14.8125 9.575T13 10.85V11.75L21.6 18.2Q21.8 18.325 21.9 18.5375T22 19Q22 19.425 21.7125 19.7125T21 20H3ZM6 18H18L12 13.5 6 18Z"),
|
||||
CHECK_CIRCLE("M10.6 16.6 17.65 9.55 16.25 8.15 10.6 13.8 7.75 10.95 6.35 12.35 10.6 16.6ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
|
||||
CLOSE("M6.4 19 5 17.6 10.6 12 5 6.4 6.4 5 12 10.6 17.6 5 19 6.4 13.4 12 19 17.6 17.6 19 12 13.4 6.4 19Z"),
|
||||
CONTENT_CUT("m12 14l-2.35 2.35q.2.375.275.8T10 18q0 1.65-1.175 2.825T6 22t-2.825-1.175T2 18t1.175-2.825T6 14q.425 0 .85.075t.8.275L10 12L7.65 9.65q-.375.2-.8.275T6 10q-1.65 0-2.825-1.175T2 6t1.175-2.825T6 2t2.825 1.175T10 6q0 .425-.075.85t-.275.8L20.6 18.6q.675.675.3 1.538T19.575 21q-.275 0-.537-.112t-.463-.313zm3-3l-2-2l5.575-5.575q.2-.2.463-.312T19.574 3q.95 0 1.313.875t-.313 1.55zM6 8q.825 0 1.413-.587T8 6t-.587-1.412T6 4t-1.412.588T4 6t.588 1.413T6 8m6 4.5q.2 0 .35-.15t.15-.35t-.15-.35t-.35-.15t-.35.15t-.15.35t.15.35t.35.15M6 20q.825 0 1.413-.587T8 18t-.587-1.412T6 16t-1.412.588T4 18t.588 1.413T6 20"),
|
||||
CONTENT_CUT("M19 21l-7-7-2.35 2.35q.2.375.275.8T10 18q0 1.65-1.175 2.825T6 22q-1.65 0-2.825-1.175T2 18t1.175-2.825T6 14q.425 0 .85.075t.8.275L10 12 7.65 9.65q-.375.2-.8.275T6 10q-1.65 0-2.825-1.175T2 6q0-1.65 1.175-2.825T6 2q1.65 0 2.825 1.175T10 6q0 .425-.075.85t-.275.8L22 20v1H19Zm-4-10-2-2 6-6h3v1l-7 7ZM7.4125 7.4125Q8 6.825 8 6t-.5875-1.4125Q6.825 4 6 4t-1.4125.5875Q4 5.175 4 6t.5875 1.4125T6 8t1.4125-.5875ZM12.35 12.35q.15-.15.15-.35t-.15-.35-.35-.15-.35.15-.15.35.15.35.35.15.35-.15ZM7.4125 19.4125Q8 18.825 8 18t-.5875-1.4125Q6.825 16 6 16t-1.4125.5875Q4 17.175 4 18t.5875 1.4125Q5.175 20 6 20t1.4125-.5875Z"),
|
||||
CONTENT_COPY("M9 18Q8.175 18 7.5875 17.4125T7 16V4Q7 3.175 7.5875 2.5875T9 2H18Q18.825 2 19.4125 2.5875T20 4V16Q20 16.825 19.4125 17.4125T18 18H9ZM9 16H18V4H9V16ZM5 22Q4.175 22 3.5875 21.4125T3 20V6H5V20H16V22H5ZM9 16V4 16Z"),
|
||||
CONTENT_PASTE("M5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h4.175q.275-.875 1.075-1.437T12 1q1 0 1.788.563T14.85 3H19q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21zm0-2h14V5h-2v2q0 .425-.288.713T16 8H8q-.425 0-.712-.288T7 7V5H5zm7-14q.425 0 .713-.288T13 4t-.288-.712T12 3t-.712.288T11 4t.288.713T12 5"),
|
||||
CONTENT_PASTE("M5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h4.175q.275-.875 1.075-1.437T12 1q1 0 1.788.563T14.85 3H19q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21zm0-2h14V5h-2v3H7V5H5zm7-14q.425 0 .713-.288T13 4t-.288-.712T12 3t-.712.288T11 4t.288.713T12 5"),
|
||||
CREATE_NEW_FOLDER("M14 16h2V14h2V12H16V10H14v2H12v2h2v2ZM4 20q-.825 0-1.4125-.5875T2 18V6q0-.825.5875-1.4125T4 4h6l2 2h8q.825 0 1.4125.5875T22 8V18q0 .825-.5875 1.4125T20 20H4Zm0-2H20V8H11.175l-2-2H4V18ZV6 18Z"),
|
||||
DELETE("M7 21Q6.175 21 5.5875 20.4125T5 19V6H4V4H9V3H15V4H20V6H19V19Q19 19.825 18.4125 20.4125T17 21H7ZM17 6H7V19H17V6ZM9 17H11V8H9V17ZM13 17H15V8H13V17ZM7 6V19 6Z"),
|
||||
DELETE_FOREVER("M9.4 16.5 12 13.9 14.6 16.5 16 15.1 13.4 12.5 16 9.9 14.6 8.5 12 11.1 9.4 8.5 8 9.9 10.6 12.5 8 15.1 9.4 16.5ZM7 21Q6.175 21 5.5875 20.4125T5 19V6H4V4H9V3H15V4H20V6H19V19Q19 19.825 18.4125 20.4125T17 21H7ZM17 6H7V19H17V6ZM7 6V19 6Z"),
|
||||
@@ -92,8 +87,7 @@ public enum SVG {
|
||||
LOCATION_CITY("M3 21V7H9V5l3-3 3 3v6h6V21H3Zm2-2H7V17H5v2Zm0-4H7V13H5v2Zm0-4H7V9H5v2Zm6 8h2V17H11v2Zm0-4h2V13H11v2Zm0-4h2V9H11v2Zm0-4h2V5H11V7Zm6 12h2V17H17v2Zm0-4h2V13H17v2Z"),
|
||||
MENU("M3 18V16H21V18H3ZM3 13V11H21V13H3ZM3 8V6H21V8H3Z"),
|
||||
MICROSOFT("M4 20H22v2H4V13H20v7h2V4H20v7H4V4h7V20h2V4h9V2H2V22H4"), // Not Material
|
||||
MINIMIZE("M6 21V19H18V21H6Z"),
|
||||
MOJANG("M13.9658 0C12.9552.828 12.7686 2.195 12.7007 3.418 12.7219 4.4201 14.0423 4.7174 14.6538 4.0082 15.0912 2.6579 14.3692 1.2739 13.9658 0ZM10.913 2.9297C10.7559 3.6983 10.6284 4.4669 10.4925 5.2355 8.9894 3.9913 7.1505 3.0825 5.142 3.2948 3.4944 3.4646.9429 2.6961.1404 4.6452-.1229 8.314.0722 12.0124.034 15.6896-.0891 16.9126.8957 18.1101 2.1781 17.9699 6.3989 18.0039 10.6283 18.0172 14.8491 17.9661 16.1145 18.102 16.8067 16.9554 16.9851 15.8768 13.2696 16.4374 9.1761 16.9296 5.7111 15.1292 2.5986 13.5836 2.246 8.3139 5.5581 6.798 9.3203 5.1759 13.8596 8.0383 14.9424 11.7877 15.6133 12.1105 16.2844 12.4247 16.9596 12.7432 16.6241 10.3483 16.6537 7.8005 15.5793 5.5882 15.0571 4.7474 14.0975 6.2714 13.3799 5.6217 12.416 4.8659 11.7495 3.8129 10.913 2.9297Z"), // Not Material
|
||||
MINIMIZE_CENTER("M6 13v-2h12v2H6Z"), // Not Material
|
||||
MORE_HORIZ("M6 14Q5.175 14 4.5875 13.4125T4 12Q4 11.175 4.5875 10.5875T6 10Q6.825 10 7.4125 10.5875T8 12Q8 12.825 7.4125 13.4125T6 14ZM12 14Q11.175 14 10.5875 13.4125T10 12Q10 11.175 10.5875 10.5875T12 10Q12.825 10 13.4125 10.5875T14 12Q14 12.825 13.4125 13.4125T12 14ZM18 14Q17.175 14 16.5875 13.4125T16 12Q16 11.175 16.5875 10.5875T18 10Q18.825 10 19.4125 10.5875T20 12Q20 12.825 19.4125 13.4125T18 14Z"),
|
||||
MORE_VERT("M12 20Q11.175 20 10.5875 19.4125T10 18Q10 17.175 10.5875 16.5875T12 16Q12.825 16 13.4125 16.5875T14 18Q14 18.825 13.4125 19.4125T12 20ZM12 14Q11.175 14 10.5875 13.4125T10 12Q10 11.175 10.5875 10.5875T12 10Q12.825 10 13.4125 10.5875T14 12Q14 12.825 13.4125 13.4125T12 14ZM12 8Q11.175 8 10.5875 7.4125T10 6Q10 5.175 10.5875 4.5875T12 4Q12.825 4 13.4125 4.5875T14 6Q14 6.825 13.4125 7.4125T12 8Z"),
|
||||
OPEN_IN_NEW("M5 21Q4.175 21 3.5875 20.4125T3 19V5Q3 4.175 3.5875 3.5875T5 3H12V5H5V19H19V12H21V19Q21 19.825 20.4125 20.4125T19 21H5ZM9.7 15.7 8.3 14.3 17.6 5H14V3H21V10H19V6.4L9.7 15.7Z"),
|
||||
@@ -104,7 +98,7 @@ public enum SVG {
|
||||
PERSON("M12 12Q10.35 12 9.175 10.825T8 8Q8 6.35 9.175 5.175T12 4Q13.65 4 14.825 5.175T16 8Q16 9.65 14.825 10.825T12 12ZM4 20V17.2Q4 16.35 4.4375 15.6375T5.6 14.55Q7.15 13.775 8.75 13.3875T12 13Q13.65 13 15.25 13.3875T18.4 14.55Q19.125 14.925 19.5625 15.6375T20 17.2V20H4ZM6 18H18V17.2Q18 16.925 17.8625 16.7T17.5 16.35Q16.15 15.675 14.775 15.3375T12 15Q10.6 15 9.225 15.3375T6.5 16.35Q6.275 16.475 6.1375 16.7T6 17.2V18ZM12 10Q12.825 10 13.4125 9.4125T14 8Q14 7.175 13.4125 6.5875T12 6Q11.175 6 10.5875 6.5875T10 8Q10 8.825 10.5875 9.4125T12 10ZM12 8ZM12 18Z"),
|
||||
PUBLIC("M12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM11 19.95V18Q10.175 18 9.5875 17.4125T9 16V15L4.2 10.2Q4.125 10.65 4.0625 11.1T4 12Q4 15.025 5.9875 17.3T11 19.95ZM17.9 17.4Q18.925 16.275 19.4625 14.8875T20 12Q20 9.55 18.6375 7.525T15 4.6V5Q15 5.825 14.4125 6.4125T13 7H11V9Q11 9.425 10.7125 9.7125T10 10H8V12H14Q14.425 12 14.7125 12.2875T15 13V16H16Q16.65 16 17.175 16.3875T17.9 17.4Z"),
|
||||
REFRESH("M12 20Q8.65 20 6.325 17.675T4 12Q4 8.65 6.325 6.325T12 4Q13.725 4 15.3 4.7125T18 6.75V4H20V11H13V9H17.2Q16.4 7.6 15.0125 6.8T12 6Q9.5 6 7.75 7.75T6 12Q6 14.5 7.75 16.25T12 18Q13.925 18 15.475 16.9T17.65 14H19.75Q19.05 16.65 16.9 18.325T12 20Z"),
|
||||
REDO("M16.2 10H9.9q-1.575 0-2.738 1T6 13.5T7.163 16T9.9 17H16q.425 0 .713.288T17 18t-.288.713T16 19H9.9q-2.425 0-4.163-1.575T4 13.5t1.738-3.925T9.9 8h6.3l-1.9-1.9q-.275-.275-.275-.7t.275-.7t.7-.275t.7.275l3.6 3.6q.15.15.213.325t.062.375t-.062.375t-.213.325l-3.6 3.6q-.275.275-.7.275t-.7-.275t-.275-.7t.275-.7z"),
|
||||
REDO("M9.9 19q-2.425 0-4.163-1.575T4 13.5t1.738-3.925T9.9 8h6.3l-2.6-2.6L15 4l5 5l-5 5l-1.4-1.4l2.6-2.6H9.9q-1.575 0-2.738 1T6 13.5T7.163 16T9.9 17H17v2z"),
|
||||
RELEASE_CIRCLE("M9,7H13A2,2 0 0,1 15,9V11C15,11.84 14.5,12.55 13.76,12.85L15,17H13L11.8,13H11V17H9V7M11,9V11H13V9H11M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12C4,16.41 7.58,20 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z"), // Not Material
|
||||
RESTORE("M12 21Q8.55 21 5.9875 18.7125T3.05 13H5.1Q5.45 15.6 7.4125 17.3T12 19Q14.925 19 16.9625 16.9625T19 12Q19 9.075 16.9625 7.0375T12 5Q10.275 5 8.775 5.8T6.25 8H9V10H3V4H5V6.35Q6.275 4.75 8.1125 3.875T12 3Q13.875 3 15.5125 3.7125T18.3625 5.6375Q19.575 6.85 20.2875 8.4875T21 12Q21 13.875 20.2875 15.5125T18.3625 18.3625Q17.15 19.575 15.5125 20.2875T12 21Z"), // Not Material
|
||||
ROCKET_LAUNCH("M5.65 10.025 7.6 10.85Q7.95 10.15 8.325 9.5T9.15 8.2L7.75 7.925 5.65 10.025ZM9.2 12.1 12.05 14.925Q13.1 14.525 14.3 13.7T16.55 11.825Q18.3 10.075 19.2875 7.9375T20.15 4Q18.35 3.875 16.2 4.8625T12.3 7.6Q11.25 8.65 10.425 9.85T9.2 12.1ZM13.65 10.475Q13.075 9.9 13.075 9.0625T13.65 7.65Q14.225 7.075 15.075 7.075T16.5 7.65Q17.075 8.225 17.075 9.0625T16.5 10.475Q15.925 11.05 15.075 11.05T13.65 10.475ZM14.125 18.5 16.225 16.4 15.95 15Q15.3 15.45 14.65 15.8125T13.3 16.525L14.125 18.5ZM21.95 2.175Q22.425 5.2 21.3625 8.0625T17.7 13.525L18.2 16Q18.3 16.5 18.15 16.975T17.65 17.8L13.45 22 11.35 17.075 7.075 12.8 2.15 10.7 6.325 6.5Q6.675 6.15 7.1625 6T8.15 5.95L10.625 6.45Q13.225 3.85 16.075 2.775T21.95 2.175ZM3.925 15.975Q4.8 15.1 6.0625 15.0875T8.2 15.95Q9.075 16.825 9.0625 18.0875T8.175 20.225Q7.55 20.85 6.0875 21.3T2.05 22.1Q2.4 19.525 2.85 18.0625T3.925 15.975ZM5.35 17.375Q5.1 17.625 4.85 18.2875T4.5 19.625Q5.175 19.525 5.8375 19.2875T6.75 18.8Q7.05 18.5 7.075 18.075T6.8 17.35Q6.5 17.05 6.075 17.0625T5.35 17.375Z"),
|
||||
@@ -120,11 +114,12 @@ public enum SVG {
|
||||
STADIA_CONTROLLER_FILL("M4.725 20q-1.5 0-2.5625-1.075T1.05 16.325q0-.225.025-.45t.075-.45l2.1-8.4q.35-1.35 1.425-2.1875T7.125 4h9.75q1.375 0 2.45.8375T20.75 7.025l2.1 8.4q.05.225.0875.4625t.0375.4625q0 1.525-1.0875 2.5875T19.275 20q-1.05 0-1.95-.55t-1.35-1.5l-.7-1.45q-.125-.25-.375-.375T14.375 16H9.625q-.275 0-.525.125t-.375.375l-.7 1.45q-.45.95-1.35 1.5T4.725 20ZM13.5 11q.425 0 .7125-.2875T14.5 10t-.2875-.7125T13.5 9t-.7125.2875T12.5 10t.2875.7125T13.5 11Zm2-2q.425 0 .7125-.2875T16.5 8q0-.425-.2875-.7125T15.5 7q-.425 0-.7125.2875T14.5 8t.2875.7125T15.5 9Zm0 4q.425 0 .7125-.2875T16.5 12q0-.425-.2875-.7125T15.5 11q-.425 0-.7125.2875T14.5 12t.2875.7125T15.5 13Zm2-2q.425 0 .7125-.2875T18.5 10q0-.425-.2875-.7125T17.5 9q-.425 0-.7125.2875T16.5 10q0 .425.2875.7125T17.5 11Zm-9 1.5q.325 0 .5375-.2125T9.25 11.75v-1h1q.325 0 .5375-.2125T11 10t-.2125-.5375T10.25 9.25h-1v-1q0-.325-.2125-.5375T8.5 7.5q-.325 0-.5375.2125T7.75 8.25v1h-1q-.325 0-.5375.2125T6 10q0 .325.2125.5375T6.75 10.75h1v1q0 .325.2125.5375T8.5 12.5Z"),
|
||||
STYLE("M3.975 19.8 3.125 19.45Q2.35 19.125 2.0875 18.325T2.175 16.75L3.975 12.85V19.8ZM7.975 22Q7.15 22 6.5625 21.4125T5.975 20V14L8.625 21.35Q8.7 21.525 8.775 21.6875T8.975 22H7.975ZM13.125 21.9Q12.325 22.2 11.575 21.825T10.525 20.65L6.075 8.45Q5.775 7.65 6.125 6.8875T7.275 5.85L14.825 3.1Q15.625 2.8 16.375 3.175T17.425 4.35L21.875 16.55Q22.175 17.35 21.825 18.1125T20.675 19.15L13.125 21.9ZM10.975 10Q11.4 10 11.6875 9.7125T11.975 9Q11.975 8.575 11.6875 8.2875T10.975 8Q10.55 8 10.2625 8.2875T9.975 9Q9.975 9.425 10.2625 9.7125T10.975 10ZM12.425 20 19.975 17.25 15.525 5 7.975 7.75 12.425 20ZM7.975 7.75 15.525 5 7.975 7.75Z"),
|
||||
STYLE_FILL("M3.975 19.8l-.85-.35q-.775-.325-1.0375-1.125T2.175 16.75l1.8-3.9V19.8Zm4 2.2q-.825 0-1.4125-.5875T5.975 20V14l2.65 7.35q.075.175.15.3375t.2.3125h-1Zm5.15-.1q-.8.3-1.55-.075t-1.05-1.175L6.075 8.45q-.3-.8.05-1.5625T7.275 5.85l7.55-2.75q.8-.3 1.55.075t1.05 1.175l4.45 12.2q.3.8-.05 1.5625T20.675 19.15l-7.55 2.75ZM10.975 10q.425 0 .7125-.2875T11.975 9q0-.425-.2875-.7125T10.975 8t-.7125.2875T9.975 9t.2875.7125T10.975 10Z"),
|
||||
TEXTURE("M4.4-3Q3.925-3.1 3.5125-3.5125T3-4.4L19.6-21Q20.125-20.875 20.5-20.4875T21.025-19.6L4.4-3ZM3-9.3V-12.1L11.9-21H14.7L3-9.3ZM3-17V-19Q3-19.825 3.5875-20.4125T5-21H7L3-17ZM17-3 21-7V-5Q21-4.175 20.4125-3.5875T19-3H17ZM9.3-3 21-14.7V-11.9L12.1-3H9.3Z"),
|
||||
TEXTURE("M4.4 21q-.475-.1-.8875-.5125T3 19.6L19.6 3q.525.125.9.5125t.525.8875L4.4 21ZM3 14.7v-2.8L11.9 3h2.8L3 14.7ZM3 7V5q0-.825.5875-1.4125T5 3h2L3 7Zm14 14 4-4v2q0 .825-.5875 1.4125T19 21h-2Zm-7.7 0L21 9.3v2.8L12.1 21H9.3Z"),
|
||||
TRIP("M4 21Q3.175 21 2.5875 20.4125T2 19V8Q2 7.175 2.5875 6.5875T4 6H8V4Q8 3.175 8.5875 2.5875T10 2H14Q14.825 2 15.4125 2.5875T16 4V6H20Q20.825 6 21.4125 6.5875T22 8V19Q22 19.825 21.4125 20.4125T20 21H4ZM10 6H14V4H10V6ZM6 8H4V19H6V8ZM16 19V8H8V19H16ZM18 8V19H20V8H18ZM12 13.5Z"),
|
||||
TUNE("M11 21V15H13V17H21V19H13V21H11ZM3 19V17H9V19H3ZM7 15V13H3V11H7V9H9V15H7ZM11 13V11H21V13H11ZM15 9V3H17V5H21V7H17V9H15ZM3 7V5H13V7H3Z"),
|
||||
UNFOLD_MORE("M12 21 7.5 16.5l1.45-1.45L12 18.1l3.05-3.05 1.45 1.45L12 21ZM8.95 9.05 7.5 7.6 12 3.1l4.5 4.5-1.45 1.45L12 6 8.95 9.05Z"),
|
||||
UPDATE("M12 21Q10.125 21 8.4875 20.2875T5.6375 18.3625Q4.425 17.15 3.7125 15.5125T3 12Q3 10.125 3.7125 8.4875T5.6375 5.6375Q6.85 4.425 8.4875 3.7125T12 3Q14.05 3 15.8875 3.875T19 6.35V4H21V10H15V8H17.75Q16.725 6.6 15.225 5.8T12 5Q9.075 5 7.0375 7.0375T5 12Q5 14.925 7.0375 16.9625T12 19Q14.625 19 16.5875 17.3T18.9 13H20.95Q20.575 16.425 18.0125 18.7125T12 21ZM14.8 16.2 11 12.4V7H13V11.6L16.2 14.8 14.8 16.2Z"),
|
||||
UNDO("M8 19q-.425 0-.712-.288T7 18t.288-.712T8 17h6.1q1.575 0 2.738-1T18 13.5T16.838 11T14.1 10H7.8l1.9 1.9q.275.275.275.7t-.275.7t-.7.275t-.7-.275L4.7 9.7q-.15-.15-.213-.325T4.426 9t.063-.375T4.7 8.3l3.6-3.6q.275-.275.7-.275t.7.275t.275.7t-.275.7L7.8 8h6.3q2.425 0 4.163 1.575T20 13.5t-1.737 3.925T14.1 19z"),
|
||||
UNDO("M7 19v-2h7.1q1.575 0 2.738-1T18 13.5T16.838 11T14.1 10H7.8l2.6 2.6L9 14L4 9l5-5l1.4 1.4L7.8 8h6.3q2.425 0 4.163 1.575T20 13.5t-1.737 3.925T14.1 19z"),
|
||||
VISIBILITY("M12 16q1.875 0 3.1875-1.3125T16.5 11.5 15.1875 8.3125 12 7 8.8125 8.3125 7.5 11.5t1.3125 3.1875T12 16Zm0-1.8q-1.125 0-1.9125-.7875T9.3 11.5t.7875-1.9125T12 8.8q1.125 0 1.9125.7875T14.7 11.5q0 1.125-.7875 1.9125T12 14.2ZM12 19q-3.65 0-6.65-2.0375T1 11.5Q2.35 8.075 5.35 6.0375T12 4q3.65 0 6.65 2.0375T23 11.5q-1.35 3.425-4.35 5.4625T12 19Zm0-7.5ZM12 17q2.825 0 5.1875-1.4875T20.8 11.5q-1.25-2.525-3.6125-4.0125T12 6 6.8125 7.4875 3.2 11.5q1.25 2.525 3.6125 4.0125T12 17Z"),
|
||||
VISIBILITY_OFF("M16.1 13.3l-1.45-1.45q.225-1.175-.675-2.2t-2.325-.8L10.2 7.4q.425-.2.8625-.3T12 7q1.875 0 3.1875 1.3125T16.5 11.5q0 .5-.1.9375t-.3.8625Zm3.2 3.15-1.45-1.4q.95-.725 1.6875-1.5875T20.8 11.5q-1.25-2.525-3.5875-4.0125T12 6q-.725 0-1.425.1T9.2 6.4L7.65 4.85q1.025-.425 2.1-.6375T12 4q3.775 0 6.725 2.0875T23 11.5q-.575 1.475-1.5125 2.7375T19.3 16.45Zm.5 6.15-4.2-4.15q-.875.275-1.7625.4125T12 19q-3.775 0-6.725-2.0875T1 11.5q.525-1.325 1.325-2.4625T4.15 7L1.4 4.2 2.8 2.8 21.2 21.2l-1.4 1.4ZM5.55 8.4q-.725.65-1.325 1.425T3.2 11.5q1.25 2.525 3.5875 4.0125T12 17q.5 0 .975-.0625T13.95 16.8l-.9-.95q-.275.075-.525.1125T12 16q-1.875 0-3.1875-1.3125T7.5 11.5q0-.275.0375-.525T7.65 10.45L5.55 8.4Zm7.975 2.325ZM9.75 12.6Z"),
|
||||
WARNING("M1 21 12 2 23 21H1ZM4.45 19H19.55L12 6 4.45 19ZM12 18Q12.425 18 12.7125 17.7125T13 17Q13 16.575 12.7125 16.2875T12 16Q11.575 16 11.2875 16.2875T11 17Q11 17.425 11.2875 17.7125T12 18ZM11 15H13V10H11V15ZM12 12.5Z"),
|
||||
@@ -134,48 +129,40 @@ public enum SVG {
|
||||
|
||||
public static final double DEFAULT_SIZE = 24;
|
||||
|
||||
private final String path;
|
||||
static void setSize(SVGPath path, double size) {
|
||||
double scale = size / DEFAULT_SIZE;
|
||||
path.setScaleX(scale);
|
||||
path.setScaleY(scale);
|
||||
}
|
||||
|
||||
SVG(String path) {
|
||||
this.path = path;
|
||||
private final String rawPath;
|
||||
private String path;
|
||||
|
||||
SVG(String rawPath) {
|
||||
this.rawPath = rawPath;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
if (path == null)
|
||||
// We move the current point so that SVGPath will treat 0 0 24 24 as the layout bounds
|
||||
path = "M24 24ZM0 0Z" + rawPath;
|
||||
return path;
|
||||
}
|
||||
|
||||
private static Node createIcon(SVGPath path, double size) {
|
||||
if (size < 0) {
|
||||
StackPane pane = new StackPane(path);
|
||||
pane.setAlignment(Pos.CENTER);
|
||||
return pane;
|
||||
}
|
||||
|
||||
double scale = size / 24;
|
||||
path.setScaleX(scale);
|
||||
path.setScaleY(scale);
|
||||
return new Group(path);
|
||||
public SVGPath createIcon() {
|
||||
var path = new SVGPath();
|
||||
path.getStyleClass().add("svg");
|
||||
path.setContent(getPath());
|
||||
return path;
|
||||
}
|
||||
|
||||
public Node createIcon(double size) {
|
||||
SVGPath p = new SVGPath();
|
||||
p.setContent(path);
|
||||
p.getStyleClass().add("svg");
|
||||
return createIcon(p, size);
|
||||
public SVGContainer createIcon(double size) {
|
||||
return new SVGContainer(this, size);
|
||||
}
|
||||
|
||||
public Node createIcon() {
|
||||
SVGPath p = new SVGPath();
|
||||
p.setContent(path);
|
||||
p.getStyleClass().add("svg");
|
||||
return createIcon(p, -1);
|
||||
}
|
||||
|
||||
public Node createIcon(ObservableValue<? extends Paint> color) {
|
||||
SVGPath p = new SVGPath();
|
||||
p.setContent(path);
|
||||
p.getStyleClass().add("svg");
|
||||
p.fillProperty().bind(color);
|
||||
return createIcon(p, -1);
|
||||
public SVGPath createIcon(ObservableValue<? extends Paint> color) {
|
||||
SVGPath path = createIcon();
|
||||
path.fillProperty().bind(color);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.shape.SVGPath;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
|
||||
/// A lightweight wrapper for displaying [SVG] icons.
|
||||
///
|
||||
/// @author Glavo
|
||||
public final class SVGContainer extends Parent {
|
||||
|
||||
private static final String DEFAULT_STYLE_CLASS = "svg-container";
|
||||
|
||||
private final SVGPath path = new SVGPath();
|
||||
private SVG icon = SVG.NONE;
|
||||
private double iconSize = SVG.DEFAULT_SIZE;
|
||||
private SVGPath tempPath;
|
||||
private Timeline timeline;
|
||||
|
||||
{
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
this.path.getStyleClass().add("svg");
|
||||
}
|
||||
|
||||
/// Creates an SVGContainer with the default icon and the default icon size.
|
||||
public SVGContainer() {
|
||||
this(SVG.NONE, SVG.DEFAULT_SIZE);
|
||||
}
|
||||
|
||||
/// Creates an SVGContainer showing the given icon using the default icon size.
|
||||
///
|
||||
/// @param icon the [SVG] icon to display
|
||||
public SVGContainer(SVG icon) {
|
||||
this(icon, SVG.DEFAULT_SIZE);
|
||||
}
|
||||
|
||||
/// Creates an SVGContainer with a custom icon size. The initial icon is
|
||||
/// [SVG#NONE].
|
||||
///
|
||||
/// @param iconSize the icon size
|
||||
public SVGContainer(double iconSize) {
|
||||
this(SVG.NONE, iconSize);
|
||||
}
|
||||
|
||||
/// Creates an SVGContainer with the specified icon and size.
|
||||
///
|
||||
/// @param icon the [SVG] icon to display
|
||||
/// @param iconSize the icon size
|
||||
public SVGContainer(SVG icon, double iconSize) {
|
||||
setIconSizeImpl(iconSize);
|
||||
setIcon(icon);
|
||||
}
|
||||
|
||||
public double getIconSize() {
|
||||
return iconSize;
|
||||
}
|
||||
|
||||
private void setIconSizeImpl(double newSize) {
|
||||
this.iconSize = newSize;
|
||||
SVG.setSize(path, newSize);
|
||||
if (tempPath != null)
|
||||
SVG.setSize(tempPath, newSize);
|
||||
}
|
||||
|
||||
public void setIconSize(double newSize) {
|
||||
setIconSizeImpl(newSize);
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
/// Gets the currently displayed icon.
|
||||
public SVG getIcon() {
|
||||
return icon;
|
||||
}
|
||||
|
||||
/// Sets the icon to display without animation.
|
||||
public void setIcon(SVG newIcon) {
|
||||
setIcon(newIcon, Duration.ZERO);
|
||||
}
|
||||
|
||||
/// Sets the icon to display with a cross-fade animation.
|
||||
public void setIcon(SVG newIcon, Duration animationDuration) {
|
||||
if (timeline != null) {
|
||||
timeline.stop();
|
||||
timeline = null;
|
||||
}
|
||||
|
||||
SVG oldIcon = this.icon;
|
||||
this.icon = newIcon;
|
||||
|
||||
if (animationDuration.equals(Duration.ZERO)) {
|
||||
path.setContent(newIcon.getPath());
|
||||
path.setOpacity(1);
|
||||
if (getChildren().size() != 1)
|
||||
getChildren().setAll(path);
|
||||
} else {
|
||||
if (tempPath == null) {
|
||||
tempPath = new SVGPath();
|
||||
tempPath.getStyleClass().add("svg");
|
||||
SVG.setSize(tempPath, iconSize);
|
||||
} else
|
||||
tempPath.setOpacity(1);
|
||||
|
||||
tempPath.setContent(oldIcon.getPath());
|
||||
getChildren().setAll(path, tempPath);
|
||||
|
||||
path.setOpacity(0);
|
||||
path.setContent(newIcon.getPath());
|
||||
|
||||
timeline = new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(path.opacityProperty(), 0, Motion.LINEAR),
|
||||
new KeyValue(tempPath.opacityProperty(), 1, Motion.LINEAR)
|
||||
),
|
||||
new KeyFrame(animationDuration,
|
||||
new KeyValue(path.opacityProperty(), 1, Motion.LINEAR),
|
||||
new KeyValue(tempPath.opacityProperty(), 0, Motion.LINEAR)
|
||||
)
|
||||
);
|
||||
timeline.setOnFinished(e -> {
|
||||
getChildren().setAll(path);
|
||||
timeline = null;
|
||||
});
|
||||
timeline.play();
|
||||
}
|
||||
}
|
||||
|
||||
// Parent
|
||||
|
||||
@Override
|
||||
public double prefWidth(double height) {
|
||||
return iconSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double prefHeight(double width) {
|
||||
return iconSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double minHeight(double width) {
|
||||
return iconSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double minWidth(double height) {
|
||||
return iconSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void layoutChildren() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
}
|
||||
@@ -25,6 +25,8 @@ import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
@@ -40,14 +42,11 @@ public abstract class ToolbarListPageSkin<E, P extends ListPageBase<E>> extends
|
||||
public ToolbarListPageSkin(P skinnable) {
|
||||
super(skinnable);
|
||||
|
||||
SpinnerPane spinnerPane = new SpinnerPane();
|
||||
spinnerPane.loadingProperty().bind(skinnable.loadingProperty());
|
||||
spinnerPane.failedReasonProperty().bind(skinnable.failedReasonProperty());
|
||||
spinnerPane.onFailedActionProperty().bind(skinnable.onFailedActionProperty());
|
||||
spinnerPane.getStyleClass().add("large-spinner-pane");
|
||||
|
||||
ComponentList root = new ComponentList();
|
||||
root.getStyleClass().add("no-padding");
|
||||
|
||||
StackPane container = new StackPane();
|
||||
container.getChildren().add(root);
|
||||
StackPane.setMargin(root, new Insets(10));
|
||||
|
||||
List<Node> toolbarButtons = initializeToolbar(skinnable);
|
||||
@@ -59,31 +58,33 @@ public abstract class ToolbarListPageSkin<E, P extends ListPageBase<E>> extends
|
||||
root.getContent().add(toolbar);
|
||||
}
|
||||
|
||||
SpinnerPane spinnerPane = new SpinnerPane();
|
||||
spinnerPane.loadingProperty().bind(skinnable.loadingProperty());
|
||||
spinnerPane.failedReasonProperty().bind(skinnable.failedReasonProperty());
|
||||
spinnerPane.onFailedActionProperty().bind(skinnable.onFailedActionProperty());
|
||||
|
||||
ComponentList.setVgrow(spinnerPane, Priority.ALWAYS);
|
||||
|
||||
{
|
||||
this.listView = new JFXListView<>();
|
||||
this.listView.setPadding(Insets.EMPTY);
|
||||
this.listView.setCellFactory(listView -> createListCell((JFXListView<E>) listView));
|
||||
ComponentList.setVgrow(listView, Priority.ALWAYS);
|
||||
this.listView.getStyleClass().add("no-horizontal-scrollbar");
|
||||
Bindings.bindContent(this.listView.getItems(), skinnable.itemsProperty());
|
||||
root.getContent().add(listView);
|
||||
FXUtils.ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
|
||||
|
||||
spinnerPane.setContent(listView);
|
||||
}
|
||||
|
||||
spinnerPane.setContent(root);
|
||||
root.getContent().add(spinnerPane);
|
||||
|
||||
getChildren().setAll(spinnerPane);
|
||||
}
|
||||
|
||||
public static Node wrap(Node node) {
|
||||
StackPane stackPane = new StackPane();
|
||||
stackPane.setPadding(new Insets(0, 5, 0, 2));
|
||||
stackPane.getChildren().setAll(node);
|
||||
return stackPane;
|
||||
getChildren().setAll(container);
|
||||
}
|
||||
|
||||
public static JFXButton createToolbarButton2(String text, SVG svg, Runnable onClick) {
|
||||
JFXButton ret = new JFXButton();
|
||||
ret.getStyleClass().add("jfx-tool-bar-button");
|
||||
ret.setGraphic(wrap(svg.createIcon()));
|
||||
ret.setGraphic(svg.createIcon(20));
|
||||
ret.setText(text);
|
||||
ret.setOnAction(e -> onClick.run());
|
||||
return ret;
|
||||
@@ -92,7 +93,7 @@ public abstract class ToolbarListPageSkin<E, P extends ListPageBase<E>> extends
|
||||
public static JFXButton createDecoratorButton(String tooltip, SVG svg, Runnable onClick) {
|
||||
JFXButton ret = new JFXButton();
|
||||
ret.getStyleClass().add("jfx-decorator-button");
|
||||
ret.setGraphic(wrap(svg.createIcon()));
|
||||
ret.setGraphic(svg.createIcon(20));
|
||||
FXUtils.installFastTooltip(ret, tooltip);
|
||||
ret.setOnAction(e -> onClick.run());
|
||||
return ret;
|
||||
|
||||
@@ -20,14 +20,19 @@ package org.jackhuang.hmcl.ui;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.beans.value.WeakChangeListener;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.WeakListChangeListener;
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
import org.jackhuang.hmcl.event.EventManager;
|
||||
import org.jackhuang.hmcl.event.EventPriority;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class WeakListenerHolder {
|
||||
public final class WeakListenerHolder {
|
||||
private final List<Object> refs = new ArrayList<>(0);
|
||||
|
||||
public WeakListenerHolder() {
|
||||
@@ -48,6 +53,22 @@ public class WeakListenerHolder {
|
||||
return new WeakListChangeListener<>(listener);
|
||||
}
|
||||
|
||||
public <T extends Event> void registerWeak(EventManager<T> manager, Consumer<T> consumer) {
|
||||
refs.add(manager.registerWeak(consumer));
|
||||
}
|
||||
|
||||
public <T extends Event> void registerWeak(EventManager<T> manager, Consumer<T> consumer, EventPriority priority) {
|
||||
refs.add(manager.registerWeak(consumer, priority));
|
||||
}
|
||||
|
||||
public <T> void onWeakChange(ObservableValue<T> value, Consumer<T> consumer) {
|
||||
refs.add(FXUtils.onWeakChange(value, consumer));
|
||||
}
|
||||
|
||||
public <T> void onWeakChangeAndOperate(ObservableValue<T> value, Consumer<T> consumer) {
|
||||
refs.add(FXUtils.onWeakChangeAndOperate(value, consumer));
|
||||
}
|
||||
|
||||
public void add(Object obj) {
|
||||
refs.add(obj);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
@@ -76,9 +77,10 @@ public class AccountAdvancedListItem extends AdvancedListItem {
|
||||
FXUtils.installFastTooltip(this, tooltip);
|
||||
|
||||
canvas = new Canvas(32, 32);
|
||||
setLeftGraphic(canvas);
|
||||
canvas.setMouseTransparent(true);
|
||||
AdvancedListItem.setAlignment(canvas, Pos.CENTER);
|
||||
|
||||
setActionButtonVisible(false);
|
||||
setLeftGraphic(canvas);
|
||||
|
||||
if (account != null) {
|
||||
this.accountProperty().set(account);
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.account;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ListProperty;
|
||||
@@ -54,7 +53,6 @@ import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.wrap;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@@ -130,35 +128,25 @@ public final class AccountListPage extends DecoratorAnimatedPage implements Deco
|
||||
|
||||
AdvancedListItem microsoftItem = new AdvancedListItem();
|
||||
microsoftItem.getStyleClass().add("navigation-drawer-item");
|
||||
microsoftItem.setActionButtonVisible(false);
|
||||
microsoftItem.setTitle(i18n("account.methods.microsoft"));
|
||||
microsoftItem.setLeftGraphic(wrap(SVG.MICROSOFT));
|
||||
microsoftItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_MICROSOFT)));
|
||||
microsoftItem.setLeftIcon(SVG.MICROSOFT);
|
||||
microsoftItem.setOnAction(e -> Controllers.dialog(new MicrosoftAccountLoginPane()));
|
||||
|
||||
AdvancedListItem offlineItem = new AdvancedListItem();
|
||||
offlineItem.getStyleClass().add("navigation-drawer-item");
|
||||
offlineItem.setActionButtonVisible(false);
|
||||
offlineItem.setTitle(i18n("account.methods.offline"));
|
||||
offlineItem.setLeftGraphic(wrap(SVG.PERSON));
|
||||
offlineItem.setLeftIcon(SVG.PERSON);
|
||||
offlineItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_OFFLINE)));
|
||||
|
||||
VBox boxAuthServers = new VBox();
|
||||
authServerItems = MappedObservableList.create(skinnable.authServersProperty(), server -> {
|
||||
AdvancedListItem item = new AdvancedListItem();
|
||||
item.getStyleClass().add("navigation-drawer-item");
|
||||
item.setLeftGraphic(wrap(SVG.DRESSER));
|
||||
item.setLeftIcon(SVG.DRESSER);
|
||||
item.setOnAction(e -> Controllers.dialog(new CreateAccountPane(server)));
|
||||
|
||||
JFXButton btnRemove = new JFXButton();
|
||||
btnRemove.setOnAction(e -> {
|
||||
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
|
||||
skinnable.authServersProperty().remove(server);
|
||||
}, null);
|
||||
e.consume();
|
||||
});
|
||||
btnRemove.getStyleClass().add("toggle-icon4");
|
||||
btnRemove.setGraphic(SVG.CLOSE.createIcon(14));
|
||||
item.setRightGraphic(btnRemove);
|
||||
item.setRightAction(SVG.CLOSE, () -> Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
|
||||
skinnable.authServersProperty().remove(server);
|
||||
}, null));
|
||||
|
||||
ObservableValue<String> title = BindingMapping.of(server, AuthlibInjectorServer::getName);
|
||||
item.titleProperty().bind(title);
|
||||
@@ -206,8 +194,7 @@ public final class AccountListPage extends DecoratorAnimatedPage implements Deco
|
||||
addAuthServerItem.getStyleClass().add("navigation-drawer-item");
|
||||
addAuthServerItem.setTitle(i18n("account.injector.add"));
|
||||
addAuthServerItem.setSubtitle(i18n("account.methods.authlib_injector"));
|
||||
addAuthServerItem.setActionButtonVisible(false);
|
||||
addAuthServerItem.setLeftGraphic(wrap(SVG.ADD_CIRCLE));
|
||||
addAuthServerItem.setLeftIcon(SVG.ADD_CIRCLE);
|
||||
addAuthServerItem.setOnAction(e -> Controllers.dialog(new AddAuthlibInjectorServerPane()));
|
||||
VBox.setMargin(addAuthServerItem, new Insets(0, 0, 12, 0));
|
||||
}
|
||||
|
||||
@@ -23,9 +23,7 @@ import javafx.application.Platform;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -45,7 +43,6 @@ import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||
import org.jackhuang.hmcl.game.OAuthServer;
|
||||
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -54,7 +51,6 @@ import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.upgrade.IntegrityChecker;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
@@ -85,13 +81,11 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
private final JFXButton btnAccept;
|
||||
private final SpinnerPane spinner;
|
||||
private final Node body;
|
||||
|
||||
private final HBox actions;
|
||||
private Node detailsPane; // AccountDetailsInputPane for Offline / Mojang / authlib-injector, Label for Microsoft
|
||||
private final Pane detailsContainer;
|
||||
|
||||
private final BooleanProperty logging = new SimpleBooleanProperty();
|
||||
private final ObjectProperty<OAuthServer.GrantDeviceCodeEvent> deviceCode = new SimpleObjectProperty<>();
|
||||
private final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
|
||||
private TaskExecutor loginTask;
|
||||
|
||||
@@ -146,10 +140,10 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
btnCancel.setOnAction(e -> onCancel());
|
||||
onEscPressed(this, btnCancel::fire);
|
||||
|
||||
HBox hbox = new HBox(spinner, btnCancel);
|
||||
hbox.setAlignment(Pos.CENTER_RIGHT);
|
||||
actions = new HBox(spinner, btnCancel);
|
||||
actions.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
setActions(lblErrorMessage, hbox);
|
||||
setActions(lblErrorMessage, actions);
|
||||
}
|
||||
|
||||
if (showMethodSwitcher) {
|
||||
@@ -226,7 +220,6 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
|
||||
Runnable doCreate = () -> {
|
||||
logging.set(true);
|
||||
deviceCode.set(null);
|
||||
|
||||
loginTask = Task.supplyAsync(() -> factory.create(new DialogCharacterSelector(), username, password, null, additionalData))
|
||||
.whenComplete(Schedulers.javafx(), account -> {
|
||||
@@ -280,93 +273,16 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
btnAccept.disableProperty().unbind();
|
||||
detailsContainer.getChildren().remove(detailsPane);
|
||||
lblErrorMessage.setText("");
|
||||
lblErrorMessage.setVisible(true);
|
||||
setActions(lblErrorMessage, actions);
|
||||
}
|
||||
|
||||
if (factory == Accounts.FACTORY_MICROSOFT) {
|
||||
VBox vbox = new VBox(8);
|
||||
detailsPane = vbox;
|
||||
|
||||
if (Accounts.OAUTH_CALLBACK.getClientId().isEmpty()) {
|
||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
|
||||
hintPane.setSegment(i18n("account.methods.microsoft.snapshot"));
|
||||
vbox.getChildren().add(hintPane);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IntegrityChecker.isOfficial()) {
|
||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
|
||||
hintPane.setSegment(i18n("unofficial.hint"));
|
||||
vbox.getChildren().add(hintPane);
|
||||
}
|
||||
|
||||
VBox codeBox = new VBox(8);
|
||||
Label hint = new Label(i18n("account.methods.microsoft.code"));
|
||||
Label code = new Label();
|
||||
code.setMouseTransparent(true);
|
||||
code.setStyle("-fx-font-size: 24");
|
||||
codeBox.getChildren().addAll(hint, code);
|
||||
codeBox.setAlignment(Pos.CENTER);
|
||||
vbox.getChildren().add(codeBox);
|
||||
|
||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||
HintPane errHintPane = new HintPane(MessageDialogPane.MessageType.ERROR);
|
||||
errHintPane.setVisible(false);
|
||||
errHintPane.setManaged(false);
|
||||
|
||||
codeBox.setVisible(false);
|
||||
codeBox.setManaged(false);
|
||||
|
||||
FXUtils.onChangeAndOperate(deviceCode, deviceCode -> {
|
||||
if (deviceCode != null) {
|
||||
FXUtils.copyText(deviceCode.getUserCode());
|
||||
code.setText(deviceCode.getUserCode());
|
||||
hintPane.setSegment(i18n("account.methods.microsoft.manual", deviceCode.getVerificationUri()));
|
||||
codeBox.setVisible(true);
|
||||
codeBox.setManaged(true);
|
||||
} else {
|
||||
hintPane.setSegment(i18n("account.methods.microsoft.hint"));
|
||||
codeBox.setVisible(false);
|
||||
codeBox.setManaged(false);
|
||||
}
|
||||
});
|
||||
|
||||
lblErrorMessage.setVisible(false);
|
||||
lblErrorMessage.textProperty().addListener((obs, oldVal, newVal) -> {
|
||||
boolean hasError = !newVal.isEmpty();
|
||||
errHintPane.setSegment(newVal);
|
||||
errHintPane.setVisible(hasError);
|
||||
errHintPane.setManaged(hasError);
|
||||
hintPane.setVisible(!hasError);
|
||||
hintPane.setManaged(!hasError);
|
||||
codeBox.setVisible(!hasError && deviceCode.get() != null);
|
||||
codeBox.setManaged(!hasError && deviceCode.get() != null);
|
||||
});
|
||||
|
||||
FXUtils.onClicked(codeBox, () -> {
|
||||
if (deviceCode.get() != null) FXUtils.copyText(deviceCode.get().getUserCode());
|
||||
});
|
||||
|
||||
holder.add(Accounts.OAUTH_CALLBACK.onGrantDeviceCode.registerWeak(value ->
|
||||
runInFX(() -> deviceCode.set(value))
|
||||
));
|
||||
|
||||
HBox linkBox = new HBox();
|
||||
JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile"));
|
||||
profileLink.setExternalLink("https://account.live.com/editprof.aspx");
|
||||
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.microsoft.purchase"));
|
||||
purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL);
|
||||
JFXHyperlink deauthorizeLink = new JFXHyperlink(i18n("account.methods.microsoft.deauthorize"));
|
||||
deauthorizeLink.setExternalLink("https://account.live.com/consent/Edit?client_id=000000004C794E0A");
|
||||
JFXHyperlink forgotpasswordLink = new JFXHyperlink(i18n("account.methods.forgot_password"));
|
||||
forgotpasswordLink.setExternalLink("https://account.live.com/ResetPassword.aspx");
|
||||
linkBox.getChildren().setAll(profileLink, purchaseLink, deauthorizeLink, forgotpasswordLink);
|
||||
|
||||
vbox.getChildren().addAll(hintPane, errHintPane, linkBox);
|
||||
btnAccept.setDisable(false);
|
||||
detailsPane = new MicrosoftAccountLoginPane(true);
|
||||
setActions();
|
||||
} else {
|
||||
detailsPane = new AccountDetailsInputPane(factory, btnAccept::fire);
|
||||
btnAccept.disableProperty().bind(((AccountDetailsInputPane) detailsPane).validProperty().not());
|
||||
setActions(lblErrorMessage, actions);
|
||||
}
|
||||
|
||||
detailsContainer.getChildren().add(detailsPane);
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
package org.jackhuang.hmcl.ui.account;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import com.jfoenix.controls.JFXSpinner;
|
||||
import io.nayuki.qrcodegen.QrCode;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.FlowPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.shape.SVGPath;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.OAuth;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.upgrade.IntegrityChecker;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.QrCodeUtils;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class MicrosoftAccountLoginPane extends JFXDialogLayout implements DialogAware {
|
||||
private final Account accountToRelogin;
|
||||
private final Consumer<AuthInfo> loginCallback;
|
||||
private final Runnable cancelCallback;
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
|
||||
private final ObjectProperty<Step> step = new SimpleObjectProperty<>();
|
||||
|
||||
private TaskExecutor browserTaskExecutor;
|
||||
private TaskExecutor deviceTaskExecutor;
|
||||
|
||||
private final JFXButton btnLogin;
|
||||
private final SpinnerPane loginButtonSpinner;
|
||||
|
||||
public MicrosoftAccountLoginPane() {
|
||||
this(false);
|
||||
}
|
||||
|
||||
public MicrosoftAccountLoginPane(boolean bodyonly) {
|
||||
this(null, null, null, bodyonly);
|
||||
}
|
||||
|
||||
public MicrosoftAccountLoginPane(Account account, Consumer<AuthInfo> callback, Runnable onCancel, boolean bodyonly) {
|
||||
this.accountToRelogin = account;
|
||||
this.loginCallback = callback;
|
||||
this.cancelCallback = onCancel;
|
||||
|
||||
getStyleClass().add("microsoft-login-dialog");
|
||||
if (bodyonly) {
|
||||
this.pseudoClassStateChanged(PseudoClass.getPseudoClass("bodyonly"), true);
|
||||
} else {
|
||||
Label heading = new Label(accountToRelogin != null ? i18n("account.login.refresh") : i18n("account.create.microsoft"));
|
||||
heading.getStyleClass().add("header-label");
|
||||
setHeading(heading);
|
||||
}
|
||||
|
||||
this.setMaxWidth(650);
|
||||
|
||||
onEscPressed(this, this::onCancel);
|
||||
|
||||
btnLogin = new JFXButton(i18n("account.login"));
|
||||
btnLogin.getStyleClass().add("dialog-accept");
|
||||
|
||||
loginButtonSpinner = new SpinnerPane();
|
||||
loginButtonSpinner.getStyleClass().add("small-spinner-pane");
|
||||
loginButtonSpinner.setContent(btnLogin);
|
||||
|
||||
JFXButton btnCancel = new JFXButton(i18n("button.cancel"));
|
||||
btnCancel.getStyleClass().add("dialog-cancel");
|
||||
btnCancel.setOnAction(e -> onCancel());
|
||||
|
||||
setActions(loginButtonSpinner, btnCancel);
|
||||
|
||||
holder.registerWeak(Accounts.OAUTH_CALLBACK.onOpenBrowserAuthorizationCode, event -> Platform.runLater(() -> {
|
||||
if (step.get() instanceof Step.StartAuthorizationCodeLogin)
|
||||
step.set(new Step.WaitForOpenBrowser(event.getUrl()));
|
||||
}));
|
||||
|
||||
holder.registerWeak(Accounts.OAUTH_CALLBACK.onGrantDeviceCode, event -> Platform.runLater(() -> {
|
||||
if (step.get() instanceof Step.StartDeviceCodeLogin)
|
||||
step.set(new Step.WaitForScanQrCode(event.getUserCode(), event.getVerificationUri()));
|
||||
}));
|
||||
|
||||
this.step.set(Accounts.OAUTH_CALLBACK.getClientId().isEmpty()
|
||||
? new Step.Init()
|
||||
: new Step.StartAuthorizationCodeLogin());
|
||||
FXUtils.onChangeAndOperate(step, this::onStep);
|
||||
}
|
||||
|
||||
private void onStep(Step currentStep) {
|
||||
VBox rootContainer = new VBox(10);
|
||||
setBody(rootContainer);
|
||||
rootContainer.setAlignment(Pos.TOP_CENTER);
|
||||
|
||||
if (Accounts.OAUTH_CALLBACK.getClientId().isEmpty()) {
|
||||
var snapshotHint = new HintPane(MessageDialogPane.MessageType.WARNING);
|
||||
snapshotHint.setSegment(i18n("account.methods.microsoft.snapshot"));
|
||||
rootContainer.getChildren().add(snapshotHint);
|
||||
btnLogin.setDisable(true);
|
||||
loginButtonSpinner.setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IntegrityChecker.isOfficial()) {
|
||||
var unofficialHintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
|
||||
unofficialHintPane.setSegment(i18n("unofficial.hint"));
|
||||
rootContainer.getChildren().add(unofficialHintPane);
|
||||
}
|
||||
|
||||
if (currentStep instanceof Step.Init) {
|
||||
btnLogin.setOnAction(e -> this.step.set(new Step.StartAuthorizationCodeLogin()));
|
||||
loginButtonSpinner.setLoading(false);
|
||||
|
||||
var hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||
hintPane.setText(i18n("account.methods.microsoft.hint"));
|
||||
rootContainer.getChildren().add(hintPane);
|
||||
} else if (currentStep instanceof Step.StartAuthorizationCodeLogin) {
|
||||
loginButtonSpinner.setLoading(true);
|
||||
cancelAllTasks();
|
||||
|
||||
rootContainer.getChildren().add(new JFXSpinner());
|
||||
|
||||
browserTaskExecutor = Task.supplyAsync(() -> Accounts.FACTORY_MICROSOFT.create(null, null, null, null, OAuth.GrantFlow.AUTHORIZATION_CODE))
|
||||
.whenComplete(Schedulers.javafx(), this::onLoginCompleted)
|
||||
.executor(true);
|
||||
} else if (currentStep instanceof Step.StartDeviceCodeLogin) {
|
||||
loginButtonSpinner.setLoading(true);
|
||||
cancelAllTasks();
|
||||
|
||||
rootContainer.getChildren().add(new JFXSpinner());
|
||||
|
||||
deviceTaskExecutor = Task.supplyAsync(() -> Accounts.FACTORY_MICROSOFT.create(null, null, null, null, OAuth.GrantFlow.DEVICE))
|
||||
.whenComplete(Schedulers.javafx(), this::onLoginCompleted)
|
||||
.executor(true);
|
||||
} else if (currentStep instanceof Step.WaitForOpenBrowser wait) {
|
||||
btnLogin.setOnAction(e -> {
|
||||
FXUtils.openLink(wait.url());
|
||||
loginButtonSpinner.setLoading(true);
|
||||
});
|
||||
loginButtonSpinner.setLoading(false);
|
||||
|
||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||
hintPane.setSegment(
|
||||
i18n("account.methods.microsoft.methods.browser.hint", StringUtils.escapeXmlAttribute(wait.url()), wait.url()),
|
||||
FXUtils::copyText
|
||||
);
|
||||
|
||||
rootContainer.getChildren().add(hintPane);
|
||||
} else if (currentStep instanceof Step.WaitForScanQrCode wait) {
|
||||
loginButtonSpinner.setLoading(true);
|
||||
|
||||
var deviceHint = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||
deviceHint.setSegment(i18n("account.methods.microsoft.methods.device.hint",
|
||||
StringUtils.escapeXmlAttribute(wait.verificationUri()),
|
||||
wait.verificationUri(),
|
||||
wait.userCode()
|
||||
));
|
||||
|
||||
var qrCode = new SVGPath();
|
||||
qrCode.fillProperty().bind(Themes.colorSchemeProperty().getPrimary());
|
||||
qrCode.setContent(QrCodeUtils.toSVGPath(QrCode.encodeText(wait.verificationUri() + "?otc=" + wait.userCode(), QrCode.Ecc.MEDIUM)));
|
||||
qrCode.setScaleX(3);
|
||||
qrCode.setScaleY(3);
|
||||
|
||||
var lblCode = new Label(wait.userCode());
|
||||
lblCode.getStyleClass().add("code-label");
|
||||
lblCode.setStyle("-fx-font-family: \"" + Lang.requireNonNullElse(config().getFontFamily(), FXUtils.DEFAULT_MONOSPACE_FONT) + "\";");
|
||||
|
||||
var codeBox = new StackPane(lblCode);
|
||||
codeBox.getStyleClass().add("code-box");
|
||||
codeBox.setCursor(Cursor.HAND);
|
||||
FXUtils.onClicked(codeBox, () -> FXUtils.copyText(wait.userCode()));
|
||||
codeBox.setMaxWidth(USE_PREF_SIZE);
|
||||
|
||||
rootContainer.getChildren().addAll(deviceHint, new Group(qrCode), codeBox);
|
||||
} else if (currentStep instanceof Step.LoginFailed failed) {
|
||||
btnLogin.setOnAction(e -> this.step.set(new Step.StartAuthorizationCodeLogin()));
|
||||
loginButtonSpinner.setLoading(false);
|
||||
cancelAllTasks();
|
||||
|
||||
HintPane errHintPane = new HintPane(MessageDialogPane.MessageType.ERROR);
|
||||
errHintPane.setText(failed.message());
|
||||
rootContainer.getChildren().add(errHintPane);
|
||||
}
|
||||
|
||||
var linkBox = new FlowPane(8, 8);
|
||||
linkBox.setAlignment(Pos.CENTER_LEFT);
|
||||
linkBox.setPrefWrapLength(500);
|
||||
|
||||
if (currentStep instanceof Step.Init || currentStep instanceof Step.StartAuthorizationCodeLogin || currentStep instanceof Step.WaitForOpenBrowser) {
|
||||
JFXHyperlink useQrCode = new JFXHyperlink(i18n("account.methods.microsoft.methods.device"));
|
||||
useQrCode.setOnAction(e -> this.step.set(new Step.StartDeviceCodeLogin()));
|
||||
linkBox.getChildren().add(useQrCode);
|
||||
} else if (currentStep instanceof Step.StartDeviceCodeLogin || currentStep instanceof Step.WaitForScanQrCode) {
|
||||
JFXHyperlink userBrowser = new JFXHyperlink(i18n("account.methods.microsoft.methods.browser"));
|
||||
userBrowser.setOnAction(e -> this.step.set(new Step.StartAuthorizationCodeLogin()));
|
||||
linkBox.getChildren().add(userBrowser);
|
||||
}
|
||||
|
||||
JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile"));
|
||||
profileLink.setExternalLink("https://account.live.com/editprof.aspx");
|
||||
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.microsoft.purchase"));
|
||||
purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL);
|
||||
|
||||
linkBox.getChildren().addAll(profileLink, purchaseLink);
|
||||
rootContainer.getChildren().add(linkBox);
|
||||
|
||||
setBody(rootContainer);
|
||||
}
|
||||
|
||||
private void cancelAllTasks() {
|
||||
if (browserTaskExecutor != null) browserTaskExecutor.cancel();
|
||||
if (deviceTaskExecutor != null) deviceTaskExecutor.cancel();
|
||||
}
|
||||
|
||||
private void onCancel() {
|
||||
cancelAllTasks();
|
||||
if (cancelCallback != null) cancelCallback.run();
|
||||
fireEvent(new DialogCloseEvent());
|
||||
}
|
||||
|
||||
private void onLoginCompleted(MicrosoftAccount account, Exception exception) {
|
||||
if (exception == null) {
|
||||
if (accountToRelogin != null) Accounts.getAccounts().remove(accountToRelogin);
|
||||
|
||||
int oldIndex = Accounts.getAccounts().indexOf(account);
|
||||
if (oldIndex == -1) {
|
||||
Accounts.getAccounts().add(account);
|
||||
} else {
|
||||
Accounts.getAccounts().remove(oldIndex);
|
||||
Accounts.getAccounts().add(oldIndex, account);
|
||||
}
|
||||
|
||||
Accounts.setSelectedAccount(account);
|
||||
|
||||
if (loginCallback != null) {
|
||||
try {
|
||||
loginCallback.accept(account.logIn());
|
||||
} catch (AuthenticationException e) {
|
||||
this.step.set(new Step.LoginFailed(Accounts.localizeErrorMessage(e)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
fireEvent(new DialogCloseEvent());
|
||||
} else if (!(exception instanceof CancellationException)) {
|
||||
this.step.set(new Step.LoginFailed(Accounts.localizeErrorMessage(exception)));
|
||||
}
|
||||
}
|
||||
|
||||
private sealed interface Step {
|
||||
final class Init implements Step {
|
||||
}
|
||||
|
||||
final class StartAuthorizationCodeLogin implements Step {
|
||||
}
|
||||
|
||||
record WaitForOpenBrowser(String url) implements Step {
|
||||
|
||||
}
|
||||
|
||||
final class StartDeviceCodeLogin implements Step {
|
||||
}
|
||||
|
||||
record WaitForScanQrCode(String userCode, String verificationUri) implements Step {
|
||||
|
||||
}
|
||||
|
||||
record LoginFailed(String message) implements Step {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
package org.jackhuang.hmcl.ui.account;
|
||||
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.auth.OAuthAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||
import org.jackhuang.hmcl.game.OAuthServer;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.HintPane;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class OAuthAccountLoginDialog extends DialogPane {
|
||||
private final OAuthAccount account;
|
||||
private final Consumer<AuthInfo> success;
|
||||
private final Runnable failed;
|
||||
private final ObjectProperty<OAuthServer.GrantDeviceCodeEvent> deviceCode = new SimpleObjectProperty<>();
|
||||
|
||||
private final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
|
||||
public OAuthAccountLoginDialog(OAuthAccount account, Consumer<AuthInfo> success, Runnable failed) {
|
||||
this.account = account;
|
||||
this.success = success;
|
||||
this.failed = failed;
|
||||
|
||||
setTitle(i18n("account.login.refresh"));
|
||||
|
||||
VBox vbox = new VBox(8);
|
||||
Label usernameLabel = new Label(account.getUsername());
|
||||
|
||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||
FXUtils.onChangeAndOperate(deviceCode, deviceCode -> {
|
||||
if (deviceCode != null) {
|
||||
FXUtils.copyText(deviceCode.getUserCode());
|
||||
hintPane.setSegment(
|
||||
"<b>" + i18n("account.login.refresh.microsoft.hint") + "</b>\n"
|
||||
+ i18n("account.methods.microsoft.manual", deviceCode.getUserCode(), deviceCode.getVerificationUri())
|
||||
);
|
||||
} else {
|
||||
hintPane.setSegment(
|
||||
"<b>" + i18n("account.login.refresh.microsoft.hint") + "</b>\n"
|
||||
+ i18n("account.methods.microsoft.hint")
|
||||
);
|
||||
}
|
||||
});
|
||||
FXUtils.onClicked(hintPane, () -> {
|
||||
if (deviceCode.get() != null) {
|
||||
FXUtils.copyText(deviceCode.get().getUserCode());
|
||||
}
|
||||
});
|
||||
|
||||
HBox box = new HBox(8);
|
||||
JFXHyperlink birthLink = new JFXHyperlink(i18n("account.methods.microsoft.birth"));
|
||||
birthLink.setExternalLink("https://support.microsoft.com/account-billing/how-to-change-a-birth-date-on-a-microsoft-account-837badbc-999e-54d2-2617-d19206b9540a");
|
||||
JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile"));
|
||||
profileLink.setExternalLink("https://account.live.com/editprof.aspx");
|
||||
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.microsoft.purchase"));
|
||||
purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL);
|
||||
box.getChildren().setAll(profileLink, birthLink, purchaseLink);
|
||||
GridPane.setColumnSpan(box, 2);
|
||||
|
||||
vbox.getChildren().setAll(usernameLabel, hintPane, box);
|
||||
setBody(vbox);
|
||||
|
||||
holder.add(Accounts.OAUTH_CALLBACK.onGrantDeviceCode.registerWeak(this::onGrantDeviceCode));
|
||||
}
|
||||
|
||||
private void onGrantDeviceCode(OAuthServer.GrantDeviceCodeEvent event) {
|
||||
FXUtils.runInFX(() -> {
|
||||
deviceCode.set(event);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAccept() {
|
||||
setLoading();
|
||||
Task.supplyAsync(account::logInWhenCredentialsExpired)
|
||||
.whenComplete(Schedulers.javafx(), (authInfo, exception) -> {
|
||||
if (exception == null) {
|
||||
success.accept(authInfo);
|
||||
onSuccess();
|
||||
} else {
|
||||
LOG.info("Failed to login when credentials expired: " + account, exception);
|
||||
onFailure(Accounts.localizeErrorMessage(exception));
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCancel() {
|
||||
failed.run();
|
||||
super.onCancel();
|
||||
}
|
||||
}
|
||||
@@ -29,9 +29,6 @@ import javafx.scene.control.Label;
|
||||
import javafx.scene.input.DragEvent;
|
||||
import javafx.scene.input.TransferMode;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.ui.skin.SkinCanvas;
|
||||
import org.jackhuang.hmcl.ui.skin.animation.SkinAniRunning;
|
||||
import org.jackhuang.hmcl.ui.skin.animation.SkinAniWavingArms;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||
import org.jackhuang.hmcl.auth.offline.Skin;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
|
||||
@@ -40,6 +37,9 @@ import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.skin.SkinCanvas;
|
||||
import org.jackhuang.hmcl.ui.skin.animation.SkinAniRunning;
|
||||
import org.jackhuang.hmcl.ui.skin.animation.SkinAniWavingArms;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.nio.file.Path;
|
||||
@@ -48,8 +48,8 @@ import java.util.UUID;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class OfflineAccountSkinPane extends StackPane {
|
||||
private final OfflineAccount account;
|
||||
@@ -110,6 +110,7 @@ public class OfflineAccountSkinPane extends StackPane {
|
||||
|
||||
cslApiField.setPromptText(i18n("account.skin.type.csl_api.location.hint"));
|
||||
cslApiField.setValidators(new URLValidator());
|
||||
FXUtils.setValidateWhileTextChanged(cslApiField, true);
|
||||
|
||||
skinItem.loadChildren(Arrays.asList(
|
||||
new MultiFileItem.Option<>(i18n("message.default"), Skin.Type.DEFAULT),
|
||||
@@ -218,6 +219,10 @@ public class OfflineAccountSkinPane extends StackPane {
|
||||
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
|
||||
onEscPressed(this, cancelButton::fire);
|
||||
|
||||
acceptButton.disableProperty().bind(
|
||||
skinItem.selectedDataProperty().isEqualTo(Skin.Type.CUSTOM_SKIN_LOADER_API)
|
||||
.and(cslApiField.activeValidatorProperty().isNotNull()));
|
||||
|
||||
layout.setActions(littleSkinLink, acceptButton, cancelButton);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
@@ -28,8 +27,8 @@ import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.SVGContainer;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -71,10 +70,9 @@ public class AdvancedListBox extends ScrollPane {
|
||||
private AdvancedListItem createNavigationDrawerItem(String title, SVG leftGraphic) {
|
||||
AdvancedListItem item = new AdvancedListItem();
|
||||
item.getStyleClass().add("navigation-drawer-item");
|
||||
item.setActionButtonVisible(false);
|
||||
item.setTitle(title);
|
||||
if (leftGraphic != null) {
|
||||
item.setLeftGraphic(FXUtils.wrap(leftGraphic));
|
||||
item.setLeftIcon(leftGraphic);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
@@ -107,18 +105,11 @@ public class AdvancedListBox extends ScrollPane {
|
||||
item.activeProperty().bind(tabHeader.getSelectionModel().selectedItemProperty().isEqualTo(tab));
|
||||
item.setOnAction(e -> tabHeader.select(tab));
|
||||
|
||||
Node unselectedIcon = unselectedGraphic.createIcon(20);
|
||||
Node selectedIcon = selectedGraphic.createIcon(20);
|
||||
|
||||
TransitionPane leftGraphic = new TransitionPane();
|
||||
leftGraphic.setAlignment(Pos.CENTER);
|
||||
FXUtils.setLimitWidth(leftGraphic, 30);
|
||||
FXUtils.setLimitHeight(leftGraphic, 20);
|
||||
leftGraphic.setPadding(Insets.EMPTY);
|
||||
leftGraphic.setContent(item.isActive() ? selectedIcon : unselectedIcon, ContainerAnimations.NONE);
|
||||
FXUtils.onChange(item.activeProperty(), active ->
|
||||
leftGraphic.setContent(active ? selectedIcon : unselectedIcon, ContainerAnimations.FADE));
|
||||
|
||||
var leftGraphic = new SVGContainer(item.isActive() ? selectedGraphic : unselectedGraphic, AdvancedListItem.LEFT_ICON_SIZE);
|
||||
leftGraphic.setMouseTransparent(true);
|
||||
AdvancedListItem.setAlignment(leftGraphic, Pos.CENTER);
|
||||
AdvancedListItem.setMargin(leftGraphic, AdvancedListItem.LEFT_ICON_MARGIN);
|
||||
FXUtils.onChange(item.activeProperty(), active -> leftGraphic.setIcon(active ? selectedGraphic : unselectedGraphic, Motion.SHORT4));
|
||||
item.setLeftGraphic(leftGraphic);
|
||||
return add(item);
|
||||
}
|
||||
|
||||
@@ -17,105 +17,134 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
|
||||
public class AdvancedListItem extends Control {
|
||||
private final ObjectProperty<Node> leftGraphic = new SimpleObjectProperty<>(this, "leftGraphic");
|
||||
private final ObjectProperty<Node> rightGraphic = new SimpleObjectProperty<>(this, "rightGraphic");
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title");
|
||||
private final BooleanProperty active = new SimpleBooleanProperty(this, "active");
|
||||
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
|
||||
private final BooleanProperty actionButtonVisible = new SimpleBooleanProperty(this, "actionButtonVisible", true);
|
||||
public static final double LEFT_GRAPHIC_SIZE = 32;
|
||||
public static final double LEFT_ICON_SIZE = 20;
|
||||
public static final Insets LEFT_ICON_MARGIN = new Insets(0, 6, 0, 6);
|
||||
|
||||
public static void setMargin(Node graphic, Insets margin) {
|
||||
BorderPane.setMargin(graphic, margin);
|
||||
}
|
||||
|
||||
public static void setAlignment(Node graphic, Pos alignment) {
|
||||
BorderPane.setAlignment(graphic, alignment);
|
||||
}
|
||||
|
||||
public AdvancedListItem() {
|
||||
getStyleClass().add("advanced-list-item");
|
||||
FXUtils.onClicked(this, () -> fireEvent(new ActionEvent()));
|
||||
}
|
||||
|
||||
public Node getLeftGraphic() {
|
||||
return leftGraphic.get();
|
||||
}
|
||||
private final ObjectProperty<Node> leftGraphic = new SimpleObjectProperty<>(this, "leftGraphic");
|
||||
|
||||
public ObjectProperty<Node> leftGraphicProperty() {
|
||||
return leftGraphic;
|
||||
}
|
||||
|
||||
public Node getLeftGraphic() {
|
||||
return leftGraphic.get();
|
||||
}
|
||||
|
||||
public void setLeftGraphic(Node leftGraphic) {
|
||||
this.leftGraphic.set(leftGraphic);
|
||||
}
|
||||
|
||||
public Node getRightGraphic() {
|
||||
return rightGraphic.get();
|
||||
public void setLeftIcon(SVG svg) {
|
||||
Node icon = svg.createIcon(LEFT_ICON_SIZE);
|
||||
icon.setMouseTransparent(true);
|
||||
BorderPane.setMargin(icon, LEFT_ICON_MARGIN);
|
||||
BorderPane.setAlignment(icon, Pos.CENTER);
|
||||
leftGraphicProperty().set(icon);
|
||||
}
|
||||
|
||||
private final ObjectProperty<Node> rightGraphic = new SimpleObjectProperty<>(this, "rightGraphic");
|
||||
|
||||
public ObjectProperty<Node> rightGraphicProperty() {
|
||||
return rightGraphic;
|
||||
}
|
||||
|
||||
public Node getRightGraphic() {
|
||||
return rightGraphic.get();
|
||||
}
|
||||
|
||||
public void setRightGraphic(Node rightGraphic) {
|
||||
this.rightGraphic.set(rightGraphic);
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title.get();
|
||||
public void setRightAction(SVG icon, Runnable action) {
|
||||
var button = new JFXButton();
|
||||
button.setOnAction(e -> {
|
||||
action.run();
|
||||
e.consume();
|
||||
});
|
||||
button.getStyleClass().add("toggle-icon4");
|
||||
button.setGraphic(icon.createIcon(14));
|
||||
setAlignment(button, Pos.CENTER);
|
||||
setRightGraphic(button);
|
||||
}
|
||||
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title");
|
||||
|
||||
public StringProperty titleProperty() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title.get();
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title.set(title);
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return active.get();
|
||||
}
|
||||
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
|
||||
|
||||
public BooleanProperty activeProperty() {
|
||||
return active;
|
||||
}
|
||||
|
||||
public void setActive(boolean active) {
|
||||
this.active.set(active);
|
||||
public StringProperty subtitleProperty() {
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
public String getSubtitle() {
|
||||
return subtitle.get();
|
||||
}
|
||||
|
||||
public StringProperty subtitleProperty() {
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
public void setSubtitle(String subtitle) {
|
||||
this.subtitle.set(subtitle);
|
||||
}
|
||||
|
||||
public boolean isActionButtonVisible() {
|
||||
return actionButtonVisible.get();
|
||||
private final BooleanProperty active = new SimpleBooleanProperty(this, "active");
|
||||
|
||||
public BooleanProperty activeProperty() {
|
||||
return active;
|
||||
}
|
||||
|
||||
public BooleanProperty actionButtonVisibleProperty() {
|
||||
return actionButtonVisible;
|
||||
public boolean isActive() {
|
||||
return active.get();
|
||||
}
|
||||
|
||||
public void setActionButtonVisible(boolean actionButtonVisible) {
|
||||
this.actionButtonVisible.set(actionButtonVisible);
|
||||
public void setActive(boolean active) {
|
||||
this.active.set(active);
|
||||
}
|
||||
|
||||
private final ObjectProperty<EventHandler<ActionEvent>> onAction = new SimpleObjectProperty<>(this, "onAction") {
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
setEventHandler(ActionEvent.ACTION, get());
|
||||
}
|
||||
};
|
||||
|
||||
public final ObjectProperty<EventHandler<ActionEvent>> onActionProperty() {
|
||||
return onAction;
|
||||
}
|
||||
@@ -128,32 +157,9 @@ public class AdvancedListItem extends Control {
|
||||
return onActionProperty().get();
|
||||
}
|
||||
|
||||
private ObjectProperty<EventHandler<ActionEvent>> onAction = new SimpleObjectProperty<EventHandler<ActionEvent>>(this, "onAction") {
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
setEventHandler(ActionEvent.ACTION, get());
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new AdvancedListItemSkin(this);
|
||||
}
|
||||
|
||||
public static Pair<Node, ImageView> createImageView(Image image) {
|
||||
return createImageView(image, 32, 32);
|
||||
}
|
||||
|
||||
public static Pair<Node, ImageView> createImageView(Image image, double width, double height) {
|
||||
StackPane imageViewContainer = new StackPane();
|
||||
FXUtils.setLimitWidth(imageViewContainer, width);
|
||||
FXUtils.setLimitHeight(imageViewContainer, height);
|
||||
|
||||
ImageView imageView = new ImageView();
|
||||
FXUtils.limitSize(imageView, width, height);
|
||||
imageView.setPreserveRatio(true);
|
||||
imageView.setImage(image);
|
||||
imageViewContainer.getChildren().setAll(imageView);
|
||||
return pair(imageViewContainer, imageView);
|
||||
}
|
||||
}
|
||||
@@ -18,14 +18,12 @@
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
|
||||
private final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
|
||||
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
|
||||
|
||||
public AdvancedListItemSkin(AdvancedListItem skinnable) {
|
||||
super(skinnable);
|
||||
@@ -40,41 +38,14 @@ public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
|
||||
|
||||
RipplerContainer container = new RipplerContainer(root);
|
||||
|
||||
HBox left = new HBox();
|
||||
left.setAlignment(Pos.CENTER_LEFT);
|
||||
left.setMouseTransparent(true);
|
||||
|
||||
TwoLineListItem item = new TwoLineListItem();
|
||||
root.setCenter(item);
|
||||
item.setMouseTransparent(true);
|
||||
item.titleProperty().bind(skinnable.titleProperty());
|
||||
item.subtitleProperty().bind(skinnable.subtitleProperty());
|
||||
|
||||
FXUtils.onChangeAndOperate(skinnable.leftGraphicProperty(),
|
||||
newGraphic -> {
|
||||
if (newGraphic == null) {
|
||||
left.getChildren().clear();
|
||||
} else {
|
||||
left.getChildren().setAll(newGraphic);
|
||||
}
|
||||
});
|
||||
root.setLeft(left);
|
||||
|
||||
HBox right = new HBox();
|
||||
right.setAlignment(Pos.CENTER);
|
||||
right.getStyleClass().add("toggle-icon4");
|
||||
FXUtils.setLimitWidth(right, 40);
|
||||
FXUtils.onChangeAndOperate(skinnable.rightGraphicProperty(),
|
||||
newGraphic -> {
|
||||
if (newGraphic == null) {
|
||||
right.getChildren().clear();
|
||||
} else {
|
||||
right.getChildren().setAll(newGraphic);
|
||||
}
|
||||
});
|
||||
|
||||
FXUtils.onChangeAndOperate(skinnable.actionButtonVisibleProperty(),
|
||||
visible -> root.setRight(visible ? right : null));
|
||||
root.leftProperty().bind(skinnable.leftGraphicProperty());
|
||||
root.rightProperty().bind(skinnable.rightGraphicProperty());
|
||||
|
||||
getChildren().setAll(container);
|
||||
}
|
||||
|
||||
@@ -17,13 +17,8 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.beans.DefaultProperty;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.PseudoClass;
|
||||
@@ -33,88 +28,21 @@ import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@DefaultProperty("content")
|
||||
public class ComponentList extends Control {
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title", "Group");
|
||||
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle", "");
|
||||
private final IntegerProperty depth = new SimpleIntegerProperty(this, "depth", 0);
|
||||
private boolean hasSubtitle = false;
|
||||
public final ObservableList<Node> content = FXCollections.observableArrayList();
|
||||
private Supplier<List<? extends Node>> lazyInitializer;
|
||||
public class ComponentList extends Control implements NoPaddingComponent {
|
||||
|
||||
public ComponentList() {
|
||||
getStyleClass().add("options-list");
|
||||
}
|
||||
|
||||
public ComponentList(Supplier<List<? extends Node>> lazyInitializer) {
|
||||
this();
|
||||
this.lazyInitializer = lazyInitializer;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title.get();
|
||||
}
|
||||
|
||||
public StringProperty titleProperty() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title.set(title);
|
||||
}
|
||||
|
||||
public String getSubtitle() {
|
||||
return subtitle.get();
|
||||
}
|
||||
|
||||
public StringProperty subtitleProperty() {
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
public void setSubtitle(String subtitle) {
|
||||
this.subtitle.set(subtitle);
|
||||
}
|
||||
|
||||
public int getDepth() {
|
||||
return depth.get();
|
||||
}
|
||||
|
||||
public IntegerProperty depthProperty() {
|
||||
return depth;
|
||||
}
|
||||
|
||||
public void setDepth(int depth) {
|
||||
this.depth.set(depth);
|
||||
}
|
||||
|
||||
public boolean isHasSubtitle() {
|
||||
return hasSubtitle;
|
||||
}
|
||||
|
||||
public void setHasSubtitle(boolean hasSubtitle) {
|
||||
this.hasSubtitle = hasSubtitle;
|
||||
}
|
||||
private final ObservableList<Node> content = FXCollections.observableArrayList();
|
||||
|
||||
public ObservableList<Node> getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
void doLazyInit() {
|
||||
if (lazyInitializer != null) {
|
||||
this.getContent().setAll(lazyInitializer.get());
|
||||
setNeedsLayout(true);
|
||||
lazyInitializer = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Orientation getContentBias() {
|
||||
return Orientation.HORIZONTAL;
|
||||
@@ -130,49 +58,72 @@ public class ComponentList extends Control {
|
||||
private static final PseudoClass PSEUDO_CLASS_LAST = PseudoClass.getPseudoClass("last");
|
||||
|
||||
private final ObservableList<Node> list;
|
||||
private final ObjectBinding<Node> firstItem;
|
||||
private final ObjectBinding<Node> lastItem;
|
||||
|
||||
Skin(ComponentList control) {
|
||||
super(control);
|
||||
|
||||
list = MappedObservableList.create(control.getContent(), node -> {
|
||||
ComponentListCell cell = new ComponentListCell(node);
|
||||
cell.getStyleClass().add("options-list-item");
|
||||
if (node.getProperties().containsKey("ComponentList.vgrow")) {
|
||||
VBox.setVgrow(cell, (Priority) node.getProperties().get("ComponentList.vgrow"));
|
||||
Pane wrapper;
|
||||
if (node instanceof ComponentSublist sublist) {
|
||||
sublist.getStyleClass().remove("options-list");
|
||||
sublist.getStyleClass().add("options-sublist");
|
||||
wrapper = new ComponentSublistWrapper(sublist);
|
||||
} else {
|
||||
wrapper = new StackPane(node);
|
||||
}
|
||||
if (node.getProperties().containsKey("ComponentList.noPadding")) {
|
||||
cell.getStyleClass().add("no-padding");
|
||||
|
||||
wrapper.getStyleClass().add("options-list-item");
|
||||
|
||||
if (node.getProperties().get("ComponentList.vgrow") instanceof Priority priority) {
|
||||
VBox.setVgrow(wrapper, priority);
|
||||
}
|
||||
return cell;
|
||||
|
||||
if (node instanceof NoPaddingComponent || node.getProperties().containsKey("ComponentList.noPadding")) {
|
||||
wrapper.getStyleClass().add("no-padding");
|
||||
}
|
||||
return wrapper;
|
||||
});
|
||||
|
||||
firstItem = Bindings.valueAt(list, 0);
|
||||
firstItem.addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue != null)
|
||||
newValue.pseudoClassStateChanged(PSEUDO_CLASS_FIRST, true);
|
||||
if (oldValue != null)
|
||||
oldValue.pseudoClassStateChanged(PSEUDO_CLASS_FIRST, false);
|
||||
});
|
||||
if (!list.isEmpty())
|
||||
list.get(0).pseudoClassStateChanged(PSEUDO_CLASS_FIRST, true);
|
||||
|
||||
lastItem = Bindings.valueAt(list, Bindings.subtract(Bindings.size(list), 1));
|
||||
lastItem.addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue != null)
|
||||
newValue.pseudoClassStateChanged(PSEUDO_CLASS_LAST, true);
|
||||
if (oldValue != null)
|
||||
oldValue.pseudoClassStateChanged(PSEUDO_CLASS_LAST, false);
|
||||
});
|
||||
if (!list.isEmpty())
|
||||
list.get(list.size() - 1).pseudoClassStateChanged(PSEUDO_CLASS_LAST, true);
|
||||
updateStyle();
|
||||
list.addListener((InvalidationListener) o -> updateStyle());
|
||||
|
||||
VBox vbox = new VBox();
|
||||
vbox.setFillWidth(true);
|
||||
Bindings.bindContent(vbox.getChildren(), list);
|
||||
node = vbox;
|
||||
}
|
||||
|
||||
private Node prevFirstItem;
|
||||
private Node prevLastItem;
|
||||
|
||||
private void updateStyle() {
|
||||
Node firstItem;
|
||||
Node lastItem;
|
||||
|
||||
if (list.isEmpty()) {
|
||||
firstItem = null;
|
||||
lastItem = null;
|
||||
} else {
|
||||
firstItem = list.get(0);
|
||||
lastItem = list.get(list.size() - 1);
|
||||
}
|
||||
|
||||
if (firstItem != prevFirstItem) {
|
||||
if (prevFirstItem != null)
|
||||
prevFirstItem.pseudoClassStateChanged(PSEUDO_CLASS_FIRST, false);
|
||||
if (firstItem != null)
|
||||
firstItem.pseudoClassStateChanged(PSEUDO_CLASS_FIRST, true);
|
||||
prevFirstItem = firstItem;
|
||||
}
|
||||
|
||||
if (lastItem != prevLastItem) {
|
||||
if (prevLastItem != null)
|
||||
prevLastItem.pseudoClassStateChanged(PSEUDO_CLASS_LAST, false);
|
||||
if (lastItem != null)
|
||||
lastItem.pseudoClassStateChanged(PSEUDO_CLASS_LAST, true);
|
||||
prevLastItem = lastItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Node createComponentListTitle(String title) {
|
||||
@@ -189,4 +140,8 @@ public class ComponentList extends Control {
|
||||
public static void setVgrow(Node node, Priority priority) {
|
||||
node.getProperties().put("ComponentList.vgrow", priority);
|
||||
}
|
||||
|
||||
public static void setNoPadding(Node node) {
|
||||
node.getProperties().put("ComponentList.noPadding", true);
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.animation.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
*/
|
||||
final class ComponentListCell extends StackPane {
|
||||
private final Node content;
|
||||
private Animation expandAnimation;
|
||||
private boolean expanded = false;
|
||||
|
||||
ComponentListCell(Node content) {
|
||||
this.content = content;
|
||||
|
||||
updateLayout();
|
||||
}
|
||||
|
||||
private void updateLayout() {
|
||||
if (content instanceof ComponentList list) {
|
||||
content.getStyleClass().remove("options-list");
|
||||
content.getStyleClass().add("options-sublist");
|
||||
|
||||
getStyleClass().add("no-padding");
|
||||
|
||||
VBox groupNode = new VBox();
|
||||
|
||||
Node expandIcon = SVG.KEYBOARD_ARROW_DOWN.createIcon(20);
|
||||
expandIcon.setMouseTransparent(true);
|
||||
HBox.setMargin(expandIcon, new Insets(0, 8, 0, 8));
|
||||
|
||||
VBox labelVBox = new VBox();
|
||||
labelVBox.setMouseTransparent(true);
|
||||
labelVBox.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
boolean overrideHeaderLeft = false;
|
||||
if (list instanceof ComponentSublist) {
|
||||
Node leftNode = ((ComponentSublist) list).getHeaderLeft();
|
||||
if (leftNode != null) {
|
||||
labelVBox.getChildren().setAll(leftNode);
|
||||
overrideHeaderLeft = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!overrideHeaderLeft) {
|
||||
Label label = new Label();
|
||||
label.textProperty().bind(list.titleProperty());
|
||||
label.getStyleClass().add("title-label");
|
||||
labelVBox.getChildren().add(label);
|
||||
|
||||
if (list.isHasSubtitle()) {
|
||||
Label subtitleLabel = new Label();
|
||||
subtitleLabel.textProperty().bind(list.subtitleProperty());
|
||||
subtitleLabel.getStyleClass().add("subtitle-label");
|
||||
subtitleLabel.textFillProperty().bind(Themes.colorSchemeProperty().getOnSurfaceVariant());
|
||||
labelVBox.getChildren().add(subtitleLabel);
|
||||
}
|
||||
}
|
||||
|
||||
HBox header = new HBox();
|
||||
header.setSpacing(16);
|
||||
header.getChildren().add(labelVBox);
|
||||
header.setPadding(new Insets(10, 16, 10, 16));
|
||||
header.setAlignment(Pos.CENTER_LEFT);
|
||||
HBox.setHgrow(labelVBox, Priority.ALWAYS);
|
||||
if (list instanceof ComponentSublist) {
|
||||
Node rightNode = ((ComponentSublist) list).getHeaderRight();
|
||||
if (rightNode != null)
|
||||
header.getChildren().add(rightNode);
|
||||
}
|
||||
header.getChildren().add(expandIcon);
|
||||
|
||||
RipplerContainer headerRippler = new RipplerContainer(header);
|
||||
groupNode.getChildren().add(headerRippler);
|
||||
|
||||
VBox container = new VBox();
|
||||
boolean hasPadding = !(list instanceof ComponentSublist subList) || subList.hasComponentPadding();
|
||||
if (hasPadding) {
|
||||
container.setPadding(new Insets(8, 16, 10, 16));
|
||||
}
|
||||
FXUtils.setLimitHeight(container, 0);
|
||||
FXUtils.setOverflowHidden(container);
|
||||
container.getChildren().setAll(content);
|
||||
groupNode.getChildren().add(container);
|
||||
|
||||
headerRippler.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
|
||||
if (event.getButton() != MouseButton.PRIMARY)
|
||||
return;
|
||||
|
||||
event.consume();
|
||||
|
||||
if (expandAnimation != null && expandAnimation.getStatus() == Animation.Status.RUNNING) {
|
||||
expandAnimation.stop();
|
||||
}
|
||||
|
||||
boolean expanded = !this.expanded;
|
||||
this.expanded = expanded;
|
||||
if (expanded) {
|
||||
list.doLazyInit();
|
||||
list.layout();
|
||||
}
|
||||
|
||||
Platform.runLater(() -> {
|
||||
// FIXME: ComponentSubList without padding must have a 4 pixel padding for displaying a border radius.
|
||||
double newAnimatedHeight = (list.prefHeight(list.getWidth()) + (hasPadding ? 8 + 10 : 4)) * (expanded ? 1 : -1);
|
||||
double contentHeight = expanded ? newAnimatedHeight : 0;
|
||||
double targetRotate = expanded ? -180 : 0;
|
||||
|
||||
if (AnimationUtils.isAnimationEnabled()) {
|
||||
double currentRotate = expandIcon.getRotate();
|
||||
Duration duration = Motion.LONG2.multiply(Math.abs(currentRotate - targetRotate) / 180.0);
|
||||
Interpolator interpolator = Motion.EASE_IN_OUT_CUBIC_EMPHASIZED;
|
||||
|
||||
expandAnimation = new Timeline(
|
||||
new KeyFrame(duration,
|
||||
new KeyValue(container.minHeightProperty(), contentHeight, interpolator),
|
||||
new KeyValue(container.maxHeightProperty(), contentHeight, interpolator),
|
||||
new KeyValue(expandIcon.rotateProperty(), targetRotate, interpolator))
|
||||
);
|
||||
|
||||
expandAnimation.play();
|
||||
} else {
|
||||
container.setMinHeight(contentHeight);
|
||||
container.setMaxHeight(contentHeight);
|
||||
expandIcon.setRotate(targetRotate);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
getChildren().setAll(groupNode);
|
||||
} else {
|
||||
getStyleClass().remove("no-padding");
|
||||
getChildren().setAll(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,57 +17,100 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.beans.DefaultProperty;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.scene.Node;
|
||||
|
||||
@DefaultProperty("content")
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class ComponentSublist extends ComponentList {
|
||||
|
||||
private final ObjectProperty<Node> headerLeft = new SimpleObjectProperty<>(this, "headerLeft");
|
||||
private final ObjectProperty<Node> headerRight = new SimpleObjectProperty<>(this, "headerRight");
|
||||
private final BooleanProperty componentPadding = new SimpleBooleanProperty(this, "componentPadding", true);
|
||||
Supplier<List<? extends Node>> lazyInitializer;
|
||||
|
||||
public ComponentSublist() {
|
||||
super();
|
||||
}
|
||||
|
||||
public Node getHeaderLeft() {
|
||||
return headerLeft.get();
|
||||
public ComponentSublist(Supplier<List<? extends Node>> lazyInitializer) {
|
||||
this.lazyInitializer = lazyInitializer;
|
||||
}
|
||||
|
||||
public ObjectProperty<Node> headerLeftProperty() {
|
||||
void doLazyInit() {
|
||||
if (lazyInitializer != null) {
|
||||
this.getContent().setAll(lazyInitializer.get());
|
||||
setNeedsLayout(true);
|
||||
lazyInitializer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title", "Group");
|
||||
|
||||
public StringProperty titleProperty() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return titleProperty().get();
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
titleProperty().set(title);
|
||||
}
|
||||
|
||||
private StringProperty subtitle;
|
||||
|
||||
public StringProperty subtitleProperty() {
|
||||
if (subtitle == null)
|
||||
subtitle = new SimpleStringProperty(this, "subtitle", "");
|
||||
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
public String getSubtitle() {
|
||||
return subtitleProperty().get();
|
||||
}
|
||||
|
||||
public void setSubtitle(String subtitle) {
|
||||
subtitleProperty().set(subtitle);
|
||||
}
|
||||
|
||||
private boolean hasSubtitle = false;
|
||||
|
||||
public boolean isHasSubtitle() {
|
||||
return hasSubtitle;
|
||||
}
|
||||
|
||||
public void setHasSubtitle(boolean hasSubtitle) {
|
||||
this.hasSubtitle = hasSubtitle;
|
||||
}
|
||||
|
||||
private Node headerLeft;
|
||||
|
||||
public Node getHeaderLeft() {
|
||||
return headerLeft;
|
||||
}
|
||||
|
||||
public void setHeaderLeft(Node headerLeft) {
|
||||
this.headerLeft.set(headerLeft);
|
||||
this.headerLeft = headerLeft;
|
||||
}
|
||||
|
||||
private Node headerRight;
|
||||
|
||||
public Node getHeaderRight() {
|
||||
return headerRight.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<Node> headerRightProperty() {
|
||||
return headerRight;
|
||||
}
|
||||
|
||||
public void setHeaderRight(Node headerRight) {
|
||||
this.headerRight.set(headerRight);
|
||||
this.headerRight = headerRight;
|
||||
}
|
||||
|
||||
private boolean componentPadding = true;
|
||||
|
||||
public boolean hasComponentPadding() {
|
||||
return componentPadding.get();
|
||||
}
|
||||
|
||||
public BooleanProperty componentPaddingProperty() {
|
||||
return componentPadding;
|
||||
}
|
||||
|
||||
public void setComponentPadding(boolean componentPadding) {
|
||||
this.componentPadding.set(componentPadding);
|
||||
this.componentPadding = componentPadding;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.animation.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
|
||||
/// @author Glavo
|
||||
final class ComponentSublistWrapper extends VBox implements NoPaddingComponent {
|
||||
private VBox container;
|
||||
|
||||
private Animation expandAnimation;
|
||||
private boolean expanded = false;
|
||||
|
||||
ComponentSublistWrapper(ComponentSublist sublist) {
|
||||
boolean noPadding = !sublist.hasComponentPadding();
|
||||
|
||||
this.getStyleClass().add("options-sublist-wrapper");
|
||||
|
||||
Node expandIcon = SVG.KEYBOARD_ARROW_DOWN.createIcon(20);
|
||||
expandIcon.getStyleClass().add("expand-icon");
|
||||
expandIcon.setMouseTransparent(true);
|
||||
|
||||
VBox labelVBox = new VBox();
|
||||
labelVBox.setMouseTransparent(true);
|
||||
labelVBox.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
Node leftNode = sublist.getHeaderLeft();
|
||||
if (leftNode == null) {
|
||||
Label label = new Label();
|
||||
label.textProperty().bind(sublist.titleProperty());
|
||||
label.getStyleClass().add("title-label");
|
||||
labelVBox.getChildren().add(label);
|
||||
|
||||
if (sublist.isHasSubtitle()) {
|
||||
Label subtitleLabel = new Label();
|
||||
subtitleLabel.textProperty().bind(sublist.subtitleProperty());
|
||||
subtitleLabel.getStyleClass().add("subtitle-label");
|
||||
subtitleLabel.textFillProperty().bind(Themes.colorSchemeProperty().getOnSurfaceVariant());
|
||||
labelVBox.getChildren().add(subtitleLabel);
|
||||
}
|
||||
} else {
|
||||
labelVBox.getChildren().setAll(leftNode);
|
||||
}
|
||||
|
||||
HBox header = new HBox();
|
||||
header.setSpacing(12);
|
||||
header.getChildren().add(labelVBox);
|
||||
header.setPadding(new Insets(10, 16, 10, 16));
|
||||
header.setAlignment(Pos.CENTER_LEFT);
|
||||
HBox.setHgrow(labelVBox, Priority.ALWAYS);
|
||||
Node rightNode = sublist.getHeaderRight();
|
||||
if (rightNode != null)
|
||||
header.getChildren().add(rightNode);
|
||||
header.getChildren().add(expandIcon);
|
||||
|
||||
RipplerContainer headerRippler = new RipplerContainer(header);
|
||||
this.getChildren().add(headerRippler);
|
||||
|
||||
headerRippler.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
|
||||
if (event.getButton() != MouseButton.PRIMARY)
|
||||
return;
|
||||
|
||||
event.consume();
|
||||
|
||||
if (expandAnimation != null && expandAnimation.getStatus() == Animation.Status.RUNNING) {
|
||||
expandAnimation.stop();
|
||||
}
|
||||
|
||||
boolean expanded = !this.expanded;
|
||||
this.expanded = expanded;
|
||||
if (expanded) {
|
||||
sublist.doLazyInit();
|
||||
|
||||
if (container == null) {
|
||||
this.container = new VBox();
|
||||
|
||||
if (!noPadding) {
|
||||
container.setPadding(new Insets(8, 16, 10, 16));
|
||||
}
|
||||
FXUtils.setLimitHeight(container, 0);
|
||||
FXUtils.setOverflowHidden(container);
|
||||
container.getChildren().setAll(sublist);
|
||||
ComponentSublistWrapper.this.getChildren().add(container);
|
||||
|
||||
this.applyCss();
|
||||
}
|
||||
|
||||
this.layout();
|
||||
}
|
||||
|
||||
Platform.runLater(() -> {
|
||||
// FIXME: ComponentSubList without padding must have a 4 pixel padding for displaying a border radius.
|
||||
double contentHeight = expanded ? (sublist.prefHeight(sublist.getWidth()) + (noPadding ? 4 : 8 + 10)) : 0;
|
||||
double targetRotate = expanded ? -180 : 0;
|
||||
|
||||
if (AnimationUtils.isAnimationEnabled()) {
|
||||
double currentRotate = expandIcon.getRotate();
|
||||
Duration duration = Motion.LONG2.multiply(Math.abs(currentRotate - targetRotate) / 180.0);
|
||||
Interpolator interpolator = Motion.EASE_IN_OUT_CUBIC_EMPHASIZED;
|
||||
|
||||
expandAnimation = new Timeline(
|
||||
new KeyFrame(duration,
|
||||
new KeyValue(container.minHeightProperty(), contentHeight, interpolator),
|
||||
new KeyValue(container.maxHeightProperty(), contentHeight, interpolator),
|
||||
new KeyValue(expandIcon.rotateProperty(), targetRotate, interpolator))
|
||||
);
|
||||
|
||||
expandAnimation.play();
|
||||
} else {
|
||||
container.setMinHeight(contentHeight);
|
||||
container.setMaxHeight(contentHeight);
|
||||
expandIcon.setRotate(targetRotate);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.DirectoryChooser;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class FileItem extends BorderPane {
|
||||
private final Label lblPath = new Label();
|
||||
|
||||
private final SimpleStringProperty name = new SimpleStringProperty(this, "name");
|
||||
private final SimpleStringProperty title = new SimpleStringProperty(this, "title");
|
||||
private final SimpleStringProperty path = new SimpleStringProperty(this, "path");
|
||||
private final SimpleBooleanProperty convertToRelativePath = new SimpleBooleanProperty(this, "convertToRelativePath");
|
||||
|
||||
public FileItem() {
|
||||
VBox left = new VBox();
|
||||
Label name = new Label();
|
||||
name.textProperty().bind(nameProperty());
|
||||
lblPath.getStyleClass().addAll("subtitle-label");
|
||||
lblPath.textProperty().bind(path);
|
||||
left.getChildren().addAll(name, lblPath);
|
||||
setLeft(left);
|
||||
|
||||
JFXButton right = new JFXButton();
|
||||
right.setGraphic(SVG.EDIT.createIcon(16));
|
||||
right.getStyleClass().add("toggle-icon4");
|
||||
right.setOnAction(e -> onExplore());
|
||||
FXUtils.installFastTooltip(right, i18n("button.edit"));
|
||||
setRight(right);
|
||||
|
||||
convertToRelativePath.addListener(onInvalidating(() -> path.set(processPath(path.get()))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given path to absolute/relative(if possible) path according to {@link #convertToRelativePathProperty()}.
|
||||
*/
|
||||
private String processPath(String path) {
|
||||
Path given;
|
||||
try {
|
||||
given = Path.of(path).toAbsolutePath().normalize();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return path;
|
||||
}
|
||||
|
||||
if (isConvertToRelativePath()) {
|
||||
try {
|
||||
return Metadata.CURRENT_DIRECTORY.relativize(given).normalize().toString();
|
||||
} catch (IllegalArgumentException e) {
|
||||
// the given path can't be relativized against current path
|
||||
}
|
||||
}
|
||||
return given.normalize().toString();
|
||||
}
|
||||
|
||||
public void onExplore() {
|
||||
DirectoryChooser chooser = new DirectoryChooser();
|
||||
if (path.get() != null) {
|
||||
Path file;
|
||||
try {
|
||||
file = Path.of(path.get());
|
||||
if (Files.exists(file)) {
|
||||
if (Files.isRegularFile(file))
|
||||
file = file.toAbsolutePath().normalize().getParent();
|
||||
else if (Files.isDirectory(file))
|
||||
file = file.toAbsolutePath().normalize();
|
||||
chooser.setInitialDirectory(file.toFile());
|
||||
}
|
||||
} catch (InvalidPathException e) {
|
||||
LOG.warning("Failed to resolve path: " + path.get());
|
||||
}
|
||||
}
|
||||
chooser.titleProperty().bind(titleProperty());
|
||||
var selectedDir = chooser.showDialog(Controllers.getStage());
|
||||
if (selectedDir != null) {
|
||||
path.set(processPath(selectedDir.toString()));
|
||||
}
|
||||
chooser.titleProperty().unbind();
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name.get();
|
||||
}
|
||||
|
||||
public StringProperty nameProperty() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name.set(name);
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title.get();
|
||||
}
|
||||
|
||||
public StringProperty titleProperty() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title.set(title);
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path.get();
|
||||
}
|
||||
|
||||
public StringProperty pathProperty() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public void setPath(String path) {
|
||||
this.path.set(path);
|
||||
}
|
||||
|
||||
public boolean isConvertToRelativePath() {
|
||||
return convertToRelativePath.get();
|
||||
}
|
||||
|
||||
public BooleanProperty convertToRelativePathProperty() {
|
||||
return convertToRelativePath;
|
||||
}
|
||||
|
||||
public void setConvertToRelativePath(boolean convertToRelativePath) {
|
||||
this.convertToRelativePath.set(convertToRelativePath);
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class HintPane extends VBox {
|
||||
private final Text label = new Text();
|
||||
@@ -66,7 +67,11 @@ public class HintPane extends VBox {
|
||||
}
|
||||
|
||||
public void setSegment(String segment) {
|
||||
flow.getChildren().setAll(FXUtils.parseSegment(segment, Controllers::onHyperlinkAction));
|
||||
this.setSegment(segment, Controllers::onHyperlinkAction);
|
||||
}
|
||||
|
||||
public void setSegment(String segment, Consumer<String> hyperlinkAction) {
|
||||
flow.getChildren().setAll(FXUtils.parseSegment(segment, hyperlinkAction));
|
||||
}
|
||||
|
||||
public void setChildren(Node... children) {
|
||||
|
||||
@@ -24,7 +24,7 @@ import org.jackhuang.hmcl.ui.SVG;
|
||||
public final class IconedMenuItem extends IconedItem {
|
||||
|
||||
public IconedMenuItem(SVG icon, String text, Runnable action, JFXPopup popup) {
|
||||
super(icon != null ? FXUtils.limitingSize(icon.createIcon(14), 14, 14) : null, text);
|
||||
super(icon != null ? icon.createIcon(14) : null, text);
|
||||
|
||||
getStyleClass().setAll("iconed-menu-item");
|
||||
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.beans.InvalidationListener;
|
||||
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.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
public class IconedTwoLineListItem extends HBox {
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title");
|
||||
private final ObservableList<Label> tags = FXCollections.observableArrayList();
|
||||
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
|
||||
private final StringProperty externalLink = new SimpleStringProperty(this, "externalLink");
|
||||
private final ObjectProperty<Image> image = new SimpleObjectProperty<>(this, "image");
|
||||
|
||||
private final ImageView imageView = new ImageView();
|
||||
private final TwoLineListItem twoLineListItem = new TwoLineListItem();
|
||||
private JFXButton externalLinkButton;
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final InvalidationListener observer;
|
||||
|
||||
public IconedTwoLineListItem() {
|
||||
setAlignment(Pos.CENTER);
|
||||
setSpacing(16);
|
||||
imageView.imageProperty().bind(image);
|
||||
twoLineListItem.titleProperty().bind(title);
|
||||
twoLineListItem.subtitleProperty().bind(subtitle);
|
||||
HBox.setHgrow(twoLineListItem, Priority.ALWAYS);
|
||||
Bindings.bindContent(twoLineListItem.getTags(), tags);
|
||||
|
||||
observer = FXUtils.observeWeak(() -> {
|
||||
getChildren().clear();
|
||||
if (image.get() != null) getChildren().add(imageView);
|
||||
getChildren().add(twoLineListItem);
|
||||
if (StringUtils.isNotBlank(externalLink.get())) getChildren().add(getExternalLinkButton());
|
||||
}, image, externalLink);
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title.get();
|
||||
}
|
||||
|
||||
public StringProperty titleProperty() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title.set(title);
|
||||
}
|
||||
|
||||
public ObservableList<Label> getTags() {
|
||||
return tags;
|
||||
}
|
||||
|
||||
public String getSubtitle() {
|
||||
return subtitle.get();
|
||||
}
|
||||
|
||||
public StringProperty subtitleProperty() {
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
public void setSubtitle(String subtitle) {
|
||||
this.subtitle.set(subtitle);
|
||||
}
|
||||
|
||||
public String getExternalLink() {
|
||||
return externalLink.get();
|
||||
}
|
||||
|
||||
public StringProperty externalLinkProperty() {
|
||||
return externalLink;
|
||||
}
|
||||
|
||||
public void setExternalLink(String externalLink) {
|
||||
this.externalLink.set(externalLink);
|
||||
}
|
||||
|
||||
public Image getImage() {
|
||||
return image.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<Image> imageProperty() {
|
||||
return image;
|
||||
}
|
||||
|
||||
public void setImage(Image image) {
|
||||
this.image.set(image);
|
||||
}
|
||||
|
||||
public ImageView getImageView() {
|
||||
return imageView;
|
||||
}
|
||||
|
||||
public JFXButton getExternalLinkButton() {
|
||||
if (externalLinkButton == null) {
|
||||
externalLinkButton = new JFXButton();
|
||||
externalLinkButton.getStyleClass().add("toggle-icon4");
|
||||
externalLinkButton.setGraphic(SVG.OPEN_IN_NEW.createIcon());
|
||||
externalLinkButton.setOnAction(e -> FXUtils.openLink(externalLink.get()));
|
||||
}
|
||||
return externalLinkButton;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.beans.property.*;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.OverrunStyle;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
|
||||
/// @author Glavo
|
||||
public class LineButton extends LineButtonBase {
|
||||
private static final String DEFAULT_STYLE_CLASS = "line-button";
|
||||
|
||||
private static final int IDX_TRAILING_TEXT = IDX_TRAILING;
|
||||
private static final int IDX_TRAILING_ICON = IDX_TRAILING + 1;
|
||||
|
||||
public static LineButton createNavigationButton() {
|
||||
var button = new LineButton();
|
||||
button.setTrailingIcon(SVG.ARROW_FORWARD);
|
||||
return button;
|
||||
}
|
||||
|
||||
public static LineButton createExternalLinkButton(String url) {
|
||||
var button = new LineButton();
|
||||
button.setTrailingIcon(SVG.OPEN_IN_NEW);
|
||||
if (url != null) {
|
||||
button.setOnAction(event -> FXUtils.openLink(url));
|
||||
}
|
||||
return button;
|
||||
}
|
||||
|
||||
public LineButton() {
|
||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
container.setMouseTransparent(true);
|
||||
}
|
||||
|
||||
private ObjectProperty<EventHandler<ActionEvent>> onAction;
|
||||
|
||||
public ObjectProperty<EventHandler<ActionEvent>> onActionProperty() {
|
||||
if (onAction == null) {
|
||||
onAction = new ObjectPropertyBase<>() {
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return LineButton.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "onAction";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
setEventHandler(ActionEvent.ACTION, get());
|
||||
}
|
||||
};
|
||||
}
|
||||
return onAction;
|
||||
}
|
||||
|
||||
public EventHandler<ActionEvent> getOnAction() {
|
||||
return onActionProperty().get();
|
||||
}
|
||||
|
||||
public void setOnAction(EventHandler<ActionEvent> value) {
|
||||
onActionProperty().set(value);
|
||||
}
|
||||
|
||||
private StringProperty trailingText;
|
||||
|
||||
public StringProperty trailingTextProperty() {
|
||||
if (trailingText == null) {
|
||||
trailingText = new StringPropertyBase() {
|
||||
private Label trailingTextLabel;
|
||||
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return LineButton.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "trailingText";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
String message = get();
|
||||
if (message != null && !message.isEmpty()) {
|
||||
if (trailingTextLabel == null) {
|
||||
trailingTextLabel = new Label();
|
||||
trailingTextLabel.getStyleClass().add("trailing-label");
|
||||
trailingTextLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
|
||||
}
|
||||
trailingTextLabel.setText(message);
|
||||
setNode(IDX_TRAILING_TEXT, trailingTextLabel);
|
||||
} else if (trailingTextLabel != null) {
|
||||
trailingTextLabel.setText("");
|
||||
setNode(IDX_TRAILING_TEXT, null);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return trailingText;
|
||||
}
|
||||
|
||||
public String getTrailingText() {
|
||||
return trailingText != null ? trailingText.get() : null;
|
||||
}
|
||||
|
||||
public void setTrailingText(String trailingText) {
|
||||
trailingTextProperty().set(trailingText);
|
||||
}
|
||||
|
||||
private ObjectProperty<Node> trailingIcon;
|
||||
|
||||
public ObjectProperty<Node> trailingIconProperty() {
|
||||
if (trailingIcon == null)
|
||||
trailingIcon = new ObjectPropertyBase<>() {
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return LineButton.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "trailingIcon";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
setNode(IDX_TRAILING_ICON, get());
|
||||
}
|
||||
};
|
||||
|
||||
return trailingIcon;
|
||||
}
|
||||
|
||||
public Node getTrailingIcon() {
|
||||
return trailingIcon != null ? trailingIcon.get() : null;
|
||||
}
|
||||
|
||||
public void setTrailingIcon(Node trailingIcon) {
|
||||
trailingIconProperty().set(trailingIcon);
|
||||
}
|
||||
|
||||
public void setTrailingIcon(SVG rightIcon) {
|
||||
setTrailingIcon(rightIcon, 20);
|
||||
}
|
||||
|
||||
public void setTrailingIcon(SVG rightIcon, double size) {
|
||||
Node rightIconNode = rightIcon.createIcon(size);
|
||||
rightIconNode.getStyleClass().add("trailing-icon");
|
||||
setTrailingIcon(rightIconNode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.event.ActionEvent;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
/// @author Glavo
|
||||
public abstract class LineButtonBase extends LineComponent {
|
||||
private static final String DEFAULT_STYLE_CLASS = "line-button-base";
|
||||
|
||||
protected final RipplerContainer ripplerContainer;
|
||||
|
||||
public LineButtonBase() {
|
||||
this.getStyleClass().addAll(LineButtonBase.DEFAULT_STYLE_CLASS);
|
||||
|
||||
this.ripplerContainer = new RipplerContainer(container);
|
||||
FXUtils.onClicked(this, this::fire);
|
||||
|
||||
this.getChildren().setAll(ripplerContainer);
|
||||
}
|
||||
|
||||
public void fire() {
|
||||
fireEvent(new ActionEvent());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ObjectPropertyBase;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.property.StringPropertyBase;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
/// @author Glavo
|
||||
public abstract class LineComponent extends StackPane implements NoPaddingComponent {
|
||||
private static final String DEFAULT_STYLE_CLASS = "line-component";
|
||||
private static final double MIN_HEIGHT = 48.0;
|
||||
private static final PseudoClass PSEUDO_LARGER_TITLE = PseudoClass.getPseudoClass("large-title");
|
||||
|
||||
protected static final int IDX_LEADING = 0;
|
||||
protected static final int IDX_TITLE = 1;
|
||||
protected static final int IDX_TRAILING = 2;
|
||||
|
||||
public static final double SPACING = 12;
|
||||
public static final double DEFAULT_ICON_SIZE = 20;
|
||||
|
||||
public static void setMargin(Node child, Insets value) {
|
||||
HBox.setMargin(child, value);
|
||||
}
|
||||
|
||||
protected final HBox container;
|
||||
|
||||
private final Label titleLabel;
|
||||
private final VBox titleContainer;
|
||||
|
||||
public LineComponent() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
|
||||
this.setMinHeight(MIN_HEIGHT);
|
||||
|
||||
this.container = new HBox(SPACING);
|
||||
container.getStyleClass().add("line-component-container");
|
||||
container.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
this.titleLabel = new Label();
|
||||
titleLabel.getStyleClass().add("title-label");
|
||||
titleLabel.setMinWidth(Region.USE_PREF_SIZE);
|
||||
|
||||
this.titleContainer = new VBox(titleLabel);
|
||||
titleContainer.getStyleClass().add("title-container");
|
||||
titleContainer.setMouseTransparent(true);
|
||||
titleContainer.setAlignment(Pos.CENTER_LEFT);
|
||||
titleContainer.minWidthProperty().bind(titleLabel.prefWidthProperty());
|
||||
HBox.setHgrow(titleContainer, Priority.ALWAYS);
|
||||
|
||||
this.setNode(IDX_TITLE, titleContainer);
|
||||
|
||||
this.getChildren().setAll(container);
|
||||
}
|
||||
|
||||
private Node[] nodes = new Node[2];
|
||||
|
||||
protected void setNode(int idx, Node node) {
|
||||
if (nodes.length <= idx)
|
||||
nodes = Arrays.copyOf(nodes, idx + 1);
|
||||
|
||||
if (nodes[idx] != node) {
|
||||
nodes[idx] = node;
|
||||
container.getChildren().setAll(Arrays.stream(nodes).filter(Objects::nonNull).toArray(Node[]::new));
|
||||
}
|
||||
}
|
||||
|
||||
public void setLargeTitle(boolean largeTitle) {
|
||||
pseudoClassStateChanged(PSEUDO_LARGER_TITLE, largeTitle);
|
||||
}
|
||||
|
||||
private final StringProperty title = new StringPropertyBase() {
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return LineComponent.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "title";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
titleLabel.setText(get());
|
||||
}
|
||||
};
|
||||
|
||||
public final StringProperty titleProperty() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return titleProperty().get();
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
titleProperty().set(title);
|
||||
}
|
||||
|
||||
private StringProperty subtitle;
|
||||
|
||||
public final StringProperty subtitleProperty() {
|
||||
if (subtitle == null) {
|
||||
subtitle = new StringPropertyBase() {
|
||||
private Label subtitleLabel;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "subtitle";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return LineComponent.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
String subtitle = get();
|
||||
if (subtitle != null && !subtitle.isEmpty()) {
|
||||
if (subtitleLabel == null) {
|
||||
subtitleLabel = new Label();
|
||||
subtitleLabel.setWrapText(true);
|
||||
subtitleLabel.setMinHeight(Region.USE_PREF_SIZE);
|
||||
subtitleLabel.getStyleClass().add("subtitle-label");
|
||||
}
|
||||
subtitleLabel.setText(subtitle);
|
||||
if (titleContainer.getChildren().size() == 1)
|
||||
titleContainer.getChildren().add(subtitleLabel);
|
||||
} else if (subtitleLabel != null) {
|
||||
subtitleLabel.setText(null);
|
||||
if (titleContainer.getChildren().size() == 2)
|
||||
titleContainer.getChildren().remove(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
public final String getSubtitle() {
|
||||
return subtitle != null ? subtitle.get() : null;
|
||||
}
|
||||
|
||||
public final void setSubtitle(String subtitle) {
|
||||
subtitleProperty().set(subtitle);
|
||||
}
|
||||
|
||||
private ObjectProperty<Node> leading;
|
||||
|
||||
public final ObjectProperty<Node> leadingProperty() {
|
||||
if (leading == null) {
|
||||
leading = new ObjectPropertyBase<>() {
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return LineComponent.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "leading";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
setNode(IDX_LEADING, get());
|
||||
}
|
||||
};
|
||||
}
|
||||
return leading;
|
||||
}
|
||||
|
||||
public final Node getLeading() {
|
||||
return leadingProperty().get();
|
||||
}
|
||||
|
||||
public final void setLeading(Node node) {
|
||||
leadingProperty().set(node);
|
||||
}
|
||||
|
||||
public void setLeading(Image icon) {
|
||||
setLeading(icon, -1);
|
||||
}
|
||||
|
||||
public void setLeading(Image icon, double size) {
|
||||
var imageView = new ImageView(icon);
|
||||
if (size > 0) {
|
||||
imageView.setFitWidth(size);
|
||||
imageView.setFitHeight(size);
|
||||
imageView.setPreserveRatio(true);
|
||||
imageView.setSmooth(true);
|
||||
}
|
||||
imageView.setMouseTransparent(true);
|
||||
|
||||
setNode(IDX_LEADING, imageView);
|
||||
}
|
||||
|
||||
public void setLeading(SVG svg) {
|
||||
setLeading(svg, DEFAULT_ICON_SIZE);
|
||||
}
|
||||
|
||||
public void setLeading(SVG svg, double size) {
|
||||
Node node = svg.createIcon(size);
|
||||
node.setMouseTransparent(true);
|
||||
setNode(IDX_LEADING, node);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.stage.DirectoryChooser;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class LineFileChooserButton extends LineButton {
|
||||
private static final String DEFAULT_STYLE_CLASS = "line-file-select-button";
|
||||
|
||||
public LineFileChooserButton() {
|
||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
setTrailingIcon(SVG.EDIT);
|
||||
}
|
||||
|
||||
/// Converts the given path to absolute/relative(if possible) path according to [#convertToRelativePathProperty()].
|
||||
private String processPath(Path path) {
|
||||
if (isConvertToRelativePath() && path.isAbsolute()) {
|
||||
try {
|
||||
return Metadata.CURRENT_DIRECTORY.relativize(path).normalize().toString();
|
||||
} catch (IllegalArgumentException e) {
|
||||
// the given path can't be relativized against current path
|
||||
}
|
||||
}
|
||||
return path.normalize().toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fire() {
|
||||
super.fire();
|
||||
|
||||
Stage owner = Controllers.getStage(); // TODO: Allow user to set owner stage
|
||||
String windowTitle = getFileChooserTitle();
|
||||
|
||||
Path initialDirectory = null;
|
||||
if (getLocation() != null) {
|
||||
Path file;
|
||||
try {
|
||||
file = FileUtils.toAbsolute(Path.of(getLocation()));
|
||||
if (Files.exists(file)) {
|
||||
if (Files.isRegularFile(file))
|
||||
initialDirectory = file.getParent();
|
||||
else if (Files.isDirectory(file))
|
||||
initialDirectory = file;
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.warning("Failed to resolve path: " + getLocation());
|
||||
}
|
||||
}
|
||||
|
||||
Path path;
|
||||
Type type = getType();
|
||||
if (type == Type.OPEN_DIRECTORY) {
|
||||
var directoryChooser = new DirectoryChooser();
|
||||
if (windowTitle != null)
|
||||
directoryChooser.setTitle(windowTitle);
|
||||
if (initialDirectory != null)
|
||||
directoryChooser.setInitialDirectory(initialDirectory.toFile());
|
||||
|
||||
path = FileUtils.toPath(directoryChooser.showDialog(owner));
|
||||
} else {
|
||||
var fileChooser = new FileChooser();
|
||||
if (windowTitle != null)
|
||||
fileChooser.setTitle(windowTitle);
|
||||
if (initialDirectory != null)
|
||||
fileChooser.setInitialDirectory(initialDirectory.toFile());
|
||||
|
||||
if (extensionFilters != null)
|
||||
fileChooser.getExtensionFilters().setAll(extensionFilters);
|
||||
|
||||
fileChooser.setInitialFileName(getInitialFileName());
|
||||
|
||||
path = FileUtils.toPath(switch (type) {
|
||||
case OPEN_FILE -> fileChooser.showOpenDialog(owner);
|
||||
case SAVE_FILE -> fileChooser.showSaveDialog(owner);
|
||||
default -> throw new AssertionError("Unknown Type: " + type);
|
||||
});
|
||||
}
|
||||
|
||||
if (path != null) {
|
||||
setLocation(processPath(path));
|
||||
}
|
||||
}
|
||||
|
||||
private final StringProperty location = new StringPropertyBase() {
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return LineFileChooserButton.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "location";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
setTrailingText(get());
|
||||
}
|
||||
};
|
||||
|
||||
public StringProperty locationProperty() {
|
||||
return location;
|
||||
}
|
||||
|
||||
public String getLocation() {
|
||||
return locationProperty().get();
|
||||
}
|
||||
|
||||
public void setLocation(String location) {
|
||||
locationProperty().set(location);
|
||||
}
|
||||
|
||||
private final StringProperty fileChooserTitle = new SimpleStringProperty(this, "fileChooserTitle");
|
||||
|
||||
public StringProperty fileChooserTitleProperty() {
|
||||
return fileChooserTitle;
|
||||
}
|
||||
|
||||
public String getFileChooserTitle() {
|
||||
return fileChooserTitleProperty().get();
|
||||
}
|
||||
|
||||
public void setFileChooserTitle(String fileChooserTitle) {
|
||||
fileChooserTitleProperty().set(fileChooserTitle);
|
||||
}
|
||||
|
||||
private ObjectProperty<Type> type;
|
||||
|
||||
public ObjectProperty<Type> typeProperty() {
|
||||
if (type == null) {
|
||||
type = new SimpleObjectProperty<>(this, "type", Type.OPEN_FILE);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type != null ? type.get() : Type.OPEN_FILE;
|
||||
}
|
||||
|
||||
public void setType(Type type) {
|
||||
typeProperty().set(type);
|
||||
}
|
||||
|
||||
private ObjectProperty<String> initialFileName;
|
||||
|
||||
public final ObjectProperty<String> initialFileNameProperty() {
|
||||
if (initialFileName == null)
|
||||
initialFileName = new SimpleObjectProperty<>(this, "initialFileName");
|
||||
|
||||
return initialFileName;
|
||||
}
|
||||
|
||||
public final String getInitialFileName() {
|
||||
return initialFileName != null ? initialFileName.get() : null;
|
||||
}
|
||||
|
||||
public final void setInitialFileName(String value) {
|
||||
initialFileNameProperty().set(value);
|
||||
}
|
||||
|
||||
private ObservableList<FileChooser.ExtensionFilter> extensionFilters;
|
||||
|
||||
public ObservableList<FileChooser.ExtensionFilter> getExtensionFilters() {
|
||||
if (extensionFilters == null)
|
||||
extensionFilters = FXCollections.observableArrayList();
|
||||
return extensionFilters;
|
||||
}
|
||||
|
||||
private BooleanProperty convertToRelativePath;
|
||||
|
||||
public BooleanProperty convertToRelativePathProperty() {
|
||||
if (convertToRelativePath == null)
|
||||
convertToRelativePath = new BooleanPropertyBase(false) {
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return LineFileChooserButton.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "convertToRelativePath";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
String location = getLocation();
|
||||
if (location == null)
|
||||
return;
|
||||
try {
|
||||
setLocation(processPath(FileUtils.toAbsolute(Path.of(getLocation()))));
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
}
|
||||
};
|
||||
return convertToRelativePath;
|
||||
}
|
||||
|
||||
public boolean isConvertToRelativePath() {
|
||||
return convertToRelativePath != null && convertToRelativePath.get();
|
||||
}
|
||||
|
||||
public void setConvertToRelativePath(boolean convertToRelativePath) {
|
||||
convertToRelativePathProperty().set(convertToRelativePath);
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
OPEN_FILE,
|
||||
OPEN_DIRECTORY,
|
||||
SAVE_FILE
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ObjectPropertyBase;
|
||||
import javafx.scene.Node;
|
||||
|
||||
/// @author Glavo
|
||||
public class LinePane extends LineComponent {
|
||||
private static final String DEFAULT_STYLE_CLASS = "line-pane";
|
||||
|
||||
public LinePane() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
}
|
||||
|
||||
private ObjectProperty<Node> right;
|
||||
|
||||
public ObjectProperty<Node> rightProperty() {
|
||||
if (right == null) {
|
||||
right = new ObjectPropertyBase<>() {
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return LinePane.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "right";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
setNode(IDX_TRAILING, get());
|
||||
}
|
||||
};
|
||||
}
|
||||
return right;
|
||||
}
|
||||
|
||||
public Node getRight() {
|
||||
return rightProperty().get();
|
||||
}
|
||||
|
||||
public void setRight(Node right) {
|
||||
rightProperty().set(right);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.input.ScrollEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition;
|
||||
|
||||
/// @author Glavo
|
||||
public final class LineSelectButton<T> extends LineButton {
|
||||
|
||||
private static final String DEFAULT_STYLE_CLASS = "line-select-button";
|
||||
private static final PseudoClass SELECTED_PSEUDO_CLASS = PseudoClass.getPseudoClass("selected");
|
||||
|
||||
private JFXPopup popup;
|
||||
|
||||
public LineSelectButton() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
|
||||
InvalidationListener updateTrailingText = observable -> {
|
||||
T value = getValue();
|
||||
if (value != null) {
|
||||
Function<T, String> converter = getConverter();
|
||||
setTrailingText(converter != null ? converter.apply(value) : value.toString());
|
||||
} else {
|
||||
setTrailingText(null);
|
||||
}
|
||||
};
|
||||
converterProperty().addListener(updateTrailingText);
|
||||
valueProperty().addListener(updateTrailingText);
|
||||
|
||||
setTrailingIcon(SVG.UNFOLD_MORE);
|
||||
|
||||
ripplerContainer.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
|
||||
if (event.getButton() == MouseButton.SECONDARY) {
|
||||
if (popup != null)
|
||||
popup.hide();
|
||||
event.consume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fire() {
|
||||
super.fire();
|
||||
if (popup == null) {
|
||||
PopupMenu popupMenu = new PopupMenu();
|
||||
this.popup = new JFXPopup(popupMenu);
|
||||
|
||||
ripplerContainer.addEventFilter(ScrollEvent.ANY, ignored -> popup.hide());
|
||||
|
||||
Bindings.bindContent(popupMenu.getContent(), MappedObservableList.create(itemsProperty(), item -> {
|
||||
VBox vbox = new VBox();
|
||||
|
||||
var itemTitleLabel = new Label();
|
||||
itemTitleLabel.getStyleClass().add("title-label");
|
||||
itemTitleLabel.textProperty().bind(Bindings.createStringBinding(() -> {
|
||||
if (item == null)
|
||||
return "";
|
||||
|
||||
Function<T, String> converter = getConverter();
|
||||
return converter != null ? converter.apply(item) : Objects.toString(item, "");
|
||||
}, converterProperty()));
|
||||
|
||||
var itemSubtitleLabel = new Label();
|
||||
itemSubtitleLabel.getStyleClass().add("subtitle-label");
|
||||
itemSubtitleLabel.textProperty().bind(Bindings.createStringBinding(() -> {
|
||||
Function<T, String> descriptionConverter = getDescriptionConverter();
|
||||
return descriptionConverter != null ? descriptionConverter.apply(item) : "";
|
||||
}, descriptionConverterProperty()));
|
||||
|
||||
FXUtils.onChangeAndOperate(itemSubtitleLabel.textProperty(), text -> {
|
||||
if (text == null || text.isEmpty()) {
|
||||
vbox.getChildren().setAll(itemTitleLabel);
|
||||
} else {
|
||||
vbox.getChildren().setAll(itemTitleLabel, itemSubtitleLabel);
|
||||
}
|
||||
});
|
||||
|
||||
var wrapper = new StackPane(vbox);
|
||||
wrapper.setAlignment(Pos.CENTER_LEFT);
|
||||
wrapper.getStyleClass().add("menu-container");
|
||||
wrapper.setMouseTransparent(true);
|
||||
RipplerContainer ripplerContainer = new RipplerContainer(wrapper);
|
||||
FXUtils.onClicked(ripplerContainer, () -> {
|
||||
setValue(item);
|
||||
popup.hide();
|
||||
});
|
||||
|
||||
FXUtils.onChangeAndOperate(valueProperty(),
|
||||
value -> wrapper.pseudoClassStateChanged(SELECTED_PSEUDO_CLASS, Objects.equals(value, item)));
|
||||
|
||||
return ripplerContainer;
|
||||
}));
|
||||
|
||||
popup.showingProperty().addListener((observable, oldValue, newValue) ->
|
||||
ripplerContainer.getRippler().setRipplerDisabled(newValue));
|
||||
}
|
||||
|
||||
if (popup.isShowing()) {
|
||||
popup.hide();
|
||||
} else {
|
||||
JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(this, popup);
|
||||
popup.show(this, vPosition, JFXPopup.PopupHPosition.RIGHT,
|
||||
0,
|
||||
vPosition == JFXPopup.PopupVPosition.TOP ? this.getHeight() : -this.getHeight(),
|
||||
true);
|
||||
}
|
||||
}
|
||||
|
||||
private final ObjectProperty<T> value = new SimpleObjectProperty<>(this, "value");
|
||||
|
||||
public ObjectProperty<T> valueProperty() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public T getValue() {
|
||||
return valueProperty().get();
|
||||
}
|
||||
|
||||
public void setValue(T value) {
|
||||
valueProperty().set(value);
|
||||
}
|
||||
|
||||
private final ObjectProperty<Function<T, String>> converter = new SimpleObjectProperty<>(this, "converter");
|
||||
|
||||
public ObjectProperty<Function<T, String>> converterProperty() {
|
||||
return converter;
|
||||
}
|
||||
|
||||
public Function<T, String> getConverter() {
|
||||
return converterProperty().get();
|
||||
}
|
||||
|
||||
public void setConverter(Function<T, String> value) {
|
||||
converterProperty().set(value);
|
||||
}
|
||||
|
||||
private ObjectProperty<Function<T, String>> descriptionConverter;
|
||||
|
||||
public ObjectProperty<Function<T, String>> descriptionConverterProperty() {
|
||||
if (descriptionConverter == null)
|
||||
descriptionConverter = new SimpleObjectProperty<>(this, "descriptionConverter");
|
||||
return descriptionConverter;
|
||||
}
|
||||
|
||||
public Function<T, String> getDescriptionConverter() {
|
||||
return descriptionConverterProperty().get();
|
||||
}
|
||||
|
||||
public void setDescriptionConverter(Function<T, String> value) {
|
||||
descriptionConverterProperty().set(value);
|
||||
}
|
||||
|
||||
private final ListProperty<T> items = new SimpleListProperty<>(this, "items", FXCollections.emptyObservableList());
|
||||
|
||||
public ListProperty<T> itemsProperty() {
|
||||
return items;
|
||||
}
|
||||
|
||||
public void setItems(ObservableList<T> value) {
|
||||
itemsProperty().set(value);
|
||||
}
|
||||
|
||||
public void setItems(Collection<T> value) {
|
||||
if (value instanceof ObservableList<T> observableList) {
|
||||
this.setItems(observableList);
|
||||
} else {
|
||||
this.setItems(FXCollections.observableArrayList(value));
|
||||
}
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public final void setItems(T... values) {
|
||||
this.setItems(FXCollections.observableArrayList(values));
|
||||
}
|
||||
|
||||
public ObservableList<T> getItems() {
|
||||
return items.get();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.property.StringPropertyBase;
|
||||
import javafx.scene.control.Label;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
/// @author Glavo
|
||||
public final class LineTextPane extends LineComponent {
|
||||
|
||||
private static final String DEFAULT_STYLE_CLASS = "line-text-pane";
|
||||
|
||||
public LineTextPane() {
|
||||
this.getStyleClass().addAll(DEFAULT_STYLE_CLASS);
|
||||
}
|
||||
|
||||
private StringProperty text;
|
||||
|
||||
public StringProperty textProperty() {
|
||||
if (text == null) {
|
||||
text = new StringPropertyBase() {
|
||||
private Label rightLabel;
|
||||
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return LineTextPane.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "text";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
String text = get();
|
||||
if (text != null && !text.isEmpty()) {
|
||||
if (rightLabel == null) {
|
||||
rightLabel = FXUtils.newSafeTruncatedLabel();
|
||||
FXUtils.copyOnDoubleClick(rightLabel);
|
||||
}
|
||||
rightLabel.setText(text);
|
||||
setNode(IDX_TRAILING, rightLabel);
|
||||
} else {
|
||||
if (rightLabel != null)
|
||||
rightLabel.setText(null);
|
||||
|
||||
setNode(IDX_TRAILING, null);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return textProperty().get();
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
textProperty().set(text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXToggleButton;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public final class LineToggleButton extends LineButtonBase {
|
||||
private static final String DEFAULT_STYLE_CLASS = "line-toggle-button";
|
||||
|
||||
private final JFXToggleButton toggleButton;
|
||||
|
||||
public LineToggleButton() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
|
||||
this.toggleButton = new JFXToggleButton();
|
||||
toggleButton.selectedProperty().bindBidirectional(selectedProperty());
|
||||
toggleButton.setSize(8);
|
||||
FXUtils.setLimitHeight(toggleButton, 30);
|
||||
setNode(IDX_TRAILING, toggleButton);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fire() {
|
||||
toggleButton.fire();
|
||||
super.fire();
|
||||
}
|
||||
|
||||
private final BooleanProperty selected = new SimpleBooleanProperty(this, "selected");
|
||||
|
||||
public BooleanProperty selectedProperty() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
public boolean isSelected() {
|
||||
return selectedProperty().get();
|
||||
}
|
||||
|
||||
public void setSelected(boolean selected) {
|
||||
selectedProperty().set(selected);
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public abstract class MDListCell<T> extends ListCell<T> {
|
||||
private final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
|
||||
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
|
||||
|
||||
private final StackPane container = new StackPane();
|
||||
private final StackPane root = new StackPane();
|
||||
|
||||
@@ -31,16 +31,12 @@ import javafx.scene.text.TextFlow;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class MessageDialogPane extends HBox {
|
||||
@@ -72,7 +68,7 @@ public final class MessageDialogPane extends HBox {
|
||||
private @Nullable ButtonBase cancelButton;
|
||||
|
||||
public MessageDialogPane(@NotNull String text, @Nullable String title, @NotNull MessageType type) {
|
||||
this.setSpacing(8);
|
||||
this.setSpacing(16);
|
||||
this.getStyleClass().add("jfx-dialog-layout");
|
||||
|
||||
Label graphic = new Label();
|
||||
@@ -219,36 +215,6 @@ public final class MessageDialogPane extends HBox {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder cancelOnTimeout(long timeoutMs) {
|
||||
if (dialog.getCancelButton() == null) {
|
||||
throw new IllegalStateException("Call ok/yesOrNo/actionOrCancel before calling cancelOnTimeout");
|
||||
}
|
||||
|
||||
ButtonBase cancelButton = dialog.getCancelButton();
|
||||
String originalText = cancelButton.getText();
|
||||
|
||||
Timer timer = Lang.getTimer();
|
||||
timer.scheduleAtFixedRate(new TimerTask() {
|
||||
long timeout = timeoutMs;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (timeout <= 0) {
|
||||
cancel();
|
||||
runInFX(() -> {
|
||||
cancelButton.fire();
|
||||
});
|
||||
return;
|
||||
}
|
||||
timeout -= 1000;
|
||||
long currentTimeout = timeout;
|
||||
runInFX(() -> cancelButton.setText(originalText + " (" + (currentTimeout / 1000) + ")"));
|
||||
}
|
||||
}, 1000, 1000);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public MessageDialogPane build() {
|
||||
return dialog;
|
||||
}
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXColorPicker;
|
||||
import com.jfoenix.controls.JFXRadioButton;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Toggle;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.paint.Color;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class MultiColorItem extends ComponentList {
|
||||
private final StringProperty customTitle = new SimpleStringProperty(this, "customTitle", i18n("selector.custom"));
|
||||
private final StringProperty chooserTitle = new SimpleStringProperty(this, "chooserTitle", i18n("selector.choose_file"));
|
||||
|
||||
private final ToggleGroup group = new ToggleGroup();
|
||||
private final JFXColorPicker colorPicker = new JFXColorPicker();
|
||||
private final JFXRadioButton radioCustom = new JFXRadioButton();
|
||||
private final BorderPane custom = new BorderPane();
|
||||
private final VBox pane = new VBox();
|
||||
private final boolean hasCustom;
|
||||
|
||||
private Consumer<Toggle> toggleSelectedListener;
|
||||
private Consumer<Color> colorConsumer;
|
||||
|
||||
public MultiColorItem(@NamedArg(value = "hasCustom", defaultValue = "true") boolean hasCustom) {
|
||||
this.hasCustom = hasCustom;
|
||||
|
||||
radioCustom.textProperty().bind(customTitleProperty());
|
||||
radioCustom.setToggleGroup(group);
|
||||
colorPicker.disableProperty().bind(radioCustom.selectedProperty().not());
|
||||
|
||||
colorPicker.setOnAction(e -> Optional.ofNullable(colorConsumer).ifPresent(c -> c.accept(colorPicker.getValue())));
|
||||
|
||||
custom.setLeft(radioCustom);
|
||||
custom.setStyle("-fx-padding: 3;");
|
||||
HBox right = new HBox();
|
||||
right.setSpacing(3);
|
||||
right.getChildren().addAll(colorPicker);
|
||||
custom.setRight(right);
|
||||
FXUtils.setLimitHeight(custom, 40);
|
||||
|
||||
pane.setStyle("-fx-padding: 0 0 10 0;");
|
||||
pane.setSpacing(8);
|
||||
|
||||
if (hasCustom)
|
||||
pane.getChildren().add(custom);
|
||||
getContent().add(pane);
|
||||
|
||||
group.selectedToggleProperty().addListener((a, b, newValue) -> {
|
||||
if (toggleSelectedListener != null)
|
||||
toggleSelectedListener.accept(newValue);
|
||||
});
|
||||
}
|
||||
|
||||
public Node createChildren(String title) {
|
||||
return createChildren(title, null);
|
||||
}
|
||||
|
||||
public Node createChildren(String title, Object userData) {
|
||||
return createChildren(title, "", userData);
|
||||
}
|
||||
|
||||
public Node createChildren(String title, String subtitle, Object userData) {
|
||||
BorderPane pane = new BorderPane();
|
||||
pane.setStyle("-fx-padding: 3;");
|
||||
|
||||
JFXRadioButton left = new JFXRadioButton(title);
|
||||
left.setToggleGroup(group);
|
||||
left.setUserData(userData);
|
||||
pane.setLeft(left);
|
||||
|
||||
Label right = new Label(subtitle);
|
||||
right.getStyleClass().add("subtitle-label");
|
||||
right.setStyle("-fx-font-size: 10;");
|
||||
pane.setRight(right);
|
||||
|
||||
return pane;
|
||||
}
|
||||
|
||||
public void loadChildren(Collection<Node> list) {
|
||||
pane.getChildren().setAll(list);
|
||||
|
||||
if (hasCustom)
|
||||
pane.getChildren().add(custom);
|
||||
}
|
||||
|
||||
public ToggleGroup getGroup() {
|
||||
return group;
|
||||
}
|
||||
|
||||
public String getCustomTitle() {
|
||||
return customTitle.get();
|
||||
}
|
||||
|
||||
public StringProperty customTitleProperty() {
|
||||
return customTitle;
|
||||
}
|
||||
|
||||
public void setCustomTitle(String customTitle) {
|
||||
this.customTitle.set(customTitle);
|
||||
}
|
||||
|
||||
public String getChooserTitle() {
|
||||
return chooserTitle.get();
|
||||
}
|
||||
|
||||
public StringProperty chooserTitleProperty() {
|
||||
return chooserTitle;
|
||||
}
|
||||
|
||||
public void setChooserTitle(String chooserTitle) {
|
||||
this.chooserTitle.set(chooserTitle);
|
||||
}
|
||||
|
||||
public void setCustomUserData(Object userData) {
|
||||
radioCustom.setUserData(userData);
|
||||
}
|
||||
|
||||
public boolean isCustomToggle(Toggle toggle) {
|
||||
return radioCustom == toggle;
|
||||
}
|
||||
|
||||
public void setToggleSelectedListener(Consumer<Toggle> consumer) {
|
||||
toggleSelectedListener = consumer;
|
||||
}
|
||||
|
||||
public void setOnColorPickerChanged(Consumer<Color> consumer) {
|
||||
colorConsumer = consumer;
|
||||
}
|
||||
|
||||
public void setColor(Color color) {
|
||||
colorPicker.setValue(color);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
/// Marker interface for no padding in [ComponentList].
|
||||
///
|
||||
/// @author Glavo
|
||||
interface NoPaddingComponent {
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXToggleButton;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
public class OptionToggleButton extends StackPane {
|
||||
private final StringProperty title = new SimpleStringProperty();
|
||||
private final StringProperty subtitle = new SimpleStringProperty();
|
||||
private final BooleanProperty selected = new SimpleBooleanProperty();
|
||||
|
||||
public OptionToggleButton() {
|
||||
getProperties().put("ComponentList.noPadding", true);
|
||||
|
||||
BorderPane pane = new BorderPane();
|
||||
pane.setPadding(new Insets(8, 8, 8, 16));
|
||||
RipplerContainer container = new RipplerContainer(pane);
|
||||
getChildren().setAll(container);
|
||||
|
||||
VBox left = new VBox();
|
||||
left.setMouseTransparent(true);
|
||||
Label titleLabel = new Label();
|
||||
titleLabel.textProperty().bind(title);
|
||||
Label subtitleLabel = new Label();
|
||||
subtitleLabel.setWrapText(true);
|
||||
subtitleLabel.setMouseTransparent(true);
|
||||
subtitleLabel.getStyleClass().add("subtitle");
|
||||
subtitleLabel.textProperty().bind(subtitle);
|
||||
pane.setCenter(left);
|
||||
left.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
JFXToggleButton toggleButton = new JFXToggleButton();
|
||||
StackPane right = new StackPane(toggleButton);
|
||||
right.setAlignment(Pos.CENTER);
|
||||
pane.setRight(right);
|
||||
toggleButton.selectedProperty().bindBidirectional(selected);
|
||||
toggleButton.setSize(8);
|
||||
FXUtils.setLimitHeight(toggleButton, 30);
|
||||
|
||||
FXUtils.onClicked(container, () -> toggleButton.setSelected(!toggleButton.isSelected()));
|
||||
|
||||
FXUtils.onChangeAndOperate(subtitleProperty(), subtitle -> {
|
||||
if (StringUtils.isNotBlank(subtitle)) {
|
||||
left.getChildren().setAll(titleLabel, subtitleLabel);
|
||||
} else {
|
||||
left.getChildren().setAll(titleLabel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title.get();
|
||||
}
|
||||
|
||||
public StringProperty titleProperty() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title.set(title);
|
||||
}
|
||||
|
||||
public String getSubtitle() {
|
||||
return subtitle.get();
|
||||
}
|
||||
|
||||
public StringProperty subtitleProperty() {
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
public void setSubtitle(String subtitle) {
|
||||
this.subtitle.set(subtitle);
|
||||
}
|
||||
|
||||
public boolean isSelected() {
|
||||
return selected.get();
|
||||
}
|
||||
|
||||
public BooleanProperty selectedProperty() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
public void setSelected(boolean selected) {
|
||||
this.selected.set(selected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.CssMetaData;
|
||||
import javafx.css.Styleable;
|
||||
import javafx.css.StyleableObjectProperty;
|
||||
import javafx.css.StyleableProperty;
|
||||
import javafx.css.converter.InsetsConverter;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Skin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
// TODO: We plan to replace ComponentList with this class, but we need to address some issues first
|
||||
|
||||
/// @author Glavo
|
||||
public final class OptionsList extends Control {
|
||||
public OptionsList() {
|
||||
this.getStyleClass().add("options-list");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new OptionsListSkin(this);
|
||||
}
|
||||
|
||||
private final ObservableList<Element> elements = FXCollections.observableArrayList();
|
||||
|
||||
public ObservableList<Element> getElements() {
|
||||
return elements;
|
||||
}
|
||||
|
||||
public void addTitle(String title) {
|
||||
elements.add(new Title(title));
|
||||
}
|
||||
|
||||
public void addNode(Node node) {
|
||||
elements.add(new NodeElement(node));
|
||||
}
|
||||
|
||||
public void addListElement(@NotNull Node node) {
|
||||
elements.add(new ListElement(node));
|
||||
}
|
||||
|
||||
public void addListElements(@NotNull Node... nodes) {
|
||||
for (Node node : nodes) {
|
||||
elements.add(new ListElement(node));
|
||||
}
|
||||
}
|
||||
|
||||
private final StyleableObjectProperty<Insets> contentPadding = new StyleableObjectProperty<>() {
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return OptionsList.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "contentPadding";
|
||||
}
|
||||
|
||||
@Override
|
||||
public javafx.css.CssMetaData<OptionsList, Insets> getCssMetaData() {
|
||||
return StyleableProperties.CONTENT_PADDING;
|
||||
}
|
||||
};
|
||||
|
||||
public StyleableObjectProperty<Insets> contentPaddingProperty() {
|
||||
return contentPadding;
|
||||
}
|
||||
|
||||
public Insets getContentPadding() {
|
||||
return contentPaddingProperty().get();
|
||||
}
|
||||
|
||||
public void setContentPadding(Insets padding) {
|
||||
contentPaddingProperty().set(padding);
|
||||
}
|
||||
|
||||
private static final class StyleableProperties {
|
||||
private static final CssMetaData<OptionsList, Insets> CONTENT_PADDING = new CssMetaData<>("-jfx-content-padding", InsetsConverter.getInstance()) {
|
||||
@Override
|
||||
public boolean isSettable(OptionsList styleable) {
|
||||
return styleable.contentPadding == null || !styleable.contentPadding.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Insets> getStyleableProperty(OptionsList styleable) {
|
||||
return styleable.contentPaddingProperty();
|
||||
}
|
||||
};
|
||||
|
||||
private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
|
||||
|
||||
static {
|
||||
List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
|
||||
Collections.addAll(styleables, CONTENT_PADDING);
|
||||
STYLEABLES = List.copyOf(styleables);
|
||||
}
|
||||
}
|
||||
|
||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
||||
return StyleableProperties.STYLEABLES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
||||
return getClassCssMetaData();
|
||||
}
|
||||
|
||||
public static abstract class Element {
|
||||
protected Node node;
|
||||
|
||||
Node getNode() {
|
||||
if (node == null)
|
||||
node = createNode();
|
||||
return node;
|
||||
}
|
||||
|
||||
protected abstract Node createNode();
|
||||
}
|
||||
|
||||
public static final class Title extends Element {
|
||||
private final @NotNull String title;
|
||||
|
||||
public Title(@NotNull String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Node createNode() {
|
||||
Label label = new Label(title);
|
||||
label.setPadding(new Insets(8, 0, 8, 0));
|
||||
return label;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return this == obj || obj instanceof Title that && Objects.equals(this.title, that.title);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return title.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Title[%s]".formatted(title);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static final class NodeElement extends Element {
|
||||
public NodeElement(@NotNull Node node) {
|
||||
this.node = node;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Node createNode() {
|
||||
return node;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return this == obj || obj instanceof NodeElement that && this.node.equals(that.node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return node.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "NodeElement[node=%s]".formatted(node);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class ListElement extends Element {
|
||||
private final Node original;
|
||||
|
||||
public ListElement(@NotNull Node node) {
|
||||
this.original = node;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Node createNode() {
|
||||
if (original instanceof ComponentSublist sublist) {
|
||||
return new ComponentSublistWrapper(sublist);
|
||||
} else {
|
||||
return original;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return this == obj || obj instanceof ListElement that && this.original.equals(that.original);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return original.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ListElement[node=%s]".formatted(original);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
/// @author Glavo
|
||||
public final class OptionsListSkin extends SkinBase<OptionsList> {
|
||||
|
||||
private final JFXListView<OptionsList.Element> listView;
|
||||
private final ObjectBinding<ContentPaddings> contentPaddings;
|
||||
|
||||
OptionsListSkin(OptionsList control) {
|
||||
super(control);
|
||||
|
||||
this.listView = new JFXListView<>();
|
||||
listView.setItems(control.getElements());
|
||||
listView.setCellFactory(listView1 -> new Cell());
|
||||
|
||||
this.contentPaddings = Bindings.createObjectBinding(() -> {
|
||||
Insets padding = control.getContentPadding();
|
||||
return padding == null ? ContentPaddings.EMPTY : new ContentPaddings(
|
||||
new Insets(padding.getTop(), padding.getRight(), 0, padding.getLeft()),
|
||||
new Insets(0, padding.getRight(), padding.getBottom(), padding.getLeft()),
|
||||
new Insets(0, padding.getRight(), 0, padding.getLeft())
|
||||
);
|
||||
}, control.contentPaddingProperty());
|
||||
|
||||
this.getChildren().setAll(listView);
|
||||
}
|
||||
|
||||
private record ContentPaddings(Insets first, Insets last, Insets middle) {
|
||||
static final ContentPaddings EMPTY = new ContentPaddings(Insets.EMPTY, Insets.EMPTY, Insets.EMPTY);
|
||||
}
|
||||
|
||||
private final class Cell extends ListCell<OptionsList.Element> {
|
||||
private static final PseudoClass PSEUDO_CLASS_FIRST = PseudoClass.getPseudoClass("first");
|
||||
private static final PseudoClass PSEUDO_CLASS_LAST = PseudoClass.getPseudoClass("last");
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final InvalidationListener updateStyleListener = o -> updateStyle();
|
||||
|
||||
private StackPane wrapper;
|
||||
|
||||
public Cell() {
|
||||
FXUtils.limitCellWidth(listView, this);
|
||||
|
||||
WeakInvalidationListener weakListener = new WeakInvalidationListener(updateStyleListener);
|
||||
listView.itemsProperty().addListener((o, oldValue, newValue) -> {
|
||||
if (oldValue != null)
|
||||
oldValue.removeListener(weakListener);
|
||||
if (newValue != null)
|
||||
newValue.addListener(weakListener);
|
||||
|
||||
weakListener.invalidated(o);
|
||||
});
|
||||
itemProperty().addListener(weakListener);
|
||||
contentPaddings.addListener(weakListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(OptionsList.Element item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (empty || item == null) {
|
||||
setGraphic(null);
|
||||
} else if (item instanceof OptionsList.ListElement element) {
|
||||
if (wrapper == null)
|
||||
wrapper = createWrapper();
|
||||
else
|
||||
wrapper.getStyleClass().remove("no-padding");
|
||||
|
||||
Node node = element.getNode();
|
||||
if (node instanceof NoPaddingComponent || node.getProperties().containsKey("ComponentList.noPadding"))
|
||||
wrapper.getStyleClass().add("no-padding");
|
||||
|
||||
wrapper.getChildren().setAll(node);
|
||||
|
||||
setGraphic(wrapper);
|
||||
} else {
|
||||
setGraphic(item.getNode());
|
||||
}
|
||||
|
||||
updateStyle();
|
||||
}
|
||||
|
||||
private StackPane createWrapper() {
|
||||
var wrapper = new StackPane();
|
||||
wrapper.setAlignment(Pos.CENTER_LEFT);
|
||||
wrapper.getStyleClass().add("options-list-item");
|
||||
updateStyle();
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private void updateStyle() {
|
||||
OptionsList.Element item = getItem();
|
||||
int index = getIndex();
|
||||
ObservableList<OptionsList.Element> items = getListView().getItems();
|
||||
|
||||
if (item == null || index < 0 || index >= items.size()) {
|
||||
this.setPadding(Insets.EMPTY);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isFirst = index == 0;
|
||||
boolean isLast = index == items.size() - 1;
|
||||
|
||||
ContentPaddings paddings = contentPaddings.get();
|
||||
if (isFirst) {
|
||||
this.setPadding(paddings.first);
|
||||
} else if (isLast) {
|
||||
this.setPadding(paddings.last);
|
||||
} else {
|
||||
this.setPadding(paddings.middle);
|
||||
}
|
||||
|
||||
if (item instanceof OptionsList.ListElement && wrapper != null) {
|
||||
wrapper.pseudoClassStateChanged(PSEUDO_CLASS_FIRST, isFirst || !(items.get(index - 1) instanceof OptionsList.ListElement));
|
||||
wrapper.pseudoClassStateChanged(PSEUDO_CLASS_LAST, isLast || !(items.get(index + 1) instanceof OptionsList.ListElement));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,17 +19,12 @@ package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXRippler;
|
||||
import javafx.animation.Transition;
|
||||
import javafx.beans.DefaultProperty;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.css.*;
|
||||
import javafx.css.converter.PaintConverter;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundFill;
|
||||
import javafx.scene.layout.CornerRadii;
|
||||
@@ -37,39 +32,41 @@ import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@DefaultProperty("container")
|
||||
public class RipplerContainer extends StackPane {
|
||||
private static final String DEFAULT_STYLE_CLASS = "rippler-container";
|
||||
private static final Duration DURATION = Duration.millis(200);
|
||||
private static final CornerRadii DEFAULT_RADII = new CornerRadii(3);
|
||||
private static final Color DEFAULT_RIPPLER_FILL = Color.rgb(0, 200, 255);
|
||||
|
||||
private final ObjectProperty<Node> container = new SimpleObjectProperty<>(this, "container", null);
|
||||
private final StyleableObjectProperty<Paint> ripplerFill = new SimpleStyleableObjectProperty<>(StyleableProperties.RIPPLER_FILL, this, "ripplerFill", null);
|
||||
private final BooleanProperty selected = new SimpleBooleanProperty(this, "selected", false);
|
||||
private final Node container;
|
||||
|
||||
private final StackPane buttonContainer = new StackPane();
|
||||
private final JFXRippler buttonRippler = new JFXRippler(new StackPane()) {
|
||||
private static final Background DEFAULT_MASK_BACKGROUND = new Background(new BackgroundFill(Color.WHITE, DEFAULT_RADII, Insets.EMPTY));
|
||||
|
||||
@Override
|
||||
protected Node getMask() {
|
||||
StackPane mask = new StackPane();
|
||||
mask.shapeProperty().bind(buttonContainer.shapeProperty());
|
||||
mask.backgroundProperty().bind(Bindings.createObjectBinding(() -> new Background(new BackgroundFill(Color.WHITE, buttonContainer.getBackground() != null && buttonContainer.getBackground().getFills().size() > 0 ? buttonContainer.getBackground().getFills().get(0).getRadii() : defaultRadii, buttonContainer.getBackground() != null && buttonContainer.getBackground().getFills().size() > 0 ? buttonContainer.getBackground().getFills().get(0).getInsets() : Insets.EMPTY)), buttonContainer.backgroundProperty()));
|
||||
mask.resize(buttonContainer.getWidth() - buttonContainer.snappedRightInset() - buttonContainer.snappedLeftInset(), buttonContainer.getHeight() - buttonContainer.snappedBottomInset() - buttonContainer.snappedTopInset());
|
||||
mask.setBackground(DEFAULT_MASK_BACKGROUND);
|
||||
mask.resize(
|
||||
buttonContainer.getWidth() - buttonContainer.snappedRightInset() - buttonContainer.snappedLeftInset(),
|
||||
buttonContainer.getHeight() - buttonContainer.snappedBottomInset() - buttonContainer.snappedTopInset()
|
||||
);
|
||||
return mask;
|
||||
}
|
||||
};
|
||||
|
||||
private final CornerRadii defaultRadii = new CornerRadii(3);
|
||||
private Transition coverAnimation;
|
||||
|
||||
public RipplerContainer(@NamedArg("container") Node container) {
|
||||
setContainer(container);
|
||||
public RipplerContainer(Node container) {
|
||||
this.container = container;
|
||||
|
||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
buttonRippler.setPosition(JFXRippler.RipplerPos.BACK);
|
||||
@@ -86,71 +83,79 @@ public class RipplerContainer extends StackPane {
|
||||
setPickOnBounds(false);
|
||||
|
||||
buttonContainer.setPickOnBounds(false);
|
||||
buttonRippler.ripplerFillProperty().bind(ripplerFillProperty());
|
||||
|
||||
containerProperty().addListener(o -> updateChildren());
|
||||
updateChildren();
|
||||
|
||||
InvalidationListener listener = o -> {
|
||||
if (isSelected()) setBackground(new Background(new BackgroundFill(getRipplerFill(), defaultRadii, null)));
|
||||
else setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, defaultRadii, null)));
|
||||
};
|
||||
|
||||
selectedProperty().addListener(listener);
|
||||
selectedProperty().addListener((a, b, newValue) ->
|
||||
pseudoClassStateChanged(PseudoClass.getPseudoClass("selected"), newValue));
|
||||
ripplerFillProperty().addListener(listener);
|
||||
|
||||
setShape(Lang.apply(new Rectangle(), rectangle -> {
|
||||
rectangle.widthProperty().bind(widthProperty());
|
||||
rectangle.heightProperty().bind(heightProperty());
|
||||
}));
|
||||
var shape = new Rectangle();
|
||||
shape.widthProperty().bind(widthProperty());
|
||||
shape.heightProperty().bind(heightProperty());
|
||||
setShape(shape);
|
||||
|
||||
EventHandler<MouseEvent> mouseEventHandler;
|
||||
if (AnimationUtils.isAnimationEnabled()) {
|
||||
setOnMouseEntered(e -> new Transition() {
|
||||
{
|
||||
setCycleDuration(DURATION);
|
||||
setInterpolator(Motion.EASE_IN);
|
||||
mouseEventHandler = event -> {
|
||||
if (coverAnimation != null) {
|
||||
coverAnimation.stop();
|
||||
coverAnimation = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void interpolate(double frac) {
|
||||
interpolateBackground(frac);
|
||||
}
|
||||
}.play());
|
||||
if (event.getEventType() == MouseEvent.MOUSE_ENTERED) {
|
||||
coverAnimation = new Transition() {
|
||||
{
|
||||
setCycleDuration(Motion.SHORT4);
|
||||
setInterpolator(Motion.EASE_IN);
|
||||
}
|
||||
|
||||
setOnMouseExited(e -> new Transition() {
|
||||
{
|
||||
setCycleDuration(DURATION);
|
||||
setInterpolator(Motion.EASE_OUT);
|
||||
@Override
|
||||
protected void interpolate(double frac) {
|
||||
interpolateBackground(frac);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
coverAnimation = new Transition() {
|
||||
{
|
||||
setCycleDuration(Motion.SHORT4);
|
||||
setInterpolator(Motion.EASE_OUT);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void interpolate(double frac) {
|
||||
interpolateBackground(1 - frac);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void interpolate(double frac) {
|
||||
interpolateBackground(1 - frac);
|
||||
}
|
||||
}.play());
|
||||
coverAnimation.play();
|
||||
};
|
||||
} else {
|
||||
setOnMouseEntered(e -> interpolateBackground(1));
|
||||
setOnMouseExited(e -> interpolateBackground(0));
|
||||
mouseEventHandler = event ->
|
||||
interpolateBackground(event.getEventType() == MouseEvent.MOUSE_ENTERED ? 1 : 0);
|
||||
}
|
||||
|
||||
addEventHandler(MouseEvent.MOUSE_ENTERED, mouseEventHandler);
|
||||
addEventHandler(MouseEvent.MOUSE_EXITED, mouseEventHandler);
|
||||
}
|
||||
|
||||
private void interpolateBackground(double frac) {
|
||||
Color onSurface = Themes.getColorScheme().getOnSurface();
|
||||
setBackground(new Background(new BackgroundFill(
|
||||
Color.color(onSurface.getRed(), onSurface.getGreen(), onSurface.getBlue(), frac * 0.04),
|
||||
CornerRadii.EMPTY, Insets.EMPTY)));
|
||||
if (frac < 0.01) {
|
||||
setBackground(null);
|
||||
} else {
|
||||
Color onSurface = Themes.getColorScheme().getOnSurface();
|
||||
setBackground(new Background(new BackgroundFill(
|
||||
Color.color(onSurface.getRed(), onSurface.getGreen(), onSurface.getBlue(), frac * 0.04),
|
||||
CornerRadii.EMPTY, Insets.EMPTY)));
|
||||
}
|
||||
}
|
||||
|
||||
protected void updateChildren() {
|
||||
if (buttonRippler.getPosition() == JFXRippler.RipplerPos.BACK)
|
||||
getChildren().setAll(buttonContainer, getContainer());
|
||||
else
|
||||
getChildren().setAll(getContainer(), buttonContainer);
|
||||
|
||||
for (int i = 1; i < getChildren().size(); ++i)
|
||||
getChildren().get(i).setPickOnBounds(false);
|
||||
Node container = getContainer();
|
||||
if (buttonRippler.getPosition() == JFXRippler.RipplerPos.BACK) {
|
||||
getChildren().setAll(buttonContainer, container);
|
||||
container.setPickOnBounds(false);
|
||||
} else {
|
||||
getChildren().setAll(container, buttonContainer);
|
||||
buttonContainer.setPickOnBounds(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void setPosition(JFXRippler.RipplerPos pos) {
|
||||
@@ -158,40 +163,46 @@ public class RipplerContainer extends StackPane {
|
||||
updateChildren();
|
||||
}
|
||||
|
||||
public Node getContainer() {
|
||||
return container.get();
|
||||
public JFXRippler getRippler() {
|
||||
return buttonRippler;
|
||||
}
|
||||
|
||||
public ObjectProperty<Node> containerProperty() {
|
||||
public Node getContainer() {
|
||||
return container;
|
||||
}
|
||||
|
||||
public void setContainer(Node container) {
|
||||
this.container.set(container);
|
||||
}
|
||||
private final StyleableObjectProperty<Paint> ripplerFill = new StyleableObjectProperty<>(DEFAULT_RIPPLER_FILL) {
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return RipplerContainer.this;
|
||||
}
|
||||
|
||||
public Paint getRipplerFill() {
|
||||
return ripplerFill.get();
|
||||
}
|
||||
@Override
|
||||
public String getName() {
|
||||
return "ripplerFill";
|
||||
}
|
||||
|
||||
@Override
|
||||
public CssMetaData<? extends Styleable, Paint> getCssMetaData() {
|
||||
return StyleableProperties.RIPPLER_FILL;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
buttonRippler.setRipplerFill(get());
|
||||
}
|
||||
};
|
||||
|
||||
public StyleableObjectProperty<Paint> ripplerFillProperty() {
|
||||
return ripplerFill;
|
||||
}
|
||||
|
||||
public Paint getRipplerFill() {
|
||||
return ripplerFillProperty().get();
|
||||
}
|
||||
|
||||
public void setRipplerFill(Paint ripplerFill) {
|
||||
this.ripplerFill.set(ripplerFill);
|
||||
}
|
||||
|
||||
public boolean isSelected() {
|
||||
return selected.get();
|
||||
}
|
||||
|
||||
public BooleanProperty selectedProperty() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
public void setSelected(boolean selected) {
|
||||
this.selected.set(selected);
|
||||
ripplerFillProperty().set(ripplerFill);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -200,12 +211,28 @@ public class RipplerContainer extends StackPane {
|
||||
}
|
||||
|
||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
||||
return StyleableProperties.FACTORY.getCssMetaData();
|
||||
return StyleableProperties.STYLEABLES;
|
||||
}
|
||||
|
||||
private final static class StyleableProperties {
|
||||
private static final StyleablePropertyFactory<RipplerContainer> FACTORY = new StyleablePropertyFactory<>(StackPane.getClassCssMetaData());
|
||||
private static final CssMetaData<RipplerContainer, Paint> RIPPLER_FILL = new CssMetaData<>("-jfx-rippler-fill", PaintConverter.getInstance(), DEFAULT_RIPPLER_FILL) {
|
||||
@Override
|
||||
public boolean isSettable(RipplerContainer styleable) {
|
||||
return styleable.ripplerFill == null || !styleable.ripplerFill.isBound();
|
||||
}
|
||||
|
||||
private static final CssMetaData<RipplerContainer, Paint> RIPPLER_FILL = FACTORY.createPaintCssMetaData("-jfx-rippler-fill", s -> s.ripplerFill, Color.rgb(0, 200, 255));
|
||||
@Override
|
||||
public StyleableProperty<Paint> getStyleableProperty(RipplerContainer styleable) {
|
||||
return styleable.ripplerFillProperty();
|
||||
}
|
||||
};
|
||||
|
||||
private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
|
||||
|
||||
static {
|
||||
var styleables = new ArrayList<>(StackPane.getClassCssMetaData());
|
||||
styleables.add(RIPPLER_FILL);
|
||||
STYLEABLES = List.copyOf(styleables);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,6 @@
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXSpinner;
|
||||
import javafx.beans.DefaultProperty;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.event.Event;
|
||||
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.TransitionPane;
|
||||
|
||||
@DefaultProperty("content")
|
||||
/// A spinner pane that can show spinner, failed reason, or content.
|
||||
public class SpinnerPane extends Control {
|
||||
private final ObjectProperty<Node> content = new SimpleObjectProperty<>(this, "content");
|
||||
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading");
|
||||
private final StringProperty failedReason = new SimpleStringProperty(this, "failedReason");
|
||||
private static final String DEFAULT_STYLE_CLASS = "spinner-pane";
|
||||
|
||||
public SpinnerPane() {
|
||||
getStyleClass().add("spinner-pane");
|
||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
}
|
||||
|
||||
public void showSpinner() {
|
||||
@@ -52,108 +48,195 @@ public class SpinnerPane extends Control {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
public Node getContent() {
|
||||
return content.get();
|
||||
private void updateContent() {
|
||||
if (getSkin() instanceof Skin skin) {
|
||||
skin.updateContent();
|
||||
}
|
||||
}
|
||||
|
||||
private ObjectProperty<Node> content;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public void setContent(Node content) {
|
||||
this.content.set(content);
|
||||
public Node getContent() {
|
||||
return contentProperty().get();
|
||||
}
|
||||
|
||||
public boolean isLoading() {
|
||||
return loading.get();
|
||||
public void setContent(Node content) {
|
||||
contentProperty().set(content);
|
||||
}
|
||||
|
||||
private BooleanProperty loading;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public void setLoading(boolean loading) {
|
||||
this.loading.set(loading);
|
||||
public boolean isLoading() {
|
||||
return loading != null && loading.get();
|
||||
}
|
||||
|
||||
public String getFailedReason() {
|
||||
return failedReason.get();
|
||||
public void setLoading(boolean loading) {
|
||||
loadingProperty().set(loading);
|
||||
}
|
||||
|
||||
private StringProperty failedReason;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public void setFailedReason(String failedReason) {
|
||||
this.failedReason.set(failedReason);
|
||||
public String getFailedReason() {
|
||||
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() {
|
||||
if (onFailedAction == null) {
|
||||
onFailedAction = new ObjectPropertyBase<>() {
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return SpinnerPane.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "onFailedAction";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
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);
|
||||
}
|
||||
|
||||
public final EventHandler<Event> getOnFailedAction() {
|
||||
return onFailedActionProperty().get();
|
||||
}
|
||||
|
||||
private final ObjectProperty<EventHandler<Event>> onFailedAction = new SimpleObjectProperty<EventHandler<Event>>(this, "onFailedAction") {
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
setEventHandler(FAILED_ACTION, get());
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected SkinBase<SpinnerPane> createDefaultSkin() {
|
||||
return new Skin(this);
|
||||
}
|
||||
|
||||
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 StackPane failedPane = new StackPane();
|
||||
private final Label failedReasonLabel = new Label();
|
||||
@SuppressWarnings("FieldCanBeLocal") // prevent from gc.
|
||||
private final InvalidationListener observer;
|
||||
|
||||
Skin(SpinnerPane control) {
|
||||
super(control);
|
||||
|
||||
topPane.getChildren().setAll(spinner);
|
||||
topPane.getStyleClass().add("notice-pane");
|
||||
failedPane.getStyleClass().add("notice-pane");
|
||||
failedPane.getChildren().setAll(failedReasonLabel);
|
||||
FXUtils.onClicked(failedPane, () -> {
|
||||
EventHandler<Event> action = control.getOnFailedAction();
|
||||
if (action != null)
|
||||
action.handle(new Event(FAILED_ACTION));
|
||||
});
|
||||
updateContent();
|
||||
this.getChildren().setAll(root);
|
||||
}
|
||||
|
||||
FXUtils.onChangeAndOperate(getSkinnable().content, newValue -> {
|
||||
if (newValue == null) {
|
||||
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");
|
||||
FXUtils.onClicked(failedPane, () -> control.fireEvent(new Event(SpinnerPane.FAILED_ACTION)));
|
||||
}
|
||||
failedReasonLabel.setText(control.getFailedReason());
|
||||
nextContent = failedPane;
|
||||
} else {
|
||||
if (contentPane == null) {
|
||||
contentPane = new StackPane();
|
||||
}
|
||||
|
||||
Node content = control.getContent();
|
||||
if (content != null)
|
||||
contentPane.getChildren().setAll(content);
|
||||
else
|
||||
contentPane.getChildren().clear();
|
||||
} else {
|
||||
contentPane.getChildren().setAll(newValue);
|
||||
}
|
||||
});
|
||||
getChildren().setAll(root);
|
||||
|
||||
observer = FXUtils.observeWeak(() -> {
|
||||
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());
|
||||
nextContent = contentPane;
|
||||
}
|
||||
|
||||
if (nextContent != failedPane && failedReasonLabel != null) {
|
||||
failedReasonLabel.setText(null);
|
||||
}
|
||||
|
||||
root.setContent(nextContent, ContainerAnimations.FADE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -326,8 +326,7 @@ public final class TaskListPane extends StackPane {
|
||||
if (prevStageNodeRef != null && (prevStageNode = prevStageNodeRef.get()) != null)
|
||||
prevStageNode.status.removeListener(statusChangeListener);
|
||||
|
||||
if (item instanceof ProgressListNode) {
|
||||
var progressListNode = (ProgressListNode) item;
|
||||
if (item instanceof ProgressListNode progressListNode) {
|
||||
title.setText(progressListNode.title);
|
||||
message.textProperty().bind(progressListNode.message);
|
||||
bar.progressProperty().bind(progressListNode.progress);
|
||||
@@ -336,8 +335,7 @@ public final class TaskListPane extends StackPane {
|
||||
pane.setLeft(null);
|
||||
pane.setRight(message);
|
||||
pane.setBottom(bar);
|
||||
} else if (item instanceof StageNode) {
|
||||
var stageNode = (StageNode) item;
|
||||
} else if (item instanceof StageNode stageNode) {
|
||||
title.textProperty().bind(stageNode.title);
|
||||
message.setText("");
|
||||
bar.setProgress(ProgressIndicator.INDETERMINATE_PROGRESS);
|
||||
@@ -493,12 +491,10 @@ public final class TaskListPane extends StackPane {
|
||||
|
||||
private ProgressListNode(Task<?> task) {
|
||||
this.title = task.getName();
|
||||
message.bind(task.messageProperty());
|
||||
progress.bind(task.progressProperty());
|
||||
}
|
||||
|
||||
public void unbind() {
|
||||
message.unbind();
|
||||
progress.unbind();
|
||||
}
|
||||
|
||||
|
||||
@@ -18,126 +18,185 @@
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.property.StringPropertyBase;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.AggregatedObservableList;
|
||||
|
||||
public class TwoLineListItem extends VBox {
|
||||
private static final String DEFAULT_STYLE_CLASS = "two-line-list-item";
|
||||
|
||||
private static Label createTagLabel(String tag) {
|
||||
Label tagLabel = new Label();
|
||||
tagLabel.setText(tag);
|
||||
HBox.setMargin(tagLabel, new Insets(0, 8, 0, 0));
|
||||
return tagLabel;
|
||||
}
|
||||
private final HBox firstLine;
|
||||
private HBox secondLine;
|
||||
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title");
|
||||
private final ObservableList<Label> tags = FXCollections.observableArrayList();
|
||||
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
|
||||
|
||||
private final Label lblSubtitle;
|
||||
private final Label lblTitle;
|
||||
private Label lblSubtitle;
|
||||
|
||||
private final AggregatedObservableList<Node> firstLineChildren;
|
||||
public TwoLineListItem() {
|
||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
setMouseTransparent(true);
|
||||
|
||||
lblTitle = new Label();
|
||||
lblTitle.getStyleClass().add("title");
|
||||
|
||||
this.firstLine = new HBox(lblTitle);
|
||||
firstLine.getStyleClass().add("first-line");
|
||||
firstLine.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
this.getChildren().setAll(firstLine);
|
||||
}
|
||||
|
||||
public TwoLineListItem(String titleString, String subtitleString) {
|
||||
this();
|
||||
|
||||
title.set(titleString);
|
||||
subtitle.set(subtitleString);
|
||||
setTitle(titleString);
|
||||
setSubtitle(subtitleString);
|
||||
}
|
||||
|
||||
public TwoLineListItem() {
|
||||
setMouseTransparent(true);
|
||||
private void initSecondLine() {
|
||||
if (secondLine == null) {
|
||||
lblSubtitle = new Label();
|
||||
lblSubtitle.getStyleClass().add("subtitle");
|
||||
|
||||
HBox firstLine = new HBox();
|
||||
firstLine.getStyleClass().add("first-line");
|
||||
secondLine = new HBox(lblSubtitle);
|
||||
}
|
||||
}
|
||||
|
||||
lblTitle = new Label();
|
||||
lblTitle.getStyleClass().add("title");
|
||||
lblTitle.textProperty().bind(title);
|
||||
private final StringProperty title = new StringPropertyBase() {
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return TwoLineListItem.this;
|
||||
}
|
||||
|
||||
firstLineChildren = new AggregatedObservableList<>();
|
||||
firstLineChildren.appendList(FXCollections.singletonObservableList(lblTitle));
|
||||
firstLineChildren.appendList(tags);
|
||||
Bindings.bindContent(firstLine.getChildren(), firstLineChildren.getAggregatedList());
|
||||
@Override
|
||||
public String getName() {
|
||||
return "title";
|
||||
}
|
||||
|
||||
lblSubtitle = new Label();
|
||||
lblSubtitle.getStyleClass().add("subtitle");
|
||||
lblSubtitle.textProperty().bind(subtitle);
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
lblTitle.setText(get());
|
||||
}
|
||||
};
|
||||
|
||||
HBox secondLine = new HBox();
|
||||
secondLine.getChildren().setAll(lblSubtitle);
|
||||
|
||||
getChildren().setAll(firstLine, secondLine);
|
||||
|
||||
FXUtils.onChangeAndOperate(subtitle, subtitleString -> {
|
||||
if (subtitleString == null) getChildren().setAll(firstLine);
|
||||
else getChildren().setAll(firstLine, secondLine);
|
||||
});
|
||||
|
||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
public StringProperty titleProperty() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title.get();
|
||||
}
|
||||
|
||||
public StringProperty titleProperty() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title.set(title);
|
||||
}
|
||||
|
||||
public String getSubtitle() {
|
||||
return subtitle.get();
|
||||
}
|
||||
private StringProperty subtitle;
|
||||
|
||||
public StringProperty subtitleProperty() {
|
||||
if (subtitle == null) {
|
||||
subtitle = new StringPropertyBase() {
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return TwoLineListItem.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "subtitle";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
String subtitle = get();
|
||||
|
||||
if (subtitle != null) {
|
||||
initSecondLine();
|
||||
lblSubtitle.setText(subtitle);
|
||||
|
||||
if (getChildren().size() == 1)
|
||||
getChildren().add(secondLine);
|
||||
} else if (secondLine != null) {
|
||||
lblSubtitle.setText(null);
|
||||
if (getChildren().size() > 1)
|
||||
getChildren().setAll(firstLine);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
public void setSubtitle(String subtitle) {
|
||||
this.subtitle.set(subtitle);
|
||||
public String getSubtitle() {
|
||||
return subtitle != null ? subtitleProperty().get() : null;
|
||||
}
|
||||
|
||||
public Label getSubtitleLabel() {
|
||||
return lblSubtitle;
|
||||
public void setSubtitle(String subtitle) {
|
||||
if (this.subtitle == null && subtitle == null)
|
||||
return;
|
||||
|
||||
subtitleProperty().set(subtitle);
|
||||
}
|
||||
|
||||
public Label getTitleLabel() {
|
||||
return lblTitle;
|
||||
}
|
||||
|
||||
public void addTag(String tag) {
|
||||
Label tagLabel = createTagLabel(tag);
|
||||
tagLabel.getStyleClass().add("tag");
|
||||
getTags().add(tagLabel);
|
||||
public Label getSubtitleLabel() {
|
||||
initSecondLine();
|
||||
return lblSubtitle;
|
||||
}
|
||||
|
||||
public void addTagWarning(String tag) {
|
||||
Label tagLabel = createTagLabel(tag);
|
||||
tagLabel.getStyleClass().add("tag-warning");
|
||||
getTags().add(tagLabel);
|
||||
}
|
||||
private ObservableList<Label> tags;
|
||||
|
||||
public ObservableList<Label> getTags() {
|
||||
if (tags == null) {
|
||||
tags = FXCollections.observableArrayList();
|
||||
|
||||
var tagsBox = new HBox(8);
|
||||
tagsBox.getStyleClass().add("tags");
|
||||
tagsBox.setAlignment(Pos.CENTER_LEFT);
|
||||
Bindings.bindContent(tagsBox.getChildren(), tags);
|
||||
|
||||
var scrollPane = new ScrollPane(tagsBox);
|
||||
HBox.setHgrow(scrollPane, Priority.ALWAYS);
|
||||
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
FXUtils.onChangeAndOperate(tagsBox.heightProperty(), height -> FXUtils.setLimitHeight(scrollPane, height.doubleValue()));
|
||||
scrollPane.setPrefWidth(50);
|
||||
firstLine.getChildren().setAll(lblTitle, scrollPane);
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
public void addTag(String tag, PseudoClass pseudoClass) {
|
||||
var tagLabel = new Label(tag);
|
||||
tagLabel.getStyleClass().add("tag");
|
||||
if (pseudoClass != null)
|
||||
tagLabel.pseudoClassStateChanged(pseudoClass, true);
|
||||
getTags().add(tagLabel);
|
||||
}
|
||||
|
||||
public void addTag(String tag) {
|
||||
addTag(tag, null);
|
||||
}
|
||||
|
||||
private static final PseudoClass WARNING_PSEUDO_CLASS = PseudoClass.getPseudoClass("warning");
|
||||
|
||||
public void addTagWarning(String tag) {
|
||||
addTag(tag, WARNING_PSEUDO_CLASS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getTitle();
|
||||
return "TwoLineListItem[title=%s, subtitle=%s, tags=%s]".formatted(getTitle(), getSubtitle(), tags);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.ui.decorator;
|
||||
|
||||
import com.jfoenix.controls.JFXSnackbar;
|
||||
import com.jfoenix.controls.JFXSnackbarLayout;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
@@ -454,7 +455,7 @@ public class DecoratorController {
|
||||
// ==== Toast ====
|
||||
|
||||
public void showToast(String content) {
|
||||
decorator.getSnackbar().fireEvent(new JFXSnackbar.SnackbarEvent(content, null, 2000L, false, null));
|
||||
decorator.getSnackbar().fireEvent(new JFXSnackbar.SnackbarEvent(new JFXSnackbarLayout(content)));
|
||||
}
|
||||
|
||||
// ==== Wizard ====
|
||||
|
||||
@@ -239,7 +239,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
|
||||
JFXButton btnMin = new JFXButton();
|
||||
btnMin.setFocusTraversable(false);
|
||||
btnMin.setGraphic(SVG.MINIMIZE.createIcon(Themes.titleFillProperty()));
|
||||
btnMin.setGraphic(SVG.MINIMIZE_CENTER.createIcon(Themes.titleFillProperty()));
|
||||
btnMin.getStyleClass().add("jfx-decorator-button");
|
||||
btnMin.setOnAction(e -> skinnable.minimize());
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
.whenComplete(Schedulers.javafx(), (manifest, exception) -> {
|
||||
if (exception instanceof ManuallyCreatedModpackException) {
|
||||
hideSpinner();
|
||||
lblName.setText(FileUtils.getName(selectedFile));
|
||||
nameProperty.set(FileUtils.getName(selectedFile));
|
||||
installAsVersion.set(false);
|
||||
|
||||
if (name == null) {
|
||||
@@ -127,9 +127,9 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
} else {
|
||||
hideSpinner();
|
||||
controller.getSettings().put(MODPACK_MANIFEST, manifest);
|
||||
lblName.setText(manifest.getName());
|
||||
lblVersion.setText(manifest.getVersion());
|
||||
lblAuthor.setText(manifest.getAuthor());
|
||||
nameProperty.set(manifest.getName());
|
||||
versionProperty.set(manifest.getVersion());
|
||||
authorProperty.set(manifest.getAuthor());
|
||||
|
||||
if (name == null) {
|
||||
// trim: https://github.com/HMCL-dev/HMCL/issues/962
|
||||
|
||||
@@ -2,14 +2,15 @@ package org.jackhuang.hmcl.ui.download;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.LinePane;
|
||||
import org.jackhuang.hmcl.ui.construct.LineTextPane;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
@@ -23,9 +24,9 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
|
||||
|
||||
protected final WizardController controller;
|
||||
|
||||
protected final Label lblName;
|
||||
protected final Label lblVersion;
|
||||
protected final Label lblAuthor;
|
||||
protected final StringProperty nameProperty;
|
||||
protected final StringProperty versionProperty;
|
||||
protected final StringProperty authorProperty;
|
||||
protected final JFXTextField txtModpackName;
|
||||
protected final JFXButton btnInstall;
|
||||
protected final JFXButton btnDescription;
|
||||
@@ -33,54 +34,43 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
|
||||
protected ModpackPage(WizardController controller) {
|
||||
this.controller = controller;
|
||||
|
||||
this.getStyleClass().add("large-spinner-pane");
|
||||
|
||||
VBox borderPane = new VBox();
|
||||
borderPane.setAlignment(Pos.CENTER);
|
||||
FXUtils.setLimitWidth(borderPane, 500);
|
||||
|
||||
ComponentList componentList = new ComponentList();
|
||||
{
|
||||
BorderPane archiveNamePane = new BorderPane();
|
||||
var archiveNamePane = new LinePane();
|
||||
{
|
||||
Label label = new Label(i18n("archive.file.name"));
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
archiveNamePane.setLeft(label);
|
||||
archiveNamePane.setTitle(i18n("archive.file.name"));
|
||||
|
||||
txtModpackName = new JFXTextField();
|
||||
BorderPane.setMargin(txtModpackName, new Insets(0, 0, 8, 32));
|
||||
txtModpackName.setPrefWidth(300);
|
||||
// FIXME: Validator are not shown properly
|
||||
// BorderPane.setMargin(txtModpackName, new Insets(0, 0, 8, 32));
|
||||
BorderPane.setAlignment(txtModpackName, Pos.CENTER_RIGHT);
|
||||
archiveNamePane.setCenter(txtModpackName);
|
||||
archiveNamePane.setRight(txtModpackName);
|
||||
}
|
||||
|
||||
BorderPane modpackNamePane = new BorderPane();
|
||||
var modpackNamePane = new LineTextPane();
|
||||
{
|
||||
modpackNamePane.setLeft(new Label(i18n("modpack.name")));
|
||||
|
||||
lblName = new Label();
|
||||
BorderPane.setAlignment(lblName, Pos.CENTER_RIGHT);
|
||||
modpackNamePane.setCenter(lblName);
|
||||
modpackNamePane.setTitle(i18n("modpack.name"));
|
||||
nameProperty = modpackNamePane.textProperty();
|
||||
}
|
||||
|
||||
BorderPane versionPane = new BorderPane();
|
||||
var versionPane = new LineTextPane();
|
||||
{
|
||||
versionPane.setLeft(new Label(i18n("archive.version")));
|
||||
|
||||
lblVersion = new Label();
|
||||
BorderPane.setAlignment(lblVersion, Pos.CENTER_RIGHT);
|
||||
versionPane.setCenter(lblVersion);
|
||||
versionPane.setTitle(i18n("archive.version"));
|
||||
versionProperty = versionPane.textProperty();
|
||||
}
|
||||
|
||||
BorderPane authorPane = new BorderPane();
|
||||
var authorPane = new LineTextPane();
|
||||
{
|
||||
authorPane.setLeft(new Label(i18n("archive.author")));
|
||||
|
||||
lblAuthor = new Label();
|
||||
BorderPane.setAlignment(lblAuthor, Pos.CENTER_RIGHT);
|
||||
authorPane.setCenter(lblAuthor);
|
||||
authorPane.setTitle(i18n("archive.author"));
|
||||
authorProperty = authorPane.textProperty();
|
||||
}
|
||||
|
||||
BorderPane descriptionPane = new BorderPane();
|
||||
var descriptionPane = new BorderPane();
|
||||
{
|
||||
btnDescription = FXUtils.newBorderButton(i18n("modpack.description"));
|
||||
btnDescription.setOnAction(e -> onDescribe());
|
||||
|
||||
@@ -53,9 +53,9 @@ public final class RemoteModpackPage extends ModpackPage {
|
||||
return;
|
||||
}
|
||||
|
||||
lblName.setText(manifest.getName());
|
||||
lblVersion.setText(manifest.getVersion());
|
||||
lblAuthor.setText(manifest.getAuthor());
|
||||
nameProperty.set(manifest.getName());
|
||||
versionProperty.set(manifest.getVersion());
|
||||
authorProperty.set(manifest.getAuthor());
|
||||
|
||||
Profile profile = controller.getSettings().get(ModpackPage.PROFILE);
|
||||
String name = controller.getSettings().get(MODPACK_NAME);
|
||||
|
||||
@@ -60,7 +60,6 @@ import org.jackhuang.hmcl.ui.wizard.Navigation;
|
||||
import org.jackhuang.hmcl.ui.wizard.Refreshable;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
import org.jackhuang.hmcl.util.NativePatcher;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
@@ -131,11 +130,6 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
return title;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup(SettingsMap settings) {
|
||||
settings.remove(libraryId);
|
||||
}
|
||||
|
||||
private void onRefresh() {
|
||||
refresh();
|
||||
}
|
||||
@@ -168,6 +162,8 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
RemoteVersionListCell(VersionsPage control) {
|
||||
this.control = control;
|
||||
|
||||
imageView.setMouseTransparent(true);
|
||||
|
||||
HBox hbox = new HBox(16);
|
||||
HBox.setHgrow(twoLineListItem, Priority.ALWAYS);
|
||||
hbox.setAlignment(Pos.CENTER);
|
||||
|
||||
@@ -17,7 +17,11 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.export;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXSlider;
|
||||
import com.jfoenix.controls.JFXTextArea;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import com.jfoenix.validation.base.ValidatorBase;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.ObservableList;
|
||||
@@ -53,8 +57,6 @@ import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.jfxListCellFactory;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
||||
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE;
|
||||
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE_MODRINTH;
|
||||
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE_SERVER;
|
||||
@@ -127,7 +129,6 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
exportInfo.setVersion(version.get());
|
||||
exportInfo.setAuthor(author.get());
|
||||
exportInfo.setDescription(description.get());
|
||||
exportInfo.setPackWithLauncher(packWithLauncher.get());
|
||||
exportInfo.setUrl(url.get());
|
||||
exportInfo.setForceUpdate(forceUpdate.get());
|
||||
exportInfo.setPackWithLauncher(packWithLauncher.get());
|
||||
@@ -171,10 +172,11 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
public static class ModpackInfoPageSkin extends SkinBase<ModpackInfoPage> {
|
||||
private ObservableList<Node> originList;
|
||||
|
||||
private final List<JFXTextField> validatingFields = new ArrayList<>();
|
||||
|
||||
public ModpackInfoPageSkin(ModpackInfoPage skinnable) {
|
||||
super(skinnable);
|
||||
|
||||
Insets insets = new Insets(5, 0, 12, 0);
|
||||
Insets componentListMargin = new Insets(16, 0, 16, 0);
|
||||
|
||||
ScrollPane scroll = new ScrollPane();
|
||||
@@ -182,7 +184,6 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
scroll.setFitToHeight(true);
|
||||
getChildren().setAll(scroll);
|
||||
|
||||
List<JFXTextField> validatingFields = new ArrayList<>();
|
||||
|
||||
{
|
||||
BorderPane borderPane = new BorderPane();
|
||||
@@ -209,83 +210,52 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
BorderPane.setMargin(list, componentListMargin);
|
||||
borderPane.setCenter(list);
|
||||
|
||||
var instanceNamePane = new LineTextPane();
|
||||
{
|
||||
BorderPane borderPane1 = new BorderPane();
|
||||
borderPane1.setLeft(new Label(i18n("modpack.wizard.step.initialization.exported_version")));
|
||||
instanceNamePane.setTitle(i18n("modpack.wizard.step.initialization.exported_version"));
|
||||
instanceNamePane.setText(skinnable.versionName);
|
||||
|
||||
Label versionNameLabel = new Label();
|
||||
versionNameLabel.setText(skinnable.versionName);
|
||||
borderPane1.setRight(versionNameLabel);
|
||||
list.getContent().add(borderPane1);
|
||||
list.getContent().add(instanceNamePane);
|
||||
}
|
||||
|
||||
{
|
||||
GridPane pane = new GridPane();
|
||||
list.getContent().add(pane);
|
||||
pane.setHgap(16);
|
||||
pane.setVgap(8);
|
||||
pane.getColumnConstraints().setAll(new ColumnConstraints(), FXUtils.getColumnHgrowing());
|
||||
|
||||
int rowIndex = 0;
|
||||
list.getContent().addAll(
|
||||
createTextFieldLinePane(i18n("modpack.name"), skinnable.name, new RequiredValidator()),
|
||||
createTextFieldLinePane(i18n("archive.author"), skinnable.author, new RequiredValidator()),
|
||||
createTextFieldLinePane(i18n("archive.version"), skinnable.version, new RequiredValidator())
|
||||
);
|
||||
|
||||
JFXTextField txtModpackName = new JFXTextField();
|
||||
txtModpackName.textProperty().bindBidirectional(skinnable.name);
|
||||
txtModpackName.getValidators().add(new RequiredValidator());
|
||||
validatingFields.add(txtModpackName);
|
||||
pane.addRow(rowIndex++, new Label(i18n("modpack.name")), txtModpackName);
|
||||
if (skinnable.options.isRequireFileApi()) {
|
||||
list.getContent().add(createTextFieldLinePane(
|
||||
i18n("modpack.file_api"), skinnable.fileApi,
|
||||
skinnable.options.isValidateFileApi() ? new RequiredValidator() : null,
|
||||
new URLValidator(true)
|
||||
));
|
||||
}
|
||||
|
||||
JFXTextField txtModpackAuthor = new JFXTextField();
|
||||
txtModpackAuthor.textProperty().bindBidirectional(skinnable.author);
|
||||
txtModpackAuthor.getValidators().add(new RequiredValidator());
|
||||
validatingFields.add(txtModpackAuthor);
|
||||
pane.addRow(rowIndex++, new Label(i18n("archive.author")), txtModpackAuthor);
|
||||
if (skinnable.options.isRequireLaunchArguments()) {
|
||||
list.getContent().add(createTextFieldLinePane(
|
||||
i18n("settings.advanced.minecraft_arguments"), skinnable.launchArguments
|
||||
));
|
||||
}
|
||||
|
||||
JFXTextField txtModpackVersion = new JFXTextField();
|
||||
txtModpackVersion.textProperty().bindBidirectional(skinnable.version);
|
||||
txtModpackVersion.getValidators().add(new RequiredValidator());
|
||||
validatingFields.add(txtModpackVersion);
|
||||
pane.addRow(rowIndex++, new Label(i18n("archive.version")), txtModpackVersion);
|
||||
if (skinnable.options.isRequireJavaArguments()) {
|
||||
list.getContent().add(createTextFieldLinePane(
|
||||
i18n("settings.advanced.jvm_args"), skinnable.javaArguments
|
||||
));
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireFileApi()) {
|
||||
JFXTextField txtModpackFileApi = new JFXTextField();
|
||||
txtModpackFileApi.textProperty().bindBidirectional(skinnable.fileApi);
|
||||
validatingFields.add(txtModpackFileApi);
|
||||
|
||||
if (skinnable.options.isValidateFileApi()) {
|
||||
txtModpackFileApi.getValidators().add(new RequiredValidator());
|
||||
}
|
||||
|
||||
txtModpackFileApi.getValidators().add(new URLValidator(true));
|
||||
pane.addRow(rowIndex++, new Label(i18n("modpack.file_api")), txtModpackFileApi);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireLaunchArguments()) {
|
||||
JFXTextField txtLaunchArguments = new JFXTextField();
|
||||
txtLaunchArguments.textProperty().bindBidirectional(skinnable.launchArguments);
|
||||
pane.addRow(rowIndex++, new Label(i18n("settings.advanced.minecraft_arguments")), txtLaunchArguments);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireJavaArguments()) {
|
||||
JFXTextField txtJavaArguments = new JFXTextField();
|
||||
txtJavaArguments.textProperty().bindBidirectional(skinnable.javaArguments);
|
||||
pane.addRow(rowIndex++, new Label(i18n("settings.advanced.jvm_args")), txtJavaArguments);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireUrl()) {
|
||||
JFXTextField txtModpackUrl = new JFXTextField();
|
||||
txtModpackUrl.textProperty().bindBidirectional(skinnable.url);
|
||||
pane.addRow(rowIndex++, new Label(i18n("modpack.origin.url")), txtModpackUrl);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireOrigins()) {
|
||||
JFXTextField txtMcbbs = new JFXTextField();
|
||||
FXUtils.setValidateWhileTextChanged(txtMcbbs, true);
|
||||
txtMcbbs.getValidators().add(new NumberValidator(i18n("input.number"), true));
|
||||
txtMcbbs.textProperty().bindBidirectional(skinnable.mcbbsThreadId);
|
||||
validatingFields.add(txtMcbbs);
|
||||
pane.addRow(rowIndex++, new Label(i18n("modpack.origin.mcbbs")), txtMcbbs);
|
||||
}
|
||||
if (skinnable.options.isRequireUrl()) {
|
||||
list.getContent().add(createTextFieldLinePane(
|
||||
i18n("modpack.origin.url"), skinnable.url
|
||||
));
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireOrigins()) {
|
||||
list.getContent().add(createTextFieldLinePane(
|
||||
i18n("modpack.origin.mcbbs"), skinnable.mcbbsThreadId,
|
||||
new NumberValidator(i18n("input.number"), true)
|
||||
));
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireMinMemory()) {
|
||||
@@ -339,78 +309,51 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireAuthlibInjectorServer()) {
|
||||
JFXComboBox<AuthlibInjectorServer> cboServers = new JFXComboBox<>();
|
||||
cboServers.setMaxWidth(Double.MAX_VALUE);
|
||||
cboServers.setCellFactory(jfxListCellFactory(server -> new TwoLineListItem(server.getName(), server.getUrl())));
|
||||
cboServers.setConverter(stringConverter(AuthlibInjectorServer::getName));
|
||||
Bindings.bindContent(cboServers.getItems(), config().getAuthlibInjectorServers());
|
||||
var serversSelectButton = new LineSelectButton<AuthlibInjectorServer>();
|
||||
serversSelectButton.setTitle(i18n("account.injector.server"));
|
||||
serversSelectButton.setConverter(AuthlibInjectorServer::getName);
|
||||
serversSelectButton.setDescriptionConverter(AuthlibInjectorServer::getUrl);
|
||||
serversSelectButton.itemsProperty().set(config().getAuthlibInjectorServers());
|
||||
|
||||
skinnable.authlibInjectorServer.bind(Bindings.createStringBinding(() ->
|
||||
Optional.ofNullable(cboServers.getSelectionModel().getSelectedItem())
|
||||
.map(AuthlibInjectorServer::getUrl)
|
||||
.orElse(null)));
|
||||
skinnable.authlibInjectorServer.bind(Bindings.createStringBinding(() -> {
|
||||
AuthlibInjectorServer selected = serversSelectButton.getValue();
|
||||
return selected != null ? selected.getUrl() : null;
|
||||
}, serversSelectButton.valueProperty()));
|
||||
|
||||
BorderPane pane = new BorderPane();
|
||||
|
||||
Label left = new Label(i18n("account.injector.server"));
|
||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||
pane.setLeft(left);
|
||||
pane.setRight(cboServers);
|
||||
|
||||
list.getContent().add(pane);
|
||||
list.getContent().add(serversSelectButton);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireForceUpdate()) {
|
||||
BorderPane pane = new BorderPane();
|
||||
pane.setLeft(new Label(i18n("modpack.wizard.step.initialization.force_update")));
|
||||
list.getContent().add(pane);
|
||||
var requireForceUpdateButton = new LineToggleButton();
|
||||
requireForceUpdateButton.setTitle(i18n("modpack.wizard.step.initialization.force_update"));
|
||||
requireForceUpdateButton.selectedProperty().bindBidirectional(skinnable.forceUpdate);
|
||||
|
||||
JFXToggleButton button = new JFXToggleButton();
|
||||
button.selectedProperty().bindBidirectional(skinnable.forceUpdate);
|
||||
button.setSize(8);
|
||||
button.setMinHeight(16);
|
||||
button.setMaxHeight(16);
|
||||
pane.setRight(button);
|
||||
list.getContent().add(requireForceUpdateButton);
|
||||
}
|
||||
|
||||
{
|
||||
BorderPane pane = new BorderPane();
|
||||
pane.setLeft(new Label(i18n("modpack.wizard.step.initialization.include_launcher")));
|
||||
list.getContent().add(pane);
|
||||
var canIncludeLauncherButton = new LineToggleButton();
|
||||
canIncludeLauncherButton.setTitle(i18n("modpack.wizard.step.initialization.include_launcher"));
|
||||
canIncludeLauncherButton.setDisable(!skinnable.canIncludeLauncher);
|
||||
canIncludeLauncherButton.selectedProperty().bindBidirectional(skinnable.packWithLauncher);
|
||||
|
||||
JFXToggleButton button = new JFXToggleButton();
|
||||
button.setDisable(!skinnable.canIncludeLauncher);
|
||||
button.selectedProperty().bindBidirectional(skinnable.packWithLauncher);
|
||||
button.setSize(8);
|
||||
button.setMinHeight(16);
|
||||
button.setMaxHeight(16);
|
||||
pane.setRight(button);
|
||||
list.getContent().add(canIncludeLauncherButton);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireNoCreateRemoteFiles()) {
|
||||
BorderPane noCreateRemoteFiles = new BorderPane();
|
||||
noCreateRemoteFiles.setLeft(new Label(i18n("modpack.wizard.step.initialization.no_create_remote_files")));
|
||||
list.getContent().add(noCreateRemoteFiles);
|
||||
var requireNoCreateRemoteFilesButton = new LineToggleButton();
|
||||
requireNoCreateRemoteFilesButton.setTitle(i18n("modpack.wizard.step.initialization.no_create_remote_files"));
|
||||
requireNoCreateRemoteFilesButton.selectedProperty().bindBidirectional(skinnable.noCreateRemoteFiles);
|
||||
|
||||
JFXToggleButton noCreateRemoteFilesButton = new JFXToggleButton();
|
||||
noCreateRemoteFilesButton.selectedProperty().bindBidirectional(skinnable.noCreateRemoteFiles);
|
||||
noCreateRemoteFilesButton.setSize(8);
|
||||
noCreateRemoteFilesButton.setMinHeight(16);
|
||||
noCreateRemoteFilesButton.setMaxHeight(16);
|
||||
noCreateRemoteFiles.setRight(noCreateRemoteFilesButton);
|
||||
list.getContent().add(requireNoCreateRemoteFilesButton);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireSkipCurseForgeRemoteFiles()) {
|
||||
BorderPane skipCurseForgeRemoteFiles = new BorderPane();
|
||||
skipCurseForgeRemoteFiles.setLeft(new Label(i18n("modpack.wizard.step.initialization.skip_curseforge_remote_files")));
|
||||
list.getContent().add(skipCurseForgeRemoteFiles);
|
||||
|
||||
JFXToggleButton skipCurseForgeRemoteFilesButton = new JFXToggleButton();
|
||||
var skipCurseForgeRemoteFilesButton = new LineToggleButton();
|
||||
skipCurseForgeRemoteFilesButton.setTitle(i18n("modpack.wizard.step.initialization.skip_curseforge_remote_files"));
|
||||
skipCurseForgeRemoteFilesButton.selectedProperty().bindBidirectional(skinnable.skipCurseForgeRemoteFiles);
|
||||
skipCurseForgeRemoteFilesButton.setSize(8);
|
||||
skipCurseForgeRemoteFilesButton.setMinHeight(16);
|
||||
skipCurseForgeRemoteFilesButton.setMaxHeight(16);
|
||||
skipCurseForgeRemoteFiles.setRight(skipCurseForgeRemoteFilesButton);
|
||||
|
||||
list.getContent().add(skipCurseForgeRemoteFilesButton);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,5 +379,31 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
|
||||
FXUtils.smoothScrolling(scroll);
|
||||
}
|
||||
|
||||
private LinePane createTextFieldLinePane(String title, StringProperty property, ValidatorBase... validators) {
|
||||
LinePane linePane = new LinePane();
|
||||
JFXTextField textField = new JFXTextField();
|
||||
textField.setMinWidth(500);
|
||||
|
||||
linePane.setTitle(title);
|
||||
linePane.setRight(textField);
|
||||
textField.textProperty().bindBidirectional(property);
|
||||
|
||||
boolean needValidation = false;
|
||||
if (validators != null) {
|
||||
for (ValidatorBase validator : validators) {
|
||||
if (validator != null) {
|
||||
needValidation = true;
|
||||
textField.getValidators().add(validator);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (needValidation) {
|
||||
FXUtils.setValidateWhileTextChanged(textField, true);
|
||||
validatingFields.add(textField);
|
||||
}
|
||||
|
||||
return linePane;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,11 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import com.google.gson.*;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.image.Image;
|
||||
@@ -30,8 +26,10 @@ import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.IconedTwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.LineButton;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -42,20 +40,22 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class AboutPage extends StackPane {
|
||||
|
||||
private final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
|
||||
public AboutPage() {
|
||||
ComponentList about = new ComponentList();
|
||||
{
|
||||
IconedTwoLineListItem launcher = new IconedTwoLineListItem();
|
||||
launcher.setImage(FXUtils.newBuiltinImage("/assets/img/icon.png"));
|
||||
var launcher = LineButton.createExternalLinkButton(Metadata.PUBLISH_URL);
|
||||
launcher.setLargeTitle(true);
|
||||
launcher.setLeading(FXUtils.newBuiltinImage("/assets/img/icon.png"));
|
||||
launcher.setTitle("Hello Minecraft! Launcher");
|
||||
launcher.setSubtitle(Metadata.VERSION);
|
||||
launcher.setExternalLink(Metadata.PUBLISH_URL);
|
||||
|
||||
IconedTwoLineListItem author = new IconedTwoLineListItem();
|
||||
author.setImage(FXUtils.newBuiltinImage("/assets/img/yellow_fish.png"));
|
||||
var author = LineButton.createExternalLinkButton("https://space.bilibili.com/1445341");
|
||||
author.setLargeTitle(true);
|
||||
author.setLeading(FXUtils.newBuiltinImage("/assets/img/yellow_fish.png"));
|
||||
author.setTitle("huanghongxun");
|
||||
author.setSubtitle(i18n("about.author.statement"));
|
||||
author.setExternalLink("https://space.bilibili.com/1445341");
|
||||
|
||||
about.getContent().setAll(launcher, author);
|
||||
}
|
||||
@@ -66,20 +66,20 @@ public final class AboutPage extends StackPane {
|
||||
|
||||
ComponentList legal = new ComponentList();
|
||||
{
|
||||
IconedTwoLineListItem copyright = new IconedTwoLineListItem();
|
||||
var copyright = LineButton.createExternalLinkButton(Metadata.ABOUT_URL);
|
||||
copyright.setLargeTitle(true);
|
||||
copyright.setTitle(i18n("about.copyright"));
|
||||
copyright.setSubtitle(i18n("about.copyright.statement"));
|
||||
copyright.setExternalLink(Metadata.ABOUT_URL);
|
||||
|
||||
IconedTwoLineListItem claim = new IconedTwoLineListItem();
|
||||
var claim = LineButton.createExternalLinkButton(Metadata.EULA_URL);
|
||||
claim.setLargeTitle(true);
|
||||
claim.setTitle(i18n("about.claim"));
|
||||
claim.setSubtitle(i18n("about.claim.statement"));
|
||||
claim.setExternalLink(Metadata.EULA_URL);
|
||||
|
||||
IconedTwoLineListItem openSource = new IconedTwoLineListItem();
|
||||
var openSource = LineButton.createExternalLinkButton("https://github.com/HMCL-dev/HMCL");
|
||||
openSource.setLargeTitle(true);
|
||||
openSource.setTitle(i18n("about.open_source"));
|
||||
openSource.setSubtitle(i18n("about.open_source.statement"));
|
||||
openSource.setExternalLink("https://github.com/HMCL-dev/HMCL");
|
||||
|
||||
legal.getContent().setAll(copyright, claim, openSource);
|
||||
}
|
||||
@@ -113,7 +113,7 @@ public final class AboutPage extends StackPane {
|
||||
: new Image(url);
|
||||
}
|
||||
|
||||
private static ComponentList loadIconedTwoLineList(String path) {
|
||||
private ComponentList loadIconedTwoLineList(String path) {
|
||||
ComponentList componentList = new ComponentList();
|
||||
|
||||
InputStream input = FXUtils.class.getResourceAsStream(path);
|
||||
@@ -127,36 +127,42 @@ public final class AboutPage extends StackPane {
|
||||
|
||||
for (JsonElement element : array) {
|
||||
JsonObject obj = element.getAsJsonObject();
|
||||
IconedTwoLineListItem item = new IconedTwoLineListItem();
|
||||
|
||||
var button = new LineButton();
|
||||
button.setLargeTitle(true);
|
||||
|
||||
if (obj.get("externalLink") instanceof JsonPrimitive externalLink) {
|
||||
button.setTrailingIcon(SVG.OPEN_IN_NEW);
|
||||
|
||||
String link = externalLink.getAsString();
|
||||
button.setOnAction(event -> FXUtils.openLink(link));
|
||||
}
|
||||
|
||||
if (obj.has("image")) {
|
||||
JsonElement image = obj.get("image");
|
||||
if (image.isJsonPrimitive()) {
|
||||
item.setImage(loadImage(image.getAsString()));
|
||||
button.setLeading(loadImage(image.getAsString()));
|
||||
} else if (image.isJsonObject()) {
|
||||
item.imageProperty().bind(Bindings.when(Themes.darkModeProperty())
|
||||
.then(loadImage(image.getAsJsonObject().get("dark").getAsString()))
|
||||
.otherwise(loadImage(image.getAsJsonObject().get("light").getAsString())));
|
||||
holder.add(FXUtils.onWeakChangeAndOperate(Themes.darkModeProperty(), darkMode -> {
|
||||
button.setLeading(darkMode
|
||||
? loadImage(image.getAsJsonObject().get("dark").getAsString())
|
||||
: loadImage(image.getAsJsonObject().get("light").getAsString())
|
||||
);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (obj.has("title"))
|
||||
item.setTitle(obj.get("title").getAsString());
|
||||
else if (obj.has("titleLocalized"))
|
||||
item.setTitle(i18n(obj.get("titleLocalized").getAsString()));
|
||||
if (obj.get("title") instanceof JsonPrimitive title)
|
||||
button.setTitle(title.getAsString());
|
||||
else if (obj.get("titleLocalized") instanceof JsonPrimitive titleLocalized)
|
||||
button.setTitle(i18n(titleLocalized.getAsString()));
|
||||
|
||||
if (obj.has("subtitle"))
|
||||
item.setSubtitle(obj.get("subtitle").getAsString());
|
||||
else if (obj.has("subtitleLocalized"))
|
||||
item.setSubtitle(i18n(obj.get("subtitleLocalized").getAsString()));
|
||||
if (obj.get("subtitle") instanceof JsonPrimitive subtitle)
|
||||
button.setSubtitle(subtitle.getAsString());
|
||||
else if (obj.get("subtitleLocalized") instanceof JsonPrimitive subtitleLocalized)
|
||||
button.setSubtitle(i18n(subtitleLocalized.getAsString()));
|
||||
|
||||
if (obj.has("externalLink")) {
|
||||
String link = obj.get("externalLink").getAsString();
|
||||
item.setExternalLink(link);
|
||||
FXUtils.installFastTooltip(item.getExternalLinkButton(), link);
|
||||
}
|
||||
|
||||
componentList.getContent().add(item);
|
||||
componentList.getContent().add(button);
|
||||
}
|
||||
} catch (IOException | JsonParseException e) {
|
||||
LOG.warning("Failed to load list: " + path, e);
|
||||
|
||||
@@ -31,15 +31,15 @@ import org.jackhuang.hmcl.task.FetchTask;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
|
||||
|
||||
import java.net.Proxy;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
|
||||
|
||||
public class DownloadSettingsPage extends StackPane {
|
||||
|
||||
@@ -55,51 +55,37 @@ public class DownloadSettingsPage extends StackPane {
|
||||
getChildren().setAll(scrollPane);
|
||||
|
||||
{
|
||||
VBox downloadSource = new VBox(8);
|
||||
var downloadSource = new ComponentList();
|
||||
downloadSource.getStyleClass().add("card-non-transparent");
|
||||
{
|
||||
|
||||
VBox chooseWrapper = new VBox();
|
||||
chooseWrapper.setPadding(new Insets(8, 0, 8, 0));
|
||||
JFXCheckBox chkAutoChooseDownloadSource = new JFXCheckBox(i18n("settings.launcher.download_source.auto"));
|
||||
chkAutoChooseDownloadSource.selectedProperty().bindBidirectional(config().autoChooseDownloadTypeProperty());
|
||||
chooseWrapper.getChildren().setAll(chkAutoChooseDownloadSource);
|
||||
var autoChooseDownloadSource = new LineToggleButton();
|
||||
autoChooseDownloadSource.setTitle(i18n("settings.launcher.download_source.auto"));
|
||||
autoChooseDownloadSource.selectedProperty().bindBidirectional(config().autoChooseDownloadTypeProperty());
|
||||
|
||||
BorderPane versionListSourcePane = new BorderPane();
|
||||
versionListSourcePane.setPadding(new Insets(0, 0, 8, 30));
|
||||
versionListSourcePane.disableProperty().bind(chkAutoChooseDownloadSource.selectedProperty().not());
|
||||
{
|
||||
Label label = new Label(i18n("settings.launcher.version_list_source"));
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
versionListSourcePane.setLeft(label);
|
||||
Function<String, String> converter = key -> i18n("download.provider." + key);
|
||||
Function<String, String> descriptionConverter = key -> {
|
||||
String bundleKey = "download.provider." + key + ".desc";
|
||||
return I18n.hasKey(bundleKey) ? i18n(bundleKey) : null;
|
||||
};
|
||||
|
||||
JFXComboBox<String> cboVersionListSource = new JFXComboBox<>();
|
||||
cboVersionListSource.setConverter(stringConverter(key -> i18n("download.provider." + key)));
|
||||
versionListSourcePane.setRight(cboVersionListSource);
|
||||
FXUtils.setLimitWidth(cboVersionListSource, 400);
|
||||
var versionListSourcePane = new LineSelectButton<String>();
|
||||
versionListSourcePane.disableProperty().bind(autoChooseDownloadSource.selectedProperty().not());
|
||||
versionListSourcePane.setTitle(i18n("settings.launcher.version_list_source"));
|
||||
versionListSourcePane.setConverter(converter);
|
||||
versionListSourcePane.setDescriptionConverter(descriptionConverter);
|
||||
versionListSourcePane.setItems(DownloadProviders.AUTO_PROVIDERS.keySet());
|
||||
versionListSourcePane.valueProperty().bindBidirectional(config().versionListSourceProperty());
|
||||
|
||||
cboVersionListSource.getItems().setAll(DownloadProviders.AUTO_PROVIDERS.keySet());
|
||||
selectedItemPropertyFor(cboVersionListSource).bindBidirectional(config().versionListSourceProperty());
|
||||
}
|
||||
var downloadSourcePane = new LineSelectButton<String>();
|
||||
downloadSourcePane.disableProperty().bind(autoChooseDownloadSource.selectedProperty());
|
||||
downloadSourcePane.setTitle(i18n("settings.launcher.download_source"));
|
||||
downloadSourcePane.setConverter(converter);
|
||||
downloadSourcePane.setDescriptionConverter(descriptionConverter);
|
||||
downloadSourcePane.setItems(DownloadProviders.DIRECT_PROVIDERS.keySet());
|
||||
downloadSourcePane.valueProperty().bindBidirectional(config().downloadTypeProperty());
|
||||
|
||||
BorderPane downloadSourcePane = new BorderPane();
|
||||
downloadSourcePane.setPadding(new Insets(0, 0, 8, 30));
|
||||
downloadSourcePane.disableProperty().bind(chkAutoChooseDownloadSource.selectedProperty());
|
||||
{
|
||||
Label label = new Label(i18n("settings.launcher.download_source"));
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
downloadSourcePane.setLeft(label);
|
||||
|
||||
JFXComboBox<String> cboDownloadSource = new JFXComboBox<>();
|
||||
cboDownloadSource.setConverter(stringConverter(key -> i18n("download.provider." + key)));
|
||||
downloadSourcePane.setRight(cboDownloadSource);
|
||||
FXUtils.setLimitWidth(cboDownloadSource, 420);
|
||||
|
||||
cboDownloadSource.getItems().setAll(DownloadProviders.DIRECT_PROVIDERS.keySet());
|
||||
selectedItemPropertyFor(cboDownloadSource).bindBidirectional(config().downloadTypeProperty());
|
||||
}
|
||||
|
||||
downloadSource.getChildren().setAll(chooseWrapper, versionListSourcePane, downloadSourcePane);
|
||||
downloadSource.getContent().setAll(autoChooseDownloadSource, versionListSourcePane, downloadSourcePane);
|
||||
}
|
||||
|
||||
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.download_source")), downloadSource);
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.IconedTwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.LineButton;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -33,6 +33,8 @@ import org.jackhuang.hmcl.Metadata;
|
||||
|
||||
public class FeedbackPage extends SpinnerPane {
|
||||
|
||||
private final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
|
||||
public FeedbackPage() {
|
||||
VBox content = new VBox();
|
||||
content.setPadding(new Insets(10));
|
||||
@@ -45,30 +47,33 @@ public class FeedbackPage extends SpinnerPane {
|
||||
|
||||
ComponentList groups = new ComponentList();
|
||||
{
|
||||
IconedTwoLineListItem users = new IconedTwoLineListItem();
|
||||
users.setImage(FXUtils.newBuiltinImage("/assets/img/icon.png"));
|
||||
var users = LineButton.createExternalLinkButton(Metadata.GROUPS_URL);
|
||||
users.setLargeTitle(true);
|
||||
users.setLeading(FXUtils.newBuiltinImage("/assets/img/icon.png"));
|
||||
users.setTitle(i18n("contact.chat.qq_group"));
|
||||
users.setSubtitle(i18n("contact.chat.qq_group.statement"));
|
||||
users.setExternalLink(Metadata.GROUPS_URL);
|
||||
|
||||
IconedTwoLineListItem discord = new IconedTwoLineListItem();
|
||||
discord.setImage(FXUtils.newBuiltinImage("/assets/img/discord.png"));
|
||||
var discord = LineButton.createExternalLinkButton("https://discord.gg/jVvC7HfM6U");
|
||||
discord.setLargeTitle(true);
|
||||
discord.setLeading(FXUtils.newBuiltinImage("/assets/img/discord.png"));
|
||||
discord.setTitle(i18n("contact.chat.discord"));
|
||||
discord.setSubtitle(i18n("contact.chat.discord.statement"));
|
||||
discord.setExternalLink("https://discord.gg/jVvC7HfM6U");
|
||||
|
||||
groups.getContent().setAll(users, discord);
|
||||
}
|
||||
|
||||
ComponentList feedback = new ComponentList();
|
||||
{
|
||||
IconedTwoLineListItem github = new IconedTwoLineListItem();
|
||||
github.imageProperty().bind(Bindings.when(Themes.darkModeProperty())
|
||||
.then(FXUtils.newBuiltinImage("/assets/img/github-white.png"))
|
||||
.otherwise(FXUtils.newBuiltinImage("/assets/img/github.png")));
|
||||
var github = LineButton.createExternalLinkButton("https://github.com/HMCL-dev/HMCL/issues/new/choose");
|
||||
github.setLargeTitle(true);
|
||||
github.setTitle(i18n("contact.feedback.github"));
|
||||
github.setSubtitle(i18n("contact.feedback.github.statement"));
|
||||
github.setExternalLink("https://github.com/HMCL-dev/HMCL/issues/new/choose");
|
||||
|
||||
holder.add(FXUtils.onWeakChangeAndOperate(Themes.darkModeProperty(), darkMode -> {
|
||||
github.setLeading(darkMode
|
||||
? FXUtils.newBuiltinImage("/assets/img/github-white.png")
|
||||
: FXUtils.newBuiltinImage("/assets/img/github.png"));
|
||||
}));
|
||||
|
||||
feedback.getContent().setAll(github);
|
||||
}
|
||||
|
||||
@@ -26,11 +26,11 @@ import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.IconedTwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.LineButton;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSerializable;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
|
||||
@@ -50,10 +50,11 @@ public class HelpPage extends SpinnerPane {
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
setContent(scrollPane);
|
||||
|
||||
IconedTwoLineListItem docPane = new IconedTwoLineListItem();
|
||||
var docPane = LineButton.createExternalLinkButton(Metadata.DOCS_URL);
|
||||
docPane.setLargeTitle(true);
|
||||
docPane.setTitle(i18n("help.doc"));
|
||||
docPane.setSubtitle(i18n("help.detail"));
|
||||
docPane.setExternalLink(Metadata.DOCS_URL);
|
||||
|
||||
ComponentList doc = new ComponentList();
|
||||
doc.getContent().setAll(docPane);
|
||||
content.getChildren().add(doc);
|
||||
@@ -68,77 +69,32 @@ public class HelpPage extends SpinnerPane {
|
||||
for (HelpCategory category : helpCategories) {
|
||||
ComponentList categoryPane = new ComponentList();
|
||||
|
||||
for (Help help : category.getItems()) {
|
||||
IconedTwoLineListItem item = new IconedTwoLineListItem();
|
||||
item.setTitle(help.getTitle());
|
||||
item.setSubtitle(help.getSubtitle());
|
||||
item.setExternalLink(help.getUrl());
|
||||
for (Help help : category.items()) {
|
||||
var item = LineButton.createExternalLinkButton(help.url());
|
||||
item.setLargeTitle(true);
|
||||
item.setTitle(help.title());
|
||||
item.setSubtitle(help.subtitle());
|
||||
|
||||
categoryPane.getContent().add(item);
|
||||
}
|
||||
|
||||
content.getChildren().add(ComponentList.createComponentListTitle(category.title));
|
||||
content.getChildren().add(ComponentList.createComponentListTitle(category.title()));
|
||||
content.getChildren().add(categoryPane);
|
||||
}
|
||||
hideSpinner();
|
||||
}).start();
|
||||
}
|
||||
|
||||
private static class HelpCategory {
|
||||
@SerializedName("title")
|
||||
private final String title;
|
||||
|
||||
@SerializedName("items")
|
||||
private final List<Help> items;
|
||||
|
||||
public HelpCategory() {
|
||||
this("", Collections.emptyList());
|
||||
}
|
||||
|
||||
public HelpCategory(String title, List<Help> items) {
|
||||
this.title = title;
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public List<Help> getItems() {
|
||||
return items;
|
||||
}
|
||||
@JsonSerializable
|
||||
private record HelpCategory(
|
||||
@SerializedName("title") String title,
|
||||
@SerializedName("items") List<Help> items) {
|
||||
}
|
||||
|
||||
private static class Help {
|
||||
@SerializedName("title")
|
||||
private final String title;
|
||||
|
||||
@SerializedName("subtitle")
|
||||
private final String subtitle;
|
||||
|
||||
@SerializedName("url")
|
||||
private final String url;
|
||||
|
||||
public Help() {
|
||||
this("", "", "");
|
||||
}
|
||||
|
||||
public Help(String title, String subtitle, String url) {
|
||||
this.title = title;
|
||||
this.subtitle = subtitle;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getSubtitle() {
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
@JsonSerializable
|
||||
private record Help(
|
||||
@SerializedName("title") String title,
|
||||
@SerializedName("subtitle") String subtitle,
|
||||
@SerializedName("url") String url) {
|
||||
}
|
||||
}
|
||||
@@ -18,13 +18,13 @@
|
||||
package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.scene.layout.ColumnConstraints;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
@@ -48,6 +48,8 @@ import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
|
||||
import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Result;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
@@ -108,23 +110,39 @@ public final class JavaDownloadDialog extends StackPane {
|
||||
}
|
||||
|
||||
private final class DownloadMojangJava extends DialogPane {
|
||||
private final ToggleGroup toggleGroup = new ToggleGroup();
|
||||
private final List<JFXCheckBox> checkboxes = new ArrayList<>();
|
||||
|
||||
DownloadMojangJava() {
|
||||
setTitle(i18n("java.download"));
|
||||
validProperty().bind(toggleGroup.selectedToggleProperty().isNotNull());
|
||||
setTitle(i18n("java.download.title"));
|
||||
|
||||
VBox vbox = new VBox(16);
|
||||
Label prompt = new Label(i18n("java.download.prompt"));
|
||||
vbox.getChildren().add(prompt);
|
||||
|
||||
setValid(false);
|
||||
InvalidationListener updateValidStatus = e -> {
|
||||
for (JFXCheckBox box : checkboxes) {
|
||||
if (!box.isDisabled() && box.isSelected()) {
|
||||
setValid(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
setValid(false);
|
||||
};
|
||||
|
||||
for (GameJavaVersion version : supportedGameJavaVersions) {
|
||||
JFXRadioButton button = new JFXRadioButton("Java " + version.majorVersion());
|
||||
JFXCheckBox button = new JFXCheckBox("Java " + version.majorVersion());
|
||||
button.setUserData(version);
|
||||
vbox.getChildren().add(button);
|
||||
toggleGroup.getToggles().add(button);
|
||||
if (JavaManager.REPOSITORY.isInstalled(platform, version))
|
||||
|
||||
if (JavaManager.REPOSITORY.isInstalled(platform, version)) {
|
||||
button.setDisable(true);
|
||||
button.setSelected(true);
|
||||
} else {
|
||||
button.selectedProperty().addListener(updateValidStatus);
|
||||
}
|
||||
|
||||
vbox.getChildren().add(button);
|
||||
checkboxes.add(button);
|
||||
}
|
||||
|
||||
setBody(vbox);
|
||||
@@ -137,38 +155,52 @@ public final class JavaDownloadDialog extends StackPane {
|
||||
setActions(warningLabel, acceptPane, cancelButton);
|
||||
}
|
||||
|
||||
private Task<Void> downloadTask(GameJavaVersion javaVersion) {
|
||||
return JavaManager.getDownloadJavaTask(downloadProvider, platform, javaVersion).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception != null) {
|
||||
Throwable resolvedException = resolveException(exception);
|
||||
LOG.warning("Failed to download java", exception);
|
||||
if (!(resolvedException instanceof CancellationException)) {
|
||||
Controllers.dialog(DownloadProviders.localizeErrorMessage(resolvedException), i18n("install.failed"));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAccept() {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
|
||||
GameJavaVersion javaVersion = (GameJavaVersion) toggleGroup.getSelectedToggle().getUserData();
|
||||
if (javaVersion == null)
|
||||
List<GameJavaVersion> selectedVersions = new ArrayList<>();
|
||||
for (JFXCheckBox box : checkboxes) {
|
||||
if (!box.isDisabled() && box.isSelected()) {
|
||||
selectedVersions.add((GameJavaVersion) box.getUserData());
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedVersions.isEmpty())
|
||||
return;
|
||||
|
||||
if (JavaManager.REPOSITORY.isInstalled(platform, javaVersion))
|
||||
Controllers.confirm(i18n("download.java.override"), null, () -> {
|
||||
Controllers.taskDialog(Task.supplyAsync(() -> JavaManager.REPOSITORY.getJavaExecutable(platform, javaVersion))
|
||||
.thenComposeAsync(Schedulers.javafx(), realPath -> {
|
||||
if (realPath != null) {
|
||||
JavaManager.removeJava(realPath);
|
||||
Task<Void> task = Task.allOf(selectedVersions.stream().map(javaVersion -> JavaManager.getDownloadJavaTask(downloadProvider, platform, javaVersion).wrapResult()).toList())
|
||||
.whenComplete(Schedulers.javafx(), (results, exception) -> {
|
||||
Throwable exceptionToDisplay;
|
||||
if (exception == null) {
|
||||
List<Throwable> exceptions = results.stream()
|
||||
.filter(Result::isFailure)
|
||||
.map(Result::getException)
|
||||
.map(Lang::resolveException)
|
||||
.filter(it -> !(it instanceof CancellationException))
|
||||
.toList();
|
||||
if (!exceptions.isEmpty()) {
|
||||
if (exceptions.size() == 1) {
|
||||
exceptionToDisplay = exceptions.get(0);
|
||||
} else {
|
||||
exceptionToDisplay = new IOException("Failed to download Java");
|
||||
for (Throwable e : exceptions)
|
||||
exceptionToDisplay.addSuppressed(e);
|
||||
}
|
||||
return downloadTask(javaVersion);
|
||||
}), i18n("download.java"), TaskCancellationAction.NORMAL);
|
||||
}, null);
|
||||
else
|
||||
Controllers.taskDialog(downloadTask(javaVersion), i18n("download.java.process"), TaskCancellationAction.NORMAL);
|
||||
} else {
|
||||
exceptionToDisplay = null;
|
||||
}
|
||||
} else {
|
||||
exceptionToDisplay = exception;
|
||||
}
|
||||
|
||||
if (exceptionToDisplay != null) {
|
||||
LOG.warning("Failed to download java", exceptionToDisplay);
|
||||
Controllers.dialog(DownloadProviders.localizeErrorMessage(exceptionToDisplay), i18n("install.failed"));
|
||||
}
|
||||
});
|
||||
|
||||
Controllers.taskDialog(task, i18n("download.java.process"), TaskCancellationAction.NORMAL);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,7 +343,7 @@ public final class JavaDownloadDialog extends StackPane {
|
||||
FXUtils.onChange(distributionBox.getSelectionModel().selectedItemProperty(),
|
||||
it -> currentJavaVersionList.set(getJavaVersionList(it)));
|
||||
|
||||
setHeading(new Label(i18n("java.download")));
|
||||
setHeading(new Label(i18n("java.download.title")));
|
||||
setBody(body);
|
||||
setActions(warningLabel, downloadButtonPane, cancelButton);
|
||||
if (platform.getOperatingSystem() == OperatingSystem.LINUX && platform.getArchitecture() == Architecture.RISCV64) {
|
||||
|
||||
@@ -21,9 +21,7 @@ import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
@@ -110,17 +108,13 @@ public final class JavaInstallPage extends WizardSinglePage {
|
||||
|
||||
|
||||
{
|
||||
BorderPane namePane = new BorderPane();
|
||||
var namePane = new LinePane();
|
||||
{
|
||||
Label label = new Label(i18n("java.install.name"));
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
namePane.setLeft(label);
|
||||
namePane.setTitle(i18n("java.install.name"));
|
||||
|
||||
nameField = new JFXTextField();
|
||||
nameField.textProperty().bindBidirectional(control.nameProperty);
|
||||
FXUtils.setLimitWidth(nameField, 200);
|
||||
BorderPane.setAlignment(nameField, Pos.CENTER_RIGHT);
|
||||
BorderPane.setMargin(nameField, new Insets(0, 0, 12, 0));
|
||||
namePane.setRight(nameField);
|
||||
nameField.setValidators(
|
||||
new RequiredValidator(),
|
||||
@@ -176,16 +170,9 @@ public final class JavaInstallPage extends WizardSinglePage {
|
||||
}
|
||||
|
||||
private void addInfo(String name, String value) {
|
||||
BorderPane pane = new BorderPane();
|
||||
|
||||
pane.setLeft(new Label(name));
|
||||
|
||||
Label valueLabel = FXUtils.newSafeTruncatedLabel(value);
|
||||
FXUtils.copyOnDoubleClick(valueLabel);
|
||||
BorderPane.setMargin(valueLabel, new Insets(0, 0, 0, 16));
|
||||
BorderPane.setAlignment(valueLabel, Pos.CENTER_RIGHT);
|
||||
pane.setCenter(valueLabel);
|
||||
|
||||
LineTextPane pane = new LineTextPane();
|
||||
pane.setTitle(name);
|
||||
pane.setText(value);
|
||||
this.componentList.getContent().add(pane);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.java.JavaInfo;
|
||||
@@ -189,7 +190,6 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
|
||||
|
||||
JavaPageSkin(JavaManagementPage skinnable) {
|
||||
super(skinnable);
|
||||
this.listView.getStyleClass().add("no-horizontal-scrollbar");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -232,6 +232,7 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
|
||||
center.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
this.content = new TwoLineListItem();
|
||||
HBox.setHgrow(content, Priority.ALWAYS);
|
||||
|
||||
BorderPane.setAlignment(content, Pos.CENTER);
|
||||
center.getChildren().setAll(content);
|
||||
@@ -241,7 +242,7 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
|
||||
right.setAlignment(Pos.CENTER_RIGHT);
|
||||
{
|
||||
JFXButton revealButton = new JFXButton();
|
||||
revealButton.setGraphic(FXUtils.limitingSize(SVG.FOLDER_OPEN.createIcon(24), 24, 24));
|
||||
revealButton.setGraphic(SVG.FOLDER_OPEN.createIcon());
|
||||
revealButton.getStyleClass().add("toggle-icon4");
|
||||
revealButton.setOnAction(e -> {
|
||||
JavaRuntime java = getItem();
|
||||
|
||||
@@ -159,7 +159,7 @@ public final class JavaRestorePage extends ListPageBase<JavaRestorePage.Disabled
|
||||
{
|
||||
JFXButton revealButton = new JFXButton();
|
||||
revealButton.getStyleClass().add("toggle-icon4");
|
||||
revealButton.setGraphic(FXUtils.limitingSize(SVG.FOLDER_OPEN.createIcon(24), 24, 24));
|
||||
revealButton.setGraphic(SVG.FOLDER_OPEN.createIcon());
|
||||
revealButton.setOnAction(e -> skinnable.onReveal());
|
||||
FXUtils.installFastTooltip(revealButton, i18n("reveal.in_file_manager"));
|
||||
|
||||
@@ -168,7 +168,7 @@ public final class JavaRestorePage extends ListPageBase<JavaRestorePage.Disabled
|
||||
|
||||
JFXButton removeButton = new JFXButton();
|
||||
removeButton.getStyleClass().add("toggle-icon4");
|
||||
removeButton.setGraphic(FXUtils.limitingSize(SVG.DELETE.createIcon(24), 24, 24));
|
||||
removeButton.setGraphic(SVG.DELETE.createIcon());
|
||||
removeButton.setOnAction(e -> skinnable.onRemove());
|
||||
FXUtils.installFastTooltip(removeButton, i18n("java.disabled.management.remove"));
|
||||
|
||||
@@ -176,7 +176,7 @@ public final class JavaRestorePage extends ListPageBase<JavaRestorePage.Disabled
|
||||
} else {
|
||||
JFXButton restoreButton = new JFXButton();
|
||||
restoreButton.getStyleClass().add("toggle-icon4");
|
||||
restoreButton.setGraphic(FXUtils.limitingSize(SVG.RESTORE.createIcon(24), 24, 24));
|
||||
restoreButton.setGraphic(SVG.RESTORE.createIcon());
|
||||
restoreButton.setOnAction(e -> skinnable.onRestore());
|
||||
FXUtils.installFastTooltip(restoreButton, i18n("java.disabled.management.restore"));
|
||||
|
||||
|
||||
@@ -64,12 +64,12 @@ import org.jackhuang.hmcl.ui.versions.Versions;
|
||||
import org.jackhuang.hmcl.upgrade.RemoteVersion;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
@@ -320,6 +320,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
Task<?> task = versionList.refreshAsync("")
|
||||
.thenSupplyAsync(() -> versionList.getVersions("").stream()
|
||||
.filter(it -> it.getVersionType() == RELEASE)
|
||||
.filter(it -> NativePatcher.checkSupportedStatus(GameVersionNumber.asGameVersion(it.getGameVersion()), Platform.SYSTEM_PLATFORM, OperatingSystem.SYSTEM_VERSION) != NativePatcher.SupportStatus.UNSUPPORTED)
|
||||
.sorted()
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IOException("No versions found")))
|
||||
|
||||
@@ -25,7 +25,6 @@ import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.binding.When;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.ColorPicker;
|
||||
@@ -78,19 +77,13 @@ public class PersonalizationPage extends StackPane {
|
||||
|
||||
ComponentList themeList = new ComponentList();
|
||||
{
|
||||
BorderPane brightnessPane = new BorderPane();
|
||||
var brightnessPane = new LineSelectButton<String>();
|
||||
brightnessPane.setTitle(i18n("settings.launcher.brightness"));
|
||||
brightnessPane.setConverter(name -> i18n("settings.launcher.brightness." + name));
|
||||
brightnessPane.setItems("auto", "light", "dark");
|
||||
brightnessPane.valueProperty().bindBidirectional(config().themeBrightnessProperty());
|
||||
|
||||
themeList.getContent().add(brightnessPane);
|
||||
|
||||
Label left = new Label(i18n("settings.launcher.brightness"));
|
||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||
|
||||
brightnessPane.setLeft(left);
|
||||
|
||||
JFXComboBox<String> cboBrightness = new JFXComboBox<>();
|
||||
cboBrightness.getItems().setAll("auto", "light", "dark");
|
||||
cboBrightness.setConverter(FXUtils.stringConverter(name -> i18n("settings.launcher.brightness." + name)));
|
||||
cboBrightness.valueProperty().bindBidirectional(config().themeBrightnessProperty());
|
||||
brightnessPane.setRight(cboBrightness);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -112,13 +105,13 @@ public class PersonalizationPage extends StackPane {
|
||||
Platform.runLater(() -> JFXDepthManager.setDepth(picker, 0));
|
||||
}
|
||||
{
|
||||
OptionToggleButton titleTransparentButton = new OptionToggleButton();
|
||||
LineToggleButton titleTransparentButton = new LineToggleButton();
|
||||
themeList.getContent().add(titleTransparentButton);
|
||||
titleTransparentButton.selectedProperty().bindBidirectional(config().titleTransparentProperty());
|
||||
titleTransparentButton.setTitle(i18n("settings.launcher.title_transparent"));
|
||||
}
|
||||
{
|
||||
OptionToggleButton animationButton = new OptionToggleButton();
|
||||
LineToggleButton animationButton = new LineToggleButton();
|
||||
themeList.getContent().add(animationButton);
|
||||
animationButton.selectedProperty().bindBidirectional(config().animationDisabledProperty());
|
||||
animationButton.setTitle(i18n("settings.launcher.turn_off_animations"));
|
||||
@@ -187,7 +180,7 @@ public class PersonalizationPage extends StackPane {
|
||||
}
|
||||
|
||||
Label textOpacity = new Label();
|
||||
FXUtils.setLimitWidth(textOpacity, 35);
|
||||
FXUtils.setLimitWidth(textOpacity, 50);
|
||||
|
||||
StringBinding valueBinding = Bindings.createStringBinding(() -> ((int) slider.getValue()) + "%", slider.valueProperty());
|
||||
textOpacity.textProperty().bind(valueBinding);
|
||||
@@ -204,8 +197,7 @@ public class PersonalizationPage extends StackPane {
|
||||
}
|
||||
|
||||
{
|
||||
ComponentList logPane = new ComponentSublist();
|
||||
logPane.setTitle(i18n("settings.launcher.log"));
|
||||
ComponentList logPane = new ComponentList();
|
||||
|
||||
{
|
||||
VBox fontPane = new VBox();
|
||||
@@ -261,8 +253,7 @@ public class PersonalizationPage extends StackPane {
|
||||
}
|
||||
|
||||
{
|
||||
ComponentSublist fontPane = new ComponentSublist();
|
||||
fontPane.setTitle(i18n("settings.launcher.font"));
|
||||
ComponentList fontPane = new ComponentList();
|
||||
|
||||
{
|
||||
VBox vbox = new VBox();
|
||||
@@ -304,45 +295,32 @@ public class PersonalizationPage extends StackPane {
|
||||
}
|
||||
|
||||
{
|
||||
BorderPane fontAntiAliasingPane = new BorderPane();
|
||||
{
|
||||
VBox left = new VBox();
|
||||
Label title = new Label(i18n("settings.launcher.font.anti_aliasing"));
|
||||
title.getStyleClass().add("title");
|
||||
Label subtitle = new Label(i18n("settings.take_effect_after_restart"));
|
||||
subtitle.getStyleClass().add("subtitle");
|
||||
left.getChildren().setAll(title, subtitle);
|
||||
fontAntiAliasingPane.setLeft(left);
|
||||
var fontAntiAliasingPane = new LineSelectButton<Optional<FontSmoothingType>>();
|
||||
fontAntiAliasingPane.setTitle(i18n("settings.launcher.font.anti_aliasing"));
|
||||
fontAntiAliasingPane.setSubtitle(i18n("settings.take_effect_after_restart"));
|
||||
fontAntiAliasingPane.setConverter(value ->
|
||||
value.isPresent()
|
||||
? i18n("settings.launcher.font.anti_aliasing." + value.get().name().toLowerCase(Locale.ROOT))
|
||||
: i18n("settings.launcher.font.anti_aliasing.auto")
|
||||
);
|
||||
fontAntiAliasingPane.setItems(
|
||||
Optional.empty(),
|
||||
Optional.of(FontSmoothingType.LCD),
|
||||
Optional.of(FontSmoothingType.GRAY)
|
||||
);
|
||||
|
||||
String fontAntiAliasing = globalConfig().getFontAntiAliasing();
|
||||
if ("lcd".equalsIgnoreCase(fontAntiAliasing)) {
|
||||
fontAntiAliasingPane.setValue(Optional.of(FontSmoothingType.LCD));
|
||||
} else if ("gray".equalsIgnoreCase(fontAntiAliasing)) {
|
||||
fontAntiAliasingPane.setValue(Optional.of(FontSmoothingType.GRAY));
|
||||
} else {
|
||||
fontAntiAliasingPane.setValue(Optional.empty());
|
||||
}
|
||||
|
||||
{
|
||||
@SuppressWarnings("unchecked")
|
||||
JFXComboBox<Optional<FontSmoothingType>> cboAntiAliasing = new JFXComboBox<>(FXCollections.observableArrayList(
|
||||
Optional.empty(),
|
||||
Optional.of(FontSmoothingType.LCD),
|
||||
Optional.of(FontSmoothingType.GRAY)
|
||||
));
|
||||
String fontAntiAliasing = globalConfig().getFontAntiAliasing();
|
||||
if ("lcd".equalsIgnoreCase(fontAntiAliasing)) {
|
||||
cboAntiAliasing.setValue(Optional.of(FontSmoothingType.LCD));
|
||||
} else if ("gray".equalsIgnoreCase(fontAntiAliasing)) {
|
||||
cboAntiAliasing.setValue(Optional.of(FontSmoothingType.GRAY));
|
||||
} else {
|
||||
cboAntiAliasing.setValue(Optional.empty());
|
||||
}
|
||||
cboAntiAliasing.setConverter(FXUtils.stringConverter(value -> {
|
||||
if (value.isPresent()) {
|
||||
return i18n("settings.launcher.font.anti_aliasing." + value.get().name().toLowerCase(Locale.ROOT));
|
||||
} else {
|
||||
return i18n("settings.launcher.font.anti_aliasing.auto");
|
||||
}
|
||||
}));
|
||||
FXUtils.onChange(cboAntiAliasing.valueProperty(), value ->
|
||||
globalConfig().setFontAntiAliasing(value.map(it -> it.name().toLowerCase(Locale.ROOT))
|
||||
.orElse(null)));
|
||||
|
||||
fontAntiAliasingPane.setRight(cboAntiAliasing);
|
||||
}
|
||||
FXUtils.onChange(fontAntiAliasingPane.valueProperty(), value ->
|
||||
globalConfig().setFontAntiAliasing(value.map(it -> it.name().toLowerCase(Locale.ROOT))
|
||||
.orElse(null)));
|
||||
|
||||
fontPane.getContent().add(fontAntiAliasingPane);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.scene.layout.Region;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
|
||||
@@ -65,7 +66,6 @@ import java.util.Locale;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.wrap;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@@ -171,16 +171,14 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
|
||||
// third item in left sidebar
|
||||
AdvancedListItem gameItem = new AdvancedListItem();
|
||||
gameItem.setLeftGraphic(wrap(SVG.FORMAT_LIST_BULLETED));
|
||||
gameItem.setActionButtonVisible(false);
|
||||
gameItem.setLeftIcon(SVG.FORMAT_LIST_BULLETED);
|
||||
gameItem.setTitle(i18n("version.manage"));
|
||||
gameItem.setOnAction(e -> Controllers.navigate(Controllers.getGameListPage()));
|
||||
FXUtils.onSecondaryButtonClicked(gameItem, () -> showGameListPopupMenu(gameItem));
|
||||
|
||||
// forth item in left sidebar
|
||||
AdvancedListItem downloadItem = new AdvancedListItem();
|
||||
downloadItem.setLeftGraphic(wrap(SVG.DOWNLOAD));
|
||||
downloadItem.setActionButtonVisible(false);
|
||||
downloadItem.setLeftIcon(SVG.DOWNLOAD);
|
||||
downloadItem.setTitle(i18n("download"));
|
||||
downloadItem.setOnAction(e -> {
|
||||
Controllers.getDownloadPage().showGameDownloads();
|
||||
@@ -193,8 +191,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
|
||||
// fifth item in left sidebar
|
||||
AdvancedListItem launcherSettingsItem = new AdvancedListItem();
|
||||
launcherSettingsItem.setLeftGraphic(wrap(SVG.SETTINGS));
|
||||
launcherSettingsItem.setActionButtonVisible(false);
|
||||
launcherSettingsItem.setLeftIcon(SVG.SETTINGS);
|
||||
launcherSettingsItem.setTitle(i18n("settings"));
|
||||
launcherSettingsItem.setOnAction(e -> {
|
||||
Controllers.getSettingsPage().showGameSettings(Profiles.getSelectedProfile());
|
||||
@@ -206,8 +203,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
|
||||
// sixth item in left sidebar
|
||||
AdvancedListItem terracottaItem = new AdvancedListItem();
|
||||
terracottaItem.setLeftGraphic(wrap(SVG.GRAPH2));
|
||||
terracottaItem.setActionButtonVisible(false);
|
||||
terracottaItem.setLeftIcon(SVG.GRAPH2);
|
||||
terracottaItem.setTitle(i18n("terracotta"));
|
||||
terracottaItem.setOnAction(e -> {
|
||||
if (TerracottaMetadata.PROVIDER != null) {
|
||||
@@ -249,7 +245,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
setCenter(getSkinnable().getMainPage());
|
||||
}
|
||||
|
||||
public void showGameListPopupMenu(AdvancedListItem gameListItem) {
|
||||
public void showGameListPopupMenu(Region gameListItem) {
|
||||
GameListPopupMenu.show(gameListItem,
|
||||
JFXPopup.PopupVPosition.TOP,
|
||||
JFXPopup.PopupHPosition.LEFT,
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXComboBox;
|
||||
import com.jfoenix.controls.JFXRadioButton;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
@@ -40,11 +39,8 @@ import org.jackhuang.hmcl.setting.Settings;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentSublist;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.ui.construct.MultiFileItem;
|
||||
import org.jackhuang.hmcl.ui.construct.OptionToggleButton;
|
||||
import org.jackhuang.hmcl.upgrade.RemoteVersion;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChannel;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
@@ -69,7 +65,6 @@ import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
|
||||
@@ -183,20 +178,21 @@ public final class SettingsPage extends ScrollPane {
|
||||
}
|
||||
|
||||
{
|
||||
VBox content = new VBox();
|
||||
content.setSpacing(8);
|
||||
|
||||
JFXRadioButton chkUpdateStable = new JFXRadioButton(i18n("update.channel.stable"));
|
||||
JFXRadioButton chkUpdateDev = new JFXRadioButton(i18n("update.channel.dev"));
|
||||
VBox content = new VBox(12);
|
||||
content.setPadding(new Insets(8, 0, 0, 0));
|
||||
|
||||
updateChannelGroup = new ToggleGroup();
|
||||
chkUpdateDev.setToggleGroup(updateChannelGroup);
|
||||
chkUpdateDev.setUserData(UpdateChannel.DEVELOPMENT);
|
||||
chkUpdateStable.setToggleGroup(updateChannelGroup);
|
||||
|
||||
JFXRadioButton chkUpdateStable = new JFXRadioButton(i18n("update.channel.stable"));
|
||||
chkUpdateStable.setUserData(UpdateChannel.STABLE);
|
||||
chkUpdateStable.setToggleGroup(updateChannelGroup);
|
||||
|
||||
JFXRadioButton chkUpdateDev = new JFXRadioButton(i18n("update.channel.dev"));
|
||||
chkUpdateDev.setUserData(UpdateChannel.DEVELOPMENT);
|
||||
chkUpdateDev.setToggleGroup(updateChannelGroup);
|
||||
|
||||
Label noteWrapper = new Label(i18n("update.note"));
|
||||
VBox.setMargin(noteWrapper, new Insets(10, 0, 0, 0));
|
||||
VBox.setMargin(noteWrapper, new Insets(8, 0, 0, 0));
|
||||
|
||||
content.getChildren().setAll(chkUpdateStable, chkUpdateDev, noteWrapper);
|
||||
|
||||
@@ -206,7 +202,7 @@ public final class SettingsPage extends ScrollPane {
|
||||
}
|
||||
|
||||
{
|
||||
OptionToggleButton previewPane = new OptionToggleButton();
|
||||
LineToggleButton previewPane = new LineToggleButton();
|
||||
previewPane.setTitle(i18n("update.preview"));
|
||||
previewPane.setSubtitle(i18n("update.preview.subtitle"));
|
||||
previewPane.selectedProperty().bindBidirectional(config().acceptPreviewUpdateProperty());
|
||||
@@ -250,37 +246,27 @@ public final class SettingsPage extends ScrollPane {
|
||||
}
|
||||
|
||||
{
|
||||
BorderPane languagePane = new BorderPane();
|
||||
|
||||
VBox left = new VBox();
|
||||
Label title = new Label(i18n("settings.launcher.language"));
|
||||
title.getStyleClass().add("title");
|
||||
Label subtitle = new Label(i18n("settings.take_effect_after_restart"));
|
||||
subtitle.getStyleClass().add("subtitle");
|
||||
left.getChildren().setAll(title, subtitle);
|
||||
languagePane.setLeft(left);
|
||||
var chooseLanguagePane = new LineSelectButton<SupportedLocale>();
|
||||
chooseLanguagePane.setTitle(i18n("settings.launcher.language"));
|
||||
chooseLanguagePane.setSubtitle(i18n("settings.take_effect_after_restart"));
|
||||
|
||||
SupportedLocale currentLocale = I18n.getLocale();
|
||||
JFXComboBox<SupportedLocale> cboLanguage = new JFXComboBox<>();
|
||||
cboLanguage.setConverter(stringConverter(locale -> {
|
||||
chooseLanguagePane.setConverter(locale -> {
|
||||
if (locale.isDefault())
|
||||
return locale.getDisplayName(currentLocale);
|
||||
else if (locale.isSameLanguage(currentLocale))
|
||||
return locale.getDisplayName(locale);
|
||||
else
|
||||
return locale.getDisplayName(currentLocale) + " - " + locale.getDisplayName(locale);
|
||||
}));
|
||||
cboLanguage.getItems().setAll(SupportedLocale.getSupportedLocales());
|
||||
selectedItemPropertyFor(cboLanguage).bindBidirectional(config().localizationProperty());
|
||||
});
|
||||
chooseLanguagePane.setItems(SupportedLocale.getSupportedLocales());
|
||||
chooseLanguagePane.valueProperty().bindBidirectional(config().localizationProperty());
|
||||
|
||||
FXUtils.setLimitWidth(cboLanguage, 300);
|
||||
languagePane.setRight(cboLanguage);
|
||||
|
||||
settingsPane.getContent().add(languagePane);
|
||||
settingsPane.getContent().add(chooseLanguagePane);
|
||||
}
|
||||
|
||||
{
|
||||
OptionToggleButton disableAutoGameOptionsPane = new OptionToggleButton();
|
||||
LineToggleButton disableAutoGameOptionsPane = new LineToggleButton();
|
||||
disableAutoGameOptionsPane.setTitle(i18n("settings.launcher.disable_auto_game_options"));
|
||||
disableAutoGameOptionsPane.selectedProperty().bindBidirectional(config().disableAutoGameOptionsProperty());
|
||||
|
||||
|
||||
@@ -20,13 +20,14 @@ package org.jackhuang.hmcl.ui.nbt;
|
||||
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.ListTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.Tag;
|
||||
import com.jfoenix.controls.JFXTreeView;
|
||||
import javafx.scene.control.TreeCell;
|
||||
import javafx.scene.control.TreeItem;
|
||||
import javafx.scene.control.TreeView;
|
||||
import javafx.scene.control.skin.TreeViewSkin;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.util.Callback;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.EnumMap;
|
||||
@@ -36,13 +37,23 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class NBTTreeView extends JFXTreeView<Tag> {
|
||||
public final class NBTTreeView extends TreeView<Tag> {
|
||||
|
||||
public NBTTreeView(NBTTreeView.Item tree) {
|
||||
this.setRoot(tree);
|
||||
if (tree != null) tree.setExpanded(true);
|
||||
this.setCellFactory(cellFactory());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected javafx.scene.control.Skin<?> createDefaultSkin() {
|
||||
return new TreeViewSkin<Tag>(this) {
|
||||
{
|
||||
FXUtils.smoothScrolling(getVirtualFlow());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static Callback<TreeView<Tag>, TreeCell<Tag>> cellFactory() {
|
||||
EnumMap<NBTTagType, Image> icons = new EnumMap<>(NBTTagType.class);
|
||||
|
||||
@@ -131,6 +142,9 @@ public final class NBTTreeView extends JFXTreeView<Tag> {
|
||||
item.getChildren().add(subTree);
|
||||
}
|
||||
}
|
||||
FXUtils.onChangeAndOperate(item.expandedProperty(), expanded -> {
|
||||
if (expanded && item.getChildren().size() == 1) item.getChildren().get(0).setExpanded(true);
|
||||
});
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.profile;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.SkinBase;
|
||||
@@ -30,12 +31,11 @@ import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
|
||||
public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
|
||||
private final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
|
||||
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
|
||||
|
||||
public ProfileListItemSkin(ProfileListItem skinnable) {
|
||||
super(skinnable);
|
||||
|
||||
|
||||
BorderPane root = new BorderPane();
|
||||
root.setPickOnBounds(false);
|
||||
RipplerContainer container = new RipplerContainer(root);
|
||||
@@ -46,7 +46,9 @@ public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
|
||||
|
||||
FXUtils.onClicked(getSkinnable(), () -> getSkinnable().setSelected(true));
|
||||
|
||||
Node left = FXUtils.wrap(SVG.FOLDER);
|
||||
Node left = SVG.FOLDER.createIcon(20);
|
||||
left.setMouseTransparent(true);
|
||||
BorderPane.setMargin(left, new Insets(0, 6, 0, 6));
|
||||
root.setLeft(left);
|
||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||
|
||||
|
||||
@@ -35,10 +35,7 @@ import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.FileItem;
|
||||
import org.jackhuang.hmcl.ui.construct.OptionToggleButton;
|
||||
import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
@@ -54,8 +51,8 @@ public final class ProfilePage extends BorderPane implements DecoratorPage {
|
||||
private final StringProperty location;
|
||||
private final Profile profile;
|
||||
private final JFXTextField txtProfileName;
|
||||
private final FileItem gameDir;
|
||||
private final OptionToggleButton toggleUseRelativePath;
|
||||
private final LineFileChooserButton gameDir;
|
||||
private final LineToggleButton toggleUseRelativePath;
|
||||
|
||||
/**
|
||||
* @param profile null if creating a new profile.
|
||||
@@ -79,7 +76,6 @@ public final class ProfilePage extends BorderPane implements DecoratorPage {
|
||||
rootPane.setStyle("-fx-padding: 20;");
|
||||
{
|
||||
ComponentList componentList = new ComponentList();
|
||||
componentList.setDepth(1);
|
||||
{
|
||||
BorderPane profileNamePane = new BorderPane();
|
||||
{
|
||||
@@ -108,12 +104,13 @@ public final class ProfilePage extends BorderPane implements DecoratorPage {
|
||||
});
|
||||
}
|
||||
|
||||
gameDir = new FileItem();
|
||||
gameDir.setName(i18n("profile.instance_directory"));
|
||||
gameDir.setTitle(i18n("profile.instance_directory.choose"));
|
||||
gameDir.pathProperty().bindBidirectional(location);
|
||||
gameDir = new LineFileChooserButton();
|
||||
gameDir.setTitle(i18n("profile.instance_directory"));
|
||||
gameDir.setFileChooserTitle(i18n("profile.instance_directory.choose"));
|
||||
gameDir.setType(LineFileChooserButton.Type.OPEN_DIRECTORY);
|
||||
gameDir.locationProperty().bindBidirectional(location);
|
||||
|
||||
toggleUseRelativePath = new OptionToggleButton();
|
||||
toggleUseRelativePath = new LineToggleButton();
|
||||
toggleUseRelativePath.setTitle(i18n("profile.use_relative_path"));
|
||||
|
||||
gameDir.convertToRelativePathProperty().bind(toggleUseRelativePath.selectedProperty());
|
||||
@@ -189,7 +186,7 @@ public final class ProfilePage extends BorderPane implements DecoratorPage {
|
||||
}
|
||||
} else {
|
||||
if (StringUtils.isBlank(getLocation())) {
|
||||
gameDir.onExplore();
|
||||
gameDir.fire();
|
||||
}
|
||||
Profile newProfile = new Profile(txtProfileName.getText(), Path.of(getLocation()));
|
||||
newProfile.setUseRelativePath(toggleUseRelativePath.isSelected());
|
||||
|
||||
+67
-177
@@ -22,8 +22,6 @@ import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.WeakChangeListener;
|
||||
import javafx.collections.FXCollections;
|
||||
@@ -34,13 +32,8 @@ import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
@@ -57,13 +50,7 @@ import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentSublist;
|
||||
import org.jackhuang.hmcl.ui.construct.HintPane;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.versions.Versions;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
|
||||
@@ -163,12 +150,12 @@ public class TerracottaControllerPage extends StackPane {
|
||||
body.getStyleClass().add("terracotta-hint");
|
||||
body.setLineSpacing(4);
|
||||
|
||||
LineButton download = LineButton.of();
|
||||
download.setLeftImage(FXUtils.newBuiltinImage("/assets/img/terracotta.png"));
|
||||
var download = createLargeTitleLineButton();
|
||||
download.setLeading(FXUtils.newBuiltinImage("/assets/img/terracotta.png"));
|
||||
download.setTitle(i18n(String.format("terracotta.status.uninitialized.%s.title", fork)));
|
||||
download.setSubtitle(i18n("terracotta.status.uninitialized.desc"));
|
||||
download.setRightIcon(SVG.ARROW_FORWARD);
|
||||
FXUtils.onClicked(download, () -> {
|
||||
download.setTrailingIcon(SVG.ARROW_FORWARD, ICON_SIZE);
|
||||
download.setOnAction(event -> {
|
||||
TerracottaState.Preparing s = TerracottaManager.download();
|
||||
if (s != null) {
|
||||
UI_STATE.set(s);
|
||||
@@ -207,12 +194,12 @@ public class TerracottaControllerPage extends StackPane {
|
||||
flow.getStyleClass().add("terracotta-hint");
|
||||
flow.setLineSpacing(4);
|
||||
|
||||
LineButton host = LineButton.of();
|
||||
host.setLeftIcon(SVG.HOST);
|
||||
var host = createLargeTitleLineButton();
|
||||
host.setLeading(SVG.HOST, ICON_SIZE);
|
||||
host.setTitle(i18n("terracotta.status.waiting.host.title"));
|
||||
host.setSubtitle(i18n("terracotta.status.waiting.host.desc"));
|
||||
host.setRightIcon(SVG.ARROW_FORWARD);
|
||||
FXUtils.onClicked(host, () -> {
|
||||
host.setTrailingIcon(SVG.ARROW_FORWARD, ICON_SIZE);
|
||||
host.setOnAction(event -> {
|
||||
if (LauncherHelper.countMangedProcesses() >= 1) {
|
||||
TerracottaState.HostScanning s1 = TerracottaManager.setScanning();
|
||||
if (s1 != null) {
|
||||
@@ -239,12 +226,12 @@ public class TerracottaControllerPage extends StackPane {
|
||||
}
|
||||
});
|
||||
|
||||
LineButton guest = LineButton.of();
|
||||
guest.setLeftIcon(SVG.ADD_CIRCLE);
|
||||
var guest = createLargeTitleLineButton();
|
||||
guest.setLeading(SVG.ADD_CIRCLE, ICON_SIZE);
|
||||
guest.setTitle(i18n("terracotta.status.waiting.guest.title"));
|
||||
guest.setSubtitle(i18n("terracotta.status.waiting.guest.desc"));
|
||||
guest.setRightIcon(SVG.ARROW_FORWARD);
|
||||
FXUtils.onClicked(guest, () -> {
|
||||
guest.setTrailingIcon(SVG.ARROW_FORWARD, ICON_SIZE);
|
||||
guest.setOnAction(event -> {
|
||||
Controllers.prompt(i18n("terracotta.status.waiting.guest.prompt.title"), (code, handler) -> {
|
||||
Task<TerracottaState.GuestConnecting> task = TerracottaManager.setGuesting(code);
|
||||
if (task != null) {
|
||||
@@ -263,11 +250,11 @@ public class TerracottaControllerPage extends StackPane {
|
||||
});
|
||||
|
||||
if (ThreadLocalRandom.current().nextDouble() < 0.02D) {
|
||||
LineButton feedback = LineButton.of();
|
||||
feedback.setLeftIcon(SVG.FEEDBACK);
|
||||
var feedback = createLargeTitleLineButton();
|
||||
feedback.setLeading(SVG.FEEDBACK, ICON_SIZE);
|
||||
feedback.setTitle(i18n("terracotta.feedback.title"));
|
||||
feedback.setSubtitle(i18n("terracotta.feedback.desc"));
|
||||
feedback.setRightIcon(SVG.OPEN_IN_NEW);
|
||||
feedback.setTrailingIcon(SVG.OPEN_IN_NEW, ICON_SIZE);
|
||||
FXUtils.onClicked(feedback, () -> FXUtils.openLink(TerracottaMetadata.FEEDBACK_LINK));
|
||||
|
||||
nodesProperty.setAll(flow, host, guest, feedback);
|
||||
@@ -282,11 +269,11 @@ public class TerracottaControllerPage extends StackPane {
|
||||
body.getStyleClass().add("terracotta-hint");
|
||||
body.setLineSpacing(4);
|
||||
|
||||
LineButton room = LineButton.of();
|
||||
room.setLeftIcon(SVG.ARROW_BACK);
|
||||
var room = createLargeTitleLineButton();
|
||||
room.setLeading(SVG.ARROW_BACK, ICON_SIZE);
|
||||
room.setTitle(i18n("terracotta.back"));
|
||||
room.setSubtitle(i18n("terracotta.status.scanning.back"));
|
||||
FXUtils.onClicked(room, () -> {
|
||||
room.setOnAction(event -> {
|
||||
TerracottaState.Waiting s = TerracottaManager.setWaiting();
|
||||
if (s != null) {
|
||||
UI_STATE.set(s);
|
||||
@@ -298,11 +285,11 @@ public class TerracottaControllerPage extends StackPane {
|
||||
statusProperty.set(i18n("terracotta.status.host_starting"));
|
||||
progressProperty.set(-1);
|
||||
|
||||
LineButton room = LineButton.of();
|
||||
room.setLeftIcon(SVG.ARROW_BACK);
|
||||
var room = createLargeTitleLineButton();
|
||||
room.setLeading(SVG.ARROW_BACK, ICON_SIZE);
|
||||
room.setTitle(i18n("terracotta.back"));
|
||||
room.setSubtitle(i18n("terracotta.status.host_starting.back"));
|
||||
FXUtils.onClicked(room, () -> {
|
||||
room.setOnAction(event -> {
|
||||
TerracottaState.Waiting s = TerracottaManager.setWaiting();
|
||||
if (s != null) {
|
||||
UI_STATE.set(s);
|
||||
@@ -342,17 +329,17 @@ public class TerracottaControllerPage extends StackPane {
|
||||
code.setCursor(Cursor.HAND);
|
||||
FXUtils.onClicked(code, () -> copyCode(cs));
|
||||
|
||||
LineButton copy = LineButton.of();
|
||||
copy.setLeftIcon(SVG.CONTENT_COPY);
|
||||
var copy = createLargeTitleLineButton();
|
||||
copy.setLeading(SVG.CONTENT_COPY, ICON_SIZE);
|
||||
copy.setTitle(i18n("terracotta.status.host_ok.code.copy"));
|
||||
copy.setSubtitle(i18n("terracotta.status.host_ok.code.desc"));
|
||||
FXUtils.onClicked(copy, () -> copyCode(cs));
|
||||
|
||||
LineButton back = LineButton.of();
|
||||
back.setLeftIcon(SVG.ARROW_BACK);
|
||||
var back = createLargeTitleLineButton();
|
||||
back.setLeading(SVG.ARROW_BACK, ICON_SIZE);
|
||||
back.setTitle(i18n("terracotta.back"));
|
||||
back.setSubtitle(i18n("terracotta.status.host_ok.back"));
|
||||
FXUtils.onClicked(back, () -> {
|
||||
back.setOnAction(event -> {
|
||||
TerracottaState.Waiting s = TerracottaManager.setWaiting();
|
||||
if (s != null) {
|
||||
UI_STATE.set(s);
|
||||
@@ -369,11 +356,11 @@ public class TerracottaControllerPage extends StackPane {
|
||||
statusProperty.set(i18n("terracotta.status.guest_starting"));
|
||||
progressProperty.set(-1);
|
||||
|
||||
LineButton room = LineButton.of();
|
||||
room.setLeftIcon(SVG.ARROW_BACK);
|
||||
var room = createLargeTitleLineButton();
|
||||
room.setLeading(SVG.ARROW_BACK, ICON_SIZE);
|
||||
room.setTitle(i18n("terracotta.back"));
|
||||
room.setSubtitle(i18n("terracotta.status.guest_starting.back"));
|
||||
FXUtils.onClicked(room, () -> {
|
||||
room.setOnAction(event -> {
|
||||
TerracottaState.Waiting s = TerracottaManager.setWaiting();
|
||||
if (s != null) {
|
||||
UI_STATE.set(s);
|
||||
@@ -384,12 +371,12 @@ public class TerracottaControllerPage extends StackPane {
|
||||
if (state instanceof TerracottaState.GuestStarting) {
|
||||
TerracottaState.GuestStarting.Difficulty difficulty = ((TerracottaState.GuestStarting) state).getDifficulty();
|
||||
if (difficulty != null && difficulty != TerracottaState.GuestStarting.Difficulty.UNKNOWN) {
|
||||
LineButton info = LineButton.of();
|
||||
info.setLeftIcon(switch (difficulty) {
|
||||
var info = createLargeTitleLineButton();
|
||||
info.setLeading(switch (difficulty) {
|
||||
case UNKNOWN -> throw new AssertionError();
|
||||
case EASIEST, SIMPLE -> SVG.INFO;
|
||||
case MEDIUM, TOUGH -> SVG.WARNING;
|
||||
});
|
||||
}, ICON_SIZE);
|
||||
|
||||
String difficultyID = difficulty.name().toLowerCase(Locale.ROOT);
|
||||
info.setTitle(i18n(String.format("terracotta.difficulty.%s", difficultyID)));
|
||||
@@ -412,15 +399,15 @@ public class TerracottaControllerPage extends StackPane {
|
||||
statusProperty.set(i18n("terracotta.status.guest_ok"));
|
||||
progressProperty.set(1);
|
||||
|
||||
LineButton tutorial = LineButton.of();
|
||||
var tutorial = createLargeTitleLineButton();
|
||||
tutorial.setTitle(i18n("terracotta.status.guest_ok.title"));
|
||||
tutorial.setSubtitle(i18n("terracotta.status.guest_ok.desc", guestOK.getUrl()));
|
||||
|
||||
LineButton back = LineButton.of();
|
||||
back.setLeftIcon(SVG.ARROW_BACK);
|
||||
var back = createLargeTitleLineButton();
|
||||
back.setLeading(SVG.ARROW_BACK, ICON_SIZE);
|
||||
back.setTitle(i18n("terracotta.back"));
|
||||
back.setSubtitle(i18n("terracotta.status.guest_ok.back"));
|
||||
FXUtils.onClicked(back, () -> {
|
||||
back.setOnAction(event -> {
|
||||
TerracottaState.Waiting s = TerracottaManager.setWaiting();
|
||||
if (s != null) {
|
||||
UI_STATE.set(s);
|
||||
@@ -438,11 +425,11 @@ public class TerracottaControllerPage extends StackPane {
|
||||
progressProperty.set(1);
|
||||
nodesProperty.setAll();
|
||||
|
||||
LineButton back = LineButton.of();
|
||||
back.setLeftIcon(SVG.ARROW_BACK);
|
||||
var back = createLargeTitleLineButton();
|
||||
back.setLeading(SVG.ARROW_BACK, ICON_SIZE);
|
||||
back.setTitle(i18n("terracotta.back"));
|
||||
back.setSubtitle(i18n("terracotta.status.exception.back"));
|
||||
FXUtils.onClicked(back, () -> {
|
||||
back.setOnAction(event -> {
|
||||
TerracottaState.Waiting s = TerracottaManager.setWaiting();
|
||||
if (s != null) {
|
||||
UI_STATE.set(s);
|
||||
@@ -450,16 +437,16 @@ public class TerracottaControllerPage extends StackPane {
|
||||
});
|
||||
|
||||
SpinnerPane exportLog = new SpinnerPane();
|
||||
LineButton exportLogInner = LineButton.of();
|
||||
exportLogInner.setLeftIcon(SVG.OUTPUT);
|
||||
var exportLogInner = createLargeTitleLineButton();
|
||||
exportLogInner.setLeading(SVG.OUTPUT, ICON_SIZE);
|
||||
exportLogInner.setTitle(i18n("terracotta.export_log"));
|
||||
exportLogInner.setSubtitle(i18n("terracotta.export_log.desc"));
|
||||
exportLog.setContent(exportLogInner);
|
||||
exportLog.getProperties().put("ComponentList.noPadding", true);
|
||||
ComponentList.setNoPadding(exportLog);
|
||||
// FIXME: SpinnerPane loses its content width in loading state.
|
||||
exportLog.minHeightProperty().bind(back.heightProperty());
|
||||
|
||||
FXUtils.onClicked(exportLogInner, () -> {
|
||||
exportLogInner.setOnAction(event -> {
|
||||
exportLog.setLoading(true);
|
||||
|
||||
TerracottaManager.exportLogs().thenAcceptAsync(Schedulers.io(), data -> {
|
||||
@@ -493,11 +480,11 @@ public class TerracottaControllerPage extends StackPane {
|
||||
progressProperty.set(1);
|
||||
|
||||
if (fatal.isRecoverable()) {
|
||||
LineButton retry = LineButton.of();
|
||||
retry.setLeftIcon(SVG.RESTORE);
|
||||
var retry = createLargeTitleLineButton();
|
||||
retry.setLeading(SVG.RESTORE, ICON_SIZE);
|
||||
retry.setTitle(i18n("terracotta.status.fatal.retry"));
|
||||
retry.setSubtitle(message);
|
||||
FXUtils.onClicked(retry, () -> {
|
||||
retry.setOnAction(event -> {
|
||||
TerracottaState s = TerracottaManager.recover();
|
||||
if (s != null) {
|
||||
UI_STATE.set(s);
|
||||
@@ -534,7 +521,8 @@ public class TerracottaControllerPage extends StackPane {
|
||||
children.add(statusPane);
|
||||
children.addAll(nodesProperty);
|
||||
}
|
||||
|
||||
// Prevent the shadow of components from being clipped
|
||||
StackPane.setMargin(components, new Insets(0, 0, 5, 0));
|
||||
transition.setContent(components, ContainerAnimations.SLIDE_UP_FADE_IN);
|
||||
};
|
||||
listener.changed(UI_STATE, null, UI_STATE.get());
|
||||
@@ -559,36 +547,29 @@ public class TerracottaControllerPage extends StackPane {
|
||||
getChildren().setAll(scrollPane);
|
||||
}
|
||||
|
||||
private ComponentList getThirdPartyDownloadNodes() {
|
||||
private ComponentSublist getThirdPartyDownloadNodes() {
|
||||
ComponentSublist locals = new ComponentSublist();
|
||||
locals.setComponentPadding(false);
|
||||
|
||||
LineButton header = LineButton.of(false);
|
||||
header.setLeftImage(FXUtils.newBuiltinImage("/assets/img/terracotta.png"));
|
||||
var header = new LinePane();
|
||||
header.getStyleClass().add("no-padding");
|
||||
header.setLargeTitle(true);
|
||||
header.setMinHeight(LinePane.USE_COMPUTED_SIZE);
|
||||
header.setMouseTransparent(true);
|
||||
header.setLeading(FXUtils.newBuiltinImage("/assets/img/terracotta.png"));
|
||||
header.setTitle(i18n("terracotta.from_local.title"));
|
||||
header.setSubtitle(i18n("terracotta.from_local.desc"));
|
||||
locals.setHeaderLeft(header);
|
||||
|
||||
for (TerracottaMetadata.Link link : TerracottaMetadata.PACKAGE_LINKS) {
|
||||
HBox node = new HBox();
|
||||
node.setAlignment(Pos.CENTER_LEFT);
|
||||
node.setPadding(new Insets(10, 16, 10, 16));
|
||||
|
||||
Label description = new Label(link.description().getText(I18n.getLocale().getCandidateLocales()));
|
||||
HBox placeholder = new HBox();
|
||||
HBox.setHgrow(placeholder, Priority.ALWAYS);
|
||||
Node icon = SVG.OPEN_IN_NEW.createIcon(16);
|
||||
node.getChildren().setAll(description, placeholder, icon);
|
||||
|
||||
String url = link.link();
|
||||
RipplerContainer container = new RipplerContainer(node);
|
||||
container.setOnMouseClicked(ev -> Controllers.dialog(
|
||||
LineButton item = new LineButton();
|
||||
item.setTrailingIcon(SVG.OPEN_IN_NEW);
|
||||
item.setTitle(link.description().getText(I18n.getLocale().getCandidateLocales()));
|
||||
item.setOnAction(event -> Controllers.dialog(
|
||||
i18n("terracotta.from_local.guide", TerracottaMetadata.PACKAGE_NAME),
|
||||
i18n("message.info"), MessageDialogPane.MessageType.INFO,
|
||||
() -> FXUtils.openLink(url)
|
||||
() -> FXUtils.openLink(link.link())
|
||||
));
|
||||
container.getProperties().put("ComponentList.noPadding", true);
|
||||
locals.getContent().add(container);
|
||||
locals.getContent().add(item);
|
||||
}
|
||||
return locals;
|
||||
}
|
||||
@@ -597,103 +578,12 @@ public class TerracottaControllerPage extends StackPane {
|
||||
FXUtils.copyText(code, i18n("terracotta.status.host_ok.code.copy.toast"));
|
||||
}
|
||||
|
||||
private static final class LineButton extends RipplerContainer {
|
||||
private final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
private static final double ICON_SIZE = 28;
|
||||
|
||||
private final ObjectProperty<Node> left = new SimpleObjectProperty<>(this, "left");
|
||||
private final ObjectProperty<Node> right = new SimpleObjectProperty<>(this, "right");
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title", "");
|
||||
private final StringProperty subTitle = new SimpleStringProperty(this, "subTitle", "");
|
||||
|
||||
public static LineButton of() {
|
||||
return of(true);
|
||||
}
|
||||
|
||||
public static LineButton of(boolean padding) {
|
||||
HBox container = new HBox();
|
||||
if (padding) {
|
||||
container.setPadding(new Insets(10, 16, 10, 16));
|
||||
}
|
||||
container.setAlignment(Pos.CENTER_LEFT);
|
||||
container.setCursor(Cursor.HAND);
|
||||
container.setSpacing(16);
|
||||
|
||||
LineButton button = new LineButton(container);
|
||||
VBox spacing = new VBox();
|
||||
HBox.setHgrow(spacing, Priority.ALWAYS);
|
||||
button.holder.add(FXUtils.observeWeak(() -> {
|
||||
List<Node> nodes = new ArrayList<>(4);
|
||||
Node left = button.left.get();
|
||||
if (left != null) {
|
||||
nodes.add(left);
|
||||
}
|
||||
|
||||
{
|
||||
// FIXME: It's sucked to have the following TwoLineListItem-liked logic whose subtitle is a TextFlow.
|
||||
VBox middle = new VBox();
|
||||
middle.getStyleClass().add("two-line-list-item");
|
||||
middle.setMouseTransparent(true);
|
||||
{
|
||||
HBox firstLine = new HBox();
|
||||
firstLine.getStyleClass().add("first-line");
|
||||
{
|
||||
Label lblTitle = new Label(button.title.get());
|
||||
lblTitle.getStyleClass().add("title");
|
||||
firstLine.getChildren().setAll(lblTitle);
|
||||
}
|
||||
|
||||
HBox secondLine = new HBox();
|
||||
secondLine.getStyleClass().add("second-line");
|
||||
{
|
||||
Text text = new Text(button.subTitle.get());
|
||||
|
||||
TextFlow lblSubtitle = new TextFlow(text);
|
||||
lblSubtitle.getStyleClass().add("subtitle");
|
||||
secondLine.getChildren().setAll(lblSubtitle);
|
||||
}
|
||||
|
||||
middle.getChildren().setAll(firstLine, secondLine);
|
||||
}
|
||||
nodes.add(middle);
|
||||
}
|
||||
|
||||
nodes.add(spacing);
|
||||
|
||||
Node right = button.right.get();
|
||||
if (right != null) {
|
||||
nodes.add(right);
|
||||
}
|
||||
|
||||
container.getChildren().setAll(nodes);
|
||||
}, button.title, button.subTitle, button.left, button.right));
|
||||
button.getProperties().put("ComponentList.noPadding", true);
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
private LineButton(Node container) {
|
||||
super(container);
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title.set(title);
|
||||
}
|
||||
|
||||
public void setSubtitle(String subtitle) {
|
||||
this.subTitle.set(subtitle);
|
||||
}
|
||||
|
||||
public void setLeftImage(Image left) {
|
||||
this.left.set(new ImageView(left));
|
||||
}
|
||||
|
||||
public void setLeftIcon(SVG left) {
|
||||
this.left.set(left.createIcon(28));
|
||||
}
|
||||
|
||||
public void setRightIcon(SVG right) {
|
||||
this.right.set(right.createIcon(28));
|
||||
}
|
||||
private static LineButton createLargeTitleLineButton() {
|
||||
var lineButton = new LineButton();
|
||||
lineButton.setLargeTitle(true);
|
||||
return lineButton;
|
||||
}
|
||||
|
||||
private static final class PlayerProfileUI extends VBox {
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.JFXComboBox;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.*;
|
||||
@@ -26,7 +24,6 @@ import java.nio.file.FileSystems;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class AdvancedVersionSettingPage extends StackPane implements DecoratorPage {
|
||||
@@ -44,17 +41,17 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
private final JFXTextField txtWrapper;
|
||||
private final JFXTextField txtPreLaunchCommand;
|
||||
private final JFXTextField txtPostExitCommand;
|
||||
private final OptionToggleButton noJVMArgsPane;
|
||||
private final OptionToggleButton noOptimizingJVMArgsPane;
|
||||
private final OptionToggleButton noGameCheckPane;
|
||||
private final OptionToggleButton noJVMCheckPane;
|
||||
private final OptionToggleButton noNativesPatchPane;
|
||||
private final OptionToggleButton useNativeGLFWPane;
|
||||
private final OptionToggleButton useNativeOpenALPane;
|
||||
private final LineToggleButton noJVMArgsPane;
|
||||
private final LineToggleButton noOptimizingJVMArgsPane;
|
||||
private final LineToggleButton noGameCheckPane;
|
||||
private final LineToggleButton noJVMCheckPane;
|
||||
private final LineToggleButton noNativesPatchPane;
|
||||
private final LineToggleButton useNativeGLFWPane;
|
||||
private final LineToggleButton useNativeOpenALPane;
|
||||
private final ComponentSublist nativesDirSublist;
|
||||
private final MultiFileItem<NativesDirectoryType> nativesDirItem;
|
||||
private final MultiFileItem.FileOption<NativesDirectoryType> nativesDirCustomOption;
|
||||
private final JFXComboBox<Renderer> cboRenderer;
|
||||
private final LineSelectButton<Renderer> rendererPane;
|
||||
|
||||
public AdvancedVersionSettingPage(Profile profile, @Nullable String versionId, VersionSetting versionSetting) {
|
||||
this.profile = profile;
|
||||
@@ -170,40 +167,31 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
nativesDirHint.setText(i18n("settings.advanced.natives_directory.hint"));
|
||||
nativesDirItem.getChildren().add(nativesDirHint);
|
||||
|
||||
BorderPane rendererPane = new BorderPane();
|
||||
{
|
||||
Label label = new Label(i18n("settings.advanced.renderer"));
|
||||
rendererPane.setLeft(label);
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
rendererPane = new LineSelectButton<>();
|
||||
rendererPane.setTitle(i18n("settings.advanced.renderer"));
|
||||
rendererPane.setConverter(e -> i18n("settings.advanced.renderer." + e.name().toLowerCase(Locale.ROOT)));
|
||||
rendererPane.setItems(Renderer.values());
|
||||
|
||||
cboRenderer = new JFXComboBox<>();
|
||||
cboRenderer.getItems().setAll(Renderer.values());
|
||||
cboRenderer.setConverter(stringConverter(e -> i18n("settings.advanced.renderer." + e.name().toLowerCase(Locale.ROOT))));
|
||||
rendererPane.setRight(cboRenderer);
|
||||
BorderPane.setAlignment(cboRenderer, Pos.CENTER_RIGHT);
|
||||
FXUtils.setLimitWidth(cboRenderer, 300);
|
||||
}
|
||||
|
||||
noJVMArgsPane = new OptionToggleButton();
|
||||
noJVMArgsPane = new LineToggleButton();
|
||||
noJVMArgsPane.setTitle(i18n("settings.advanced.no_jvm_args"));
|
||||
|
||||
noOptimizingJVMArgsPane = new OptionToggleButton();
|
||||
noOptimizingJVMArgsPane = new LineToggleButton();
|
||||
noOptimizingJVMArgsPane.setTitle(i18n("settings.advanced.no_optimizing_jvm_args"));
|
||||
noOptimizingJVMArgsPane.disableProperty().bind(noJVMArgsPane.selectedProperty());
|
||||
|
||||
noGameCheckPane = new OptionToggleButton();
|
||||
noGameCheckPane = new LineToggleButton();
|
||||
noGameCheckPane.setTitle(i18n("settings.advanced.dont_check_game_completeness"));
|
||||
|
||||
noJVMCheckPane = new OptionToggleButton();
|
||||
noJVMCheckPane = new LineToggleButton();
|
||||
noJVMCheckPane.setTitle(i18n("settings.advanced.dont_check_jvm_validity"));
|
||||
|
||||
noNativesPatchPane = new OptionToggleButton();
|
||||
noNativesPatchPane = new LineToggleButton();
|
||||
noNativesPatchPane.setTitle(i18n("settings.advanced.dont_patch_natives"));
|
||||
|
||||
useNativeGLFWPane = new OptionToggleButton();
|
||||
useNativeGLFWPane = new LineToggleButton();
|
||||
useNativeGLFWPane.setTitle(i18n("settings.advanced.use_native_glfw"));
|
||||
|
||||
useNativeOpenALPane = new OptionToggleButton();
|
||||
useNativeOpenALPane = new LineToggleButton();
|
||||
useNativeOpenALPane.setTitle(i18n("settings.advanced.use_native_openal"));
|
||||
|
||||
workaroundPane.getContent().setAll(
|
||||
@@ -239,7 +227,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
FXUtils.bindString(txtWrapper, versionSetting.wrapperProperty());
|
||||
FXUtils.bindString(txtPreLaunchCommand, versionSetting.preLaunchCommandProperty());
|
||||
FXUtils.bindString(txtPostExitCommand, versionSetting.postExitCommandProperty());
|
||||
FXUtils.bindEnum(cboRenderer, versionSetting.rendererProperty());
|
||||
rendererPane.valueProperty().bindBidirectional(versionSetting.rendererProperty());
|
||||
noGameCheckPane.selectedProperty().bindBidirectional(versionSetting.notCheckGameProperty());
|
||||
noJVMCheckPane.selectedProperty().bindBidirectional(versionSetting.notCheckJVMProperty());
|
||||
noJVMArgsPane.selectedProperty().bindBidirectional(versionSetting.noJVMArgsProperty());
|
||||
@@ -281,7 +269,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
FXUtils.unbind(txtWrapper, versionSetting.wrapperProperty());
|
||||
FXUtils.unbind(txtPreLaunchCommand, versionSetting.preLaunchCommandProperty());
|
||||
FXUtils.unbind(txtPostExitCommand, versionSetting.postExitCommandProperty());
|
||||
FXUtils.unbindEnum(cboRenderer, versionSetting.rendererProperty());
|
||||
rendererPane.valueProperty().unbindBidirectional(versionSetting.rendererProperty());
|
||||
noGameCheckPane.selectedProperty().unbindBidirectional(versionSetting.notCheckGameProperty());
|
||||
noJVMCheckPane.selectedProperty().unbindBidirectional(versionSetting.notCheckJVMProperty());
|
||||
noJVMArgsPane.selectedProperty().unbindBidirectional(versionSetting.noJVMArgsProperty());
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.stage.FileChooser;
|
||||
@@ -42,20 +43,29 @@ import java.util.regex.Pattern;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.DatapackInfoObject> {
|
||||
public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.DatapackInfoObject> implements WorldManagePage.WorldRefreshable {
|
||||
private final Path worldDir;
|
||||
private final Datapack datapack;
|
||||
final BooleanProperty readOnly;
|
||||
|
||||
public DatapackListPage(WorldManagePage worldManagePage) {
|
||||
this.worldDir = worldManagePage.getWorld().getFile();
|
||||
datapack = new Datapack(worldDir.resolve("datapacks"));
|
||||
setItems(MappedObservableList.create(datapack.getPacks(), DatapackListPageSkin.DatapackInfoObject::new));
|
||||
readOnly = worldManagePage.readOnlyProperty();
|
||||
FXUtils.applyDragListener(this, it -> Objects.equals("zip", FileUtils.getExtension(it)),
|
||||
mods -> mods.forEach(this::installSingleDatapack), this::refresh);
|
||||
this::installMultiDatapack, this::refresh);
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
private void installMultiDatapack(List<Path> datapackPath) {
|
||||
datapackPath.forEach(this::installSingleDatapack);
|
||||
if (readOnly.get()) {
|
||||
Controllers.showToast(i18n("datapack.reload.toast"));
|
||||
}
|
||||
}
|
||||
|
||||
private void installSingleDatapack(Path datapack) {
|
||||
try {
|
||||
this.datapack.installPack(datapack);
|
||||
@@ -78,12 +88,12 @@ public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.Da
|
||||
|
||||
public void add() {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("datapack.choose_datapack"));
|
||||
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("datapack.extension"), "*.zip"));
|
||||
chooser.setTitle(i18n("datapack.add.title"));
|
||||
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.datapack"), "*.zip"));
|
||||
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
|
||||
|
||||
if (res != null) {
|
||||
res.forEach(this::installSingleDatapack);
|
||||
installMultiDatapack(res);
|
||||
}
|
||||
|
||||
datapack.loadFromDir();
|
||||
|
||||
@@ -114,16 +114,23 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
createToolbarButton2(i18n("search"), SVG.SEARCH, () -> isSearching.set(true))
|
||||
);
|
||||
|
||||
JFXButton removeButton = createToolbarButton2(i18n("button.remove"), SVG.DELETE, () -> {
|
||||
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
|
||||
skinnable.removeSelected(listView.getSelectionModel().getSelectedItems());
|
||||
}, null);
|
||||
});
|
||||
JFXButton enableButton = createToolbarButton2(i18n("mods.enable"), SVG.CHECK, () ->
|
||||
skinnable.enableSelected(listView.getSelectionModel().getSelectedItems()));
|
||||
JFXButton disableButton = createToolbarButton2(i18n("mods.disable"), SVG.CLOSE, () ->
|
||||
skinnable.disableSelected(listView.getSelectionModel().getSelectedItems()));
|
||||
removeButton.disableProperty().bind(getSkinnable().readOnly);
|
||||
enableButton.disableProperty().bind(getSkinnable().readOnly);
|
||||
disableButton.disableProperty().bind(getSkinnable().readOnly);
|
||||
|
||||
selectingToolbar.getChildren().addAll(
|
||||
createToolbarButton2(i18n("button.remove"), SVG.DELETE, () -> {
|
||||
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
|
||||
skinnable.removeSelected(listView.getSelectionModel().getSelectedItems());
|
||||
}, null);
|
||||
}),
|
||||
createToolbarButton2(i18n("mods.enable"), SVG.CHECK, () ->
|
||||
skinnable.enableSelected(listView.getSelectionModel().getSelectedItems())),
|
||||
createToolbarButton2(i18n("mods.disable"), SVG.CLOSE, () ->
|
||||
skinnable.disableSelected(listView.getSelectionModel().getSelectedItems())),
|
||||
removeButton,
|
||||
enableButton,
|
||||
disableButton,
|
||||
createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () ->
|
||||
listView.getSelectionModel().selectRange(0, listView.getItems().size())),//reason for not using selectAll() is that selectAll() first clears all selected then selects all, causing the toolbar to flicker
|
||||
createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () ->
|
||||
@@ -176,10 +183,9 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
{
|
||||
SpinnerPane center = new SpinnerPane();
|
||||
ComponentList.setVgrow(center, Priority.ALWAYS);
|
||||
center.getStyleClass().add("large-spinner-pane");
|
||||
center.loadingProperty().bind(skinnable.loadingProperty());
|
||||
|
||||
listView.setCellFactory(x -> new DatapackInfoListCell(listView));
|
||||
listView.setCellFactory(x -> new DatapackInfoListCell(listView, getSkinnable().readOnly));
|
||||
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
this.listView.setItems(filteredList);
|
||||
|
||||
@@ -302,7 +308,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
final TwoLineListItem content = new TwoLineListItem();
|
||||
BooleanProperty booleanProperty;
|
||||
|
||||
DatapackInfoListCell(JFXListView<DatapackInfoObject> listView) {
|
||||
DatapackInfoListCell(JFXListView<DatapackInfoObject> listView, BooleanProperty isReadOnlyProperty) {
|
||||
super(listView);
|
||||
|
||||
HBox container = new HBox(8);
|
||||
@@ -312,6 +318,8 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
content.setMouseTransparent(true);
|
||||
setSelectable();
|
||||
|
||||
checkBox.disableProperty().bind(isReadOnlyProperty);
|
||||
|
||||
imageView.setFitWidth(32);
|
||||
imageView.setFitHeight(32);
|
||||
imageView.setPreserveRatio(true);
|
||||
|
||||
@@ -48,6 +48,7 @@ import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.NoneMultipleSelectionModel;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
@@ -244,6 +245,8 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
protected ModDownloadListPageSkin(DownloadListPage control) {
|
||||
super(control);
|
||||
|
||||
listView.getStyleClass().add("no-horizontal-scrollbar");
|
||||
|
||||
BorderPane pane = new BorderPane();
|
||||
|
||||
GridPane searchPane = new GridPane();
|
||||
@@ -521,7 +524,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
|
||||
spinnerPane.setContent(listView);
|
||||
Bindings.bindContent(listView.getItems(), getSkinnable().items);
|
||||
listView.setSelectionModel(null);
|
||||
listView.setSelectionModel(new NoneMultipleSelectionModel<>());
|
||||
// ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here
|
||||
ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
|
||||
|
||||
|
||||
@@ -27,9 +27,18 @@ import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
@@ -46,7 +55,11 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.SimpleMultimap;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
@@ -54,7 +67,14 @@ import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -300,13 +320,13 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
|
||||
for (String gameVersion : control.versions.keys().stream()
|
||||
.sorted(Collections.reverseOrder(GameVersionNumber::compare))
|
||||
.collect(Collectors.toList())) {
|
||||
.toList()) {
|
||||
List<RemoteMod.Version> versions = control.versions.get(gameVersion);
|
||||
if (versions == null || versions.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ComponentList sublist = new ComponentList(() -> {
|
||||
var sublist = new ComponentSublist(() -> {
|
||||
ArrayList<ModItem> items = new ArrayList<>(versions.size());
|
||||
for (RemoteMod.Version v : versions) {
|
||||
items.add(new ModItem(control.addon, v, control));
|
||||
@@ -465,13 +485,14 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
box.getChildren().setAll(modItem);
|
||||
SpinnerPane spinnerPane = new SpinnerPane();
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
ComponentList dependenciesList = new ComponentList(Lang::immutableListOf);
|
||||
ComponentList dependenciesList = new ComponentList();
|
||||
loadDependencies(version, selfPage, spinnerPane, dependenciesList);
|
||||
spinnerPane.setOnFailedAction(e -> loadDependencies(version, selfPage, spinnerPane, dependenciesList));
|
||||
|
||||
scrollPane.setContent(dependenciesList);
|
||||
scrollPane.setFitToWidth(true);
|
||||
scrollPane.setFitToHeight(true);
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
spinnerPane.setContent(scrollPane);
|
||||
box.getChildren().add(spinnerPane);
|
||||
VBox.setVgrow(spinnerPane, Priority.SOMETIMES);
|
||||
@@ -517,8 +538,10 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
|
||||
private void loadDependencies(RemoteMod.Version version, DownloadPage selfPage, SpinnerPane spinnerPane, ComponentList dependenciesList) {
|
||||
spinnerPane.setLoading(true);
|
||||
Task.supplyAsync(() -> {
|
||||
Task.composeAsync(() -> {
|
||||
// TODO: Massive tasks may cause OOM.
|
||||
EnumMap<RemoteMod.DependencyType, List<Node>> dependencies = new EnumMap<>(RemoteMod.DependencyType.class);
|
||||
List<Task<?>> queue = new ArrayList<>(version.getDependencies().size());
|
||||
for (RemoteMod.Dependency dependency : version.getDependencies()) {
|
||||
if (dependency.getType() == RemoteMod.DependencyType.INCOMPATIBLE || dependency.getType() == RemoteMod.DependencyType.BROKEN) {
|
||||
continue;
|
||||
@@ -531,11 +554,22 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
list.add(title);
|
||||
dependencies.put(dependency.getType(), list);
|
||||
}
|
||||
DependencyModItem dependencyModItem = new DependencyModItem(selfPage.page, dependency.load(), selfPage.version, selfPage.callback);
|
||||
dependencies.get(dependency.getType()).add(dependencyModItem);
|
||||
|
||||
queue.add(Task.supplyAsync(Schedulers.io(), dependency::load)
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.thenAcceptAsync(Schedulers.javafx(), dep -> {
|
||||
if (dep == RemoteMod.BROKEN) {
|
||||
return;
|
||||
}
|
||||
DependencyModItem dependencyModItem = new DependencyModItem(selfPage.page, dep, selfPage.version, selfPage.callback);
|
||||
dependencies.get(dependency.getType()).add(dependencyModItem);
|
||||
})
|
||||
.setSignificance(Task.TaskSignificance.MINOR));
|
||||
}
|
||||
|
||||
return dependencies.values().stream().flatMap(Collection::stream).collect(Collectors.toList());
|
||||
return Task.allOf(queue).thenSupplyAsync(() ->
|
||||
dependencies.values().stream().flatMap(Collection::stream).collect(Collectors.toList())
|
||||
);
|
||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
spinnerPane.setLoading(false);
|
||||
if (exception == null) {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.image.ImageView;
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
@@ -26,7 +27,6 @@ import org.jackhuang.hmcl.setting.VersionIconType;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -39,14 +39,20 @@ public class GameAdvancedListItem extends AdvancedListItem {
|
||||
@SuppressWarnings("unused")
|
||||
private Consumer<Event> onVersionIconChangedListener;
|
||||
|
||||
@SuppressWarnings("SuspiciousNameCombination")
|
||||
public GameAdvancedListItem() {
|
||||
Pair<Node, ImageView> view = createImageView(null);
|
||||
setLeftGraphic(view.getKey());
|
||||
imageView = view.getValue();
|
||||
this.imageView = new ImageView();
|
||||
FXUtils.limitSize(imageView, LEFT_GRAPHIC_SIZE, LEFT_GRAPHIC_SIZE);
|
||||
imageView.setPreserveRatio(true);
|
||||
imageView.setImage(null);
|
||||
|
||||
Node imageViewWrapper = FXUtils.limitingSize(imageView, LEFT_GRAPHIC_SIZE, LEFT_GRAPHIC_SIZE);
|
||||
imageView.setMouseTransparent(true);
|
||||
AdvancedListItem.setAlignment(imageViewWrapper, Pos.CENTER);
|
||||
setLeftGraphic(imageViewWrapper);
|
||||
|
||||
holder.add(FXUtils.onWeakChangeAndOperate(Profiles.selectedVersionProperty(), this::loadVersion));
|
||||
|
||||
setActionButtonVisible(false);
|
||||
}
|
||||
|
||||
private void loadVersion(String version) {
|
||||
|
||||
@@ -115,7 +115,7 @@ public final class GameListCell extends ListCell<GameListItem> {
|
||||
item.update();
|
||||
});
|
||||
btnUpgrade.getStyleClass().add("toggle-icon4");
|
||||
btnUpgrade.setGraphic(FXUtils.limitingSize(SVG.UPDATE.createIcon(24), 24, 24));
|
||||
btnUpgrade.setGraphic(SVG.UPDATE.createIcon());
|
||||
FXUtils.installFastTooltip(btnUpgrade, i18n("version.update"));
|
||||
right.getChildren().add(btnUpgrade);
|
||||
|
||||
@@ -127,7 +127,7 @@ public final class GameListCell extends ListCell<GameListItem> {
|
||||
});
|
||||
btnLaunch.getStyleClass().add("toggle-icon4");
|
||||
BorderPane.setAlignment(btnLaunch, Pos.CENTER);
|
||||
btnLaunch.setGraphic(FXUtils.limitingSize(SVG.ROCKET_LAUNCH.createIcon(24), 24, 24));
|
||||
btnLaunch.setGraphic(SVG.ROCKET_LAUNCH.createIcon());
|
||||
FXUtils.installFastTooltip(btnLaunch, i18n("version.launch.test"));
|
||||
right.getChildren().add(btnLaunch);
|
||||
|
||||
@@ -143,7 +143,7 @@ public final class GameListCell extends ListCell<GameListItem> {
|
||||
});
|
||||
btnManage.getStyleClass().add("toggle-icon4");
|
||||
BorderPane.setAlignment(btnManage, Pos.CENTER);
|
||||
btnManage.setGraphic(FXUtils.limitingSize(SVG.MORE_VERT.createIcon(24), 24, 24));
|
||||
btnManage.setGraphic(SVG.MORE_VERT.createIcon());
|
||||
FXUtils.installFastTooltip(btnManage, i18n("settings.game.management"));
|
||||
right.getChildren().add(btnManage);
|
||||
}
|
||||
|
||||
@@ -69,8 +69,7 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
AdvancedListItem addProfileItem = new AdvancedListItem();
|
||||
addProfileItem.getStyleClass().add("navigation-drawer-item");
|
||||
addProfileItem.setTitle(i18n("profile.new"));
|
||||
addProfileItem.setActionButtonVisible(false);
|
||||
addProfileItem.setLeftGraphic(FXUtils.wrap(SVG.ADD_CIRCLE));
|
||||
addProfileItem.setLeftIcon(SVG.ADD_CIRCLE);
|
||||
addProfileItem.setOnAction(e -> Controllers.navigate(new ProfilePage(null)));
|
||||
|
||||
pane.setFitToWidth(true);
|
||||
|
||||
@@ -136,7 +136,7 @@ public class InstallerListPage extends ListPageBase<InstallerItem> implements Ve
|
||||
|
||||
public void installOffline() {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("install.installer.install_offline.extension"), "*.jar", "*.exe"));
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.modloader.installer"), "*.jar", "*.exe"));
|
||||
Path file = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
|
||||
if (file != null) doInstallOffline(file);
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
|
||||
public void add() {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("mods.choose_mod"));
|
||||
chooser.setTitle(i18n("mods.add.title"));
|
||||
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.mod"), "*.jar", "*.zip", "*.litemod"));
|
||||
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
|
||||
|
||||
|
||||
@@ -102,6 +102,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
ComponentList root = new ComponentList();
|
||||
root.getStyleClass().add("no-padding");
|
||||
listView = new JFXListView<>();
|
||||
listView.getStyleClass().add("no-horizontal-scrollbar");
|
||||
|
||||
{
|
||||
toolbarPane = new TransitionPane();
|
||||
@@ -199,7 +200,6 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
{
|
||||
SpinnerPane center = new SpinnerPane();
|
||||
ComponentList.setVgrow(center, Priority.ALWAYS);
|
||||
center.getStyleClass().add("large-spinner-pane");
|
||||
center.loadingProperty().bind(skinnable.loadingProperty());
|
||||
|
||||
listView.setCellFactory(x -> new ModInfoListCell(listView));
|
||||
@@ -581,15 +581,15 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
imageView.setImage(VersionIconType.COMMAND.getIcon());
|
||||
|
||||
restoreButton.getStyleClass().add("toggle-icon4");
|
||||
restoreButton.setGraphic(FXUtils.limitingSize(SVG.RESTORE.createIcon(24), 24, 24));
|
||||
restoreButton.setGraphic(SVG.RESTORE.createIcon());
|
||||
|
||||
FXUtils.installFastTooltip(restoreButton, i18n("mods.restore"));
|
||||
|
||||
revealButton.getStyleClass().add("toggle-icon4");
|
||||
revealButton.setGraphic(FXUtils.limitingSize(SVG.FOLDER.createIcon(24), 24, 24));
|
||||
revealButton.setGraphic(SVG.FOLDER.createIcon());
|
||||
|
||||
infoButton.getStyleClass().add("toggle-icon4");
|
||||
infoButton.setGraphic(FXUtils.limitingSize(SVG.INFO.createIcon(24), 24, 24));
|
||||
infoButton.setGraphic(SVG.INFO.createIcon());
|
||||
|
||||
container.getChildren().setAll(checkBox, imageView, content, restoreButton, revealButton, infoButton);
|
||||
|
||||
|
||||
@@ -2,28 +2,27 @@ package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.resourcepack.ResourcepackFile;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.ListPageBase;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.*;
|
||||
import org.jackhuang.hmcl.ui.construct.MDListCell;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
@@ -36,7 +35,6 @@ import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@@ -113,8 +111,8 @@ public final class ResourcepackListPage extends ListPageBase<ResourcepackListPag
|
||||
|
||||
public void onAddFiles() {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle(i18n("resourcepack.add"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("resourcepack"), "*.zip"));
|
||||
fileChooser.setTitle(i18n("resourcepack.add.title"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.resourcepack"), "*.zip"));
|
||||
List<Path> files = FileUtils.toPaths(fileChooser.showOpenMultipleDialog(Controllers.getStage()));
|
||||
if (files != null && !files.isEmpty()) {
|
||||
addFiles(files);
|
||||
@@ -126,43 +124,24 @@ public final class ResourcepackListPage extends ListPageBase<ResourcepackListPag
|
||||
Controllers.navigate(Controllers.getDownloadPage());
|
||||
}
|
||||
|
||||
private static final class ResourcepackListPageSkin extends SkinBase<ResourcepackListPage> {
|
||||
private final JFXListView<ResourcepackInfoObject> listView;
|
||||
private static final class ResourcepackListPageSkin extends ToolbarListPageSkin<ResourcepackInfoObject, ResourcepackListPage> {
|
||||
|
||||
private ResourcepackListPageSkin(ResourcepackListPage control) {
|
||||
public ResourcepackListPageSkin(ResourcepackListPage control) {
|
||||
super(control);
|
||||
}
|
||||
|
||||
StackPane pane = new StackPane();
|
||||
pane.setPadding(new Insets(10));
|
||||
pane.getStyleClass().addAll("notice-pane");
|
||||
|
||||
ComponentList root = new ComponentList();
|
||||
root.getStyleClass().add("no-padding");
|
||||
listView = new JFXListView<>();
|
||||
|
||||
HBox toolbar = new HBox();
|
||||
toolbar.setAlignment(Pos.CENTER_LEFT);
|
||||
toolbar.setPickOnBounds(false);
|
||||
toolbar.getChildren().setAll(
|
||||
createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, control::refresh),
|
||||
createToolbarButton2(i18n("resourcepack.add"), SVG.ADD, control::onAddFiles),
|
||||
createToolbarButton2(i18n("resourcepack.download"), SVG.DOWNLOAD, control::onDownload)
|
||||
@Override
|
||||
protected List<Node> initializeToolbar(ResourcepackListPage skinnable) {
|
||||
return List.of(
|
||||
createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh),
|
||||
createToolbarButton2(i18n("resourcepack.add"), SVG.ADD, skinnable::onAddFiles),
|
||||
createToolbarButton2(i18n("resourcepack.download"), SVG.DOWNLOAD, skinnable::onDownload)
|
||||
);
|
||||
root.getContent().add(toolbar);
|
||||
}
|
||||
|
||||
SpinnerPane center = new SpinnerPane();
|
||||
ComponentList.setVgrow(center, Priority.ALWAYS);
|
||||
center.getStyleClass().add("large-spinner-pane");
|
||||
center.loadingProperty().bind(control.loadingProperty());
|
||||
|
||||
listView.setCellFactory(x -> new ResourcepackListCell(listView, control));
|
||||
Bindings.bindContent(listView.getItems(), control.getItems());
|
||||
|
||||
center.setContent(listView);
|
||||
root.getContent().add(center);
|
||||
|
||||
pane.getChildren().setAll(root);
|
||||
getChildren().setAll(pane);
|
||||
@Override
|
||||
protected ListCell<ResourcepackInfoObject> createListCell(JFXListView<ResourcepackInfoObject> listView) {
|
||||
return new ResourcepackListCell(listView, getSkinnable());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ import com.jfoenix.controls.JFXDialogLayout;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
@@ -32,7 +31,6 @@ import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.shape.SVGPath;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.schematic.LitematicFile;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
@@ -146,9 +144,9 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
||||
|
||||
public void onAddFiles() {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle(i18n("schematics.add"));
|
||||
fileChooser.setTitle(i18n("schematics.add.title"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(
|
||||
i18n("schematics"), "*.litematic"));
|
||||
i18n("extension.schematic"), "*.litematic"));
|
||||
List<Path> files = FileUtils.toPaths(fileChooser.showOpenMultipleDialog(Controllers.getStage()));
|
||||
if (files != null && !files.isEmpty()) {
|
||||
addFiles(files);
|
||||
@@ -551,8 +549,7 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
||||
private final HBox right;
|
||||
|
||||
private final ImageView iconImageView;
|
||||
private final SVGPath iconSVG;
|
||||
private final StackPane iconSVGWrapper;
|
||||
private final SVGContainer iconSVGView;
|
||||
|
||||
private final Tooltip tooltip = new Tooltip();
|
||||
|
||||
@@ -568,15 +565,7 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
||||
this.iconImageView = new ImageView();
|
||||
FXUtils.limitSize(iconImageView, 32, 32);
|
||||
|
||||
this.iconSVG = new SVGPath();
|
||||
iconSVG.getStyleClass().add("svg");
|
||||
iconSVG.setScaleX(32.0 / SVG.DEFAULT_SIZE);
|
||||
iconSVG.setScaleY(32.0 / SVG.DEFAULT_SIZE);
|
||||
|
||||
this.iconSVGWrapper = new StackPane(new Group(iconSVG));
|
||||
iconSVGWrapper.setAlignment(Pos.CENTER);
|
||||
FXUtils.setLimitWidth(iconSVGWrapper, 32);
|
||||
FXUtils.setLimitHeight(iconSVGWrapper, 32);
|
||||
this.iconSVGView = new SVGContainer(32);
|
||||
|
||||
BorderPane.setAlignment(left, Pos.CENTER);
|
||||
root.setLeft(left);
|
||||
@@ -638,8 +627,8 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
||||
iconImageView.setImage(fileItem.getImage());
|
||||
left.getChildren().setAll(iconImageView);
|
||||
} else {
|
||||
iconSVG.setContent(item.getIcon().getPath());
|
||||
left.getChildren().setAll(iconSVGWrapper);
|
||||
iconSVGView.setIcon(item.getIcon());
|
||||
left.getChildren().setAll(iconSVGView);
|
||||
}
|
||||
|
||||
center.setTitle(item.getName());
|
||||
|
||||
@@ -43,6 +43,7 @@ import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
|
||||
@@ -57,7 +58,6 @@ import java.lang.ref.WeakReference;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
||||
import static org.jackhuang.hmcl.util.DataSizeUnit.GIGABYTES;
|
||||
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
@@ -78,7 +78,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
private final JFXComboBox<String> cboWindowsSize;
|
||||
private final JFXTextField txtServerIP;
|
||||
private final ComponentList componentList;
|
||||
private final JFXComboBox<LauncherVisibility> cboLauncherVisibility;
|
||||
private final LineSelectButton<LauncherVisibility> launcherVisibilityPane;
|
||||
private final JFXCheckBox chkAutoAllocate;
|
||||
private final JFXCheckBox chkFullscreen;
|
||||
private final ComponentSublist javaSublist;
|
||||
@@ -90,9 +90,9 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
private final ComponentSublist gameDirSublist;
|
||||
private final MultiFileItem<GameDirectoryType> gameDirItem;
|
||||
private final MultiFileItem.FileOption<GameDirectoryType> gameDirCustomOption;
|
||||
private final JFXComboBox<ProcessPriority> cboProcessPriority;
|
||||
private final OptionToggleButton showLogsPane;
|
||||
private final OptionToggleButton enableDebugLogOutputPane;
|
||||
private final LineSelectButton<ProcessPriority> processPriorityPane;
|
||||
private final LineToggleButton showLogsPane;
|
||||
private final LineToggleButton enableDebugLogOutputPane;
|
||||
private final ImagePickerItem iconPickerItem;
|
||||
|
||||
private final ChangeListener<Collection<JavaRuntime>> javaListChangeListener;
|
||||
@@ -114,7 +114,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
scrollPane.setFitToHeight(true);
|
||||
scrollPane.setFitToWidth(true);
|
||||
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS);
|
||||
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
|
||||
getChildren().setAll(scrollPane);
|
||||
|
||||
rootPane = new VBox();
|
||||
@@ -173,30 +173,21 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
|
||||
{
|
||||
componentList = new ComponentList();
|
||||
componentList.setDepth(1);
|
||||
|
||||
if (!globalSetting) {
|
||||
BorderPane copyGlobalPane = new BorderPane();
|
||||
{
|
||||
Label label = new Label(i18n("settings.game.copy_global"));
|
||||
copyGlobalPane.setLeft(label);
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
var copyGlobalButton = LineButton.createNavigationButton();
|
||||
copyGlobalButton.setTitle(i18n("settings.game.copy_global"));
|
||||
copyGlobalButton.setOnAction(event ->
|
||||
Controllers.confirm(i18n("settings.game.copy_global.copy_all.confirm"), null, () -> {
|
||||
Set<String> ignored = new HashSet<>(Arrays.asList(
|
||||
"usesGlobal",
|
||||
"versionIcon"
|
||||
));
|
||||
|
||||
JFXButton copyAll = FXUtils.newBorderButton(i18n("settings.game.copy_global.copy_all"));
|
||||
copyAll.disableProperty().bind(modpack);
|
||||
copyGlobalPane.setRight(copyAll);
|
||||
copyAll.setOnAction(e -> Controllers.confirm(i18n("settings.game.copy_global.copy_all.confirm"), null, () -> {
|
||||
Set<String> ignored = new HashSet<>(Arrays.asList(
|
||||
"usesGlobal",
|
||||
"versionIcon"
|
||||
));
|
||||
PropertyUtils.copyProperties(profile.getGlobal(), lastVersionSetting, name -> !ignored.contains(name));
|
||||
}, null));
|
||||
|
||||
PropertyUtils.copyProperties(profile.getGlobal(), lastVersionSetting, name -> !ignored.contains(name));
|
||||
}, null));
|
||||
BorderPane.setAlignment(copyAll, Pos.CENTER_RIGHT);
|
||||
}
|
||||
|
||||
componentList.getContent().add(copyGlobalPane);
|
||||
componentList.getContent().add(copyGlobalButton);
|
||||
}
|
||||
|
||||
javaItem = new MultiFileItem<>();
|
||||
@@ -351,17 +342,10 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
maxMemoryPane.getChildren().setAll(title, chkAutoAllocate, lowerBoundPane, progressBarPane, digitalPane);
|
||||
}
|
||||
|
||||
BorderPane launcherVisibilityPane = new BorderPane();
|
||||
{
|
||||
Label label = new Label(i18n("settings.advanced.launcher_visible"));
|
||||
launcherVisibilityPane.setLeft(label);
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
|
||||
cboLauncherVisibility = new JFXComboBox<>();
|
||||
launcherVisibilityPane.setRight(cboLauncherVisibility);
|
||||
BorderPane.setAlignment(cboLauncherVisibility, Pos.CENTER_RIGHT);
|
||||
FXUtils.setLimitWidth(cboLauncherVisibility, 300);
|
||||
}
|
||||
launcherVisibilityPane = new LineSelectButton<>();
|
||||
launcherVisibilityPane.setTitle(i18n("settings.advanced.launcher_visible"));
|
||||
launcherVisibilityPane.setItems(LauncherVisibility.values());
|
||||
launcherVisibilityPane.setConverter(e -> i18n("settings.advanced.launcher_visibility." + e.name().toLowerCase(Locale.ROOT)));
|
||||
|
||||
BorderPane dimensionPane = new BorderPane();
|
||||
{
|
||||
@@ -391,23 +375,19 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
}
|
||||
}
|
||||
|
||||
showLogsPane = new OptionToggleButton();
|
||||
showLogsPane = new LineToggleButton();
|
||||
showLogsPane.setTitle(i18n("settings.show_log"));
|
||||
|
||||
enableDebugLogOutputPane = new OptionToggleButton();
|
||||
enableDebugLogOutputPane = new LineToggleButton();
|
||||
enableDebugLogOutputPane.setTitle(i18n("settings.enable_debug_log_output"));
|
||||
|
||||
BorderPane processPriorityPane = new BorderPane();
|
||||
{
|
||||
Label label = new Label(i18n("settings.advanced.process_priority"));
|
||||
processPriorityPane.setLeft(label);
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
|
||||
cboProcessPriority = new JFXComboBox<>();
|
||||
processPriorityPane.setRight(cboProcessPriority);
|
||||
BorderPane.setAlignment(cboProcessPriority, Pos.CENTER_RIGHT);
|
||||
FXUtils.setLimitWidth(cboProcessPriority, 300);
|
||||
}
|
||||
processPriorityPane = new LineSelectButton<>();
|
||||
processPriorityPane.setTitle(i18n("settings.advanced.process_priority"));
|
||||
processPriorityPane.setConverter(e -> i18n("settings.advanced.process_priority." + e.name().toLowerCase(Locale.ROOT)));
|
||||
processPriorityPane.setDescriptionConverter(e -> {
|
||||
String bundleKey = "settings.advanced.process_priority." + e.name().toLowerCase(Locale.ROOT) + ".desc";
|
||||
return I18n.hasKey(bundleKey) ? i18n(bundleKey) : null;
|
||||
});
|
||||
processPriorityPane.setItems(ProcessPriority.values());
|
||||
|
||||
GridPane serverPane = new GridPane();
|
||||
{
|
||||
@@ -436,23 +416,16 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
serverPane.addRow(0, new Label(i18n("settings.advanced.server_ip")), txtServerIP);
|
||||
}
|
||||
|
||||
BorderPane showAdvancedSettingPane = new BorderPane();
|
||||
{
|
||||
Label label = new Label(i18n("settings.advanced"));
|
||||
showAdvancedSettingPane.setLeft(label);
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
var showAdvancedSettingPane = LineButton.createNavigationButton();
|
||||
showAdvancedSettingPane.setTitle(i18n("settings.advanced"));
|
||||
showAdvancedSettingPane.setOnAction(event -> {
|
||||
if (lastVersionSetting != null) {
|
||||
if (advancedVersionSettingPage == null)
|
||||
advancedVersionSettingPage = new AdvancedVersionSettingPage(profile, versionId, lastVersionSetting);
|
||||
|
||||
JFXButton button = FXUtils.newBorderButton(i18n("settings.advanced.modify"));
|
||||
button.setOnAction(e -> {
|
||||
if (lastVersionSetting != null) {
|
||||
if (advancedVersionSettingPage == null)
|
||||
advancedVersionSettingPage = new AdvancedVersionSettingPage(profile, versionId, lastVersionSetting);
|
||||
|
||||
Controllers.navigateForward(advancedVersionSettingPage);
|
||||
}
|
||||
});
|
||||
showAdvancedSettingPane.setRight(button);
|
||||
}
|
||||
Controllers.navigateForward(advancedVersionSettingPage);
|
||||
}
|
||||
});
|
||||
|
||||
componentList.getContent().addAll(
|
||||
javaSublist,
|
||||
@@ -487,12 +460,6 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
|
||||
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onDecoratorPageNavigating);
|
||||
|
||||
cboLauncherVisibility.getItems().setAll(LauncherVisibility.values());
|
||||
cboLauncherVisibility.setConverter(stringConverter(e -> i18n("settings.advanced.launcher_visibility." + e.name().toLowerCase(Locale.ROOT))));
|
||||
|
||||
cboProcessPriority.getItems().setAll(ProcessPriority.values());
|
||||
cboProcessPriority.setConverter(stringConverter(e -> i18n("settings.advanced.process_priority." + e.name().toLowerCase(Locale.ROOT))));
|
||||
|
||||
componentList.disableProperty().bind(enableSpecificSettings.not());
|
||||
}
|
||||
|
||||
@@ -537,8 +504,8 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
chkFullscreen.selectedProperty().unbindBidirectional(lastVersionSetting.fullscreenProperty());
|
||||
showLogsPane.selectedProperty().unbindBidirectional(lastVersionSetting.showLogsProperty());
|
||||
enableDebugLogOutputPane.selectedProperty().unbindBidirectional(lastVersionSetting.enableDebugLogOutputProperty());
|
||||
FXUtils.unbindEnum(cboLauncherVisibility, lastVersionSetting.launcherVisibilityProperty());
|
||||
FXUtils.unbindEnum(cboProcessPriority, lastVersionSetting.processPriorityProperty());
|
||||
launcherVisibilityPane.valueProperty().unbindBidirectional(lastVersionSetting.launcherVisibilityProperty());
|
||||
processPriorityPane.valueProperty().unbindBidirectional(lastVersionSetting.processPriorityProperty());
|
||||
|
||||
lastVersionSetting.usesGlobalProperty().removeListener(usesGlobalListener);
|
||||
lastVersionSetting.javaVersionTypeProperty().removeListener(javaListener);
|
||||
@@ -572,8 +539,8 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
chkFullscreen.selectedProperty().bindBidirectional(versionSetting.fullscreenProperty());
|
||||
showLogsPane.selectedProperty().bindBidirectional(versionSetting.showLogsProperty());
|
||||
enableDebugLogOutputPane.selectedProperty().bindBidirectional(versionSetting.enableDebugLogOutputProperty());
|
||||
FXUtils.bindEnum(cboLauncherVisibility, versionSetting.launcherVisibilityProperty());
|
||||
FXUtils.bindEnum(cboProcessPriority, versionSetting.processPriorityProperty());
|
||||
launcherVisibilityPane.valueProperty().bindBidirectional(versionSetting.launcherVisibilityProperty());
|
||||
processPriorityPane.valueProperty().bindBidirectional(versionSetting.processPriorityProperty());
|
||||
|
||||
if (versionId != null)
|
||||
enableSpecificSettings.set(!versionSetting.isUsesGlobal());
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -61,18 +62,18 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.BackupInfo> {
|
||||
public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.BackupInfo> implements WorldManagePage.WorldRefreshable {
|
||||
static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss");
|
||||
|
||||
private final World world;
|
||||
private final Path backupsDir;
|
||||
private final boolean isReadOnly;
|
||||
private final BooleanProperty readOnly;
|
||||
private final Pattern backupFileNamePattern;
|
||||
|
||||
public WorldBackupsPage(WorldManagePage worldManagePage) {
|
||||
this.world = worldManagePage.getWorld();
|
||||
this.backupsDir = worldManagePage.getBackupsDir();
|
||||
this.isReadOnly = worldManagePage.isReadOnly();
|
||||
this.readOnly = worldManagePage.readOnlyProperty();
|
||||
this.backupFileNamePattern = Pattern.compile("(?<datetime>[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2})_" + Pattern.quote(world.getFileName()) + "( (?<count>[0-9]+))?\\.zip");
|
||||
|
||||
refresh();
|
||||
@@ -164,7 +165,7 @@ public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.Backup
|
||||
@Override
|
||||
protected List<Node> initializeToolbar(WorldBackupsPage skinnable) {
|
||||
JFXButton createBackup = createToolbarButton2(i18n("world.backup.create.new_one"), SVG.ARCHIVE, skinnable::createBackup);
|
||||
createBackup.setDisable(isReadOnly);
|
||||
createBackup.disableProperty().bind(getSkinnable().readOnly);
|
||||
|
||||
return Arrays.asList(
|
||||
createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh),
|
||||
|
||||
@@ -20,17 +20,16 @@ package org.jackhuang.hmcl.ui.versions;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.FileItem;
|
||||
import org.jackhuang.hmcl.ui.construct.LineFileChooserButton;
|
||||
import org.jackhuang.hmcl.ui.construct.LinePane;
|
||||
import org.jackhuang.hmcl.ui.construct.LineTextPane;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
@@ -42,47 +41,40 @@ public class WorldExportPageSkin extends SkinBase<WorldExportPage> {
|
||||
public WorldExportPageSkin(WorldExportPage skinnable) {
|
||||
super(skinnable);
|
||||
|
||||
Insets insets = new Insets(0, 0, 12, 0);
|
||||
VBox container = new VBox();
|
||||
container.setSpacing(16);
|
||||
container.setAlignment(Pos.CENTER);
|
||||
FXUtils.setLimitWidth(container, 500);
|
||||
{
|
||||
HBox labelContainer = new HBox();
|
||||
labelContainer.setPadding(new Insets(0, 0, 0, 5));
|
||||
Label label = new Label(i18n("world.export"));
|
||||
labelContainer.getChildren().setAll(label);
|
||||
container.getChildren().add(labelContainer);
|
||||
}
|
||||
|
||||
ComponentList list = new ComponentList();
|
||||
|
||||
FileItem fileItem = new FileItem();
|
||||
fileItem.setName(i18n("world.export.location"));
|
||||
fileItem.pathProperty().bindBidirectional(skinnable.pathProperty());
|
||||
list.getContent().add(fileItem);
|
||||
var chooseFileButton = new LineFileChooserButton();
|
||||
chooseFileButton.setTitle(i18n("world.export.location"));
|
||||
chooseFileButton.setType(LineFileChooserButton.Type.SAVE_FILE);
|
||||
chooseFileButton.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("world"), "*.zip"));
|
||||
chooseFileButton.locationProperty().bindBidirectional(skinnable.pathProperty());
|
||||
|
||||
var worldNamePane = new LinePane();
|
||||
worldNamePane.setTitle(i18n("world.name"));
|
||||
JFXTextField txtWorldName = new JFXTextField();
|
||||
txtWorldName.textProperty().bindBidirectional(skinnable.worldNameProperty());
|
||||
txtWorldName.setLabelFloat(true);
|
||||
txtWorldName.setPromptText(i18n("world.name"));
|
||||
StackPane.setMargin(txtWorldName, insets);
|
||||
list.getContent().add(txtWorldName);
|
||||
txtWorldName.setPrefWidth(300);
|
||||
worldNamePane.setRight(txtWorldName);
|
||||
|
||||
Label lblGameVersionTitle = new Label(i18n("world.game_version"));
|
||||
Label lblGameVersion = new Label();
|
||||
lblGameVersion.textProperty().bind(skinnable.gameVersionProperty());
|
||||
BorderPane gameVersionPane = new BorderPane();
|
||||
gameVersionPane.setPadding(new Insets(4, 0, 4, 0));
|
||||
gameVersionPane.setLeft(lblGameVersionTitle);
|
||||
gameVersionPane.setRight(lblGameVersion);
|
||||
list.getContent().add(gameVersionPane);
|
||||
LineTextPane gameVersionPane = new LineTextPane();
|
||||
gameVersionPane.setTitle(i18n("world.game_version"));
|
||||
gameVersionPane.textProperty().bind(skinnable.gameVersionProperty());
|
||||
|
||||
container.getChildren().add(list);
|
||||
list.getContent().setAll(chooseFileButton, worldNamePane, gameVersionPane);
|
||||
|
||||
container.getChildren().setAll(
|
||||
ComponentList.createComponentListTitle(i18n("world.export")),
|
||||
list
|
||||
);
|
||||
|
||||
JFXButton btnExport = FXUtils.newRaisedButton(i18n("button.export"));
|
||||
btnExport.disableProperty().bind(Bindings.createBooleanBinding(() -> txtWorldName.getText().isEmpty() || Files.exists(Paths.get(fileItem.getPath())),
|
||||
txtWorldName.textProperty().isEmpty(), fileItem.pathProperty()));
|
||||
btnExport.disableProperty().bind(Bindings.createBooleanBinding(() -> txtWorldName.getText().isEmpty() || Files.exists(Paths.get(chooseFileButton.getLocation())),
|
||||
txtWorldName.textProperty().isEmpty(), chooseFileButton.locationProperty()));
|
||||
btnExport.setOnAction(e -> skinnable.export());
|
||||
HBox bottom = new HBox();
|
||||
bottom.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
@@ -19,9 +19,9 @@ package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.github.steveice10.opennbt.tag.builtin.*;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXComboBox;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -31,12 +31,10 @@ import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.effect.BoxBlur;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.glavo.png.javafx.PNGJavaFXUtils;
|
||||
import org.jackhuang.hmcl.game.World;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -44,17 +42,19 @@ import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jetbrains.annotations.PropertyKey;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.text.DecimalFormat;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.formatDateTime;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -63,8 +63,9 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class WorldInfoPage extends SpinnerPane {
|
||||
public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.WorldRefreshable {
|
||||
private final WorldManagePage worldManagePage;
|
||||
private boolean isReadOnly;
|
||||
private final World world;
|
||||
private CompoundTag levelDat;
|
||||
|
||||
@@ -73,19 +74,7 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
public WorldInfoPage(WorldManagePage worldManagePage) {
|
||||
this.worldManagePage = worldManagePage;
|
||||
this.world = worldManagePage.getWorld();
|
||||
|
||||
this.setLoading(true);
|
||||
Task.supplyAsync(this::loadWorldInfo)
|
||||
.whenComplete(Schedulers.javafx(), ((result, exception) -> {
|
||||
if (exception == null) {
|
||||
this.levelDat = result;
|
||||
updateControls();
|
||||
setLoading(false);
|
||||
} else {
|
||||
LOG.warning("Failed to load level.dat", exception);
|
||||
setFailedReason(i18n("world.info.failed"));
|
||||
}
|
||||
})).start();
|
||||
refresh();
|
||||
}
|
||||
|
||||
private CompoundTag loadWorldInfo() throws IOException {
|
||||
@@ -97,7 +86,6 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
|
||||
private void updateControls() {
|
||||
CompoundTag dataTag = levelDat.get("Data");
|
||||
CompoundTag worldGenSettings = dataTag.get("WorldGenSettings");
|
||||
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
scrollPane.setFitToHeight(true);
|
||||
@@ -111,22 +99,23 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
rootPane.getStyleClass().add("card-list");
|
||||
|
||||
ComponentList basicInfo = new ComponentList();
|
||||
ComponentList worldInfo = new ComponentList();
|
||||
{
|
||||
BorderPane worldNamePane = new BorderPane();
|
||||
var worldNamePane = new LinePane();
|
||||
{
|
||||
setLeftLabel(worldNamePane, "world.name");
|
||||
worldNamePane.setTitle(i18n("world.name"));
|
||||
JFXTextField worldNameField = new JFXTextField();
|
||||
setRightTextField(worldNamePane, worldNameField, 200);
|
||||
|
||||
Tag tag = dataTag.get("LevelName");
|
||||
if (tag instanceof StringTag stringTag) {
|
||||
worldNameField.setText(stringTag.getValue());
|
||||
|
||||
worldNameField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
if (dataTag.get("LevelName") instanceof StringTag worldNameTag) {
|
||||
var worldName = new SimpleStringProperty(worldNameTag.getValue());
|
||||
FXUtils.bindString(worldNameField, worldName);
|
||||
worldNameField.getProperties().put(WorldInfoPage.class.getName() + ".worldNameProperty", worldName);
|
||||
worldName.addListener((observable, oldValue, newValue) -> {
|
||||
if (StringUtils.isNotBlank(newValue)) {
|
||||
try {
|
||||
world.setWorldName(newValue);
|
||||
worldManagePage.setTitle(i18n("world.manage.title", StringUtils.parseColorEscapes(world.getWorldName())));
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to set world name", e);
|
||||
}
|
||||
@@ -137,25 +126,18 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
}
|
||||
}
|
||||
|
||||
BorderPane gameVersionPane = new BorderPane();
|
||||
var gameVersionPane = new LineTextPane();
|
||||
{
|
||||
setLeftLabel(gameVersionPane, "world.info.game_version");
|
||||
Label gameVersionLabel = new Label();
|
||||
setRightTextLabel(gameVersionPane, gameVersionLabel, () -> world.getGameVersion() == null ? "" : world.getGameVersion().toNormalizedString());
|
||||
gameVersionPane.setTitle(i18n("world.info.game_version"));
|
||||
gameVersionPane.setText(world.getGameVersion() == null ? "" : world.getGameVersion().toNormalizedString());
|
||||
}
|
||||
|
||||
BorderPane iconPane = new BorderPane();
|
||||
var iconPane = new LinePane();
|
||||
{
|
||||
setLeftLabel(iconPane, "world.icon");
|
||||
iconPane.setTitle(i18n("world.icon"));
|
||||
|
||||
Runnable onClickAction = () -> Controllers.confirm(
|
||||
i18n("world.icon.change.tip"), i18n("world.icon.change"), MessageDialogPane.MessageType.INFO,
|
||||
this::changeWorldIcon,
|
||||
null
|
||||
);
|
||||
|
||||
FXUtils.limitSize(iconImageView, 32, 32);
|
||||
{
|
||||
FXUtils.limitSize(iconImageView, 32, 32);
|
||||
iconImageView.setImage(world.getIcon() == null ? FXUtils.newBuiltinImage("/assets/img/unknown_server.png") : world.getIcon());
|
||||
}
|
||||
|
||||
@@ -163,14 +145,20 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
JFXButton resetIconButton = new JFXButton();
|
||||
{
|
||||
editIconButton.setGraphic(SVG.EDIT.createIcon(20));
|
||||
editIconButton.setDisable(worldManagePage.isReadOnly());
|
||||
FXUtils.onClicked(editIconButton, onClickAction);
|
||||
editIconButton.setDisable(isReadOnly);
|
||||
editIconButton.setOnAction(event -> Controllers.confirm(
|
||||
I18n.i18n("world.icon.change.tip"),
|
||||
I18n.i18n("world.icon.change"),
|
||||
MessageDialogPane.MessageType.INFO,
|
||||
this::changeWorldIcon,
|
||||
null
|
||||
));
|
||||
FXUtils.installFastTooltip(editIconButton, i18n("button.edit"));
|
||||
editIconButton.getStyleClass().add("toggle-icon4");
|
||||
|
||||
resetIconButton.setGraphic(SVG.RESTORE.createIcon(20));
|
||||
resetIconButton.setDisable(worldManagePage.isReadOnly());
|
||||
FXUtils.onClicked(resetIconButton, this::clearWorldIcon);
|
||||
resetIconButton.setDisable(isReadOnly);
|
||||
resetIconButton.setOnAction(event -> this.clearWorldIcon());
|
||||
FXUtils.installFastTooltip(resetIconButton, i18n("button.reset"));
|
||||
resetIconButton.getStyleClass().add("toggle-icon4");
|
||||
}
|
||||
@@ -182,17 +170,15 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
iconPane.setRight(hBox);
|
||||
}
|
||||
|
||||
BorderPane seedPane = new BorderPane();
|
||||
var seedPane = new LinePane();
|
||||
{
|
||||
setLeftLabel(seedPane, "world.info.random_seed");
|
||||
seedPane.setTitle(i18n("world.info.random_seed"));
|
||||
|
||||
SimpleBooleanProperty visibility = new SimpleBooleanProperty();
|
||||
StackPane visibilityButton = new StackPane();
|
||||
{
|
||||
visibilityButton.setCursor(Cursor.HAND);
|
||||
visibilityButton.setAlignment(Pos.BOTTOM_RIGHT);
|
||||
FXUtils.setLimitWidth(visibilityButton, 12);
|
||||
FXUtils.setLimitHeight(visibilityButton, 12);
|
||||
visibilityButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
FXUtils.onClicked(visibilityButton, () -> visibility.set(!visibility.get()));
|
||||
}
|
||||
|
||||
@@ -214,282 +200,233 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
|
||||
HBox right = new HBox(8);
|
||||
{
|
||||
BorderPane.setAlignment(right, Pos.CENTER_RIGHT);
|
||||
right.setAlignment(Pos.CENTER_RIGHT);
|
||||
right.getChildren().setAll(visibilityButton, seedLabel);
|
||||
seedPane.setRight(right);
|
||||
}
|
||||
}
|
||||
|
||||
BorderPane lastPlayedPane = new BorderPane();
|
||||
var worldSpawnPoint = new LineTextPane();
|
||||
{
|
||||
setLeftLabel(lastPlayedPane, "world.info.last_played");
|
||||
Label lastPlayedLabel = new Label();
|
||||
setRightTextLabel(lastPlayedPane, lastPlayedLabel, () -> formatDateTime(Instant.ofEpochMilli(world.getLastPlayed())));
|
||||
worldSpawnPoint.setTitle(i18n("world.info.spawn"));
|
||||
|
||||
String value;
|
||||
if (dataTag.get("spawn") instanceof CompoundTag spawnTag && spawnTag.get("pos") instanceof IntArrayTag posTag) {
|
||||
value = Dimension.of(spawnTag.get("dimension") instanceof StringTag dimensionTag
|
||||
? dimensionTag
|
||||
: new StringTag("SpawnDimension", "minecraft:overworld"))
|
||||
.formatPosition(posTag);
|
||||
} else if (dataTag.get("SpawnX") instanceof IntTag intX
|
||||
&& dataTag.get("SpawnY") instanceof IntTag intY
|
||||
&& dataTag.get("SpawnZ") instanceof IntTag intZ) {
|
||||
value = Dimension.OVERWORLD.formatPosition(intX.getValue(), intY.getValue(), intZ.getValue());
|
||||
} else {
|
||||
value = null;
|
||||
}
|
||||
|
||||
worldSpawnPoint.setText(value);
|
||||
}
|
||||
|
||||
BorderPane timePane = new BorderPane();
|
||||
var lastPlayedPane = new LineTextPane();
|
||||
{
|
||||
setLeftLabel(timePane, "world.info.time");
|
||||
|
||||
Label timeLabel = new Label();
|
||||
setRightTextLabel(timePane, timeLabel, () -> {
|
||||
Tag tag = dataTag.get("Time");
|
||||
if (tag instanceof LongTag) {
|
||||
long days = ((LongTag) tag).getValue() / 24000;
|
||||
return i18n("world.info.time.format", days);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
});
|
||||
lastPlayedPane.setTitle(i18n("world.info.last_played"));
|
||||
lastPlayedPane.setText(formatDateTime(Instant.ofEpochMilli(world.getLastPlayed())));
|
||||
}
|
||||
|
||||
OptionToggleButton allowCheatsButton = new OptionToggleButton();
|
||||
var timePane = new LineTextPane();
|
||||
{
|
||||
timePane.setTitle(i18n("world.info.time"));
|
||||
if (dataTag.get("Time") instanceof LongTag timeTag) {
|
||||
Duration duration = Duration.ofSeconds(timeTag.getValue() / 20);
|
||||
timePane.setText(i18n("world.info.time.format", duration.toDays(), duration.toHoursPart(), duration.toMinutesPart()));
|
||||
}
|
||||
}
|
||||
|
||||
var allowCheatsButton = new LineToggleButton();
|
||||
{
|
||||
allowCheatsButton.setTitle(i18n("world.info.allow_cheats"));
|
||||
allowCheatsButton.setDisable(worldManagePage.isReadOnly());
|
||||
Tag tag = dataTag.get("allowCommands");
|
||||
allowCheatsButton.setDisable(isReadOnly);
|
||||
|
||||
checkTagAndSetListener(tag, allowCheatsButton);
|
||||
bindTagAndToggleButton(dataTag.get("allowCommands"), allowCheatsButton);
|
||||
}
|
||||
|
||||
OptionToggleButton generateFeaturesButton = new OptionToggleButton();
|
||||
var generateFeaturesButton = new LineToggleButton();
|
||||
{
|
||||
generateFeaturesButton.setTitle(i18n("world.info.generate_features"));
|
||||
generateFeaturesButton.setDisable(worldManagePage.isReadOnly());
|
||||
Tag tag = worldGenSettings != null ? worldGenSettings.get("generate_features") : dataTag.get("MapFeatures");
|
||||
generateFeaturesButton.setDisable(isReadOnly);
|
||||
|
||||
checkTagAndSetListener(tag, generateFeaturesButton);
|
||||
// generate_features was valid after 20w20a and MapFeatures was before that
|
||||
if (dataTag.get("WorldGenSettings") instanceof CompoundTag worldGenSettings) {
|
||||
bindTagAndToggleButton(worldGenSettings.get("generate_features"), generateFeaturesButton);
|
||||
} else {
|
||||
bindTagAndToggleButton(dataTag.get("MapFeatures"), generateFeaturesButton);
|
||||
}
|
||||
}
|
||||
|
||||
BorderPane difficultyPane = new BorderPane();
|
||||
var difficultyButton = new LineSelectButton<Difficulty>();
|
||||
{
|
||||
setLeftLabel(difficultyPane, "world.info.difficulty");
|
||||
difficultyButton.setTitle(i18n("world.info.difficulty"));
|
||||
difficultyButton.setDisable(worldManagePage.isReadOnly());
|
||||
difficultyButton.setItems(Difficulty.items);
|
||||
|
||||
JFXComboBox<Difficulty> difficultyBox = new JFXComboBox<>(Difficulty.items);
|
||||
difficultyBox.setDisable(worldManagePage.isReadOnly());
|
||||
BorderPane.setAlignment(difficultyBox, Pos.CENTER_RIGHT);
|
||||
difficultyPane.setRight(difficultyBox);
|
||||
|
||||
Tag tag = dataTag.get("Difficulty");
|
||||
if (tag instanceof ByteTag byteTag) {
|
||||
Difficulty difficulty = Difficulty.of(byteTag.getValue());
|
||||
if (dataTag.get("Difficulty") instanceof ByteTag difficultyTag) {
|
||||
Difficulty difficulty = Difficulty.of(difficultyTag.getValue());
|
||||
if (difficulty != null) {
|
||||
difficultyBox.setValue(difficulty);
|
||||
difficultyBox.valueProperty().addListener((o, oldValue, newValue) -> {
|
||||
difficultyButton.setValue(difficulty);
|
||||
difficultyButton.valueProperty().addListener((o, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
byteTag.setValue((byte) newValue.ordinal());
|
||||
difficultyTag.setValue((byte) newValue.ordinal());
|
||||
saveLevelDat();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
difficultyBox.setDisable(true);
|
||||
difficultyButton.setDisable(true);
|
||||
}
|
||||
} else {
|
||||
difficultyBox.setDisable(true);
|
||||
difficultyButton.setDisable(true);
|
||||
}
|
||||
}
|
||||
|
||||
OptionToggleButton difficultyLockPane = new OptionToggleButton();
|
||||
var difficultyLockPane = new LineToggleButton();
|
||||
{
|
||||
difficultyLockPane.setTitle(i18n("world.info.difficulty_lock"));
|
||||
difficultyLockPane.setDisable(worldManagePage.isReadOnly());
|
||||
difficultyLockPane.setDisable(isReadOnly);
|
||||
|
||||
Tag tag = dataTag.get("DifficultyLocked");
|
||||
checkTagAndSetListener(tag, difficultyLockPane);
|
||||
bindTagAndToggleButton(dataTag.get("DifficultyLocked"), difficultyLockPane);
|
||||
}
|
||||
|
||||
basicInfo.getContent().setAll(
|
||||
worldNamePane, gameVersionPane, iconPane, seedPane, lastPlayedPane, timePane,
|
||||
allowCheatsButton, generateFeaturesButton, difficultyPane, difficultyLockPane);
|
||||
worldInfo.getContent().setAll(
|
||||
worldNamePane, gameVersionPane, iconPane, seedPane, worldSpawnPoint, lastPlayedPane, timePane,
|
||||
allowCheatsButton, generateFeaturesButton, difficultyButton, difficultyLockPane);
|
||||
|
||||
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("world.info.basic")), basicInfo);
|
||||
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("world.info")), worldInfo);
|
||||
}
|
||||
|
||||
Tag playerTag = dataTag.get("Player");
|
||||
if (playerTag instanceof CompoundTag player) {
|
||||
if (dataTag.get("Player") instanceof CompoundTag playerTag) {
|
||||
ComponentList playerInfo = new ComponentList();
|
||||
|
||||
BorderPane locationPane = new BorderPane();
|
||||
var locationPane = new LineTextPane();
|
||||
{
|
||||
setLeftLabel(locationPane, "world.info.player.location");
|
||||
Label locationLabel = new Label();
|
||||
setRightTextLabel(locationPane, locationLabel, () -> {
|
||||
Dimension dim = Dimension.of(player.get("Dimension"));
|
||||
if (dim != null) {
|
||||
String posString = dim.formatPosition(player.get("Pos"));
|
||||
if (posString != null)
|
||||
return posString;
|
||||
}
|
||||
return "";
|
||||
});
|
||||
locationPane.setTitle(i18n("world.info.player.location"));
|
||||
Dimension dimension = Dimension.of(playerTag.get("Dimension"));
|
||||
if (dimension != null && playerTag.get("Pos") instanceof ListTag posTag) {
|
||||
locationPane.setText(dimension.formatPosition(posTag));
|
||||
}
|
||||
}
|
||||
|
||||
BorderPane lastDeathLocationPane = new BorderPane();
|
||||
var lastDeathLocationPane = new LineTextPane();
|
||||
{
|
||||
setLeftLabel(lastDeathLocationPane, "world.info.player.last_death_location");
|
||||
Label lastDeathLocationLabel = new Label();
|
||||
setRightTextLabel(lastDeathLocationPane, lastDeathLocationLabel, () -> {
|
||||
Tag tag = player.get("LastDeathLocation");// Valid after 22w14a; prior to this version, the game did not record the last death location data.
|
||||
if (tag instanceof CompoundTag compoundTag) {
|
||||
Dimension dim = Dimension.of(compoundTag.get("dimension"));
|
||||
if (dim != null) {
|
||||
String posString = dim.formatPosition(compoundTag.get("pos"));
|
||||
if (posString != null)
|
||||
return posString;
|
||||
}
|
||||
lastDeathLocationPane.setTitle(i18n("world.info.player.last_death_location"));
|
||||
// Valid after 22w14a; prior to this version, the game did not record the last death location data.
|
||||
if (playerTag.get("LastDeathLocation") instanceof CompoundTag LastDeathLocationTag) {
|
||||
Dimension dimension = Dimension.of(LastDeathLocationTag.get("dimension"));
|
||||
if (dimension != null && LastDeathLocationTag.get("pos") instanceof IntArrayTag posTag) {
|
||||
lastDeathLocationPane.setText(dimension.formatPosition(posTag));
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
BorderPane spawnPane = new BorderPane();
|
||||
var spawnPane = new LineTextPane();
|
||||
{
|
||||
setLeftLabel(spawnPane, "world.info.player.spawn");
|
||||
Label spawnLabel = new Label();
|
||||
setRightTextLabel(spawnPane, spawnLabel, () -> {
|
||||
|
||||
Dimension dimension;
|
||||
if (player.get("respawn") instanceof CompoundTag respawnTag && respawnTag.get("dimension") != null) { // Valid after 25w07a
|
||||
dimension = Dimension.of(respawnTag.get("dimension"));
|
||||
Tag posTag = respawnTag.get("pos");
|
||||
|
||||
if (posTag instanceof IntArrayTag intArrayTag && intArrayTag.length() >= 3) {
|
||||
return dimension.formatPosition(intArrayTag.getValue(0), intArrayTag.getValue(1), intArrayTag.getValue(2));
|
||||
}
|
||||
} else if (player.get("SpawnX") instanceof IntTag intX
|
||||
&& player.get("SpawnY") instanceof IntTag intY
|
||||
&& player.get("SpawnZ") instanceof IntTag intZ) { // Valid before 25w07a
|
||||
// SpawnDimension tag is valid after 20w12a. Prior to this version, the game did not record the respawn point dimension and respawned in the Overworld.
|
||||
dimension = Dimension.of(player.get("SpawnDimension") == null ? new IntTag("SpawnDimension", 0) : player.get("SpawnDimension"));
|
||||
if (dimension == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return dimension.formatPosition(intX.getValue(), intY.getValue(), intZ.getValue());
|
||||
}
|
||||
|
||||
return "";
|
||||
});
|
||||
spawnPane.setTitle(i18n("world.info.player.spawn"));
|
||||
if (playerTag.get("respawn") instanceof CompoundTag respawnTag
|
||||
&& respawnTag.get("dimension") instanceof StringTag dimensionTag
|
||||
&& respawnTag.get("pos") instanceof IntArrayTag intArrayTag
|
||||
&& intArrayTag.length() >= 3) { // Valid after 25w07a
|
||||
spawnPane.setText(Dimension.of(dimensionTag).formatPosition(intArrayTag));
|
||||
} else if (playerTag.get("SpawnX") instanceof IntTag intX
|
||||
&& playerTag.get("SpawnY") instanceof IntTag intY
|
||||
&& playerTag.get("SpawnZ") instanceof IntTag intZ) { // Valid before 25w07a
|
||||
// SpawnDimension tag is valid after 20w12a. Prior to this version, the game did not record the respawn point dimension and respawned in the Overworld.
|
||||
spawnPane.setText((playerTag.get("SpawnDimension") instanceof StringTag dimensionTag ? Dimension.of(dimensionTag) : Dimension.OVERWORLD)
|
||||
.formatPosition(intX.getValue(), intY.getValue(), intZ.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
BorderPane playerGameTypePane = new BorderPane();
|
||||
var playerGameTypePane = new LineSelectButton<GameType>();
|
||||
{
|
||||
setLeftLabel(playerGameTypePane, "world.info.player.game_type");
|
||||
playerGameTypePane.setTitle(i18n("world.info.player.game_type"));
|
||||
playerGameTypePane.setDisable(worldManagePage.isReadOnly());
|
||||
playerGameTypePane.setItems(GameType.items);
|
||||
|
||||
JFXComboBox<GameType> gameTypeBox = new JFXComboBox<>(GameType.items);
|
||||
gameTypeBox.setDisable(worldManagePage.isReadOnly());
|
||||
BorderPane.setAlignment(gameTypeBox, Pos.CENTER_RIGHT);
|
||||
playerGameTypePane.setRight(gameTypeBox);
|
||||
|
||||
Tag tag = player.get("playerGameType");
|
||||
Tag hardcoreTag = dataTag.get("hardcore");
|
||||
boolean isHardcore = hardcoreTag instanceof ByteTag && ((ByteTag) hardcoreTag).getValue() == 1;
|
||||
|
||||
if (tag instanceof IntTag intTag) {
|
||||
GameType gameType = GameType.of(intTag.getValue(), isHardcore);
|
||||
if (playerTag.get("playerGameType") instanceof IntTag playerGameTypeTag
|
||||
&& dataTag.get("hardcore") instanceof ByteTag hardcoreTag) {
|
||||
boolean isHardcore = hardcoreTag.getValue() == 1;
|
||||
GameType gameType = GameType.of(playerGameTypeTag.getValue(), isHardcore);
|
||||
if (gameType != null) {
|
||||
gameTypeBox.setValue(gameType);
|
||||
gameTypeBox.valueProperty().addListener((o, oldValue, newValue) -> {
|
||||
playerGameTypePane.setValue(gameType);
|
||||
playerGameTypePane.valueProperty().addListener((o, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
if (newValue == GameType.HARDCORE) {
|
||||
intTag.setValue(0); // survival (hardcore worlds are survival+hardcore flag)
|
||||
if (hardcoreTag instanceof ByteTag) {
|
||||
((ByteTag) hardcoreTag).setValue((byte) 1);
|
||||
}
|
||||
playerGameTypeTag.setValue(0); // survival (hardcore worlds are survival+hardcore flag)
|
||||
hardcoreTag.setValue((byte) 1);
|
||||
} else {
|
||||
intTag.setValue(newValue.ordinal());
|
||||
if (hardcoreTag instanceof ByteTag) {
|
||||
((ByteTag) hardcoreTag).setValue((byte) 0);
|
||||
}
|
||||
playerGameTypeTag.setValue(newValue.ordinal());
|
||||
hardcoreTag.setValue((byte) 0);
|
||||
}
|
||||
saveLevelDat();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
gameTypeBox.setDisable(true);
|
||||
playerGameTypePane.setDisable(true);
|
||||
}
|
||||
} else {
|
||||
gameTypeBox.setDisable(true);
|
||||
playerGameTypePane.setDisable(true);
|
||||
}
|
||||
}
|
||||
|
||||
BorderPane healthPane = new BorderPane();
|
||||
var healthPane = new LinePane();
|
||||
{
|
||||
setLeftLabel(healthPane, "world.info.player.health");
|
||||
JFXTextField healthField = new JFXTextField();
|
||||
setRightTextField(healthPane, healthField, 50);
|
||||
|
||||
Tag tag = player.get("Health");
|
||||
if (tag instanceof FloatTag floatTag) {
|
||||
setTagAndTextField(floatTag, healthField);
|
||||
} else {
|
||||
healthField.setDisable(true);
|
||||
}
|
||||
healthPane.setTitle(i18n("world.info.player.health"));
|
||||
setRightTextField(healthPane, 50, playerTag.get("Health"));
|
||||
}
|
||||
|
||||
BorderPane foodLevelPane = new BorderPane();
|
||||
var foodLevelPane = new LinePane();
|
||||
{
|
||||
setLeftLabel(foodLevelPane, "world.info.player.food_level");
|
||||
JFXTextField foodLevelField = new JFXTextField();
|
||||
setRightTextField(foodLevelPane, foodLevelField, 50);
|
||||
|
||||
Tag tag = player.get("foodLevel");
|
||||
if (tag instanceof IntTag intTag) {
|
||||
setTagAndTextField(intTag, foodLevelField);
|
||||
} else {
|
||||
foodLevelField.setDisable(true);
|
||||
}
|
||||
foodLevelPane.setTitle(i18n("world.info.player.food_level"));
|
||||
setRightTextField(foodLevelPane, 50, playerTag.get("foodLevel"));
|
||||
}
|
||||
|
||||
BorderPane xpLevelPane = new BorderPane();
|
||||
var foodSaturationPane = new LinePane();
|
||||
{
|
||||
setLeftLabel(xpLevelPane, "world.info.player.xp_level");
|
||||
JFXTextField xpLevelField = new JFXTextField();
|
||||
setRightTextField(xpLevelPane, xpLevelField, 50);
|
||||
foodSaturationPane.setTitle(i18n("world.info.player.food_saturation_level"));
|
||||
setRightTextField(foodSaturationPane, 50, playerTag.get("foodSaturationLevel"));
|
||||
}
|
||||
|
||||
Tag tag = player.get("XpLevel");
|
||||
if (tag instanceof IntTag intTag) {
|
||||
setTagAndTextField(intTag, xpLevelField);
|
||||
} else {
|
||||
xpLevelField.setDisable(true);
|
||||
}
|
||||
var xpLevelPane = new LinePane();
|
||||
{
|
||||
xpLevelPane.setTitle(i18n("world.info.player.xp_level"));
|
||||
setRightTextField(xpLevelPane, 50, playerTag.get("XpLevel"));
|
||||
}
|
||||
|
||||
playerInfo.getContent().setAll(
|
||||
locationPane, lastDeathLocationPane, spawnPane,
|
||||
playerGameTypePane, healthPane, foodLevelPane, xpLevelPane
|
||||
locationPane, lastDeathLocationPane, spawnPane, playerGameTypePane,
|
||||
healthPane, foodLevelPane, foodSaturationPane, xpLevelPane
|
||||
);
|
||||
|
||||
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("world.info.player")), playerInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private void setLeftLabel(BorderPane borderPane, @PropertyKey(resourceBundle = "assets.lang.I18N") String key) {
|
||||
Label label = new Label(i18n(key));
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
borderPane.setLeft(label);
|
||||
}
|
||||
|
||||
private void setRightTextField(BorderPane borderPane, JFXTextField textField, int perfWidth) {
|
||||
textField.setDisable(worldManagePage.isReadOnly());
|
||||
textField.setPrefWidth(perfWidth);
|
||||
textField.setAlignment(Pos.CENTER_RIGHT);
|
||||
borderPane.setRight(textField);
|
||||
}
|
||||
|
||||
private void setRightTextLabel(BorderPane borderPane, Label label, Callable<String> setNameCall) {
|
||||
FXUtils.copyOnDoubleClick(label);
|
||||
BorderPane.setAlignment(label, Pos.CENTER_RIGHT);
|
||||
try {
|
||||
label.setText(setNameCall.call());
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Exception happened when setting name", e);
|
||||
private void setRightTextField(LinePane linePane, int perfWidth, Tag tag) {
|
||||
JFXTextField textField = new JFXTextField();
|
||||
setRightTextField(linePane, textField, perfWidth);
|
||||
if (tag instanceof IntTag intTag) {
|
||||
bindTagAndTextField(intTag, textField);
|
||||
} else if (tag instanceof FloatTag floatTag) {
|
||||
bindTagAndTextField(floatTag, textField);
|
||||
} else {
|
||||
textField.setDisable(true);
|
||||
}
|
||||
borderPane.setRight(label);
|
||||
}
|
||||
|
||||
private void checkTagAndSetListener(Tag tag, OptionToggleButton toggleButton) {
|
||||
private void setRightTextField(LinePane linePane, JFXTextField textField, int perfWidth) {
|
||||
textField.setDisable(isReadOnly);
|
||||
textField.setPrefWidth(perfWidth);
|
||||
linePane.setRight(textField);
|
||||
}
|
||||
|
||||
private void bindTagAndToggleButton(Tag tag, LineToggleButton toggleButton) {
|
||||
if (tag instanceof ByteTag byteTag) {
|
||||
byte value = byteTag.getValue();
|
||||
if (value == 0 || value == 1) {
|
||||
@@ -511,14 +448,17 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
}
|
||||
}
|
||||
|
||||
private void setTagAndTextField(IntTag intTag, JFXTextField jfxTextField) {
|
||||
jfxTextField.setText(String.valueOf(intTag.getValue()));
|
||||
private void bindTagAndTextField(IntTag intTag, JFXTextField jfxTextField) {
|
||||
jfxTextField.setText(intTag.getValue().toString());
|
||||
|
||||
jfxTextField.textProperty().addListener((o, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
try {
|
||||
intTag.setValue(Integer.parseInt(newValue));
|
||||
saveLevelDat();
|
||||
Integer integer = Lang.toIntOrNull(newValue);
|
||||
if (integer != null) {
|
||||
intTag.setValue(integer);
|
||||
saveLevelDat();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
jfxTextField.setText(oldValue);
|
||||
LOG.warning("Exception happened when saving level.dat", e);
|
||||
@@ -529,14 +469,17 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
jfxTextField.setValidators(new NumberValidator(i18n("input.number"), true));
|
||||
}
|
||||
|
||||
private void setTagAndTextField(FloatTag floatTag, JFXTextField jfxTextField) {
|
||||
jfxTextField.setText(new DecimalFormat("#").format(floatTag.getValue().floatValue()));
|
||||
private void bindTagAndTextField(FloatTag floatTag, JFXTextField jfxTextField) {
|
||||
jfxTextField.setText(new DecimalFormat("0.#").format(floatTag.getValue()));
|
||||
|
||||
jfxTextField.textProperty().addListener((o, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
try {
|
||||
floatTag.setValue(Float.parseFloat(newValue));
|
||||
saveLevelDat();
|
||||
Float floatValue = Lang.toFloatOrNull(newValue);
|
||||
if (floatValue != null) {
|
||||
floatTag.setValue(floatValue);
|
||||
saveLevelDat();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
jfxTextField.setText(oldValue);
|
||||
LOG.warning("Exception happened when saving level.dat", e);
|
||||
@@ -556,6 +499,23 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refresh() {
|
||||
this.isReadOnly = worldManagePage.isReadOnly();
|
||||
this.setLoading(true);
|
||||
Task.supplyAsync(this::loadWorldInfo)
|
||||
.whenComplete(Schedulers.javafx(), ((result, exception) -> {
|
||||
if (exception == null) {
|
||||
this.levelDat = result;
|
||||
updateControls();
|
||||
setLoading(false);
|
||||
} else {
|
||||
LOG.warning("Failed to load level.dat", exception);
|
||||
setFailedReason(i18n("world.info.failed"));
|
||||
}
|
||||
})).start();
|
||||
}
|
||||
|
||||
private record Dimension(String name) {
|
||||
static final Dimension OVERWORLD = new Dimension(null);
|
||||
static final Dimension THE_NETHER = new Dimension(i18n("world.info.dimension.the_nether"));
|
||||
@@ -565,8 +525,8 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
if (tag instanceof IntTag intTag) {
|
||||
return switch (intTag.getValue()) {
|
||||
case 0 -> OVERWORLD;
|
||||
case 1 -> THE_NETHER;
|
||||
case 2 -> THE_END;
|
||||
case -1 -> THE_NETHER;
|
||||
case 1 -> THE_END;
|
||||
default -> null;
|
||||
};
|
||||
} else if (tag instanceof StringTag stringTag) {
|
||||
@@ -662,12 +622,12 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.png"), "*.png"));
|
||||
fileChooser.setInitialFileName("icon.png");
|
||||
|
||||
File file = fileChooser.showOpenDialog(Controllers.getStage());
|
||||
if (file == null) return;
|
||||
Path iconPath = FileUtils.toPath(fileChooser.showOpenDialog(Controllers.getStage()));
|
||||
if (iconPath == null) return;
|
||||
|
||||
Image image;
|
||||
try {
|
||||
image = FXUtils.loadImage(file.toPath());
|
||||
image = FXUtils.loadImage(iconPath);
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to load image", e);
|
||||
Controllers.dialog(i18n("world.icon.change.fail.load.text"), i18n("world.icon.change.fail.load.title"), MessageDialogPane.MessageType.ERROR);
|
||||
@@ -675,16 +635,16 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
}
|
||||
if ((int) image.getWidth() == 64 && (int) image.getHeight() == 64) {
|
||||
Path output = world.getFile().resolve("icon.png");
|
||||
saveImage(image, output);
|
||||
saveWorldIcon(iconPath, image, output);
|
||||
} else {
|
||||
Controllers.dialog(i18n("world.icon.change.fail.not_64x64.text", (int) image.getWidth(), (int) image.getHeight()), i18n("world.icon.change.fail.not_64x64.title"), MessageDialogPane.MessageType.ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveImage(Image image, Path path) {
|
||||
private void saveWorldIcon(Path sourcePath, Image image, Path targetPath) {
|
||||
Image oldImage = iconImageView.getImage();
|
||||
try {
|
||||
PNGJavaFXUtils.writeImage(image, path);
|
||||
FileUtils.copyFile(sourcePath, targetPath);
|
||||
iconImageView.setImage(image);
|
||||
Controllers.showToast(i18n("world.icon.change.succeed.toast"));
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.jfoenix.controls.JFXCheckBox;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -53,7 +54,7 @@ import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition;
|
||||
import static org.jackhuang.hmcl.util.StringUtils.parseColorEscapes;
|
||||
@@ -65,10 +66,10 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
private final BooleanProperty showAll = new SimpleBooleanProperty(this, "showAll", false);
|
||||
|
||||
private Path savesDir;
|
||||
private Path backupsDir;
|
||||
private List<World> worlds;
|
||||
private Profile profile;
|
||||
private String id;
|
||||
private String instanceId;
|
||||
private final BooleanProperty supportQuickPlay = new SimpleBooleanProperty(this, "supportQuickPlay", false);
|
||||
|
||||
private int refreshCount = 0;
|
||||
|
||||
@@ -88,9 +89,8 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
@Override
|
||||
public void loadVersion(Profile profile, String id) {
|
||||
this.profile = profile;
|
||||
this.id = id;
|
||||
this.instanceId = id;
|
||||
this.savesDir = profile.getRepository().getSavesDirectory(id);
|
||||
this.backupsDir = profile.getRepository().getBackupsDirectory(id);
|
||||
refresh();
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
} else if (showAll.get()) {
|
||||
getItems().setAll(worlds);
|
||||
} else {
|
||||
GameVersionNumber gameVersion = profile.getRepository().getGameVersion(id).map(GameVersionNumber::asGameVersion).orElse(null);
|
||||
GameVersionNumber gameVersion = profile.getRepository().getGameVersion(instanceId).map(GameVersionNumber::asGameVersion).orElse(null);
|
||||
getItems().setAll(worlds.stream()
|
||||
.filter(world -> world.getGameVersion() == null || world.getGameVersion().equals(gameVersion))
|
||||
.toList());
|
||||
@@ -108,7 +108,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
if (profile == null || id == null)
|
||||
if (profile == null || instanceId == null)
|
||||
return;
|
||||
|
||||
int currentRefresh = ++refreshCount;
|
||||
@@ -116,16 +116,17 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
setLoading(true);
|
||||
Task.supplyAsync(Schedulers.io(), () -> {
|
||||
// Ensure the game version number is parsed
|
||||
profile.getRepository().getGameVersion(id);
|
||||
try (Stream<World> stream = World.getWorlds(savesDir)) {
|
||||
return stream.toList();
|
||||
}
|
||||
profile.getRepository().getGameVersion(instanceId);
|
||||
return World.getWorlds(savesDir);
|
||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (refreshCount != currentRefresh) {
|
||||
// A newer refresh task is running, discard this result
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<String> gameVersion = profile.getRepository().getGameVersion(instanceId);
|
||||
supportQuickPlay.set(World.supportQuickPlay(GameVersionNumber.asGameVersion(gameVersion)));
|
||||
|
||||
worlds = result;
|
||||
updateWorldList();
|
||||
|
||||
@@ -138,8 +139,8 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
|
||||
public void add() {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("world.import.choose"));
|
||||
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("world.extension"), "*.zip"));
|
||||
chooser.setTitle(i18n("world.add.title"));
|
||||
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.world"), "*.zip"));
|
||||
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
|
||||
|
||||
if (res == null || res.isEmpty()) return;
|
||||
@@ -163,21 +164,21 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
refresh();
|
||||
}, e -> {
|
||||
if (e instanceof FileAlreadyExistsException)
|
||||
handler.reject(i18n("world.import.failed", i18n("world.import.already_exists")));
|
||||
handler.reject(i18n("world.add.failed", i18n("world.add.already_exists")));
|
||||
else if (e instanceof IOException && e.getCause() instanceof InvalidPathException)
|
||||
handler.reject(i18n("world.import.failed", i18n("install.new_game.malformed")));
|
||||
handler.reject(i18n("world.add.failed", i18n("install.new_game.malformed")));
|
||||
else
|
||||
handler.reject(i18n("world.import.failed", e.getClass().getName() + ": " + e.getLocalizedMessage()));
|
||||
handler.reject(i18n("world.add.failed", e.getClass().getName() + ": " + e.getLocalizedMessage()));
|
||||
}).start();
|
||||
}, world.getWorldName(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid));
|
||||
}, e -> {
|
||||
LOG.warning("Unable to parse world file " + zipFile, e);
|
||||
Controllers.dialog(i18n("world.import.invalid"));
|
||||
Controllers.dialog(i18n("world.add.invalid"));
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void showManagePage(World world) {
|
||||
Controllers.navigate(new WorldManagePage(world, backupsDir, profile, id));
|
||||
Controllers.navigate(new WorldManagePage(world, profile, instanceId));
|
||||
}
|
||||
|
||||
public void export(World world) {
|
||||
@@ -197,17 +198,21 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
}
|
||||
|
||||
public void launch(World world) {
|
||||
Versions.launchAndEnterWorld(profile, id, world.getFileName());
|
||||
Versions.launchAndEnterWorld(profile, instanceId, world.getFileName());
|
||||
}
|
||||
|
||||
public void generateLaunchScript(World world) {
|
||||
Versions.generateLaunchScriptForQuickEnterWorld(profile, id, world.getFileName());
|
||||
Versions.generateLaunchScriptForQuickEnterWorld(profile, instanceId, world.getFileName());
|
||||
}
|
||||
|
||||
public BooleanProperty showAllProperty() {
|
||||
return showAll;
|
||||
}
|
||||
|
||||
public ReadOnlyBooleanProperty supportQuickPlayProperty() {
|
||||
return supportQuickPlay;
|
||||
}
|
||||
|
||||
private final class WorldListPageSkin extends ToolbarListPageSkin<World, WorldListPage> {
|
||||
|
||||
WorldListPageSkin() {
|
||||
@@ -216,14 +221,15 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
|
||||
@Override
|
||||
protected List<Node> initializeToolbar(WorldListPage skinnable) {
|
||||
JFXCheckBox chkShowAll = new JFXCheckBox();
|
||||
chkShowAll.setText(i18n("world.show_all"));
|
||||
JFXCheckBox chkShowAll = new JFXCheckBox(i18n("world.show_all"));
|
||||
chkShowAll.selectedProperty().bindBidirectional(skinnable.showAllProperty());
|
||||
|
||||
return Arrays.asList(chkShowAll,
|
||||
return Arrays.asList(
|
||||
chkShowAll,
|
||||
createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh),
|
||||
createToolbarButton2(i18n("world.add"), SVG.ADD, skinnable::add),
|
||||
createToolbarButton2(i18n("world.download"), SVG.DOWNLOAD, skinnable::download));
|
||||
createToolbarButton2(i18n("world.download"), SVG.DOWNLOAD, skinnable::download)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -240,6 +246,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
private final ImageView imageView;
|
||||
private final Tooltip leftTooltip;
|
||||
private final TwoLineListItem content;
|
||||
private final JFXButton btnLaunch;
|
||||
|
||||
public WorldListCell(WorldListPage page) {
|
||||
this.page = page;
|
||||
@@ -271,6 +278,19 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
root.setRight(right);
|
||||
right.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
btnLaunch = new JFXButton();
|
||||
btnLaunch.visibleProperty().bind(page.supportQuickPlayProperty());
|
||||
btnLaunch.managedProperty().bind(btnLaunch.visibleProperty());
|
||||
right.getChildren().add(btnLaunch);
|
||||
btnLaunch.getStyleClass().add("toggle-icon4");
|
||||
btnLaunch.setGraphic(SVG.ROCKET_LAUNCH.createIcon());
|
||||
FXUtils.installFastTooltip(btnLaunch, i18n("version.launch"));
|
||||
btnLaunch.setOnAction(event -> {
|
||||
World world = getItem();
|
||||
if (world != null)
|
||||
page.launch(world);
|
||||
});
|
||||
|
||||
JFXButton btnMore = new JFXButton();
|
||||
right.getChildren().add(btnMore);
|
||||
btnMore.getStyleClass().add("toggle-icon4");
|
||||
@@ -278,7 +298,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
btnMore.setOnAction(event -> {
|
||||
World world = getItem();
|
||||
if (world != null)
|
||||
showPopupMenu(world, JFXPopup.PopupHPosition.RIGHT, 0, root.getHeight());
|
||||
showPopupMenu(world, page.supportQuickPlayProperty().get(), JFXPopup.PopupHPosition.RIGHT, 0, root.getHeight());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -294,7 +314,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
if (event.getButton() == MouseButton.PRIMARY)
|
||||
page.showManagePage(world);
|
||||
else if (event.getButton() == MouseButton.SECONDARY)
|
||||
showPopupMenu(world, JFXPopup.PopupHPosition.LEFT, event.getX(), event.getY());
|
||||
showPopupMenu(world, page.supportQuickPlayProperty().get(), JFXPopup.PopupHPosition.LEFT, event.getX(), event.getY());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -317,8 +337,12 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
|
||||
if (world.getGameVersion() != null)
|
||||
content.addTag(I18n.getDisplayVersion(world.getGameVersion()));
|
||||
if (world.isLocked())
|
||||
if (world.isLocked()) {
|
||||
content.addTag(i18n("world.locked"));
|
||||
btnLaunch.setDisable(true);
|
||||
} else {
|
||||
btnLaunch.setDisable(false);
|
||||
}
|
||||
|
||||
content.setSubtitle(i18n("world.datetime", formatDateTime(Instant.ofEpochMilli(world.getLastPlayed()))));
|
||||
|
||||
@@ -328,17 +352,19 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
|
||||
// Popup Menu
|
||||
|
||||
public void showPopupMenu(World world, JFXPopup.PopupHPosition hPosition, double initOffsetX, double initOffsetY) {
|
||||
public void showPopupMenu(World world, boolean supportQuickPlay, JFXPopup.PopupHPosition hPosition, double initOffsetX, double initOffsetY) {
|
||||
boolean worldLocked = world.isLocked();
|
||||
|
||||
PopupMenu popupMenu = new PopupMenu();
|
||||
JFXPopup popup = new JFXPopup(popupMenu);
|
||||
|
||||
if (world.getGameVersion() != null && world.getGameVersion().isAtLeast("1.20", "23w14a")) {
|
||||
if (supportQuickPlay) {
|
||||
|
||||
IconedMenuItem launchItem = new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch_and_enter_world"), () -> page.launch(world), popup);
|
||||
launchItem.setDisable(world.isLocked());
|
||||
popupMenu.getContent().add(launchItem);
|
||||
launchItem.setDisable(worldLocked);
|
||||
|
||||
popupMenu.getContent().addAll(
|
||||
launchItem,
|
||||
new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), () -> page.generateLaunchScript(world), popup),
|
||||
new MenuSeparator()
|
||||
);
|
||||
@@ -354,18 +380,19 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
||||
new IconedMenuItem(SVG.FORT, i18n("world.chunkbase.nether_fortress"), () -> ChunkBaseApp.openNetherFortressFinder(world), popup)
|
||||
);
|
||||
|
||||
if (world.getGameVersion() != null && world.getGameVersion().compareTo("1.13") >= 0) {
|
||||
popupMenu.getContent().add(new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"),
|
||||
() -> ChunkBaseApp.openEndCityFinder(world), popup));
|
||||
if (ChunkBaseApp.supportEndCity(world)) {
|
||||
popupMenu.getContent().add(new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"), () -> ChunkBaseApp.openEndCityFinder(world), popup));
|
||||
}
|
||||
}
|
||||
|
||||
IconedMenuItem exportMenuItem = new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), () -> page.export(world), popup);
|
||||
exportMenuItem.setDisable(worldLocked);
|
||||
|
||||
IconedMenuItem deleteMenuItem = new IconedMenuItem(SVG.DELETE, i18n("world.delete"), () -> page.delete(world), popup);
|
||||
deleteMenuItem.setDisable(worldLocked);
|
||||
|
||||
IconedMenuItem duplicateMenuItem = new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.duplicate"), () -> page.copy(world), popup);
|
||||
boolean worldLocked = world.isLocked();
|
||||
Stream.of(exportMenuItem, deleteMenuItem, duplicateMenuItem)
|
||||
.forEach(iconedMenuItem -> iconedMenuItem.setDisable(worldLocked));
|
||||
duplicateMenuItem.setDisable(worldLocked);
|
||||
|
||||
popupMenu.getContent().addAll(
|
||||
new MenuSeparator(),
|
||||
|
||||
@@ -19,9 +19,7 @@ package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
@@ -37,10 +35,13 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.ChunkBaseApp;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@@ -50,143 +51,96 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
*/
|
||||
public final class WorldManagePage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
|
||||
private final ObjectProperty<State> state;
|
||||
private final World world;
|
||||
private final Path backupsDir;
|
||||
private final Profile profile;
|
||||
private final String id;
|
||||
private final String instanceId;
|
||||
private final boolean supportQuickPlay;
|
||||
private FileChannel sessionLockChannel;
|
||||
|
||||
private boolean loadFailed = false;
|
||||
private final ObjectProperty<State> state;
|
||||
private boolean isFirstNavigation = true;
|
||||
private final BooleanProperty refreshable = new SimpleBooleanProperty(true);
|
||||
private final BooleanProperty readOnly = new SimpleBooleanProperty(false);
|
||||
|
||||
private final TabHeader header;
|
||||
private final TransitionPane transitionPane = new TransitionPane();
|
||||
private final TabHeader header = new TabHeader(transitionPane);
|
||||
private final TabHeader.Tab<WorldInfoPage> worldInfoTab = new TabHeader.Tab<>("worldInfoPage");
|
||||
private final TabHeader.Tab<WorldBackupsPage> worldBackupsTab = new TabHeader.Tab<>("worldBackupsPage");
|
||||
private final TabHeader.Tab<DatapackListPage> datapackTab = new TabHeader.Tab<>("datapackListPage");
|
||||
|
||||
private final TransitionPane transitionPane = new TransitionPane();
|
||||
|
||||
private FileChannel sessionLockChannel;
|
||||
|
||||
public WorldManagePage(World world, Path backupsDir, Profile profile, String id) {
|
||||
public WorldManagePage(World world, Profile profile, String instanceId) {
|
||||
this.world = world;
|
||||
this.backupsDir = backupsDir;
|
||||
this.backupsDir = profile.getRepository().getBackupsDirectory(instanceId);
|
||||
this.profile = profile;
|
||||
this.id = id;
|
||||
this.instanceId = instanceId;
|
||||
|
||||
updateSessionLockChannel();
|
||||
|
||||
sessionLockChannel = WorldManageUIUtils.getSessionLockChannel(world);
|
||||
try {
|
||||
world.reloadLevelDat();
|
||||
this.world.reloadLevelDat();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Can not load world level.dat of world: " + world.getFile(), e);
|
||||
loadFailed = true;
|
||||
LOG.warning("Can not load world level.dat of world: " + this.world.getFile(), e);
|
||||
this.addEventHandler(Navigator.NavigationEvent.NAVIGATED, event -> closePageForLoadingFail());
|
||||
}
|
||||
|
||||
this.worldInfoTab.setNodeSupplier(() -> new WorldInfoPage(this));
|
||||
this.worldBackupsTab.setNodeSupplier(() -> new WorldBackupsPage(this));
|
||||
this.datapackTab.setNodeSupplier(() -> new DatapackListPage(this));
|
||||
worldInfoTab.setNodeSupplier(() -> new WorldInfoPage(this));
|
||||
worldBackupsTab.setNodeSupplier(() -> new WorldBackupsPage(this));
|
||||
datapackTab.setNodeSupplier(() -> new DatapackListPage(this));
|
||||
|
||||
this.state = new SimpleObjectProperty<>(State.fromTitle(i18n("world.manage.title", StringUtils.parseColorEscapes(world.getWorldName()))));
|
||||
this.header = new TabHeader(transitionPane, worldInfoTab, worldBackupsTab);
|
||||
header.select(worldInfoTab);
|
||||
this.state = new SimpleObjectProperty<>(new State(i18n("world.manage.title", StringUtils.parseColorEscapes(world.getWorldName())), null, true, true, true));
|
||||
|
||||
setCenter(transitionPane);
|
||||
|
||||
BorderPane left = new BorderPane();
|
||||
FXUtils.setLimitWidth(left, 200);
|
||||
VBox.setVgrow(left, Priority.ALWAYS);
|
||||
setLeft(left);
|
||||
|
||||
AdvancedListBox sideBar = new AdvancedListBox()
|
||||
.addNavigationDrawerTab(header, worldInfoTab, i18n("world.info"), SVG.INFO, SVG.INFO_FILL)
|
||||
.addNavigationDrawerTab(header, worldBackupsTab, i18n("world.backup"), SVG.ARCHIVE, SVG.ARCHIVE_FILL);
|
||||
|
||||
if (world.getGameVersion() != null && // old game will not write game version to level.dat
|
||||
world.getGameVersion().isAtLeast("1.13", "17w43a")) {
|
||||
header.getTabs().add(datapackTab);
|
||||
sideBar.addNavigationDrawerTab(header, datapackTab, i18n("world.datapack"), SVG.EXTENSION, SVG.EXTENSION_FILL);
|
||||
}
|
||||
|
||||
left.setTop(sideBar);
|
||||
|
||||
AdvancedListBox toolbar = new AdvancedListBox();
|
||||
|
||||
if (world.getGameVersion() != null && world.getGameVersion().isAtLeast("1.20", "23w14a")) {
|
||||
toolbar.addNavigationDrawerItem(i18n("version.launch"), SVG.ROCKET_LAUNCH, this::launch, advancedListItem -> advancedListItem.setDisable(isReadOnly()));
|
||||
}
|
||||
|
||||
if (ChunkBaseApp.isSupported(world)) {
|
||||
PopupMenu chunkBasePopupMenu = new PopupMenu();
|
||||
JFXPopup chunkBasePopup = new JFXPopup(chunkBasePopupMenu);
|
||||
|
||||
chunkBasePopupMenu.getContent().addAll(
|
||||
new IconedMenuItem(SVG.EXPLORE, i18n("world.chunkbase.seed_map"), () -> ChunkBaseApp.openSeedMap(world), chunkBasePopup),
|
||||
new IconedMenuItem(SVG.VISIBILITY, i18n("world.chunkbase.stronghold"), () -> ChunkBaseApp.openStrongholdFinder(world), chunkBasePopup),
|
||||
new IconedMenuItem(SVG.FORT, i18n("world.chunkbase.nether_fortress"), () -> ChunkBaseApp.openNetherFortressFinder(world), chunkBasePopup)
|
||||
);
|
||||
|
||||
if (world.getGameVersion() != null && world.getGameVersion().compareTo("1.13") >= 0) {
|
||||
chunkBasePopupMenu.getContent().add(
|
||||
new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"), () -> ChunkBaseApp.openEndCityFinder(world), chunkBasePopup));
|
||||
}
|
||||
|
||||
toolbar.addNavigationDrawerItem(i18n("world.chunkbase"), SVG.EXPLORE, null, chunkBaseMenuItem ->
|
||||
chunkBaseMenuItem.setOnAction(e ->
|
||||
chunkBasePopup.show(chunkBaseMenuItem,
|
||||
JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT,
|
||||
chunkBaseMenuItem.getWidth(), 0)));
|
||||
}
|
||||
|
||||
toolbar.addNavigationDrawerItem(i18n("settings.game.exploration"), SVG.FOLDER_OPEN, () -> FXUtils.openFolder(world.getFile()), null);
|
||||
|
||||
{
|
||||
PopupMenu managePopupMenu = new PopupMenu();
|
||||
JFXPopup managePopup = new JFXPopup(managePopupMenu);
|
||||
|
||||
if (world.getGameVersion() != null && world.getGameVersion().isAtLeast("1.20", "23w14a")) {
|
||||
managePopupMenu.getContent().addAll(
|
||||
new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch"), this::launch, managePopup),
|
||||
new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), this::generateLaunchScript, managePopup),
|
||||
new MenuSeparator()
|
||||
);
|
||||
}
|
||||
|
||||
managePopupMenu.getContent().addAll(
|
||||
new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), () -> WorldManageUIUtils.export(world, sessionLockChannel), managePopup),
|
||||
new IconedMenuItem(SVG.DELETE, i18n("world.delete"), () -> WorldManageUIUtils.delete(world, () -> fireEvent(new PageCloseEvent()), sessionLockChannel), managePopup),
|
||||
new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.duplicate"), () -> WorldManageUIUtils.copyWorld(world, null), managePopup)
|
||||
);
|
||||
|
||||
toolbar.addNavigationDrawerItem(i18n("settings.game.management"), SVG.MENU, null, managePopupMenuItem ->
|
||||
{
|
||||
managePopupMenuItem.setOnAction(e ->
|
||||
managePopup.show(managePopupMenuItem,
|
||||
JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT,
|
||||
managePopupMenuItem.getWidth(), 0));
|
||||
managePopupMenuItem.setDisable(isReadOnly());
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0));
|
||||
left.setBottom(toolbar);
|
||||
Optional<String> gameVersion = profile.getRepository().getGameVersion(instanceId);
|
||||
supportQuickPlay = World.supportQuickPlay(GameVersionNumber.asGameVersion(gameVersion));
|
||||
|
||||
this.addEventHandler(Navigator.NavigationEvent.EXITED, this::onExited);
|
||||
this.addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
|
||||
}
|
||||
|
||||
private void onNavigated(Navigator.NavigationEvent event) {
|
||||
if (loadFailed) {
|
||||
Platform.runLater(() -> {
|
||||
fireEvent(new PageCloseEvent());
|
||||
Controllers.dialog(i18n("world.load.fail"), null, MessageDialogPane.MessageType.ERROR);
|
||||
});
|
||||
@Override
|
||||
protected @NotNull Skin createDefaultSkin() {
|
||||
return new Skin(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refresh() {
|
||||
updateSessionLockChannel();
|
||||
try {
|
||||
world.reloadLevelDat();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Can not load world level.dat of world: " + world.getFile(), e);
|
||||
closePageForLoadingFail();
|
||||
return;
|
||||
}
|
||||
|
||||
for (var tab : header.getTabs()) {
|
||||
if (tab.getNode() instanceof WorldRefreshable r) {
|
||||
r.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void closePageForLoadingFail() {
|
||||
Platform.runLater(() -> {
|
||||
fireEvent(new PageCloseEvent());
|
||||
Controllers.dialog(i18n("world.load.fail"), null, MessageDialogPane.MessageType.ERROR);
|
||||
});
|
||||
}
|
||||
|
||||
private void updateSessionLockChannel() {
|
||||
if (sessionLockChannel == null || !sessionLockChannel.isOpen()) {
|
||||
sessionLockChannel = WorldManageUIUtils.getSessionLockChannel(world);
|
||||
readOnly.set(sessionLockChannel == null);
|
||||
}
|
||||
}
|
||||
|
||||
private void onNavigated(Navigator.NavigationEvent event) {
|
||||
if (isFirstNavigation)
|
||||
isFirstNavigation = false;
|
||||
else
|
||||
refresh();
|
||||
}
|
||||
|
||||
public void onExited(Navigator.NavigationEvent event) {
|
||||
try {
|
||||
WorldManageUIUtils.closeSessionLockChannel(world, sessionLockChannel);
|
||||
@@ -194,11 +148,24 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
}
|
||||
}
|
||||
|
||||
public void launch() {
|
||||
fireEvent(new PageCloseEvent());
|
||||
Versions.launchAndEnterWorld(profile, instanceId, world.getFileName());
|
||||
}
|
||||
|
||||
public void generateLaunchScript() {
|
||||
Versions.generateLaunchScriptForQuickEnterWorld(profile, instanceId, world.getFileName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadOnlyObjectProperty<State> stateProperty() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.state.set(new DecoratorPage.State(title, null, true, true, true));
|
||||
}
|
||||
|
||||
public World getWorld() {
|
||||
return world;
|
||||
}
|
||||
@@ -208,15 +175,123 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
}
|
||||
|
||||
public boolean isReadOnly() {
|
||||
return sessionLockChannel == null;
|
||||
return readOnly.get();
|
||||
}
|
||||
|
||||
public void launch() {
|
||||
fireEvent(new PageCloseEvent());
|
||||
Versions.launchAndEnterWorld(profile, id, world.getFileName());
|
||||
public BooleanProperty readOnlyProperty() {
|
||||
return readOnly;
|
||||
}
|
||||
|
||||
public void generateLaunchScript() {
|
||||
Versions.generateLaunchScriptForQuickEnterWorld(profile, id, world.getFileName());
|
||||
@Override
|
||||
public BooleanProperty refreshableProperty() {
|
||||
return refreshable;
|
||||
}
|
||||
|
||||
public static class Skin extends DecoratorAnimatedPageSkin<WorldManagePage> {
|
||||
|
||||
protected Skin(WorldManagePage control) {
|
||||
super(control);
|
||||
|
||||
setCenter(control.transitionPane);
|
||||
setLeft(getSidebar());
|
||||
}
|
||||
|
||||
private BorderPane getSidebar() {
|
||||
BorderPane sidebar = new BorderPane();
|
||||
{
|
||||
FXUtils.setLimitWidth(sidebar, 200);
|
||||
VBox.setVgrow(sidebar, Priority.ALWAYS);
|
||||
}
|
||||
|
||||
sidebar.setTop(getTabBar());
|
||||
sidebar.setBottom(getToolBar());
|
||||
|
||||
return sidebar;
|
||||
}
|
||||
|
||||
private AdvancedListBox getTabBar() {
|
||||
AdvancedListBox tabBar = new AdvancedListBox();
|
||||
{
|
||||
getSkinnable().header.getTabs().addAll(getSkinnable().worldInfoTab, getSkinnable().worldBackupsTab);
|
||||
getSkinnable().header.select(getSkinnable().worldInfoTab);
|
||||
|
||||
tabBar.addNavigationDrawerTab(getSkinnable().header, getSkinnable().worldInfoTab, i18n("world.info"), SVG.INFO, SVG.INFO_FILL)
|
||||
.addNavigationDrawerTab(getSkinnable().header, getSkinnable().worldBackupsTab, i18n("world.backup"), SVG.ARCHIVE, SVG.ARCHIVE_FILL);
|
||||
|
||||
if (getSkinnable().world.supportDatapacks()) {
|
||||
getSkinnable().header.getTabs().add(getSkinnable().datapackTab);
|
||||
tabBar.addNavigationDrawerTab(getSkinnable().header, getSkinnable().datapackTab, i18n("world.datapack"), SVG.EXTENSION, SVG.EXTENSION_FILL);
|
||||
}
|
||||
}
|
||||
|
||||
return tabBar;
|
||||
}
|
||||
|
||||
private AdvancedListBox getToolBar() {
|
||||
AdvancedListBox toolbar = new AdvancedListBox();
|
||||
BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0));
|
||||
{
|
||||
if (getSkinnable().supportQuickPlay) {
|
||||
toolbar.addNavigationDrawerItem(i18n("version.launch"), SVG.ROCKET_LAUNCH, () -> getSkinnable().launch(), advancedListItem -> advancedListItem.disableProperty().bind(getSkinnable().readOnlyProperty()));
|
||||
}
|
||||
|
||||
if (ChunkBaseApp.isSupported(getSkinnable().world)) {
|
||||
PopupMenu chunkBasePopupMenu = new PopupMenu();
|
||||
JFXPopup chunkBasePopup = new JFXPopup(chunkBasePopupMenu);
|
||||
|
||||
chunkBasePopupMenu.getContent().addAll(
|
||||
new IconedMenuItem(SVG.EXPLORE, i18n("world.chunkbase.seed_map"), () -> ChunkBaseApp.openSeedMap(getSkinnable().world), chunkBasePopup),
|
||||
new IconedMenuItem(SVG.VISIBILITY, i18n("world.chunkbase.stronghold"), () -> ChunkBaseApp.openStrongholdFinder(getSkinnable().world), chunkBasePopup),
|
||||
new IconedMenuItem(SVG.FORT, i18n("world.chunkbase.nether_fortress"), () -> ChunkBaseApp.openNetherFortressFinder(getSkinnable().world), chunkBasePopup)
|
||||
);
|
||||
|
||||
if (ChunkBaseApp.supportEndCity(getSkinnable().world)) {
|
||||
chunkBasePopupMenu.getContent().add(
|
||||
new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"), () -> ChunkBaseApp.openEndCityFinder(getSkinnable().world), chunkBasePopup));
|
||||
}
|
||||
|
||||
toolbar.addNavigationDrawerItem(i18n("world.chunkbase"), SVG.EXPLORE, null, chunkBaseMenuItem ->
|
||||
chunkBaseMenuItem.setOnAction(e ->
|
||||
chunkBasePopup.show(chunkBaseMenuItem,
|
||||
JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT,
|
||||
chunkBaseMenuItem.getWidth(), 0)));
|
||||
}
|
||||
|
||||
toolbar.addNavigationDrawerItem(i18n("settings.game.exploration"), SVG.FOLDER_OPEN, () -> FXUtils.openFolder(getSkinnable().world.getFile()));
|
||||
|
||||
{
|
||||
PopupMenu managePopupMenu = new PopupMenu();
|
||||
JFXPopup managePopup = new JFXPopup(managePopupMenu);
|
||||
|
||||
if (getSkinnable().supportQuickPlay) {
|
||||
managePopupMenu.getContent().addAll(
|
||||
new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch"), () -> getSkinnable().launch(), managePopup),
|
||||
new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), () -> getSkinnable().generateLaunchScript(), managePopup),
|
||||
new MenuSeparator()
|
||||
);
|
||||
}
|
||||
|
||||
managePopupMenu.getContent().addAll(
|
||||
new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), () -> WorldManageUIUtils.export(getSkinnable().world, getSkinnable().sessionLockChannel), managePopup),
|
||||
new IconedMenuItem(SVG.DELETE, i18n("world.delete"), () -> WorldManageUIUtils.delete(getSkinnable().world, () -> getSkinnable().fireEvent(new PageCloseEvent()), getSkinnable().sessionLockChannel), managePopup),
|
||||
new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.duplicate"), () -> WorldManageUIUtils.copyWorld(getSkinnable().world, null), managePopup)
|
||||
);
|
||||
|
||||
toolbar.addNavigationDrawerItem(i18n("settings.game.management"), SVG.MENU, null, managePopupMenuItem ->
|
||||
{
|
||||
managePopupMenuItem.setOnAction(e ->
|
||||
managePopup.show(managePopupMenuItem,
|
||||
JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT,
|
||||
managePopupMenuItem.getWidth(), 0));
|
||||
managePopupMenuItem.disableProperty().bind(getSkinnable().readOnlyProperty());
|
||||
});
|
||||
}
|
||||
}
|
||||
return toolbar;
|
||||
}
|
||||
}
|
||||
|
||||
public interface WorldRefreshable {
|
||||
void refresh();
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,7 @@ public final class WorldManageUIUtils {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle(i18n("world.export.title"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("world"), "*.zip"));
|
||||
fileChooser.setInitialFileName(world.getWorldName());
|
||||
fileChooser.setInitialFileName(world.getWorldName() + ".zip");
|
||||
Path file = FileUtils.toPath(fileChooser.showSaveDialog(Controllers.getStage()));
|
||||
if (file == null) {
|
||||
return;
|
||||
@@ -144,7 +144,7 @@ public final class WorldManageUIUtils {
|
||||
FileChannel lock = world.lock();
|
||||
LOG.info("Acquired lock on world " + world.getFileName());
|
||||
return lock;
|
||||
} catch (IOException ignored) {
|
||||
} catch (WorldLockedException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ public final class ChunkBaseApp {
|
||||
private static final String CHUNK_BASE_URL = "https://www.chunkbase.com";
|
||||
|
||||
private static final GameVersionNumber MIN_GAME_VERSION = GameVersionNumber.asGameVersion("1.7");
|
||||
private static final GameVersionNumber MIN_END_CITY_VERSION = GameVersionNumber.asGameVersion("1.13");
|
||||
|
||||
private static final String[] SEED_MAP_GAME_VERSIONS = {
|
||||
"1.21.9", "1.21.6", "1.21.5", "1.21.4", "1.21.2", "1.21", "1.20",
|
||||
@@ -52,6 +53,11 @@ public final class ChunkBaseApp {
|
||||
world.getGameVersion().compareTo(MIN_GAME_VERSION) >= 0;
|
||||
}
|
||||
|
||||
public static boolean supportEndCity(@NotNull World world) {
|
||||
return world.getSeed() != null && world.getGameVersion() != null &&
|
||||
world.getGameVersion().compareTo(MIN_END_CITY_VERSION) >= 0;
|
||||
}
|
||||
|
||||
public static ChunkBaseApp newBuilder(String app, long seed) {
|
||||
return new ChunkBaseApp(new StringBuilder(CHUNK_BASE_URL).append("/apps/").append(app).append("#seed=").append(seed));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import io.nayuki.qrcodegen.QrCode;
|
||||
|
||||
/// @author Glavo
|
||||
public final class QrCodeUtils {
|
||||
public static String toSVGPath(QrCode qr) {
|
||||
return toSVGPath(qr, 0);
|
||||
}
|
||||
|
||||
public static String toSVGPath(QrCode qr, int border) {
|
||||
int actualSize = qr.size + border * 2;
|
||||
|
||||
var builder = new StringBuilder(qr.size * qr.size * 12);
|
||||
builder.append('M').append(actualSize).append(' ').append(actualSize).append("ZM0 0Z");
|
||||
|
||||
for (int y = 0; y < qr.size; y++) {
|
||||
for (int x = 0; x < qr.size; x++) {
|
||||
if (qr.getModule(x, y)) {
|
||||
if (x != 0 || y != 0)
|
||||
builder.append(' ');
|
||||
builder.append("M").append(x + border).append(',').append(y + border).append("h1v1h-1z");
|
||||
}
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private QrCodeUtils() {
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,7 @@
|
||||
},
|
||||
{
|
||||
"title": "EasyTier",
|
||||
"subtitle": "Copyright 2024-present Easytier Programme within The Commons Conservancy",
|
||||
"subtitle": "Copyright 2024-present Easytier Programme within The Commons Conservancy.\nLicensed under the LGPL 3.0 License.",
|
||||
"externalLink": "https://github.com/EasyTier/EasyTier"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -324,20 +324,25 @@
|
||||
-fx-fill: -monet-on-surface-variant;
|
||||
}
|
||||
|
||||
.two-line-list-item > .first-line > .tag {
|
||||
.two-line-list-item > .first-line .scroll-bar {
|
||||
-fx-opacity: 0;
|
||||
-fx-max-height: 0;
|
||||
-fx-min-height: 0;
|
||||
-fx-pref-height: 0;
|
||||
}
|
||||
|
||||
.two-line-list-item > .first-line .tag {
|
||||
-fx-text-fill: -monet-on-secondary-container;
|
||||
-fx-background-color: -monet-secondary-container;
|
||||
-fx-background-radius: 2;
|
||||
-fx-padding: 2;
|
||||
-fx-font-weight: normal;
|
||||
-fx-font-size: 12px;
|
||||
}
|
||||
|
||||
.two-line-list-item > .first-line > .tag-warning {
|
||||
.two-line-list-item > .first-line .tag:warning {
|
||||
-fx-text-fill: -monet-on-error-container;
|
||||
-fx-background-color: -fixed-warning-tag-background;
|
||||
-fx-padding: 2;
|
||||
-fx-font-weight: normal;
|
||||
-fx-font-size: 12px;
|
||||
}
|
||||
|
||||
.two-line-item-second-large {
|
||||
@@ -567,6 +572,41 @@
|
||||
-fx-text-fill: -monet-on-surface;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Microsoft Login Dialog *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.microsoft-login-dialog:bodyonly {
|
||||
-fx-padding: 0px 0px 0px 0px;
|
||||
}
|
||||
|
||||
.method-title {
|
||||
-fx-text-fill: -monet-on-surface;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.method-desc {
|
||||
-fx-text-fill: -monet-outline;
|
||||
-fx-font-size: 0.9em;
|
||||
-fx-line-spacing: 2;
|
||||
-fx-wrap-text: true;
|
||||
-fx-text-alignment: center;
|
||||
}
|
||||
|
||||
.code-box {
|
||||
-fx-background-color: -monet-surface-variant;
|
||||
-fx-background-radius: 6;
|
||||
-fx-padding: 0 10 0 10;
|
||||
-fx-alignment: center;
|
||||
}
|
||||
|
||||
.code-box .code-label {
|
||||
-fx-font-size: 22px;
|
||||
-fx-font-weight: bold;
|
||||
-fx-text-fill: -monet-primary;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
@@ -587,8 +627,16 @@
|
||||
-fx-pref-width: 150.0px;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* JFX Snack Bar *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.jfx-snackbar-content {
|
||||
-fx-background-color: -monet-inverse-surface;
|
||||
-fx-padding: 5;
|
||||
-fx-spacing: 5;
|
||||
}
|
||||
|
||||
.jfx-snackbar-toast {
|
||||
@@ -1076,6 +1124,14 @@
|
||||
-fx-border-width: 0;
|
||||
}
|
||||
|
||||
.options-sublist-wrapper .expand-icon .svg {
|
||||
-fx-fill: -monet-on-surface-variant;
|
||||
}
|
||||
|
||||
.options-sublist-wrapper .expand-icon .svg:disabled {
|
||||
-fx-opacity: 0.4;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* JFX Toggle Button *
|
||||
@@ -1103,15 +1159,16 @@
|
||||
.jfx-toggle-button Line {
|
||||
-fx-stroke: -jfx-untoggle-line-color;
|
||||
}
|
||||
.jfx-toggle-button:selected Line{
|
||||
|
||||
.jfx-toggle-button:selected Line {
|
||||
-fx-stroke: -jfx-toggle-line-color;
|
||||
}
|
||||
|
||||
.jfx-toggle-button Circle{
|
||||
.jfx-toggle-button Circle {
|
||||
-fx-fill: -jfx-untoggle-color;
|
||||
}
|
||||
|
||||
.jfx-toggle-button:selected Circle{
|
||||
.jfx-toggle-button:selected Circle {
|
||||
-fx-fill: -jfx-toggle-color;
|
||||
}
|
||||
|
||||
@@ -1268,6 +1325,7 @@
|
||||
|
||||
.log-toggle {
|
||||
-fx-border: 1px;
|
||||
-fx-background-insets: 0;
|
||||
-fx-border-color: -fixed-log-toggle-unselected;
|
||||
-fx-text-fill: -fixed-log-toggle-unselected;
|
||||
}
|
||||
@@ -1348,6 +1406,10 @@
|
||||
-fx-background-color: -fixed-log-info;
|
||||
}
|
||||
|
||||
.log-window-list-cell {
|
||||
-fx-background-color: -fixed-log-info;
|
||||
}
|
||||
|
||||
.log-window-list-cell:debug {
|
||||
-fx-text-fill: -fixed-log-text-fill;
|
||||
-fx-background-color: -fixed-log-debug;
|
||||
@@ -1403,14 +1465,6 @@
|
||||
-fx-stroke-width: 3.0;
|
||||
}
|
||||
|
||||
.large-spinner-pane .jfx-spinner {
|
||||
-jfx-radius: 20;
|
||||
}
|
||||
|
||||
.large-spinner-pane .jfx-spinner {
|
||||
-fx-stroke-width: 5.0;
|
||||
}
|
||||
|
||||
.second-spinner {
|
||||
-jfx-radius: 30;
|
||||
}
|
||||
@@ -1857,3 +1911,54 @@
|
||||
.tooltip .text {
|
||||
-fx-fill: -monet-inverse-on-surface;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Line Component *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.line-component .line-component-container {
|
||||
-fx-padding: 10 16 10 16;
|
||||
}
|
||||
|
||||
.line-component.no-padding .line-component-container {
|
||||
-fx-padding: 0;
|
||||
}
|
||||
|
||||
.line-component:large-title .title-label {
|
||||
-fx-font-size: 15px;
|
||||
}
|
||||
|
||||
.line-button .svg:disabled {
|
||||
-fx-opacity: 0.4;
|
||||
}
|
||||
|
||||
.line-button .trailing-label {
|
||||
-fx-text-fill: -monet-on-surface-variant;
|
||||
}
|
||||
|
||||
.line-button .trailing-icon .svg {
|
||||
-fx-fill: -monet-on-surface-variant;
|
||||
-fx-opacity: 1;
|
||||
}
|
||||
|
||||
.line-button .trailing-icon .svg:disabled {
|
||||
-fx-opacity: 0.4;
|
||||
}
|
||||
|
||||
.line-select-button .menu-container .title-label {
|
||||
-fx-font-size: 13px;
|
||||
}
|
||||
|
||||
.line-select-button .menu-container:selected .title-label {
|
||||
-fx-text-fill: -monet-primary;
|
||||
}
|
||||
|
||||
.line-select-button .menu-container:selected .subtitle-label {
|
||||
-fx-text-fill: -monet-primary;
|
||||
}
|
||||
|
||||
.line-toggle-button .jfx-toggle-button {
|
||||
-fx-padding: 0;
|
||||
}
|
||||
@@ -118,11 +118,11 @@ account.methods.microsoft.error.wrong_verify_method=Failed to log in. Please try
|
||||
account.methods.microsoft.logging_in=Logging in...
|
||||
account.methods.microsoft.makegameidsettings=Create Profile / Edit Profile Name
|
||||
account.methods.microsoft.hint=Click the "Log in" button to start adding your Microsoft account.
|
||||
account.methods.microsoft.manual=Please enter the code shown above on the pop-up webpage to complete the login.\n\
|
||||
\n\
|
||||
If the website fails to load, please open %s manually in your browser.\n\
|
||||
\n\
|
||||
<b>If your internet connection is bad, it may cause web pages to load slowly or fail to load altogether. You may try again later or switch to a different internet connection.</b>
|
||||
account.methods.microsoft.methods.device=Log In with QR Code
|
||||
account.methods.microsoft.methods.device.hint=Scan QR code or visit <a href="%s">%s</a> to complete login, enter <b>%s</b> in the opened page.
|
||||
account.methods.microsoft.methods.browser=Log In via Browser
|
||||
account.methods.microsoft.methods.browser.hint=Click the "Log in" button or <a href="%s">copy the link</a> and paste it into the browser to log in.
|
||||
account.methods.microsoft.manual=<b>If your internet connection is bad, it may cause web pages to load slowly or fail to load altogether.\nYou may try again later or switch to a different internet connection.</b>
|
||||
account.methods.microsoft.profile=Account Profile
|
||||
account.methods.microsoft.purchase=Buy Minecraft
|
||||
account.methods.microsoft.snapshot=You are using an unofficial build of HMCL. Please download the <a href="https://hmcl.huangyuhui.net/download">official build</a> to log in.
|
||||
@@ -141,7 +141,6 @@ account.methods.offline.uuid.hint=UUID is a unique identifier for Minecraft play
|
||||
\n\
|
||||
This option is for advanced users only. We do not recommend changing this option unless you know what you are doing.
|
||||
account.methods.offline.uuid.malformed=Invalid format.
|
||||
account.methods.forgot_password=Forgot Password
|
||||
account.methods.ban_query=Ban Query
|
||||
account.missing=No Accounts
|
||||
account.missing.add=Click here to add one.
|
||||
@@ -357,13 +356,17 @@ download.failed.empty=No versions are available. Please click here to go back.
|
||||
download.failed.no_code=Failed to download
|
||||
download.failed.refresh=Failed to fetch version list. Please click here to retry.
|
||||
download.game=New Game
|
||||
download.provider.bmclapi=BMCLAPI (bangbang93, https://bmclapi2.bangbang93.com/)
|
||||
download.provider.mojang=Official (OptiFine is provided by BMCLAPI)
|
||||
download.provider.bmclapi=BMCLAPI
|
||||
download.provider.bmclapi.desc=bangbang93, https://bmclapi2.bangbang93.com/
|
||||
download.provider.mojang=Official
|
||||
download.provider.mojang.desc=OptiFine is provided by BMCLAPI
|
||||
download.provider.official=From Official Sources
|
||||
download.provider.official.desc=Latest, but may load slowly
|
||||
download.provider.balanced=From Fastest Available
|
||||
download.provider.balanced.desc=Balanced, but may not be the latest
|
||||
download.provider.mirror=From Mirror
|
||||
download.provider.mirror.desc=Fast, but may not be the latest
|
||||
download.java=Downloading Java
|
||||
download.java.override=This Java version already exists. Do you want to uninstall and reinstall it?
|
||||
download.java.process=Java Download Process
|
||||
download.javafx=Downloading dependencies for the launcher...
|
||||
download.javafx.notes=We are currently downloading dependencies for HMCL from the Internet.\n\
|
||||
@@ -385,12 +388,18 @@ exception.ssl_handshake=Failed to establish SSL connection because the SSL certi
|
||||
exception.dns.pollution=Failed to establish an SSL connection. DNS resolution may be incorrect. Please try changing your DNS server or using a proxy service.
|
||||
|
||||
extension.bat=Windows Batch File
|
||||
extension.mod=Mod File
|
||||
extension.png=Image File
|
||||
extension.ps1=Windows PowerShell Script
|
||||
extension.sh=Shell Script
|
||||
extension.command=macOS Shell Script
|
||||
|
||||
extension.datapack=Datapack Archive
|
||||
extension.mod=Mod File
|
||||
extension.modloader.installer=Mod Loader Installer
|
||||
extension.resourcepack=Resource Pack Archive
|
||||
extension.schematic=Schematic File
|
||||
extension.world=World Archive
|
||||
|
||||
fatal.create_hmcl_current_directory_failure=Hello Minecraft! Launcher cannot create the HMCL directory (%s). Please move HMCL to another location and reopen it.
|
||||
fatal.javafx.incompatible=Missing JavaFX environment.\n\
|
||||
Hello Minecraft! Launcher cannot automatically install JavaFX on Java <11.\n\
|
||||
@@ -436,7 +445,7 @@ fatal.samba=If you opened Hello Minecraft! Launcher from a Samba network drive,
|
||||
fatal.illegal_char=Your user path contains an illegal character "=". You will not be able to use authlib-injector or change the skin of your offline account.
|
||||
fatal.unsupported_platform=Minecraft is not fully supported on your platform yet, so you may experience missing features or even be unable to launch the game.\n\
|
||||
\n\
|
||||
If you cannot launch Minecraft 1.17 and later, you can try switching the "Renderer" to "Software" in "Global/Instance-specific Settings → Advanced Settings" to use CPU rendering for better compatibility.
|
||||
If you cannot launch Minecraft 1.17 and later, you can try switching the "Renderer" to "Mesa LLVMpipe (Software)" in "Global/Instance-specific Settings → Advanced Settings" to use CPU rendering for better compatibility.
|
||||
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher has provided support for the Loongson platform.\n\
|
||||
If you encounter problems when playing a game, you can visit https://docs.hmcl.net/groups.html for help.
|
||||
fatal.unsupported_platform.macos_arm64=Hello Minecraft! Launcher has provided support for the Apple silicon platform, using native ARM Java to launch games to get a smoother gaming experience.\n\
|
||||
@@ -454,7 +463,7 @@ folder.logs=Logs
|
||||
folder.mod=Mods
|
||||
folder.resourcepacks=Resource Packs
|
||||
folder.shaderpacks=Shader Packs
|
||||
folder.saves=Saves
|
||||
folder.saves=Worlds
|
||||
folder.schematics=Schematics
|
||||
folder.screenshots=Screenshots
|
||||
folder.world=World Directory
|
||||
@@ -467,7 +476,7 @@ game.crash.reason.analyzing=Analyzing...
|
||||
game.crash.reason.multiple=Several reasons detected:\n\n
|
||||
game.crash.reason.block=The game crashed because of a block in the world.\n\
|
||||
\n\
|
||||
You can try removing this block using MCEdit or deleting the mod that added it.\n\
|
||||
You can try removing this block using the <a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">map editors</a> or deleting the mod that added it.\n\
|
||||
\n\
|
||||
Block Type: %1$s\n\
|
||||
Location: %2$s
|
||||
@@ -483,7 +492,7 @@ game.crash.reason.duplicated_mod=The game cannot continue to run because of dupl
|
||||
Each mod can only be installed once. Please delete the duplicate mod and try again.
|
||||
game.crash.reason.entity=The game crashed because of an entity in the world.\n\
|
||||
\n\
|
||||
You can try removing the entity using MCEdit or deleting the mod that added it.\n\
|
||||
You can try removing the entity using the <a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">map editors</a> or deleting the mod that added it.\n\
|
||||
\n\
|
||||
Entity Type: %1$s\n\
|
||||
Location: %2$s
|
||||
@@ -728,10 +737,9 @@ install.installer.game=Minecraft
|
||||
install.installer.incompatible=Incompatible with %s
|
||||
install.installer.install=Installing %s
|
||||
install.installer.install_offline=Install/Update from Local File
|
||||
install.installer.install_offline.extension=(Neo)Forge/Cleanroom/OptiFine installer
|
||||
install.installer.install_offline.tooltip=We support using the local (Neo)Forge, Cleanroom, and OptiFine installer.
|
||||
install.installer.install_online=Online Install
|
||||
install.installer.install_online.tooltip=We currently support Forge, NeoForge, Cleanroom, OptiFine, Fabric, Quilt, and LiteLoader.
|
||||
install.installer.install_online.tooltip=We currently support Forge, NeoForge, Cleanroom, OptiFine, Fabric, Legacy Fabric, Quilt, and LiteLoader.
|
||||
install.installer.liteloader=LiteLoader
|
||||
install.installer.not_installed=Not installed
|
||||
install.installer.optifine=OptiFine
|
||||
@@ -751,17 +759,18 @@ install.new_game.malformed=Invalid name.
|
||||
install.select=Choose operation
|
||||
install.success=Successfully installed.
|
||||
|
||||
java.add=Add Java
|
||||
java.add=Add
|
||||
java.add.failed=This Java is invalid or incompatible with the current platform.
|
||||
java.disable=Disable Java
|
||||
java.disable.confirm=Are you sure you want to disable this Java?
|
||||
java.disabled.management=Disabled Java
|
||||
java.disabled.management.remove=Remove this Java from the list
|
||||
java.disabled.management.restore=Re-enable this Java
|
||||
java.download=Download Java
|
||||
java.download=Download
|
||||
java.download.banshanjdk-8=Download Banshan JDK 8
|
||||
java.download.load_list.failed=Failed to load version list
|
||||
java.download.more=More Java distributions
|
||||
java.download.title=Download Java
|
||||
java.download.prompt=Please choose the Java version you want to download:
|
||||
java.download.distribution=Distribution
|
||||
java.download.version=Version
|
||||
@@ -1058,9 +1067,10 @@ modrinth.category.256x=256x
|
||||
modrinth.category.512x+=512x+
|
||||
|
||||
mods=Mods
|
||||
mods.add=Add Mod
|
||||
mods.add=Add
|
||||
mods.add.failed=Failed to add mod %s.
|
||||
mods.add.success=%s was successfully added.
|
||||
mods.add.title=Choose mod file you want to add
|
||||
mods.broken_dependency.title=Broken dependency
|
||||
mods.broken_dependency.desc=This dependency existed before, but it does not exist anymore. Try using another download source.
|
||||
mods.category=Category
|
||||
@@ -1077,7 +1087,6 @@ mods.check_updates.failed_download=Failed to download some files.
|
||||
mods.check_updates.file=File
|
||||
mods.check_updates.source=Source
|
||||
mods.check_updates.target_version=Target Version
|
||||
mods.choose_mod=Choose mod
|
||||
mods.curseforge=CurseForge
|
||||
mods.dependency.embedded=Built-in Dependencies (Already packaged in the mod file by the author. No need to download separately)
|
||||
mods.dependency.optional=Optional Dependencies (If missing, the game will run normally, but the mod features may be missing)
|
||||
@@ -1099,7 +1108,7 @@ mods.mcmod.page=MCMod Page
|
||||
mods.mcmod.search=Search in MCMod
|
||||
mods.modrinth=Modrinth
|
||||
mods.name=Name
|
||||
mods.not_modded=You must install a modloader (Forge, NeoForge, Fabric, Quilt, or LiteLoader) first to manage your mods!
|
||||
mods.not_modded=You must install a modloader (Forge, NeoForge, Fabric, Legacy Fabric, Quilt, or LiteLoader) first to manage your mods!
|
||||
mods.restore=Restore
|
||||
mods.url=Official Page
|
||||
mods.update_modpack_mod.warning=Updating mods in a modpack can lead to irreparable results, possibly corrupting the modpack so that it cannot launch. Are you sure you want to update?
|
||||
@@ -1122,9 +1131,9 @@ nbt.save.failed=Failed to save file
|
||||
nbt.title=View File - %s
|
||||
|
||||
datapack=Datapacks
|
||||
datapack.add=Install Datapack
|
||||
datapack.choose_datapack=Choose datapack to import
|
||||
datapack.extension=Datapack
|
||||
datapack.add=Add
|
||||
datapack.add.title=Choose datapack archive you want to add
|
||||
datapack.reload.toast=Minecraft is running, please use the /reload command to reload the data pack
|
||||
datapack.title=World [%s] - Datapacks
|
||||
|
||||
web.failed=Failed to load page
|
||||
@@ -1132,9 +1141,13 @@ web.open_in_browser=Do you want to open this address in a browser:\n%s
|
||||
web.view_in_browser=View all in browser
|
||||
|
||||
world=Worlds
|
||||
world.add=Add World
|
||||
world.add=Add
|
||||
world.add.already_exists=This world already exists.
|
||||
world.add.failed=Failed to add this world: %s
|
||||
world.add.invalid=Failed to parse the world.
|
||||
world.add.title=Choose world archive you want to add
|
||||
world.backup=World Backup
|
||||
world.backup.create.new_one=Create New Backup
|
||||
world.backup.create.new_one=New Backup
|
||||
world.backup.create.failed=Failed to create backup.\n%s
|
||||
world.backup.create.success=Successfully created a new backup: %s
|
||||
world.backup.delete=Delete this backup
|
||||
@@ -1155,13 +1168,12 @@ world.datapack=Datapacks
|
||||
world.datetime=Last played on %s
|
||||
world.delete=Delete the World
|
||||
world.delete.failed=Failed to delete world.\n%s
|
||||
world.download=Download World
|
||||
world.download=Download
|
||||
world.download.title=Download World - %1s
|
||||
world.export=Export the World
|
||||
world.export.title=Choose the directory for this exported world
|
||||
world.export.location=Save As
|
||||
world.export.wizard=Export World "%s"
|
||||
world.extension=World Archive
|
||||
world.game_version=Game Version
|
||||
world.icon=World Icon
|
||||
world.icon.change=Change world icon
|
||||
@@ -1172,10 +1184,6 @@ world.icon.change.fail.not_64x64.text=The image resolution is %d×%d instead of
|
||||
world.icon.change.succeed.toast=Successfully updated the world icon.
|
||||
world.icon.change.tip=A 64×64 PNG image is required. Images with an incorrect resolution cannot be parsed by Minecraft.
|
||||
world.icon.choose.title=Select world icon
|
||||
world.import.already_exists=This world already exists.
|
||||
world.import.choose=Choose world archive you want to import
|
||||
world.import.failed=Failed to import this world: %s
|
||||
world.import.invalid=Failed to parse the world.
|
||||
world.info=World Information
|
||||
world.info.basic=Basic Information
|
||||
world.info.allow_cheats=Allow Commands/Cheats
|
||||
@@ -1193,6 +1201,7 @@ world.info.last_played=Last Played
|
||||
world.info.generate_features=Generate Structures
|
||||
world.info.player=Player Information
|
||||
world.info.player.food_level=Hunger Level
|
||||
world.info.player.food_saturation_level=Saturation
|
||||
world.info.player.game_type=Game Mode
|
||||
world.info.player.game_type.adventure=Adventure
|
||||
world.info.player.game_type.creative=Creative
|
||||
@@ -1205,8 +1214,9 @@ world.info.player.location=Location
|
||||
world.info.player.spawn=Spawn Location
|
||||
world.info.player.xp_level=Experience Level
|
||||
world.info.random_seed=Seed
|
||||
world.info.time=Game Time
|
||||
world.info.time.format=%s days
|
||||
world.info.spawn=World Spawn Location
|
||||
world.info.time=Played Time
|
||||
world.info.time.format=%dd %dh %dm
|
||||
world.load.fail=Failed to load world
|
||||
world.locked=In use
|
||||
world.locked.failed=The world is currently in use. Please close the game and try again.
|
||||
@@ -1242,6 +1252,7 @@ repositories.chooser.title=Choose download source for JavaFX
|
||||
|
||||
resourcepack=Resource Packs
|
||||
resourcepack.add=Add
|
||||
resourcepack.add.title=Choose resource pack archive you want to add
|
||||
resourcepack.manage=Resource Packs
|
||||
resourcepack.download=Download
|
||||
resourcepack.add.failed=Failed to add resource pack
|
||||
@@ -1251,10 +1262,11 @@ resourcepack.download.title=Download Resource Pack - %1s
|
||||
reveal.in_file_manager=Reveal in File Manager
|
||||
|
||||
schematics=Schematics
|
||||
schematics.add=Add Schematic Files
|
||||
schematics.add=Add
|
||||
schematics.add.failed=Failed to add schematic files
|
||||
schematics.add.title=Choose schematic file you want to import
|
||||
schematics.back_to=Back to "%s"
|
||||
schematics.create_directory=Create Directory
|
||||
schematics.create_directory=New Directory
|
||||
schematics.create_directory.prompt=Please enter the new directory name
|
||||
schematics.create_directory.failed=Failed to create directory
|
||||
schematics.create_directory.failed.already_exists=Directory already exists
|
||||
@@ -1343,17 +1355,22 @@ settings.advanced.precall_command=Pre-launch Command
|
||||
settings.advanced.precall_command.prompt=Commands to execute before the game launches
|
||||
settings.advanced.process_priority=Process Priority
|
||||
settings.advanced.process_priority.low=Low
|
||||
settings.advanced.process_priority.low.desc=
|
||||
settings.advanced.process_priority.below_normal=Below Normal
|
||||
settings.advanced.process_priority.below_normal.desc=
|
||||
settings.advanced.process_priority.normal=Normal
|
||||
settings.advanced.process_priority.normal.desc=
|
||||
settings.advanced.process_priority.above_normal=Above Normal
|
||||
settings.advanced.process_priority.above_normal.desc=
|
||||
settings.advanced.process_priority.high=High
|
||||
settings.advanced.process_priority.high.desc=
|
||||
settings.advanced.post_exit_command=Post-exit Command
|
||||
settings.advanced.post_exit_command.prompt=Commands to execute after the game exits
|
||||
settings.advanced.renderer=Renderer
|
||||
settings.advanced.renderer.default=OpenGL (Default)
|
||||
settings.advanced.renderer.d3d12=DirectX 12 (Poor performance and compatibility)
|
||||
settings.advanced.renderer.llvmpipe=Software (Poor performance, best compatibility)
|
||||
settings.advanced.renderer.zink=Vulkan (Best performance, poor compatibility)
|
||||
settings.advanced.renderer.default=Default
|
||||
settings.advanced.renderer.d3d12=Mesa D3D12
|
||||
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe (Software)
|
||||
settings.advanced.renderer.zink=Mesa Zink
|
||||
settings.advanced.server_ip=Server Address
|
||||
settings.advanced.server_ip.prompt=Automatically join after launching the game
|
||||
settings.advanced.unsupported_system_options=Settings not applicable to the current system
|
||||
@@ -1385,9 +1402,9 @@ settings.game.java_directory.template=%1$s (%2$s)
|
||||
settings.game.management=Manage
|
||||
settings.game.working_directory=Working Directory
|
||||
settings.game.working_directory.choose=Choose the working directory
|
||||
settings.game.working_directory.hint=Enable the "Isolated" option in "Working Directory" to allow the current instance to store its settings, saves, and mods in a separate directory.\n\
|
||||
settings.game.working_directory.hint=Enable the "Isolated" option in "Working Directory" to allow the current instance to store its settings, worlds, and mods in a separate directory.\n\
|
||||
\n\
|
||||
It is recommended to enable this option to avoid mod conflicts, but you will need to move your saves manually.
|
||||
It is recommended to enable this option to avoid mod conflicts, but you will need to move your worlds manually.
|
||||
|
||||
settings.icon=Icon
|
||||
|
||||
@@ -1614,7 +1631,7 @@ version.manage=All Instances
|
||||
version.manage.clean=Delete Log Files
|
||||
version.manage.clean.tooltip=Delete the files in "logs" and "crash-reports" directories.
|
||||
version.manage.duplicate=Duplicate Instance
|
||||
version.manage.duplicate.duplicate_save=Duplicate Saves
|
||||
version.manage.duplicate.duplicate_save=Duplicate Worlds
|
||||
version.manage.duplicate.prompt=Enter New Instance Name
|
||||
version.manage.duplicate.confirm=The duplicated instance will have a copy of all files in the instance directory (".minecraft/versions/<instance name>"), with an isolated working directory and settings.
|
||||
version.manage.manage=Edit Instance
|
||||
@@ -1622,7 +1639,7 @@ version.manage.manage.title=Edit Instance - %1s
|
||||
version.manage.redownload_assets_index=Update Game Assets
|
||||
version.manage.remove=Delete Instance
|
||||
version.manage.remove.confirm.trash=Are you sure you want to remove the instance "%1$s"? You can still find its files in your recycle bin by the name of "%2$s".
|
||||
version.manage.remove.confirm.independent=Since this instance is stored in an isolated directory, deleting it will also delete its saves and other data. Do you still want to delete the instance "%s"?
|
||||
version.manage.remove.confirm.independent=Since this instance is stored in an isolated directory, deleting it will also delete its worlds and other data. Do you still want to delete the instance "%s"?
|
||||
version.manage.remove.failed=Failed to delete the instance. Some files might be in use.
|
||||
version.manage.remove_assets=Delete All Assets
|
||||
version.manage.remove_libraries=Delete All Libraries
|
||||
|
||||
Loaded 100 of 161 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user