Browse Source

Pull request #13486: REPORT-99485 提供默认反白图标以及单层缓存

Merge in DESIGN/design from ~VITO/c-design:newui to newui

* commit '13e48ca74331beee06ba048e066faed599225cbc':
  无jira任务 代码质量
  无jira任务 代码质量
  REPORT-99485 提供默认反白图标以及单层缓存
newui
vito-刘恒霖 9 months ago
parent
commit
583f5d388e
  1. 11
      designer-base/src/main/java/com/fine/theme/icon/AbstractIconSet.java
  2. 59
      designer-base/src/main/java/com/fine/theme/icon/GraphicsFilter.java
  3. 31
      designer-base/src/main/java/com/fine/theme/icon/IconManager.java
  4. 8
      designer-base/src/main/java/com/fine/theme/icon/IconSet.java
  5. 2
      designer-base/src/main/java/com/fine/theme/icon/IconSource.java
  6. 14
      designer-base/src/main/java/com/fine/theme/icon/LazyIcon.java
  7. 22
      designer-base/src/main/java/com/fine/theme/icon/WhiteIcon.java
  8. 159
      designer-base/src/main/java/com/fine/theme/icon/svg/SvgIcon.java
  9. 6
      designer-base/src/main/java/com/fine/theme/icon/svg/SvgIconSource.java
  10. 50
      designer-base/src/main/java/com/fine/theme/icon/svg/WhiteParser.java
  11. 2
      designer-base/src/main/resources/com/fine/theme/light/ui/laf/FineLightLaf.properties
  12. 13
      designer-base/src/test/java/com/fr/design/gui/storybook/components/ButtonStoryBoard.java
  13. 60
      designer-base/src/test/java/com/fr/design/gui/storybook/components/IconStoryBoard.java
  14. 7
      designer-realize/src/main/java/com/fr/start/MainDesigner.java

11
designer-base/src/main/java/com/fine/theme/icon/AbstractIconSet.java

