package com.fine.theme.utils; import com.fine.theme.light.ui.CollapsibleScrollBarLayerUI; import com.formdev.flatlaf.ui.FlatUIUtils; import com.fr.design.border.FineBorderFactory; import com.fr.design.constants.LayoutConstants; import com.fr.design.gui.icontainer.UIScrollPane; import com.fr.design.gui.ilable.UILabel; import com.fr.design.mainframe.DesignerContext; import com.fr.design.mainframe.DesignerFrame; import com.fr.design.mainframe.theme.edit.ui.LabelUtils; import com.fr.design.i18n.DesignSizeI18nManager; import com.fr.stable.os.OperatingSystem; import com.fr.value.AtomicClearableLazyValue; import javax.swing.JLabel; import javax.swing.JLayer; import javax.swing.ScrollPaneConstants; import javax.swing.UIManager; import javax.swing.JTextArea; import java.awt.Color; import java.awt.Component; import java.awt.Composite; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GraphicsDevice; import java.awt.GraphicsEnvironment; import java.awt.Insets; import java.awt.geom.Path2D; import java.awt.geom.RoundRectangle2D; import java.lang.reflect.Field; import static com.fine.swing.ui.layout.Layouts.cell; import static com.fine.swing.ui.layout.Layouts.column; import static com.fine.theme.light.ui.FineButtonUI.isLeftRoundButton; import static com.formdev.flatlaf.util.UIScale.scale; /** * UI绘制的一些常用方法 * * @author vito * @since 11.0 * Created on 2023/11/3 */ public class FineUIUtils { public static final String LEFT = "LEFT"; public static final String RIGHT = "RIGHT"; public static final int RETINA_SCALE_FACTOR = 2; /** * 判断是否支持retina,制作一些特殊效果,如HIDPI图片绘制。 * retina 是一种特殊的效果,使用4个像素点模拟一个像素点, * 因此在其他操作系统上,即使是高分屏也不具备retina的效果。 * 甚至还有劣化的效果以及更差的性能。 * * @since 2023.11.16 */ private static final AtomicClearableLazyValue RETINA = AtomicClearableLazyValue.create(() -> { // 经过测试win11,ubuntu等,没有retina效果 if (!OperatingSystem.isMacos()) { return false; } GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment(); GraphicsDevice device = env.getDefaultScreenDevice(); try { Field field = device.getClass().getDeclaredField("scale"); field.setAccessible(true); Object scale = field.get(device); if (scale instanceof Integer && (Integer) scale == RETINA_SCALE_FACTOR) { return true; } } catch (Exception ignored) { } return false; }); /** * 是否支持 retina * * @return 是否支持 retina */ public static boolean getRetina() { return RETINA.getValue(); } /** * 放弃 retina 判断结果,用于清理或者切换环境 */ public static void clearRetina() { RETINA.drop(); } /** * 通过key获取UI的颜色,如果没有则使用后备key获取 * * @param key 颜色key * @param defaultKey 颜色后备key * @return 颜色 */ public static Color getUIColor(String key, String defaultKey) { Color color = UIManager.getColor(key); return (color != null) ? color : UIManager.getColor(defaultKey); } /** * 获取key指定的int值,如果没有则使用后备key获取 * * @param key int所在的key * @param defaultKey 后备key * @return 长度 */ public static int getUIInt(String key, String defaultKey) { Object value = UIManager.get(key); return (value instanceof Integer) ? (Integer) value : UIManager.getInt(defaultKey); } /** * 获取key指定的int值,并根据dpi进行缩放 * * @param key int所在的key * @param defaultKey 后备key * @return 长度 */ public static int getAndScaleInt(String key, String defaultKey) { int intNum = getUIInt(key, defaultKey); return FineUIScale.scale(intNum); } /** * 获取key指定的int值,并根据dpi进行缩放 * * @param key int所在的key * @param defaultInt 默认值 * @return 长度 */ public static int getAndScaleInt(String key, int defaultInt) { int intNum = FlatUIUtils.getUIInt(key, defaultInt); return FineUIScale.scale(intNum); } /** * 通过key获取UI的边距,如果没有则使用后备key获取 * * @param key 边距key * @param defaultKey 边距后备key * @return 边距 */ public static Insets getUIInsets(String key, String defaultKey) { Insets margin = UIManager.getInsets(key); return (margin != null) ? margin : UIManager.getInsets(defaultKey); } /** * 通过key获取UI的边距,如果没有则使用后备边距 * * @param key 边距key * @param defaultInsets 后备边距 * @return 边距 */ public static Insets getUIInsets(String key, Insets defaultInsets) { Insets margin = UIManager.getInsets(key); return (margin != null) ? margin : defaultInsets; } /** * 通过key获取UI的边距,如果没有则使用后备边距,并根据dpi进行缩放 * * @param key 边距key * @param defaultInsets 后备边距 * @return 根据dpi缩放后的边距 */ public static Insets getAndScaleUIInsets(String key, Insets defaultInsets) { Insets margin = UIManager.getInsets(key); Insets insets = (margin != null) ? margin : defaultInsets; return FineUIScale.scale(insets); } /** * 绘制混合图像,含圆角、背景色设置 * * @param g 图像 * @param composite 混合图像 * @param background 背景色 * @param width 宽度 * @param height 高度 * @param arc 圆角 */ public static void paintWithComposite(Graphics g, Composite composite, Color background, int width, int height, int arc) { Graphics2D g2d = (Graphics2D) g; FlatUIUtils.setRenderingHints(g2d); Composite oldComposite = g2d.getComposite(); g2d.setComposite(composite); g2d.setColor(background); g2d.fill(new RoundRectangle2D.Float(0, 0, width, height, arc, arc)); g2d.setComposite(oldComposite); } /** * 绘制部分圆角矩形边框 * * @param g2 Graphics2D * @param x x坐标 * @param y y坐标 * @param width 宽度 * @param height 高度 * @param borderWidth 边框宽度 * @param arc 圆角 */ public static void paintPartRoundButtonBorder(Component c, Graphics2D g2, int x, int y, int width, int height, float borderWidth, float arc) { if (isLeftRoundButton(c)) { paintPartRoundButtonBorder(g2, x, y, width, height, borderWidth, arc, LEFT, false); } else { paintPartRoundButtonBorder(g2, x, y, width, height, borderWidth, arc, RIGHT, false); } } /** * 绘制部分圆角矩形边框 * * @param g2 Graphics2D * @param x x坐标 * @param y y坐标 * @param width 宽度 * @param height 高度 * @param borderWidth 边框宽度 * @param arc 圆角 * @param roundPart 圆角的方位,当前只能设置一侧 * @param closedPath 是否封闭,非圆角那一侧是否有边框,是为有边框 */ public static void paintPartRoundButtonBorder(Graphics2D g2, int x, int y, int width, int height, float borderWidth, float arc, String roundPart, boolean closedPath) { FlatUIUtils.setRenderingHints(g2); arc = scale(arc); float t = scale(borderWidth); float t2x = t * 2; Path2D path2D = new Path2D.Float(Path2D.WIND_EVEN_ODD); switch (roundPart) { case LEFT: { path2D.append(createLeftRoundRectangle(x, y, width, height, arc), false); path2D.append(createLeftRoundRectangle(x + t, y + t, width - (closedPath ? t2x : t), height - t2x, arc - t2x), false); break; } case RIGHT: default: { path2D.append(createRightRoundRectangle(x, y, width, height, arc), false); path2D.append(createRightRoundRectangle(x + (closedPath ? t : 0), y + t, width - (closedPath ? t2x : t), height - t2x, arc - t2x), false); break; } } g2.fill(path2D); } /** * 绘制圆角tab边框 * * @param g2 Graphics2D * @param x x坐标 * @param y y坐标 * @param width 宽度 * @param height 高度 * @param borderWidth 边框宽度 * @param arc 圆角 */ public static void paintRoundTabBorder(Graphics2D g2, double x, double y, double width, double height, float borderWidth, float arc) { FlatUIUtils.setRenderingHints(g2); arc = scale(arc); float t = scale(borderWidth); float t2x = t * 2; Path2D path2D = new Path2D.Float(Path2D.WIND_EVEN_ODD); path2D.append(createTopRoundRectangle(x, y, width, height, arc), false); path2D.append(createTopRoundRectangle(x + t, y + t, width - t2x, height - t, arc - t2x), false); g2.fill(path2D); } /** * 创建一个部分圆角的矩形路径 *

* 注意: * 在swing中,UI的样式的 arc 数值是直径,而 css 中 border-radius 为半径, * 因此我们配置的 arc 全部为 border-radius 的2倍。但是使用 Path2D 绘制时, * 绘制方式其实是使用半径来进行计算的,为了保持调用一致,对外 API 还是以 arc * 的形式,因此方法内部需要对 arc 进行取半处理, * * @param x x坐标 * @param y y坐标 * @param width 矩形宽度 * @param height 矩形高度 * @param arcTopLeft 左上圆角弧度 * @param arcTopRight 右上圆角弧度 * @param arcBottomRight 右下圆角弧度 * @param arcBottomLeft 左下圆角弧度 * @return 路径 */ public static Path2D createPartRoundRectangle(double x, double y, double width, double height, double arcTopLeft, double arcTopRight, double arcBottomRight, double arcBottomLeft) { double radiusTopLeft = arcTopLeft / 2; double radiusTopRight = arcTopRight / 2; double radiusBottomLeft = arcBottomLeft / 2; double radiusBottomRight = arcBottomRight / 2; Path2D path = new Path2D.Double(Path2D.WIND_EVEN_ODD, 10); path.moveTo(x + radiusTopLeft, y); path.lineTo(x + width - radiusTopRight, y); path.quadTo(x + width, y, x + width, y + radiusTopRight); path.lineTo(x + width, y + height - radiusBottomRight); path.quadTo(x + width, y + height, x + width - radiusBottomRight, y + height); path.lineTo(x + radiusBottomLeft, y + height); path.quadTo(x, y + height, x, y + height - radiusBottomLeft); path.lineTo(x, y + radiusTopLeft); path.quadTo(x, y, x + radiusTopLeft, y); path.closePath(); return path; } /** * 创建一个左圆角矩形路径 * * @param x x坐标 * @param y y坐标 * @param width 矩形宽度 * @param height 矩形高度 * @param arc 圆角弧度 * @return 路径 */ public static Path2D createLeftRoundRectangle(float x, float y, float width, float height, float arc) { return createPartRoundRectangle(x, y, width, height, arc, 0, 0, arc); } /** * 创建一个右圆角矩形路径 * * @param x x坐标 * @param y y坐标 * @param width 矩形宽度 * @param height 矩形高度 * @param arc 圆角弧度 * @return 路径 */ public static Path2D createRightRoundRectangle(float x, float y, float width, float height, float arc) { return createPartRoundRectangle(x, y, width, height, 0, arc, arc, 0); } /** * 创建一个顶圆角矩形路径 * * @param x x坐标 * @param y y坐标 * @param width 矩形宽度 * @param height 矩形高度 * @param arc 圆角弧度 * @return 路径 */ public static Path2D createTopRoundRectangle(double x, double y, double width, double height, double arc) { return createPartRoundRectangle(x, y, width, height, arc, arc, 0, 0); } /** * 创建一个左上圆角的矩形路径 * * @param x x坐标 * @param y y坐标 * @param width 矩形宽度 * @param height 矩形高度 * @param arc 圆角弧度 * @return 路径 */ public static Path2D createTopLeftRoundRectangle(float x, float y, float width, float height, float arc) { return createPartRoundRectangle(x, y, width, height, arc, 0, 0, 0); } /** * 创建一个左下圆角的矩形路径 * * @param x x坐标 * @param y y坐标 * @param width 矩形宽度 * @param height 矩形高度 * @param arc 圆角弧度 * @return 路径 */ public static Path2D createBottomLeftRoundRectangle(float x, float y, float width, float height, float arc) { return createPartRoundRectangle(x, y, width, height, 0, 0, 0, arc); } /** * 创建一个右上圆角的矩形路径 * * @param x x坐标 * @param y y坐标 * @param width 矩形宽度 * @param height 矩形高度 * @param arc 圆角弧度 * @return 路径 */ public static Path2D createTopRightRoundRectangle(float x, float y, float width, float height, float arc) { return createPartRoundRectangle(x, y, width, height, 0, arc, 0, 0); } /** * 创建一个右下圆角的矩形路径 * * @param x x坐标 * @param y y坐标 * @param width 矩形宽度 * @param height 矩形高度 * @param arc 圆角弧度 * @return 路径 */ public static Path2D createBottomRightRoundRectangle(float x, float y, float width, float height, float arc) { return createPartRoundRectangle(x, y, width, height, 0, 0, arc, 0); } /** * 标签加粗显示,并设置下划线;适用于小标题 * * @param label 标签 */ public static void wrapBoldLabelWithUnderline(JLabel label) { label.setBorder(FineBorderFactory.createDefaultUnderlineBorder()); FineUIStyle.setStyle(label, FineUIStyle.LABEL_BOLD); } /** * 面板元素头部添加小标题 * * @param component 面板元素 * @param title 标题文本 * @return 包装面板 */ public static Component wrapComponentWithTitle(Component component, String title) { UILabel label = new UILabel(title); wrapBoldLabelWithUnderline(label); return column(LayoutConstants.VERTICAL_GAP, cell(label), cell(component).weight(1.0) ).getComponent(); } /** * 基于组件创建一个UIScrollPane的装饰层,内部的ScrollPane仅当悬浮时显示滚动条 * * @param c 组件 * @return UIScrollPane的装饰层 */ public static JLayer createCollapsibleScrollBarLayer(Component c) { return new JLayer<>(new UIScrollPane(c, ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER), new CollapsibleScrollBarLayerUI()); } /** * 基于组件创建一个UIScrollPane的装饰层,内部的ScrollPane仅当悬浮时显示滚动条 * * @param c 组件 * @param verticalPolicy 滚动条垂直显示策略 * @param horizontalPolicy 滚动条水平显示策略 * @return UIScrollPane的装饰层 */ public static JLayer createCollapsibleScrollBarLayer(Component c, int verticalPolicy, int horizontalPolicy) { return new JLayer<>(new UIScrollPane(c, ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER), new CollapsibleScrollBarLayerUI(verticalPolicy, horizontalPolicy)); } /** * 设置组件字体大小,已适配字体缩放 * * @param c 组件 * @param size 字体大小 */ public static void setFontSize(Component c, int size) { Font font = c.getFont(); Font newSizeFont = font.deriveFont(font.getStyle(), scale(size)); c.setFont(newSizeFont); } /** * 获取缩放后的国际化尺寸 * * @param i18nDimensionKey 国际化key值 * @return 缩放后的国际化尺寸 */ public static Dimension getScaledI18nDimension(String i18nDimensionKey) { return FineUIScale.scale(DesignSizeI18nManager.getInstance().i18nDimension(i18nDimensionKey)); } /** * 创建一个支持自动换行的提示文本 * * @param text 显示的文本内容 * @return 自动换行提示文本 */ public static JTextArea createAutoWrapTipLabel(String text) { return LabelUtils.createAutoWrapLabel(text, FineUIUtils.getUIColor("Label.tipColor", "inactiveCaption")); } /** * 基于设计器父面板,计算当前面板尺寸 * @param width 宽度比例 * @param height 高度比例 * * @return 面板尺寸 */ public static Dimension calPaneDimensionByContext(double width, double height) { DesignerFrame parent = DesignerContext.getDesignerFrame(); return new Dimension((int) (parent.getWidth() * width),(int) (parent.getHeight() * height)); } }