diff --git a/designer-base/src/main/java/com/fine/component/popup/ComponentLayer.java b/designer-base/src/main/java/com/fine/component/popup/ComponentLayer.java new file mode 100755 index 0000000000..ccd2b13be6 --- /dev/null +++ b/designer-base/src/main/java/com/fine/component/popup/ComponentLayer.java @@ -0,0 +1,242 @@ +package com.fine.component.popup; + +import com.fine.theme.utils.FineUIUtils; +import com.formdev.flatlaf.ui.FlatUIUtils; +import com.formdev.flatlaf.util.Animator; +import com.formdev.flatlaf.util.CubicBezierEasing; +import com.formdev.flatlaf.util.UIScale; +import net.miginfocom.swing.MigLayout; + +import javax.swing.JPanel; +import java.awt.AlphaComposite; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.image.BufferedImage; +import java.util.Stack; + +/** + * 遮罩功能:渐显遮罩层 + * + * @author Levy.Xie + * @since 12.0 + * Created on 2024/12/12 + */ +public class ComponentLayer extends JPanel { + + private GlassPaneChild component; + private GlassPaneChild nextComponent; + private Animator animator; + private float animate; + private boolean showSnapshot; + private boolean simpleSnapshot; + private Image image; + + private Image nextImage; + private Stack componentStack; + private boolean push; + + private void pushStack(GlassPaneChild component) { + if (componentStack == null) { + componentStack = new Stack<>(); + } + componentStack.push(component); + } + + public ComponentLayer(GlassPaneChild component) { + this.component = component; + init(); + } + + private void init() { + MigLayout layout = new MigLayout("insets 0,fill", "fill", "fill"); + setLayout(layout); + if (component.getRoundBorder() > 0) { + setOpaque(false); + component.setOpaque(false); + } + add(component); + component.setVisible(false); + } + + private void initAnimator() { + animator = new Animator(350, new Animator.TimingTarget() { + + @Override + public void timingEvent(float v) { + animate = v; + repaint(); + } + + @Override + public void begin() { + nextImage = FineUIUtils.createImage(nextComponent); + } + + @Override + public void end() { + showSnapshot = false; + component = nextComponent; + component.popupShow(); + nextComponent = null; + animate = 0; + component.setVisible(true); + if (nextImage != null) { + nextImage.flush(); + } + } + }); + animator.setInterpolator(CubicBezierEasing.EASE); + } + + private void startAnimate() { + if (animator == null) { + initAnimator(); + } + if (animator.isRunning()) { + animator.stop(); + } + animate = 0; + animator.start(); + } + + /** + * 放入组件 + * + * @param component 组件 + */ + public void pushComponent(GlassPaneChild component) { + component.onPush(); + pushStack(this.component); + push = true; + showComponent(component); + } + + + /** + * 弹出组件 + */ + public void popComponent() { + if (!componentStack.isEmpty()) { + GlassPaneChild component = componentStack.pop(); + component.onPop(); + push = false; + showComponent(component); + } + } + + private void showComponent(GlassPaneChild component) { + showSnapshot = true; + this.nextComponent = component; + if (!component.isTransparent()) { + image = FineUIUtils.createImage(this.component); + } + if (component.getRoundBorder() > 0) { + setOpaque(false); + component.setOpaque(false); + } + component.setVisible(false); + remove(this.component); + add(component); + revalidate(); + startAnimate(); + } + + /** + * 显示遮罩 + */ + public void showSnapshot() { + if (animator != null && animator.isRunning()) { + animator.stop(); + } + simpleSnapshot = true; + doLayout(); + if (!component.isTransparent()) { + image = FineUIUtils.createImage(component, component.getRoundBorder()); + } + component.setVisible(false); + } + + /** + * 隐藏遮罩 + */ + public void hideSnapshot() { + showSnapshot = false; + simpleSnapshot = false; + component.setVisible(true); + if (image != null) { + image.flush(); + } + } + + @Override + protected void paintComponent(Graphics g) { + if (!component.isTransparent()) { + if (!isOpaque() && component.getRoundBorder() > 0) { + Graphics2D g2 = (Graphics2D) g.create(); + FlatUIUtils.setRenderingHints(g2); + g2.setColor(getBackground()); + int arc = UIScale.scale(component.getRoundBorder()); + FlatUIUtils.paintComponentBackground(g2, 0, 0, getWidth(), getHeight(), 0, arc); + g2.dispose(); + } + } + super.paintComponent(g); + } + + @Override + public void paint(Graphics g) { + if (simpleSnapshot) { + g.drawImage(image, 0, 0, null); + } else if (showSnapshot) { + int width = getWidth(); + int height = getHeight(); + if (width > 0 && height > 0) { + BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2 = bufferedImage.createGraphics(); + FlatUIUtils.setRenderingHints(g2); + int arc = UIScale.scale(component.getRoundBorder()); + g2.setColor(getBackground()); + FlatUIUtils.paintComponentBackground(g2, 0, 0, width, height, 0, arc); + if (image != null) { + paintImage(g2); + } + if (nextImage != null) { + paintNextImage(g2); + } + g2.dispose(); + g.drawImage(bufferedImage, 0, 0, null); + } + } else { + super.paint(g); + } + } + + private void paintNextImage(Graphics2D g2) { + int w = nextImage.getWidth(null); + double x; + if (push) { + x = getWidth() - (w * animate); + } else { + x = -getWidth() + (w * animate); + } + g2.setComposite(AlphaComposite.SrcAtop.derive(animate)); + g2.drawImage(nextImage, (int) x, 0, null); + } + + private void paintImage(Graphics2D g2) { + int w = image.getWidth(null); + double x; + if (push) { + x = -w * animate; + } else { + x = w * animate; + } + g2.setComposite(AlphaComposite.SrcAtop.derive(1f - animate)); + g2.drawImage(image, (int) x, 0, null); + } + + public GlassPaneChild getComponent() { + return component; + } +} diff --git a/designer-base/src/main/java/com/fine/component/popup/GlassPaneChild.java b/designer-base/src/main/java/com/fine/component/popup/GlassPaneChild.java new file mode 100755 index 0000000000..fc09817c31 --- /dev/null +++ b/designer-base/src/main/java/com/fine/component/popup/GlassPaneChild.java @@ -0,0 +1,59 @@ +package com.fine.component.popup; + +import com.formdev.flatlaf.ui.FlatUIUtils; + +import javax.swing.JPanel; + +/** + * 遮罩加载面板组件 + * + * @author Levy.Xie + * @since 12.0 + * Created on 2024/11/28 + */ +public class GlassPaneChild extends JPanel { + + protected boolean transparent; + + public int getRoundBorder() { + return FlatUIUtils.getUIInt("Component.arc", 6); + } + + /** + * 组件是否透明 + * + * @return 组件透明 + */ + protected boolean isTransparent() { + return transparent; + } + + /** + * 放入组件时的回调方法 + */ + public void onPush() { + + } + + /** + * 弹出组件时的回调方法 + */ + public void onPop() { + + } + + /** + * 显示弹窗组件时的回调方法 + */ + public void popupShow() { + + } + + /** + * 关闭窗口时的回调方法 + */ + public void onClose() { + + } + +} diff --git a/designer-base/src/main/java/com/fine/component/popup/GlassPopup.java b/designer-base/src/main/java/com/fine/component/popup/GlassPopup.java new file mode 100755 index 0000000000..f681f76dab --- /dev/null +++ b/designer-base/src/main/java/com/fine/component/popup/GlassPopup.java @@ -0,0 +1,139 @@ +package com.fine.component.popup; + +import com.formdev.flatlaf.FlatLaf; +import com.formdev.flatlaf.util.Animator; +import com.formdev.flatlaf.util.CubicBezierEasing; +import net.miginfocom.swing.MigLayout; + +import javax.swing.JComponent; +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.geom.Rectangle2D; + +/** + * 遮罩功能: 弹出面板组件 + * + * @author Levy.Xie + * @since 12.0 + * Created on 2024/12/12 + */ +public class GlassPopup extends JComponent { + private final GlassPopupManager parent; + private final ComponentLayer componentLayer; + private Animator animator; + private MigLayout layout; + private float animate; + private boolean show; + + public GlassPopup(GlassPopupManager parent, GlassPaneChild component) { + this.parent = parent; + this.componentLayer = new ComponentLayer(component); + init(); + initAnimator(); + } + + private void init() { + layout = new MigLayout(); + setLayout(layout); + add(componentLayer, getLayout(parent.layerPane, 0)); + } + + private void initAnimator() { + animator = new Animator(400, new Animator.TimingTarget() { + @Override + public void timingEvent(float fraction) { + if (show) { + animate = fraction; + } else { + animate = 1f - fraction; + } + float f = Math.round(animate * 100) / 100f; + String lc = getLayout(parent.layerPane, f); + layout.setComponentConstraints(componentLayer, lc); + repaint(); + revalidate(); + } + + @Override + public void begin() { + prepareAnimation(); + } + + @Override + public void end() { + finishAnimation(); + } + }); + animator.setInterpolator(CubicBezierEasing.EASE); + } + + private void prepareAnimation() { + componentLayer.showSnapshot(); + parent.windowSnapshots.createSnapshot(); + parent.contentPane.setVisible(false); + } + + private void finishAnimation() { + componentLayer.hideSnapshot(); + if (show) { + componentLayer.getComponent().popupShow(); + } else { + parent.removePopup(GlassPopup.this); + } + parent.contentPane.setVisible(true); + parent.windowSnapshots.removeSnapshot(); + } + + public void setShowPopup(boolean show) { + if (this.show != show) { + // 前置动画未结束时,先关闭前置动画 + if (animator.isRunning()) { + animator.stop(); + } + this.show = show; + animator.start(); + if (show) { + setFocusCycleRoot(true); + } else { + uninstallListener(); + } + } + } + + private void uninstallListener() { + if (getMouseListeners().length != 0) { + removeMouseListener(getMouseListeners()[0]); + } + } + + @Override + protected void paintComponent(Graphics g) { + Graphics2D g2 = (Graphics2D) g; + g2.setComposite(AlphaComposite.SrcOver.derive(animate * 0.5f)); + g2.setColor(background()); + g2.fill(new Rectangle2D.Double(0, 0, getWidth(), getHeight())); + g2.setComposite(AlphaComposite.SrcOver.derive(animate)); + super.paintComponent(g); + } + + protected Component getComponent() { + return componentLayer.getComponent(); + } + + protected ComponentLayer getComponentLayer() { + return componentLayer; + } + + private Color background() { + return FlatLaf.isLafDark() ? new Color(50, 50, 50) : new Color(20, 20, 20); + } + + private String getLayout(Component parent, float animate) { + float an = 20f / 600; + float space = 0.5f + an - (animate * an); + return "pos 0.5al " + space + "al,height ::100%-20"; + } +} diff --git a/designer-base/src/main/java/com/fine/component/popup/GlassPopupManager.java b/designer-base/src/main/java/com/fine/component/popup/GlassPopupManager.java new file mode 100755 index 0000000000..c286af31d8 --- /dev/null +++ b/designer-base/src/main/java/com/fine/component/popup/GlassPopupManager.java @@ -0,0 +1,292 @@ +package com.fine.component.popup; + +import javax.swing.JLayeredPane; +import javax.swing.JRootPane; +import javax.swing.SwingUtilities; +import java.awt.CardLayout; +import java.awt.Component; +import java.awt.Container; +import java.awt.Window; +import java.awt.event.MouseAdapter; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; + +/** + * 遮罩功能: 遮罩面板管理器 + * + * @author Levy.Xie + * @since 12.0 + * Created on 2024/12/12 + */ +public class GlassPopupManager { + + private static GlassPopupManager instance; + protected WindowSnapshots windowSnapshots; + protected JLayeredPane layerPane; + protected Container contentPane; + + private GlassPopupManager() { + init(); + } + + private void init() { + layerPane = new JLayeredPane(); + layerPane.setLayout(new CardLayout()); + } + + /** + * 添加并显示弹窗组件 + * + * @param component 组件 + * @param name 组件名 + */ + protected void addAndShowPopup(GlassPaneChild component, String name) { + GlassPopup popup = new GlassPopup(this, component); + instance.layerPane.applyComponentOrientation(instance.contentPane.getComponentOrientation()); + popup.applyComponentOrientation(instance.contentPane.getComponentOrientation()); + if (name != null) { + popup.setName(name); + } + layerPane.setLayer(popup, JLayeredPane.DRAG_LAYER); + layerPane.add(popup, 0); + popup.setVisible(true); + popup.setShowPopup(true); + if (!layerPane.isVisible()) { + layerPane.setVisible(true); + } + layerPane.grabFocus(); + } + + private void updateLayout() { + for (Component com : layerPane.getComponents()) { + com.revalidate(); + } + } + + /** + * 设置遮罩父窗口 + * + * @param container 遮罩父容器 + */ + public static void install(Container container) { + instance = new GlassPopupManager(); + Window window; + if (container instanceof Window) { + window = (Window) container; + } else { + window = SwingUtilities.getWindowAncestor(container); + } + instance.windowSnapshots = new WindowSnapshots(window); + + window.addWindowStateListener(new WindowAdapter() { + @Override + public void windowStateChanged(WindowEvent e) { + SwingUtilities.invokeLater(() -> { + instance.updateLayout(); + }); + } + }); + + JRootPane rootPane = SwingUtilities.getRootPane(container); + instance.contentPane = rootPane.getContentPane(); + rootPane.setGlassPane(instance.layerPane); + } + + /** + * 显示组件 + * + * @param component 组件 + * @param name 组件名 + */ + public static void showPopup(GlassPaneChild component, String name) { + if (component.getMouseListeners().length == 0) { + component.addMouseListener(new MouseAdapter() { + }); + } + instance.addAndShowPopup(component, name); + } + + /** + * 显示组件 + * + * @param component 组件 + */ + public static void showPopup(GlassPaneChild component) { + showPopup(component, null); + } + + /** + * 压栈,放入弹窗组件 + * + * @param component 组件 + * @param name 组件名 + */ + public static void push(GlassPaneChild component, String name) { + for (Component com : instance.layerPane.getComponents()) { + if (com.getName() != null && com.getName().equals(name)) { + if (com instanceof GlassPopup) { + GlassPopup popup = (GlassPopup) com; + popup.getComponentLayer().pushComponent(component); + break; + } + } + } + } + + /** + * 出栈,弹出弹窗组件 + * + * @param name 组件名 + */ + public static void pop(String name) { + for (Component com : instance.layerPane.getComponents()) { + if (com.getName() != null && com.getName().equals(name)) { + if (com instanceof GlassPopup) { + GlassPopup popup = (GlassPopup) com; + popup.getComponentLayer().popComponent(); + break; + } + } + } + } + + /** + * 出栈,弹出弹窗组件 + * + * @param component 组件 + */ + public static void pop(Component component) { + for (Component com : instance.layerPane.getComponents()) { + if (com instanceof GlassPopup) { + GlassPopup popup = (GlassPopup) com; + if (popup.getComponent() == component) { + popup.getComponentLayer().popComponent(); + } + } + } + } + + /** + * 基于序号关闭弹窗组件 + * + * @param index 序号 + */ + public static void closePopup(int index) { + index = instance.layerPane.getComponentCount() - 1 - index; + if (index >= 0 && index < instance.layerPane.getComponentCount()) { + if (instance.layerPane.getComponent(index) instanceof GlassPopup) { + GlassPopup popup = (GlassPopup) instance.layerPane.getComponent(index); + popup.setShowPopup(false); + } + } + } + + /** + * 基于名称关闭组件 + * + * @param name 组件名称 + */ + public static void closePopup(String name) { + for (Component com : instance.layerPane.getComponents()) { + if (com.getName() != null && com.getName().equals(name)) { + if (com instanceof GlassPopup) { + GlassPopup popup = (GlassPopup) com; + popup.setShowPopup(false); + } + } + } + } + + /** + * 关闭组件 + * + * @param component 组件 + */ + public static void closePopup(Component component) { + for (Component com : instance.layerPane.getComponents()) { + if (com instanceof GlassPopup) { + GlassPopup popup = (GlassPopup) com; + if (popup.getComponent() == component) { + popup.setShowPopup(false); + } + } + } + } + + /** + * 关闭最后一个组件 + */ + public static void closePopupLast() { + closePopup(getPopupCount() - 1); + } + + /** + * 关闭全部组件 + */ + public static void closePopupAll() { + for (Component com : instance.layerPane.getComponents()) { + if (com instanceof GlassPopup) { + GlassPopup popup = (GlassPopup) com; + popup.setShowPopup(false); + } + } + } + + /** + * 基于组件名,判断组件是否显示 + * + * @param name 组件名 + * @return 是否显示 + */ + public static boolean isShowing(String name) { + boolean act = false; + for (Component com : instance.layerPane.getComponents()) { + if (com.getName() != null && com.getName().equals(name)) { + act = true; + break; + } + } + return act; + } + + /** + * 判断组件是否显示 + * + * @param component 组件 + * @return 是否显示 + */ + public static boolean isShowing(Component component) { + boolean act = false; + for (Component com : instance.layerPane.getComponents()) { + if (com instanceof GlassPopup) { + GlassPopup popup = (GlassPopup) com; + if (popup.getComponent() == component) { + act = true; + break; + } + } + } + return act; + } + + /** + * 获取弹窗组件数量 + * + * @return 弹窗数量 + */ + public static int getPopupCount() { + return instance.layerPane.getComponentCount(); + } + + /** + * 移除组件 + * + * @param popup 弹窗组件 + */ + protected synchronized void removePopup(Component popup) { + layerPane.remove(popup); + if (layerPane.getComponentCount() == 0) { + layerPane.setVisible(false); + } + } +} diff --git a/designer-base/src/main/java/com/fine/component/popup/ImageChild.java b/designer-base/src/main/java/com/fine/component/popup/ImageChild.java new file mode 100644 index 0000000000..adf14061f1 --- /dev/null +++ b/designer-base/src/main/java/com/fine/component/popup/ImageChild.java @@ -0,0 +1,38 @@ +package com.fine.component.popup; + +import com.formdev.flatlaf.util.ScaledEmptyBorder; +import com.fr.design.gui.ilable.UILabel; + +import javax.swing.ImageIcon; +import java.awt.BorderLayout; +import java.util.Objects; + +/** + * 图片遮罩层,支撑静态图片及动态GIF + * + * @author Levy.Xie + * @since 12.0 + * Created on 2024/12/20 + */ +public class ImageChild extends GlassPaneChild { + + private static final String DEFAULT_LOADING_PATH = "com/fine/component/pop/loading.gif"; + + public ImageChild() { + this(DEFAULT_LOADING_PATH); + } + + public ImageChild(String imgPath) { + this(imgPath, true); + } + + public ImageChild(String imgPath, boolean transparent) { + this.transparent = transparent; + + setLayout(new BorderLayout()); + setBorder(new ScaledEmptyBorder(8, 8, 8, 8)); + ImageIcon icon = new ImageIcon(Objects.requireNonNull(getClass().getClassLoader().getResource(imgPath))); + add(new UILabel(icon), BorderLayout.CENTER); + } + +} diff --git a/designer-base/src/main/java/com/fine/component/popup/ProgressChild.java b/designer-base/src/main/java/com/fine/component/popup/ProgressChild.java new file mode 100644 index 0000000000..d56cafc95a --- /dev/null +++ b/designer-base/src/main/java/com/fine/component/popup/ProgressChild.java @@ -0,0 +1,142 @@ +package com.fine.component.popup; + +import com.fine.theme.utils.FineUIScale; +import com.formdev.flatlaf.util.ScaledEmptyBorder; +import com.fr.concurrent.NamedThreadFactory; +import com.fr.design.locale.impl.SupportLocaleImpl; +import com.fr.design.ui.util.UIUtil; +import com.fr.design.utils.DesignUtils; +import com.fr.general.FRFont; +import com.fr.general.locale.LocaleCenter; + +import javax.swing.JLabel; +import javax.swing.JProgressBar; +import javax.swing.plaf.ColorUIResource; + +import java.awt.BorderLayout; +import java.awt.Font; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import static com.fine.swing.ui.layout.Layouts.cell; +import static com.fine.swing.ui.layout.Layouts.column; +import static com.fine.swing.ui.layout.Layouts.flex; +import static com.fine.swing.ui.layout.Layouts.row; +import static com.fine.theme.utils.FineUIScale.scale; + +/** + * 进度条遮罩组件 + * + * @author Levy.Xie + * @since 11.0 + * Created on 2024/12/13 + */ +public class ProgressChild extends GlassPaneChild { + + private int progress; + protected JLabel text; + protected JProgressBar progressBar; + + private int step = 10; + private static final int STEP_HEARTBEAT = 40; + + private static final int FONT_RGB = 333334; + private static final int FONT_SIZE = 14; + + private static final String FONT_NAME = "Dialog"; + + public ProgressChild(String text) { + this(text, DesignUtils.getDefaultGUIFont() + .applySize(scale(FONT_SIZE)) + .applyForeground(new ColorUIResource(FONT_RGB))); + } + + public ProgressChild(String text, Font font) { + initComponents(text, font); + initLayout(); + initScheduler(); + } + + private void initComponents(String text, Font font) { + this.text = new JLabel(text); + this.text.setFont(font); + //由于默认值的字体不支持韩文,所以要对韩文单独生成字体 + LocaleCenter.buildAction(() -> { + FRFont frFont = FRFont.getInstance().applySize(FONT_SIZE) + .applyForeground(new ColorUIResource(FONT_RGB)).applyName(FONT_NAME); + this.text.setFont(frFont); + }, SupportLocaleImpl.SUPPORT_KOREA); + this.progressBar = new JProgressBar(); + progressBar.setBorderPainted(false); + progressBar.setOpaque(false); + progressBar.setBorder(null); + progressBar.setMaximum(1000); + } + + /** + * 设置进度条最大加载时间,单位为s + * + * @param maxWait 最大等待时间 + * @return 进度条遮罩层 + */ + public ProgressChild setMaxWait(int maxWait) { + this.step = progressBar.getMaximum() * STEP_HEARTBEAT / (maxWait * 1000); + return this; + } + + private void initLayout() { + setLayout(new BorderLayout()); + setPreferredSize(FineUIScale.createScaleDimension(400, 100)); + add(column(10, + cell(progressBar).weight(1), row(flex(), cell(text), flex()).weight(1.5) + ).with(it -> it.setBorder(new ScaledEmptyBorder(30, 30, 20, 30))).getComponent(), + BorderLayout.CENTER); + } + + private void initScheduler() { + final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1, + new NamedThreadFactory(getClass().getSimpleName())); + scheduler.scheduleWithFixedDelay(() -> { + if (isComplete() && !scheduler.isShutdown()) { + scheduler.shutdown(); + return; + } + UIUtil.invokeLaterIfNeeded(() -> progressBar.setValue(incrementProgress())); + }, 0, STEP_HEARTBEAT, TimeUnit.MILLISECONDS); + } + + /** + * 递增进度条 + * + * @return 进度值 + */ + public int incrementProgress() { + if (progress != progressBar.getMaximum()) { + progress += step; + } + return progress; + } + + @Override + public void onClose() { + this.progress = progressBar.getMaximum(); + } + + /** + * 重置进度条 + */ + public void reset() { + this.progress = 0; + } + + /** + * 进度是否已结束 + * + * @return 进度结束状态 + */ + public boolean isComplete() { + return this.progress >= progressBar.getMaximum(); + } + +} diff --git a/designer-base/src/main/java/com/fine/component/popup/WindowSnapshots.java b/designer-base/src/main/java/com/fine/component/popup/WindowSnapshots.java new file mode 100755 index 0000000000..409acc15ea --- /dev/null +++ b/designer-base/src/main/java/com/fine/component/popup/WindowSnapshots.java @@ -0,0 +1,91 @@ +package com.fine.component.popup; + +import org.jetbrains.annotations.NotNull; + +import javax.swing.JComponent; +import javax.swing.JLayeredPane; +import javax.swing.JRootPane; +import javax.swing.SwingUtilities; +import java.awt.Graphics; +import java.awt.Window; +import java.awt.image.VolatileImage; + +/** + * WindowSnapshots, 捕获 Window 的快照 + * + * @author Levy.Xie + * @since 12.0 + * Created on 2024/12/12 + */ +public class WindowSnapshots { + + private final Window window; + private JComponent snapshotLayer; + private boolean inShowSnapshot; + + public WindowSnapshots(Window window) { + this.window = window; + } + + /** + * 创建窗口快照并将其显示为覆盖层 + */ + public void createSnapshot() { + if (inShowSnapshot) { + return; + } + inShowSnapshot = true; + if ((window != null && window.isShowing())) { + VolatileImage snapshot = window.createVolatileImage(window.getWidth(), window.getHeight()); + if (snapshot != null) { + JLayeredPane layeredPane = getLayeredPane(); + if (layeredPane != null) { + layeredPane.paint(snapshot.getGraphics()); + snapshotLayer = createSnapLayer(snapshot); + snapshotLayer.setSize(layeredPane.getSize()); + layeredPane.add(snapshotLayer, Integer.valueOf(JLayeredPane.DRAG_LAYER + 1)); + } + } + } + } + + private static @NotNull JComponent createSnapLayer(VolatileImage snapshot) { + return new JComponent() { + @Override + public void paint(Graphics g) { + if (snapshot.contentsLost()) { + return; + } + g.drawImage(snapshot, 0, 0, null); + } + + @Override + public void removeNotify() { + super.removeNotify(); + snapshot.flush(); + } + }; + } + + /** + * 移除窗口快照覆盖层 + */ + public void removeSnapshot() { + if (snapshotLayer == null) { + return; + } + JLayeredPane layeredPane = getLayeredPane(); + if (layeredPane != null) { + layeredPane.remove(snapshotLayer); + } + inShowSnapshot = false; + } + + private JLayeredPane getLayeredPane() { + JRootPane rootPane = SwingUtilities.getRootPane(window); + if (rootPane != null) { + return rootPane.getLayeredPane(); + } + return null; + } +} diff --git a/designer-base/src/main/java/com/fine/component/toast/ToastMsgHolder.java b/designer-base/src/main/java/com/fine/component/toast/ToastMsgHolder.java new file mode 100755 index 0000000000..3dcea733a0 --- /dev/null +++ b/designer-base/src/main/java/com/fine/component/toast/ToastMsgHolder.java @@ -0,0 +1,76 @@ +package com.fine.component.toast; + +import java.util.ArrayList; +import java.util.List; + +/** + * 弹出消息容器 + * + * @author Levy.Xie + * @since 12.0 + * Created on 2024/12/12 + */ +public class ToastMsgHolder { + + private final List lists = new ArrayList<>(); + private final Object lock = new Object(); + + public ToastMsgManager.ToastAnimation getHold(ToastMsgManager.Location location) { + synchronized (lock) { + for (ToastMsgManager.ToastAnimation n : lists) { + if (n.getLocation() == location) { + return n; + } + } + return null; + } + } + + /** + * 删除弹窗动画 + * + * @param toastAnimation 弹窗动画 + */ + public void removeHold(ToastMsgManager.ToastAnimation toastAnimation) { + synchronized (lock) { + lists.remove(toastAnimation); + } + } + + /** + * 添加弹窗动画 + * + * @param toastAnimation 弹窗动画 + */ + public void hold(ToastMsgManager.ToastAnimation toastAnimation) { + synchronized (lock) { + lists.add(toastAnimation); + } + } + + /** + * 清空弹窗动画 + */ + public void clearHold() { + synchronized (lock) { + lists.clear(); + } + } + + /** + * 清空指定位置的弹窗动画 + * + * @param location 弹窗位置 + */ + public void clearHold(ToastMsgManager.Location location) { + synchronized (lock) { + for (int i = 0; i < lists.size(); i++) { + ToastMsgManager.ToastAnimation n = lists.get(i); + if (n.getLocation() == location) { + lists.remove(n); + i--; + } + } + } + } +} diff --git a/designer-base/src/main/java/com/fine/component/toast/ToastMsgManager.java b/designer-base/src/main/java/com/fine/component/toast/ToastMsgManager.java new file mode 100755 index 0000000000..c70cf828b7 --- /dev/null +++ b/designer-base/src/main/java/com/fine/component/toast/ToastMsgManager.java @@ -0,0 +1,566 @@ +package com.fine.component.toast; + +import com.fine.theme.utils.FineClientProperties; +import com.fine.theme.utils.FineUIUtils; +import com.formdev.flatlaf.ui.FlatUIUtils; +import com.formdev.flatlaf.util.Animator; +import com.formdev.flatlaf.util.UIScale; +import com.fr.design.mainframe.DesignerContext; + +import javax.swing.JComponent; +import javax.swing.JWindow; +import javax.swing.SwingUtilities; +import java.awt.Color; +import java.awt.Insets; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Toolkit; +import java.awt.Window; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * Toast Message Manager + * + *

+ * Toast.outlineWidth int 0 (default) + * Toast.iconTextGap int 5 (default) + * Toast.closeButtonGap int 5 (default) + * Toast.arc int 20 (default) + * Toast.horizontalGap int 10 (default) + *

+ * Toast.limit int -1 (default) -1 as unlimited + * Toast.duration long 2500 (default) + * Toast.animation int 200 (default) + * Toast.animationResolution int 5 (default) + * Toast.animationMove int 10 (default) + * Toast.minimumWidth int 50 (default) + * Toast.maximumWidth int -1 (default) -1 as not set + *

+ * Toast.shadowColor Color + * Toast.shadowOpacity float 0.1f (default) + * Toast.shadowInsets Insets 0,0,6,6 (default) + *

+ * Toast.outlineColor Color + * Toast.foreground Color + * Toast.background Color + *

+ * Toast.success.outlineColor Color + * Toast.success.foreground Color + * Toast.success.background Color + * Toast.info.outlineColor Color + * Toast.info.foreground Color + * Toast.info.background Color + * Toast.warning.outlineColor Color + * Toast.warning.foreground Color + * Toast.warning.background Color + * Toast.error.outlineColor Color + * Toast.error.foreground Color + * Toast.error.background Color + *

+ * Toast.frameInsets Insets 10,10,10,10 (default) + * Toast.margin Insets 8,8,8,8 (default) + *

+ * Toast.showCloseButton boolean true (default) + * Toast.closeIconColor Color + * + * @author Levy.Xie + * @since 12.0 + * Created on 2024/12/10 + */ +public class ToastMsgManager { + + private static ToastMsgManager instance; + private Window window; + private final Map> lists = new HashMap<>(); + private final ToastMsgHolder toastMsgHolder = new ToastMsgHolder(); + + private ComponentListener windowEvent; + + private void installEvent(Window window) { + if (windowEvent == null && window != null) { + windowEvent = new ComponentAdapter() { + @Override + public void componentMoved(ComponentEvent e) { + move(window.getBounds()); + } + + @Override + public void componentResized(ComponentEvent e) { + move(window.getBounds()); + } + }; + } + if (this.window != null) { + this.window.removeComponentListener(windowEvent); + } + if (window != null) { + window.addComponentListener(windowEvent); + } + this.window = window; + } + + public static ToastMsgManager getInstance() { + if (instance == null) { + instance = new ToastMsgManager(); + } + return instance; + } + + private int getCurrentShowCount(Location location) { + List list = lists.get(location); + return list == null ? 0 : list.size(); + } + + private synchronized void move(Rectangle rectangle) { + for (Map.Entry> set : lists.entrySet()) { + for (int i = 0; i < set.getValue().size(); i++) { + ToastAnimation an = set.getValue().get(i); + if (an != null) { + an.move(rectangle); + } + } + } + } + + public void setWindow(Window window) { + installEvent(window); + } + + /** + * 弹窗: 成功提示 + * + * @param message 文案 + * @param window 父窗体 + */ + public void success(String message, Window window) { + show(Type.SUCCESS, Location.TOP_CENTER, message, window); + } + + /** + * 弹窗: 成功提示 + * + * @param message 文案 + */ + public void success(String message) { + show(Type.SUCCESS, Location.TOP_CENTER, message); + } + + /** + * 弹窗: 信息提示 + * + * @param message 文案 + * @param window 父窗体 + */ + public void info(String message, Window window) { + show(Type.INFO, Location.TOP_CENTER, message, window); + } + + /** + * 弹窗: 信息提示 + * + * @param message 文案 + */ + public void info(String message) { + show(Type.INFO, Location.TOP_CENTER, message); + } + + /** + * 弹窗: 警告提示 + * + * @param message 文案 + * @param window 父窗体 + */ + public void warn(String message, Window window) { + show(Type.WARNING, Location.TOP_CENTER, message, window); + } + + /** + * 弹窗: 警告提示 + * + * @param message 文案 + */ + public void warn(String message) { + show(Type.WARNING, Location.TOP_CENTER, message); + } + + /** + * 弹窗: 错误提示 + * + * @param message 文案 + * @param window 父窗体 + */ + public void error(String message, Window window) { + show(Type.ERROR, Location.TOP_CENTER, message, window); + } + + /** + * 弹窗: 错误提示 + * + * @param message 文案 + */ + public void error(String message) { + show(Type.ERROR, Location.TOP_CENTER, message); + } + + /** + * 弹窗: 支持自定义通知类型、位置、信息、父窗体 + * + * @param type 通知类型 + * @param location 位置 + * @param message 信息 + */ + public void show(Type type, Location location, String message, Window window) { + setWindow(window); + long duration = FlatUIUtils.getUIInt("Toast.duration", 2500); + initStart(new ToastAnimation(type, location, duration, message), duration); + } + + /** + * 弹窗: 支持自定义通知类型、位置、信息。父窗体默认为设计器主Frame + * + * @param type 通知类型 + * @param location 位置 + * @param message 信息 + */ + public void show(Type type, Location location, String message) { + show(type, location, message, DesignerContext.getDesignerFrame()); + } + + /** + * 弹窗: 组件提示 + * + * @param component 组件 + */ + public void show(JComponent component) { + long duration = FlatUIUtils.getUIInt("Toast.duration", 2500); + initStart(new ToastAnimation(Location.TOP_CENTER, duration, component), duration); + } + + + private synchronized boolean initStart(ToastAnimation toastAnimation, long duration) { + int limit = FlatUIUtils.getUIInt("Toast.limit", -1); + if (limit == -1 || getCurrentShowCount(toastAnimation.getLocation()) < limit) { + toastAnimation.start(); + return true; + } else { + toastMsgHolder.hold(toastAnimation); + return false; + } + } + + private synchronized void closeToast(ToastAnimation toastAnimation) { + ToastAnimation hold = toastMsgHolder.getHold(toastAnimation.getLocation()); + if (hold != null) { + if (initStart(hold, hold.getDuration())) { + toastMsgHolder.removeHold(hold); + } + } + } + + /** + * 清理全量弹出消息 + */ + public void clearAll() { + toastMsgHolder.clearHold(); + for (Map.Entry> set : lists.entrySet()) { + for (int i = 0; i < set.getValue().size(); i++) { + ToastAnimation an = set.getValue().get(i); + if (an != null) { + an.close(); + } + } + } + } + + /** + * 清理弹出消息 + * + * @param location 位置 + */ + public void clear(Location location) { + toastMsgHolder.clearHold(location); + List list = lists.get(location); + if (list != null) { + for (ToastAnimation an : list) { + if (an != null) { + an.close(); + } + } + } + } + + /** + * 创建消息弹窗 + * + * @param type 消息类型 + * @param message 消息 + * @return 弹出消息面板 + */ + protected ToastPane createToastPane(Type type, String message) { + ToastPane toastPanel = new ToastPane(); + toastPanel.set(type, message); + return toastPanel; + } + + private synchronized void updateList(Location key, ToastAnimation values, boolean add) { + if (add) { + if (lists.containsKey(key)) { + lists.get(key).add(values); + } else { + List list = new ArrayList<>(); + list.add(values); + lists.put(key, list); + } + } else { + if (lists.containsKey(key)) { + lists.get(key).remove(values); + if (lists.get(key).isEmpty()) { + lists.remove(key); + } + } + } + } + + /** + * 消息类型 + */ + public enum Type { + // 成功 + SUCCESS, + // 通知 + INFO, + // 警告 + WARNING, + // 错误 + ERROR + } + + /** + * 消息位置 + */ + public enum Location { + // 顶部居左 + TOP_LEFT, + // 顶部居中 + TOP_CENTER, + // 顶部居右 + TOP_RIGHT, + // 底部居左 + BOTTOM_LEFT, + // 底部居中 + BOTTOM_CENTER, + // 底部具右 + BOTTOM_RIGHT + } + + /** + * 消息弹窗动画 + */ + public class ToastAnimation { + + private JWindow window; + private Animator animator; + private boolean show = true; + private float animate; + private int x; + private int y; + private final Location location; + private final long duration; + private Insets frameInsets; + private int horizontalSpace; + private int animationMove; + private boolean top; + private boolean close = false; + + public ToastAnimation(Type type, Location location, long duration, String message) { + installDefault(); + this.location = location; + this.duration = duration; + window = new JWindow(ToastMsgManager.this.window); + ToastPane toastPanel = createToastPane(type, message); + toastPanel.putClientProperty(FineClientProperties.TOAST_CLOSE_CALLBACK, (Consumer) o -> close()); + window.setContentPane(toastPanel); + window.setFocusableWindowState(false); + window.pack(); + toastPanel.setDialog(window); + } + + public ToastAnimation(Location location, long duration, JComponent component) { + installDefault(); + this.location = location; + this.duration = duration; + window = new JWindow(ToastMsgManager.this.window); + window.setBackground(new Color(0, 0, 0, 0)); + window.setContentPane(component); + window.setFocusableWindowState(false); + window.setSize(component.getPreferredSize()); + } + + private void installDefault() { + frameInsets = FineUIUtils.getUIInsets("Toast.frameInsets", new Insets(25, 25, 25, 25)); + horizontalSpace = FlatUIUtils.getUIInt("Toast.horizontalGap", 10); + animationMove = FlatUIUtils.getUIInt("Toast.animationMove", 10); + } + + /** + * 开始动画 + */ + public void start() { + int animation = FlatUIUtils.getUIInt("Toast.duration", 200); + int resolution = FlatUIUtils.getUIInt("Toast.animationResolution", 5); + animator = new Animator(animation, new Animator.TimingTarget() { + @Override + public void begin() { + if (show) { + updateList(location, ToastAnimation.this, true); + installLocation(); + } + } + + @Override + public void timingEvent(float f) { + animate = show ? f : 1f - f; + updateLocation(true); + } + + @Override + public void end() { + if (show && !close) { + closeAnimation(); + } else { + updateList(location, ToastAnimation.this, false); + window.dispose(); + closeToast(ToastAnimation.this); + } + } + }); + animator.setResolution(resolution); + animator.start(); + } + + private void closeAnimation() { + SwingUtilities.invokeLater(() -> new Thread(() -> { + try { + Thread.sleep(duration); + } catch (InterruptedException ignored) { + } + if (!close) { + show = false; + animator.start(); + } + }).start()); + } + + private void installLocation() { + Insets insets; + Rectangle rec; + if (ToastMsgManager.this.window == null) { + insets = UIScale.scale(frameInsets); + rec = new Rectangle(new Point(0, 0), Toolkit.getDefaultToolkit().getScreenSize()); + } else { + insets = UIScale.scale(FlatUIUtils.addInsets(frameInsets, ToastMsgManager.this.window.getInsets())); + rec = ToastMsgManager.this.window.getBounds(); + } + setupLocation(rec, insets); + window.setOpacity(0f); + window.setVisible(true); + } + + private void move(Rectangle rec) { + Insets insets = UIScale.scale(FlatUIUtils.addInsets(frameInsets, ToastMsgManager.this.window.getInsets())); + setupLocation(rec, insets); + } + + private void setupLocation(Rectangle rec, Insets insets) { + if (location == Location.TOP_LEFT) { + x = rec.x + insets.left; + y = rec.y + insets.top; + top = true; + } else if (location == Location.TOP_CENTER) { + x = rec.x + (rec.width - window.getWidth()) / 2; + y = rec.y + insets.top; + top = true; + } else if (location == Location.TOP_RIGHT) { + x = rec.x + rec.width - (window.getWidth() + insets.right); + y = rec.y + insets.top; + top = true; + } else if (location == Location.BOTTOM_LEFT) { + x = rec.x + insets.left; + y = rec.y + rec.height - (window.getHeight() + insets.bottom); + top = false; + } else if (location == Location.BOTTOM_CENTER) { + x = rec.x + (rec.width - window.getWidth()) / 2; + y = rec.y + rec.height - (window.getHeight() + insets.bottom); + top = false; + } else if (location == Location.BOTTOM_RIGHT) { + x = rec.x + rec.width - (window.getWidth() + insets.right); + y = rec.y + rec.height - (window.getHeight() + insets.bottom); + top = false; + } + int am = UIScale.scale(top ? animationMove : -animationMove); + int ly = (int) (getLocation(ToastAnimation.this) + y + animate * am); + window.setLocation(x, ly); + } + + private void updateLocation(boolean loop) { + int am = UIScale.scale(top ? animationMove : -animationMove); + int ly = (int) (getLocation(ToastAnimation.this) + y + animate * am); + window.setLocation(x, ly); + window.setOpacity(animate); + if (loop) { + update(this); + } + } + + private int getLocation(ToastAnimation notification) { + int height = 0; + List list = lists.get(location); + for (ToastAnimation n : list) { + if (notification == n) { + return height; + } + double v = n.animate * (n.window.getHeight() + UIScale.scale(horizontalSpace)); + height += (int) (top ? v : -v); + } + return height; + } + + private void update(ToastAnimation except) { + List list = lists.get(location); + for (ToastAnimation n : list) { + if (n != except) { + n.updateLocation(false); + } + } + } + + /** + * 关闭动画 + */ + public void close() { + if (show) { + if (animator.isRunning()) { + animator.stop(); + } + close = true; + show = false; + animator.start(); + } + } + + public Location getLocation() { + return location; + } + + public long getDuration() { + return duration; + } + } +} diff --git a/designer-base/src/main/java/com/fine/component/toast/ToastPane.java b/designer-base/src/main/java/com/fine/component/toast/ToastPane.java new file mode 100755 index 0000000000..c6f386fabb --- /dev/null +++ b/designer-base/src/main/java/com/fine/component/toast/ToastPane.java @@ -0,0 +1,95 @@ +package com.fine.component.toast; + +import com.fine.theme.icon.LazyIcon; +import com.fine.theme.light.ui.ToastPanelUI; +import com.fine.theme.utils.FineClientProperties; +import com.fr.stable.StringUtils; + +import javax.swing.Icon; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextPane; +import javax.swing.JWindow; +import java.awt.Color; +import java.awt.Cursor; + +/** + * 弹窗通知面板 + * + * @author Levy.Xie + * @since 12.0 + * Created on 2024/11/27 + */ +public class ToastPane extends JPanel { + + private static final String UI_CLASS_ID = "ToastPanelUI"; + + protected JWindow window; + protected JLabel labelIcon; + protected JTextPane textPane; + + private ToastMsgManager.Type type; + + public ToastPane() { + installDefault(); + } + + @Override + public void updateUI() { + setUI(new ToastPanelUI()); + removeDialogBackground(); + } + + @Override + public String getUIClassID() { + return UI_CLASS_ID; + } + + private void removeDialogBackground() { + if (window != null) { + Color bg = getBackground(); + window.setBackground(new Color(bg.getRed(), bg.getGreen(), bg.getBlue(), 0)); + window.setSize(getPreferredSize()); + } + } + + private void installDefault() { + labelIcon = new JLabel(); + textPane = new JTextPane(); + textPane.setText(StringUtils.EMPTY); + textPane.setOpaque(false); + textPane.setFocusable(false); + textPane.setCursor(Cursor.getDefaultCursor()); + putClientProperty(FineClientProperties.TOAST_ICON, labelIcon); + putClientProperty(FineClientProperties.TOAST_COMPONENT, textPane); + } + + public void set(ToastMsgManager.Type type, String message) { + this.type = type; + labelIcon.setIcon(getDefaultIcon()); + textPane.setText(message); + } + + public void setDialog(JWindow window) { + this.window = window; + removeDialogBackground(); + } + + public Icon getDefaultIcon() { + String key = getKey(); + return new LazyIcon(key, 20); + } + + public String getKey() { + if (type == ToastMsgManager.Type.SUCCESS) { + return "success"; + } else if (type == ToastMsgManager.Type.INFO) { + return "information"; + } else if (type == ToastMsgManager.Type.WARNING) { + return "warning"; + } else { + return "error"; + } + } + +} diff --git a/designer-base/src/main/java/com/fine/theme/light/ui/ShadowBorder.java b/designer-base/src/main/java/com/fine/theme/light/ui/ShadowBorder.java new file mode 100755 index 0000000000..637879f018 --- /dev/null +++ b/designer-base/src/main/java/com/fine/theme/light/ui/ShadowBorder.java @@ -0,0 +1,125 @@ +package com.fine.theme.light.ui; + +import com.fine.theme.utils.ShadowRenderer; +import com.formdev.flatlaf.FlatPropertiesLaf; +import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable; +import com.formdev.flatlaf.ui.FlatUIUtils; +import com.formdev.flatlaf.util.UIScale; +import com.fr.stable.AssistUtils; + +import javax.swing.JComponent; +import javax.swing.border.EmptyBorder; +import java.awt.Color; +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Insets; +import java.awt.Shape; +import java.awt.image.BufferedImage; + +/** + * 阴影边框 + * + * @author Levy.Xie + * @since 12.0 + * Created on 2024/12/12 + */ +public class ShadowBorder extends EmptyBorder { + + @Styleable + protected Color shadowColor; + @Styleable + protected Insets shadowInsets; + @Styleable + protected float shadowOpacity; + + private Image shadowImage; + private int shadowSize; + private Color lastShadowColor; + private float lastShadowOpacity; + private int lastShadowSize; + private int lastArc; + private int lastWidth; + private int lastHeight; + + public ShadowBorder() { + this(new Color(0, 0, 0), new Insets(6, 6, 6, 6), 0.1f); + } + + public ShadowBorder(Color shadowColor, Insets shadowInsets, float shadowOpacity) { + super(nonNegativeInsets(shadowInsets)); + + this.shadowColor = shadowColor; + this.shadowInsets = shadowInsets; + this.shadowOpacity = shadowOpacity; + + this.shadowSize = maxInset(shadowInsets); + } + + private static Insets nonNegativeInsets(Insets shadowInsets) { + return new Insets(Math.max(shadowInsets.top, 0), Math.max(shadowInsets.left, 0), Math.max(shadowInsets.bottom, 0), Math.max(shadowInsets.right, 0)); + } + + private int maxInset(Insets shadowInsets) { + return Math.max(Math.max(shadowInsets.left, shadowInsets.right), Math.max(shadowInsets.top, shadowInsets.bottom)); + } + + @Override + public Insets getBorderInsets() { + return UIScale.scale(super.getBorderInsets()); + } + + @Override + public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) { + JComponent com = (JComponent) c; + int arc = FlatPropertiesLaf.getStyleableValue(com, "arc"); + if (shadowImage == null || !shadowColor.equals(lastShadowColor) + || width != lastWidth || height != lastHeight || shadowSize != lastShadowSize + || !AssistUtils.equals(shadowOpacity, lastShadowOpacity) || arc != lastArc) { + shadowImage = createShadowImage(width, height, arc); + lastShadowColor = shadowColor; + lastWidth = width; + lastHeight = height; + lastShadowSize = shadowSize; + lastShadowOpacity = shadowOpacity; + lastArc = arc; + } + g.drawImage(shadowImage, 0, 0, null); + Insets insets = getBorderInsets(); + int lx = insets.left; + int ly = insets.top; + int lw = width - (insets.left + insets.right); + int lh = height - (insets.top + insets.bottom); + Graphics2D g2 = (Graphics2D) g.create(); + doPaintShadow(c, arc, g2, lx, ly, lw, lh, com); + g2.dispose(); + } + + private static void doPaintShadow(Component c, int arc, Graphics2D g2, int lx, int ly, int lw, int lh, JComponent component) { + if (arc > 0) { + FlatUIUtils.setRenderingHints(g2); + g2.setColor(c.getBackground()); + FlatUIUtils.paintComponentBackground(g2, lx, ly, lw, lh, 0, UIScale.scale(arc)); + } else { + g2.setColor(c.getBackground()); + g2.fillRect(lx, ly, lw, lh); + } + int outlineWidth = FlatPropertiesLaf.getStyleableValue(component, "outlineWidth"); + if (outlineWidth > 0) { + Color outlineColor = FlatPropertiesLaf.getStyleableValue(component, "outlineColor"); + g2.setColor(outlineColor); + FlatUIUtils.paintOutline(g2, lx, ly, lw, lh, UIScale.scale(outlineWidth), UIScale.scale(arc)); + } + } + + private BufferedImage createShadowImage(int width, int height, int arc) { + int size = UIScale.scale(shadowSize); + float round = UIScale.scale(arc * 0.7f); + int shadowWidth = width - size * 2; + int shadowHeight = height - size * 2; + Shape shape = FlatUIUtils.createRoundRectanglePath(0, 0, shadowWidth, shadowHeight, round, round, round, round); + return new ShadowRenderer(size, shadowOpacity, shadowColor).createShadow(shape); + } +} + diff --git a/designer-base/src/main/java/com/fine/theme/light/ui/ToastPanelUI.java b/designer-base/src/main/java/com/fine/theme/light/ui/ToastPanelUI.java new file mode 100755 index 0000000000..ad0dcdc8bf --- /dev/null +++ b/designer-base/src/main/java/com/fine/theme/light/ui/ToastPanelUI.java @@ -0,0 +1,370 @@ +package com.fine.theme.light.ui; + +import com.fine.theme.icon.LazyIcon; +import com.fine.theme.utils.FineUIUtils; +import com.formdev.flatlaf.FlatClientProperties; +import com.formdev.flatlaf.ui.FlatStylingSupport; +import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable; +import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableUI; +import com.formdev.flatlaf.ui.FlatUIUtils; +import com.formdev.flatlaf.util.LoggingFacade; +import com.formdev.flatlaf.util.UIScale; + +import javax.swing.Icon; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JPanel; +import javax.swing.LookAndFeel; +import javax.swing.border.Border; +import javax.swing.plaf.basic.BasicPanelUI; +import java.awt.Color; +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.Insets; +import java.awt.LayoutManager; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.Map; +import java.util.function.Consumer; + +import static com.fine.theme.utils.FineClientProperties.BUTTON_TYPE; +import static com.fine.theme.utils.FineClientProperties.BUTTON_TYPE_TOOLBAR_BUTTON; +import static com.fine.theme.utils.FineClientProperties.STYLE; +import static com.fine.theme.utils.FineClientProperties.STYLE_CLASS; +import static com.fine.theme.utils.FineClientProperties.TOAST_CLOSE_CALLBACK; +import static com.fine.theme.utils.FineClientProperties.TOAST_COMPONENT; +import static com.fine.theme.utils.FineClientProperties.TOAST_ICON; +import static com.fine.theme.utils.FineClientProperties.TOAST_SHOW_CLOSE_BUTTON; +import static com.formdev.flatlaf.FlatClientProperties.clientProperty; +import static com.formdev.flatlaf.FlatClientProperties.clientPropertyBoolean; + +/** + * 弹出消息面板UI + * + * @author Levy.Xie + * @since 12.0 + * Created on 2024/12/12 + */ +public class ToastPanelUI extends BasicPanelUI implements StyleableUI, PropertyChangeListener { + + protected JComponent iconComponent; + protected JComponent component; + protected JComponent closeButton; + + @Styleable + protected int iconTextGap; + @Styleable + protected int closeButtonGap; + @Styleable + protected int minimumWidth; + @Styleable + protected int maximumWidth; + @Styleable + protected int arc; + @Styleable + protected int outlineWidth; + @Styleable + protected Color outlineColor; + @Styleable + protected boolean showCloseButton; + @Styleable + protected Color closeIconColor; + @Styleable + protected Insets margin; + @Styleable + protected Icon closeButtonIcon; + + private NotificationPanelLayout layout; + private Map oldStyleValues; + + @Override + public void installUI(JComponent c) { + super.installUI(c); + c.addPropertyChangeListener(this); + installIconComponent(c); + installComponent(c); + installCloseButton(c); + installStyle((JPanel) c); + } + + @Override + public void uninstallUI(JComponent c) { + super.uninstallUI(c); + c.removePropertyChangeListener(this); + uninstallIconComponent(c); + uninstallComponent(c); + uninstallCloseButton(c); + } + + @Override + protected void installDefaults(JPanel p) { + super.installDefaults(p); + String prefix = getPropertyPrefix(); + iconTextGap = FlatUIUtils.getUIInt(prefix + ".iconTextGap", 5); + closeButtonGap = FlatUIUtils.getUIInt(prefix + ".closeButtonGap", 5); + minimumWidth = FlatUIUtils.getUIInt(prefix + ".minimumWidth", 50); + maximumWidth = FlatUIUtils.getUIInt(prefix + ".maximumWidth", -1); + arc = FlatUIUtils.getUIInt(prefix + ".arc", 6); + outlineWidth = FlatUIUtils.getUIInt(prefix + ".outlineWidth", 0); + outlineColor = FlatUIUtils.getUIColor(prefix + ".outlineColor", "Component.focusColor"); + margin = FineUIUtils.getUIInsets(prefix + ".margin", new Insets(8, 8, 8, 8)); + showCloseButton = FlatUIUtils.getUIBoolean(prefix + ".showCloseButton", true); + closeIconColor = FlatUIUtils.getUIColor(prefix + ".closeIconColor", new Color(150, 150, 150)); + closeButtonIcon = new LazyIcon("close"); + p.setBackground(FlatUIUtils.getUIColor(prefix + ".background", "Panel.background")); + p.setBorder(createDefaultBorder()); + LookAndFeel.installProperty(p, "opaque", false); + } + + @Override + protected void uninstallDefaults(JPanel p) { + super.uninstallDefaults(p); + oldStyleValues = null; + } + + protected Border createDefaultBorder() { + Color color = FlatUIUtils.getUIColor("Toast.shadowColor", new Color(0, 0, 0)); + Insets insets = FineUIUtils.getUIInsets("Toast.shadowInsets", new Insets(6, 6, 6, 6)); + float shadowOpacity = FlatUIUtils.getUIFloat("Toast.shadowOpacity", 0.2f); + return new ShadowBorder(color, insets, shadowOpacity); + } + + protected String getPropertyPrefix() { + return "Toast"; + } + + @Override + public void propertyChange(PropertyChangeEvent e) { + JPanel c = (JPanel) e.getSource(); + switch (e.getPropertyName()) { + default: + case TOAST_ICON: { + uninstallIconComponent(c); + installIconComponent(c); + refreshUI(c); + break; + } + case TOAST_COMPONENT: { + uninstallComponent(c); + installComponent(c); + refreshUI(c); + break; + } + case TOAST_SHOW_CLOSE_BUTTON: { + uninstallCloseButton(c); + installCloseButton(c); + refreshUI(c); + break; + } + case STYLE: + case STYLE_CLASS: { + installStyle(c); + refreshUI(c); + break; + } + } + } + + private static void refreshUI(JPanel c) { + c.revalidate(); + c.repaint(); + } + + private void installIconComponent(JComponent c) { + iconComponent = clientProperty(c, TOAST_ICON, null, JComponent.class); + if (iconComponent != null) { + installLayout(c); + c.add(iconComponent); + } + } + + + private void uninstallIconComponent(JComponent c) { + if (iconComponent != null) { + c.remove(iconComponent); + iconComponent = null; + } + } + + private void installComponent(JComponent c) { + component = FlatClientProperties.clientProperty(c, TOAST_COMPONENT, null, JComponent.class); + if (component != null) { + installLayout(c); + c.add(component); + } + } + + private void uninstallComponent(JComponent c) { + if (component != null) { + c.remove(component); + component = null; + } + } + + private void installCloseButton(JComponent c) { + if (clientPropertyBoolean(c, TOAST_SHOW_CLOSE_BUTTON, showCloseButton)) { + closeButton = createCloseButton(c); + installLayout(c); + c.add(closeButton); + } + } + + private void uninstallCloseButton(JComponent c) { + if (closeButton != null) { + c.remove(closeButton); + closeButton = null; + } + } + + protected JComponent createCloseButton(JComponent c) { + JButton button = new JButton(); + button.setFocusable(false); + button.setName("Toast.closeButton"); + button.putClientProperty(BUTTON_TYPE, BUTTON_TYPE_TOOLBAR_BUTTON); + button.putClientProperty(STYLE, "arc:999"); + button.setIcon(closeButtonIcon); + button.addActionListener(e -> closeButtonClicked(c)); + return button; + } + + @SuppressWarnings("all") + protected void closeButtonClicked(JComponent c) { + Object callback = c.getClientProperty(TOAST_CLOSE_CALLBACK); + if (callback instanceof Runnable) { + ((Runnable) callback).run(); + } else if (callback instanceof Consumer) { + ((Consumer) callback).accept(c); + } + } + + private void installLayout(JComponent c) { + if (layout == null) { + layout = new NotificationPanelLayout(); + } + c.setLayout(layout); + } + + protected void installStyle(JPanel c) { + try { + applyStyle(c, FlatStylingSupport.getResolvedStyle(c, "ToastPanel")); + } catch (RuntimeException ex) { + LoggingFacade.INSTANCE.logSevere(null, ex); + } + } + + protected void applyStyle(JPanel c, Object style) { + boolean oldShowCloseButton = showCloseButton; + oldStyleValues = FlatStylingSupport.parseAndApply(oldStyleValues, style, (key, value) -> applyStyleProperty(c, key, value)); + if (oldShowCloseButton != showCloseButton) { + uninstallCloseButton(c); + installCloseButton(c); + } + } + + protected Object applyStyleProperty(JPanel c, String key, Object value) { + return FlatStylingSupport.applyToAnnotatedObjectOrComponent(this, c, key, value); + } + + @Override + public Map> getStyleableInfos(JComponent c) { + return FlatStylingSupport.getAnnotatedStyleableInfos(this); + } + + @Override + public Object getStyleableValue(JComponent c, String key) { + return FlatStylingSupport.getAnnotatedStyleableValue(this, key); + } + + protected class NotificationPanelLayout implements LayoutManager { + + @Override + public void addLayoutComponent(String name, Component comp) { + + } + + @Override + public void removeLayoutComponent(Component comp) { + + } + + @Override + public Dimension preferredLayoutSize(Container parent) { + synchronized (parent.getTreeLock()) { + Insets insets = FlatUIUtils.addInsets(parent.getInsets(), UIScale.scale(margin)); + int width = insets.left + insets.right; + int height = 0; + int gap = 0; + int closeGap = 0; + if (iconComponent != null) { + width += iconComponent.getPreferredSize().width; + height = Math.max(height, iconComponent.getPreferredSize().height); + gap = UIScale.scale(iconTextGap); + } + if (component != null) { + width += gap; + width += component.getPreferredSize().width; + height = Math.max(height, component.getPreferredSize().height); + closeGap = UIScale.scale(closeButtonGap); + } + if (closeButton != null) { + width += closeGap; + width += closeButton.getPreferredSize().width; + height = Math.max(height, closeButton.getPreferredSize().height); + } + height += (insets.top + insets.bottom); + width = Math.max(minimumWidth, maximumWidth == -1 ? width : Math.min(maximumWidth, width)); + return new Dimension(width, height); + } + } + + @Override + public Dimension minimumLayoutSize(Container parent) { + synchronized (parent.getTreeLock()) { + return new Dimension(0, 0); + } + } + + private int getMaxWidth(int insets) { + int width = Math.max(maximumWidth, minimumWidth) - insets; + if (iconComponent != null) { + width -= (iconComponent.getPreferredSize().width + UIScale.scale(iconTextGap)); + } + if (closeButton != null) { + width -= (UIScale.scale(closeButtonGap) + closeButton.getPreferredSize().width); + } + return width; + } + + @Override + public void layoutContainer(Container parent) { + synchronized (parent.getTreeLock()) { + Insets insets = FlatUIUtils.addInsets(parent.getInsets(), UIScale.scale(margin)); + int x = insets.left; + int y = insets.top; + int height = 0; + if (iconComponent != null) { + int iconW = iconComponent.getPreferredSize().width; + int iconH = iconComponent.getPreferredSize().height; + iconComponent.setBounds(x, y, iconW, iconH); + x += iconW; + height = iconH; + } + if (component != null) { + int cW = maximumWidth == -1 ? component.getPreferredSize().width : Math.min(component.getPreferredSize().width, getMaxWidth(insets.left + insets.right)); + int cH = component.getPreferredSize().height; + x += UIScale.scale(iconTextGap); + component.setBounds(x, y, cW, cH); + height = Math.max(height, cH); + } + if (closeButton != null) { + int cW = closeButton.getPreferredSize().width; + int cH = closeButton.getPreferredSize().height; + int cX = parent.getWidth() - insets.right - cW; + int cy = y + ((height - cH) / 2); + closeButton.setBounds(cX, cy, cW, cH); + } + } + } + } +} diff --git a/designer-base/src/main/java/com/fine/theme/utils/FineClientProperties.java b/designer-base/src/main/java/com/fine/theme/utils/FineClientProperties.java index da8e252ca5..8b9ba8b718 100644 --- a/designer-base/src/main/java/com/fine/theme/utils/FineClientProperties.java +++ b/designer-base/src/main/java/com/fine/theme/utils/FineClientProperties.java @@ -33,6 +33,12 @@ public interface FineClientProperties extends FlatClientProperties { String COMBO_BOX_TYPE = "comboBoxType"; String ADAPTIVE_COMBO_BOX = "adaptiveComboBox"; + //---------------------------- Toast ---------------------------- + String TOAST_ICON = "Toast.icon"; + String TOAST_COMPONENT = "Toast.component"; + String TOAST_SHOW_CLOSE_BUTTON = "Toast.showCloseButton"; + String TOAST_CLOSE_CALLBACK = "Toast.closeCallback"; + int GROUP_BUTTON_POSITION_INNER = 0; int GROUP_BUTTON_POSITION_LEFT = 1; int GROUP_BUTTON_POSITION_RIGHT = 2; diff --git a/designer-base/src/main/java/com/fine/theme/utils/FineUIUtils.java b/designer-base/src/main/java/com/fine/theme/utils/FineUIUtils.java index c14fe698fd..280d0614d1 100644 --- a/designer-base/src/main/java/com/fine/theme/utils/FineUIUtils.java +++ b/designer-base/src/main/java/com/fine/theme/utils/FineUIUtils.java @@ -2,6 +2,7 @@ package com.fine.theme.utils; import com.fine.theme.light.ui.CollapsibleScrollBarLayerUI; import com.formdev.flatlaf.ui.FlatUIUtils; +import com.formdev.flatlaf.util.UIScale; import com.fr.design.border.FineBorderFactory; import com.fr.design.constants.LayoutConstants; import com.fr.design.gui.icontainer.UIScrollPane; @@ -12,12 +13,14 @@ 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 org.jetbrains.annotations.Nullable; import javax.swing.JLabel; import javax.swing.JLayer; import javax.swing.ScrollPaneConstants; import javax.swing.UIManager; import javax.swing.JTextArea; +import java.awt.AlphaComposite; import javax.swing.JScrollBar; import java.awt.Color; import java.awt.Component; @@ -28,9 +31,13 @@ import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GraphicsDevice; import java.awt.GraphicsEnvironment; +import java.awt.Image; import java.awt.Insets; +import java.awt.RenderingHints; import java.awt.geom.Path2D; import java.awt.geom.RoundRectangle2D; +import java.awt.image.BufferedImage; +import java.awt.image.VolatileImage; import java.lang.reflect.Field; import static com.fine.swing.ui.layout.Layouts.cell; @@ -602,4 +609,50 @@ public class FineUIUtils { // 获取滚动条的单位增量,并计算实际的滚动增量 return scrollAmount * scrollBar.getUnitIncrement(1); } + + /** + * 创建一个与指定组件大小相同的 VolatileImage,并将组件内容绘制到该图像上。 + * + * @param component 组件 + * @return 包含组件内容的 VolatileImage;如果组件的宽度或高度为零,则返回 null + */ + @Nullable + public static VolatileImage createImage(Component component) { + int width = component.getWidth(); + int height = component.getHeight(); + if (width > 0 && height > 0) { + VolatileImage image = component.createVolatileImage(width, height); + component.paint(image.createGraphics()); + return image; + } + return null; + } + + /** + * 创建一个与指定组件内容相同并带有圆角的 BufferedImage。 + * + * @param component 要生成图像的组件 + * @param round 圆角半径 + * @return 包含圆角组件内容的 BufferedImage;如果组件的宽度或高度为零,则返回 null + */ + public static Image createImage(Component component, int round) { + int width = component.getWidth(); + int height = component.getHeight(); + if (width > 0 && height > 0) { + VolatileImage image = createImage(component); + if (image != null) { + component.paint(image.createGraphics()); + BufferedImage buffImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = buffImage.createGraphics(); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + int arc = UIScale.scale(round); + FlatUIUtils.paintComponentBackground(g, 0, 0, width, height, 0, arc); + g.setComposite(AlphaComposite.SrcIn); + g.drawImage(image, 0, 0, null); + image.flush(); + return buffImage; + } + } + return null; + } } diff --git a/designer-base/src/main/java/com/fine/theme/utils/GlassLayerLoader.java b/designer-base/src/main/java/com/fine/theme/utils/GlassLayerLoader.java new file mode 100644 index 0000000000..ced8ba30df --- /dev/null +++ b/designer-base/src/main/java/com/fine/theme/utils/GlassLayerLoader.java @@ -0,0 +1,103 @@ +package com.fine.theme.utils; + +import com.fine.component.popup.ProgressChild; +import com.fine.component.popup.GlassPopupManager; +import com.fine.component.popup.GlassPaneChild; +import com.fr.design.mainframe.DesignerContext; +import com.fr.design.ui.util.UIUtil; +import com.fr.log.FineLoggerFactory; + +import javax.swing.SwingWorker; +import java.awt.Container; +import java.util.concurrent.Callable; +import java.util.function.Consumer; + +/** + * 遮罩层加载器 + * + * @author Levy.Xie + * @since 11.0 + * Created on 2024/12/06 + */ +public class GlassLayerLoader { + + private static volatile GlassLayerLoader loader = null; + + private GlassLayerLoader() { + } + + public static GlassLayerLoader getInstance() { + if (loader == null) { + synchronized (GlassLayerLoader.class) { + if (loader == null) { + loader = new GlassLayerLoader(); + } + } + } + return loader; + } + + /** + * 带有加载面板的后台任务执行器(无回调) + * + * @param task 耗时任务 + * @param container 遮罩的底层容器 + * @param loadingPane 显示加载动画的 GlassPaneChild + * @param 任务的返回值类型 + */ + public void runWithLoader(Callable task, Container container, GlassPaneChild loadingPane) { + // 默认无回调 + runWithLoader(task, container, loadingPane, result -> {}, e -> {}); + } + + /** + * 带有加载面板的后台任务执行器(有成功和失败的回调) + * + * @param task 耗时任务 + * @param container 遮罩的底层容器 + * @param loadingPane 显示加载动画的 GlassPaneChild + * @param 任务的返回值类型 + */ + public void runWithLoader(Callable task, Container container, GlassPaneChild loadingPane, + Consumer onSuccess, Consumer onFailure) { + // 安装并显示加载面板 + UIUtil.invokeAndWaitIfNeeded(() -> { + GlassPopupManager.install(container); + GlassPopupManager.showPopup(loadingPane); + }); + SwingWorker worker = new SwingWorker() { + @Override + protected T doInBackground() throws Exception { + return task.call(); + } + + @Override + protected void done() { + try { + T t = get(); + onSuccess.accept(t); + } catch (Exception e) { + onFailure.accept(e); + FineLoggerFactory.getLogger().error(e.getMessage()); + } finally { + loadingPane.onClose(); + GlassPopupManager.closePopup(loadingPane); + } + } + }; + worker.execute(); + } + + /** + * 进度条遮罩加载动画 + * + * @param task 耗时任务 + * @param info 显示加载面板的 JFrame + * @param maxWait 最大等待时间 + * @param 任务的返回值类型 + */ + public void runWithProgressLoader(Callable task, String info, int maxWait) { + runWithLoader(task, DesignerContext.getDesignerFrame(), new ProgressChild(info).setMaxWait(maxWait)); + } + +} diff --git a/designer-base/src/main/java/com/fine/theme/utils/ShadowRenderer.java b/designer-base/src/main/java/com/fine/theme/utils/ShadowRenderer.java new file mode 100755 index 0000000000..58e3b90046 --- /dev/null +++ b/designer-base/src/main/java/com/fine/theme/utils/ShadowRenderer.java @@ -0,0 +1,220 @@ +package com.fine.theme.utils; + +import org.jetbrains.annotations.NotNull; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.Shape; +import java.awt.image.BufferedImage; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; + +/** + * 阴影绘制工具类 + * + * @author Levy.Xie + * @since 12.0 + * Created on 2024/11/28 + */ +public class ShadowRenderer { + + private final int size; + private final float opacity; + private final Color color; + + public ShadowRenderer() { + this(5, 0.5f, Color.BLACK); + } + + public ShadowRenderer(final int size, final float opacity, final Color color) { + this.size = size; + this.opacity = opacity; + this.color = color; + } + + public Color getColor() { + return color; + } + + public float getOpacity() { + return opacity; + } + + public int getSize() { + return size; + } + + /** + * 基于形状创建阴影 + * + * @param shape 形状 + * @return 阴影图片 + */ + public BufferedImage createShadow(Shape shape) { + Rectangle rec = shape.getBounds(); + BufferedImage img = new BufferedImage(rec.width, rec.height, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2 = img.createGraphics(); + g2.setColor(Color.BLACK); + g2.translate(-rec.x, -rec.y); + g2.fill(shape); + g2.dispose(); + return createShadow(img); + } + + /** + * 基于图片创建阴影 + * + * @param image 图片 + * @return 阴影图片 + */ + public BufferedImage createShadow(final BufferedImage image) { + int shadowSize = size * 2; + int srcWidth = image.getWidth(); + int srcHeight = image.getHeight(); + int dstWidth = srcWidth + shadowSize; + int dstHeight = srcHeight + shadowSize; + int left = size; + int right = shadowSize - left; + int yStop = dstHeight - right; + int shadowRgb = color.getRGB() & 0x00FFFFFF; + int[] aHistory = new int[shadowSize]; + BufferedImage dst = new BufferedImage(dstWidth, dstHeight, BufferedImage.TYPE_INT_ARGB); + int[] dstBuffer = new int[dstWidth * dstHeight]; + int[] srcBuffer = new int[srcWidth * srcHeight]; + getPixels(image, 0, 0, srcWidth, srcHeight, srcBuffer); + int lastPixelOffset = right * dstWidth; + + int[] hSumLookup = initSumLookupArray(1.0f, shadowSize); + int[] vSumLookup = initSumLookupArray(opacity, shadowSize); + + applyHorizontalShadowBlur(left, dstWidth, srcHeight, shadowSize, aHistory, srcWidth, hSumLookup, dstBuffer, srcBuffer); + + applyVerticalShadowBlur(dstWidth, left, aHistory, right, dstBuffer, yStop, vSumLookup, shadowRgb, + lastPixelOffset, shadowSize, dstHeight); + + setPixels(dst, 0, 0, dstWidth, dstHeight, dstBuffer); + return dst; + } + + private static int @NotNull [] initSumLookupArray(float x, int shadowSize) { + float sumDivider = x / shadowSize; + int[] sumLookup = new int[256 * shadowSize]; + for (int i = 0; i < sumLookup.length; i++) { + sumLookup[i] = (int) (i * sumDivider); + } + return sumLookup; + } + + private static void applyVerticalShadowBlur(int dstWidth, int left, int[] aHistory, int right, int[] dstBuffer, + int yStop, int[] vSumLookup, int shadowRgb, int lastPixelOffset, + int shadowSize, int dstHeight) { + for (int x = 0, bufferOffset = 0; x < dstWidth; x++, bufferOffset = x) { + int aSum = initializeAlphaSum(dstWidth, left, aHistory, right, dstBuffer, bufferOffset, 0); + bufferOffset = x; + int historyIdx = 0; + for (int y = 0; y < yStop; y++, bufferOffset += dstWidth) { + int a = vSumLookup[aSum]; + dstBuffer[bufferOffset] = a << 24 | shadowRgb; + aSum -= aHistory[historyIdx]; + a = dstBuffer[bufferOffset + lastPixelOffset] >>> 24; + aHistory[historyIdx] = a; + aSum += a; + if (++historyIdx >= shadowSize) { + historyIdx -= shadowSize; + } + } + for (int y = yStop; y < dstHeight; y++, bufferOffset += dstWidth) { + int a = vSumLookup[aSum]; + dstBuffer[bufferOffset] = a << 24 | shadowRgb; + aSum -= aHistory[historyIdx]; + if (++historyIdx >= shadowSize) { + historyIdx -= shadowSize; + } + } + } + } + + private static int initializeAlphaSum(int dstWidth, int left, int[] aHistory, int right, int[] dstBuffer, int bufferOffset, int aSum) { + int historyIdx; + for (historyIdx = 0; historyIdx < left; ) { + aHistory[historyIdx++] = 0; + } + for (int y = 0; y < right; y++, bufferOffset += dstWidth) { + int a = dstBuffer[bufferOffset] >>> 24; + aHistory[historyIdx++] = a; + aSum += a; + } + return aSum; + } + + private static void applyHorizontalShadowBlur(int left, int dstWidth, int srcHeight, int shadowSize, + int[] aHistory, int srcWidth, int[] hSumLookup, int[] dstBuffer, int[] srcBuffer) { + int historyIdx; + for (int srcY = 0, dstOffset = left * dstWidth; srcY < srcHeight; srcY++) { + for (historyIdx = 0; historyIdx < shadowSize; ) { + aHistory[historyIdx++] = 0; + } + int aSum = 0; + historyIdx = 0; + int srcOffset = srcY * srcWidth; + for (int srcX = 0; srcX < srcWidth; srcX++) { + int a = hSumLookup[aSum]; + dstBuffer[dstOffset++] = a << 24; + aSum -= aHistory[historyIdx]; + a = srcBuffer[srcOffset + srcX] >>> 24; + aHistory[historyIdx] = a; + aSum += a; + if (++historyIdx >= shadowSize) { + historyIdx -= shadowSize; + } + } + dstOffset = updateDstBufferOffset(shadowSize, aHistory, hSumLookup, dstBuffer, aSum, dstOffset, historyIdx); + } + } + + private static int updateDstBufferOffset(int shadowSize, int[] aHistory, int[] hSumLookup, int[] dstBuffer, + int aSum, int dstOffset, int historyIdx) { + for (int i = 0; i < shadowSize; i++) { + int a = hSumLookup[aSum]; + dstBuffer[dstOffset++] = a << 24; + aSum -= aHistory[historyIdx]; + if (++historyIdx >= shadowSize) { + historyIdx -= shadowSize; + } + } + return dstOffset; + } + + private int[] getPixels(BufferedImage img, int x, int y, int w, int h, int[] pixels) { + if (w == 0 || h == 0) { + return new int[0]; + } + if (pixels == null) { + pixels = new int[w * h]; + } else if (pixels.length < w * h) { + throw new IllegalArgumentException("pixels array must have a length" + " >= w*h"); + } + int imageType = img.getType(); + if (imageType == BufferedImage.TYPE_INT_ARGB || imageType == BufferedImage.TYPE_INT_RGB) { + Raster raster = img.getRaster(); + return (int[]) raster.getDataElements(x, y, w, h, pixels); + } + return img.getRGB(x, y, w, h, pixels, 0, w); + } + + private void setPixels(BufferedImage img, int x, int y, int w, int h, int[] pixels) { + if (pixels == null || w == 0 || h == 0) { + return; + } else if (pixels.length < w * h) { + throw new IllegalArgumentException("pixels array must have a length" + " >= w*h"); + } + int imageType = img.getType(); + if (imageType == BufferedImage.TYPE_INT_ARGB || imageType == BufferedImage.TYPE_INT_RGB) { + WritableRaster raster = img.getRaster(); + raster.setDataElements(x, y, w, h, pixels); + } else { + img.setRGB(x, y, w, h, pixels, 0, w); + } + } +} \ No newline at end of file diff --git a/designer-base/src/main/java/com/fr/design/EnvChangeEntrance.java b/designer-base/src/main/java/com/fr/design/EnvChangeEntrance.java index 4b3eec482f..4b8c41659c 100644 --- a/designer-base/src/main/java/com/fr/design/EnvChangeEntrance.java +++ b/designer-base/src/main/java/com/fr/design/EnvChangeEntrance.java @@ -1,5 +1,6 @@ package com.fr.design; +import com.fine.theme.utils.GlassLayerLoader; import com.fr.common.report.ReportState; import com.fr.design.backup.EnvBackupHelper; import com.fr.design.data.DesignTableDataManager; @@ -102,9 +103,12 @@ public class EnvChangeEntrance { * @param envName 目标工作目录名称 */ public void switch2Env(final String envName) { - if (switch2Env(envName, PopTipStrategy.LATER)) { - VersionCheckUtils.showVersionCheckDialog(envName); - } + GlassLayerLoader.getInstance().runWithProgressLoader(() -> { + if (switch2Env(envName, PopTipStrategy.LATER)) { + VersionCheckUtils.showVersionCheckDialog(envName); + } + return null; + }, Toolkit.i18nText("Fine-Design_Basic_M_Switch_Workspace"), 5); } /** @@ -525,12 +529,15 @@ public class EnvChangeEntrance { // 用户取消保存时,取消切换目录操作 return; } - boolean changeResult = envListOkAction(envListPane, PopTipStrategy.LATER); - // 切换完成后清理密码 - updateNotRememberPwdEnv(); - if (changeResult) { - VersionCheckUtils.showVersionCheckDialog(envListPane.getSelectedName()); - } + GlassLayerLoader.getInstance().runWithProgressLoader(() -> { + boolean changeResult = envListOkAction(envListPane, PopTipStrategy.LATER); + // 切换完成后清理密码 + updateNotRememberPwdEnv(); + if (changeResult) { + VersionCheckUtils.showVersionCheckDialog(envListPane.getSelectedName()); + } + return null; + }, Toolkit.i18nText("Fine-Design_Basic_M_Switch_Workspace"), 5); } @Override diff --git a/designer-base/src/main/java/com/fr/design/file/DefaultTemplateTreeDefineProcessor.java b/designer-base/src/main/java/com/fr/design/file/DefaultTemplateTreeDefineProcessor.java index 163c48e66d..77011055ce 100644 --- a/designer-base/src/main/java/com/fr/design/file/DefaultTemplateTreeDefineProcessor.java +++ b/designer-base/src/main/java/com/fr/design/file/DefaultTemplateTreeDefineProcessor.java @@ -22,8 +22,7 @@ import com.fr.design.mainframe.manager.clip.TemplateTreeClipboard; import com.fr.design.mainframe.manager.search.TemplateDirTreeSearchManager; import com.fr.design.mainframe.manager.search.TemplateTreeSearchManager; import com.fr.design.mainframe.manager.search.searcher.control.pane.TemplateDirTreeSearchPane; -import com.fr.design.mainframe.toast.DesignerToastMsgUtil; -import com.fr.design.mainframe.toast.ToastMsgDialog; +import com.fine.component.toast.ToastMsgManager; import com.fr.design.utils.TemplateUtils; import com.fr.design.utils.gui.GUICoreUtils; import com.fr.file.filetree.FileNode; @@ -402,8 +401,7 @@ public class DefaultTemplateTreeDefineProcessor extends AbstractTemplateTreeDefi boolean moveSuccess = doMove(); dispose(); if (moveSuccess) { - ToastMsgDialog dialog = DesignerToastMsgUtil.createPromptDialog(Toolkit.i18nText("Fine-Design_Basic_Template_Moved_Success")); - dialog.setVisible(true); + ToastMsgManager.getInstance().info(Toolkit.i18nText("Fine-Design_Basic_Template_Moved_Success")); NodeAuthProcessor.getInstance().refresh(); DesignerFrameFileDealerPane.getInstance().refreshRightToolBarBy(null); LocateAction.gotoEditingTemplateLeaf(targetFile); diff --git a/designer-base/src/main/java/com/fr/design/foldablepane/UIExpandablePane.java b/designer-base/src/main/java/com/fr/design/foldablepane/UIExpandablePane.java index faae8d9f84..355c95ab6d 100644 --- a/designer-base/src/main/java/com/fr/design/foldablepane/UIExpandablePane.java +++ b/designer-base/src/main/java/com/fr/design/foldablepane/UIExpandablePane.java @@ -1,12 +1,12 @@ package com.fr.design.foldablepane; - +import com.formdev.flatlaf.util.Animator; import com.formdev.flatlaf.util.ScaledEmptyBorder; import com.fr.design.border.FineBorderFactory; import javax.swing.JPanel; import java.awt.BorderLayout; -import java.awt.Color; +import java.awt.Dimension; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; @@ -14,29 +14,29 @@ import static com.fine.swing.ui.layout.Layouts.cell; import static com.fine.swing.ui.layout.Layouts.column; import static com.fine.swing.ui.layout.Layouts.fix; - /** - * Created by MoMeak on 2017/7/5. + * 折叠面板 + * + * @author Levy.Xie + * @since 11.0 + * Created on 2024/11/15 */ public class UIExpandablePane extends JPanel { private static final long serialVersionUID = 1L; - private HeaderPane headerPanel; - private JPanel contentPanel; - private Color color = Color.black; - private String title; - private int headWidth; - private int headHeight; + private boolean showExpand = true; + private Animator animator; + private int fullHeight; + private final JPanel contentPanel; + private final String title; public JPanel getContentPanel() { return contentPanel; } - public UIExpandablePane(String title, int headWidth, int headHeight, JPanel contentPanel) { + public UIExpandablePane(String title, @Deprecated int headWidth, @Deprecated int headHeight, JPanel contentPanel) { super(); this.title = title; - this.headWidth = headWidth; - this.headHeight = headHeight; this.contentPanel = contentPanel; initComponents(false); } @@ -48,48 +48,80 @@ public class UIExpandablePane extends JPanel { public UIExpandablePane(String title, JPanel contentPanel, boolean withUnderline) { super(); this.title = title; - this.headHeight = headHeight; this.contentPanel = contentPanel; initComponents(withUnderline); } private void initComponents(boolean withUnderline) { this.setLayout(new BorderLayout()); - - headerPanel = new HeaderPane(title); + HeaderPane headerPanel = new HeaderPane(title); headerPanel.addMouseListener(new PanelAction()); setcontentPanelontentPanelBorder(); if (withUnderline) { this.add(column( - cell(headerPanel), cell(contentPanel).with(it -> it.setBorder(new ScaledEmptyBorder(0, 0, 10, 0))), + cell(headerPanel), + cell(contentPanel).with(it -> it.setBorder(new ScaledEmptyBorder(0, 0, 10, 0))), fix(1).with(it -> it.setBorder(FineBorderFactory.createDefaultUnderlineBorder()))) .getComponent()); } else { this.add(headerPanel, BorderLayout.NORTH); this.add(contentPanel, BorderLayout.CENTER); } - + initAnimation(); setOpaque(false); } + /** + * 组件竖向折叠显示动画 + */ + private void initAnimation() { + int width = contentPanel.getWidth(); + boolean sizeSet = contentPanel.isPreferredSizeSet(); + animator = new Animator(200, new Animator.TimingTarget() { + @Override + public void timingEvent(float fraction) { + float ratio = !showExpand ? fraction : 1f - fraction; + contentPanel.setPreferredSize(new Dimension(width, (int) (fullHeight * ratio))); + contentPanel.revalidate(); + contentPanel.repaint(); + } + + @Override + public void begin() { + contentPanel.setVisible(true); + if (showExpand) { + fullHeight = contentPanel.getPreferredSize().height; + } + } + + @Override + public void end() { + // 重置中心面板 + if (!sizeSet) { + contentPanel.setPreferredSize(null); + } + contentPanel.setVisible(!showExpand); + showExpand = !showExpand; + } + }); + } + protected void setcontentPanelontentPanelBorder() { } class PanelAction extends MouseAdapter { + @Override public void mouseClicked(MouseEvent e) { HeaderPane hp = (HeaderPane) e.getSource(); - if (contentPanel.isShowing()) { - contentPanel.setVisible(false); - hp.setShow(false); - } else { - contentPanel.setVisible(true); - hp.setShow(true); + if (!Animator.useAnimation()) { + contentPanel.setVisible(!showExpand); + } else if (!animator.isRunning()) { + animator.start(); } + hp.setShow(!showExpand); hp.setPressed(false); - hp.getParent().validate(); - hp.getParent().repaint(); } public void mousePressed(MouseEvent e) { diff --git a/designer-base/src/main/java/com/fr/design/mainframe/ComponentReuseNotifyUtil.java b/designer-base/src/main/java/com/fr/design/mainframe/ComponentReuseNotifyUtil.java index 2dc77bb62c..ac3b2b5932 100644 --- a/designer-base/src/main/java/com/fr/design/mainframe/ComponentReuseNotifyUtil.java +++ b/designer-base/src/main/java/com/fr/design/mainframe/ComponentReuseNotifyUtil.java @@ -3,7 +3,7 @@ package com.fr.design.mainframe; import com.fr.design.DesignerEnvManager; import com.fr.design.i18n.Toolkit; import com.fr.design.mainframe.reuse.ComponentReuseNotificationInfo; -import com.fr.design.mainframe.toast.DesignerToastMsgUtil; +import com.fine.component.toast.ToastMsgManager; import com.fr.design.notification.SnapChat; import com.fr.design.notification.SnapChatFactory; import com.fr.design.notification.SnapChatKey; @@ -29,7 +29,7 @@ public class ComponentReuseNotifyUtil { } }); if (snapChat.hasRead()) { - DesignerToastMsgUtil.toastPrompt(Toolkit.i18nText("Fine-Design_Component_Reuse_Merge_Prompt")); + ToastMsgManager.getInstance().info(Toolkit.i18nText("Fine-Design_Component_Reuse_Merge_Prompt")); } ComponentReuseNotificationInfo.getInstance().setClickedWidgetLib(true); DesignerEnvManager.getEnvManager().saveXMLFile(); diff --git a/designer-base/src/main/java/com/fr/design/mainframe/theme/TemplateThemeProfilePane.java b/designer-base/src/main/java/com/fr/design/mainframe/theme/TemplateThemeProfilePane.java index 4e8273161f..a607f416c3 100644 --- a/designer-base/src/main/java/com/fr/design/mainframe/theme/TemplateThemeProfilePane.java +++ b/designer-base/src/main/java/com/fr/design/mainframe/theme/TemplateThemeProfilePane.java @@ -16,7 +16,7 @@ import com.fr.design.mainframe.theme.dialog.TemplateThemeProfileDialog; import com.fr.design.mainframe.theme.edit.ui.LabelUtils; import com.fr.design.mainframe.theme.ui.AutoCheckTextField; import com.fr.design.mainframe.theme.ui.AutoCheckThemeNameTextField; -import com.fr.design.mainframe.toast.DesignerToastMsgUtil; +import com.fine.component.toast.ToastMsgManager; import com.fr.design.utils.gui.GUICoreUtils; import com.fr.log.FineLoggerFactory; import com.fr.stable.StringUtils; @@ -226,12 +226,10 @@ public abstract class TemplateThemeProfilePane extends saveButton.setEnabled(false); saveAsButton.setEnabled(true); actionListener.onSaved(config.cachedFetch(getName())); - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - DesignerToastMsgUtil.toastPrompt(Toolkit.i18nText("Fine-Design_Basic_Template_Theme_Profile_Pane_Save_Successfully")); - profileDialog.exit(); - } + SwingUtilities.invokeLater(() -> { + ToastMsgManager.getInstance().info( + Toolkit.i18nText("Fine-Design_Basic_Template_Theme_Profile_Pane_Save_Successfully")); + profileDialog.exit(); }); } }); diff --git a/designer-base/src/main/java/com/fr/design/mainframe/toast/DesignerToastMsgUtil.java b/designer-base/src/main/java/com/fr/design/mainframe/toast/DesignerToastMsgUtil.java index fc326c60e6..25d36786fa 100644 --- a/designer-base/src/main/java/com/fr/design/mainframe/toast/DesignerToastMsgUtil.java +++ b/designer-base/src/main/java/com/fr/design/mainframe/toast/DesignerToastMsgUtil.java @@ -21,6 +21,7 @@ import java.awt.Window; /** * Created by kerry on 5/6/21 */ +@Deprecated public class DesignerToastMsgUtil { private static final int MIN_WIDTH = 134; private static final int MAX_WIDTH = 454; diff --git a/designer-base/src/main/java/com/fr/design/style/background/image/ImageFileChooser.java b/designer-base/src/main/java/com/fr/design/style/background/image/ImageFileChooser.java index 739dc81d55..9ef470609a 100644 --- a/designer-base/src/main/java/com/fr/design/style/background/image/ImageFileChooser.java +++ b/designer-base/src/main/java/com/fr/design/style/background/image/ImageFileChooser.java @@ -7,7 +7,7 @@ import com.fr.design.DesignerEnvManager; import com.fr.design.gui.ifilechooser.FileChooserFactory; import com.fr.design.gui.ifilechooser.FileChooserProvider; import com.fr.design.i18n.Toolkit; -import com.fr.design.mainframe.toast.DesignerToastMsgUtil; +import com.fine.component.toast.ToastMsgManager; import java.awt.Component; @@ -56,7 +56,7 @@ public class ImageFileChooser { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { - DesignerToastMsgUtil.toastWarning(Toolkit.i18nText("Fine-Design_Image_Compress_Move_Tip")); + ToastMsgManager.getInstance().warn(Toolkit.i18nText("Fine-Design_Image_Compress_Move_Tip")); DesignerEnvManager.getEnvManager().setShowImageCompressMoveTip(false); } }); diff --git a/designer-base/src/main/java/com/fr/start/ServerStarter.java b/designer-base/src/main/java/com/fr/start/ServerStarter.java index 9c2bfa2620..3d2d748122 100644 --- a/designer-base/src/main/java/com/fr/start/ServerStarter.java +++ b/designer-base/src/main/java/com/fr/start/ServerStarter.java @@ -1,17 +1,15 @@ package com.fr.start; +import com.fine.theme.utils.GlassLayerLoader; import com.fr.base.ServerConfig; -import com.fr.concurrent.NamedThreadFactory; import com.fr.design.DesignerEnvManager; +import com.fr.design.i18n.Toolkit; import com.fr.design.mainframe.DesignerContext; import com.fr.design.utils.BrowseUtils; import com.fr.general.GeneralContext; import com.fr.start.server.FineEmbedServer; -import com.fr.start.server.FineEmbedServerMonitor; import com.fr.workspace.WorkContext; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; public class ServerStarter { @@ -43,25 +41,15 @@ public class ServerStarter { public static void browserURLWithLocalEnv(final String url) { // 内置服务器没有启动并且设计器已经打开,可以使用带进度条的启动方式 if (!FineEmbedServer.isRunning() && DesignerContext.getDesignerFrame().isDesignerOpened()) { - FineEmbedServerMonitor.getInstance().monitor(); - ExecutorService service = Executors.newSingleThreadExecutor(new NamedThreadFactory("ServerStarter")); - service.submit(new Runnable() { - - @Override - public void run() { - try { - try { - FineEmbedServer.start(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } finally { - FineEmbedServerMonitor.getInstance().setComplete(); - } - BrowseUtils.browser(url); + GlassLayerLoader.getInstance().runWithProgressLoader(() -> { + try { + FineEmbedServer.start(); + } catch (Exception e) { + throw new RuntimeException(e); } - }); - service.shutdown(); + BrowseUtils.browser(url); + return null; + }, Toolkit.i18nText("Fine-Design_Basic_Loading_Embed_Server"), 10); } else if (!FineEmbedServer.isRunning()) { // 普通方式启动内置服务器 try { diff --git a/designer-base/src/main/java/com/fr/start/server/DesignEmbedHelper.java b/designer-base/src/main/java/com/fr/start/server/DesignEmbedHelper.java index 1a1ed91398..fc9fdff739 100644 --- a/designer-base/src/main/java/com/fr/start/server/DesignEmbedHelper.java +++ b/designer-base/src/main/java/com/fr/start/server/DesignEmbedHelper.java @@ -27,7 +27,6 @@ import java.util.HashSet; import java.util.Properties; import java.util.Set; -; /** * 内置服务器工具类 @@ -48,15 +47,11 @@ public class DesignEmbedHelper { public static synchronized void start() { try { - FineEmbedServerMonitor.getInstance().reset(); //初始化tomcat initTomcat(); tomcat.start(); - } catch (LifecycleException e) { FineLoggerFactory.getLogger().error(e.getMessage(), e); - } finally { - FineEmbedServerMonitor.getInstance().setComplete(); } } diff --git a/designer-base/src/main/java/com/fr/start/server/FineEmbedServerMonitor.java b/designer-base/src/main/java/com/fr/start/server/FineEmbedServerMonitor.java index c20aaa153d..e53cd75f2f 100644 --- a/designer-base/src/main/java/com/fr/start/server/FineEmbedServerMonitor.java +++ b/designer-base/src/main/java/com/fr/start/server/FineEmbedServerMonitor.java @@ -24,6 +24,7 @@ import java.util.concurrent.TimeUnit; * @author zack * @date 2018/8/21 */ +@Deprecated public class FineEmbedServerMonitor { private int progress; private static final int STEP = 1; diff --git a/designer-base/src/main/java/com/fr/startup/ui/StartupLoadingPanel.java b/designer-base/src/main/java/com/fr/startup/ui/StartupLoadingPanel.java index 3faf1c2232..0bdf5bf2cd 100644 --- a/designer-base/src/main/java/com/fr/startup/ui/StartupLoadingPanel.java +++ b/designer-base/src/main/java/com/fr/startup/ui/StartupLoadingPanel.java @@ -20,6 +20,7 @@ import java.util.concurrent.TimeUnit; * @version 11.0 * created by Harrison on 2022/11/08 **/ +@Deprecated public class StartupLoadingPanel { /** diff --git a/designer-base/src/main/java/com/fr/startup/ui/StartupPageWindow.java b/designer-base/src/main/java/com/fr/startup/ui/StartupPageWindow.java index 3100c064ae..1e3a02c83a 100644 --- a/designer-base/src/main/java/com/fr/startup/ui/StartupPageWindow.java +++ b/designer-base/src/main/java/com/fr/startup/ui/StartupPageWindow.java @@ -1,8 +1,10 @@ package com.fr.startup.ui; +import com.fine.component.popup.ProgressChild; import com.fine.swing.ui.layout.Column; import com.fine.theme.utils.FineUIStyle; import com.fine.theme.utils.FineUIUtils; +import com.fine.theme.utils.GlassLayerLoader; import com.formdev.flatlaf.ui.FlatUIUtils; import com.formdev.flatlaf.util.ScaledEmptyBorder; import com.fr.design.DesignerEnvManager; @@ -15,7 +17,6 @@ import com.fr.design.layout.FRGUIPaneFactory; import com.fr.design.layout.VerticalFlowLayout; import com.fr.design.mainframe.messagecollect.StartErrorMessageCollector; import com.fr.design.mainframe.messagecollect.entity.DesignerErrorMessage; -import com.fr.design.ui.util.UIUtil; import com.fr.design.utils.ColorUtils; import com.fr.design.utils.gui.GUICoreUtils; import com.fr.exit.DesignerExiter; @@ -35,7 +36,6 @@ import javax.swing.JPanel; import javax.swing.JSeparator; import javax.swing.ScrollPaneConstants; import javax.swing.SwingConstants; -import javax.swing.SwingWorker; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; @@ -233,54 +233,32 @@ public class StartupPageWindow extends JFrame { } private void enterWorkspace(Runnable action) { - - UIUtil.invokeAndWaitIfNeeded(() -> { - - // 必须直接初始化 - // 见 https://work.fineres.com/browse/REPORT-85293 - StartupLoadingPanel loadingPanel = new StartupLoadingPanel(this); - loadingPanel.show(); + GlassLayerLoader.getInstance().runWithLoader(() -> { setEnabled(false); - - SwingWorker task = new SwingWorker() { - @Override - protected Void doInBackground() throws Exception { - action.run(); - return null; - } - - @Override - protected void done() { - - try { - Void result = get(); - setVisible(false); - StartErrorMessageCollector.getInstance().setExtraJudgeStart(false); - } catch (Exception e) { - // 处理错误 - UIUtil.invokeLaterIfNeeded(() -> { - UIExpandDialog.Builder() - .owner(StartupPageWindow.this) - .title(Toolkit.i18nText("Fine-Design_Basic_Remote_Env_Try")) - .message(Toolkit.i18nText("Fine-Design_Basic_Connection_Failed")) - .messageType(UIExpandDialog.WARNING_MESSAGE) - .detail(e.getMessage()) - .expand(true) - .modal(false) - .build() - .setVisible(true); - setEnabled(true); - }); - FineLoggerFactory.getLogger().error(e.getMessage(), e); - StartErrorMessageCollector.getInstance().asyncRecord(DesignerErrorMessage.UNEXCEPTED_START_FAILED.getId(), - DesignerErrorMessage.UNEXCEPTED_START_FAILED.getMessage(), - e.getMessage()); - } finally { - loadingPanel.hide(); - } - } - }; - task.execute(); + action.run(); + return null; + }, this, new ProgressChild(Toolkit.i18nText("Fine-Design_Basic_Initializing")).setMaxWait(10), + // 进入工作目录成功回调 + res -> { + setVisible(false); + StartErrorMessageCollector.getInstance().setExtraJudgeStart(false); + }, e -> { + // 进入工作目录失败回调 + UIExpandDialog.Builder() + .owner(StartupPageWindow.this) + .title(Toolkit.i18nText("Fine-Design_Basic_Remote_Env_Try")) + .message(Toolkit.i18nText("Fine-Design_Basic_Connection_Failed")) + .messageType(UIExpandDialog.WARNING_MESSAGE) + .detail(e.getMessage()) + .expand(true) + .modal(false) + .build() + .setVisible(true); + setEnabled(true); + FineLoggerFactory.getLogger().error(e.getMessage(), e); + StartErrorMessageCollector.getInstance().asyncRecord(DesignerErrorMessage.UNEXCEPTED_START_FAILED.getId(), + DesignerErrorMessage.UNEXCEPTED_START_FAILED.getMessage(), + e.getMessage()); }); } diff --git a/designer-base/src/main/resources/com/fine/component/pop/loading.gif b/designer-base/src/main/resources/com/fine/component/pop/loading.gif new file mode 100644 index 0000000000..2425a6790e Binary files /dev/null and b/designer-base/src/main/resources/com/fine/component/pop/loading.gif differ diff --git a/designer-base/src/main/resources/com/fine/theme/light/ui/laf/FineLaf.properties b/designer-base/src/main/resources/com/fine/theme/light/ui/laf/FineLaf.properties index 456d77ac89..8ef2ec289f 100644 --- a/designer-base/src/main/resources/com/fine/theme/light/ui/laf/FineLaf.properties +++ b/designer-base/src/main/resources/com/fine/theme/light/ui/laf/FineLaf.properties @@ -59,3 +59,4 @@ TemplateTabPaneUI=com.fine.theme.light.ui.FineTemplateTabPaneUI ReportComponentCompositeUI=com.fine.theme.light.ui.FineReportComponentCompositeUI ColorButtonUI=com.fine.theme.light.ui.FineColorButtonUI HeaderPaneUI=com.fine.theme.light.ui.FineHeaderPaneUI +ToastPanelUI=com.fine.theme.light.ui.ToastPanelUI diff --git a/designer-base/src/test/java/com/fr/design/gui/storybook/components/GlassLoaderStoryBoard.java b/designer-base/src/test/java/com/fr/design/gui/storybook/components/GlassLoaderStoryBoard.java new file mode 100644 index 0000000000..e5b5ed6cf8 --- /dev/null +++ b/designer-base/src/test/java/com/fr/design/gui/storybook/components/GlassLoaderStoryBoard.java @@ -0,0 +1,62 @@ +package com.fr.design.gui.storybook.components; + +import com.fine.component.popup.ImageChild; +import com.fine.component.popup.ProgressChild; +import com.fine.theme.utils.GlassLayerLoader; +import com.fr.design.gui.ibutton.UIButton; +import com.fr.design.gui.storybook.StoryBoard; +import com.fr.stable.StringUtils; + +import javax.swing.JFrame; +import javax.swing.SwingUtilities; + +import java.awt.Font; + +import static com.fine.swing.ui.layout.Layouts.cell; + +/** + * GlassLoaderStoryBoard + * + * @author Levy.Xie + * @since 11.0 + * Created on 2024/12/19 + */ +public class GlassLoaderStoryBoard extends StoryBoard { + + public GlassLoaderStoryBoard() { + super("遮罩动画"); + add( + cell(createProgressBtn()), + cell(createImageBtn("GIF图片遮罩-1", null)), + cell(createImageBtn("GIF图片遮罩-2", "com/fr/design/gui/storybook/components/loadingBar.gif")) + ); + } + + private UIButton createProgressBtn() { + UIButton btn = new UIButton("进度条遮罩"); + btn.addActionListener(e -> { + GlassLayerLoader.getInstance().runWithLoader(() -> { + Thread.sleep(3000); + return null; + }, (JFrame) SwingUtilities.getWindowAncestor(this), new ProgressChild("running...", + new Font("宋体", Font.PLAIN, 14)).setMaxWait(3)); + }); + return btn; + } + + private UIButton createImageBtn(String name, String path) { + UIButton btn = new UIButton(name); + btn.addActionListener(e -> { + showGlassLoadingPane(path); + }); + return btn; + } + + private void showGlassLoadingPane(String gifPath) { + GlassLayerLoader.getInstance().runWithLoader(() -> { + Thread.sleep(3000); + return null; + }, (JFrame) SwingUtilities.getWindowAncestor(this), StringUtils.isEmpty(gifPath) ? new ImageChild() : new ImageChild(gifPath)); + } + +} diff --git a/designer-base/src/test/java/com/fr/design/gui/storybook/components/ToastStoryBoard.java b/designer-base/src/test/java/com/fr/design/gui/storybook/components/ToastStoryBoard.java new file mode 100644 index 0000000000..4aeec9babc --- /dev/null +++ b/designer-base/src/test/java/com/fr/design/gui/storybook/components/ToastStoryBoard.java @@ -0,0 +1,58 @@ +package com.fr.design.gui.storybook.components; + +import com.fine.component.toast.ToastMsgManager; +import com.fr.design.gui.ibutton.UIButton; +import com.fr.design.gui.storybook.StoryBoard; + +import javax.swing.JFrame; +import javax.swing.SwingUtilities; + +import java.util.Random; + +import static com.fine.swing.ui.layout.Layouts.cell; + +/** + * ToastStoryBoard + * + * @author Levy.Xie + * @since 11.0 + * Created on 2024/12/19 + */ +public class ToastStoryBoard extends StoryBoard { + + public ToastStoryBoard() { + super("消息弹窗"); + add( + cell(createToastButton(ToastMsgManager.Location.TOP_LEFT)), + cell(createToastButton(ToastMsgManager.Location.TOP_CENTER)), + cell(createToastButton(ToastMsgManager.Location.TOP_RIGHT)), + cell(createToastButton(ToastMsgManager.Location.BOTTOM_LEFT)), + cell(createToastButton(ToastMsgManager.Location.BOTTOM_CENTER)), + cell(createToastButton(ToastMsgManager.Location.BOTTOM_RIGHT)) + ); + } + + private UIButton createToastButton(ToastMsgManager.Location location) { + UIButton button = new UIButton(location.name().toLowerCase()); + button.addActionListener(e -> { + ToastMsgManager.getInstance().show(getRandomType(), location, "Hello FineReport", + SwingUtilities.getWindowAncestor(this)); + }); + return button; + } + + private ToastMsgManager.Type getRandomType() { + Random ran = new Random(); + int a = ran.nextInt(4); + if (a == 0) { + return ToastMsgManager.Type.SUCCESS; + } else if (a == 1) { + return ToastMsgManager.Type.INFO; + } else if (a == 2) { + return ToastMsgManager.Type.WARNING; + } else { + return ToastMsgManager.Type.ERROR; + } + } + +} diff --git a/designer-base/src/test/resources/com/fr/design/gui/storybook/components/loadingBar.gif b/designer-base/src/test/resources/com/fr/design/gui/storybook/components/loadingBar.gif new file mode 100644 index 0000000000..1c9a58cea1 Binary files /dev/null and b/designer-base/src/test/resources/com/fr/design/gui/storybook/components/loadingBar.gif differ diff --git a/designer-realize/src/main/java/com/fr/design/actions/replace/ui/ITReplaceMainDialog.java b/designer-realize/src/main/java/com/fr/design/actions/replace/ui/ITReplaceMainDialog.java index b9c3ac37d5..5c52440f85 100644 --- a/designer-realize/src/main/java/com/fr/design/actions/replace/ui/ITReplaceMainDialog.java +++ b/designer-realize/src/main/java/com/fr/design/actions/replace/ui/ITReplaceMainDialog.java @@ -18,8 +18,7 @@ import com.fr.design.i18n.Toolkit; import com.fr.design.mainframe.DesignerContext; import com.fr.design.mainframe.JTemplate; -import com.fr.design.mainframe.toast.DesignerToastMsgUtil; -import com.fr.design.mainframe.toast.ToastMsgDialog; +import com.fine.component.toast.ToastMsgManager; import com.fr.general.GeneralUtils; import com.fr.stable.StringUtils; @@ -212,6 +211,7 @@ public class ITReplaceMainDialog extends UIDialog { * 模板内容替换相关 */ private void replace4Content() { + checkLegalValidity(); String type = GeneralUtils.objectToString(northPane.getFindCombobox().getSelectedItem()); String searchStr = ((UITextField) (northPane.getFindInputCombobox().getEditor().getEditorComponent())).getText(); String replaceStr = ((UITextField) (northPane.getReplaceInputCombobox().getEditor().getEditorComponent())).getText(); @@ -306,8 +306,7 @@ public class ITReplaceMainDialog extends UIDialog { */ public void checkLegalValidity() { if (contentReplaceFailedCount == 0) { - ToastMsgDialog dialog = DesignerToastMsgUtil.createPromptDialog(Toolkit.i18nText("Fine-Design_Replace_Check_None")); - dialog.setVisible(true); + ToastMsgManager.getInstance().info(Toolkit.i18nText("Fine-Design_Replace_Check_None")); } else { new ITCheckDialog(); }