重构 Decorator (#6557)
This commit is contained in:
1 parent
612998a98f
commit
5a14449b44
12 files changed
+1489
-1247
No files matched your search
@@ -24,18 +24,11 @@ import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.ReadOnlyDoubleProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.geometry.Rectangle2D;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.ButtonBase;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.*;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
@@ -47,12 +40,11 @@ import org.jackhuang.hmcl.setting.*;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.account.AccountListPage;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorController;
|
||||
import org.jackhuang.hmcl.ui.decorator.Decorator;
|
||||
import org.jackhuang.hmcl.ui.download.DownloadPage;
|
||||
import org.jackhuang.hmcl.ui.main.LauncherSettingsPage;
|
||||
import org.jackhuang.hmcl.ui.main.RootPage;
|
||||
@@ -74,6 +66,7 @@ import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
|
||||
@@ -89,26 +82,11 @@ public final class Controllers {
|
||||
public static final String SOFTWARE_RENDERING = "softwareRendering";
|
||||
public static final String APRIL_FOOLS = "aprilFools";
|
||||
|
||||
private static final int CUSTOM_DECORATION_SHADOW_SIZE = 8;
|
||||
private static final int CUSTOM_DECORATION_SHADOW_EXTENT = CUSTOM_DECORATION_SHADOW_SIZE * 2;
|
||||
|
||||
public static final int MIN_CONTENT_WIDTH = 800 + 2; // bg width + border width*2
|
||||
public static final int MIN_CONTENT_HEIGHT = 450 + 2 + 40; // bg height + border width*2 + toolbar height
|
||||
public static final int MIN_WIDTH = MIN_CONTENT_WIDTH + CUSTOM_DECORATION_SHADOW_EXTENT;
|
||||
public static final int MIN_HEIGHT = MIN_CONTENT_HEIGHT + CUSTOM_DECORATION_SHADOW_EXTENT;
|
||||
public static final Rectangle2D PRIMARY_SCREEN_BOUNDS = Screen.getPrimary().getBounds();
|
||||
private static InvalidationListener stageSizeChangeListener;
|
||||
private static final DoubleProperty contentX = new SimpleDoubleProperty();
|
||||
private static final DoubleProperty contentY = new SimpleDoubleProperty();
|
||||
private static final DoubleProperty contentWidth = new SimpleDoubleProperty();
|
||||
private static final DoubleProperty contentHeight = new SimpleDoubleProperty();
|
||||
|
||||
private static Scene scene;
|
||||
private static Stage stage;
|
||||
private static GameInstancePage gameInstancePage;
|
||||
private static Lazy<GameListPage> gameListPage = new Lazy<>(GameListPage::new);
|
||||
private static Lazy<RootPage> rootPage = new Lazy<>(RootPage::new);
|
||||
private static DecoratorController decorator;
|
||||
/// The coordinator for the main window's scene graph and navigation stack.
|
||||
private static @Nullable Decorator decorator;
|
||||
private static DownloadPage downloadPage;
|
||||
private static Lazy<AccountListPage> accountListPage = new Lazy<>(() -> {
|
||||
AccountListPage accountListPage = new AccountListPage();
|
||||
@@ -132,16 +110,12 @@ public final class Controllers {
|
||||
void run() throws Exception;
|
||||
}
|
||||
|
||||
/// Returns the stage currently attached to the main-window decorator.
|
||||
///
|
||||
/// @return the current stage, or `null` before initialization or after detachment
|
||||
public static @Nullable Stage getStage() {
|
||||
return stage;
|
||||
}
|
||||
|
||||
public static ReadOnlyDoubleProperty windowWidthProperty() {
|
||||
return contentWidth;
|
||||
}
|
||||
|
||||
public static ReadOnlyDoubleProperty windowHeightProperty() {
|
||||
return contentHeight;
|
||||
@Nullable Decorator currentDecorator = decorator;
|
||||
return currentDecorator == null ? null : currentDecorator.getStage();
|
||||
}
|
||||
|
||||
@FXThread
|
||||
@@ -212,15 +186,24 @@ public final class Controllers {
|
||||
return terracottaPage.get();
|
||||
}
|
||||
|
||||
/// Returns the initialized main-window decorator.
|
||||
///
|
||||
/// @return the application-wide main-window decorator
|
||||
@FXThread
|
||||
public static DecoratorController getDecorator() {
|
||||
return decorator;
|
||||
public static Decorator getDecorator() {
|
||||
return Objects.requireNonNull(decorator, "Main window is not initialized");
|
||||
}
|
||||
|
||||
/// Releases stage-specific listeners and retained window ownership before application shutdown.
|
||||
public static void onApplicationStop() {
|
||||
stageSizeChangeListener = null;
|
||||
if (decorator != null) {
|
||||
decorator.detachStage();
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the main application stage, scene graph, and background services.
|
||||
///
|
||||
/// @param stage the primary application stage, which must not have been shown
|
||||
public static void initialize(Stage stage) {
|
||||
LOG.info("Start initializing application");
|
||||
|
||||
@@ -238,99 +221,10 @@ public final class Controllers {
|
||||
}
|
||||
}
|
||||
|
||||
Controllers.stage = stage;
|
||||
|
||||
double initContentWidth = Math.max(MIN_CONTENT_WIDTH, state().getWidth());
|
||||
double initContentHeight = Math.max(MIN_CONTENT_HEIGHT, state().getHeight());
|
||||
double initWidth = initContentWidth + CUSTOM_DECORATION_SHADOW_EXTENT;
|
||||
double initHeight = initContentHeight + CUSTOM_DECORATION_SHADOW_EXTENT;
|
||||
|
||||
{
|
||||
double initContentX = state().getX() * PRIMARY_SCREEN_BOUNDS.getWidth();
|
||||
double initContentY = state().getY() * PRIMARY_SCREEN_BOUNDS.getHeight();
|
||||
|
||||
boolean invalid = true;
|
||||
double border = 20D;
|
||||
for (Screen screen : Screen.getScreens()) {
|
||||
Rectangle2D bound = screen.getBounds();
|
||||
|
||||
if (bound.getMinX() + border <= initContentX + initContentWidth
|
||||
&& initContentX <= bound.getMaxX() - border
|
||||
&& bound.getMinY() + border <= initContentY
|
||||
&& initContentY <= bound.getMaxY() - border) {
|
||||
invalid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (invalid) {
|
||||
initContentX = (0.5D - initContentWidth / PRIMARY_SCREEN_BOUNDS.getWidth() / 2)
|
||||
* PRIMARY_SCREEN_BOUNDS.getWidth();
|
||||
initContentY = (0.5D - initContentHeight / PRIMARY_SCREEN_BOUNDS.getHeight() / 2)
|
||||
* PRIMARY_SCREEN_BOUNDS.getHeight();
|
||||
}
|
||||
|
||||
double initX = initContentX - CUSTOM_DECORATION_SHADOW_SIZE;
|
||||
double initY = initContentY - CUSTOM_DECORATION_SHADOW_SIZE;
|
||||
stage.setX(initX);
|
||||
stage.setY(initY);
|
||||
contentX.set(initContentX);
|
||||
contentY.set(initContentY);
|
||||
}
|
||||
|
||||
stage.setHeight(initHeight);
|
||||
stage.setWidth(initWidth);
|
||||
contentHeight.set(initContentHeight);
|
||||
contentWidth.set(initContentWidth);
|
||||
|
||||
stageSizeChangeListener = o -> {
|
||||
ReadOnlyDoubleProperty property = (ReadOnlyDoubleProperty) o;
|
||||
Stage currentStage = property.getBean() instanceof Stage s ? s : null;
|
||||
if (currentStage == null)
|
||||
return;
|
||||
|
||||
boolean saveState = !currentStage.isIconified()
|
||||
// https://github.com/HMCL-dev/HMCL/issues/4290
|
||||
&& (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
|
||||
|| !currentStage.isFullScreen() && !currentStage.isMaximized());
|
||||
|
||||
switch (property.getName()) {
|
||||
case "x" -> {
|
||||
double value = property.get() + CUSTOM_DECORATION_SHADOW_SIZE;
|
||||
contentX.set(value);
|
||||
if (saveState)
|
||||
state().setX(value / PRIMARY_SCREEN_BOUNDS.getWidth());
|
||||
}
|
||||
case "y" -> {
|
||||
double value = property.get() + CUSTOM_DECORATION_SHADOW_SIZE;
|
||||
contentY.set(value);
|
||||
if (saveState)
|
||||
state().setY(value / PRIMARY_SCREEN_BOUNDS.getHeight());
|
||||
}
|
||||
case "width" -> {
|
||||
double value = Math.max(MIN_CONTENT_WIDTH, property.get() - CUSTOM_DECORATION_SHADOW_EXTENT);
|
||||
contentWidth.set(value);
|
||||
if (saveState)
|
||||
state().setWidth(value);
|
||||
}
|
||||
case "height" -> {
|
||||
double value = Math.max(MIN_CONTENT_HEIGHT, property.get() - CUSTOM_DECORATION_SHADOW_EXTENT);
|
||||
contentHeight.set(value);
|
||||
if (saveState)
|
||||
state().setHeight(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
WeakInvalidationListener weakListener = new WeakInvalidationListener(stageSizeChangeListener);
|
||||
stage.xProperty().addListener(weakListener);
|
||||
stage.yProperty().addListener(weakListener);
|
||||
stage.heightProperty().addListener(weakListener);
|
||||
stage.widthProperty().addListener(weakListener);
|
||||
|
||||
stage.setOnCloseRequest(e -> Launcher.stopApplication());
|
||||
|
||||
decorator = new DecoratorController(stage, getRootPage());
|
||||
decorator = new Decorator(getRootPage());
|
||||
Scene mainScene = decorator.attachStage(stage);
|
||||
getRootPage().getMainPage().showUpdateProperty().bind(UpdateChecker.checkingUpdateProperty().not().and(UpdateChecker.outdatedProperty()));
|
||||
getRootPage().getMainPage().showUpdateDialogProperty().bind(
|
||||
decorator.backableProperty().not()
|
||||
@@ -346,36 +240,10 @@ public final class Controllers {
|
||||
|
||||
Lang.thread(JavaManager::initialize, "Search Java", true);
|
||||
|
||||
scene = new Scene(decorator.getDecorator());
|
||||
scene.setFill(Color.TRANSPARENT);
|
||||
stage.setMinWidth(MIN_WIDTH);
|
||||
stage.setMinHeight(MIN_HEIGHT);
|
||||
decorator.getDecorator().prefWidthProperty().bind(scene.widthProperty());
|
||||
decorator.getDecorator().prefHeightProperty().bind(scene.heightProperty());
|
||||
StyleSheets.init(scene);
|
||||
StyleSheets.init(mainScene);
|
||||
|
||||
FXUtils.setIcon(stage);
|
||||
stage.setTitle(Metadata.FULL_TITLE);
|
||||
stage.initStyle(StageStyle.TRANSPARENT);
|
||||
stage.setScene(scene);
|
||||
|
||||
if (AnimationUtils.playWindowAnimation()) {
|
||||
Timeline timeline = new Timeline(
|
||||
new KeyFrame(Duration.millis(0),
|
||||
new KeyValue(decorator.getDecorator().opacityProperty(), 0, Motion.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleXProperty(), 0.8, Motion.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleYProperty(), 0.8, Motion.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleZProperty(), 0.8, Motion.EASE)
|
||||
),
|
||||
new KeyFrame(Duration.millis(600),
|
||||
new KeyValue(decorator.getDecorator().opacityProperty(), 1, Motion.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleXProperty(), 1, Motion.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleYProperty(), 1, Motion.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleZProperty(), 1, Motion.EASE)
|
||||
)
|
||||
);
|
||||
timeline.play();
|
||||
}
|
||||
|
||||
if (!Architecture.SYSTEM_ARCH.isX86() && SettingsManager.userState().platformPromptVersionProperty().get() < 1) {
|
||||
Runnable continueAction = () -> {
|
||||
@@ -644,20 +512,36 @@ public final class Controllers {
|
||||
decorator.showToast(content);
|
||||
}
|
||||
|
||||
/// Shows `directoryChooser` with the current main window as its owner.
|
||||
///
|
||||
/// @param directoryChooser the chooser to show
|
||||
/// @return the selected directory, or `null` if the chooser is cancelled
|
||||
public static @Nullable Path showDialog(DirectoryChooser directoryChooser) {
|
||||
return FileUtils.toPath(directoryChooser.showDialog(stage));
|
||||
return FileUtils.toPath(directoryChooser.showDialog(getStage()));
|
||||
}
|
||||
|
||||
/// Shows `fileChooser` for opening one file with the current main window as its owner.
|
||||
///
|
||||
/// @param fileChooser the chooser to show
|
||||
/// @return the selected file, or `null` if the chooser is cancelled
|
||||
public static @Nullable Path showOpenDialog(FileChooser fileChooser) {
|
||||
return FileUtils.toPath(fileChooser.showOpenDialog(stage));
|
||||
return FileUtils.toPath(fileChooser.showOpenDialog(getStage()));
|
||||
}
|
||||
|
||||
/// Shows `fileChooser` for saving one file with the current main window as its owner.
|
||||
///
|
||||
/// @param fileChooser the chooser to show
|
||||
/// @return the selected file, or `null` if the chooser is cancelled
|
||||
public static @Nullable Path showSaveDialog(FileChooser fileChooser) {
|
||||
return FileUtils.toPath(fileChooser.showSaveDialog(stage));
|
||||
return FileUtils.toPath(fileChooser.showSaveDialog(getStage()));
|
||||
}
|
||||
|
||||
/// Shows `fileChooser` for opening multiple files with the current main window as its owner.
|
||||
///
|
||||
/// @param fileChooser the chooser to show
|
||||
/// @return the selected files, or `null` if the chooser is cancelled
|
||||
public static @Nullable List<Path> showOpenMultipleDialog(FileChooser fileChooser) {
|
||||
return FileUtils.toPaths(fileChooser.showOpenMultipleDialog(stage));
|
||||
return FileUtils.toPaths(fileChooser.showOpenMultipleDialog(getStage()));
|
||||
}
|
||||
|
||||
public static void onHyperlinkAction(String href) {
|
||||
@@ -681,7 +565,9 @@ public final class Controllers {
|
||||
return decorator == null;
|
||||
}
|
||||
|
||||
/// Releases controller-owned pages, window state, and JavaFX helper resources.
|
||||
public static void shutdown() {
|
||||
onApplicationStop();
|
||||
rootPage = null;
|
||||
gameInstancePage = null;
|
||||
gameListPage = null;
|
||||
@@ -690,9 +576,6 @@ public final class Controllers {
|
||||
settingsPage = null;
|
||||
terracottaPage = null;
|
||||
decorator = null;
|
||||
stage = null;
|
||||
scene = null;
|
||||
onApplicationStop();
|
||||
|
||||
FXUtils.shutdown();
|
||||
}
|
||||
|
||||
@@ -49,17 +49,31 @@ public final class DialogUtils {
|
||||
|
||||
public static final String PROPERTY_DIALOG_SHOW_LATER = DialogUtils.class.getName() + ".dialog.showLater";
|
||||
|
||||
/// Shows content in the main-window dialog stack owned by `decorator`.
|
||||
///
|
||||
/// @param decorator the main-window decorator
|
||||
/// @param content the dialog content
|
||||
public static void show(Decorator decorator, Node content) {
|
||||
if (decorator.getDrawerWrapper() == null) {
|
||||
Platform.runLater(() -> show(decorator, content));
|
||||
StackPane dialogContainer = decorator.getDialogContainer();
|
||||
if (decorator.getRoot().getScene() == null) {
|
||||
Platform.runLater(() -> showInDecorator(decorator, dialogContainer, content));
|
||||
return;
|
||||
}
|
||||
|
||||
show(decorator.getDrawerWrapper(), content, (dialog) -> {
|
||||
showInDecorator(decorator, dialogContainer, content);
|
||||
}
|
||||
|
||||
/// Shows content in a decorator's resolved dialog container.
|
||||
///
|
||||
/// @param decorator the main-window decorator
|
||||
/// @param dialogContainer the container resolved before any deferred execution
|
||||
/// @param content the dialog content
|
||||
private static void showInDecorator(Decorator decorator, StackPane dialogContainer, Node content) {
|
||||
show(dialogContainer, content, dialog -> {
|
||||
JFXDialogPane pane = (JFXDialogPane) dialog.getContent();
|
||||
decorator.capableDraggingWindow(dialog);
|
||||
decorator.forbidDraggingWindow(pane);
|
||||
dialog.setDialogContainer(decorator.getDrawerWrapper());
|
||||
dialog.setDialogContainer(dialogContainer);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -125,12 +139,18 @@ public final class DialogUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/// Queues content in the main-window dialog stack owned by `decorator`.
|
||||
///
|
||||
/// @param decorator the main-window decorator
|
||||
/// @param content the dialog content
|
||||
public static void showLater(Decorator decorator, Node content) {
|
||||
if (decorator.getDrawerWrapper() == null) {
|
||||
Platform.runLater(() -> showLater(decorator, content));
|
||||
StackPane dialogContainer = decorator.getDialogContainer();
|
||||
Runnable showDialogAction = () -> showInDecorator(decorator, dialogContainer, content);
|
||||
if (decorator.getRoot().getScene() == null) {
|
||||
Platform.runLater(() -> showLater(dialogContainer, showDialogAction));
|
||||
return;
|
||||
}
|
||||
showLater(decorator.getDrawerWrapper(), () -> show(decorator, content));
|
||||
showLater(dialogContainer, showDialogAction);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
|
||||
@@ -44,8 +44,8 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
public final class UpgradeDialog extends JFXDialogLayout {
|
||||
|
||||
public UpgradeDialog(RemoteVersion remoteVersion, Runnable updateRunnable) {
|
||||
maxWidthProperty().bind(Controllers.windowWidthProperty().multiply(0.7));
|
||||
maxHeightProperty().bind(Controllers.windowHeightProperty().multiply(0.7));
|
||||
maxWidthProperty().bind(Controllers.getDecorator().contentWidthProperty().multiply(0.7));
|
||||
maxHeightProperty().bind(Controllers.getDecorator().contentHeightProperty().multiply(0.7));
|
||||
|
||||
setHeading(new Label(i18n("update.changelog")));
|
||||
setBody(new JFXSpinner());
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,253 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <[email protected]> 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.decorator;
|
||||
|
||||
import com.jfoenix.controls.JFXSnackbar;
|
||||
import com.jfoenix.controls.JFXSnackbarLayout;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDnD;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.DialogUtils;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.account.AddAuthlibInjectorServerPane;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane.AnimationProducer;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.Navigator;
|
||||
import org.jackhuang.hmcl.ui.wizard.Refreshable;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
|
||||
public class DecoratorController {
|
||||
private final Decorator decorator;
|
||||
private final Navigator navigator;
|
||||
|
||||
public DecoratorController(Stage stage, Node mainPage) {
|
||||
decorator = new Decorator(stage);
|
||||
decorator.titleTransparentProperty().bind(Themes.titleBarTransparentProperty());
|
||||
|
||||
navigator = new Navigator();
|
||||
navigator.setOnNavigated(this::onNavigated);
|
||||
navigator.init(mainPage);
|
||||
|
||||
decorator.getContent().setAll(navigator);
|
||||
decorator.onCloseNavButtonActionProperty().set(e -> close());
|
||||
decorator.onBackNavButtonActionProperty().set(e -> back());
|
||||
decorator.onRefreshNavButtonActionProperty().set(e -> refresh());
|
||||
|
||||
setupAuthlibInjectorDnD();
|
||||
|
||||
decorator.contentBackgroundProperty().bind(Themes.backgroundProperty());
|
||||
|
||||
// pass key events to current dialog / current page
|
||||
decorator.addEventFilter(KeyEvent.ANY, e -> {
|
||||
if (!(e.getTarget() instanceof Node t)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Node newTarget;
|
||||
|
||||
JFXDialogPane currentDialogPane = null;
|
||||
if (decorator.getDrawerWrapper() != null) {
|
||||
currentDialogPane = (JFXDialogPane) decorator.getDrawerWrapper().getProperties().get(DialogUtils.PROPERTY_DIALOG_PANE_INSTANCE);
|
||||
}
|
||||
|
||||
if (currentDialogPane != null && currentDialogPane.peek().isPresent()) {
|
||||
newTarget = currentDialogPane.peek().get();
|
||||
} else {
|
||||
newTarget = navigator.getCurrentPage();
|
||||
}
|
||||
boolean needsRedirect = true;
|
||||
|
||||
while (t != null) {
|
||||
if (t == newTarget) {
|
||||
// current event target is in newTarget
|
||||
needsRedirect = false;
|
||||
break;
|
||||
}
|
||||
t = t.getParent();
|
||||
}
|
||||
if (!needsRedirect) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.consume();
|
||||
newTarget.fireEvent(e.copyFor(e.getSource(), newTarget));
|
||||
});
|
||||
|
||||
// press ESC to go back
|
||||
onEscPressed(navigator, this::back);
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/4290
|
||||
if (OperatingSystem.CURRENT_OS != OperatingSystem.MACOS) {
|
||||
// press F11 to toggle full screen
|
||||
navigator.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
|
||||
if (e.getCode() == KeyCode.F11) {
|
||||
stage.setFullScreen(!stage.isFullScreen());
|
||||
e.consume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
navigator.addEventFilter(MouseEvent.MOUSE_CLICKED, e -> {
|
||||
if (e.getButton() == MouseButton.BACK) {
|
||||
back();
|
||||
e.consume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Decorator getDecorator() {
|
||||
return decorator;
|
||||
}
|
||||
|
||||
// ==== Navigation ====
|
||||
|
||||
public void navigate(Node node, AnimationProducer animationProducer, Duration duration, Interpolator interpolator) {
|
||||
navigator.navigate(node, animationProducer, duration, interpolator);
|
||||
}
|
||||
|
||||
public BooleanProperty backableProperty() {
|
||||
return navigator.backableProperty();
|
||||
}
|
||||
|
||||
private void close() {
|
||||
if (navigator.getCurrentPage() instanceof DecoratorPage) {
|
||||
DecoratorPage page = (DecoratorPage) navigator.getCurrentPage();
|
||||
|
||||
if (page.isPageCloseable()) {
|
||||
page.closePage();
|
||||
return;
|
||||
}
|
||||
}
|
||||
navigator.clear();
|
||||
}
|
||||
|
||||
private void back() {
|
||||
if (navigator.getCurrentPage() instanceof DecoratorPage) {
|
||||
DecoratorPage page = (DecoratorPage) navigator.getCurrentPage();
|
||||
|
||||
if (page.back()) {
|
||||
if (navigator.canGoBack()) {
|
||||
navigator.close();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (navigator.canGoBack()) {
|
||||
navigator.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void refresh() {
|
||||
if (navigator.getCurrentPage() instanceof Refreshable) {
|
||||
Refreshable refreshable = (Refreshable) navigator.getCurrentPage();
|
||||
|
||||
if (refreshable.refreshableProperty().get())
|
||||
refreshable.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
private void onNavigated(Navigator.NavigationEvent event) {
|
||||
if (event.getSource() != this.navigator) return;
|
||||
Node to = event.getNode();
|
||||
|
||||
if (to instanceof Refreshable) {
|
||||
decorator.canRefreshProperty().bind(((Refreshable) to).refreshableProperty());
|
||||
} else {
|
||||
decorator.canRefreshProperty().unbind();
|
||||
decorator.canRefreshProperty().set(false);
|
||||
}
|
||||
|
||||
decorator.canCloseProperty().set(navigator.size() > 2);
|
||||
|
||||
if (to instanceof DecoratorPage) {
|
||||
decorator.showCloseAsHomeProperty().set(!((DecoratorPage) to).isPageCloseable());
|
||||
} else {
|
||||
decorator.showCloseAsHomeProperty().set(true);
|
||||
}
|
||||
|
||||
decorator.setNavigationDirection(event.getDirection());
|
||||
|
||||
// state property should be updated at last.
|
||||
if (to instanceof DecoratorPage) {
|
||||
decorator.stateProperty().bind(((DecoratorPage) to).stateProperty());
|
||||
} else {
|
||||
decorator.stateProperty().unbind();
|
||||
decorator.stateProperty().set(new DecoratorPage.State("", null, navigator.canGoBack(), false, true));
|
||||
}
|
||||
|
||||
if (to instanceof Region) {
|
||||
Region region = (Region) to;
|
||||
// Let root pane fix window size.
|
||||
StackPane parent = (StackPane) region.getParent();
|
||||
region.prefWidthProperty().bind(parent.widthProperty());
|
||||
region.prefHeightProperty().bind(parent.heightProperty());
|
||||
}
|
||||
}
|
||||
|
||||
// ==== Dialog ====
|
||||
public void showDialog(Node node) {
|
||||
DialogUtils.show(decorator, node);
|
||||
}
|
||||
|
||||
private void closeDialog(Node node) {
|
||||
DialogUtils.close(node);
|
||||
}
|
||||
|
||||
public void showDialogLater(Node node) {
|
||||
DialogUtils.showLater(decorator, node);
|
||||
}
|
||||
|
||||
// ==== Toast ====
|
||||
|
||||
public void showToast(String content) {
|
||||
decorator.getSnackbar().fireEvent(new JFXSnackbar.SnackbarEvent(new JFXSnackbarLayout(content)));
|
||||
}
|
||||
|
||||
// ==== Wizard ====
|
||||
|
||||
public void startWizard(WizardProvider wizardProvider) {
|
||||
startWizard(wizardProvider, null);
|
||||
}
|
||||
|
||||
public void startWizard(WizardProvider wizardProvider, String category) {
|
||||
FXUtils.checkFxUserThread();
|
||||
|
||||
navigator.navigate(new DecoratorWizardDisplayer(wizardProvider, category),
|
||||
ContainerAnimations.FORWARD, Motion.SHORT4, Motion.EASE);
|
||||
}
|
||||
|
||||
// ==== Authlib Injector DnD ====
|
||||
|
||||
private void setupAuthlibInjectorDnD() {
|
||||
decorator.addEventFilter(DragEvent.DRAG_OVER, AuthlibInjectorDnD.dragOverHandler());
|
||||
decorator.addEventFilter(DragEvent.DRAG_DROPPED, AuthlibInjectorDnD.dragDroppedHandler(
|
||||
url -> Controllers.dialog(new AddAuthlibInjectorServerPane(url))));
|
||||
}
|
||||
}
|
||||
@@ -1,602 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <[email protected]> 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.decorator;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Bounds;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.effect.BlurType;
|
||||
import javafx.scene.effect.DropShadow;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import javafx.util.Duration;
|
||||
import org.glavo.monetfx.ColorRole;
|
||||
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.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.wizard.Navigation;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
private final StackPane root, parent;
|
||||
private final StackPane titleContainer;
|
||||
private final Stage primaryStage;
|
||||
private final TransitionPane navBarPane;
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final InvalidationListener onWindowsStatusChange;
|
||||
private final EventHandler<MouseEvent> onTitleBarDoubleClick;
|
||||
|
||||
private double mouseInitX, mouseInitY, stageInitX, stageInitY, stageInitWidth, stageInitHeight;
|
||||
|
||||
/**
|
||||
* Constructor for all SkinBase instances.
|
||||
*
|
||||
* @param control The control for which this Skin should attach to.
|
||||
*/
|
||||
public DecoratorSkin(Decorator control) {
|
||||
super(control);
|
||||
|
||||
primaryStage = control.getPrimaryStage();
|
||||
|
||||
Decorator skinnable = getSkinnable();
|
||||
root = new StackPane();
|
||||
root.getStyleClass().add("window");
|
||||
|
||||
StackPane shadowContainer = new StackPane();
|
||||
shadowContainer.getStyleClass().add("body");
|
||||
shadowContainer.setEffect(new DropShadow(BlurType.ONE_PASS_BOX, Color.rgb(0, 0, 0, 0.4), 10, 0.3, 0.0, 0.0));
|
||||
|
||||
parent = new StackPane();
|
||||
Rectangle clip = new Rectangle();
|
||||
clip.widthProperty().bind(parent.widthProperty());
|
||||
clip.heightProperty().bind(parent.heightProperty());
|
||||
clip.setArcWidth(8);
|
||||
clip.setArcHeight(8);
|
||||
parent.setClip(clip);
|
||||
|
||||
skinnable.getSnackbar().registerSnackbarContainer(parent);
|
||||
|
||||
EventHandler<MouseEvent> onMouseReleased = this::onMouseReleased;
|
||||
EventHandler<MouseEvent> onMouseDragged = this::onMouseDragged;
|
||||
EventHandler<MouseEvent> onMouseMoved = this::onMouseMoved;
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/4290
|
||||
if (OperatingSystem.CURRENT_OS != OperatingSystem.MACOS) {
|
||||
onWindowsStatusChange = observable -> {
|
||||
if (primaryStage.isIconified() || primaryStage.isFullScreen() || primaryStage.isMaximized()) {
|
||||
root.removeEventFilter(MouseEvent.MOUSE_RELEASED, onMouseReleased);
|
||||
root.removeEventFilter(MouseEvent.MOUSE_DRAGGED, onMouseDragged);
|
||||
root.removeEventFilter(MouseEvent.MOUSE_MOVED, onMouseMoved);
|
||||
} else {
|
||||
root.addEventFilter(MouseEvent.MOUSE_RELEASED, onMouseReleased);
|
||||
root.addEventFilter(MouseEvent.MOUSE_DRAGGED, onMouseDragged);
|
||||
root.addEventFilter(MouseEvent.MOUSE_MOVED, onMouseMoved);
|
||||
}
|
||||
};
|
||||
onTitleBarDoubleClick = event -> {
|
||||
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
|
||||
primaryStage.setMaximized(!primaryStage.isMaximized());
|
||||
event.consume();
|
||||
}
|
||||
};
|
||||
WeakInvalidationListener weakOnWindowsStatusChange = new WeakInvalidationListener(onWindowsStatusChange);
|
||||
primaryStage.iconifiedProperty().addListener(weakOnWindowsStatusChange);
|
||||
primaryStage.maximizedProperty().addListener(weakOnWindowsStatusChange);
|
||||
primaryStage.fullScreenProperty().addListener(weakOnWindowsStatusChange);
|
||||
onWindowsStatusChange.invalidated(null);
|
||||
} else {
|
||||
onWindowsStatusChange = null;
|
||||
onTitleBarDoubleClick = null;
|
||||
root.addEventFilter(MouseEvent.MOUSE_RELEASED, onMouseReleased);
|
||||
root.addEventFilter(MouseEvent.MOUSE_DRAGGED, onMouseDragged);
|
||||
root.addEventFilter(MouseEvent.MOUSE_MOVED, onMouseMoved);
|
||||
}
|
||||
|
||||
shadowContainer.getChildren().setAll(parent);
|
||||
root.getChildren().setAll(shadowContainer);
|
||||
|
||||
StackPane wrapper = new StackPane();
|
||||
wrapper.backgroundProperty().bind(Bindings.createObjectBinding(
|
||||
() -> Themes.windowTransparentProperty().get()
|
||||
? null
|
||||
: new Background(new BackgroundFill(
|
||||
Themes.getColorScheme().getColor(ColorRole.SURFACE_CONTAINER),
|
||||
CornerRadii.EMPTY,
|
||||
Insets.EMPTY)),
|
||||
Themes.windowTransparentProperty(),
|
||||
Themes.colorSchemeProperty()));
|
||||
|
||||
Region backgroundNode = new Region();
|
||||
backgroundNode.setMouseTransparent(true);
|
||||
backgroundNode.backgroundProperty().bind(Bindings.createObjectBinding(
|
||||
() -> skinnable.getContentBackground() == null
|
||||
? null
|
||||
: skinnable.getContentBackground().background(),
|
||||
skinnable.contentBackgroundProperty()));
|
||||
backgroundNode.opacityProperty().bind(Bindings.createDoubleBinding(
|
||||
() -> skinnable.getContentBackground() == null
|
||||
? 1.0
|
||||
: skinnable.getContentBackground().opacity(),
|
||||
skinnable.contentBackgroundProperty()));
|
||||
StackPane.setAlignment(backgroundNode, Pos.BOTTOM_CENTER);
|
||||
|
||||
BorderPane frame = new BorderPane();
|
||||
frame.getStyleClass().addAll("jfx-decorator");
|
||||
wrapper.getChildren().setAll(backgroundNode, frame);
|
||||
skinnable.setDrawerWrapper(wrapper);
|
||||
|
||||
parent.getChildren().add(wrapper);
|
||||
|
||||
// center node with an animation layer at bottom, a container layer at middle and a "welcome" layer at top.
|
||||
StackPane container = new StackPane();
|
||||
FXUtils.setOverflowHidden(container);
|
||||
|
||||
// content layer at middle
|
||||
{
|
||||
StackPane contentPlaceHolder = new StackPane();
|
||||
contentPlaceHolder.getStyleClass().add("jfx-decorator-content-container");
|
||||
Bindings.bindContent(contentPlaceHolder.getChildren(), skinnable.contentProperty());
|
||||
|
||||
container.getChildren().add(contentPlaceHolder);
|
||||
}
|
||||
|
||||
// welcome and hint layer at top
|
||||
{
|
||||
StackPane floatLayer = new StackPane();
|
||||
Bindings.bindContent(floatLayer.getChildren(), skinnable.containerProperty());
|
||||
ListChangeListener<Node> listener = c -> {
|
||||
if (skinnable.getContainer().isEmpty()) {
|
||||
floatLayer.setMouseTransparent(true);
|
||||
floatLayer.setVisible(false);
|
||||
} else {
|
||||
floatLayer.setMouseTransparent(false);
|
||||
floatLayer.setVisible(true);
|
||||
}
|
||||
};
|
||||
skinnable.containerProperty().addListener(listener);
|
||||
listener.onChanged(null);
|
||||
|
||||
container.getChildren().add(floatLayer);
|
||||
}
|
||||
|
||||
frame.setCenter(container);
|
||||
|
||||
titleContainer = new StackPane();
|
||||
titleContainer.setPickOnBounds(false);
|
||||
titleContainer.getStyleClass().addAll("jfx-tool-bar");
|
||||
backgroundNode.maxHeightProperty().bind(Bindings.createDoubleBinding(
|
||||
() -> Math.max(0.0, wrapper.getHeight()
|
||||
- (skinnable.isTitleTransparent() ? 0.0 : titleContainer.getHeight())),
|
||||
wrapper.heightProperty(),
|
||||
skinnable.titleTransparentProperty(),
|
||||
titleContainer.heightProperty()));
|
||||
|
||||
// Maybe, we can automatically identify whether the top part of the picture is light-coloured or dark when the title is transparent,
|
||||
// and decide whether the whole top bar should be rendered in white or black. TODO
|
||||
FXUtils.onChangeAndOperate(skinnable.titleTransparentProperty(), titleTransparent -> {
|
||||
if (titleTransparent) {
|
||||
titleContainer.getStyleClass().remove("background");
|
||||
titleContainer.getStyleClass().add("gray-background");
|
||||
} else {
|
||||
titleContainer.getStyleClass().add("background");
|
||||
titleContainer.getStyleClass().remove("gray-background");
|
||||
}
|
||||
});
|
||||
|
||||
control.capableDraggingWindow(titleContainer);
|
||||
|
||||
BorderPane titleBar = new BorderPane();
|
||||
titleContainer.getChildren().add(titleBar);
|
||||
|
||||
Rectangle buttonsContainerPlaceHolder = new Rectangle();
|
||||
{
|
||||
navBarPane = new TransitionPane();
|
||||
navBarPane.setId("decoratorTitleTransitionPane");
|
||||
FXUtils.onChangeAndOperate(skinnable.stateProperty(), s -> {
|
||||
if (s == null) return;
|
||||
Node node = createNavBar(skinnable, s.leftPaneWidth(), s.backable(), skinnable.canCloseProperty().get(), skinnable.showCloseAsHomeProperty().get(), s.refreshable(), s.title(), s.titleNode());
|
||||
if (s.animate()) {
|
||||
TransitionPane.AnimationProducer animation = switch (skinnable.getNavigationDirection()) {
|
||||
case NEXT -> NavBarAnimations.NEXT;
|
||||
case PREVIOUS -> NavBarAnimations.PREVIOUS;
|
||||
default -> ContainerAnimations.FADE;
|
||||
};
|
||||
skinnable.setNavigationDirection(Navigation.NavigationDirection.START);
|
||||
navBarPane.setContent(node, animation, Motion.SHORT4);
|
||||
} else {
|
||||
navBarPane.getChildren().setAll(node);
|
||||
}
|
||||
});
|
||||
titleBar.setCenter(navBarPane);
|
||||
titleBar.setRight(buttonsContainerPlaceHolder);
|
||||
}
|
||||
frame.setTop(titleContainer);
|
||||
|
||||
{
|
||||
HBox buttonsContainer = new HBox();
|
||||
buttonsContainer.setAlignment(Pos.TOP_RIGHT);
|
||||
buttonsContainer.setMaxHeight(40);
|
||||
{
|
||||
JFXButton btnHelp = new JFXButton();
|
||||
btnHelp.setFocusTraversable(false);
|
||||
btnHelp.setGraphic(SVG.HELP.createIcon(Themes.titleFillProperty()));
|
||||
btnHelp.getStyleClass().add("jfx-decorator-button");
|
||||
btnHelp.setOnAction(e -> FXUtils.openLink(Metadata.CONTACT_URL));
|
||||
|
||||
JFXButton btnMin = new JFXButton();
|
||||
btnMin.setFocusTraversable(false);
|
||||
btnMin.setGraphic(SVG.MINIMIZE_CENTER.createIcon(Themes.titleFillProperty()));
|
||||
btnMin.getStyleClass().add("jfx-decorator-button");
|
||||
btnMin.setOnAction(e -> skinnable.minimize());
|
||||
|
||||
JFXButton btnClose = new JFXButton();
|
||||
btnClose.setFocusTraversable(false);
|
||||
btnClose.setGraphic(SVG.CLOSE.createIcon(Themes.titleFillProperty()));
|
||||
btnClose.getStyleClass().add("jfx-decorator-button");
|
||||
btnClose.setOnAction(e -> skinnable.close());
|
||||
|
||||
buttonsContainer.getChildren().setAll(btnHelp, btnMin, btnClose);
|
||||
}
|
||||
AnchorPane layer = new AnchorPane();
|
||||
layer.setPickOnBounds(false);
|
||||
layer.getChildren().add(buttonsContainer);
|
||||
AnchorPane.setTopAnchor(buttonsContainer, 0.0);
|
||||
AnchorPane.setRightAnchor(buttonsContainer, 0.0);
|
||||
buttonsContainerPlaceHolder.widthProperty().bind(buttonsContainer.widthProperty());
|
||||
parent.getChildren().add(layer);
|
||||
}
|
||||
|
||||
getChildren().add(root);
|
||||
}
|
||||
|
||||
private Node createNavBar(Decorator skinnable, double leftPaneWidth, boolean canBack, boolean canClose, boolean showCloseAsHome, boolean canRefresh, String title, Node titleNode) {
|
||||
BorderPane navBar = new BorderPane();
|
||||
navBar.getStyleClass().add("navigation-bar");
|
||||
|
||||
{
|
||||
HBox navLeft = new HBox();
|
||||
navLeft.setAlignment(Pos.CENTER_LEFT);
|
||||
navLeft.setPadding(new Insets(0, 5, 0, 5));
|
||||
|
||||
if (canBack) {
|
||||
JFXButton backNavButton = new JFXButton();
|
||||
skinnable.forbidDraggingWindow(backNavButton);
|
||||
backNavButton.setFocusTraversable(false);
|
||||
backNavButton.setGraphic(SVG.ARROW_BACK.createIcon(Themes.titleFillProperty()));
|
||||
backNavButton.getStyleClass().add("jfx-decorator-button");
|
||||
backNavButton.onActionProperty().bind(skinnable.onBackNavButtonActionProperty());
|
||||
backNavButton.visibleProperty().set(canBack);
|
||||
|
||||
navLeft.getChildren().add(backNavButton);
|
||||
}
|
||||
|
||||
if (canClose) {
|
||||
JFXButton closeNavButton = new JFXButton();
|
||||
skinnable.forbidDraggingWindow(closeNavButton);
|
||||
closeNavButton.setFocusTraversable(false);
|
||||
closeNavButton.setGraphic(SVG.CLOSE.createIcon(Themes.titleFillProperty()));
|
||||
closeNavButton.getStyleClass().add("jfx-decorator-button");
|
||||
closeNavButton.onActionProperty().bind(skinnable.onCloseNavButtonActionProperty());
|
||||
if (showCloseAsHome)
|
||||
closeNavButton.setGraphic(SVG.HOME.createIcon(Themes.titleFillProperty()));
|
||||
else
|
||||
closeNavButton.setGraphic(SVG.CLOSE.createIcon(Themes.titleFillProperty()));
|
||||
|
||||
navLeft.getChildren().add(closeNavButton);
|
||||
}
|
||||
|
||||
if (canBack || canClose) {
|
||||
navBar.setLeft(navLeft);
|
||||
}
|
||||
|
||||
BorderPane center = new BorderPane();
|
||||
if (title != null) {
|
||||
Label titleLabel = new Label();
|
||||
titleLabel.textFillProperty().bind(Themes.titleFillProperty());
|
||||
BorderPane.setAlignment(titleLabel, Pos.CENTER_LEFT);
|
||||
titleLabel.getStyleClass().add("jfx-decorator-title");
|
||||
if (titleNode == null) {
|
||||
titleLabel.maxWidthProperty().bind(Bindings.createDoubleBinding(
|
||||
() -> skinnable.getWidth() - 150 - navLeft.getWidth(),
|
||||
skinnable.widthProperty(), navLeft.widthProperty()));
|
||||
} else {
|
||||
titleLabel.prefWidthProperty().bind(Bindings.createDoubleBinding(() -> {
|
||||
// 8 (margin-left)
|
||||
return leftPaneWidth - 8 - navLeft.getWidth();
|
||||
}, navLeft.widthProperty()));
|
||||
}
|
||||
titleLabel.setText(title);
|
||||
center.setLeft(titleLabel);
|
||||
BorderPane.setAlignment(titleLabel, Pos.CENTER_LEFT);
|
||||
}
|
||||
if (titleNode != null) {
|
||||
center.setCenter(titleNode);
|
||||
BorderPane.setAlignment(titleNode, Pos.CENTER_LEFT);
|
||||
BorderPane.setMargin(titleNode, new Insets(0, 0, 0, 8));
|
||||
}
|
||||
if (onTitleBarDoubleClick != null)
|
||||
center.setOnMouseClicked(onTitleBarDoubleClick);
|
||||
center.setOnMouseDragged(mouseEvent -> {
|
||||
if (!getSkinnable().isDragging() && primaryStage.isMaximized()) {
|
||||
getSkinnable().setDragging(true);
|
||||
mouseInitX = mouseEvent.getScreenX();
|
||||
mouseInitY = mouseEvent.getScreenY();
|
||||
primaryStage.setMaximized(false);
|
||||
stageInitWidth = primaryStage.getWidth();
|
||||
stageInitHeight = primaryStage.getHeight();
|
||||
primaryStage.setY(stageInitY = 0);
|
||||
primaryStage.setX(stageInitX = mouseInitX - stageInitWidth / 2);
|
||||
}
|
||||
});
|
||||
navBar.setCenter(center);
|
||||
|
||||
if (canRefresh) {
|
||||
HBox navRight = new HBox();
|
||||
navRight.setAlignment(Pos.CENTER_RIGHT);
|
||||
JFXButton refreshNavButton = new JFXButton();
|
||||
refreshNavButton.setGraphic(SVG.REFRESH.createIcon(Themes.titleFillProperty()));
|
||||
refreshNavButton.getStyleClass().add("jfx-decorator-button");
|
||||
refreshNavButton.onActionProperty().bind(skinnable.onRefreshNavButtonActionProperty());
|
||||
skinnable.forbidDraggingWindow(refreshNavButton);
|
||||
|
||||
navRight.getChildren().setAll(refreshNavButton);
|
||||
navBar.setRight(navRight);
|
||||
}
|
||||
}
|
||||
return navBar;
|
||||
}
|
||||
|
||||
private boolean isRightEdge(double x, double y, Bounds boundsInParent) {
|
||||
return x < root.getWidth() && x >= root.getWidth() - root.snappedLeftInset();
|
||||
}
|
||||
|
||||
private boolean isTopEdge(double x, double y, Bounds boundsInParent) {
|
||||
return y >= 0 && y <= root.snappedTopInset();
|
||||
}
|
||||
|
||||
private boolean isBottomEdge(double x, double y, Bounds boundsInParent) {
|
||||
return y < root.getHeight() && y >= root.getHeight() - root.snappedLeftInset();
|
||||
}
|
||||
|
||||
private boolean isLeftEdge(double x, double y, Bounds boundsInParent) {
|
||||
return x >= 0 && x <= root.snappedLeftInset();
|
||||
}
|
||||
|
||||
private void resizeStage(double newWidth, double newHeight) {
|
||||
if (newWidth < 0)
|
||||
newWidth = primaryStage.getWidth();
|
||||
if (newWidth < primaryStage.getMinWidth())
|
||||
newWidth = primaryStage.getMinWidth();
|
||||
if (newWidth < titleContainer.getMinWidth())
|
||||
newWidth = titleContainer.getMinWidth();
|
||||
|
||||
if (newHeight < 0)
|
||||
newHeight = primaryStage.getHeight();
|
||||
if (newHeight < primaryStage.getMinHeight())
|
||||
newHeight = primaryStage.getMinHeight();
|
||||
if (newHeight < titleContainer.getMinHeight())
|
||||
newHeight = titleContainer.getMinHeight();
|
||||
|
||||
// Width and height must be set simultaneously to avoid JDK-8344372 (https://github.com/openjdk/jfx/pull/1654)
|
||||
primaryStage.setWidth(newWidth);
|
||||
primaryStage.setHeight(newHeight);
|
||||
}
|
||||
|
||||
private void onMouseMoved(MouseEvent mouseEvent) {
|
||||
if (!primaryStage.isFullScreen() && primaryStage.isResizable()) {
|
||||
double x = mouseEvent.getX(), y = mouseEvent.getY();
|
||||
Bounds boundsInParent = root.getBoundsInParent();
|
||||
double diagonalSize = root.snappedLeftInset() + 10;
|
||||
if (this.isRightEdge(x, y, boundsInParent)) {
|
||||
if (y < diagonalSize) {
|
||||
root.setCursor(Cursor.NE_RESIZE);
|
||||
} else if (y > root.getHeight() - diagonalSize) {
|
||||
root.setCursor(Cursor.SE_RESIZE);
|
||||
} else {
|
||||
root.setCursor(Cursor.E_RESIZE);
|
||||
}
|
||||
} else if (this.isLeftEdge(x, y, boundsInParent)) {
|
||||
if (y < diagonalSize) {
|
||||
root.setCursor(Cursor.NW_RESIZE);
|
||||
} else if (y > root.getHeight() - diagonalSize) {
|
||||
root.setCursor(Cursor.SW_RESIZE);
|
||||
} else {
|
||||
root.setCursor(Cursor.W_RESIZE);
|
||||
}
|
||||
} else if (this.isTopEdge(x, y, boundsInParent)) {
|
||||
if (x < diagonalSize) {
|
||||
root.setCursor(Cursor.NW_RESIZE);
|
||||
} else if (x > root.getWidth() - diagonalSize) {
|
||||
root.setCursor(Cursor.NE_RESIZE);
|
||||
} else {
|
||||
root.setCursor(Cursor.N_RESIZE);
|
||||
}
|
||||
} else if (this.isBottomEdge(x, y, boundsInParent)) {
|
||||
if (x < diagonalSize) {
|
||||
root.setCursor(Cursor.SW_RESIZE);
|
||||
} else if (x > root.getWidth() - diagonalSize) {
|
||||
root.setCursor(Cursor.SE_RESIZE);
|
||||
} else {
|
||||
root.setCursor(Cursor.S_RESIZE);
|
||||
}
|
||||
} else {
|
||||
root.setCursor(Cursor.DEFAULT);
|
||||
}
|
||||
} else {
|
||||
root.setCursor(Cursor.DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
private void onMouseReleased(MouseEvent mouseEvent) {
|
||||
getSkinnable().setDragging(false);
|
||||
}
|
||||
|
||||
private void onMouseDragged(MouseEvent mouseEvent) {
|
||||
if (!getSkinnable().isDragging()) {
|
||||
getSkinnable().setDragging(true);
|
||||
mouseInitX = mouseEvent.getScreenX();
|
||||
mouseInitY = mouseEvent.getScreenY();
|
||||
stageInitX = primaryStage.getX();
|
||||
stageInitY = primaryStage.getY();
|
||||
stageInitWidth = primaryStage.getWidth();
|
||||
stageInitHeight = primaryStage.getHeight();
|
||||
}
|
||||
|
||||
if (primaryStage.isFullScreen() || !mouseEvent.isPrimaryButtonDown() || mouseEvent.isStillSincePress())
|
||||
return;
|
||||
|
||||
double dx = mouseEvent.getScreenX() - mouseInitX;
|
||||
double dy = mouseEvent.getScreenY() - mouseInitY;
|
||||
|
||||
Cursor cursor = root.getCursor();
|
||||
if (getSkinnable().isAllowMove()) {
|
||||
if (cursor == Cursor.DEFAULT) {
|
||||
primaryStage.setX(stageInitX + dx);
|
||||
primaryStage.setY(stageInitY + dy);
|
||||
mouseEvent.consume();
|
||||
}
|
||||
}
|
||||
|
||||
if (getSkinnable().isResizable()) {
|
||||
if (cursor == Cursor.E_RESIZE) {
|
||||
resizeStage(stageInitWidth + dx, -1);
|
||||
mouseEvent.consume();
|
||||
|
||||
} else if (cursor == Cursor.S_RESIZE) {
|
||||
resizeStage(-1, stageInitHeight + dy);
|
||||
mouseEvent.consume();
|
||||
|
||||
} else if (cursor == Cursor.W_RESIZE) {
|
||||
resizeStage(stageInitWidth - dx, -1);
|
||||
primaryStage.setX(stageInitX + stageInitWidth - primaryStage.getWidth());
|
||||
mouseEvent.consume();
|
||||
|
||||
} else if (cursor == Cursor.N_RESIZE) {
|
||||
resizeStage(-1, stageInitHeight - dy);
|
||||
primaryStage.setY(stageInitY + stageInitHeight - primaryStage.getHeight());
|
||||
mouseEvent.consume();
|
||||
|
||||
} else if (cursor == Cursor.SE_RESIZE) {
|
||||
resizeStage(stageInitWidth + dx, stageInitHeight + dy);
|
||||
mouseEvent.consume();
|
||||
|
||||
} else if (cursor == Cursor.SW_RESIZE) {
|
||||
resizeStage(stageInitWidth - dx, stageInitHeight + dy);
|
||||
primaryStage.setX(stageInitX + stageInitWidth - primaryStage.getWidth());
|
||||
mouseEvent.consume();
|
||||
|
||||
} else if (cursor == Cursor.NW_RESIZE) {
|
||||
resizeStage(stageInitWidth - dx, stageInitHeight - dy);
|
||||
primaryStage.setX(stageInitX + stageInitWidth - primaryStage.getWidth());
|
||||
primaryStage.setY(stageInitY + stageInitHeight - primaryStage.getHeight());
|
||||
mouseEvent.consume();
|
||||
|
||||
} else if (cursor == Cursor.NE_RESIZE) {
|
||||
resizeStage(stageInitWidth + dx, stageInitHeight - dy);
|
||||
primaryStage.setY(stageInitY + stageInitHeight - primaryStage.getHeight());
|
||||
mouseEvent.consume();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum NavBarAnimations implements TransitionPane.AnimationProducer {
|
||||
NEXT {
|
||||
@Override
|
||||
public void init(TransitionPane container, Node previousNode, Node nextNode) {
|
||||
super.init(container, previousNode, nextNode);
|
||||
nextNode.setTranslateX(container.getWidth());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Timeline animate(
|
||||
Pane container, Node previousNode, Node nextNode,
|
||||
Duration duration, Interpolator interpolator) {
|
||||
return new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(nextNode.translateXProperty(), 50, interpolator),
|
||||
new KeyValue(previousNode.translateXProperty(), 0, interpolator),
|
||||
new KeyValue(nextNode.opacityProperty(), 0, interpolator),
|
||||
new KeyValue(previousNode.opacityProperty(), 1, interpolator)),
|
||||
new KeyFrame(duration,
|
||||
new KeyValue(nextNode.translateXProperty(), 0, interpolator),
|
||||
new KeyValue(previousNode.translateXProperty(), -50, interpolator),
|
||||
new KeyValue(nextNode.opacityProperty(), 1, interpolator),
|
||||
new KeyValue(previousNode.opacityProperty(), 0, interpolator))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransitionPane.AnimationProducer opposite() {
|
||||
return NEXT;
|
||||
}
|
||||
},
|
||||
|
||||
PREVIOUS {
|
||||
@Override
|
||||
public void init(TransitionPane container, Node previousNode, Node nextNode) {
|
||||
super.init(container, previousNode, nextNode);
|
||||
nextNode.setTranslateX(container.getWidth());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Timeline animate(Pane container, Node previousNode, Node nextNode, Duration duration, Interpolator interpolator) {
|
||||
return new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(nextNode.translateXProperty(), -50, interpolator),
|
||||
new KeyValue(previousNode.translateXProperty(), 0, interpolator),
|
||||
new KeyValue(nextNode.opacityProperty(), 0, interpolator),
|
||||
new KeyValue(previousNode.opacityProperty(), 1, interpolator)),
|
||||
new KeyFrame(duration,
|
||||
new KeyValue(nextNode.translateXProperty(), 0, interpolator),
|
||||
new KeyValue(previousNode.translateXProperty(), 50, interpolator),
|
||||
new KeyValue(nextNode.opacityProperty(), 1, interpolator),
|
||||
new KeyValue(previousNode.opacityProperty(), 0, interpolator))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransitionPane.AnimationProducer opposite() {
|
||||
return PREVIOUS;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <[email protected]> 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.decorator;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundFill;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.CornerRadii;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.util.Duration;
|
||||
import org.glavo.monetfx.ColorRole;
|
||||
import org.glavo.monetfx.ColorScheme;
|
||||
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.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.wizard.Navigation;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/// Renders the clipped background, title bar, and navigation content owned by a [Decorator].
|
||||
@NotNullByDefault
|
||||
final class MainWindowPane extends StackPane {
|
||||
/// The diameter, in pixels, of the rounded window corners.
|
||||
private static final double ARC = 8.0;
|
||||
|
||||
/// The decorator whose state and actions are represented by this pane.
|
||||
private final Decorator decorator;
|
||||
|
||||
/// The clip that rounds normal window corners and becomes square while the window fills the screen.
|
||||
private final Rectangle clip = new Rectangle();
|
||||
|
||||
/// The frame containing the title bar and current navigation page.
|
||||
private final BorderPane frame;
|
||||
|
||||
/// The title bar containing page navigation state.
|
||||
private final BorderPane titleBar;
|
||||
|
||||
/// The transition container used when the title-bar state changes.
|
||||
private final TransitionPane navBarPane;
|
||||
|
||||
/// Retains listener delegates that are registered through weak listener wrappers.
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
|
||||
/// Creates and wires the main-window content for `decorator`.
|
||||
///
|
||||
/// @param decorator the owning window decorator
|
||||
MainWindowPane(Decorator decorator) {
|
||||
this.decorator = decorator;
|
||||
|
||||
clip.widthProperty().bind(widthProperty());
|
||||
clip.heightProperty().bind(heightProperty());
|
||||
clip.setArcWidth(ARC);
|
||||
clip.setArcHeight(ARC);
|
||||
setClip(clip);
|
||||
|
||||
backgroundProperty().bind(Bindings.createObjectBinding(
|
||||
() -> Themes.windowTransparentProperty().get()
|
||||
? null
|
||||
: new Background(new BackgroundFill(
|
||||
Themes.getColorScheme().getColor(ColorRole.SURFACE_CONTAINER),
|
||||
CornerRadii.EMPTY,
|
||||
Insets.EMPTY)),
|
||||
Themes.windowTransparentProperty(),
|
||||
Themes.colorSchemeProperty()));
|
||||
|
||||
Region backgroundNode = createBackgroundNode();
|
||||
|
||||
frame = new BorderPane();
|
||||
frame.getStyleClass().add("jfx-decorator");
|
||||
|
||||
StackPane center = new StackPane();
|
||||
center.getStyleClass().add("jfx-decorator-content-container");
|
||||
FXUtils.setOverflowHidden(center);
|
||||
center.getChildren().setAll(decorator.getNavigator());
|
||||
frame.setCenter(center);
|
||||
|
||||
HBox rightButtonsContainer = createWindowButtons();
|
||||
titleBar = new BorderPane();
|
||||
titleBar.setPickOnBounds(false);
|
||||
titleBar.getStyleClass().add("jfx-tool-bar");
|
||||
titleBar.setRight(rightButtonsContainer);
|
||||
|
||||
navBarPane = new TransitionPane();
|
||||
titleBar.setCenter(navBarPane);
|
||||
frame.setTop(titleBar);
|
||||
|
||||
updateTitleBarBackground();
|
||||
InvalidationListener titleBarBackgroundListener = observable -> updateTitleBarBackground();
|
||||
Themes.colorSchemeProperty().addListener(holder.weak(titleBarBackgroundListener));
|
||||
Themes.titleBarTransparentProperty().addListener(holder.weak(titleBarBackgroundListener));
|
||||
|
||||
InvalidationListener navBarListener = observable -> updateNavBar();
|
||||
decorator.stateProperty().addListener(holder.weak(navBarListener));
|
||||
updateNavBar();
|
||||
|
||||
decorator.capableDraggingWindow(titleBar);
|
||||
|
||||
getChildren().setAll(backgroundNode, frame);
|
||||
}
|
||||
|
||||
/// Updates the content-corner shape for an edge-to-edge window state.
|
||||
///
|
||||
/// @param edgeToEdge whether the attached window is maximized or full-screen
|
||||
void setWindowEdgeToEdge(boolean edgeToEdge) {
|
||||
double arc = edgeToEdge ? 0.0 : ARC;
|
||||
clip.setArcWidth(arc);
|
||||
clip.setArcHeight(arc);
|
||||
}
|
||||
|
||||
/// Creates the launcher-background layer and keeps it synchronized with the active theme.
|
||||
///
|
||||
/// @return the mouse-transparent background node
|
||||
private Region createBackgroundNode() {
|
||||
Region backgroundNode = new Region();
|
||||
backgroundNode.setMouseTransparent(true);
|
||||
StackPane.setAlignment(backgroundNode, Pos.BOTTOM_CENTER);
|
||||
holder.onWeakChangeAndOperate(Themes.backgroundProperty(), background -> {
|
||||
double opacity = background.opacity();
|
||||
if (opacity >= 0.001) {
|
||||
backgroundNode.setBackground(background.background());
|
||||
backgroundNode.setOpacity(opacity);
|
||||
} else {
|
||||
backgroundNode.setBackground(null);
|
||||
backgroundNode.setOpacity(0);
|
||||
}
|
||||
});
|
||||
return backgroundNode;
|
||||
}
|
||||
|
||||
/// Creates the help, minimize, and application-close buttons.
|
||||
///
|
||||
/// @return a size-limited top-right button row
|
||||
private HBox createWindowButtons() {
|
||||
HBox buttons = new HBox();
|
||||
buttons.setAlignment(Pos.TOP_RIGHT);
|
||||
buttons.setMaxSize(Region.USE_PREF_SIZE, 40);
|
||||
|
||||
JFXButton helpButton = new JFXButton();
|
||||
helpButton.setFocusTraversable(false);
|
||||
helpButton.setGraphic(SVG.HELP.createIcon(Themes.titleFillProperty()));
|
||||
helpButton.getStyleClass().add("jfx-decorator-button");
|
||||
helpButton.setOnAction(event -> FXUtils.openLink(Metadata.CONTACT_URL));
|
||||
decorator.forbidDraggingWindow(helpButton);
|
||||
|
||||
JFXButton minimizeButton = new JFXButton();
|
||||
minimizeButton.setFocusTraversable(false);
|
||||
minimizeButton.setGraphic(SVG.MINIMIZE_CENTER.createIcon(Themes.titleFillProperty()));
|
||||
minimizeButton.getStyleClass().add("jfx-decorator-button");
|
||||
minimizeButton.setOnAction(event -> decorator.minimizeWindow());
|
||||
decorator.forbidDraggingWindow(minimizeButton);
|
||||
|
||||
JFXButton closeButton = new JFXButton();
|
||||
closeButton.setFocusTraversable(false);
|
||||
closeButton.setGraphic(SVG.CLOSE.createIcon(Themes.titleFillProperty()));
|
||||
closeButton.getStyleClass().add("jfx-decorator-button");
|
||||
closeButton.setOnAction(event -> decorator.closeWindow());
|
||||
decorator.forbidDraggingWindow(closeButton);
|
||||
|
||||
buttons.getChildren().setAll(helpButton, minimizeButton, closeButton);
|
||||
return buttons;
|
||||
}
|
||||
|
||||
/// Recomputes the title-bar background for the current theme and transparency preference.
|
||||
private void updateTitleBarBackground() {
|
||||
@Nullable ColorScheme colorScheme = Themes.getColorScheme();
|
||||
if (colorScheme == null) {
|
||||
titleBar.setBackground(null);
|
||||
} else if (Themes.titleBarTransparentProperty().get()) {
|
||||
Color surface = colorScheme.getSurface();
|
||||
titleBar.setBackground(new Background(new BackgroundFill(
|
||||
new Color(surface.getRed(), surface.getGreen(), surface.getBlue(), 0.5),
|
||||
CornerRadii.EMPTY,
|
||||
Insets.EMPTY)));
|
||||
} else {
|
||||
titleBar.setBackground(new Background(new BackgroundFill(
|
||||
colorScheme.getPrimaryContainer(),
|
||||
CornerRadii.EMPTY,
|
||||
Insets.EMPTY)));
|
||||
}
|
||||
}
|
||||
|
||||
/// Rebuilds the navigation bar from the current page state.
|
||||
private void updateNavBar() {
|
||||
DecoratorPage.@Nullable State state = decorator.getState();
|
||||
if (state == null) {
|
||||
navBarPane.getChildren().clear();
|
||||
return;
|
||||
}
|
||||
|
||||
Node navBar = createNavBar(state);
|
||||
|
||||
if (state.animate()) {
|
||||
TransitionPane.AnimationProducer animation = switch (decorator.getNavigationDirection()) {
|
||||
case NEXT -> NavBarAnimations.NEXT;
|
||||
case PREVIOUS -> NavBarAnimations.PREVIOUS;
|
||||
default -> ContainerAnimations.FADE;
|
||||
};
|
||||
decorator.setNavigationDirection(Navigation.NavigationDirection.START);
|
||||
navBarPane.setContent(navBar, animation, Motion.SHORT4);
|
||||
} else {
|
||||
navBarPane.getChildren().setAll(navBar);
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates the page-specific navigation controls and title.
|
||||
///
|
||||
/// @param state the current page state
|
||||
/// @return a newly constructed navigation bar
|
||||
private Node createNavBar(DecoratorPage.State state) {
|
||||
HBox navBar = new HBox();
|
||||
navBar.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
// Left navigation buttons
|
||||
if (state.backable()) {
|
||||
JFXButton backButton = new JFXButton();
|
||||
backButton.setFocusTraversable(false);
|
||||
backButton.setGraphic(SVG.ARROW_BACK.createIcon(Themes.titleFillProperty()));
|
||||
backButton.getStyleClass().add("jfx-decorator-button");
|
||||
backButton.setOnAction(event -> decorator.back());
|
||||
decorator.forbidDraggingWindow(backButton);
|
||||
navBar.getChildren().add(backButton);
|
||||
}
|
||||
|
||||
if (decorator.canCloseProperty().get()) {
|
||||
boolean showCloseAsHome = decorator.showCloseAsHomeProperty().get();
|
||||
|
||||
JFXButton closeButton = new JFXButton();
|
||||
closeButton.setFocusTraversable(false);
|
||||
closeButton.setGraphic((showCloseAsHome ? SVG.HOME : SVG.CLOSE).createIcon(Themes.titleFillProperty()));
|
||||
closeButton.getStyleClass().add("jfx-decorator-button");
|
||||
closeButton.setOnAction(event -> decorator.closeCurrentPage());
|
||||
decorator.forbidDraggingWindow(closeButton);
|
||||
navBar.getChildren().add(closeButton);
|
||||
}
|
||||
|
||||
// Center title area
|
||||
StackPane titleArea = new StackPane();
|
||||
titleArea.setAlignment(Pos.CENTER_LEFT);
|
||||
titleArea.setMinWidth(0);
|
||||
HBox.setHgrow(titleArea, Priority.ALWAYS);
|
||||
if (state.titleNode() != null) {
|
||||
titleArea.getChildren().setAll(state.titleNode());
|
||||
StackPane.setMargin(state.titleNode(), new Insets(0, 0, 0, 8));
|
||||
} else if (state.title() != null) {
|
||||
Label titleLabel = new Label(state.title());
|
||||
titleLabel.textFillProperty().bind(Themes.titleFillProperty());
|
||||
titleLabel.getStyleClass().add("jfx-decorator-title");
|
||||
titleLabel.setMinWidth(0);
|
||||
titleArea.getChildren().setAll(titleLabel);
|
||||
}
|
||||
|
||||
decorator.registerTitleBar(titleArea);
|
||||
|
||||
if (!navBar.getChildren().isEmpty()) {
|
||||
Insets padding = new Insets(0, 0, 0, 5);
|
||||
navBar.setPadding(padding);
|
||||
titleArea.setPadding(padding);
|
||||
}
|
||||
|
||||
navBar.getChildren().add(titleArea);
|
||||
|
||||
// Right refresh button
|
||||
if (state.refreshable()) {
|
||||
JFXButton refreshButton = new JFXButton();
|
||||
refreshButton.setFocusTraversable(false);
|
||||
refreshButton.setGraphic(SVG.REFRESH.createIcon(Themes.titleFillProperty()));
|
||||
refreshButton.getStyleClass().add("jfx-decorator-button");
|
||||
refreshButton.setOnAction(event -> decorator.refresh());
|
||||
decorator.forbidDraggingWindow(refreshButton);
|
||||
|
||||
navBar.getChildren().add(refreshButton);
|
||||
}
|
||||
|
||||
return navBar;
|
||||
}
|
||||
|
||||
/// Produces directional transitions for page-title changes.
|
||||
private enum NavBarAnimations implements TransitionPane.AnimationProducer {
|
||||
/// Moves the next title in from the right.
|
||||
NEXT {
|
||||
/// {@inheritDoc}
|
||||
@Override
|
||||
public void init(TransitionPane container, Node previousNode, Node nextNode) {
|
||||
super.init(container, previousNode, nextNode);
|
||||
nextNode.setTranslateX(container.getWidth());
|
||||
}
|
||||
|
||||
/// {@inheritDoc}
|
||||
@Override
|
||||
public Timeline animate(
|
||||
Pane container,
|
||||
Node previousNode,
|
||||
Node nextNode,
|
||||
Duration duration,
|
||||
Interpolator interpolator) {
|
||||
return createTimeline(previousNode, nextNode, 50, -50, duration, interpolator);
|
||||
}
|
||||
|
||||
/// {@inheritDoc}
|
||||
@Override
|
||||
public TransitionPane.AnimationProducer opposite() {
|
||||
return PREVIOUS;
|
||||
}
|
||||
},
|
||||
|
||||
/// Moves the previous title in from the left.
|
||||
PREVIOUS {
|
||||
/// {@inheritDoc}
|
||||
@Override
|
||||
public void init(TransitionPane container, Node previousNode, Node nextNode) {
|
||||
super.init(container, previousNode, nextNode);
|
||||
nextNode.setTranslateX(-container.getWidth());
|
||||
}
|
||||
|
||||
/// {@inheritDoc}
|
||||
@Override
|
||||
public Timeline animate(
|
||||
Pane container,
|
||||
Node previousNode,
|
||||
Node nextNode,
|
||||
Duration duration,
|
||||
Interpolator interpolator) {
|
||||
return createTimeline(previousNode, nextNode, -50, 50, duration, interpolator);
|
||||
}
|
||||
|
||||
/// {@inheritDoc}
|
||||
@Override
|
||||
public TransitionPane.AnimationProducer opposite() {
|
||||
return NEXT;
|
||||
}
|
||||
};
|
||||
|
||||
/// Creates a title transition with the supplied horizontal offsets.
|
||||
///
|
||||
/// @param previousNode the title being removed
|
||||
/// @param nextNode the title being shown
|
||||
/// @param nextOffset the initial offset of the next title
|
||||
/// @param previousOffset the final offset of the previous title
|
||||
/// @param duration the transition duration
|
||||
/// @param interpolator the transition interpolator
|
||||
/// @return the unstarted transition timeline
|
||||
private static Timeline createTimeline(
|
||||
Node previousNode,
|
||||
Node nextNode,
|
||||
double nextOffset,
|
||||
double previousOffset,
|
||||
Duration duration,
|
||||
Interpolator interpolator) {
|
||||
return new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(nextNode.translateXProperty(), nextOffset, interpolator),
|
||||
new KeyValue(previousNode.translateXProperty(), 0, interpolator),
|
||||
new KeyValue(nextNode.opacityProperty(), 0, interpolator),
|
||||
new KeyValue(previousNode.opacityProperty(), 1, interpolator)),
|
||||
new KeyFrame(duration,
|
||||
new KeyValue(nextNode.translateXProperty(), 0, interpolator),
|
||||
new KeyValue(previousNode.translateXProperty(), previousOffset, interpolator),
|
||||
new KeyValue(nextNode.opacityProperty(), 1, interpolator),
|
||||
new KeyValue(previousNode.opacityProperty(), 0, interpolator)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -575,8 +575,8 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
this.setActions(downloadButton, saveAsButton, cancelButton);
|
||||
}
|
||||
|
||||
this.prefWidthProperty().bind(Controllers.windowWidthProperty().multiply(0.7));
|
||||
this.prefHeightProperty().bind(Controllers.windowHeightProperty().multiply(0.7));
|
||||
this.prefWidthProperty().bind(Controllers.getDecorator().contentWidthProperty().multiply(0.7));
|
||||
this.prefHeightProperty().bind(Controllers.getDecorator().contentHeightProperty().multiply(0.7));
|
||||
|
||||
onEscPressed(this, cancelButton::fire);
|
||||
}
|
||||
|
||||
@@ -402,7 +402,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
titleContainer.setSpacing(8);
|
||||
titleContainer.setPadding(new Insets(0, 0, 12, 0));
|
||||
|
||||
DoubleBinding widthBinding = Controllers.windowWidthProperty().multiply(0.7);
|
||||
DoubleBinding widthBinding = Controllers.getDecorator().contentWidthProperty().multiply(0.7);
|
||||
prefWidthProperty().bind(widthBinding);
|
||||
maxWidthProperty().bind(widthBinding);
|
||||
|
||||
@@ -442,7 +442,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
descriptionPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
|
||||
descriptionPane.setFitToWidth(true);
|
||||
description.heightProperty().addListener((obs, oldVal, newVal) -> {
|
||||
double maxHeight = Controllers.windowHeightProperty().get() * 0.5;
|
||||
double maxHeight = Controllers.getDecorator().contentHeightProperty().get() * 0.5;
|
||||
double targetHeight = Math.min(newVal.doubleValue(), maxHeight);
|
||||
descriptionPane.setPrefViewportHeight(targetHeight);
|
||||
});
|
||||
|
||||
@@ -594,7 +594,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
HBox titleContainer = new HBox();
|
||||
titleContainer.setSpacing(8);
|
||||
|
||||
maxWidthProperty().bind(Controllers.windowWidthProperty().multiply(0.7));
|
||||
maxWidthProperty().bind(Controllers.getDecorator().contentWidthProperty().multiply(0.7));
|
||||
|
||||
ImageContainer imageContainer = new ImageContainer(40);
|
||||
imageContainer.setImage(packInfoObject.getIcon());
|
||||
@@ -620,7 +620,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
descriptionPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
|
||||
descriptionPane.setFitToWidth(true);
|
||||
FXUtils.onChange(description.heightProperty(), newVal -> {
|
||||
double maxHeight = Controllers.windowHeightProperty().get() * 0.5;
|
||||
double maxHeight = Controllers.getDecorator().contentHeightProperty().get() * 0.5;
|
||||
double targetHeight = Math.min(newVal.doubleValue(), maxHeight);
|
||||
descriptionPane.setPrefViewportHeight(targetHeight);
|
||||
});
|
||||
|
||||
@@ -418,7 +418,7 @@ public final class ThemePackManagementPage extends ListPageBase<ThemePackManager
|
||||
/// @param themePack the installed theme pack to display
|
||||
private ThemePackInfoDialog(ThemePackManagementPage page, ThemePackManager.InstalledThemePack themePack) {
|
||||
ThemePackManifest manifest = themePack.manifest();
|
||||
maxWidthProperty().bind(Controllers.windowWidthProperty().multiply(0.7));
|
||||
maxWidthProperty().bind(Controllers.getDecorator().contentWidthProperty().multiply(0.7));
|
||||
|
||||
HBox heading = new HBox(8);
|
||||
heading.setAlignment(Pos.CENTER_LEFT);
|
||||
@@ -483,7 +483,7 @@ public final class ThemePackManagementPage extends ListPageBase<ThemePackManager
|
||||
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
|
||||
scrollPane.setPrefViewportWidth(520);
|
||||
scrollPane.setPrefViewportHeight(Math.min(360, manifest.themes().size() * 86));
|
||||
scrollPane.maxHeightProperty().bind(Controllers.windowHeightProperty().multiply(0.55));
|
||||
scrollPane.maxHeightProperty().bind(Controllers.getDecorator().contentHeightProperty().multiply(0.55));
|
||||
|
||||
StackPane body = new StackPane(scrollPane);
|
||||
body.setPadding(new Insets(10, 0, 0, 0));
|
||||
|
||||
@@ -1762,11 +1762,6 @@
|
||||
-fx-font-size: 14;
|
||||
}
|
||||
|
||||
.window {
|
||||
-fx-background-color: transparent;
|
||||
-fx-padding: 8;
|
||||
}
|
||||
|
||||
.debug-border {
|
||||
-fx-border-color: red;
|
||||
-fx-border-width: 1;
|
||||
|
||||
Reference in new issue
Block a user