JavaFXUtils.java
0001 /*
0002  * Copyright 2008-2017 the original author or authors.
0003  *
0004  * Licensed under the Apache License, Version 2.0 (the "License");
0005  * you may not use this file except in compliance with the License.
0006  * You may obtain a copy of the License at
0007  *
0008  *     http://www.apache.org/licenses/LICENSE-2.0
0009  *
0010  * Unless required by applicable law or agreed to in writing, software
0011  * distributed under the License is distributed on an "AS IS" BASIS,
0012  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
0013  * See the License for the specific language governing permissions and
0014  * limitations under the License.
0015  */
0016 package griffon.javafx.support;
0017 
0018 import griffon.core.GriffonApplication;
0019 import griffon.core.artifact.GriffonController;
0020 import griffon.core.controller.Action;
0021 import griffon.core.controller.ActionManager;
0022 import griffon.core.editors.ValueConversionException;
0023 import griffon.core.i18n.MessageSource;
0024 import griffon.exceptions.InstanceMethodInvocationException;
0025 import griffon.javafx.collections.GriffonFXCollections;
0026 import javafx.application.Platform;
0027 import javafx.beans.property.Property;
0028 import javafx.collections.ObservableList;
0029 import javafx.collections.ObservableMap;
0030 import javafx.collections.ObservableSet;
0031 import javafx.event.ActionEvent;
0032 import javafx.event.EventHandler;
0033 import javafx.scene.Node;
0034 import javafx.scene.Parent;
0035 import javafx.scene.Scene;
0036 import javafx.scene.chart.Axis;
0037 import javafx.scene.control.Accordion;
0038 import javafx.scene.control.ButtonBar;
0039 import javafx.scene.control.ButtonBase;
0040 import javafx.scene.control.CheckBox;
0041 import javafx.scene.control.CheckMenuItem;
0042 import javafx.scene.control.ContextMenu;
0043 import javafx.scene.control.Control;
0044 import javafx.scene.control.Labeled;
0045 import javafx.scene.control.Menu;
0046 import javafx.scene.control.MenuBar;
0047 import javafx.scene.control.MenuItem;
0048 import javafx.scene.control.RadioButton;
0049 import javafx.scene.control.RadioMenuItem;
0050 import javafx.scene.control.ScrollPane;
0051 import javafx.scene.control.SplitPane;
0052 import javafx.scene.control.Tab;
0053 import javafx.scene.control.TabPane;
0054 import javafx.scene.control.TableColumn;
0055 import javafx.scene.control.TableView;
0056 import javafx.scene.control.TitledPane;
0057 import javafx.scene.control.ToggleButton;
0058 import javafx.scene.control.ToolBar;
0059 import javafx.scene.control.Tooltip;
0060 import javafx.scene.image.Image;
0061 import javafx.scene.image.ImageView;
0062 import javafx.stage.Window;
0063 
0064 import javax.annotation.Nonnull;
0065 import javax.annotation.Nullable;
0066 import java.lang.reflect.Constructor;
0067 import java.lang.reflect.InvocationTargetException;
0068 import java.net.URL;
0069 import java.util.Collection;
0070 import java.util.LinkedHashSet;
0071 import java.util.Map;
0072 import java.util.Set;
0073 import java.util.function.Predicate;
0074 
0075 import static griffon.core.GriffonApplication.PROPERTY_LOCALE;
0076 import static griffon.util.GriffonClassUtils.EMPTY_OBJECT_ARRAY;
0077 import static griffon.util.GriffonClassUtils.getGetterName;
0078 import static griffon.util.GriffonClassUtils.getPropertyValue;
0079 import static griffon.util.GriffonClassUtils.invokeExactInstanceMethod;
0080 import static griffon.util.GriffonClassUtils.invokeInstanceMethod;
0081 import static griffon.util.GriffonNameUtils.isBlank;
0082 import static griffon.util.GriffonNameUtils.requireNonBlank;
0083 import static java.util.Objects.requireNonNull;
0084 
0085 /**
0086  @author Andres Almiray
0087  */
0088 public final class JavaFXUtils {
0089     private static final String ERROR_NODE_NULL = "Argument 'node' must not be null";
0090     private static final String ERROR_CONTROL_NULL = "Argument 'control' must not be null";
0091     private static final String ERROR_ACTION_NULL = "Argument 'action' must not be null";
0092     private static final String ERROR_ICON_BLANK = "Argument 'iconUrl' must not be blank";
0093     private static final String ERROR_ID_BLANK = "Argument 'id' must not be blank";
0094     private static final String ERROR_URL_BLANK = "Argument 'url' must not be blank";
0095     private static final String ERROR_KEY_BLANK = "Argument 'key' must not be blank";
0096     private static final String ERROR_ARGS_BLANK = "Argument 'args' must not be blank";
0097     private static final String ERROR_ROOT_NULL = "Argument 'root' must not be null";
0098     private static final String ERROR_PREDICATE_NULL = "Argument 'predicate' must not be null";
0099     private static final String ERROR_CONTROLLER_NULL = "Argument 'controller' must not be null";
0100     private static final String ERROR_APPLICATION_NULL = "Argument 'application' must not be null";
0101     private static final String PROPERTY_SUFFIX = "Property";
0102     private static final String SUFFIX_KEY = "-KEY";
0103     private static final String SUFFIX_ARGS = "-ARGS";
0104     private static final String SUFFIX_DEFAULT_VALUE = "-DEFAULT_VALUE";
0105     private static final ActionMatcher DEFAULT_ACTION_MATCHER = ActionMatcher.DEFAULT;
0106 
0107     private JavaFXUtils() {
0108 
0109     }
0110 
0111     /**
0112      * Associates an i18n key to a {@code node}. The key is used to resolve a message via the application's {@code MessageSource}.
0113      *
0114      @param node the target node on which the key will be registered.
0115      @param key  the message key to be registered.
0116      *
0117      @since 2.9.0
0118      */
0119     public static void setI18nKey(@Nonnull Labeled node, @Nonnull String key) {
0120         requireNonNull(node, ERROR_NODE_NULL);
0121         requireNonBlank(key, ERROR_KEY_BLANK);
0122         node.getProperties().put(MessageSource.class.getName() + SUFFIX_KEY, key);
0123     }
0124 
0125     /**
0126      * Finds out if an i18n {@code key} has been registered with the target {@code Node}, returning the key if found.
0127      *
0128      @param node the target node on which the key may have been registered.
0129      *
0130      @return the key registered with the target {@code Node} or {@code null} if not found.
0131      *
0132      @since 2.9.0
0133      */
0134     @Nullable
0135     public static String getI18nKey(@Nonnull Labeled node) {
0136         requireNonNull(node, ERROR_NODE_NULL);
0137         return (Stringnode.getProperties().get(MessageSource.class.getName() + SUFFIX_KEY);
0138     }
0139 
0140     /**
0141      * Associates an i18n arrays of arguments to a {@code node}.
0142      * These arguments will be used alongside a key to resolve a message via the application's {@code MessageSource}.
0143      *
0144      @param node the target node on which the key will be registered.
0145      @param args the array of arguments to be registered.
0146      *
0147      @since 2.9.0
0148      */
0149     public static void setI18nArgs(@Nonnull Labeled node, @Nullable String args) {
0150         requireNonNull(node, ERROR_NODE_NULL);
0151         requireNonBlank(args, ERROR_ARGS_BLANK);
0152         node.getProperties().put(MessageSource.class.getName() + SUFFIX_ARGS, args);
0153     }
0154 
0155     /**
0156      * Finds out if an {@code arguments array} has been registered with the target {@code Node}, returning the array if found.
0157      *
0158      @param node the target node on which the arguments may have been registered.
0159      *
0160      @return the arguments registered with the target {@code Node} or {@code null} if not found.
0161      *
0162      @since 2.9.0
0163      */
0164     @Nullable
0165     public static String getI18nArgs(@Nonnull Labeled node) {
0166         requireNonNull(node, ERROR_NODE_NULL);
0167         return (Stringnode.getProperties().get(MessageSource.class.getName() + SUFFIX_ARGS);
0168     }
0169 
0170     /**
0171      * Associates an default value {@code node}.
0172      * The value will be used alongside a key to resolve a message via the application's {@code MessageSource}.
0173      *
0174      @param node         the target node on which the key will be registered.
0175      @param defaultValue the value to be registered.
0176      *
0177      @since 2.9.0
0178      */
0179     public static void setI18nDefaultValue(@Nonnull Labeled node, @Nullable String defaultValue) {
0180         requireNonNull(node, ERROR_NODE_NULL);
0181         node.getProperties().put(MessageSource.class.getName() + SUFFIX_DEFAULT_VALUE, defaultValue);
0182     }
0183 
0184     /**
0185      * Finds out if a {@code default value} has been registered with the target {@code Node}, returning the value if found.
0186      *
0187      @param node the target node on which the value may have been registered.
0188      *
0189      @return the value registered with the target {@code Node} or {@code null} if not found.
0190      *
0191      @since 2.9.0
0192      */
0193     @Nullable
0194     public static String getI18nDefaultValue(@Nonnull Labeled node) {
0195         requireNonNull(node, ERROR_NODE_NULL);
0196         return (Stringnode.getProperties().get(MessageSource.class.getName() + SUFFIX_DEFAULT_VALUE);
0197     }
0198 
0199     /**
0200      * Associates an i18n key to a {@code node}. The key is used to resolve a message via the application's {@code MessageSource}.
0201      *
0202      @param node the target node on which the key will be registered.
0203      @param key  the message key to be registered.
0204      *
0205      @since 2.9.0
0206      */
0207     public static void setI18nKey(@Nonnull Tab node, @Nonnull String key) {
0208         requireNonNull(node, ERROR_NODE_NULL);
0209         requireNonBlank(key, ERROR_KEY_BLANK);
0210         node.getProperties().put(MessageSource.class.getName() + SUFFIX_KEY, key);
0211     }
0212 
0213     /**
0214      * Finds out if an i18n {@code key} has been registered with the target {@code Node}, returning the key if found.
0215      *
0216      @param node the target node on which the key may have been registered.
0217      *
0218      @return the key registered with the target {@code Node} or {@code null} if not found.
0219      *
0220      @since 2.9.0
0221      */
0222     @Nullable
0223     public static String getI18nKey(@Nonnull Tab node) {
0224         requireNonNull(node, ERROR_NODE_NULL);
0225         return (Stringnode.getProperties().get(MessageSource.class.getName() + SUFFIX_KEY);
0226     }
0227 
0228     /**
0229      * Associates an i18n arrays of arguments to a {@code node}.
0230      * These arguments will be used alongside a key to resolve a message via the application's {@code MessageSource}.
0231      *
0232      @param node the target node on which the key will be registered.
0233      @param args the array of arguments to be registered.
0234      *
0235      @since 2.9.0
0236      */
0237     public static void setI18nArgs(@Nonnull Tab node, @Nullable String args) {
0238         requireNonNull(node, ERROR_NODE_NULL);
0239         requireNonBlank(args, ERROR_ARGS_BLANK);
0240         node.getProperties().put(MessageSource.class.getName() + SUFFIX_ARGS, args);
0241     }
0242 
0243     /**
0244      * Finds out if an {@code arguments array} has been registered with the target {@code Node}, returning the array if found.
0245      *
0246      @param node the target node on which the arguments may have been registered.
0247      *
0248      @return the arguments registered with the target {@code Node} or {@code null} if not found.
0249      *
0250      @since 2.9.0
0251      */
0252     @Nullable
0253     public static String getI18nArgs(@Nonnull Tab node) {
0254         requireNonNull(node, ERROR_NODE_NULL);
0255         return (Stringnode.getProperties().get(MessageSource.class.getName() + SUFFIX_ARGS);
0256     }
0257 
0258     /**
0259      * Associates an default value {@code node}.
0260      * The value will be used alongside a key to resolve a message via the application's {@code MessageSource}.
0261      *
0262      @param node         the target node on which the key will be registered.
0263      @param defaultValue the value to be registered.
0264      *
0265      @since 2.9.0
0266      */
0267     public static void setI18nDefaultValue(@Nonnull Tab node, @Nullable String defaultValue) {
0268         requireNonNull(node, ERROR_NODE_NULL);
0269         node.getProperties().put(MessageSource.class.getName() + SUFFIX_DEFAULT_VALUE, defaultValue);
0270     }
0271 
0272     /**
0273      * Finds out if a {@code default value} has been registered with the target {@code Node}, returning the value if found.
0274      *
0275      @param node the target node on which the value may have been registered.
0276      *
0277      @return the value registered with the target {@code Node} or {@code null} if not found.
0278      *
0279      @since 2.9.0
0280      */
0281     @Nullable
0282     public static String getI18nDefaultValue(@Nonnull Tab node) {
0283         requireNonNull(node, ERROR_NODE_NULL);
0284         return (Stringnode.getProperties().get(MessageSource.class.getName() + SUFFIX_DEFAULT_VALUE);
0285     }
0286 
0287     /**
0288      * Associates an i18n key to a {@code node}. The key is used to resolve a message via the application's {@code MessageSource}.
0289      *
0290      @param node the target node on which the key will be registered.
0291      @param key  the message key to be registered.
0292      *
0293      @since 2.9.0
0294      */
0295     public static void setI18nKey(@Nonnull MenuItem node, @Nonnull String key) {
0296         requireNonNull(node, ERROR_NODE_NULL);
0297         requireNonBlank(key, ERROR_KEY_BLANK);
0298         node.getProperties().put(MessageSource.class.getName() + SUFFIX_KEY, key);
0299     }
0300 
0301     /**
0302      * Finds out if an i18n {@code key} has been registered with the target {@code Node}, returning the key if found.
0303      *
0304      @param node the target node on which the key may have been registered.
0305      *
0306      @return the key registered with the target {@code Node} or {@code null} if not found.
0307      *
0308      @since 2.9.0
0309      */
0310     @Nullable
0311     public static String getI18nKey(@Nonnull MenuItem node) {
0312         requireNonNull(node, ERROR_NODE_NULL);
0313         return (Stringnode.getProperties().get(MessageSource.class.getName() + SUFFIX_KEY);
0314     }
0315 
0316     /**
0317      * Associates an i18n arrays of arguments to a {@code node}.
0318      * These arguments will be used alongside a key to resolve a message via the application's {@code MessageSource}.
0319      *
0320      @param node the target node on which the key will be registered.
0321      @param args the array of arguments to be registered.
0322      *
0323      @since 2.9.0
0324      */
0325     public static void setI18nArgs(@Nonnull MenuItem node, @Nullable String args) {
0326         requireNonNull(node, ERROR_NODE_NULL);
0327         requireNonBlank(args, ERROR_ARGS_BLANK);
0328         node.getProperties().put(MessageSource.class.getName() + SUFFIX_ARGS, args);
0329     }
0330 
0331     /**
0332      * Finds out if an {@code arguments array} has been registered with the target {@code Node}, returning the array if found.
0333      *
0334      @param node the target node on which the arguments may have been registered.
0335      *
0336      @return the arguments registered with the target {@code Node} or {@code null} if not found.
0337      *
0338      @since 2.9.0
0339      */
0340     @Nullable
0341     public static String getI18nArgs(@Nonnull MenuItem node) {
0342         requireNonNull(node, ERROR_NODE_NULL);
0343         return (Stringnode.getProperties().get(MessageSource.class.getName() + SUFFIX_ARGS);
0344     }
0345 
0346     /**
0347      * Associates an default value {@code node}.
0348      * The value will be used alongside a key to resolve a message via the application's {@code MessageSource}.
0349      *
0350      @param node         the target node on which the key will be registered.
0351      @param defaultValue the value to be registered.
0352      *
0353      @since 2.9.0
0354      */
0355     public static void setI18nDefaultValue(@Nonnull MenuItem node, @Nullable String defaultValue) {
0356         requireNonNull(node, ERROR_NODE_NULL);
0357         node.getProperties().put(MessageSource.class.getName() + SUFFIX_DEFAULT_VALUE, defaultValue);
0358     }
0359 
0360     /**
0361      * Finds out if a {@code default value} has been registered with the target {@code Node}, returning the value if found.
0362      *
0363      @param node the target node on which the value may have been registered.
0364      *
0365      @return the value registered with the target {@code Node} or {@code null} if not found.
0366      *
0367      @since 2.9.0
0368      */
0369     @Nullable
0370     public static String getI18nDefaultValue(@Nonnull MenuItem node) {
0371         requireNonNull(node, ERROR_NODE_NULL);
0372         return (Stringnode.getProperties().get(MessageSource.class.getName() + SUFFIX_DEFAULT_VALUE);
0373     }
0374 
0375     /**
0376      * Associates an i18n key to a {@code node}. The key is used to resolve a message via the application's {@code MessageSource}.
0377      *
0378      @param node the target node on which the key will be registered.
0379      @param key  the message key to be registered.
0380      *
0381      @since 2.11.0
0382      */
0383     public static void setI18nKey(@Nonnull Axis<?> node, @Nonnull String key) {
0384         requireNonNull(node, ERROR_NODE_NULL);
0385         requireNonBlank(key, ERROR_KEY_BLANK);
0386         node.getProperties().put(MessageSource.class.getName() + SUFFIX_KEY, key);
0387     }
0388 
0389     /**
0390      * Finds out if an i18n {@code key} has been registered with the target {@code Node}, returning the key if found.
0391      *
0392      @param node the target node on which the key may have been registered.
0393      *
0394      @return the key registered with the target {@code Node} or {@code null} if not found.
0395      *
0396      @since 2.11.0
0397      */
0398     @Nullable
0399     public static String getI18nKey(@Nonnull Axis<?> node) {
0400         requireNonNull(node, ERROR_NODE_NULL);
0401         return (Stringnode.getProperties().get(MessageSource.class.getName() + SUFFIX_KEY);
0402     }
0403 
0404     /**
0405      * Associates an i18n arrays of arguments to a {@code node}.
0406      * These arguments will be used alongside a key to resolve a message via the application's {@code MessageSource}.
0407      *
0408      @param node the target node on which the key will be registered.
0409      @param args the array of arguments to be registered.
0410      *
0411      @since 2.11.0
0412      */
0413     public static void setI18nArgs(@Nonnull Axis<?> node, @Nullable String args) {
0414         requireNonNull(node, ERROR_NODE_NULL);
0415         requireNonBlank(args, ERROR_ARGS_BLANK);
0416         node.getProperties().put(MessageSource.class.getName() + SUFFIX_ARGS, args);
0417     }
0418 
0419     /**
0420      * Finds out if an {@code arguments array} has been registered with the target {@code Node}, returning the array if found.
0421      *
0422      @param node the target node on which the arguments may have been registered.
0423      *
0424      @return the arguments registered with the target {@code Node} or {@code null} if not found.
0425      *
0426      @since 2.11.0
0427      */
0428     @Nullable
0429     public static String getI18nArgs(@Nonnull Axis<?> node) {
0430         requireNonNull(node, ERROR_NODE_NULL);
0431         return (Stringnode.getProperties().get(MessageSource.class.getName() + SUFFIX_ARGS);
0432     }
0433 
0434     /**
0435      * Associates an default value {@code node}.
0436      * The value will be used alongside a key to resolve a message via the application's {@code MessageSource}.
0437      *
0438      @param node         the target node on which the key will be registered.
0439      @param defaultValue the value to be registered.
0440      *
0441      @since 2.11.0
0442      */
0443     public static void setI18nDefaultValue(@Nonnull Axis<?> node, @Nullable String defaultValue) {
0444         requireNonNull(node, ERROR_NODE_NULL);
0445         node.getProperties().put(MessageSource.class.getName() + SUFFIX_DEFAULT_VALUE, defaultValue);
0446     }
0447 
0448     /**
0449      * Finds out if a {@code default value} has been registered with the target {@code Node}, returning the value if found.
0450      *
0451      @param node the target node on which the value may have been registered.
0452      *
0453      @return the value registered with the target {@code Node} or {@code null} if not found.
0454      *
0455      @since 2.11.0
0456      */
0457     @Nullable
0458     public static String getI18nDefaultValue(@Nonnull Axis<?> node) {
0459         requireNonNull(node, ERROR_NODE_NULL);
0460         return (Stringnode.getProperties().get(MessageSource.class.getName() + SUFFIX_DEFAULT_VALUE);
0461     }
0462 
0463     /**
0464      * Associates an i18n key to a {@code node}. The key is used to resolve a message via the application's {@code MessageSource}.
0465      *
0466      @param node the target node on which the key will be registered.
0467      @param key  the message key to be registered.
0468      *
0469      @since 2.11.0
0470      */
0471     public static void setI18nKey(@Nonnull TableColumn<?, ?> node, @Nonnull String key) {
0472         requireNonNull(node, ERROR_NODE_NULL);
0473         requireNonBlank(key, ERROR_KEY_BLANK);
0474         node.getProperties().put(MessageSource.class.getName() + SUFFIX_KEY, key);
0475     }
0476 
0477     /**
0478      * Finds out if an i18n {@code key} has been registered with the target {@code Node}, returning the key if found.
0479      *
0480      @param node the target node on which the key may have been registered.
0481      *
0482      @return the key registered with the target {@code Node} or {@code null} if not found.
0483      *
0484      @since 2.11.0
0485      */
0486     @Nullable
0487     public static String getI18nKey(@Nonnull TableColumn<?, ?> node) {
0488         requireNonNull(node, ERROR_NODE_NULL);
0489         return (Stringnode.getProperties().get(MessageSource.class.getName() + SUFFIX_KEY);
0490     }
0491 
0492     /**
0493      * Associates an i18n arrays of arguments to a {@code node}.
0494      * These arguments will be used alongside a key to resolve a message via the application's {@code MessageSource}.
0495      *
0496      @param node the target node on which the key will be registered.
0497      @param args the array of arguments to be registered.
0498      *
0499      @since 2.11.0
0500      */
0501     public static void setI18nArgs(@Nonnull TableColumn<?, ?> node, @Nullable String args) {
0502         requireNonNull(node, ERROR_NODE_NULL);
0503         requireNonBlank(args, ERROR_ARGS_BLANK);
0504         node.getProperties().put(MessageSource.class.getName() + SUFFIX_ARGS, args);
0505     }
0506 
0507     /**
0508      * Finds out if an {@code arguments array} has been registered with the target {@code Node}, returning the array if found.
0509      *
0510      @param node the target node on which the arguments may have been registered.
0511      *
0512      @return the arguments registered with the target {@code Node} or {@code null} if not found.
0513      *
0514      @since 2.11.0
0515      */
0516     @Nullable
0517     public static String getI18nArgs(@Nonnull TableColumn<?, ?> node) {
0518         requireNonNull(node, ERROR_NODE_NULL);
0519         return (Stringnode.getProperties().get(MessageSource.class.getName() + SUFFIX_ARGS);
0520     }
0521 
0522     /**
0523      * Associates an default value {@code node}.
0524      * The value will be used alongside a key to resolve a message via the application's {@code MessageSource}.
0525      *
0526      @param node         the target node on which the key will be registered.
0527      @param defaultValue the value to be registered.
0528      *
0529      @since 2.11.0
0530      */
0531     public static void setI18nDefaultValue(@Nonnull TableColumn<?, ?> node, @Nullable String defaultValue) {
0532         requireNonNull(node, ERROR_NODE_NULL);
0533         node.getProperties().put(MessageSource.class.getName() + SUFFIX_DEFAULT_VALUE, defaultValue);
0534     }
0535 
0536     /**
0537      * Finds out if a {@code default value} has been registered with the target {@code Node}, returning the value if found.
0538      *
0539      @param node the target node on which the value may have been registered.
0540      *
0541      @return the value registered with the target {@code Node} or {@code null} if not found.
0542      *
0543      @since 2.11.0
0544      */
0545     @Nullable
0546     public static String getI18nDefaultValue(@Nonnull TableColumn<?, ?> node) {
0547         requireNonNull(node, ERROR_NODE_NULL);
0548         return (Stringnode.getProperties().get(MessageSource.class.getName() + SUFFIX_DEFAULT_VALUE);
0549     }
0550 
0551     public static void connectMessageSource(@Nonnull Object node, @Nonnull GriffonApplication application) {
0552         requireNonNull(node, ERROR_NODE_NULL);
0553         requireNonNull(application, ERROR_APPLICATION_NULL);
0554 
0555         findElements(node, arg -> (arg instanceof Labeled && !isBlank(getI18nKey((Labeledarg))) ||
0556             (arg instanceof Tab && !isBlank(getI18nKey((Tabarg))) ||
0557             (arg instanceof MenuItem && !isBlank(getI18nKey((MenuItemarg))) ||
0558             (arg instanceof TableColumn && !isBlank(getI18nKey((TableColumn<?, ?>arg))) ||
0559             (arg instanceof Axis && !isBlank(getI18nKey((Axis<?>arg))))
0560             .forEach(element -> {
0561                 if (element instanceof Labeled) {
0562                     doConnectMessageSource((Labeledelement, application);
0563                 else if (element instanceof Tab) {
0564                     doConnectMessageSource((Tabelement, application);
0565                 else if (element instanceof MenuItem) {
0566                     doConnectMessageSource((MenuItemelement, application);
0567                 else if (element instanceof TableColumn) {
0568                     doConnectMessageSource((TableColumn<?, ?>element, application);
0569                 else if (element instanceof Axis) {
0570                     doConnectMessageSource((Axis<?>element, application);
0571                 }
0572             });
0573     }
0574 
0575     private static void doConnectMessageSource(@Nonnull final Labeled node, @Nonnull final GriffonApplication application) {
0576         application.addPropertyChangeListener(PROPERTY_LOCALE, evt -> updateLabeled(node, application));
0577         updateLabeled(node, application);
0578     }
0579 
0580     private static void doConnectMessageSource(@Nonnull final Tab node, @Nonnull final GriffonApplication application) {
0581         application.addPropertyChangeListener(PROPERTY_LOCALE, evt -> updateLabeled(node, application));
0582         updateLabeled(node, application);
0583     }
0584 
0585     private static void doConnectMessageSource(@Nonnull final MenuItem node, @Nonnull final GriffonApplication application) {
0586         application.addPropertyChangeListener(PROPERTY_LOCALE, evt -> updateLabeled(node, application));
0587         updateLabeled(node, application);
0588     }
0589 
0590     private static void doConnectMessageSource(@Nonnull final TableColumn<?, ?> node, @Nonnull final GriffonApplication application) {
0591         application.addPropertyChangeListener(PROPERTY_LOCALE, evt -> updateLabeled(node, application));
0592         updateLabeled(node, application);
0593     }
0594 
0595     private static void doConnectMessageSource(@Nonnull final Axis<?> node, @Nonnull final GriffonApplication application) {
0596         application.addPropertyChangeListener(PROPERTY_LOCALE, evt -> updateLabeled(node, application));
0597         updateLabeled(node, application);
0598     }
0599 
0600     private static void updateLabeled(@Nonnull final Labeled node, @Nonnull final GriffonApplication application) {
0601         runInsideUIThread(() -> {
0602             String key = getI18nKey(node);
0603             String args = getI18nArgs(node);
0604             String defaultValue = getI18nDefaultValue(node);
0605 
0606             Object[] argArray = isBlank(args? EMPTY_OBJECT_ARRAY : args.split(",");
0607 
0608             if (isBlank(defaultValue)) {
0609                 node.setText(application.getMessageSource().getMessage(key, argArray, application.getLocale()));
0610             else {
0611                 node.setText(application.getMessageSource().getMessage(key, argArray, application.getLocale(), defaultValue));
0612             }
0613         });
0614     }
0615 
0616     private static void updateLabeled(@Nonnull final Tab node, @Nonnull final GriffonApplication application) {
0617         runInsideUIThread(() -> {
0618             String key = getI18nKey(node);
0619             String args = getI18nArgs(node);
0620             String defaultValue = getI18nDefaultValue(node);
0621 
0622             Object[] argArray = isBlank(args? EMPTY_OBJECT_ARRAY : args.split(",");
0623 
0624             if (isBlank(defaultValue)) {
0625                 node.setText(application.getMessageSource().getMessage(key, argArray, application.getLocale()));
0626             else {
0627                 node.setText(application.getMessageSource().getMessage(key, argArray, application.getLocale(), defaultValue));
0628             }
0629         });
0630     }
0631 
0632     private static void updateLabeled(@Nonnull final MenuItem node, @Nonnull final GriffonApplication application) {
0633         runInsideUIThread(() -> {
0634             String key = getI18nKey(node);
0635             String args = getI18nArgs(node);
0636             String defaultValue = getI18nDefaultValue(node);
0637 
0638             Object[] argArray = isBlank(args? EMPTY_OBJECT_ARRAY : args.split(",");
0639 
0640             if (isBlank(defaultValue)) {
0641                 node.setText(application.getMessageSource().getMessage(key, argArray, application.getLocale()));
0642             else {
0643                 node.setText(application.getMessageSource().getMessage(key, argArray, application.getLocale(), defaultValue));
0644             }
0645         });
0646     }
0647 
0648     private static void updateLabeled(@Nonnull final TableColumn<?, ?> node, @Nonnull final GriffonApplication application) {
0649         runInsideUIThread(() -> {
0650             String key = getI18nKey(node);
0651             String args = getI18nArgs(node);
0652             String defaultValue = getI18nDefaultValue(node);
0653 
0654             Object[] argArray = isBlank(args? EMPTY_OBJECT_ARRAY : args.split(",");
0655 
0656             if (isBlank(defaultValue)) {
0657                 node.setText(application.getMessageSource().getMessage(key, argArray, application.getLocale()));
0658             else {
0659                 node.setText(application.getMessageSource().getMessage(key, argArray, application.getLocale(), defaultValue));
0660             }
0661         });
0662     }
0663 
0664     private static void updateLabeled(@Nonnull final Axis<?> node, @Nonnull final GriffonApplication application) {
0665         runInsideUIThread(() -> {
0666             String key = getI18nKey(node);
0667             String args = getI18nArgs(node);
0668             String defaultValue = getI18nDefaultValue(node);
0669 
0670             Object[] argArray = isBlank(args? EMPTY_OBJECT_ARRAY : args.split(",");
0671 
0672             if (isBlank(defaultValue)) {
0673                 node.setLabel(application.getMessageSource().getMessage(key, argArray, application.getLocale()));
0674             else {
0675                 node.setLabel(application.getMessageSource().getMessage(key, argArray, application.getLocale(), defaultValue));
0676             }
0677         });
0678     }
0679 
0680     /**
0681      * Associates a {@code Action} with a target {@code Node}.
0682      *
0683      @param node     the target node on which the action will be registered.
0684      @param actionId the id of the action to be registered.
0685      *
0686      @since 2.8.0
0687      */
0688     public static void setGriffonActionId(@Nonnull Node node, @Nonnull String actionId) {
0689         requireNonNull(node, ERROR_NODE_NULL);
0690         requireNonBlank(actionId, ERROR_ID_BLANK);
0691         node.getProperties().put(Action.class.getName(), actionId);
0692     }
0693 
0694     /**
0695      * Finds out if an {@code Action} has been registered with the target {@code Node}, returning the action id if found.
0696      *
0697      @param node the target node on which the action may have been registered.
0698      *
0699      @return the name of the action registered with the target {@code Node} or {@code null} if not found.
0700      *
0701      @since 2.8.0
0702      */
0703     @Nullable
0704     public static String getGriffonActionId(@Nonnull Node node) {
0705         requireNonNull(node, ERROR_NODE_NULL);
0706         return (Stringnode.getProperties().get(Action.class.getName());
0707     }
0708 
0709     /**
0710      * Associates a {@code Action} with a target {@code MenuItem}.
0711      *
0712      @param menuItem the target menuItem on which the action will be registered.
0713      @param actionId the id of the action to be registered.
0714      *
0715      @since 2.8.0
0716      */
0717     public static void setGriffonActionId(@Nonnull MenuItem menuItem, @Nonnull String actionId) {
0718         requireNonNull(menuItem, ERROR_NODE_NULL);
0719         requireNonBlank(actionId, ERROR_ID_BLANK);
0720         menuItem.getProperties().put(Action.class.getName(), actionId);
0721     }
0722 
0723     /**
0724      * Finds out if an {@code Action} has been registered with the target {@code MenuItem}, returning the action id if found.
0725      *
0726      @param menuItem the target menuItem on which the action may have been registered.
0727      *
0728      @return the name of the action registered with the target {@code MenuItem} or {@code null} if not found.
0729      *
0730      @since 2.8.0
0731      */
0732     @Nullable
0733     public static String getGriffonActionId(@Nonnull MenuItem menuItem) {
0734         requireNonNull(menuItem, ERROR_NODE_NULL);
0735         return (StringmenuItem.getProperties().get(Action.class.getName());
0736     }
0737 
0738     /**
0739      * Wraps an <tt>ObservableList</tt>, publishing updates inside the UI thread.
0740      *
0741      @param source the <tt>ObservableList</tt> to be wrapped
0742      @param <E>    the list's parameter type.
0743      *
0744      @return a new <tt>ObservableList</tt>
0745      *
0746      @see GriffonFXCollections#uiThreadAwareObservableList
0747      @since 2.6.0
0748      @deprecated Use {@code GriffonFXCollections.uiThreadAwareObservableList} instead.
0749      */
0750     @Deprecated
0751     @Nonnull
0752     public static <E> ObservableList<E> createJavaFXThreadProxyList(@Nonnull ObservableList<E> source) {
0753         return GriffonFXCollections.uiThreadAwareObservableList(source);
0754     }
0755 
0756     /**
0757      * Wraps an <tt>ObservableSet</tt>, publishing updates inside the UI thread.
0758      *
0759      @param source the <tt>ObservableSet</tt> to be wrapped
0760      @param <E>    the set's parameter type.
0761      *
0762      @return a new <tt>ObservableSet</tt>
0763      *
0764      @see GriffonFXCollections#uiThreadAwareObservableSet
0765      @since 2.9.0
0766      @deprecated Use {@code GriffonFXCollections.uiThreadAwareObservableSet} instead.
0767      */
0768     @Deprecated
0769     @Nonnull
0770     public static <E> ObservableSet<E> createJavaFXThreadProxySet(@Nonnull ObservableSet<E> source) {
0771         return GriffonFXCollections.uiThreadAwareObservableSet(source);
0772     }
0773 
0774     /**
0775      * Wraps an <tt>ObservableMap</tt>, publishing updates inside the UI thread.
0776      *
0777      @param source the <tt>ObservableMap</tt> to be wrapped
0778      @param <K>    the type of keys maintained by the map
0779      @param <V>    the type of mapped values
0780      *
0781      @return a new <tt>ObservableMap</tt>
0782      *
0783      @see GriffonFXCollections#uiThreadAwareObservableMap
0784      @since 2.9.0
0785      @deprecated Use {@code GriffonFXCollections.uiThreadAwareObservableMap} instead.
0786      */
0787     @Deprecated
0788     @Nonnull
0789     public static <K, V> ObservableMap<K, V> createJavaFXThreadProxyMap(@Nonnull ObservableMap<K, V> source) {
0790         return GriffonFXCollections.uiThreadAwareObservableMap(source);
0791     }
0792 
0793     @Nonnull
0794     @SuppressWarnings("ConstantConditions")
0795     public static <B> Property<?> extractProperty(@Nonnull B bean, @Nonnull String propertyName) {
0796         requireNonNull(bean, "Argument 'bean' must not be null");
0797         requireNonBlank(propertyName, "Argument 'propertyName' must not be null");
0798 
0799         if (!propertyName.endsWith(PROPERTY_SUFFIX)) {
0800             propertyName += PROPERTY_SUFFIX;
0801         }
0802 
0803         InstanceMethodInvocationException imie;
0804         try {
0805             // 1. try <columnName>Property() first
0806             return (Property<?>invokeExactInstanceMethod(bean, propertyName);
0807         catch (InstanceMethodInvocationException e) {
0808             imie = e;
0809         }
0810 
0811         // 2. fallback to get<columnName>Property()
0812         try {
0813             return (Property<?>invokeExactInstanceMethod(bean, getGetterName(propertyName));
0814         catch (InstanceMethodInvocationException e) {
0815             throw imie;
0816         }
0817     }
0818 
0819     public static void connectActions(@Nonnull Object node, @Nonnull GriffonController controller, @Nonnull ActionMatcher actionMatcher) {
0820         requireNonNull(node, ERROR_NODE_NULL);
0821         requireNonNull(controller, ERROR_CONTROLLER_NULL);
0822         actionMatcher = actionMatcher != null ? actionMatcher : DEFAULT_ACTION_MATCHER;
0823         ActionManager actionManager = controller.getApplication().getActionManager();
0824         for (Map.Entry<String, Action> e : actionManager.actionsFor(controller).entrySet()) {
0825             String actionName = actionManager.normalizeName(e.getKey());
0826             JavaFXAction action = (JavaFXActione.getValue().getToolkitAction();
0827             actionMatcher.match(node, actionName, action);
0828         }
0829     }
0830 
0831     public static void connectActions(@Nonnull Object node, @Nonnull GriffonController controller) {
0832         connectActions(node, controller, DEFAULT_ACTION_MATCHER);
0833     }
0834 
0835     public static void configureControl(@Nonnull Object control, @Nonnull JavaFXAction action) {
0836         if (control instanceof ButtonBase) {
0837             configure(((ButtonBasecontrol), action);
0838         else if (control instanceof MenuItem) {
0839             JavaFXUtils.configure(((MenuItemcontrol), action);
0840         else if (control instanceof Node) {
0841             ((Nodecontrol).addEventHandler(ActionEvent.ACTION, wrapAction(action));
0842         else {
0843             // does it support the onAction property?
0844             try {
0845                 invokeInstanceMethod(control, "setOnAction", wrapAction(action));
0846             catch (InstanceMethodInvocationException imie) {
0847                 // ignore
0848             }
0849         }
0850     }
0851 
0852     private static EventHandler<ActionEvent> wrapAction(@Nonnull final JavaFXAction action) {
0853         return event -> {
0854             if (action.isEnabled()) {
0855                 action.getOnAction().handle(event);
0856             }
0857         };
0858     }
0859 
0860     private static void runInsideUIThread(@Nonnull Runnable runnable) {
0861         if (Platform.isFxApplicationThread()) {
0862             runnable.run();
0863         else {
0864             Platform.runLater(runnable);
0865         }
0866     }
0867 
0868     public static String normalizeStyle(@Nonnull String style, @Nonnull String key, @Nonnull String value) {
0869         requireNonBlank(style, "Argument 'style' must not be blank");
0870         requireNonBlank(key, "Argument 'key' must not be blank");
0871         requireNonBlank(value, "Argument 'value' must not be blank");
0872 
0873         int start = style.indexOf(key);
0874         if (start != -1) {
0875             int end = style.indexOf(';', start);
0876             end = end >= start ? end : style.length() 1;
0877             style = style.substring(0, start+ style.substring(end + 1);
0878         }
0879         return style + key + ": " + value + ";";
0880     }
0881 
0882     public static void configure(@Nonnull final ToggleButton control, @Nonnull final JavaFXAction action) {
0883         configure((ButtonBasecontrol, action);
0884 
0885         action.selectedProperty().addListener((v, o, n-> runInsideUIThread(() -> control.setSelected(n)));
0886         runInsideUIThread(() -> control.setSelected(action.isSelected()));
0887     }
0888 
0889     public static void configure(@Nonnull final CheckBox control, @Nonnull final JavaFXAction action) {
0890         configure((ButtonBasecontrol, action);
0891 
0892         action.selectedProperty().addListener((v, o, n-> runInsideUIThread(() -> control.setSelected(n)));
0893         runInsideUIThread(() -> control.setSelected(action.isSelected()));
0894     }
0895 
0896     public static void configure(@Nonnull final RadioButton control, @Nonnull final JavaFXAction action) {
0897         configure((ButtonBasecontrol, action);
0898 
0899         action.selectedProperty().addListener((v, o, n-> runInsideUIThread(() -> control.setSelected(n)));
0900         runInsideUIThread(() -> control.setSelected(action.isSelected()));
0901     }
0902 
0903     public static void configure(@Nonnull final ButtonBase control, @Nonnull final JavaFXAction action) {
0904         requireNonNull(control, ERROR_CONTROL_NULL);
0905         requireNonNull(action, ERROR_ACTION_NULL);
0906 
0907         action.onActionProperty().addListener((v, o, n-> control.setOnAction(n));
0908         control.setOnAction(action.getOnAction());
0909 
0910         action.nameProperty().addListener((v, o, n-> runInsideUIThread(() -> control.setText(n)));
0911         runInsideUIThread(() -> control.setText(action.getName()));
0912 
0913         action.descriptionProperty().addListener((v, o, n-> setTooltip(control, n));
0914         setTooltip(control, action.getDescription());
0915 
0916         action.iconProperty().addListener((v, o, n-> setIcon(control, n));
0917         if (!isBlank(action.getIcon())) {
0918             setIcon(control, action.getIcon());
0919         }
0920 
0921         action.imageProperty().addListener((v, o, n-> setGraphic(control, n));
0922         if (null != action.getImage()) {
0923             setGraphic(control, action.getImage());
0924         }
0925 
0926         action.graphicProperty().addListener((v, o, n-> setGraphic(control, n));
0927         if (null != action.getGraphic()) {
0928             setGraphic(control, action.getGraphic());
0929         }
0930 
0931         action.enabledProperty().addListener((v, o, n-> runInsideUIThread(() -> control.setDisable(!n)));
0932         runInsideUIThread(() -> control.setDisable(!action.isEnabled()));
0933 
0934         action.visibleProperty().addListener((v, o, n-> runInsideUIThread(() -> control.setVisible(n)));
0935         runInsideUIThread(() -> control.setVisible(action.isVisible()));
0936 
0937         action.styleClassProperty().addListener((v, o, n-> {
0938             setStyleClass(control, o, true);
0939             setStyleClass(control, n);
0940         });
0941         setStyleClass(control, action.getStyleClass());
0942 
0943         action.styleProperty().addListener((v, o, n-> setStyle(control, n));
0944         setStyle(control, action.getStyle());
0945 
0946         action.graphicStyleClassProperty().addListener((v, o, n-> {
0947             setGraphicStyleClass(control, o, true);
0948             setGraphicStyleClass(control, n);
0949         });
0950         setGraphicStyleClass(control, action.getGraphicStyleClass());
0951 
0952         action.graphicStyleProperty().addListener((v, o, n-> setGraphicStyle(control, n));
0953         setGraphicStyle(control, action.getGraphicStyle());
0954     }
0955 
0956     public static void configure(@Nonnull final CheckMenuItem control, @Nonnull final JavaFXAction action) {
0957         configure((MenuItemcontrol, action);
0958 
0959         action.selectedProperty().addListener((v, o, n-> runInsideUIThread(() -> control.setSelected(n)));
0960         runInsideUIThread(() -> control.setSelected(action.isSelected()));
0961     }
0962 
0963     public static void configure(@Nonnull final RadioMenuItem control, @Nonnull final JavaFXAction action) {
0964         configure((MenuItemcontrol, action);
0965 
0966         action.selectedProperty().addListener((v, o, n-> runInsideUIThread(() -> control.setSelected(n)));
0967         runInsideUIThread(() -> control.setSelected(action.isSelected()));
0968     }
0969 
0970     public static void configure(@Nonnull final MenuItem control, @Nonnull final JavaFXAction action) {
0971         requireNonNull(control, ERROR_CONTROL_NULL);
0972         requireNonNull(action, ERROR_ACTION_NULL);
0973 
0974         action.onActionProperty().addListener((v, o, n-> control.setOnAction(n));
0975         control.setOnAction(action.getOnAction());
0976 
0977         action.nameProperty().addListener((v, o, n-> runInsideUIThread(() -> control.setText(n)));
0978         runInsideUIThread(() -> control.setText(action.getName()));
0979 
0980         action.iconProperty().addListener((v, o, n-> setIcon(control, n));
0981         if (!isBlank(action.getIcon())) {
0982             setIcon(control, action.getIcon());
0983         }
0984 
0985         action.imageProperty().addListener((v, o, n-> setGraphic(control, n));
0986         if (null != action.getImage()) {
0987             setGraphic(control, action.getImage());
0988         }
0989 
0990         action.graphicProperty().addListener((v, o, n-> setGraphic(control, n));
0991         if (null != action.getGraphic()) {
0992             setGraphic(control, action.getGraphic());
0993         }
0994 
0995         action.enabledProperty().addListener((v, o, n-> runInsideUIThread(() -> control.setDisable(!n)));
0996         runInsideUIThread(() -> control.setDisable(!action.getEnabled()));
0997 
0998         action.acceleratorProperty().addListener((v, o, n-> runInsideUIThread(() -> control.setAccelerator(n)));
0999         runInsideUIThread(() -> control.setAccelerator(action.getAccelerator()));
1000 
1001         action.visibleProperty().addListener((v, o, n-> runInsideUIThread(() -> control.setVisible(n)));
1002         runInsideUIThread(() -> control.setVisible(action.isVisible()));
1003 
1004         action.styleClassProperty().addListener((v, o, n-> {
1005             setStyleClass(control, o, true);
1006             setStyleClass(control, n);
1007         });
1008         setStyleClass(control, action.getStyleClass());
1009 
1010         action.styleProperty().addListener((v, o, n-> setStyle(control, n));
1011         setStyle(control, action.getStyle());
1012 
1013         action.graphicStyleClassProperty().addListener((v, o, n-> {
1014             setGraphicStyleClass(control, o, true);
1015             setGraphicStyleClass(control, n);
1016         });
1017         setGraphicStyleClass(control, action.getGraphicStyleClass());
1018 
1019         action.graphicStyleProperty().addListener((v, o, n-> setGraphicStyle(control, n));
1020         setGraphicStyle(control, action.getGraphicStyle());
1021     }
1022 
1023     public static void setStyle(@Nonnull Node node, @Nonnull String style) {
1024         requireNonNull(node, ERROR_CONTROL_NULL);
1025         if (isBlank(style)) { return}
1026         if (style.startsWith("&")) {
1027             // append style
1028             String nodeStyle = node.getStyle();
1029             node.setStyle(nodeStyle + (nodeStyle.endsWith(";""" ";"+ style.substring(1));
1030         else {
1031             node.setStyle(style);
1032         }
1033     }
1034 
1035     public static void setStyle(@Nonnull MenuItem node, @Nonnull String style) {
1036         requireNonNull(node, ERROR_CONTROL_NULL);
1037         if (isBlank(style)) { return}
1038         if (style.startsWith("&")) {
1039             // append style
1040             String nodeStyle = node.getStyle();
1041             node.setStyle(nodeStyle + (nodeStyle.endsWith(";""" ";"+ style.substring(1));
1042         else {
1043             node.setStyle(style);
1044         }
1045     }
1046 
1047     public static void setGraphicStyle(@Nonnull ButtonBase node, @Nonnull String graphicStyle) {
1048         requireNonNull(node, ERROR_CONTROL_NULL);
1049         if (isBlank(graphicStyle)) { return}
1050         if (node.getGraphic() != null) {
1051             setStyle(node.getGraphic(), graphicStyle);
1052         }
1053     }
1054 
1055     public static void setGraphicStyle(@Nonnull MenuItem node, @Nonnull String graphicStyle) {
1056         requireNonNull(node, ERROR_CONTROL_NULL);
1057         if (isBlank(graphicStyle)) { return}
1058         if (node.getGraphic() != null) {
1059             setStyle(node.getGraphic(), graphicStyle);
1060         }
1061     }
1062 
1063     public static void setStyleClass(@Nonnull Node node, @Nonnull String styleClass) {
1064         setStyleClass(node, styleClass, false);
1065     }
1066 
1067     public static void setStyleClass(@Nonnull Node node, @Nonnull String styleClass, boolean remove) {
1068         requireNonNull(node, ERROR_CONTROL_NULL);
1069         if (isBlank(styleClass)) { return}
1070 
1071         ObservableList<String> styleClasses = node.getStyleClass();
1072         applyStyleClass(styleClass, styleClasses, remove);
1073     }
1074 
1075     public static void setStyleClass(@Nonnull MenuItem node, @Nonnull String styleClass) {
1076         setStyleClass(node, styleClass, false);
1077     }
1078 
1079     public static void setStyleClass(@Nonnull MenuItem node, @Nonnull String styleClass, boolean remove) {
1080         requireNonNull(node, ERROR_CONTROL_NULL);
1081         if (isBlank(styleClass)) { return}
1082         ObservableList<String> styleClasses = node.getStyleClass();
1083         applyStyleClass(styleClass, styleClasses, remove);
1084     }
1085 
1086     public static void setGraphicStyleClass(@Nonnull ButtonBase node, @Nonnull String graphicStyleClass) {
1087         setGraphicStyleClass(node, graphicStyleClass, false);
1088     }
1089 
1090     public static void setGraphicStyleClass(@Nonnull ButtonBase node, @Nonnull String graphicStyleClass, boolean remove) {
1091         requireNonNull(node, ERROR_CONTROL_NULL);
1092         if (isBlank(graphicStyleClass|| node.getGraphic() == null) { return}
1093 
1094         ObservableList<String> graphicStyleClasses = node.getGraphic().getStyleClass();
1095         applyStyleClass(graphicStyleClass, graphicStyleClasses, remove);
1096     }
1097 
1098     public static void setGraphicStyleClass(@Nonnull MenuItem node, @Nonnull String graphicStyleClass) {
1099         setGraphicStyleClass(node, graphicStyleClass, false);
1100     }
1101 
1102     public static void setGraphicStyleClass(@Nonnull MenuItem node, @Nonnull String graphicStyleClass, boolean remove) {
1103         requireNonNull(node, ERROR_CONTROL_NULL);
1104         if (isBlank(graphicStyleClass|| node.getGraphic() == null) { return}
1105 
1106         ObservableList<String> graphicStyleClasses = node.getGraphic().getStyleClass();
1107         applyStyleClass(graphicStyleClass, graphicStyleClasses, remove);
1108     }
1109 
1110     private static void applyStyleClass(String styleClass, ObservableList<String> styleClasses, boolean remove) {
1111         runInsideUIThread(() -> {
1112             String[] strings = styleClass.split("[,\\ ]");
1113             if (remove) {
1114                 styleClasses.removeAll(strings);
1115             else {
1116                 Set<String> classes = new LinkedHashSet<>(styleClasses);
1117                 for (String s : strings) {
1118                     if (isBlank(s)) { continue}
1119                     classes.add(s.trim());
1120                 }
1121                 styleClasses.setAll(classes);
1122             }
1123         });
1124     }
1125 
1126     public static void setTooltip(@Nonnull Control control, @Nullable String text) {
1127         runInsideUIThread(() -> {
1128             if (isBlank(text)) {
1129                 return;
1130             }
1131             requireNonNull(control, ERROR_CONTROL_NULL);
1132 
1133             Tooltip tooltip = control.tooltipProperty().get();
1134             if (tooltip == null) {
1135                 tooltip = new Tooltip();
1136                 control.tooltipProperty().set(tooltip);
1137             }
1138             tooltip.setText(text);
1139         });
1140     }
1141 
1142     public static void setIcon(@Nonnull Labeled control, @Nonnull String iconUrl) {
1143         requireNonNull(control, ERROR_CONTROL_NULL);
1144         requireNonBlank(iconUrl, ERROR_ICON_BLANK);
1145 
1146         Node graphicNode = resolveIcon(iconUrl);
1147         if (graphicNode != null) {
1148             runInsideUIThread(() -> control.graphicProperty().set(graphicNode));
1149         }
1150     }
1151 
1152     public static void setIcon(@Nonnull MenuItem control, @Nonnull String iconUrl) {
1153         requireNonNull(control, ERROR_CONTROL_NULL);
1154         requireNonBlank(iconUrl, ERROR_ICON_BLANK);
1155 
1156         Node graphicNode = resolveIcon(iconUrl);
1157         if (graphicNode != null) {
1158             runInsideUIThread(() -> control.graphicProperty().set(graphicNode));
1159         }
1160     }
1161 
1162     public static void setGraphic(@Nonnull Labeled control, @Nullable Image graphic) {
1163         requireNonNull(control, ERROR_CONTROL_NULL);
1164 
1165         runInsideUIThread(() -> {
1166             if (graphic != null) {
1167                 Node graphicNode = new ImageView(graphic);
1168                 control.graphicProperty().set(graphicNode);
1169             else {
1170                 control.graphicProperty().set(null);
1171             }
1172         });
1173     }
1174 
1175     public static void setGraphic(@Nonnull MenuItem control, @Nullable Image graphic) {
1176         requireNonNull(control, ERROR_CONTROL_NULL);
1177 
1178         runInsideUIThread(() -> {
1179             if (graphic != null) {
1180                 Node graphicNode = new ImageView(graphic);
1181                 control.graphicProperty().set(graphicNode);
1182             else {
1183                 control.graphicProperty().set(null);
1184             }
1185         });
1186     }
1187 
1188     public static void setGraphic(@Nonnull Labeled control, @Nullable Node graphic) {
1189         requireNonNull(control, ERROR_CONTROL_NULL);
1190 
1191         runInsideUIThread(() -> {
1192             if (graphic != null) {
1193                 control.graphicProperty().set(graphic);
1194             else {
1195                 control.graphicProperty().set(null);
1196             }
1197         });
1198     }
1199 
1200     public static void setGraphic(@Nonnull MenuItem control, @Nullable Node graphic) {
1201         requireNonNull(control, ERROR_CONTROL_NULL);
1202 
1203         runInsideUIThread(() -> {
1204             if (graphic != null) {
1205                 control.graphicProperty().set(graphic);
1206             else {
1207                 control.graphicProperty().set(null);
1208             }
1209         });
1210     }
1211 
1212     @Nullable
1213     public static Node resolveIcon(@Nonnull String iconUrl) {
1214         requireNonBlank(iconUrl, ERROR_URL_BLANK);
1215 
1216         if (iconUrl.contains("|")) {
1217             // assume classname|arg format
1218             return handleAsClassWithArg(iconUrl);
1219         else {
1220             URL resource = Thread.currentThread().getContextClassLoader().getResource(iconUrl);
1221             if (resource != null) {
1222                 return new ImageView(new Image(resource.toString()));
1223             }
1224         }
1225         return null;
1226     }
1227 
1228     @SuppressWarnings("unchecked")
1229     private static Node handleAsClassWithArg(String str) {
1230         String[] args = str.split("\\|");
1231         if (args.length == 2) {
1232             Class<?> iconClass = null;
1233             try {
1234                 iconClass = JavaFXUtils.class.getClassLoader().loadClass(args[0]);
1235             catch (ClassNotFoundException e) {
1236                 throw illegalValue(str, Node.class, e);
1237             }
1238 
1239             Constructor<?> constructor = null;
1240             try {
1241                 constructor = iconClass.getConstructor(String.class);
1242             catch (NoSuchMethodException e) {
1243                 throw illegalValue(str, Node.class, e);
1244             }
1245 
1246             try {
1247                 Object o = constructor.newInstance(args[1]);
1248                 if (instanceof Node) {
1249                     return (Nodeo;
1250                 else if (instanceof Image) {
1251                     return new ImageView((Imageo);
1252                 else {
1253                     throw illegalValue(str, Node.class);
1254                 }
1255             catch (InstantiationException | InvocationTargetException | IllegalAccessException e) {
1256                 throw illegalValue(str, Node.class, e);
1257             }
1258         else {
1259             throw illegalValue(str, Node.class);
1260         }
1261     }
1262 
1263     @Nullable
1264     public static Node findNode(@Nonnull Node root, @Nonnull String id) {
1265         requireNonNull(root, ERROR_ROOT_NULL);
1266         requireNonBlank(id, ERROR_ID_BLANK);
1267 
1268         if (id.equals(root.getId())) { return root; }
1269 
1270         if (root instanceof TabPane) {
1271             TabPane parent = (TabPaneroot;
1272             for (Tab child : parent.getTabs()) {
1273                 if (child.getContent() != null) {
1274                     Node found = findNode(child.getContent(), id);
1275                     if (found != null) { return found; }
1276                 }
1277             }
1278         else if (root instanceof TitledPane) {
1279             TitledPane parent = (TitledPaneroot;
1280             if (parent.getContent() != null) {
1281                 Node found = findNode(parent.getContent(), id);
1282                 if (found != null) { return found; }
1283             }
1284         else if (root instanceof Accordion) {
1285             Accordion parent = (Accordionroot;
1286             for (TitledPane child : parent.getPanes()) {
1287                 Node found = findNode(child, id);
1288                 if (found != null) { return found; }
1289             }
1290         else if (root instanceof SplitPane) {
1291             SplitPane parent = (SplitPaneroot;
1292             for (Node child : parent.getItems()) {
1293                 Node found = findNode(child, id);
1294                 if (found != null) { return found; }
1295             }
1296         else if (root instanceof ScrollPane) {
1297             ScrollPane scrollPane = (ScrollPaneroot;
1298             if (scrollPane.getContent() != null) {
1299                 Node found = findNode(scrollPane.getContent(), id);
1300                 if (found != null) { return found; }
1301             }
1302         else if (root instanceof ToolBar) {
1303             ToolBar toolBar = (ToolBarroot;
1304             for (Node child : toolBar.getItems()) {
1305                 Node found = findNode(child, id);
1306                 if (found != null) { return found; }
1307             }
1308         else if (root instanceof ButtonBar) {
1309             ButtonBar buttonBar = (ButtonBarroot;
1310             for (Node child : buttonBar.getButtons()) {
1311                 Node found = findNode(child, id);
1312                 if (found != null) { return found; }
1313             }
1314         else if (root instanceof TableView) {
1315             TableView tableView = (TableViewroot;
1316             Node placeholder = tableView.getPlaceholder();
1317             if (placeholder != null) {
1318                 Node found = findNode(placeholder, id);
1319                 if (found != null) { return found; }
1320             }
1321         }
1322 
1323         if (root instanceof Parent) {
1324             Parent parent = (Parentroot;
1325             for (Node child : parent.getChildrenUnmodifiable()) {
1326                 Node found = findNode(child, id);
1327                 if (found != null) { return found; }
1328             }
1329         }
1330 
1331         return null;
1332     }
1333 
1334     @Nullable
1335     public static Object findElement(@Nonnull Object root, @Nonnull String id) {
1336         requireNonNull(root, ERROR_ROOT_NULL);
1337         requireNonBlank(id, ERROR_ID_BLANK);
1338 
1339         if (id.equals(getPropertyValue(root, "id"))) { return root; }
1340 
1341         if (root instanceof Control) {
1342             Control control = (Controlroot;
1343             ContextMenu contextMenu = control.getContextMenu();
1344             if (contextMenu != null) {
1345                 Object found = findElement(contextMenu, id);
1346                 if (found != null) {return found;}
1347             }
1348             Tooltip tooltip = control.getTooltip();
1349             if (tooltip != null) {
1350                 Object found = findElement(tooltip, id);
1351                 if (found != null) {return found;}
1352             }
1353         }
1354 
1355         if (root instanceof ButtonBar) {
1356             ButtonBar buttonBar = (ButtonBarroot;
1357             for (Node child : buttonBar.getButtons()) {
1358                 Object found = findElement(child, id);
1359                 if (found != null) { return found; }
1360             }
1361         else if (root instanceof MenuBar) {
1362             MenuBar menuBar = (MenuBarroot;
1363             for (Menu child : menuBar.getMenus()) {
1364                 Object found = findElement(child, id);
1365                 if (found != null) { return found; }
1366             }
1367         else if (root instanceof ContextMenu) {
1368             ContextMenu contextMenu = (ContextMenuroot;
1369             for (MenuItem child : contextMenu.getItems()) {
1370                 Object found = findElement(child, id);
1371                 if (found != null) { return found; }
1372             }
1373         else if (root instanceof Menu) {
1374             Menu menu = (Menuroot;
1375             for (MenuItem child : menu.getItems()) {
1376                 Object found = findElement(child, id);
1377                 if (found != null) { return found; }
1378             }
1379         else if (root instanceof TabPane) {
1380             TabPane tabPane = (TabPaneroot;
1381             for (Tab child : tabPane.getTabs()) {
1382                 Object found = findElement(child, id);
1383                 if (found != null) { return found; }
1384             }
1385         else if (root instanceof Tab) {
1386             Tab tab = (Tabroot;
1387             if (tab.getContent() != null) {
1388                 Object found = findElement(tab.getContent(), id);
1389                 if (found != null) { return found; }
1390             }
1391         else if (root instanceof TitledPane) {
1392             TitledPane parent = (TitledPaneroot;
1393             if (parent.getContent() != null) {
1394                 Object found = findElement(parent.getContent(), id);
1395                 if (found != null) { return found; }
1396             }
1397         else if (root instanceof Accordion) {
1398             Accordion parent = (Accordionroot;
1399             for (TitledPane child : parent.getPanes()) {
1400                 Object found = findElement(child, id);
1401                 if (found != null) { return found; }
1402             }
1403         else if (root instanceof SplitPane) {
1404             SplitPane parent = (SplitPaneroot;
1405             for (Node child : parent.getItems()) {
1406                 Object found = findElement(child, id);
1407                 if (found != null) { return found; }
1408             }
1409         else if (root instanceof ScrollPane) {
1410             ScrollPane scrollPane = (ScrollPaneroot;
1411             if (scrollPane.getContent() != null) {
1412                 Object found = findElement(scrollPane.getContent(), id);
1413                 if (found != null) { return found; }
1414             }
1415         else if (root instanceof ToolBar) {
1416             ToolBar toolBar = (ToolBarroot;
1417             for (Node child : toolBar.getItems()) {
1418                 Object found = findElement(child, id);
1419                 if (found != null) { return found; }
1420             }
1421         else if (root instanceof TableView) {
1422             TableView tableView = (TableViewroot;
1423             Node placeholder = tableView.getPlaceholder();
1424             if (placeholder != null) {
1425                 Object found = findElement(placeholder, id);
1426                 if (found != null) { return found; }
1427             }
1428             for (Object child : tableView.getColumns()) {
1429                 Object found = findElement(child, id);
1430                 if (found != null) { return found; }
1431             }
1432         }
1433 
1434         if (root instanceof Parent) {
1435             Parent parent = (Parentroot;
1436             for (Node child : parent.getChildrenUnmodifiable()) {
1437                 Object found = findElement(child, id);
1438                 if (found != null) { return found; }
1439             }
1440         }
1441 
1442         return null;
1443     }
1444 
1445     @Nullable
1446     public static Object findElement(@Nonnull Object root, @Nonnull Predicate<Object> predicate) {
1447         requireNonNull(root, ERROR_ROOT_NULL);
1448         requireNonNull(predicate, ERROR_PREDICATE_NULL);
1449 
1450         if (predicate.test(root)) {
1451             return root;
1452         }
1453 
1454         if (root instanceof Control) {
1455             Control control = (Controlroot;
1456             ContextMenu contextMenu = control.getContextMenu();
1457             if (contextMenu != null) {
1458                 Object found = findElement(contextMenu, predicate);
1459                 if (found != null) {return found;}
1460             }
1461             Tooltip tooltip = control.getTooltip();
1462             if (tooltip != null) {
1463                 Object found = findElement(tooltip, predicate);
1464                 if (found != null) {return found;}
1465             }
1466         }
1467 
1468         if (root instanceof ButtonBar) {
1469             ButtonBar buttonBar = (ButtonBarroot;
1470             for (Node child : buttonBar.getButtons()) {
1471                 Object found = findElement(child, predicate);
1472                 if (found != null) { return found; }
1473             }
1474         else if (root instanceof MenuBar) {
1475             MenuBar menuBar = (MenuBarroot;
1476             for (Menu child : menuBar.getMenus()) {
1477                 Object found = findElement(child, predicate);
1478                 if (found != null) { return found; }
1479             }
1480         else if (root instanceof ContextMenu) {
1481             ContextMenu contextMenu = (ContextMenuroot;
1482             for (MenuItem child : contextMenu.getItems()) {
1483                 Object found = findElement(child, predicate);
1484                 if (found != null) { return found; }
1485             }
1486         else if (root instanceof Menu) {
1487             Menu menu = (Menuroot;
1488             for (MenuItem child : menu.getItems()) {
1489                 Object found = findElement(child, predicate);
1490                 if (found != null) { return found; }
1491             }
1492         else if (root instanceof TabPane) {
1493             TabPane tabPane = (TabPaneroot;
1494             for (Tab child : tabPane.getTabs()) {
1495                 Object found = findElement(child, predicate);
1496                 if (found != null) { return found; }
1497             }
1498         else if (root instanceof Tab) {
1499             Tab tab = (Tabroot;
1500             if (tab.getContent() != null) {
1501                 Object found = findElement(tab.getContent(), predicate);
1502                 if (found != null) { return found; }
1503             }
1504         else if (root instanceof TitledPane) {
1505             TitledPane parent = (TitledPaneroot;
1506             if (parent.getContent() != null) {
1507                 Object found = findElement(parent.getContent(), predicate);
1508                 if (found != null) { return found; }
1509             }
1510         else if (root instanceof Accordion) {
1511             Accordion parent = (Accordionroot;
1512             for (TitledPane child : parent.getPanes()) {
1513                 Object found = findElement(child, predicate);
1514                 if (found != null) { return found; }
1515             }
1516         else if (root instanceof SplitPane) {
1517             SplitPane parent = (SplitPaneroot;
1518             for (Node child : parent.getItems()) {
1519                 Object found = findElement(child, predicate);
1520                 if (found != null) { return found; }
1521             }
1522         else if (root instanceof ScrollPane) {
1523             ScrollPane scrollPane = (ScrollPaneroot;
1524             if (scrollPane.getContent() != null) {
1525                 Object found = findElement(scrollPane.getContent(), predicate);
1526                 if (found != null) { return found; }
1527             }
1528         else if (root instanceof ToolBar) {
1529             ToolBar toolBar = (ToolBarroot;
1530             for (Node child : toolBar.getItems()) {
1531                 Object found = findElement(child, predicate);
1532                 if (found != null) { return found; }
1533             }
1534         else if (root instanceof TableView) {
1535             TableView tableView = (TableViewroot;
1536             Node placeholder = tableView.getPlaceholder();
1537             if (placeholder != null) {
1538                 Object found = findElement(placeholder, predicate);
1539                 if (found != null) { return found; }
1540             }
1541             for (Object child : tableView.getColumns()) {
1542                 Object found = findElement(child, predicate);
1543                 if (found != null) { return found; }
1544             }
1545         }
1546 
1547         if (root instanceof Parent) {
1548             Parent parent = (Parentroot;
1549             for (Node child : parent.getChildrenUnmodifiable()) {
1550                 Object found = findElement(child, predicate);
1551                 if (found != null) { return found; }
1552             }
1553         }
1554 
1555         return null;
1556     }
1557 
1558     @Nonnull
1559     public static Collection<Object> findElements(@Nonnull Object root, @Nonnull Predicate<Object> predicate) {
1560         Set<Object> accumulator = new LinkedHashSet<>();
1561         findElements(root, predicate, accumulator);
1562         return accumulator;
1563     }
1564 
1565     private static void findElements(@Nonnull Object root, @Nonnull Predicate<Object> predicate, @Nonnull Collection<Object> accumulator) {
1566         requireNonNull(root, ERROR_ROOT_NULL);
1567         requireNonNull(predicate, ERROR_PREDICATE_NULL);
1568 
1569         if (predicate.test(root)) {
1570             accumulator.add(root);
1571         }
1572 
1573         if (root instanceof Control) {
1574             Control control = (Controlroot;
1575             ContextMenu contextMenu = control.getContextMenu();
1576             if (contextMenu != null) {
1577                 findElements(contextMenu, predicate, accumulator);
1578             }
1579             Tooltip tooltip = control.getTooltip();
1580             if (tooltip != null) {
1581                 findElements(tooltip, predicate, accumulator);
1582             }
1583         }
1584 
1585         if (root instanceof ButtonBar) {
1586             ButtonBar buttonBar = (ButtonBarroot;
1587             for (Node child : buttonBar.getButtons()) {
1588                 findElements(child, predicate, accumulator);
1589             }
1590         else if (root instanceof MenuBar) {
1591             MenuBar menuBar = (MenuBarroot;
1592             for (Menu child : menuBar.getMenus()) {
1593                 findElements(child, predicate, accumulator);
1594             }
1595         else if (root instanceof ContextMenu) {
1596             ContextMenu contextMenu = (ContextMenuroot;
1597             for (MenuItem child : contextMenu.getItems()) {
1598                 findElements(child, predicate, accumulator);
1599             }
1600         else if (root instanceof Menu) {
1601             Menu menu = (Menuroot;
1602             for (MenuItem child : menu.getItems()) {
1603                 findElements(child, predicate, accumulator);
1604             }
1605         else if (root instanceof TabPane) {
1606             TabPane tabPane = (TabPaneroot;
1607             for (Tab child : tabPane.getTabs()) {
1608                 findElements(child, predicate, accumulator);
1609             }
1610         else if (root instanceof Tab) {
1611             Tab tab = (Tabroot;
1612             if (tab.getContent() != null) {
1613                 findElements(tab.getContent(), predicate, accumulator);
1614             }
1615         else if (root instanceof TitledPane) {
1616             TitledPane parent = (TitledPaneroot;
1617             if (parent.getContent() != null) {
1618                 findElements(parent.getContent(), predicate, accumulator);
1619             }
1620         else if (root instanceof Accordion) {
1621             Accordion parent = (Accordionroot;
1622             for (TitledPane child : parent.getPanes()) {
1623                 findElements(child, predicate, accumulator);
1624             }
1625         else if (root instanceof SplitPane) {
1626             SplitPane parent = (SplitPaneroot;
1627             for (Node child : parent.getItems()) {
1628                 findElements(child, predicate, accumulator);
1629             }
1630         else if (root instanceof ScrollPane) {
1631             ScrollPane scrollPane = (ScrollPaneroot;
1632             if (scrollPane.getContent() != null) {
1633                 findElements(scrollPane.getContent(), predicate, accumulator);
1634             }
1635         else if (root instanceof ToolBar) {
1636             ToolBar toolBar = (ToolBarroot;
1637             for (Node child : toolBar.getItems()) {
1638                 findElements(child, predicate, accumulator);
1639             }
1640         else if (root instanceof TableView) {
1641             TableView tableView = (TableViewroot;
1642             Node placeholder = tableView.getPlaceholder();
1643             if (placeholder != null) {
1644                 findElements(placeholder, predicate, accumulator);
1645             }
1646             for (Object child : tableView.getColumns()) {
1647                 findElements(child, predicate, accumulator);
1648             }
1649         }
1650 
1651         if (root instanceof Parent) {
1652             Parent parent = (Parentroot;
1653             for (Node child : parent.getChildrenUnmodifiable()) {
1654                 findElements(child, predicate, accumulator);
1655             }
1656         }
1657     }
1658 
1659     @Nullable
1660     public static Window getWindowAncestor(@Nonnull Object node) {
1661         requireNonNull(node, ERROR_NODE_NULL);
1662 
1663         if (node instanceof Window) {
1664             return (Windownode;
1665         else if (node instanceof Scene) {
1666             return ((Scenenode).getWindow();
1667         else if (node instanceof Node) {
1668             Scene scene = ((Nodenode).getScene();
1669             if (scene != null) {
1670                 return scene.getWindow();
1671             }
1672         else if (node instanceof Tab) {
1673             TabPane tabPane = ((Tabnode).getTabPane();
1674             if (tabPane != null) {
1675                 return getWindowAncestor(tabPane);
1676             }
1677         }
1678 
1679         return null;
1680     }
1681 
1682     private static ValueConversionException illegalValue(Object value, Class<?> klass) {
1683         throw new ValueConversionException(value, klass);
1684     }
1685 
1686     private static ValueConversionException illegalValue(Object value, Class<?> klass, Exception e) {
1687         throw new ValueConversionException(value, klass, e);
1688     }
1689 }