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