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