package com.faforever.client.fx;
import com.faforever.client.preferences.PreferencesService;
import com.faforever.client.theme.ThemeService;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Bounds;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.paint.Color;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Popup;
import javafx.stage.PopupWindow;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.util.Duration;
import javafx.util.StringConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Set;
import java.util.concurrent.CompletionStage;
import java.util.function.Consumer;
import java.util.function.Function;
import static com.github.nocatch.NoCatch.noCatch;
/**
* Utility class to fix some annoying JavaFX shortcomings.
*/
public final class JavaFxUtil {
public static final StringConverter<Path> PATH_STRING_CONVERTER = new StringConverter<Path>() {
@Override
public String toString(Path object) {
if (object == null) {
return null;
}
return object.toAbsolutePath().toString();
}
@Override
public Path fromString(String string) {
if (string == null) {
return null;
}
return Paths.get(string);
}
};
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final double ZOOM_STEP = 0.2d;
private JavaFxUtil() {
throw new AssertionError("Not instantiatable");
}
public static void makeSuggestionField(TextField textField,
Function<String, CompletionStage<Set<Label>>> itemsFactory,
Consumer<Void> onAction) {
ListView<Label> listView = new ListView<>();
listView.prefWidthProperty().bind(textField.widthProperty());
listView.setFixedCellSize(24);
Popup popupControl = new Popup();
popupControl.setAutoHide(true);
popupControl.setAutoFix(false);
popupControl.setAnchorLocation(PopupWindow.AnchorLocation.CONTENT_TOP_LEFT);
popupControl.getScene().setRoot(listView);
BooleanProperty isUserSelecting = new SimpleBooleanProperty();
popupControl.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
// Don't close on space
if (event.getCode() == KeyCode.SPACE) {
event.consume();
}
});
listView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
if (newValue != null) {
isUserSelecting.set(true);
textField.setText(newValue.getText());
}
});
listView.setOnMouseClicked(event -> popupControl.hide());
listView.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode() == KeyCode.ENTER) {
onAction.accept(null);
popupControl.hide();
}
});
textField.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode() == KeyCode.DOWN) {
listView.requestFocus();
listView.getSelectionModel().selectFirst();
textField.positionCaret(Integer.MAX_VALUE);
} else {
isUserSelecting.set(false);
}
});
textField.textProperty().addListener((observable, oldValue, newValue) -> {
if (isUserSelecting.get()) {
return;
}
if (newValue.isEmpty()) {
popupControl.hide();
return;
}
if (oldValue.trim().equalsIgnoreCase(newValue)) {
return;
}
itemsFactory.apply(newValue).thenAccept(items -> Platform.runLater(() -> {
listView.getItems().setAll(items);
listView.setPrefHeight(Math.min(120, items.size() * (listView.getFixedCellSize() + 2)));
if (listView.getItems().isEmpty()) {
popupControl.hide();
} else if (!popupControl.isShowing()) {
Bounds screenBounds = textField.localToScreen(textField.getBoundsInLocal());
popupControl.show(textField.getScene().getWindow(), screenBounds.getMinX(), screenBounds.getMaxY());
}
}));
});
}
public static void makeNumericTextField(TextField textField) {
makeNumericTextField(textField, -1);
}
public static void makeNumericTextField(TextField textField, int maxLength) {
textField.textProperty().addListener((observable, oldValue, newValue) -> {
String value = newValue;
if (!value.matches("\\d*")) {
value = newValue.replaceAll("[^\\d]", "");
}
if (maxLength > 0 && value.length() > maxLength) {
value = value.substring(0, maxLength);
}
textField.setText(value);
if (textField.getCaretPosition() > textField.getLength()) {
textField.positionCaret(textField.getLength());
}
});
}
/**
* Uses reflection to change to tooltip delay/duration to some sane values.
* <p>
* See <a href="https://javafx-jira.kenai.com/browse/RT-19538">https://javafx-jira.kenai.com/browse/RT-19538</a>
*/
public static void fixTooltipDuration() {
noCatch(() -> {
Field fieldBehavior = Tooltip.class.getDeclaredField("BEHAVIOR");
fieldBehavior.setAccessible(true);
Object objBehavior = fieldBehavior.get(null);
Field activationTimerField = objBehavior.getClass().getDeclaredField("activationTimer");
activationTimerField.setAccessible(true);
Timeline objTimer = (Timeline) activationTimerField.get(objBehavior);
objTimer.getKeyFrames().setAll(new KeyFrame(new Duration(500)));
Field hideTimerField = objBehavior.getClass().getDeclaredField("hideTimer");
hideTimerField.setAccessible(true);
objTimer = (Timeline) hideTimerField.get(objBehavior);
objTimer.getKeyFrames().setAll(new KeyFrame(new Duration(100000)));
});
}
/**
* Centers a window FOR REAL. https://javafx-jira.kenai.com/browse/RT-40368
*/
public static void centerOnScreen(Stage stage) {
double width = stage.getWidth();
double height = stage.getHeight();
Rectangle2D screenBounds = Screen.getPrimary().getVisualBounds();
stage.setX((screenBounds.getMaxX() - screenBounds.getMinX() - width) / 2);
stage.setY((screenBounds.getMaxY() - screenBounds.getMinY() - height) / 2);
}
public static void assertApplicationThread() {
if (!Platform.isFxApplicationThread()) {
throw new IllegalStateException("Must run in FX Application thread");
}
}
public static void assertBackgroundThread() {
if (Platform.isFxApplicationThread()) {
throw new IllegalStateException("Must not run in FX Application thread");
}
}
public static void configureWebView(WebView webView, PreferencesService preferencesService, ThemeService themeService) {
webView.setContextMenuEnabled(false);
webView.setOnScroll(event -> {
if (event.isControlDown()) {
if (event.getDeltaY() > 0) {
webView.setZoom(webView.getZoom() + ZOOM_STEP);
} else {
webView.setZoom(webView.getZoom() - ZOOM_STEP);
}
}
});
webView.setOnKeyPressed(event -> {
if (event.isControlDown() && (event.getCode() == KeyCode.DIGIT0 || event.getCode() == KeyCode.NUMPAD0)) {
webView.setZoom(1);
}
});
WebEngine engine = webView.getEngine();
engine.setUserDataDirectory(preferencesService.getCacheDirectory().toFile());
themeService.registerWebView(webView);
}
public static boolean isVisibleRecursively(Node node) {
if (!node.isVisible()) {
return false;
}
Parent parent = node.getParent();
if (parent == null) {
return node.getScene() != null;
}
return isVisibleRecursively(parent);
}
public static String toRgbCode(Color color) {
return String.format("#%02X%02X%02X",
(int) (color.getRed() * 255),
(int) (color.getGreen() * 255),
(int) (color.getBlue() * 255));
}
}