357 lines
9.7 KiB
Java
357 lines
9.7 KiB
Java
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();
|
||
}
|
||
}
|