JavaFXUtils.java
001 /*
002  * Copyright 2008-2015 the original author or authors.
003  *
004  * Licensed under the Apache License, Version 2.0 (the "License");
005  * you may not use this file except in compliance with the License.
006  * You may obtain a copy of the License at
007  *
008  *     http://www.apache.org/licenses/LICENSE-2.0
009  *
010  * Unless required by applicable law or agreed to in writing, software
011  * distributed under the License is distributed on an "AS IS" BASIS,
012  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013  * See the License for the specific language governing permissions and
014  * limitations under the License.
015  */
016 package griffon.javafx.support;
017 
018 import griffon.core.artifact.GriffonController;
019 import griffon.core.controller.Action;
020 import griffon.core.controller.ActionManager;
021 import griffon.core.editors.ValueConversionException;
022 import griffon.exceptions.InstanceMethodInvocationException;
023 import javafx.beans.property.Property;
024 import javafx.collections.ObservableList;
025 import javafx.event.ActionEvent;
026 import javafx.scene.Node;
027 import javafx.scene.Parent;
028 import javafx.scene.Scene;
029 import javafx.scene.control.Accordion;
030 import javafx.scene.control.ButtonBase;
031 import javafx.scene.control.CheckBox;
032 import javafx.scene.control.CheckMenuItem;
033 import javafx.scene.control.ContextMenu;
034 import javafx.scene.control.Control;
035 import javafx.scene.control.Labeled;
036 import javafx.scene.control.Menu;
037 import javafx.scene.control.MenuBar;
038 import javafx.scene.control.MenuItem;
039 import javafx.scene.control.RadioButton;
040 import javafx.scene.control.RadioMenuItem;
041 import javafx.scene.control.ScrollPane;
042 import javafx.scene.control.SplitPane;
043 import javafx.scene.control.Tab;
044 import javafx.scene.control.TabPane;
045 import javafx.scene.control.TitledPane;
046 import javafx.scene.control.ToggleButton;
047 import javafx.scene.control.ToolBar;
048 import javafx.scene.control.Tooltip;
049 import javafx.scene.image.Image;
050 import javafx.scene.image.ImageView;
051 import javafx.stage.Window;
052 
053 import javax.annotation.Nonnull;
054 import javax.annotation.Nullable;
055 import java.lang.reflect.Constructor;
056 import java.lang.reflect.InvocationTargetException;
057 import java.net.URL;
058 import java.util.LinkedHashSet;
059 import java.util.Map;
060 import java.util.Set;
061 
062 import static griffon.util.GriffonClassUtils.getGetterName;
063 import static griffon.util.GriffonClassUtils.getPropertyValue;
064 import static griffon.util.GriffonClassUtils.invokeExactInstanceMethod;
065 import static griffon.util.GriffonClassUtils.invokeInstanceMethod;
066 import static griffon.util.GriffonNameUtils.isBlank;
067 import static griffon.util.GriffonNameUtils.requireNonBlank;
068 import static java.util.Objects.requireNonNull;
069 
070 /**
071  @author Andres Almiray
072  */
073 public final class JavaFXUtils {
074     private static final String ERROR_NODE_NULL = "Argument 'node' must not be null";
075     private static final String ERROR_CONTROL_NULL = "Argument 'control' must not be null";
076     private static final String ERROR_ACTION_NULL = "Argument 'action' must not be null";
077     private static final String ERROR_ICON_BLANK = "Argument 'iconUrl' must not be blank";
078     private static final String ERROR_ID_BLANK = "Argument 'id' must not be blank";
079     private static final String ERROR_URL_BLANK = "Argument 'url' must not be blank";
080     private static final String ERROR_ROOT_NULL = "Argument 'root' must not be null";
081     private static final String ERROR_CONTROLLER_NULL = "Argument 'controller' must not be null";
082     private static final String ACTION_TARGET_SUFFIX = "ActionTarget";
083     private static final String PROPERTY_SUFFIX = "Property";
084 
085     private JavaFXUtils() {
086 
087     }
088 
089     @Nonnull
090     @SuppressWarnings("ConstantConditions")
091     public static <B> Property<?> extractProperty(@Nonnull B bean, @Nonnull String propertyName) {
092         requireNonNull(bean, "Argument 'bean' must not be null");
093         requireNonBlank(propertyName, "Argument 'propertyName' must not be null");
094 
095         if (!propertyName.endsWith(PROPERTY_SUFFIX)) {
096             propertyName += PROPERTY_SUFFIX;
097         }
098 
099         InstanceMethodInvocationException imie;
100         try {
101             // 1. try <columnName>Property() first
102             return (Property<?>invokeExactInstanceMethod(bean, propertyName);
103         catch (InstanceMethodInvocationException e) {
104             imie = e;
105         }
106 
107         // 2. fallback to get<columnName>Property()
108         try {
109             return (Property<?>invokeExactInstanceMethod(bean, getGetterName(propertyName));
110         catch (InstanceMethodInvocationException e) {
111             throw imie;
112         }
113     }
114 
115     public static void connectActions(@Nonnull Object node, @Nonnull GriffonController controller) {
116         requireNonNull(node, ERROR_NODE_NULL);
117         requireNonNull(controller, ERROR_CONTROLLER_NULL);
118         ActionManager actionManager = controller.getApplication().getActionManager();
119         for (Map.Entry<String, Action> e : actionManager.actionsFor(controller).entrySet()) {
120             String actionTargetName = actionManager.normalizeName(e.getKey()) + ACTION_TARGET_SUFFIX;
121             Object control = findElement(node, actionTargetName);
122             if (control == nullcontinue;
123             JavaFXAction action = (JavaFXActione.getValue().getToolkitAction();
124 
125             if (control instanceof ButtonBase) {
126                 configure(((ButtonBasecontrol), action);
127             else if (control instanceof MenuItem) {
128                 JavaFXUtils.configure(((MenuItemcontrol), action);
129             else if (control instanceof Node) {
130                 ((Nodecontrol).addEventHandler(ActionEvent.ACTION, action.getOnAction());
131             else {
132                 // does it support the onAction property?
133                 try {
134                     invokeInstanceMethod(control, "setOnAction", action.getOnAction());
135                 catch (InstanceMethodInvocationException imie) {
136                     // ignore
137                 }
138             }
139         }
140     }
141 
142     public static void configure(final @Nonnull ToggleButton control, final @Nonnull JavaFXAction action) {
143         configure((ButtonBasecontrol, action);
144 
145         action.selectedProperty().addListener((observableValue, oldValue, newValue-> control.setSelected(newValue));
146         control.setSelected(action.isSelected());
147     }
148 
149     public static void configure(final @Nonnull CheckBox control, final @Nonnull JavaFXAction action) {
150         configure((ButtonBasecontrol, action);
151 
152         action.selectedProperty().addListener((observableValue, oldValue, newValue-> control.setSelected(newValue));
153         control.setSelected(action.isSelected());
154     }
155 
156     public static void configure(final @Nonnull RadioButton control, final @Nonnull JavaFXAction action) {
157         configure((ButtonBasecontrol, action);
158 
159         action.selectedProperty().addListener((observableValue, oldValue, newValue-> control.setSelected(newValue));
160         control.setSelected(action.isSelected());
161     }
162 
163     public static void configure(final @Nonnull ButtonBase control, final @Nonnull JavaFXAction action) {
164         requireNonNull(control, ERROR_CONTROL_NULL);
165         requireNonNull(action, ERROR_ACTION_NULL);
166 
167         action.onActionProperty().addListener((observableValue, oldValue, newValue-> control.setOnAction(newValue));
168         control.setOnAction(action.getOnAction());
169 
170         action.nameProperty().addListener((observableValue, oldValue, newValue-> control.setText(newValue));
171         control.setText(action.getName());
172 
173         action.descriptionProperty().addListener((observableValue, oldValue, newValue-> setTooltip(control, newValue));
174         setTooltip(control, action.getDescription());
175 
176         action.iconProperty().addListener((observableValue, oldValue, newValue-> setIcon(control, newValue));
177         if (!isBlank(action.getIcon())) {
178             setIcon(control, action.getIcon());
179         }
180 
181         action.imageProperty().addListener((observableValue, oldValue, newValue-> setGraphic(control, newValue));
182         if (null != action.getImage()) {
183             setGraphic(control, action.getImage());
184         }
185 
186         action.graphicProperty().addListener((observableValue, oldValue, newValue-> setGraphic(control, newValue));
187         if (null != action.getGraphic()) {
188             setGraphic(control, action.getGraphic());
189         }
190 
191         action.enabledProperty().addListener((observableValue, oldValue, newValue-> control.setDisable(!newValue));
192         control.setDisable(!action.isEnabled());
193 
194         action.visibleProperty().addListener((observableValue, oldValue, newValue-> control.setVisible(newValue));
195         control.setVisible(action.isVisible());
196 
197         action.styleClassProperty().addListener((observableValue, oldValue, newValue-> {
198             setStyleClass(control, oldValue, true);
199             setStyleClass(control, newValue);
200         });
201         setStyleClass(control, action.getStyleClass());
202     }
203 
204     public static void configure(final @Nonnull CheckMenuItem control, final @Nonnull JavaFXAction action) {
205         configure((MenuItemcontrol, action);
206 
207         action.selectedProperty().addListener((observableValue, oldValue, newValue-> control.setSelected(newValue));
208         control.setSelected(action.isSelected());
209     }
210 
211     public static void configure(final @Nonnull RadioMenuItem control, final @Nonnull JavaFXAction action) {
212         configure((MenuItemcontrol, action);
213 
214         action.selectedProperty().addListener((observableValue, oldValue, newValue-> control.setSelected(newValue));
215         control.setSelected(action.isSelected());
216     }
217 
218     public static void configure(final @Nonnull MenuItem control, final @Nonnull JavaFXAction action) {
219         requireNonNull(control, ERROR_CONTROL_NULL);
220         requireNonNull(action, ERROR_ACTION_NULL);
221 
222         action.onActionProperty().addListener((observableValue, oldValue, newValue-> control.setOnAction(newValue));
223         control.setOnAction(action.getOnAction());
224 
225         action.nameProperty().addListener((observableValue, oldValue, newValue-> control.setText(newValue));
226         control.setText(action.getName());
227 
228         action.iconProperty().addListener((observableValue, oldValue, newValue-> setIcon(control, newValue));
229         if (!isBlank(action.getIcon())) {
230             setIcon(control, action.getIcon());
231         }
232 
233         action.imageProperty().addListener((observableValue, oldValue, newValue-> setGraphic(control, newValue));
234         if (null != action.getImage()) {
235             setGraphic(control, action.getImage());
236         }
237 
238         action.graphicProperty().addListener((observableValue, oldValue, newValue-> setGraphic(control, newValue));
239         if (null != action.getGraphic()) {
240             setGraphic(control, action.getGraphic());
241         }
242 
243         action.enabledProperty().addListener((observableValue, oldValue, newValue-> control.setDisable(!newValue));
244         control.setDisable(!action.getEnabled());
245 
246         action.acceleratorProperty().addListener((observableValue, oldValue, newValue-> control.setAccelerator(newValue));
247         control.setAccelerator(action.getAccelerator());
248 
249         action.visibleProperty().addListener((observableValue, oldValue, newValue-> control.setVisible(newValue));
250         control.setVisible(action.isVisible());
251 
252         action.styleClassProperty().addListener((observableValue, oldValue, newValue-> {
253             setStyleClass(control, oldValue, true);
254             setStyleClass(control, newValue);
255         });
256         setStyleClass(control, action.getStyleClass());
257     }
258 
259     public static void setStyleClass(@Nonnull Node node, @Nonnull String styleClass) {
260         setStyleClass(node, styleClass, false);
261     }
262 
263     public static void setStyleClass(@Nonnull Node node, @Nonnull String styleClass, boolean remove) {
264         requireNonNull(node, ERROR_CONTROL_NULL);
265         if (isBlank(styleClass)) return;
266 
267         ObservableList<String> styleClasses = node.getStyleClass();
268         applyStyleClass(styleClass, styleClasses, remove);
269     }
270 
271     public static void setStyleClass(@Nonnull MenuItem node, @Nonnull String styleClass) {
272         setStyleClass(node, styleClass, false);
273     }
274 
275     public static void setStyleClass(@Nonnull MenuItem node, @Nonnull String styleClass, boolean remove) {
276         requireNonNull(node, ERROR_CONTROL_NULL);
277         if (isBlank(styleClass)) return;
278         ObservableList<String> styleClasses = node.getStyleClass();
279         applyStyleClass(styleClass, styleClasses, remove);
280     }
281 
282     private static void applyStyleClass(String styleClass, ObservableList<String> styleClasses, boolean remove) {
283         String[] strings = styleClass.split("[,\\ ]");
284         if (remove) {
285             styleClasses.removeAll(strings);
286         else {
287             Set<String> classes = new LinkedHashSet<>(styleClasses);
288             for (String s : strings) {
289                 if (isBlank(s)) continue;
290                 classes.add(s.trim());
291             }
292             styleClasses.setAll(classes);
293         }
294     }
295 
296     public static void setTooltip(@Nonnull Control control, @Nullable String text) {
297         if (isBlank(text)) {
298             return;
299         }
300         requireNonNull(control, ERROR_CONTROL_NULL);
301 
302         Tooltip tooltip = control.tooltipProperty().get();
303         if (tooltip == null) {
304             tooltip = new Tooltip();
305             control.tooltipProperty().set(tooltip);
306         }
307         tooltip.setText(text);
308     }
309 
310     public static void setIcon(@Nonnull Labeled control, @Nonnull String iconUrl) {
311         requireNonNull(control, ERROR_CONTROL_NULL);
312         requireNonBlank(iconUrl, ERROR_ICON_BLANK);
313 
314         Node graphicNode = resolveIcon(iconUrl);
315         if (graphicNode != null) {
316             control.graphicProperty().set(graphicNode);
317         }
318     }
319 
320     public static void setIcon(@Nonnull MenuItem control, @Nonnull String iconUrl) {
321         requireNonNull(control, ERROR_CONTROL_NULL);
322         requireNonBlank(iconUrl, ERROR_ICON_BLANK);
323 
324         Node graphicNode = resolveIcon(iconUrl);
325         if (graphicNode != null) {
326             control.graphicProperty().set(graphicNode);
327         }
328     }
329 
330     public static void setGraphic(@Nonnull Labeled control, @Nullable Image graphic) {
331         requireNonNull(control, ERROR_CONTROL_NULL);
332 
333         if (graphic != null) {
334             Node graphicNode = new ImageView(graphic);
335             control.graphicProperty().set(graphicNode);
336         else {
337             control.graphicProperty().set(null);
338         }
339     }
340 
341     public static void setGraphic(@Nonnull MenuItem control, @Nullable Image graphic) {
342         requireNonNull(control, ERROR_CONTROL_NULL);
343 
344         if (graphic != null) {
345             Node graphicNode = new ImageView(graphic);
346             control.graphicProperty().set(graphicNode);
347         else {
348             control.graphicProperty().set(null);
349         }
350     }
351 
352     public static void setGraphic(@Nonnull Labeled control, @Nullable Node graphic) {
353         requireNonNull(control, ERROR_CONTROL_NULL);
354 
355         if (graphic != null) {
356             control.graphicProperty().set(graphic);
357         else {
358             control.graphicProperty().set(null);
359         }
360     }
361 
362     public static void setGraphic(@Nonnull MenuItem control, @Nullable Node graphic) {
363         requireNonNull(control, ERROR_CONTROL_NULL);
364 
365         if (graphic != null) {
366             control.graphicProperty().set(graphic);
367         else {
368             control.graphicProperty().set(null);
369         }
370     }
371 
372     @Nullable
373     public static Node resolveIcon(@Nonnull String iconUrl) {
374         requireNonBlank(iconUrl, ERROR_URL_BLANK);
375 
376         if (iconUrl.contains("|")) {
377             // assume classname|arg format
378             return handleAsClassWithArg(iconUrl);
379         else {
380             URL resource = Thread.currentThread().getContextClassLoader().getResource(iconUrl);
381             if (resource != null) {
382                 return new ImageView(new Image(resource.toString()));
383             }
384         }
385         return null;
386     }
387 
388     @SuppressWarnings("unchecked")
389     private static Node handleAsClassWithArg(String str) {
390         String[] args = str.split("\\|");
391         if (args.length == 2) {
392             Class<?> iconClass = null;
393             try {
394                 iconClass = (Class<?>JavaFXUtils.class.getClassLoader().loadClass(args[0]);
395             catch (ClassNotFoundException e) {
396                 throw illegalValue(str, Node.class, e);
397             }
398 
399             Constructor<?> constructor = null;
400             try {
401                 constructor = iconClass.getConstructor(String.class);
402             catch (NoSuchMethodException e) {
403                 throw illegalValue(str, Node.class, e);
404             }
405 
406             try {
407                 Object o = constructor.newInstance(args[1]);
408                 if (instanceof Node) {
409                     return (Nodeo;
410                 else if (instanceof Image) {
411                     return new ImageView((Imageo);
412                 else {
413                     throw illegalValue(str, Node.class);
414                 }
415             catch (InstantiationException | InvocationTargetException | IllegalAccessException e) {
416                 throw illegalValue(str, Node.class, e);
417             }
418         else {
419             throw illegalValue(str, Node.class);
420         }
421     }
422 
423     @Nullable
424     public static Node findNode(@Nonnull Node root, @Nonnull String id) {
425         requireNonNull(root, ERROR_ROOT_NULL);
426         requireNonBlank(id, ERROR_ID_BLANK);
427 
428         if (id.equals(root.getId())) return root;
429 
430         if (root instanceof TabPane) {
431             TabPane parent = (TabPaneroot;
432             for (Tab child : parent.getTabs()) {
433                 if (child.getContent() != null) {
434                     Node found = findNode(child.getContent(), id);
435                     if (found != nullreturn found;
436                 }
437             }
438         else if (root instanceof TitledPane) {
439             TitledPane parent = (TitledPaneroot;
440             if (parent.getContent() != null) {
441                 Node found = findNode(parent.getContent(), id);
442                 if (found != nullreturn found;
443             }
444         else if (root instanceof Accordion) {
445             Accordion parent = (Accordionroot;
446             for (TitledPane child : parent.getPanes()) {
447                 Node found = findNode(child, id);
448                 if (found != nullreturn found;
449             }
450         else if (root instanceof SplitPane) {
451             SplitPane parent = (SplitPaneroot;
452             for (Node child : parent.getItems()) {
453                 Node found = findNode(child, id);
454                 if (found != nullreturn found;
455             }
456         else if (root instanceof ScrollPane) {
457             ScrollPane scrollPane = (ScrollPaneroot;
458             if (scrollPane.getContent() != null) {
459                 Node found = findNode(scrollPane.getContent(), id);
460                 if (found != nullreturn found;
461             }
462         else if (root instanceof ToolBar) {
463             ToolBar toolBar = (ToolBarroot;
464             for (Node child : toolBar.getItems()) {
465                 Node found = findNode(child, id);
466                 if (found != nullreturn found;
467             }
468         else if (root instanceof Parent) {
469             Parent parent = (Parentroot;
470             for (Node child : parent.getChildrenUnmodifiable()) {
471                 Node found = findNode(child, id);
472                 if (found != nullreturn found;
473             }
474         }
475 
476         return null;
477     }
478 
479     @Nullable
480     public static Object findElement(@Nonnull Object root, @Nonnull String id) {
481         requireNonNull(root, ERROR_ROOT_NULL);
482         requireNonBlank(id, ERROR_ID_BLANK);
483 
484         if (id.equals(getPropertyValue(root, "id"))) return root;
485 
486         if (root instanceof MenuBar) {
487             MenuBar menuBar = (MenuBarroot;
488             for (Menu child : menuBar.getMenus()) {
489                 Object found = findElement(child, id);
490                 if (found != nullreturn found;
491             }
492         }
493         if (root instanceof ContextMenu) {
494             ContextMenu contextMenu = (ContextMenuroot;
495             for (MenuItem child : contextMenu.getItems()) {
496                 Object found = findElement(child, id);
497                 if (found != nullreturn found;
498             }
499         else if (root instanceof Menu) {
500             Menu menu = (Menuroot;
501             for (MenuItem child : menu.getItems()) {
502                 Object found = findElement(child, id);
503                 if (found != nullreturn found;
504             }
505         else if (root instanceof TabPane) {
506             TabPane tabPane = (TabPaneroot;
507             for (Tab child : tabPane.getTabs()) {
508                 Object found = findElement(child, id);
509                 if (found != nullreturn found;
510             }
511         else if (root instanceof Tab) {
512             Tab tab = (Tabroot;
513             if (tab.getContent() != null) {
514                 Object found = findElement(tab.getContent(), id);
515                 if (found != nullreturn found;
516             }
517         else if (root instanceof TitledPane) {
518             TitledPane parent = (TitledPaneroot;
519             if (parent.getContent() != null) {
520                 Object found = findElement(parent.getContent(), id);
521                 if (found != nullreturn found;
522             }
523         else if (root instanceof Accordion) {
524             Accordion parent = (Accordionroot;
525             for (TitledPane child : parent.getPanes()) {
526                 Object found = findElement(child, id);
527                 if (found != nullreturn found;
528             }
529         else if (root instanceof SplitPane) {
530             SplitPane parent = (SplitPaneroot;
531             for (Node child : parent.getItems()) {
532                 Object found = findElement(child, id);
533                 if (found != nullreturn found;
534             }
535         else if (root instanceof ScrollPane) {
536             ScrollPane scrollPane = (ScrollPaneroot;
537             if (scrollPane.getContent() != null) {
538                 Object found = findElement(scrollPane.getContent(), id);
539                 if (found != nullreturn found;
540             }
541         else if (root instanceof ToolBar) {
542             ToolBar toolBar = (ToolBarroot;
543             for (Node child : toolBar.getItems()) {
544                 Node found = findNode(child, id);
545                 if (found != nullreturn found;
546             }
547         else if (root instanceof Parent) {
548             Parent parent = (Parentroot;
549             for (Node child : parent.getChildrenUnmodifiable()) {
550                 Object found = findElement(child, id);
551                 if (found != nullreturn found;
552             }
553         }
554 
555         return null;
556     }
557 
558     @Nullable
559     public static Window getWindowAncestor(@Nonnull Object node) {
560         requireNonNull(node, ERROR_NODE_NULL);
561 
562         if (node instanceof Window) {
563             return (Windownode;
564         else if (node instanceof Scene) {
565             return ((Scenenode).getWindow();
566         else if (node instanceof Node) {
567             Scene scene = ((Nodenode).getScene();
568             if (scene != null) {
569                 return scene.getWindow();
570             }
571         else if (node instanceof Tab) {
572             TabPane tabPane = ((Tabnode).getTabPane();
573             if (tabPane != null) {
574                 return getWindowAncestor(tabPane);
575             }
576         }
577 
578         return null;
579     }
580 
581     private static ValueConversionException illegalValue(Object value, Class<?> klass) {
582         throw new ValueConversionException(value, klass);
583     }
584 
585     private static ValueConversionException illegalValue(Object value, Class<?> klass, Exception e) {
586         throw new ValueConversionException(value, klass, e);
587     }
588 }