lemon
3 months ago
5 changed files with 617 additions and 0 deletions
@ -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<String, IconSet> SOURCE_ICON_MAPS = new HashMap<>(2); |
||||
private static final HashMap<String, HashMap<String, WeakReference<Icon>>> 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获取图标 |
||||
* <p> |
||||
* 查找路径 |
||||
* 1)查找图集图标 |
||||
* 2)路径为图片图标,从路径再查找 |
||||
* 3)提供默认svg图标 |
||||
* |
||||
* @param id 图标ID |
||||
* @param <I> 图标类型 |
||||
* @return 图标 |
||||
*/ |
||||
@NotNull |
||||
public static <I extends Icon> 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 extends Icon> I findIcon(String source, String id, Dimension dimension, IconType type) { |
||||
String cacheKey = genCacheKey(source, id, dimension, type); |
||||
HashMap<String, WeakReference<Icon>> sourceCache = CACHE.getOrDefault(cacheKey, new HashMap<>(64)); |
||||
final WeakReference<Icon> 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); |
||||
} |
||||
} |
@ -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<String, String> iconId2Path) { |
||||
addIconWithMap(iconId2Path); |
||||
|
||||
if (resource.getPath() == null) { |
||||
return; |
||||
} |
||||
Map<String, Object> json; |
||||
try (InputStream in = resource.getInputStream()) { |
||||
try (Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) { |
||||
json = (Map<String, Object>) 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<String, Object> icons = (Map<String, Object>) json.get("icons"); |
||||
|
||||
for (Map.Entry<String, Object> 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<String, Object>) 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<String, Object> 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<String, String> iconId2Path) { |
||||
for (Map.Entry<String, String> 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); |
||||
} |
||||
} |
@ -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 extends Icon> 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(); |
||||
} |
||||
} |
@ -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<String, String> iconId2Path(); |
||||
|
||||
/** |
||||
* 图标主题 |
||||
*/ |
||||
enum THEME { |
||||
|
||||
/** |
||||
* light_icon |
||||
*/ |
||||
LIGHT_ICON("light_icon"), |
||||
|
||||
/** |
||||
* dark_icon |
||||
*/ |
||||
DARK_ICON("dark_icon") |
||||
; |
||||
|
||||
final String category; |
||||
THEME(String category) { |
||||
this.category = category; |
||||
} |
||||
} |
||||
|
||||
} |
@ -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<String, String> iconId2Path() { |
||||
return null; |
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue