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.beans.value.ChangeListener;
23  import javafx.collections.ListChangeListener;
24  import javafx.collections.MapChangeListener;
25  import javafx.css.CssMetaData;
26  import javafx.css.StyleOrigin;
27  import javafx.css.Styleable;
28  import javafx.css.StyleableIntegerProperty;
29  import javafx.css.StyleableObjectProperty;
30  import javafx.css.StyleableProperty;
31  import javafx.css.converter.PaintConverter;
32  import javafx.css.converter.SizeConverter;
33  import javafx.scene.Node;
34  import javafx.scene.layout.StackPane;
35  import javafx.scene.paint.Color;
36  import javafx.scene.paint.Paint;
37  import org.kordamp.ikonli.Ikon;
38  
39  import java.util.ArrayList;
40  import java.util.Arrays;
41  import java.util.List;
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 StackedFontIcon extends StackPane implements Icon {
50      private static final String KEY_STACKED_FONT_ICON_SIZE = StackedFontIcon.class.getName() + ".iconSize";
51  
52      private StyleableIntegerProperty iconSize;
53      private StyleableObjectProperty<Paint> iconColor;
54      private double[] iconSizes = new double[0];
55  
56      private ChangeListener<Number> iconSizeChangeListener = (v, o, n) -> setIconSizeOnChildren(n.intValue());
57      private ChangeListener<Paint> iconColorChangeListener = (v, o, n) -> setIconColorOnChildren(n);
58  
59      public static void setIconSize(Node icon, double size) {
60          if (icon != null && size >= 0d && size <= 1.0d) {
61              icon.getProperties().put(KEY_STACKED_FONT_ICON_SIZE, size);
62          }
63      }
64  
65      public static double getIconSize(Node icon) {
66          if (icon != null) {
67              Object value = icon.getProperties().get(KEY_STACKED_FONT_ICON_SIZE);
68              if (value instanceof Number) {
69                  return ((Number) value).doubleValue();
70              }
71          }
72          return 1.0d;
73      }
74  
75      private class NodeSizeListener implements MapChangeListener<Object, Object> {
76          private Node node;
77  
78          private NodeSizeListener(Node node) {
79              this.node = node;
80          }
81  
82          @Override
83          public void onChanged(Change<?, ?> change) {
84              if (KEY_STACKED_FONT_ICON_SIZE.equals(String.valueOf(change.getKey()))) {
85                  int size = getChildren().size();
86                  for (int i = 0; i < size; i++) {
87                      if (node == getChildren().get(i)) {
88                          double value = 0;
89                          Object valueAdded = change.getValueAdded();
90                          if (valueAdded instanceof Number) {
91                              value = ((Number) valueAdded).doubleValue();
92                          } else {
93                              value = Double.parseDouble(String.valueOf(valueAdded));
94                          }
95                          iconSizes[i] = value;
96                          return;
97                      }
98                  }
99              }
100         }
101     }
102 
103     public StackedFontIcon() {
104         getStyleClass().setAll("stacked-ikonli-font-icon");
105 
106         final String propertiesListenerKey = StackedFontIcon.class.getName() + "-" + System.identityHashCode(this);
107 
108         getChildren().addListener(new ListChangeListener<Node>() {
109             @Override
110             public void onChanged(Change<? extends Node> c) {
111                 while (c.next()) {
112                     if (c.wasAdded()) {
113                         int size = c.getTo() - c.getFrom();
114                         // grow iconSizes by size
115                         iconSizes = Arrays.copyOf(iconSizes, iconSizes.length + size);
116                         // apply 1.0 [from..to]
117                         for (int i = c.getFrom(); i < c.getTo(); i++) {
118                             iconSizes[i] = getIconSize(c.getList().get(i));
119                         }
120                         for (Node node : c.getAddedSubList()) {
121                             node.getProperties().put(propertiesListenerKey, new NodeSizeListener(node));
122                         }
123                     } else if (c.wasRemoved()) {
124                         int size = c.getTo() - c.getFrom();
125                         // shrink iconSizes by size
126                         double[] newIconSizes = new double[iconSizes.length - size];
127                         // copy [0..from]
128                         int index = 0;
129                         for (int i = 0; i < c.getFrom(); i++) {
130                             newIconSizes[index++] = iconSizes[i];
131                         }
132                         // copy [to..-1]
133                         for (int i = c.getTo(); i < iconSizes.length; i++) {
134                             newIconSizes[index++] = iconSizes[i];
135                         }
136                         iconSizes = newIconSizes;
137                         for (Node node : c.getRemoved()) {
138                             node.getProperties().remove(propertiesListenerKey);
139                         }
140                     } else if (c.wasPermutated()) {
141                         double[] newIconSizes = Arrays.copyOf(iconSizes, iconSizes.length);
142                         for (int i = c.getFrom(); i <= c.getTo(); i++) {
143                             newIconSizes[i] = c.getPermutation(i);
144                         }
145                         iconSizes = newIconSizes;
146                     }
147                 }
148             }
149         });
150     }
151 
152     public IntegerProperty iconSizeProperty() {
153         if (iconSize == null) {
154             iconSize = new StyleableIntegerProperty(16) {
155                 @Override
156                 public CssMetaData getCssMetaData() {
157                     return StyleableProperties.ICON_SIZE;
158                 }
159 
160                 @Override
161                 public Object getBean() {
162                     return StackedFontIcon.this;
163                 }
164 
165                 @Override
166                 public String getName() {
167                     return "iconSize";
168                 }
169 
170                 @Override
171                 public StyleOrigin getStyleOrigin() {
172                     return StyleOrigin.USER_AGENT;
173                 }
174             };
175             iconSize.addListener(iconSizeChangeListener);
176         }
177         return iconSize;
178     }
179 
180     public ObjectProperty<Paint> iconColorProperty() {
181         if (iconColor == null) {
182             iconColor = new StyleableObjectProperty<Paint>(Color.BLACK) {
183                 @Override
184                 public CssMetaData getCssMetaData() {
185                     return StyleableProperties.ICON_COLOR;
186                 }
187 
188                 @Override
189                 public Object getBean() {
190                     return StackedFontIcon.this;
191                 }
192 
193                 @Override
194                 public String getName() {
195                     return "iconColor";
196                 }
197 
198                 @Override
199                 public StyleOrigin getStyleOrigin() {
200                     return StyleOrigin.USER_AGENT;
201                 }
202             };
203             iconColor.addListener(iconColorChangeListener);
204         }
205         return iconColor;
206     }
207 
208     public void setIconSize(int size) {
209         if (size <= 0) {
210             throw new IllegalStateException("Argument 'size' must be greater than zero.");
211         }
212         iconSizeProperty().set(size);
213     }
214 
215     public int getIconSize() {
216         return iconSizeProperty().get();
217     }
218 
219     public void setIconColor(Paint paint) {
220         requireNonNull(paint, "Argument 'paint' must not be null");
221         iconColorProperty().set(paint);
222     }
223 
224     public Paint getIconColor() {
225         return iconColorProperty().get();
226     }
227 
228     public void setIconCodes(Ikon... iconCodes) {
229         getChildren().clear();
230         initializeSizesIfNeeded(iconCodes);
231         updateIconCodes(iconCodes);
232     }
233 
234     public void setIconCodeLiterals(String... iconCodes) {
235         getChildren().clear();
236         Ikonl#Ikon">Ikon[] codes = new Ikon[iconCodes.length];
237         for (int i = 0; i < iconCodes.length; i++) {
238             codes[i] = IkonResolver.getInstance().resolve(iconCodes[i]).resolve(iconCodes[i]);
239         }
240         initializeSizesIfNeeded(iconCodes);
241         updateIconCodes(codes);
242     }
243 
244     /**
245      * Sets the size for each child icon relative to this icon's size.
246      *
247      * @param iconSizes values must be within the range [0..1]
248      */
249     public void setIconSizes(double... iconSizes) {
250         this.iconSizes = iconSizes;
251         setIconSizeOnChildren(getIconSize());
252     }
253 
254     public void setColors(Paint... iconColors) {
255         int i = 0;
256         for (Node node : getChildren()) {
257             if (node instanceof Icon) {
258                 ((Icon) node).setIconColor(iconColors[i++]);
259             }
260         }
261     }
262 
263     private void initializeSizesIfNeeded(Object[] array) {
264         if (iconSizes.length == 0 || iconSizes.length != array.length) {
265             iconSizes = new double[array.length];
266             Arrays.fill(iconSizes, 1d);
267         }
268     }
269 
270     private void updateIconCodes(Ikon[] iconCodes) {
271         for (int index = 0; index < iconCodes.length; index++) {
272             getChildren().add(createFontIcon(iconCodes[index], index));
273         }
274     }
275 
276     private FontIcon createFontIcon(Ikon iconCode, int index) {
277         FontIconntIcon.html#FontIcon">FontIcon icon = new FontIcon(iconCode);
278         icon.setIconSize(getIconSize());
279         icon.setIconColor(getIconColor());
280         int size = icon.getIconSize();
281         applySizeToIcon(size, icon, index);
282         return icon;
283     }
284 
285     private static class StyleableProperties {
286         private static final CssMetaData<StackedFontIcon, Number> ICON_SIZE =
287             new CssMetaData<StackedFontIcon, Number>("-fx-icon-size",
288                 SizeConverter.getInstance(), 16.0) {
289 
290                 @Override
291                 public boolean isSettable(StackedFontIcon fontIcon) {
292                     return true;
293                 }
294 
295                 @Override
296                 public StyleableProperty<Number> getStyleableProperty(StackedFontIcon icon) {
297                     return (StyleableProperty<Number>) icon.iconSizeProperty();
298                 }
299             };
300 
301         private static final CssMetaData<StackedFontIcon, Paint> ICON_COLOR =
302             new CssMetaData<StackedFontIcon, Paint>("-fx-icon-color",
303                 PaintConverter.getInstance(), Color.BLACK) {
304 
305                 @Override
306                 public boolean isSettable(StackedFontIcon node) {
307                     return true;
308                 }
309 
310                 @Override
311                 public StyleableProperty<Paint> getStyleableProperty(StackedFontIcon icon) {
312                     return (StyleableProperty<Paint>) icon.iconColorProperty();
313                 }
314             };
315 
316         private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
317 
318         static {
319             final List<CssMetaData<? extends Styleable, ?>> styleables =
320                 new ArrayList<CssMetaData<? extends Styleable, ?>>(StackPane.getClassCssMetaData());
321             styleables.add(ICON_SIZE);
322             styleables.add(ICON_COLOR);
323             STYLEABLES = unmodifiableList(styleables);
324         }
325     }
326 
327     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
328         return StyleableProperties.STYLEABLES;
329     }
330 
331     public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
332         return StackedFontIcon.getClassCssMetaData();
333     }
334 
335     private void setIconSizeOnChildren(int size) {
336         int i = 0;
337         for (Node node : getChildren()) {
338             if (node instanceof Icon) {
339                 applySizeToIcon(size, (Icon) node, i++);
340             }
341         }
342     }
343 
344     private void applySizeToIcon(int size, Icon icon, int index) {
345         double childPercentageSize = iconSizes[index];
346         double newSize = size * childPercentageSize;
347         icon.setIconSize((int) newSize);
348     }
349 
350     private void setIconColorOnChildren(Paint color) {
351         for (Node node : getChildren()) {
352             if (node instanceof Icon) {
353                 ((Icon) node).setIconColor(color);
354             }
355         }
356     }
357 }