1   /*
2    * SPDX-License-Identifier: Apache-2.0
3    *
4    * Copyright 2015-2022 Andres Almiray
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.kordamp.ikonli.javafx;
19  
20  import javafx.beans.property.IntegerProperty;
21  import javafx.beans.property.ObjectProperty;
22  import javafx.css.CssMetaData;
23  import javafx.css.StyleOrigin;
24  import javafx.css.Styleable;
25  import javafx.css.StyleableIntegerProperty;
26  import javafx.css.StyleableObjectProperty;
27  import javafx.css.StyleableProperty;
28  import javafx.css.converter.PaintConverter;
29  import javafx.css.converter.SizeConverter;
30  import javafx.scene.paint.Color;
31  import javafx.scene.paint.LinearGradient;
32  import javafx.scene.paint.Paint;
33  import javafx.scene.paint.RadialGradient;
34  import javafx.scene.text.Font;
35  import javafx.scene.text.Text;
36  import org.kordamp.ikonli.Ikon;
37  import org.kordamp.ikonli.IkonHandler;
38  
39  import java.util.ArrayList;
40  import java.util.List;
41  import java.util.Objects;
42  
43  import static java.util.Collections.unmodifiableList;
44  import static java.util.Objects.requireNonNull;
45  
46  /**
47   * @author Andres Almiray
48   */
49  public class FontIcon extends Text implements Icon {
50      private static final double EPSILON = 0.000001d;
51      protected StyleableIntegerProperty iconSize;
52      protected StyleableObjectProperty<Paint> iconColor;
53      private StyleableObjectProperty<Ikon> iconCode;
54  
55      public FontIcon() {
56          getStyleClass().setAll("ikonli-font-icon");
57          setIconSize(8);
58          setIconColor(Color.BLACK);
59  
60          fontProperty().addListener((v, o, n) -> {
61              int size = (int) n.getSize();
62              if (size != getIconSize()) {
63                  setIconSize(size);
64              }
65          });
66  
67          fillProperty().addListener((v, o, n) -> {
68              Paint fill = getIconColor();
69              if (!Objects.equals(fill, n)) {
70                  setIconColor(n);
71              }
72          });
73  
74          iconCodeProperty().addListener((v, o, n) -> {
75              if (n != null) {
76                  IkonHandler ikonHandler = IkonResolver.getInstance().resolve(n.getDescription());
77                  setStyle(normalizeStyle(getStyle(), "-fx-font-family", "'" + ikonHandler.getFontFamily() + "'"));
78                  int code = n.getCode();
79                  if (code <= '\uFFFF') {
80                      setText(String.valueOf((char) code));
81                  } else {
82                      char[] charPair = Character.toChars(code);
83                      String symbol = new String(charPair);
84                      setText(symbol);
85                  }
86              }
87          });
88      }
89  
90      public FontIcon(String iconCode) {
91          this();
92          setIconLiteral(iconCode);
93      }
94  
95      public FontIcon(Ikon iconCode) {
96          this();
97          setIconCode(iconCode);
98      }
99  
100     @Override
101     public String toString() {
102         Ikon iconCode = getIconCode();
103         return (iconCode != null ? iconCode.getDescription() : "<undef>") + ":" + getIconSize() + ":" + getIconColor();
104     }
105 
106     @Override
107     public IntegerProperty iconSizeProperty() {
108         if (iconSize == null) {
109             iconSize = new StyleableIntegerProperty(8) {
110                 @Override
111                 public CssMetaData getCssMetaData() {
112                     return StyleableProperties.ICON_SIZE;
113                 }
114 
115                 @Override
116                 public Object getBean() {
117                     return FontIcon.this;
118                 }
119 
120                 @Override
121                 public String getName() {
122                     return "iconSize";
123                 }
124 
125                 @Override
126                 public StyleOrigin getStyleOrigin() {
127                     return StyleOrigin.USER_AGENT;
128                 }
129             };
130             iconSize.addListener((v, o, n) -> {
131                 Font font = FontIcon.this.getFont();
132                 if (Math.abs(font.getSize() - n.doubleValue()) >= EPSILON) {
133                     FontIcon.this.setFont(Font.font(font.getFamily(), n.doubleValue()));
134                     FontIcon.this.setStyle(normalizeStyle(getStyle(), "-fx-font-size", n.intValue() + "px"));
135                 }
136             });
137         }
138         return iconSize;
139     }
140 
141     @Override
142     public ObjectProperty<Paint> iconColorProperty() {
143         if (iconColor == null) {
144             iconColor = new StyleableObjectProperty<>(Color.BLACK) {
145                 @Override
146                 public CssMetaData getCssMetaData() {
147                     return StyleableProperties.ICON_COLOR;
148                 }
149 
150                 @Override
151                 public Object getBean() {
152                     return FontIcon.this;
153                 }
154 
155                 @Override
156                 public String getName() {
157                     return "iconColor";
158                 }
159 
160                 @Override
161                 public StyleOrigin getStyleOrigin() {
162                     return StyleOrigin.USER_AGENT;
163                 }
164             };
165             iconColor.addListener((v, o, n) -> FontIcon.this.setFill(n));
166         }
167         return iconColor;
168     }
169 
170     public ObjectProperty<Ikon> iconCodeProperty() {
171         if (iconCode == null) {
172             iconCode = new StyleableObjectProperty<>() {
173                 @Override
174                 public CssMetaData getCssMetaData() {
175                     return StyleableProperties.ICON_CODE;
176                 }
177 
178                 @Override
179                 public Object getBean() {
180                     return FontIcon.this;
181                 }
182 
183                 @Override
184                 public String getName() {
185                     return "iconCode";
186                 }
187 
188                 @Override
189                 public StyleOrigin getStyleOrigin() {
190                     return StyleOrigin.USER_AGENT;
191                 }
192             };
193 
194             iconCode.addListener((v, o, n) -> {
195                 if (!iconCode.isBound()) {
196                     FontIcon.this.setIconCode(n);
197                 }
198             });
199         }
200         return iconCode;
201     }
202 
203     @Override
204     public int getIconSize() {
205         return iconSizeProperty().get();
206     }
207 
208     @Override
209     public void setIconSize(int size) {
210         if (size <= 0) {
211             throw new IllegalStateException("Argument 'size' must be greater than zero.");
212         }
213         iconSizeProperty().set(size);
214     }
215 
216     @Override
217     public Paint getIconColor() {
218         return iconColorProperty().get();
219     }
220 
221     @Override
222     public void setIconColor(Paint paint) {
223         iconColorProperty().set(requireNonNull(paint, "Argument 'paint' must not be null"));
224     }
225 
226     public Ikon getIconCode() {
227         return iconCodeProperty().get();
228     }
229 
230     public void setIconCode(Ikon iconCode) {
231         iconCodeProperty().set(requireNonNull(iconCode, "Argument 'code' must not be null"));
232     }
233 
234     private String normalizeStyle(String style, String key, String value) {
235         int start = style.indexOf(key);
236         if (start != -1) {
237             int end = style.indexOf(";", start);
238             end = end >= start ? end : style.length() - 1;
239             style = style.substring(0, start) + style.substring(end + 1);
240         }
241         return style + key + ": " + value + ";";
242     }
243 
244     public String getIconLiteral() {
245         Ikon ikon = iconCodeProperty().get();
246         return ikon != null ? ikon.getDescription() : null;
247     }
248 
249     public void setIconLiteral(String iconCode) {
250         String[] parts = iconCode.split(":");
251         setIconCode(org.kordamp.ikonli.javafx.IkonResolver.getInstance().resolve(parts[0]).resolve(parts[0]));
252         resolveSize(iconCode, parts);
253         resolvePaint(iconCode, parts);
254     }
255 
256     @Override
257     public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
258         return FontIcon.getClassCssMetaData();
259     }
260 
261     private void resolveSize(String iconCode, String[] parts) {
262         if (parts.length > 1) {
263             try {
264                 setIconSize(Integer.parseInt(parts[1]));
265             } catch (NumberFormatException e) {
266                 throw invalidDescription(iconCode, e);
267             }
268         }
269     }
270 
271     private void resolvePaint(String iconCode, String[] parts) {
272         if (parts.length > 2) {
273             Paint paint = resolvePaintValue(iconCode, parts[2]);
274             if (paint != null) {
275                 setIconColor(paint);
276             }
277         }
278     }
279 
280     public static FontIcon of(Ikon ikon) {
281         return of(ikon, 8, Color.BLACK);
282     }
283 
284     public static FontIcon of(Ikon ikon, int iconSize) {
285         return of(ikon, iconSize, Color.BLACK);
286     }
287 
288     public static FontIcon of(Ikon ikon, Color iconColor) {
289         return of(ikon, 8, iconColor);
290     }
291 
292     public static FontIcon of(Ikon iconCode, int iconSize, Color iconColor) {
293         FontIconntIcon.html#FontIcon">FontIcon icon = new FontIcon();
294         icon.setIconCode(iconCode);
295         icon.setIconSize(iconSize);
296         icon.setIconColor(iconColor);
297         return icon;
298     }
299 
300     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
301         return StyleableProperties.STYLEABLES;
302     }
303 
304     private static Paint resolvePaintValue(String iconCode, String value) {
305         try {
306             return Color.valueOf(value);
307         } catch (IllegalArgumentException e1) {
308             try {
309                 return LinearGradient.valueOf(value);
310             } catch (IllegalArgumentException e2) {
311                 try {
312                     return RadialGradient.valueOf(value);
313                 } catch (IllegalArgumentException e3) {
314                     throw invalidDescription(iconCode, e3);
315                 }
316             }
317         }
318     }
319 
320     public static IllegalArgumentException invalidDescription(String description, Exception e) {
321         throw new IllegalArgumentException("Description " + description + " is not a valid icon description", e);
322     }
323 
324     private static class StyleableProperties {
325         private static final CssMetaData<FontIcon, Number> ICON_SIZE =
326             new CssMetaData<FontIcon, Number>("-fx-icon-size",
327                 SizeConverter.getInstance(), 8) {
328 
329                 @Override
330                 public boolean isSettable(FontIcon icon) {
331                     return true;
332                 }
333 
334                 @Override
335                 public StyleableProperty<Number> getStyleableProperty(FontIcon icon) {
336                     return (StyleableProperty<Number>) icon.iconSizeProperty();
337                 }
338             };
339 
340         private static final CssMetaData<FontIcon, Paint> ICON_COLOR =
341             new CssMetaData<FontIcon, Paint>("-fx-icon-color",
342                 PaintConverter.getInstance(), Color.BLACK) {
343 
344                 @Override
345                 public boolean isSettable(FontIcon node) {
346                     return true;
347                 }
348 
349                 @Override
350                 public StyleableProperty<Paint> getStyleableProperty(FontIcon icon) {
351                     return (StyleableProperty<Paint>) icon.iconColorProperty();
352                 }
353             };
354 
355         private static final CssMetaData<FontIcon, Ikon> ICON_CODE =
356             new CssMetaData<FontIcon, Ikon>("-fx-icon-code",
357                 FontIconConverter.getInstance(), null) {
358 
359                 @Override
360                 public boolean isSettable(FontIcon node) {
361                     return true;
362                 }
363 
364                 @Override
365                 public StyleableProperty<Ikon> getStyleableProperty(FontIcon icon) {
366                     return (StyleableProperty<Ikon>) icon.iconCodeProperty();
367                 }
368             };
369 
370         private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
371 
372         static {
373             final List<CssMetaData<? extends Styleable, ?>> styleables =
374                 new ArrayList<>(Text.getClassCssMetaData());
375             styleables.add(ICON_SIZE);
376             styleables.add(ICON_COLOR);
377             styleables.add(ICON_CODE);
378             STYLEABLES = unmodifiableList(styleables);
379         }
380     }
381 }