Merge in DESIGN/design from ~VITO/c-design:newui to newui * commit 'bee1e0a6b993e3bac25685eefeb50a8e489f29d7': 无jira任务 代码质量 REPORT-99485 新图标管理器newui
@ -0,0 +1,79 @@
|
||||
package com.fine.theme.icon; |
||||
|
||||
import com.fr.third.errorprone.annotations.Immutable; |
||||
import org.jetbrains.annotations.NotNull; |
||||
import org.jetbrains.annotations.Nullable; |
||||
|
||||
import javax.swing.Icon; |
||||
import java.util.Collection; |
||||
import java.util.Map; |
||||
import java.util.concurrent.ConcurrentHashMap; |
||||
|
||||
/** |
||||
* 抽象图标集 |
||||
* |
||||
* @author vito |
||||
* @since 11.0 |
||||
* Created on 2023/11/15 |
||||
*/ |
||||
@Immutable |
||||
public abstract class AbstractIconSet implements IconSet { |
||||
|
||||
private final String id; |
||||
|
||||
private final Map<String, IconSource<? extends Icon>> map = new ConcurrentHashMap<>(64); |
||||
private final Map<String, Icon> cache = new ConcurrentHashMap<>(64); |
||||
private final Map<String, Icon> disableCache = new ConcurrentHashMap<>(64); |
||||
|
||||
public AbstractIconSet(String id) { |
||||
this.id = id; |
||||
} |
||||
|
||||
@Override |
||||
public @NotNull String getId() { |
||||
return id; |
||||
} |
||||
|
||||
@Override |
||||
public @NotNull Collection<String> getIds() { |
||||
return map.keySet(); |
||||
} |
||||
|
||||
@Override |
||||
public void addIcon(@NotNull IconSource<? extends Icon> icon) { |
||||
map.put(icon.getId(), icon); |
||||
} |
||||
|
||||
@Override |
||||
public void addIcon(@NotNull IconSource<? extends Icon>... icons) { |
||||
for (IconSource<? extends Icon> icon: icons){ |
||||
map.put(icon.getId(), icon); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public @Nullable Icon findIcon(@NotNull String id) { |
||||
Icon icon = cache.get(id); |
||||
if (icon == null) { |
||||
IconSource<? extends Icon> iconSource = map.get(id); |
||||
if (iconSource != null) { |
||||
icon = iconSource.loadIcon(); |
||||
cache.put(id, icon); |
||||
} |
||||
} |
||||
return icon; |
||||
} |
||||
|
||||
@Override |
||||
public @Nullable Icon findDisableIcon(@NotNull String id) { |
||||
Icon icon = disableCache.get(id); |
||||
if (icon == null) { |
||||
IconSource<? extends Icon> iconSource = map.get(id); |
||||
if (iconSource != null) { |
||||
icon = iconSource.disabled(); |
||||
disableCache.put(id, icon); |
||||
} |
||||
} |
||||
return icon; |
||||
} |
||||
} |
@ -0,0 +1,106 @@
|
||||
package com.fine.theme.icon; |
||||
|
||||
import com.fr.third.errorprone.annotations.Immutable; |
||||
import org.jetbrains.annotations.NotNull; |
||||
import org.jetbrains.annotations.Nullable; |
||||
|
||||
import javax.swing.Icon; |
||||
|
||||
/** |
||||
* 抽象图标源 |
||||
* 能够根据图标源寻找灰化图标资源 |
||||
* |
||||
* @author vito |
||||
* @since 11.0 |
||||
* Created on 2023/11/14 |
||||
*/ |
||||
@Immutable |
||||
public abstract class AbstractIconSource<I extends Icon> implements IconSource<I> { |
||||
|
||||
public static final String ICON_DISABLE = "_disable"; |
||||
|
||||
protected String id; |
||||
|
||||
protected final IconResource resource; |
||||
|
||||
@Nullable |
||||
protected IconResource grayResource; |
||||
|
||||
protected boolean autoFindDisable = false; |
||||
|
||||
public AbstractIconSource(@NotNull final String id, @NotNull final IconResource resource) { |
||||
this.id = id; |
||||
this.resource = resource; |
||||
} |
||||
|
||||
public AbstractIconSource(@NotNull final String id, |
||||
@NotNull final IconResource resource, |
||||
final boolean autoFindDisable) { |
||||
this.id = id; |
||||
this.resource = resource; |
||||
this.autoFindDisable = autoFindDisable; |
||||
} |
||||
|
||||
|
||||
public AbstractIconSource(@NotNull final String id, |
||||
@NotNull final IconResource resource, |
||||
@Nullable final IconResource grayResource) { |
||||
this.id = id; |
||||
this.resource = resource; |
||||
this.grayResource = grayResource; |
||||
} |
||||
|
||||
@NotNull |
||||
@Override |
||||
public String getId() { |
||||
return id; |
||||
} |
||||
|
||||
@NotNull |
||||
@Override |
||||
public IconResource getResource() { |
||||
return resource; |
||||
} |
||||
|
||||
@NotNull |
||||
@Override |
||||
public I loadIcon() { |
||||
try { |
||||
return loadIcon(resource); |
||||
} catch (final Exception e) { |
||||
throw new IconException("Unable to load Icon: " + getId(), e); |
||||
} |
||||
} |
||||
|
||||
@NotNull |
||||
protected abstract I loadIcon(@NotNull IconResource resource); |
||||
|
||||
|
||||
/** |
||||
* 先找提供明确URL的灰化图, |
||||
* 再找指定自动寻找路径的灰化图, |
||||
* 最后再由具体图标提供默认灰化图 |
||||
* |
||||
* @return 灰化图 |
||||
*/ |
||||
@Override |
||||
public @NotNull I disabled() { |
||||
if (grayResource != null) { |
||||
return loadIcon(grayResource); |
||||
} |
||||
if (autoFindDisable && resource instanceof UrlIconResource) { |
||||
String disablePath = findDisablePath(((UrlIconResource) resource).getPath()); |
||||
grayResource = new UrlIconResource(disablePath); |
||||
return loadIcon(grayResource); |
||||
} |
||||
return loadDisableIcon(); |
||||
} |
||||
|
||||
private static String findDisablePath(String path) { |
||||
int i = path.lastIndexOf('.'); |
||||
return path.substring(0, i) + ICON_DISABLE + path.substring(i); |
||||
} |
||||
|
||||
@NotNull |
||||
protected abstract I loadDisableIcon(); |
||||
} |
@ -0,0 +1,23 @@
|
||||
package com.fine.theme.icon; |
||||
|
||||
import org.jetbrains.annotations.NotNull; |
||||
|
||||
import javax.swing.Icon; |
||||
|
||||
/** |
||||
* 创建一个灰化 Icon |
||||
* |
||||
* @author vito |
||||
* @since 11.0 |
||||
* Created on 2023/11/16 |
||||
*/ |
||||
public interface DisabledIcon { |
||||
|
||||
/** |
||||
* 创建一份灰化图标 |
||||
* |
||||
* @return 灰化图标 |
||||
*/ |
||||
@NotNull |
||||
Icon disabled(); |
||||
} |
@ -0,0 +1,29 @@
|
||||
package com.fine.theme.icon; |
||||
|
||||
/** |
||||
* 图标异常 |
||||
* |
||||
* @author vito |
||||
* @since 11.0 |
||||
* Created on 2023/11/6 |
||||
*/ |
||||
public class IconException extends RuntimeException { |
||||
public IconException() { |
||||
} |
||||
|
||||
public IconException(String message) { |
||||
super(message); |
||||
} |
||||
|
||||
public IconException(String message, Throwable cause) { |
||||
super(message, cause); |
||||
} |
||||
|
||||
public IconException(Throwable cause) { |
||||
super(cause); |
||||
} |
||||
|
||||
public IconException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { |
||||
super(message, cause, enableSuppression, writableStackTrace); |
||||
} |
||||
} |
@ -0,0 +1,147 @@
|
||||
package com.fine.theme.icon; |
||||
|
||||
import com.fr.third.errorprone.annotations.Immutable; |
||||
import org.jetbrains.annotations.NotNull; |
||||
import org.jetbrains.annotations.Nullable; |
||||
|
||||
import javax.swing.Icon; |
||||
import java.lang.ref.WeakReference; |
||||
import java.util.ArrayList; |
||||
import java.util.HashMap; |
||||
|
||||
/** |
||||
* 图标管理器 |
||||
* 1. 提供注册管理图标集,方便整体替换 |
||||
* 2. 提供图标缓存 |
||||
* 3. 查找图标 |
||||
* 4. 配合 {@link LazyIcon} 实现图标懒加载 |
||||
* |
||||
* @author vito |
||||
* @since 11.0 |
||||
* Created on 2023/9/12 |
||||
*/ |
||||
@Immutable |
||||
public class IconManager { |
||||
|
||||
public static boolean initialized = false; |
||||
public static ArrayList<IconSet> iconSets = new ArrayList<>(2); |
||||
; |
||||
public static HashMap<String, WeakReference<Icon>> cache = new HashMap<>(60); |
||||
; |
||||
public static HashMap<String, WeakReference<Icon>> disableCache = new HashMap<>(60); |
||||
|
||||
|
||||
/** |
||||
* 获取图标集 |
||||
* |
||||
* @param id 图标集ID |
||||
* @return 图标集 |
||||
*/ |
||||
public static IconSet getSet(String id) { |
||||
for (IconSet set : iconSets) { |
||||
if (set.getId().equals(id)) { |
||||
return set; |
||||
} |
||||
} |
||||
throw new IconException("[IconManager] Can not find icon set by id: " + id); |
||||
} |
||||
|
||||
/** |
||||
* 添加图标集 |
||||
* |
||||
* @param set 图标集 |
||||
*/ |
||||
public static void addSet(@NotNull IconSet set) { |
||||
if (!iconSets.contains(set)) { |
||||
iconSets.add(set); |
||||
// 清理可能来自其他图集相同名称图标
|
||||
clearIconSetCache(set); |
||||
} else { |
||||
throw new IconException("[IconManager] IconSet by id:" + set.getId() + "is added!"); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 根据图标ID获取图标 |
||||
* |
||||
* @param id 图标ID |
||||
* @param <I> 图标类型 |
||||
* @return 图标 |
||||
*/ |
||||
@NotNull |
||||
public static <I extends Icon> I getIcon(String id) { |
||||
Icon icon = findIcon(id); |
||||
if (icon == null) { |
||||
throw new IconException("[IconManager] Can not find icon by id: " + id); |
||||
} |
||||
return (I) icon; |
||||
} |
||||
|
||||
/** |
||||
* 获取灰化图标 |
||||
* |
||||
* @param id 图标ID |
||||
* @param <I> 图标类型 |
||||
* @return 图标 |
||||
*/ |
||||
@NotNull |
||||
public static <I extends Icon> I getDisableIcon(String id) { |
||||
Icon icon = findDisableIcon(id); |
||||
if (icon == null) { |
||||
throw new IconException("[IconManager] Can not find icon by id: " + id); |
||||
} |
||||
return (I) icon; |
||||
} |
||||
|
||||
@Nullable |
||||
private static <I extends Icon> I findDisableIcon(String id) { |
||||
final WeakReference<Icon> reference = disableCache.get(id); |
||||
I icon = reference != null ? (I) reference.get() : null; |
||||
if (icon == null) { |
||||
for (IconSet set : iconSets) { |
||||
Icon f = set.findDisableIcon(id); |
||||
if (f != null) { |
||||
icon = (I) f; |
||||
disableCache.put(id, new WeakReference<>(icon)); |
||||
} |
||||
} |
||||
} |
||||
return icon; |
||||
} |
||||
|
||||
@Nullable |
||||
private static <I extends Icon> I findIcon(String id) { |
||||
final WeakReference<Icon> reference = cache.get(id); |
||||
I icon = reference != null ? (I) reference.get() : null; |
||||
if (icon == null) { |
||||
for (IconSet set : iconSets) { |
||||
Icon f = set.findIcon(id); |
||||
if (f != null) { |
||||
icon = (I) f; |
||||
cache.put(id, new WeakReference<>(icon)); |
||||
} |
||||
} |
||||
} |
||||
return icon; |
||||
} |
||||
|
||||
/** |
||||
* 清理所有缓存 |
||||
*/ |
||||
public static void clearCache() { |
||||
cache.clear(); |
||||
} |
||||
|
||||
/** |
||||
* 清理图标缓存 |
||||
*/ |
||||
public static void clearIconCache(String id) { |
||||
cache.remove(id); |
||||
} |
||||
|
||||
private static void clearIconSetCache(@NotNull IconSet set) { |
||||
for (String id : set.getIds()) { |
||||
cache.remove(id); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,23 @@
|
||||
package com.fine.theme.icon; |
||||
|
||||
import org.jetbrains.annotations.Nullable; |
||||
|
||||
import java.io.InputStream; |
||||
|
||||
/** |
||||
* 资源接口 |
||||
* |
||||
* @author vito |
||||
* @since 11.0 |
||||
* Created on 2023/11/6 |
||||
*/ |
||||
public interface IconResource { |
||||
|
||||
/** |
||||
* 获取输入资源流 |
||||
* |
||||
* @return 资源流 |
||||
*/ |
||||
@Nullable |
||||
InputStream getInputStream(); |
||||
} |
@ -0,0 +1,63 @@
|
||||
package com.fine.theme.icon; |
||||
|
||||
import com.fr.third.errorprone.annotations.Immutable; |
||||
import org.jetbrains.annotations.NotNull; |
||||
import org.jetbrains.annotations.Nullable; |
||||
|
||||
import javax.swing.Icon; |
||||
import java.util.Collection; |
||||
|
||||
/** |
||||
* 图标集 |
||||
* |
||||
* @author vito |
||||
* @since 11.0 |
||||
* Created on 2023/11/6 |
||||
*/ |
||||
@Immutable |
||||
public interface IconSet extends Identifiable { |
||||
@NotNull |
||||
@Override |
||||
String getId(); |
||||
|
||||
/** |
||||
* 返回集合中所有 Icons 的 id。 |
||||
* |
||||
* @return 集合中所有 Icons 的 id。 |
||||
*/ |
||||
@NotNull |
||||
Collection<String> getIds(); |
||||
|
||||
/** |
||||
* 将指定 IconSource 引用的新 Icon 添加到集合中。 |
||||
* |
||||
* @param icon icon 源 |
||||
*/ |
||||
void addIcon(@NotNull IconSource<? extends Icon> icon); |
||||
|
||||
/** |
||||
* 将指定 IconSource 引用的新 Icon 添加到集合中。 |
||||
* |
||||
* @param icon icon 源 |
||||
*/ |
||||
void addIcon(@NotNull IconSource<? extends Icon>... icon); |
||||
|
||||
/** |
||||
* 返回指定 id 的 Icon。 |
||||
* |
||||
* @param id id |
||||
* @return Icon |
||||
*/ |
||||
@Nullable |
||||
Icon findIcon(@NotNull String id); |
||||
|
||||
|
||||
/** |
||||
* 返回指定 id 的 Icon。 |
||||
* |
||||
* @param id id |
||||
* @return Icon |
||||
*/ |
||||
@Nullable |
||||
Icon findDisableIcon(@NotNull String id); |
||||
} |
@ -0,0 +1,36 @@
|
||||
package com.fine.theme.icon; |
||||
|
||||
import com.fr.third.errorprone.annotations.Immutable; |
||||
import org.jetbrains.annotations.NotNull; |
||||
|
||||
import javax.swing.Icon; |
||||
import java.io.Serializable; |
||||
|
||||
/** |
||||
* 图标源,在进行图标管理的时候代替真实图标对象 |
||||
* 使用时加载,节省内存 |
||||
* |
||||
* @author vito |
||||
* @since 11.0 |
||||
* Created on 2023/11/6 |
||||
*/ |
||||
@Immutable |
||||
public interface IconSource<I extends Icon> extends Identifiable, DisabledIcon, Cloneable, Serializable { |
||||
|
||||
/** |
||||
* 获取图标资源 |
||||
* |
||||
* @return 图标资源 |
||||
*/ |
||||
@NotNull |
||||
IconResource getResource(); |
||||
|
||||
|
||||
/** |
||||
* 加载图标 |
||||
* |
||||
* @return 图标 |
||||
*/ |
||||
@NotNull |
||||
I loadIcon(); |
||||
} |
@ -0,0 +1,12 @@
|
||||
package com.fine.theme.icon; |
||||
|
||||
/** |
||||
* id 接口 |
||||
* |
||||
* @author vito |
||||
* @since 11.0 |
||||
* Created on 2023/11/6 |
||||
*/ |
||||
public interface Identifiable { |
||||
String getId(); |
||||
} |
@ -0,0 +1,71 @@
|
||||
package com.fine.theme.icon; |
||||
|
||||
import com.fr.third.errorprone.annotations.Immutable; |
||||
import org.jetbrains.annotations.NotNull; |
||||
|
||||
import javax.swing.Icon; |
||||
import java.awt.Component; |
||||
import java.awt.Graphics; |
||||
|
||||
/** |
||||
* 懒加载图标 |
||||
* |
||||
* @author vito |
||||
* @since 11.0 |
||||
* Created on 2023/11/6 |
||||
*/ |
||||
@Immutable |
||||
public class LazyIcon implements Identifiable, DisabledIcon, Icon { |
||||
@NotNull |
||||
private final String id; |
||||
|
||||
public LazyIcon(@NotNull final String id) { |
||||
this.id = id; |
||||
} |
||||
|
||||
|
||||
@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 getIcon().getIconWidth(); |
||||
} |
||||
|
||||
@Override |
||||
public int getIconHeight() { |
||||
return getIcon().getIconHeight(); |
||||
} |
||||
|
||||
|
||||
@NotNull |
||||
public <I extends Icon> I getIcon() { |
||||
return IconManager.getIcon(getId()); |
||||
} |
||||
|
||||
/** |
||||
* 创建一份灰化图标 |
||||
* |
||||
* @return 灰化图标 |
||||
*/ |
||||
@NotNull |
||||
@Override |
||||
public Icon disabled() { |
||||
return IconManager.getDisableIcon(getId()); |
||||
} |
||||
|
||||
@NotNull |
||||
@Override |
||||
public String toString() { |
||||
return getClass().getCanonicalName() + "[id=" + getId() + "]"; |
||||
|
||||
} |
||||
} |
@ -0,0 +1,39 @@
|
||||
package com.fine.theme.icon; |
||||
|
||||
import com.fr.general.IOUtils; |
||||
import com.fr.io.utils.ResourceIOUtils; |
||||
import com.fr.third.errorprone.annotations.Immutable; |
||||
|
||||
import java.io.InputStream; |
||||
|
||||
/** |
||||
* url图标资源 |
||||
* |
||||
* @author vito |
||||
* @since 11.0 |
||||
* Created on 2023/11/15 |
||||
*/ |
||||
@Immutable |
||||
public class UrlIconResource implements IconResource { |
||||
|
||||
private final String path; |
||||
|
||||
public UrlIconResource(String path) { |
||||
this.path = path; |
||||
} |
||||
|
||||
public String getPath() { |
||||
return path; |
||||
} |
||||
|
||||
@Override |
||||
public InputStream getInputStream() { |
||||
return getInputStream(path); |
||||
} |
||||
|
||||
|
||||
private InputStream getInputStream(String path) { |
||||
InputStream inputStream = IOUtils.getInputStream(path); |
||||
return inputStream != null ? inputStream : ResourceIOUtils.read(path); |
||||
} |
||||
} |
@ -0,0 +1,159 @@
|
||||
package com.fine.theme.icon.svg; |
||||
|
||||
import com.fine.theme.icon.DisabledIcon; |
||||
import com.fine.theme.icon.IconResource; |
||||
import com.fr.clone.cloning.Immutable; |
||||
import org.apache.batik.transcoder.TranscoderException; |
||||
import org.apache.batik.transcoder.TranscoderInput; |
||||
import org.jetbrains.annotations.NotNull; |
||||
|
||||
import javax.swing.Icon; |
||||
import java.awt.Color; |
||||
import java.awt.Component; |
||||
import java.awt.Dimension; |
||||
import java.awt.Graphics; |
||||
import java.awt.Graphics2D; |
||||
import java.awt.image.BufferedImage; |
||||
|
||||
import static com.fine.theme.utils.FineUIScale.scale; |
||||
import static com.fine.theme.utils.FineUIScale.unscale; |
||||
import static com.fine.theme.utils.FineUIUtils.RETINA_SCALE_FACTOR; |
||||
import static com.fine.theme.utils.FineUIUtils.getRetina; |
||||
|
||||
/** |
||||
* svg图标 |
||||
* 1.绘制长度会跟随DPI比率变化 |
||||
* 2.如果设备支持 Retina 则支持Retina绘制HIDPI图 |
||||
* 1跟2的缩放原因不同,因此不能混淆,宽高测量等依旧 |
||||
* 使用DPI缩放进行,只有绘制内容时使用Retina绘制(如果有) |
||||
* Retina绘制不影响最终尺寸,注意区分 |
||||
* |
||||
* @author vito |
||||
* @since 11.0 |
||||
* Created on 2023/11/15 |
||||
*/ |
||||
@Immutable |
||||
public class SvgIcon implements DisabledIcon, Icon { |
||||
|
||||
protected transient BufferedImage cache; |
||||
|
||||
private final Dimension size; |
||||
private final IconResource resource; |
||||
private final boolean disable; |
||||
|
||||
public SvgIcon(IconResource resource, Dimension size) { |
||||
this(resource, size, false); |
||||
} |
||||
|
||||
public SvgIcon(IconResource resource, Dimension size, boolean disable) { |
||||
this.resource = resource; |
||||
// 根据dpi进行缩放
|
||||
this.size = scale(size); |
||||
this.disable = disable; |
||||
} |
||||
|
||||
public SvgIcon(IconResource resource, int side) { |
||||
this(resource, new Dimension(side, side), false); |
||||
} |
||||
|
||||
/** |
||||
* 如果支持绘制Retina绘制,则进行Retina绘制, |
||||
* 绘制结束不影响任何外部尺寸 |
||||
*/ |
||||
@Override |
||||
public void paintIcon(Component c, Graphics g, int x, int y) { |
||||
if (isCacheValid()) { |
||||
if (cache != null) { |
||||
cache.flush(); |
||||
} |
||||
Dimension scale = scaleRetina(size); |
||||
if (disable && c != null) { |
||||
cache = toGrayImage(scale, c.getBackground()); |
||||
} else { |
||||
cache = toImage(scale); |
||||
} |
||||
} |
||||
if (getRetina()) { |
||||
// 高清绘制的原理:scale(1/2,1/2)的原理是坐标减半,底层是矩阵进行坐标变换,意思是坐标减半进行绘制,
|
||||
// 这样就可以将两倍图绘制到一倍的大小,如果这时候设备支持Retina绘制(四个像素模拟一个像素),
|
||||
// 正好就可以将4个像素利用起来,每个像素点都有不同的颜色,而不像之前只能是四个共用一个颜色。因此图像
|
||||
// 可以更加细腻当然,绘图之后,需要将这个坐标变换给恢复。
|
||||
((Graphics2D) g).scale(1.0 / RETINA_SCALE_FACTOR, 1.0 / RETINA_SCALE_FACTOR); |
||||
g.drawImage(cache, x * RETINA_SCALE_FACTOR, y * RETINA_SCALE_FACTOR, null); |
||||
((Graphics2D) g).scale(RETINA_SCALE_FACTOR, RETINA_SCALE_FACTOR); |
||||
} else { |
||||
g.drawImage(cache, x, y, null); |
||||
} |
||||
} |
||||
|
||||
private boolean isCacheValid() { |
||||
return cache == null |
||||
|| cache.getWidth() != scaleRetina(size.width) |
||||
|| cache.getHeight() != scaleRetina(size.height); |
||||
} |
||||
|
||||
private static int scaleRetina(int size) { |
||||
return getRetina() |
||||
? size * RETINA_SCALE_FACTOR |
||||
: size; |
||||
} |
||||
|
||||
private static Dimension scaleRetina(Dimension dimension) { |
||||
return getRetina() |
||||
? new Dimension(dimension.width * RETINA_SCALE_FACTOR, dimension.height * RETINA_SCALE_FACTOR) |
||||
: dimension; |
||||
} |
||||
|
||||
@Override |
||||
public int getIconWidth() { |
||||
// 根据dpi解除缩放
|
||||
return unscale(size.width); |
||||
} |
||||
|
||||
@Override |
||||
public int getIconHeight() { |
||||
// 根据dpi解除缩放
|
||||
return unscale(size.height); |
||||
} |
||||
|
||||
/** |
||||
* 根据指定尺寸绘制图片,这里尺寸为结算后的尺寸, |
||||
* 因此不必进行缩放 |
||||
* |
||||
* @param size 图像尺寸 |
||||
* @return 图像 |
||||
*/ |
||||
private BufferedImage toImage(Dimension size) { |
||||
SvgTranscoder transcoder = new SvgTranscoder(size); |
||||
TranscoderInput transcoderInput = new TranscoderInput(resource.getInputStream()); |
||||
try { |
||||
transcoder.transcode(transcoderInput, null); |
||||
return transcoder.getImage(); |
||||
} catch (TranscoderException e) { |
||||
throw new RuntimeException(e); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 简单的灰化处理,不能有透明度,因此需要传入背景, |
||||
* 暂时用作灰度,后面再处理 |
||||
*/ |
||||
private BufferedImage toGrayImage(Dimension size, Color background) { |
||||
SvgTranscoder transcoder = new SvgTranscoder(size, background, true); |
||||
TranscoderInput transcoderInput = new TranscoderInput(resource.getInputStream()); |
||||
try { |
||||
transcoder.transcode(transcoderInput, null); |
||||
return transcoder.getImage(); |
||||
} catch (TranscoderException e) { |
||||
throw new RuntimeException(e); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 默认提供一个简单的灰化处理 |
||||
*/ |
||||
@Override |
||||
public @NotNull SvgIcon disabled() { |
||||
return new SvgIcon(resource, size, true); |
||||
} |
||||
} |
@ -0,0 +1,74 @@
|
||||
package com.fine.theme.icon.svg; |
||||
|
||||
import com.fine.theme.icon.AbstractIconSource; |
||||
import com.fine.theme.icon.IconResource; |
||||
import com.fine.theme.icon.UrlIconResource; |
||||
import com.fr.clone.cloning.Immutable; |
||||
import org.jetbrains.annotations.NotNull; |
||||
import org.jetbrains.annotations.Nullable; |
||||
|
||||
import java.awt.Dimension; |
||||
|
||||
/** |
||||
* svg图标源 |
||||
* |
||||
* @author vito |
||||
* @since 11.0 |
||||
* Created on 2023/11/14 |
||||
*/ |
||||
@Immutable |
||||
public class SvgIconSource extends AbstractIconSource<SvgIcon> { |
||||
|
||||
private static final int ICON_SIDE_LENGTH = 16; |
||||
|
||||
private final Dimension size; |
||||
|
||||
public SvgIconSource(@NotNull String id, @NotNull String resource) { |
||||
this(id, new UrlIconResource(resource), null, new Dimension(ICON_SIDE_LENGTH, ICON_SIDE_LENGTH)); |
||||
} |
||||
|
||||
public SvgIconSource(@NotNull String id, @NotNull String resource, boolean autoFindDisable) { |
||||
this(id, new UrlIconResource(resource), autoFindDisable, new Dimension(ICON_SIDE_LENGTH, ICON_SIDE_LENGTH)); |
||||
} |
||||
|
||||
public SvgIconSource(@NotNull String id, @NotNull String resource, int side) { |
||||
this(id, new UrlIconResource(resource), null, side); |
||||
} |
||||
|
||||
public SvgIconSource(@NotNull String id, @NotNull String resource, boolean autoFindDisable, int side) { |
||||
this(id, new UrlIconResource(resource), autoFindDisable, new Dimension(side, side)); |
||||
} |
||||
|
||||
public SvgIconSource(@NotNull String id, @NotNull String resource, @Nullable String grayResource, int side) { |
||||
this(id, new UrlIconResource(resource), new UrlIconResource(grayResource), side); |
||||
} |
||||
|
||||
public SvgIconSource(@NotNull String id, @NotNull IconResource resource, |
||||
@Nullable IconResource grayResource, int side) { |
||||
this(id, resource, grayResource, new Dimension(side, side)); |
||||
} |
||||
|
||||
public SvgIconSource(@NotNull String id, @NotNull IconResource resource, |
||||
boolean autoFindDisable, Dimension size) { |
||||
super(id, resource, autoFindDisable); |
||||
this.size = size; |
||||
} |
||||
|
||||
public SvgIconSource(@NotNull String id, @NotNull IconResource resource, |
||||
@Nullable IconResource grayResource, Dimension size) { |
||||
super(id, resource, grayResource); |
||||
this.size = size; |
||||
} |
||||
|
||||
|
||||
@NotNull |
||||
@Override |
||||
protected SvgIcon loadIcon(@NotNull IconResource resource) { |
||||
return new SvgIcon(resource, size); |
||||
} |
||||
|
||||
@Override |
||||
public @NotNull SvgIcon loadDisableIcon() { |
||||
return new SvgIcon(resource, size).disabled(); |
||||
} |
||||
} |
@ -0,0 +1,65 @@
|
||||
package com.fine.theme.icon.svg; |
||||
|
||||
import org.apache.batik.transcoder.SVGAbstractTranscoder; |
||||
import org.apache.batik.transcoder.TranscoderException; |
||||
import org.apache.batik.transcoder.TranscoderOutput; |
||||
import org.apache.batik.transcoder.image.ImageTranscoder; |
||||
|
||||
import java.awt.Color; |
||||
import java.awt.Dimension; |
||||
import java.awt.image.BufferedImage; |
||||
|
||||
/** |
||||
* Svg图标转码器 |
||||
* |
||||
* @author vito |
||||
* @since 11.0 |
||||
* Created on 2023/11/15 |
||||
*/ |
||||
public class SvgTranscoder extends ImageTranscoder { |
||||
|
||||
private BufferedImage bufferedImage; |
||||
private boolean gray = false; |
||||
|
||||
public SvgTranscoder(Dimension size) { |
||||
addTranscodingHint(SVGAbstractTranscoder.KEY_WIDTH, (float) size.getWidth()); |
||||
addTranscodingHint(SVGAbstractTranscoder.KEY_HEIGHT, (float) size.getHeight()); |
||||
} |
||||
|
||||
public SvgTranscoder(Dimension size, Color background, boolean gray) { |
||||
this.gray = gray; |
||||
addTranscodingHint(SVGAbstractTranscoder.KEY_WIDTH, (float) size.getWidth()); |
||||
addTranscodingHint(SVGAbstractTranscoder.KEY_HEIGHT, (float) size.getHeight()); |
||||
addTranscodingHint(ImageTranscoder.KEY_BACKGROUND_COLOR, background); |
||||
} |
||||
|
||||
|
||||
public SvgTranscoder(float width, float height) { |
||||
addTranscodingHint(SVGAbstractTranscoder.KEY_WIDTH, width); |
||||
addTranscodingHint(SVGAbstractTranscoder.KEY_HEIGHT, height); |
||||
} |
||||
|
||||
@Override |
||||
public BufferedImage createImage(int width, int height) { |
||||
return gray ? |
||||
createGrayImage(width, height) : |
||||
new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); |
||||
} |
||||
|
||||
/** |
||||
* 灰化底图 |
||||
*/ |
||||
private BufferedImage createGrayImage(int width, int height) { |
||||
return new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY); |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public void writeImage(BufferedImage bufferedImage, TranscoderOutput transcoderOutput) throws TranscoderException { |
||||
this.bufferedImage = bufferedImage; |
||||
} |
||||
|
||||
public BufferedImage getImage() { |
||||
return bufferedImage; |
||||
} |
||||
} |
@ -1,11 +0,0 @@
|
||||
package com.fine.theme.light.icon; |
||||
|
||||
/** |
||||
* 图标管理器 |
||||
* |
||||
* @author vito |
||||
* @since 11.0 |
||||
* Created on 2023/9/12 |
||||
*/ |
||||
public class IconManager { |
||||
} |
@ -0,0 +1,34 @@
|
||||
package com.fine.theme.light.ui; |
||||
|
||||
import com.fine.theme.icon.AbstractIconSet; |
||||
import com.fine.theme.icon.svg.SvgIconSource; |
||||
|
||||
/** |
||||
* Fine 亮主题图标集 |
||||
* |
||||
* @author vito |
||||
* @since 11.0 |
||||
* Created on 2023/11/17 |
||||
*/ |
||||
public class FineLightIconSet extends AbstractIconSet { |
||||
|
||||
public FineLightIconSet(String id) { |
||||
super(id); |
||||
load(); |
||||
} |
||||
|
||||
private void load() { |
||||
addIcon( |
||||
new SvgIconSource("cut", "com/fine/theme/icon/cut.svg", true), |
||||
new SvgIconSource("save", "com/fine/theme/icon/save.svg", true), |
||||
new SvgIconSource("copy", "com/fine/theme/icon/copy.svg", true), |
||||
new SvgIconSource("formatBrush", "com/fine/theme/icon/formatBrush.svg", true), |
||||
new SvgIconSource("paste", "com/fine/theme/icon/paste.svg", true), |
||||
new SvgIconSource("undo", "com/fine/theme/icon/undo.svg", true), |
||||
new SvgIconSource("redo", "com/fine/theme/icon/redo.svg", true), |
||||
new SvgIconSource("version_save", "com/fine/theme/icon/version_save.svg", true), |
||||
new SvgIconSource("font_miss_check", "com/fine/theme/icon/font_miss_check.svg", true), |
||||
new SvgIconSource("template_theme", "com/fine/theme/icon/template_theme.svg", true) |
||||
); |
||||
} |
||||
} |
@ -1,40 +0,0 @@
|
||||
package com.fine.theme.light.utils; |
||||
|
||||
import javax.swing.UIManager; |
||||
import java.awt.Color; |
||||
|
||||
/** |
||||
* UI绘制的一些常用方法 |
||||
* |
||||
* @author vito |
||||
* @since 11.0 |
||||
* Created on 2023/11/3 |
||||
*/ |
||||
public class FineUIUtils { |
||||
|
||||
|
||||
/** |
||||
* 通过key获取UI的颜色,如果没有则使用后备key获取 |
||||
* |
||||
* @param key 颜色key |
||||
* @param defaultKey 颜色后备key |
||||
* @return 颜色 |
||||
*/ |
||||
|
||||
public static Color getUIColor(String key, String defaultKey) { |
||||
Color color = UIManager.getColor(key); |
||||
return (color != null) ? color : UIManager.getColor(defaultKey); |
||||
} |
||||
|
||||
/** |
||||
* 获取key指定的int值,如果没有则使用后备key获取 |
||||
* |
||||
* @param key int所在的key |
||||
* @param defaultKey 后备key |
||||
* @return 长度 |
||||
*/ |
||||
public static int getUIInt(String key, String defaultKey) { |
||||
Object value = UIManager.get(key); |
||||
return (value instanceof Integer) ? (Integer) value : UIManager.getInt(defaultKey); |
||||
} |
||||
} |
@ -0,0 +1,86 @@
|
||||
package com.fine.theme.utils; |
||||
|
||||
import com.formdev.flatlaf.util.UIScale; |
||||
|
||||
import javax.swing.plaf.DimensionUIResource; |
||||
import javax.swing.plaf.InsetsUIResource; |
||||
import javax.swing.plaf.UIResource; |
||||
import java.awt.Dimension; |
||||
import java.awt.Graphics2D; |
||||
import java.awt.Insets; |
||||
|
||||
/** |
||||
* UI缩放工具 |
||||
* |
||||
* @author vito |
||||
* @since 11.0 |
||||
* Created on 2023/11/15 |
||||
*/ |
||||
public class FineUIScale { |
||||
/** |
||||
* Multiplies the given value by the user scale factor. |
||||
*/ |
||||
public static float scale(float value) { |
||||
return UIScale.scale(value); |
||||
} |
||||
|
||||
/** |
||||
* Multiplies the given value by the user scale factor and rounds the result. |
||||
*/ |
||||
public static int scale(int value) { |
||||
return UIScale.scale(value); |
||||
} |
||||
|
||||
/** |
||||
* Similar as {@link #scale(int)} but always "rounds down". |
||||
* <p> |
||||
* For use in special cases. {@link #scale(int)} is the preferred method. |
||||
*/ |
||||
public static int scale2(int value) { |
||||
return UIScale.scale2(value); |
||||
} |
||||
|
||||
/** |
||||
* Divides the given value by the user scale factor. |
||||
*/ |
||||
public static float unscale(float value) { |
||||
return UIScale.unscale(value); |
||||
} |
||||
|
||||
/** |
||||
* Divides the given value by the user scale factor and rounds the result. |
||||
*/ |
||||
public static int unscale(int value) { |
||||
return UIScale.unscale(value); |
||||
} |
||||
|
||||
/** |
||||
* If user scale factor is not 1, scale the given graphics context by invoking |
||||
* {@link Graphics2D#scale(double, double)} with user scale factor. |
||||
*/ |
||||
public static void scaleGraphics(Graphics2D g) { |
||||
UIScale.scaleGraphics(g); |
||||
} |
||||
|
||||
/** |
||||
* Scales the given dimension with the user scale factor. |
||||
* <p> |
||||
* If user scale factor is 1, then the given dimension is simply returned. |
||||
* Otherwise, a new instance of {@link Dimension} or {@link DimensionUIResource} |
||||
* is returned, depending on whether the passed dimension implements {@link UIResource}. |
||||
*/ |
||||
public static Dimension scale(Dimension dimension) { |
||||
return UIScale.scale(dimension); |
||||
} |
||||
|
||||
/** |
||||
* Scales the given insets with the user scale factor. |
||||
* <p> |
||||
* If user scale factor is 1, then the given insets is simply returned. |
||||
* Otherwise, a new instance of {@link Insets} or {@link InsetsUIResource} |
||||
* is returned, depending on whether the passed dimension implements {@link UIResource}. |
||||
*/ |
||||
public static Insets scale(Insets insets) { |
||||
return UIScale.scale(insets); |
||||
} |
||||
} |
@ -0,0 +1,90 @@
|
||||
package com.fine.theme.utils; |
||||
|
||||
import com.fr.stable.os.OperatingSystem; |
||||
import com.fr.value.AtomicClearableLazyValue; |
||||
|
||||
import javax.swing.UIManager; |
||||
import java.awt.Color; |
||||
import java.awt.GraphicsDevice; |
||||
import java.awt.GraphicsEnvironment; |
||||
import java.lang.reflect.Field; |
||||
|
||||
/** |
||||
* UI绘制的一些常用方法 |
||||
* |
||||
* @author vito |
||||
* @since 11.0 |
||||
* Created on 2023/11/3 |
||||
*/ |
||||
public class FineUIUtils { |
||||
|
||||
public static final int RETINA_SCALE_FACTOR = 2; |
||||
|
||||
/** |
||||
* 判断是否支持retina,制作一些特殊效果,如HIDPI图片绘制。 |
||||
* retina 是一种特殊的效果,使用4个像素点模拟一个像素点, |
||||
* 因此在其他操作系统上,即使是高分屏也不具备retina的效果。 |
||||
* 甚至还有劣化的效果以及更差的性能。 |
||||
* |
||||
* @since 2023.11.16 |
||||
*/ |
||||
private static final AtomicClearableLazyValue<Boolean> RETINA = AtomicClearableLazyValue.create(() -> { |
||||
// 经过测试win11,ubuntu等,没有retina效果
|
||||
if (!OperatingSystem.isMacos()) { |
||||
return false; |
||||
} |
||||
GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment(); |
||||
GraphicsDevice device = env.getDefaultScreenDevice(); |
||||
|
||||
try { |
||||
Field field = device.getClass().getDeclaredField("scale"); |
||||
field.setAccessible(true); |
||||
Object scale = field.get(device); |
||||
if (scale instanceof Integer && (Integer) scale == RETINA_SCALE_FACTOR) { |
||||
return true; |
||||
} |
||||
} catch (Exception ignored) { |
||||
} |
||||
return false; |
||||
}); |
||||
|
||||
/** |
||||
* 是否支持 retina |
||||
* |
||||
* @return 是否支持 retina |
||||
*/ |
||||
public static boolean getRetina() { |
||||
return RETINA.getValue(); |
||||
} |
||||
|
||||
/** |
||||
* 放弃 retina 判断结果,用于清理或者切换环境 |
||||
*/ |
||||
public static void clearRetina() { |
||||
RETINA.drop(); |
||||
} |
||||
|
||||
/** |
||||
* 通过key获取UI的颜色,如果没有则使用后备key获取 |
||||
* |
||||
* @param key 颜色key |
||||
* @param defaultKey 颜色后备key |
||||
* @return 颜色 |
||||
*/ |
||||
public static Color getUIColor(String key, String defaultKey) { |
||||
Color color = UIManager.getColor(key); |
||||
return (color != null) ? color : UIManager.getColor(defaultKey); |
||||
} |
||||
|
||||
/** |
||||
* 获取key指定的int值,如果没有则使用后备key获取 |
||||
* |
||||
* @param key int所在的key |
||||
* @param defaultKey 后备key |
||||
* @return 长度 |
||||
*/ |
||||
public static int getUIInt(String key, String defaultKey) { |
||||
Object value = UIManager.get(key); |
||||
return (value instanceof Integer) ? (Integer) value : UIManager.getInt(defaultKey); |
||||
} |
||||
} |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.6 KiB |