package com.imyeyu.fx;
import com.imyeyu.fx.utils.ScreenFX;
import com.imyeyu.java.bean.Callback;
import com.imyeyu.java.ref.Ref;
import com.imyeyu.utils.Calc;
import com.imyeyu.utils.OS;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.event.EventHandler;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.skin.VirtualFlow;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.stage.PopupWindow;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.Window;
import javafx.stage.WindowEvent;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* TimiFX - JavaFX 开发工具
*
* @author 夜雨
* @since 2021-02-14 10:51
*/
public final class TimiFX {
/**
* 阻止取消选择
*
* addEventFilter(MouseEvent.MOUSE_PRESSED, EVENT_CONSUME_TGBTN);
*
*/
public static final EventHandler EVENT_CONSUME_TG_BTN = e -> {
if (e.getSource() instanceof ToggleButton btn && btn.isSelected()) {
e.consume();
}
};
/**
* 十六进制颜色含透明度:0xFF00FF00
*
* @param color 颜色
* @return 十六进制颜色字符串
*/
public static String toHexString(Color color) {
int r = Calc.round(color.getRed() * 255) << 24;
int g = Calc.round(color.getGreen() * 255) << 16;
int b = Calc.round(color.getBlue() * 255) << 8;
int a = Calc.round(color.getOpacity() * 255);
return String.format("0x%08X", (r + g + b + a));
}
/**
* 解析十六进制颜色字符串
*
* @param hexColorString 十六进制颜色字符串
* @return 颜色
*/
public static Color fromHexString(String hexColorString) {
return Color.valueOf(hexColorString);
}
/**
* 重新设置 SVG 大小
*
* @param svg SVG 路径
* @param scale 缩放倍率
* @return SVG 路径
*/
public static String resizeSVG(String svg, double scale) {
Pattern compile = Pattern.compile("\\d+\\.?\\d+");
Matcher matcher = compile.matcher(svg);
return matcher.replaceAll(matchResult -> {
double d = Double.parseDouble(matchResult.group()) * scale;
if (d % 1 == 0) {
return String.valueOf((int) d);
} else {
return "%.2f".formatted(d);
}
});
}
/**
* 设置相对居中窗体
*
* @param owner 依赖窗体
* @param window 居中窗体
*/
public static void relativeCenter(Window owner, Window window) {
double x = owner.getX() + owner.getWidth() * .5 - window.getWidth() * .5;
double y = owner.getY() + owner.getHeight() * .382 - window.getHeight() * .5;
double idle = Math.abs(owner.getY() + 30 - y);
if (y < owner.getY()) {
// 防止跃出相对窗体上部分
y += idle;
}
if (ScreenFX.outOfScreen(x, y)) {
// 坐标无效或窗体完全越出屏幕,设置到主屏幕
relativeCenter4PrimaryScreen(window);
}
window.setX(x);
window.setY(y);
}
/**
* 设置窗体到主屏幕中间(偏上少许)
*
* @param stage 窗体
*/
public static void relativeCenter4PrimaryScreen(Window stage) {
relativeCenter4Screen(ScreenFX.primary, stage);
}
/**
* 设置窗体到屏幕中间(偏上少许)
*
* @param screen 屏幕
* @param stage 窗体
*/
public static void relativeCenter4Screen(Screen screen, Window stage) {
if (screen == null) {
relativeCenter4PrimaryScreen(stage);
} else {
Rectangle2D r2d = screen.getBounds();
double x = r2d.getMinX() + r2d.getWidth() * .5 - stage.getWidth() * .5;
double y = r2d.getMinY() + r2d.getHeight() * .382 - stage.getHeight() * .5;
double idle = Math.abs(r2d.getMinY() + 30 - y);
if (y < r2d.getMinY()) {
// 防止跃出屏幕上部分
y += idle;
}
if (Double.isNaN(x) || Double.isNaN(y)) {
stage.centerOnScreen();
} else {
stage.setX(x);
stage.setY(y);
}
}
}
/**
* 相对于某窗体居中显示新窗体(需预设宽高)
*
* @param owner 依赖窗体
* @param window 需显示的窗体
*/
public static void showCenter(Window owner, Window window) {
if (!window.isShowing()) {
relativeCenter(owner, window);
}
if (Double.isNaN(window.getX()) || Double.isNaN(window.getY())) {
final EventHandler onShown = window.getOnShown();
window.setOnShown(e -> {
if (onShown != null) {
onShown.handle(e);
}
window.sizeToScene();
relativeCenter(owner, window);
});
}
if (window instanceof Stage stage) {
requestTop(stage);
} else if (window instanceof PopupWindow popupWindow) {
popupWindow.show(owner);
}
}
/**
* 请求焦点,执行显示、置顶并聚焦
*
* @param stage 窗体
*/
public static void requestTop(Stage stage) {
stage.show();
if (stage.isIconified()) {
stage.setIconified(false);
}
stage.setAlwaysOnTop(true);
stage.requestFocus();
stage.setAlwaysOnTop(false);
}
/**
* 根据布尔值监听动态切换节点 CSS 类
*
* @param node 节点
* @param booleanProperty 布尔值监听
* @param onTrue 为 true 时的 css 类
* @param onFalse 为 false 时的 css 类
*/
public static void toggleStyleClass(Node node, ReadOnlyBooleanProperty booleanProperty, String onTrue, String onFalse) {
if (booleanProperty.get()) {
node.getStyleClass().remove(onFalse);
node.getStyleClass().add(onTrue);
}
booleanProperty.addListener((obs, o, isTrue) -> {
if (isTrue) {
node.getStyleClass().remove(onFalse);
node.getStyleClass().add(onTrue);
} else {
node.getStyleClass().remove(onTrue);
node.getStyleClass().add(onFalse);
}
});
}
/**
* 根据布尔值监听动态切换节点 CSS 类
*
* @param node 节点
* @param booleanBinding 布尔值监听
* @param onTrue 为 true 时的 css 类
* @param onFalse 为 false 时的 css 类
*/
public static void toggleStyleClass4Binding(Node node, BooleanBinding booleanBinding, String onTrue, String onFalse) {
if (booleanBinding.get()) {
node.getStyleClass().remove(onFalse);
node.getStyleClass().add(onTrue);
}
booleanBinding.addListener((obs, o, isTrue) -> {
if (isTrue) {
node.getStyleClass().remove(onFalse);
node.getStyleClass().add(onTrue);
} else {
node.getStyleClass().remove(onTrue);
node.getStyleClass().add(onFalse);
}
});
}
/**
* 指向半透
*
* @param node 节点
*/
public static void hoverOpacity(Node node) {
hoverOpacity(node, node);
}
/**
* 指向半透
*
* @param handler 触发节点
* @param node 节点
*/
public static void hoverOpacity(Node handler, Node node) {
node.opacityProperty().bind(Bindings.when(handler.hoverProperty()).then(.7).otherwise(1));
}
/**
* 指向即聚焦该节点
*
* @param node 节点
*/
public static void hoverFocus(Node node) {
node.hoverProperty().addListener((obs, o, isHover) -> {
if (isHover) {
node.requestFocus();
}
});
}
/**
* 滚动指定项至控件可视范围的中央
*
* @param control 控件({@link javafx.scene.control.ListView}, {@link javafx.scene.control.TableView} 等)
* @param index 指定项下标
*/
public static void scrollToCenter(Control control, int index) {
try {
if (control.getSkin() == null) {
return;
}
// 偏移
VirtualFlow> flow = Ref.getFieldValue(control.getSkin(), "flow", VirtualFlow.class);
if (flow == null) {
throw new UnsupportedOperationException("unsupported this control");
}
if (flow.getCellCount() == 0 || flow.getCell(0) == null) {
return;
}
int offset = Calc.floor(flow.getHeight() * .5 / flow.getCell(0).getHeight());
// 滚动
Method method = Ref.getMethod(control.getClass(), "scrollTo", int.class);
method.setAccessible(true);
method.invoke(control, index - offset);
} catch (InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public static void addScrollFinishedListener(Control control, Callback callback) {
BooleanProperty finish = new SimpleBooleanProperty(false);
control.skinProperty().addListener((obs, o, skin) -> {
try {
VirtualFlow> flow = Ref.getFieldValue(control.getSkin(), "flow", VirtualFlow.class);
if (flow == null) {
throw new UnsupportedOperationException("unsupported this control");
}
// 监听总高度和视口高度的变化
Observable[] dependencies = {flow.layoutBoundsProperty(), flow.parentProperty()};
BooleanBinding binding = Bindings.createBooleanBinding(() -> {
double totalHeight = flow.prefHeight(-1);
double viewportHeight = flow.getLayoutBounds().getHeight();
if (viewportHeight <= 0 || totalHeight <= 0) {
return false;
}
double maxVValue = (totalHeight - viewportHeight) / viewportHeight;
double currentVValue = flow.getLayoutY() / viewportHeight;
return currentVValue >= (maxVValue - 0.01);
}, dependencies);
finish.bind(binding);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
finish.addListener((obs, o, isFinished) -> {
if (isFinished) {
callback.handler();
}
});
}
/**
* 重启程序,命令需自定
*
* @param application FX 程序
* @param command 重启命令
* @throws Exception 异常
*/
public static void doRestart(Application application, String command) throws Exception {
Platform.setImplicitExit(true);
OS.runAfterShutdown(command);
application.stop();
}
}