From 783e2af1cd6192ccdfee30f0411c258f8e9187b6 Mon Sep 17 00:00:00 2001 From: lemon Date: Fri, 23 Aug 2024 16:52:17 +0800 Subject: [PATCH] =?UTF-8?q?REPORT-127437=20feat:=E9=80=82=E9=85=8D?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=E5=9B=BE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../theme/icon/plugin/PluginIconManager.java | 213 ++++++++++++++++++ .../fine/theme/icon/plugin/PluginIconSet.java | 136 +++++++++++ .../theme/icon/plugin/PluginLazyIcon.java | 129 +++++++++++ .../com/fr/design/fun/LazyIconProvider.java | 68 ++++++ .../fun/impl/AbstractLazyIconProvider.java | 71 ++++++ 5 files changed, 617 insertions(+) create mode 100644 designer-base/src/main/java/com/fine/theme/icon/plugin/PluginIconManager.java create mode 100644 designer-base/src/main/java/com/fine/theme/icon/plugin/PluginIconSet.java create mode 100644 designer-base/src/main/java/com/fine/theme/icon/plugin/PluginLazyIcon.java create mode 100644 designer-base/src/main/java/com/fr/design/fun/LazyIconProvider.java create mode 100644 designer-base/src/main/java/com/fr/design/fun/impl/AbstractLazyIconProvider.java diff --git a/designer-base/src/main/java/com/fine/theme/icon/plugin/PluginIconManager.java b/designer-base/src/main/java/com/fine/theme/icon/plugin/PluginIconManager.java new file mode 100644 index 0000000000..8648396f75 --- /dev/null +++ b/designer-base/src/main/java/com/fine/theme/icon/plugin/PluginIconManager.java @@ -0,0 +1,213 @@ +package com.fine.theme.icon.plugin; + +import com.fine.theme.icon.IconException; +import com.fine.theme.icon.IconManager; +import com.fine.theme.icon.IconSet; +import com.fine.theme.icon.IconType; +import com.fine.theme.icon.LazyIcon; +import com.fr.base.extension.FileExtension; +import com.fr.general.IOUtils; +import com.fr.log.FineLoggerFactory; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.Icon; +import java.awt.Dimension; +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.Map; + +/** + * 管理插件图标集 + * + * @author lemon + * @since + * Created on 2024/08/23 + */ +public class PluginIconManager { + + public static final String ICON_DISABLE_SUFFIX = "_disable"; + public static final Dimension DEFAULT_DIMENSION = new Dimension(16, 16); + private static final Map SOURCE_ICON_MAPS = new HashMap<>(2); + private static final HashMap>> CACHE = new HashMap<>(); + + + /** + * 获取图标集 + * + * @param id 图标集ID + * @return 图标集 + */ + public static IconSet getSet(String source, String id) { + IconSet set = SOURCE_ICON_MAPS.get(source); + + if (set != null && set.getId().equals(id)) { + return set; + } + throw new IconException("[PluginIconManager] Can not find icon set by id: " + id); + } + + /** + * 添加图标集 + * + * @param source 插件 + * @param set 图标集 + */ + public static void addSet(@NotNull String source, @NotNull IconSet set) { + if (SOURCE_ICON_MAPS.containsKey(source)) { + FineLoggerFactory.getLogger().warn("[PluginIconManager] plugin:{} icon set already exists: " + source); + } + SOURCE_ICON_MAPS.put(source, set); + clearCacheBySource(source); + } + + /** + * 更新指定插件图标集 + * + * @param source 插件 + * @param set 图标集 + */ + public static void updateSet(@NotNull String source, @NotNull IconSet set) { + SOURCE_ICON_MAPS.put(source, set); + clearCacheBySource(source); + } + + /** + * 删除指定插件图标集 + * + * @param source 插件 + */ + public static void removeSet(@NotNull String source) { + SOURCE_ICON_MAPS.remove(source); + clearCacheBySource(source); + } + + /** + * 根据图标ID获取图标 + *

+ * 查找路径 + * 1)查找图集图标 + * 2)路径为图片图标,从路径再查找 + * 3)提供默认svg图标 + * + * @param id 图标ID + * @param 图标类型 + * @return 图标 + */ + @NotNull + public static I getIcon(@NotNull String source, @NotNull final String id, @NotNull Dimension dimension, @NotNull IconType type) { + Icon icon = findIcon(source, id, dimension, type); + if (icon == null) { + // 只有找不到再进行其他fallback,提升效率 + if (IconManager.isImageIcon(id)) { + return (I) fallbackLegacyIcon(id); + } else { + FineLoggerFactory.getLogger().warn("[PluginIconManager] Can not find icon by id: " + id); + return (I) new LazyIcon("default"); + } + } + return (I) icon; + } + + private static Icon fallbackLegacyIcon(String id) { + return IOUtils.readIcon(id); + } + + @Nullable + private static I findIcon(String source, String id, Dimension dimension, IconType type) { + String cacheKey = genCacheKey(source, id, dimension, type); + HashMap> sourceCache = CACHE.getOrDefault(cacheKey, new HashMap<>(64)); + final WeakReference reference = sourceCache.get(cacheKey); + I icon = reference != null ? (I) reference.get() : null; + if (icon == null) { + IconSet set = SOURCE_ICON_MAPS.get(source); + if (set == null) { + return icon; + } + Icon f = set.findIcon(id, dimension, type); + if (f != null) { + icon = (I) f; + sourceCache.put(cacheKey, new WeakReference<>(icon)); + CACHE.put(source, sourceCache); + } + } + return icon; + } + + + /** + * 生成缓存key + * + * @param id id + * @param dimension 尺寸 + * @param type 图标类型 + * @return 缓存key + */ + public static @NotNull String genCacheKey(String source, String id, Dimension dimension, IconType type) { + if (DEFAULT_DIMENSION.equals(dimension)) { + return source + "_" + id + "_" + type; + } + return source + "_" + id + "_" + dimension.width + "_" + dimension.height + "_" + type; + } + + /** + * 是否SVG图标格式 + * + * @param path 路径 + * @return 是否SVG图标格式 + */ + public static boolean isSvgIcon(String path) { + return FileExtension.SVG.matchExtension(path); + } + + /** + * 是否支持的图片图标格式,目前只支持png和jpg + * + * @param path 路径 + * @return 是否支持的图片图标格式 + */ + public static boolean isImageIcon(String path) { + return FileExtension.PNG.matchExtension(path) + || FileExtension.JPG.matchExtension(path); + } + + /** + * 判断是否存在指定id的icon,非io读取行为,而是从已注册的sourceMap中遍历判断 + * + * @param id id + * @return 是否存在 + */ + public static boolean existIcon(String id, String source) { + IconSet set = SOURCE_ICON_MAPS.get(source); + if (set != null && set.getIds().contains(id)) { + return true; + } + return false; + } + + /** + * 清理所有缓存 + */ + public static void clearCache() { + CACHE.clear(); + } + + /** + * 清楚指定插件缓存 + * @param source 插件标识 + */ + public static void clearCacheBySource(String source) { + CACHE.remove(source); + } + + /** + * 查找灰化图标 + * + * @param path 原始路径 + * @return 灰化路径 + */ + public static String findDisablePath(String path) { + int i = path.lastIndexOf('.'); + return path.substring(0, i) + ICON_DISABLE_SUFFIX + path.substring(i); + } +} \ No newline at end of file diff --git a/designer-base/src/main/java/com/fine/theme/icon/plugin/PluginIconSet.java b/designer-base/src/main/java/com/fine/theme/icon/plugin/PluginIconSet.java new file mode 100644 index 0000000000..151322d725 --- /dev/null +++ b/designer-base/src/main/java/com/fine/theme/icon/plugin/PluginIconSet.java @@ -0,0 +1,136 @@ +package com.fine.theme.icon.plugin; + +import com.fine.theme.icon.AbstractIconSet; +import com.fine.theme.icon.IconManager; +import com.fine.theme.icon.IconType; +import com.formdev.flatlaf.json.Json; +import com.formdev.flatlaf.json.ParseException; +import com.fr.stable.StringUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Objects; + +/** + * 插件图标集 + * + * @author lemon + * @since + * Created on 2024/08/20 + */ +public class PluginIconSet extends AbstractIconSet { + + private String base; + + + public PluginIconSet(PluginUrlIconResource resource, Map iconId2Path) { + addIconWithMap(iconId2Path); + + if (resource.getPath() == null) { + return; + } + Map json; + try (InputStream in = resource.getInputStream()) { + try (Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) { + json = (Map) Json.parse(reader); + } + } catch (ParseException | IOException ex) { + throw new RuntimeException(ex.getMessage(), ex); + } + + name = (String) json.get("name"); + dark = Boolean.parseBoolean((String) json.get("dark")); + base = (String) json.get("base"); + if (base == null) { + base = StringUtils.EMPTY; + } + + Map icons = (Map) json.get("icons"); + + for (Map.Entry icon : icons.entrySet()) { + applyIcon(icon.getKey(), icon.getValue()); + } + + } + + private void applyIcon(String key, Object value) { + if (value instanceof String) { + dealWithIconString(key, (String) value); + } else if (value instanceof Map) { + dealWithIconMap(key, (Map) value); + } + } + + private void dealWithIconString(String key, String value) { + if (IconManager.isSvgIcon(value)) { + // 默认字符串提供正常图和灰化图 + addIcon(new PluginSvgIconSource(key, + base + value, + IconManager.findDisablePath(base + value), + null + )); + } else if (IconManager.isImageIcon(value)) { + addIcon(new PluginImageIconSource(key, base + value)); + } + // 其他无法识别格式不处理 + } + + + /** + * 处理object形式的icon配置 + */ + private void dealWithIconMap(String key, Map value) { + String normalPath = (String) value.get(IconType.normal.name()); + String disablePath = (String) value.get(IconType.disable.name()); + String whitePath = (String) value.get(IconType.white.name()); + // 暂不支持混合格式,每个id的格式需要保持一致 + if (IconManager.isSvgIcon(normalPath)) { + addIcon(new PluginSvgIconSource(key, + base + normalPath, + StringUtils.isNotBlank(disablePath) ? base + disablePath : null, + StringUtils.isNotBlank(whitePath) ? base + whitePath : null + )); + } else if (IconManager.isImageIcon(normalPath)) { + addIcon(new PluginImageIconSource(key, + base + normalPath, + StringUtils.isNotBlank(disablePath) ? base + disablePath : null, + StringUtils.isNotBlank(whitePath) ? base + whitePath : null + )); + } + } + + /** + * 根据 map 注册图标 + * @param iconId2Path key: id, value: icon path + */ + public void addIconWithMap(Map iconId2Path) { + for (Map.Entry entry: iconId2Path.entrySet()) { + if (PluginIconManager.isSvgIcon(entry.getValue())) { + addIcon(new PluginSvgIconSource(entry.getKey(), entry.getValue())); + } else if (PluginIconManager.isImageIcon(entry.getValue())) { + addIcon(new PluginImageIconSource(entry.getKey(), entry.getValue())); + } + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PluginIconSet that = (PluginIconSet) o; + return Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hashCode(name); + } +} diff --git a/designer-base/src/main/java/com/fine/theme/icon/plugin/PluginLazyIcon.java b/designer-base/src/main/java/com/fine/theme/icon/plugin/PluginLazyIcon.java new file mode 100644 index 0000000000..c6ac211335 --- /dev/null +++ b/designer-base/src/main/java/com/fine/theme/icon/plugin/PluginLazyIcon.java @@ -0,0 +1,129 @@ +package com.fine.theme.icon.plugin; + +import com.fine.theme.icon.DisabledIcon; +import com.fine.theme.icon.IconManager; +import com.fine.theme.icon.IconType; +import com.fine.theme.icon.Identifiable; +import com.fine.theme.icon.WhiteIcon; +import com.fr.design.fun.impl.AbstractLazyIconProvider; +import org.jetbrains.annotations.NotNull; + +import javax.swing.Icon; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Graphics; +import java.util.StringJoiner; + +import static com.fine.theme.utils.FineUIScale.scale; + +public class PluginLazyIcon implements Identifiable, DisabledIcon, WhiteIcon, Icon { + @NotNull + private final String id; + + private final Dimension dimension; + + private final IconType type; + + private final String source; + + /** + * + * @param source {@link AbstractLazyIconProvider#source()} + * @param id + */ + public PluginLazyIcon(@NotNull final String source, @NotNull final String id) { + this.id = id; + this.dimension = IconManager.DEFAULT_DIMENSION; + this.type = IconType.normal; + this.source = source; + } + + public PluginLazyIcon(@NotNull final String source, @NotNull final String id, int side) { + this.id = id; + this.dimension = new Dimension(side, side); + this.type = IconType.normal; + this.source = source; + } + + public PluginLazyIcon(@NotNull final String source, @NotNull final String id, @NotNull Dimension dimension) { + this.id = id; + this.dimension = dimension; + this.type = IconType.normal; + this.source = source; + } + + private PluginLazyIcon(@NotNull final String source, @NotNull final String id, @NotNull IconType type) { + this.id = id; + this.dimension = IconManager.DEFAULT_DIMENSION; + this.type = type; + this.source = source; + } + + public PluginLazyIcon(@NotNull final String source, @NotNull final String id, @NotNull Dimension dimension, @NotNull IconType type) { + this.id = id; + this.dimension = dimension; + this.type = type; + this.source = source; + } + + + @NotNull + @Override + public String getId() { + return id; + } + + @Override + public void paintIcon(@NotNull final Component c, @NotNull final Graphics g, final int x, final int y) { + getIcon().paintIcon(c, g, x, y); + } + + @Override + public int getIconWidth() { + return scale(dimension.width); + } + + @Override + public int getIconHeight() { + return scale(dimension.height); + } + + + @NotNull + public I getIcon() { + return PluginIconManager.getIcon(source, getId(), dimension, type); + } + + /** + * 创建一份灰化图标 + * + * @return 灰化图标 + */ + @NotNull + @Override + public Icon disabled() { + return new PluginLazyIcon(source, getId(), dimension, IconType.disable); + } + + /** + * 创建一份白化图标 + * + * @return 白化图标 + */ + @NotNull + @Override + public Icon white() { + return new PluginLazyIcon(source, getId(), dimension, IconType.white); + } + + + @Override + public String toString() { + return new StringJoiner(", ", PluginLazyIcon.class.getSimpleName() + "[", "]") + .add("source='" + source + "'") + .add("id='" + id + "'") + .add("size=" + "[w=" + scale(dimension.width) + ",h=" + scale(dimension.height) + "]") + .add("type=" + type) + .toString(); + } +} diff --git a/designer-base/src/main/java/com/fr/design/fun/LazyIconProvider.java b/designer-base/src/main/java/com/fr/design/fun/LazyIconProvider.java new file mode 100644 index 0000000000..93989775fe --- /dev/null +++ b/designer-base/src/main/java/com/fr/design/fun/LazyIconProvider.java @@ -0,0 +1,68 @@ +package com.fr.design.fun; + +import com.fr.stable.fun.mark.Mutable; + +import java.util.Map; + +/** + * 插件图标适配接口 + * + * @author lemon + * @since + * Created on + */ +public interface LazyIconProvider extends Mutable { + String MARK_STRING = "LazyIconProvider"; + + int CURRENT_LEVEL = 1; + + /** + * 自定义,icon 来源标识 + * + * @return 来源标识 + */ + String source(); + + /** + * json 文件路径 + * + * @return 图标注册 json 路径 + */ + String jsonPath(); + + /** + * 图标所属主题分类 + * + * @return 主题类别 + */ + THEME themeCategory(); + + /** + * 构建需要注册的图标 key: id, value: icon path + * @return map + */ + Map iconId2Path(); + + /** + * 图标主题 + */ + enum THEME { + + /** + * light_icon + */ + LIGHT_ICON("light_icon"), + + /** + * dark_icon + */ + DARK_ICON("dark_icon") + ; + + final String category; + THEME(String category) { + this.category = category; + } + } + +} diff --git a/designer-base/src/main/java/com/fr/design/fun/impl/AbstractLazyIconProvider.java b/designer-base/src/main/java/com/fr/design/fun/impl/AbstractLazyIconProvider.java new file mode 100644 index 0000000000..53002c2705 --- /dev/null +++ b/designer-base/src/main/java/com/fr/design/fun/impl/AbstractLazyIconProvider.java @@ -0,0 +1,71 @@ +package com.fr.design.fun.impl; + +import com.fr.design.fun.LazyIconProvider; +import com.fr.stable.fun.impl.AbstractProvider; +import com.fr.stable.fun.mark.API; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.Map; + + +/** + * 插件图标 LazyIcon 加载适配抽象类 + * + * @author lemon + * @since + * Created on + */ +@API(level = LazyIconProvider.CURRENT_LEVEL) +public class AbstractLazyIconProvider extends AbstractProvider implements LazyIconProvider { + + /** + * 当前接口的API等级,用于判断是否需要升级插件 + * @return API等级 + */ + @Override + public int currentAPILevel() { + return CURRENT_LEVEL; + } + + /** + * 区分插件 + * + * @return 插件 id + */ + @Override + public String source() { + throw new RuntimeException("source is blank"); + } + + /** + * 通过 json 注册图标 + * + * @return 图标注册 json 路径 + */ + @Override + public String jsonPath() { + return null; + } + + /** + * 图标主题 {@link THEME} + * + * @return 主题类别 + */ + @Override + public THEME themeCategory() { + return null; + } + + /** + * 直接注册图标:key 是 icon id, value 是 icon path + * + * @return map + */ + @Override + public Map iconId2Path() { + return null; + } + +}