Files
timi-fx/src/main/java/com/imyeyu/fx/TimiFX.java
2026-01-13 17:50:44 +08:00

357 lines
9.7 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
/**
* <b>TimiFX</b> - JavaFX 开发工具
*
* @author 夜雨
* @since 2021-02-14 10:51
*/
public final class TimiFX {
/**
* 阻止取消选择
* <pre>
* addEventFilter(MouseEvent.MOUSE_PRESSED, EVENT_CONSUME_TGBTN);
* </pre>
*/
public static final EventHandler<MouseEvent> 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<WindowEvent> 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();
}
}