@ -46,7 +46,7 @@ public abstract class AbstractIconSet implements IconSet {
@Override
public void addIcon(@NotNull IconSource<? extends Icon>... icons) {
for (IconSource<? extends Icon> icon: icons){
for (IconSource<? extends Icon> icon : icons) {
map.put(icon.getId(), icon);
}
}
@ -64,6 +64,15 @@ public abstract class AbstractIconSet implements IconSet {
return icon;
}
@Override
public @Nullable Icon findWhiteIcon(@NotNull String id) {
IconSource<? extends Icon> iconSource = map.get(id);
if (iconSource != null) {
return iconSource.white();
}
return null;
}
@Override
public @Nullable Icon findDisableIcon(@NotNull String id) {
Icon icon = disableCache.get(id);

59
designer-base/src/main/java/com/fine/theme/icon/GraphicsFilter.java

@ -0,0 +1,59 @@
package com.fine.theme.icon;
import com.formdev.flatlaf.util.Graphics2DProxy;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.image.RGBImageFilter;
/**
* 颜色过滤器画板
*
* @author vito
* @since 11.0
* Created on 2023/11/21
*/
public class GraphicsFilter
extends Graphics2DProxy {
private final RGBImageFilter grayFilter;
public GraphicsFilter(Graphics2D delegate, RGBImageFilter grayFilter) {
super(delegate);
this.grayFilter = grayFilter;
}
@Override
public Graphics create() {
return new GraphicsFilter((Graphics2D) super.create(), grayFilter);
}
@Override
public Graphics create(int x, int y, int width, int height) {
return new GraphicsFilter((Graphics2D) super.create(x, y, width, height), grayFilter);
}
@Override
public void setColor(Color c) {
super.setColor(filterColor(c));
}
@Override
public void setPaint(Paint paint) {
if (paint instanceof Color) {
paint = filterColor((Color) paint);
}
super.setPaint(paint);
}
private Color filterColor(Color color) {
if (grayFilter != null) {
int oldRGB = color.getRGB();
int newRGB = grayFilter.filterRGB(0, 0, oldRGB);
color = (newRGB != oldRGB) ? new Color(newRGB, true) : color;
}
return color;
}
}

31
designer-base/src/main/java/com/fine/theme/icon/IconManager.java

@ -25,8 +25,9 @@ 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);
public static HashMap<String, WeakReference<Icon>> cache = new HashMap<>(64);
public static HashMap<String, WeakReference<Icon>> disableCache = new HashMap<>(64);
public static HashMap<String, WeakReference<Icon>> whiteCache = new HashMap<>(32);
/**
@ -75,6 +76,32 @@ public class IconManager {
return (I) icon;
}
/**
* 获取灰化图标
*
* @param id 图标ID
* @param <I> 图标类型
* @return 图标
*/
@NotNull
public static <I extends Icon> I getWhiteIcon(String id) {
final WeakReference<Icon> reference = whiteCache.get(id);
I icon = reference != null ? (I) reference.get() : null;
if (icon == null) {
for (IconSet set : iconSets) {
Icon f = set.findWhiteIcon(id);
if (f != null) {
icon = (I) f;
whiteCache.put(id, new WeakReference<>(icon));
}
}
}
if (icon == null) {
throw new IconException("[IconManager] Can not find icon by id: " + id);
}
return icon;
}
/**
* 获取灰化图标
*

8
designer-base/src/main/java/com/fine/theme/icon/IconSet.java

@ -51,6 +51,14 @@ public interface IconSet extends Identifiable {
@Nullable
Icon findIcon(@NotNull String id);
/**
* 返回指定 id Icon
*
* @param id id
* @return Icon
*/
@Nullable
Icon findWhiteIcon(@NotNull String id);
/**
* 返回指定 id Icon

2
designer-base/src/main/java/com/fine/theme/icon/IconSource.java

@ -15,7 +15,7 @@ import java.io.Serializable;
* Created on 2023/11/6
*/
@Immutable
public interface IconSource<I extends Icon> extends Identifiable, DisabledIcon, Cloneable, Serializable {
public interface IconSource<I extends Icon> extends Identifiable, DisabledIcon, WhiteIcon, Cloneable, Serializable {
/**
* 获取图标资源

14
designer-base/src/main/java/com/fine/theme/icon/LazyIcon.java

@ -15,7 +15,7 @@ import java.awt.Graphics;
* Created on 2023/11/6
*/
@Immutable
public class LazyIcon implements Identifiable, DisabledIcon, Icon {
public class LazyIcon implements Identifiable, DisabledIcon, WhiteIcon, Icon {
@NotNull
private final String id;
@ -62,6 +62,18 @@ public class LazyIcon implements Identifiable, DisabledIcon, Icon {
return IconManager.getDisableIcon(getId());
}
/**
* 创建一份白化图标
*
* @return 白化图标
*/
@NotNull
@Override
public Icon white() {
return IconManager.getWhiteIcon(getId());
}
@NotNull
@Override
public String toString() {

22
designer-base/src/main/java/com/fine/theme/icon/WhiteIcon.java

@ -0,0 +1,22 @@
package com.fine.theme.icon;
import org.jetbrains.annotations.NotNull;
import javax.swing.Icon;
/**
* 白化图像
*
* @author vito
* @since 11.0
* Created on 2024/1/8
*/
public interface WhiteIcon {
/**
* 创建一份白化图标
*
* @return 灰化图标
*/
@NotNull
Icon white();
}

159
designer-base/src/main/java/com/fine/theme/icon/svg/SvgIcon.java

@ -1,29 +1,35 @@
package com.fine.theme.icon.svg;
import com.fine.theme.icon.DisabledIcon;
import com.fine.theme.icon.GraphicsFilter;
import com.fine.theme.icon.IconResource;
import com.fine.theme.icon.WhiteIcon;
import com.formdev.flatlaf.FlatLaf;
import com.formdev.flatlaf.ui.FlatUIUtils;
import com.formdev.flatlaf.util.GrayFilter;
import com.fr.clone.cloning.Immutable;
import org.apache.batik.transcoder.TranscoderException;
import org.apache.batik.transcoder.TranscoderInput;
import com.fr.value.NullableLazyValue;
import com.github.weisj.jsvg.SVGDocument;
import com.github.weisj.jsvg.attributes.ViewBox;
import com.github.weisj.jsvg.parser.SVGLoader;
import org.jetbrains.annotations.NotNull;
import javax.swing.Icon;
import java.awt.Color;
import javax.swing.JComponent;
import javax.swing.UIManager;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.awt.image.RGBImageFilter;
import java.util.Objects;
import java.util.StringJoiner;
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绘制不影响最终尺寸注意区分
@ -33,27 +39,43 @@ import static com.fine.theme.utils.FineUIUtils.getRetina;
* Created on 2023/11/15
*/
@Immutable
public class SvgIcon implements DisabledIcon, Icon {
protected transient BufferedImage cache;
public class SvgIcon implements DisabledIcon, WhiteIcon, Icon {
public enum Type {
/**
* 灰化图
*/
disable,
/**
* 白化图用于反白场景
*/
white,
/**
* 原始效果图
*/
origin
}
private final Dimension size;
private final IconResource resource;
private final boolean disable;
private final Type type;
private final NullableLazyValue<SVGDocument> svgDocument = NullableLazyValue.createValue(() -> load(Type.origin));
private final NullableLazyValue<SVGDocument> whiteSvgDocument = NullableLazyValue.createValue(() -> load(Type.white));
public SvgIcon(IconResource resource, Dimension size) {
this(resource, size, false);
this(resource, size, Type.origin);
}
public SvgIcon(IconResource resource, Dimension size, boolean disable) {
public SvgIcon(IconResource resource, Dimension size, Type type) {
this.resource = resource;
// 根据dpi进行缩放
this.size = scale(size);
this.disable = disable;
this.type = type;
}
public SvgIcon(IconResource resource, int side) {
this(resource, new Dimension(side, side), false);
this(resource, new Dimension(side, side), Type.origin);
}
/**
@ -62,46 +84,21 @@ public class SvgIcon implements DisabledIcon, Icon {
*/
@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);
if (type == Type.disable) {
g = grayGraphics(g);
}
Object[] oldRenderingHints = FlatUIUtils.setRenderingHints(g);
render(c, g, x, y);
FlatUIUtils.resetRenderingHints(g, oldRenderingHints);
}
private boolean isCacheValid() {
return cache == null
|| cache.getWidth() != scaleRetina(size.width)
|| cache.getHeight() != scaleRetina(size.height);
}
private Graphics2D grayGraphics(Graphics g) {
Object grayFilterObj = UIManager.get("Component.grayFilter");
RGBImageFilter grayFilter = (grayFilterObj instanceof RGBImageFilter)
? (RGBImageFilter) grayFilterObj
: GrayFilter.createDisabledIconFilter(FlatLaf.isLafDark());
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;
return new GraphicsFilter((Graphics2D) g.create(), grayFilter);
}
@Override
@ -114,44 +111,48 @@ public class SvgIcon implements DisabledIcon, Icon {
return 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 void render(Component c, Graphics g, int x, int y) {
if (type == Type.white) {
Objects.requireNonNull(whiteSvgDocument.getValue())
.render((JComponent) c, (Graphics2D) g, new ViewBox(x, y, size.width, size.height));
} else {
Objects.requireNonNull(svgDocument.getValue())
.render((JComponent) c, (Graphics2D) g, new ViewBox(x, y, size.width, size.height));
}
}
private SVGDocument load(Type type) {
SVGLoader loader = new SVGLoader();
return type == Type.white
? loader.load(resource.getInputStream(), new WhiteParser())
: loader.load(resource.getInputStream());
}
@Override
public String toString() {
return new StringJoiner(", ", SvgIcon.class.getSimpleName() + "[", "]")
.add("resource=" + resource)
.add("type=" + type)
.add("size=" + size)
.toString();
}
/**
* 简单的灰化处理不能有透明度因此需要传入背景
* 暂时用作灰度后面再处理
* 默认提供一个简单的灰化处理
*/
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 white() {
return new SvgIcon(resource, size, Type.white);
}
/**
* 默认提供一个简单的灰化处理
*/
@Override
public @NotNull SvgIcon disabled() {
return new SvgIcon(resource, size, true);
return new SvgIcon(resource, size, Type.disable);
}
}

6
designer-base/src/main/java/com/fine/theme/icon/svg/SvgIconSource.java

@ -7,6 +7,7 @@ import com.fr.clone.cloning.Immutable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.Icon;
import java.awt.Dimension;
/**
@ -71,4 +72,9 @@ public class SvgIconSource extends AbstractIconSource<SvgIcon> {
public @NotNull SvgIcon loadDisableIcon() {
return new SvgIcon(resource, size).disabled();
}
@Override
public @NotNull Icon white() {
return new SvgIcon(resource, size).white();
}
}

50
designer-base/src/main/java/com/fine/theme/icon/svg/WhiteParser.java

@ -0,0 +1,50 @@
package com.fine.theme.icon.svg;
import com.github.weisj.jsvg.attributes.paint.AwtSVGPaint;
import com.github.weisj.jsvg.attributes.paint.PaintParser;
import com.github.weisj.jsvg.attributes.paint.SVGPaint;
import com.github.weisj.jsvg.parser.AttributeNode;
import com.github.weisj.jsvg.parser.DefaultParserProvider;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.Color;
/**
* svg绘制白化转化器
*
* @author vito
* @since 11.0
* Created on 2024/1/8
*/
public class WhiteParser extends DefaultParserProvider {
@Override
public @NotNull PaintParser createPaintParser() {
return new WhitePaintParser(super.createPaintParser());
}
static class WhitePaintParser implements PaintParser {
private final PaintParser delegate;
WhitePaintParser(PaintParser delegate) {
this.delegate = delegate;
}
@Override
public @Nullable Color parseColor(@NotNull String value, @NotNull AttributeNode attributeNode) {
return delegate.parseColor(value, attributeNode);
}
@Override
public @Nullable SVGPaint parsePaint(@Nullable String value, @NotNull AttributeNode attributeNode) {
SVGPaint paint = delegate.parsePaint(value, attributeNode);
if (!(paint instanceof AwtSVGPaint)) {
return paint;
}
return new AwtSVGPaint(Color.WHITE);
}
}
}

2
designer-base/src/main/resources/com/fine/theme/light/ui/laf/FineLightLaf.properties

@ -224,7 +224,7 @@ CombinationButton.arc = $Button.arc
#---- CheckBox ----
CheckBox.border = com.formdev.flatlaf.ui.FlatMarginBorder
CheckBox.icon = com.fine.theme.icon.AnimatedSwitchIcon
CheckBox.icon = com.formdev.flatlaf.icons.FlatCheckBoxIcon
CheckBox.arc = 4
CheckBox.margin = 2,2,2,2
CheckBox.iconTextGap = 4

13
designer-base/src/test/java/com/fr/design/gui/storybook/components/ButtonStoryBoard.java

@ -43,7 +43,7 @@ public class ButtonStoryBoard extends StoryBoard {
cell(new UILabel("正常")),
cell(new UIButton("按钮"))
.with(it -> setStyle(it, STYLE_PRIMARY)),
cell(new UIButton("按钮", new LazyIcon("add")))
cell(new UIButton("按钮", new LazyIcon("add").white()))
.with(it -> setStyle(it, STYLE_PRIMARY)),
cell(new UIButton(new LazyIcon("multi")))
.with(it -> setStyle(it, STYLE_PRIMARY))
@ -54,7 +54,7 @@ public class ButtonStoryBoard extends StoryBoard {
setStyle(it, STYLE_PRIMARY);
it.setEnabled(false);
}),
cell(new UIButton("保存", new LazyIcon("save"))).with(it -> {
cell(new UIButton("保存", new LazyIcon("save").white())).with(it -> {
setStyle(it, STYLE_PRIMARY);
it.setEnabled(false);
}),
@ -68,7 +68,7 @@ public class ButtonStoryBoard extends StoryBoard {
cell(new UILabel("正常")),
cell(new UIButton("按钮"))
.with(it -> setStyle(it, joinStyle(STYLE_PRIMARY, STYLE_SIZE_SMALL))),
cell(new UIButton("按钮", new LazyIcon("add")))
cell(new UIButton("按钮", new LazyIcon("add").white()))
.with(it -> setStyle(it, joinStyle(STYLE_PRIMARY, STYLE_SIZE_SMALL))),
cell(new UIButton(new LazyIcon("multi")))
.with(it -> setStyle(it, joinStyle(STYLE_PRIMARY, STYLE_SIZE_SMALL)))
@ -145,12 +145,7 @@ public class ButtonStoryBoard extends StoryBoard {
cell(new JButton(new LazyIcon("add"))).with(it -> it.setEnabled(false))
),
row(20,
cell(new UICombinationButton("按钮", new LazyIcon("triangle_down")))
.with(it -> {
setStyle(it, STYLE_PRIMARY);
// it.setExtraPainted(false);
}),
cell(new UICombinationButton("按钮", new LazyIcon("triangle_down")))
cell(new UICombinationButton("按钮", new LazyIcon("triangle_down").white()))
.with(it -> setStyle(it, STYLE_PRIMARY)),
cell(new UICombinationButton("按钮2", new LazyIcon("triangle_down"))),
cell(new JButton("按钮", new LazyIcon("add"))),

60
designer-base/src/test/java/com/fr/design/gui/storybook/components/IconStoryBoard.java

@ -0,0 +1,60 @@
package com.fr.design.gui.storybook.components;
import com.fine.theme.icon.LazyIcon;
import com.fr.design.gui.ilable.UILabel;
import com.fr.design.gui.storybook.Story;
import com.fr.design.gui.storybook.StoryBoard;
import javax.swing.JLabel;
import static com.fine.swing.ui.layout.Layouts.cell;
import static com.fine.swing.ui.layout.Layouts.fix;
import static com.fine.swing.ui.layout.Layouts.flex;
import static com.fine.swing.ui.layout.Layouts.row;
/**
* @author vito
* @since 11.0
* Created on 2024/1/8
*/
@Story
public class IconStoryBoard extends StoryBoard {
public IconStoryBoard() {
super("图标");
add(
cell(new UILabel("普通图标")).with(this::h3),
row(10,
cell(new JLabel(new LazyIcon("cut"))),
cell(new JLabel(new LazyIcon("save"))),
cell(new JLabel(new LazyIcon("copy"))),
cell(new JLabel(new LazyIcon("formatBrush"))),
cell(new JLabel(new LazyIcon("paste"))),
cell(new JLabel(new LazyIcon("undo"))),
cell(new JLabel(new LazyIcon("redo")))
),
fix(5),
cell(new UILabel("禁用图标")).with(this::h3),
row(10,
cell(new JLabel(new LazyIcon("cut").disabled())),
cell(new JLabel(new LazyIcon("save").disabled())),
cell(new JLabel(new LazyIcon("copy").disabled())),
cell(new JLabel(new LazyIcon("formatBrush").disabled())),
cell(new JLabel(new LazyIcon("paste").disabled())),
cell(new JLabel(new LazyIcon("undo").disabled())),
cell(new JLabel(new LazyIcon("redo").disabled()))
),
fix(5),
cell(new UILabel("白化图标")).with(this::h3),
row(10,
cell(new JLabel(new LazyIcon("cut").white())),
cell(new JLabel(new LazyIcon("save").white())),
cell(new JLabel(new LazyIcon("copy").white())),
cell(new JLabel(new LazyIcon("formatBrush").white())),
cell(new JLabel(new LazyIcon("paste").white())),
cell(new JLabel(new LazyIcon("undo").white())),
cell(new JLabel(new LazyIcon("redo").white()))
),
flex()
);
}
}

7
designer-realize/src/main/java/com/fr/start/MainDesigner.java

@ -25,6 +25,7 @@ import com.fr.design.gui.ibutton.UICombinationButton;
import com.fr.design.gui.ibutton.UISaveForbiddenButton;
import com.fr.design.gui.imenu.UIMenuItem;
import com.fr.design.gui.imenu.UIPopupMenu;
import com.fr.design.i18n.Toolkit;
import com.fr.design.mainframe.ActiveKeyGenerator;
import com.fr.design.mainframe.DesignerContext;
import com.fr.design.mainframe.InformationCollector;
@ -336,8 +337,8 @@ public class MainDesigner extends BaseDesigner {
}
private UICombinationButton createRunButton() {
run = new UICombinationButton(new UISaveForbiddenButton(com.fr.design.i18n.Toolkit.i18nText("Fine-Design_Basic_Preview"), new LazyIcon("add")),
new UISaveForbiddenButton(new LazyIcon("triangle_down")));
run = new UICombinationButton(new UISaveForbiddenButton(Toolkit.i18nText("Fine-Design_Basic_Preview"), new LazyIcon("add").white()),
new UISaveForbiddenButton(new LazyIcon("triangle_down").white()));
run.addLeftClickLister(mouseEvent -> {
JTemplate<?, ?> jt = HistoryTemplateListCache.getInstance().getCurrentEditingTemplate();
if (jt == null || jt.isSaving()) {
@ -382,7 +383,7 @@ public class MainDesigner extends BaseDesigner {
redo.setEnabled(false);
}
// run.getLeftButton().setIcon(jt.getPreviewLargeIcon());
run.getLeftButton().setText(jt.getPreviewType().nameForPopupItem());
}

Loading…
Cancel
Save