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(); } }