1 /*
   2  * Copyright (c) 2008, 2016, Oracle and/or its affiliates.
   3  * All rights reserved. Use is subject to license terms.
   4  *
   5  * This file is available and licensed under the following license:
   6  *
   7  * Redistribution and use in source and binary forms, with or without
   8  * modification, are permitted provided that the following conditions
   9  * are met:
  10  *
  11  *  - Redistributions of source code must retain the above copyright
  12  *    notice, this list of conditions and the following disclaimer.
  13  *  - Redistributions in binary form must reproduce the above copyright
  14  *    notice, this list of conditions and the following disclaimer in
  15  *    the documentation and/or other materials provided with the distribution.
  16  *  - Neither the name of Oracle Corporation nor the names of its
  17  *    contributors may be used to endorse or promote products derived
  18  *    from this software without specific prior written permission.
  19  *
  20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  23  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  24  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  25  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  26  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  27  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  28  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  29  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  30  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  31  */
  32 package ensemble.samplepage;
  33 
  34 import ensemble.SampleInfo;
  35 import ensemble.playground.PlaygroundProperty;
  36 import java.lang.reflect.Field;
  37 import java.lang.reflect.InvocationTargetException;
  38 import java.text.DecimalFormat;
  39 import java.util.logging.Level;
  40 import java.util.logging.Logger;
  41 import javafx.beans.InvalidationListener;
  42 import javafx.beans.Observable;
  43 import javafx.beans.property.BooleanProperty;
  44 import javafx.beans.property.DoubleProperty;
  45 import javafx.beans.property.IntegerProperty;
  46 import javafx.beans.property.ObjectProperty;
  47 import javafx.beans.property.Property;
  48 import javafx.beans.property.StringProperty;
  49 import javafx.beans.value.ChangeListener;
  50 import javafx.beans.value.ObservableValue;
  51 import javafx.collections.FXCollections;
  52 import javafx.collections.ObservableList;
  53 import javafx.geometry.Insets;
  54 import javafx.geometry.Orientation;
  55 import javafx.geometry.Pos;
  56 import javafx.scene.Node;
  57 import javafx.scene.chart.PieChart;
  58 import javafx.scene.chart.XYChart;
  59 import javafx.scene.control.CheckBox;
  60 import javafx.scene.control.ChoiceBox;
  61 import javafx.scene.control.ComboBox;
  62 import javafx.scene.control.ContentDisplay;
  63 import javafx.scene.control.Label;
  64 import javafx.scene.control.OverrunStyle;
  65 import javafx.scene.control.ScrollPane;
  66 import javafx.scene.control.Separator;
  67 import javafx.scene.control.Slider;
  68 import javafx.scene.control.Tab;
  69 import javafx.scene.control.TabPane;
  70 import javafx.scene.control.TextField;
  71 import javafx.scene.layout.GridPane;
  72 import javafx.scene.layout.HBox;
  73 import javafx.scene.layout.Priority;
  74 import javafx.scene.layout.Region;
  75 import javafx.scene.paint.Color;
  76 import javafx.scene.paint.Paint;
  77 import javafx.scene.shape.Rectangle;
  78 import javafx.util.Callback;
  79 import javafx.util.StringConverter;
  80 
  81 /**
  82  *
  83  */
  84 class PlaygroundTabs extends TabPane {
  85     private final SamplePage samplePage;
  86     private final GridPane grid;
  87     private final Tab propertiesTab;
  88     private final Tab dataTab;
  89 
  90     PlaygroundTabs(final SamplePage samplePage) {
  91         this.samplePage = samplePage;
  92         grid = new GridPane();
  93         grid.setHgap(SamplePage.INDENT);
  94         grid.setVgap(SamplePage.INDENT);
  95         grid.setPadding(new Insets(SamplePage.INDENT));
  96         getStyleClass().add("floating");
  97         ScrollPane scrollPane = new ScrollPane(grid);
  98         scrollPane.getStyleClass().clear();
  99         setTabClosingPolicy(TabClosingPolicy.UNAVAILABLE);
 100         propertiesTab = new Tab("Properties");
 101         propertiesTab.setContent(scrollPane);
 102         dataTab = new Tab("Data");
 103         getTabs().addAll(propertiesTab, dataTab);
 104         setMinSize(100, 100);
 105         samplePage.registerSampleInfoUpdater(new Callback<SampleInfo, Void>() {
 106 
 107             @Override
 108             public Void call(SampleInfo sampleInfo) {
 109                 update(sampleInfo);
 110                 return null;
 111             }
 112         });
 113     }
 114 
 115     private PropertyController newPropertyController(PlaygroundProperty playgroundProperty, Object object, Object property) {
 116         if (playgroundProperty.propertyName.equals("getStrokeDashArray")) {
 117             return new StrokeDashArrayPropertyController(playgroundProperty, object, (ObservableList<Double>) property);
 118         }
 119         if (property instanceof DoubleProperty) {
 120             DoubleProperty prop = (DoubleProperty) property;
 121             return new DoublePropertyController(playgroundProperty, object, prop);
 122         } else if (property instanceof IntegerProperty) {
 123             IntegerProperty prop = (IntegerProperty) property;
 124             return new IntegerPropertyController(playgroundProperty, object, prop);
 125         } else if (property instanceof BooleanProperty) {
 126             BooleanProperty prop = (BooleanProperty) property;
 127             return new BooleanPropertyController(playgroundProperty, object, prop);
 128         } else if (property instanceof StringProperty) {
 129             StringProperty prop = (StringProperty) property;
 130             return new StringPropertyController(playgroundProperty, object, prop);
 131         } else if (property instanceof ObjectProperty) {
 132             final ObjectProperty prop = (ObjectProperty) property;
 133             if (prop.get() instanceof Color) {
 134                 return new ColorPropertyController(playgroundProperty, object, prop);
 135             }
 136             if (prop.get() instanceof String) {
 137                 return new StringPropertyController(playgroundProperty, object, prop);
 138             }
 139             if (prop.get() != null && prop.get().getClass().isEnum()) {
 140                 return new EnumPropertyController(playgroundProperty, object, prop, (Enum) prop.get());
 141             }
 142         }
 143         return null;
 144     }
 145 
 146     private void update(SampleInfo sampleInfo) {
 147         grid.getChildren().clear();
 148         int rowIndex = 0;
 149         boolean needsDataTab = false;
 150         for (PlaygroundProperty prop : sampleInfo.playgroundProperties) {
 151             try {
 152                 Object object = samplePage.sampleRuntimeInfoProperty.get().getApp();
 153                 if (prop.fieldName != null) {
 154                     Field declaredField = samplePage.sampleRuntimeInfoProperty.get().getClz().getDeclaredField(prop.fieldName);
 155                     declaredField.setAccessible(true);
 156                     object = declaredField.get(object);
 157                 }
 158                 Object property = null;
 159                 if (prop.propertyName.startsWith("-")) {
 160                     Label sectionLabel = new Label(prop.properties.get("name"));
 161                     Separator separator1 = new Separator(Orientation.HORIZONTAL);
 162                     HBox.setHgrow(separator1, Priority.ALWAYS);
 163                     Separator separator2 = new Separator(Orientation.HORIZONTAL);
 164                     HBox.setHgrow(separator2, Priority.ALWAYS);
 165                     HBox separator = new HBox(separator1, sectionLabel, separator2);
 166                     separator.setAlignment(Pos.CENTER);
 167                     grid.addRow(rowIndex++, separator);
 168                     GridPane.setColumnSpan(separator, 3);
 169                     if (rowIndex > 1) {
 170                         GridPane.setMargin(separator, new Insets(15, 0, 0, 0));
 171                     }
 172                     continue;
 173                 }
 174                 if (prop.propertyName.startsWith("get")) {
 175                     property = object.getClass().getMethod(prop.propertyName).invoke(object);
 176                 } else {
 177                     property = object.getClass().getMethod(prop.propertyName + "Property").invoke(object);
 178                 }
 179                 if (object instanceof XYChart && prop.propertyName.equals("data")) {
 180                     needsDataTab = true;
 181                     dataTab.setContent(new XYDataVisualizer((XYChart) object));
 182                 } else if (object instanceof PieChart && prop.propertyName.equals("data")) {
 183                     needsDataTab = true;
 184                     dataTab.setContent(new PieChartDataVisualizer((PieChart) object));
 185                 } else {
 186                     PropertyController controller = newPropertyController(prop, object, property);
 187                     if (controller != null) {
 188                         Region controllerNode = controller.getController();
 189                         grid.addRow(rowIndex++, controller.getLabel(), controllerNode, controller.getPreview());
 190                         controllerNode.maxWidthProperty().bind(widthProperty());
 191                         GridPane.setHgrow(controllerNode, Priority.ALWAYS);
 192                     } else {
 193                         System.err.println("Warning! The following property doesn't have corresponding controller: " + prop);
 194                     }
 195                 }
 196             } catch (InvocationTargetException ex) {
 197                 Logger.getLogger(SamplePage.class.getName()).log(Level.SEVERE, null, ex);
 198             } catch (NoSuchMethodException ex) {
 199                 Logger.getLogger(SamplePage.class.getName()).log(Level.SEVERE, null, ex);
 200             } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException ex) {
 201                 Logger.getLogger(SamplePage.class.getName()).log(Level.SEVERE, null, ex);
 202             }
 203         }
 204         if (needsDataTab && !getTabs().contains(dataTab)) {
 205             getTabs().add(dataTab);
 206         }
 207         if (!needsDataTab) {
 208             getTabs().remove(dataTab);
 209         }
 210     }
 211 
 212     private class PropertyController {
 213 
 214         private PlaygroundProperty playgroundProperty;
 215         private String name;
 216         private Label label;
 217         private Region controller;
 218         private Node preview;
 219 
 220         public PropertyController(PlaygroundProperty playgroundProperty) {
 221             this(playgroundProperty, playgroundProperty.propertyName);
 222         }
 223 
 224         public PropertyController(PlaygroundProperty playgroundProperty, String name) {
 225             if (playgroundProperty.properties.containsKey("name")) {
 226                 this.name = playgroundProperty.properties.get("name");
 227             } else {
 228                 this.name = name;
 229             }
 230             this.playgroundProperty = playgroundProperty;
 231         }
 232 
 233         public Region getLabel() {
 234             if (label == null) {
 235                 label = new Label(name);
 236                 label.setAlignment(Pos.BASELINE_RIGHT);
 237                 label.setLabelFor(getController());
 238                 label.setTextOverrun(OverrunStyle.ELLIPSIS);
 239                 label.setMaxWidth(200);
 240             }
 241             return label;
 242         }
 243 
 244         protected void setController(Region controller) {
 245             this.controller = controller;
 246         }
 247 
 248         protected void setPreview(Node preview) {
 249             this.preview = preview;
 250         }
 251 
 252         public Region getController() {
 253             if (controller == null) {
 254                 controller = new Region();
 255             }
 256             return controller;
 257         }
 258 
 259         public Node getPreview() {
 260             if (preview == null) {
 261                 preview = new Region();
 262             }
 263             return preview;
 264         }
 265 
 266         protected double getProperty(PlaygroundProperty playgroundProperty, String name, double defaultValue) throws NumberFormatException {
 267             String value = playgroundProperty.properties.get(name);
 268             if (value == null) {
 269                 return defaultValue;
 270             } else {
 271                 return Double.parseDouble(value);
 272             }
 273         }
 274     }
 275 
 276     private class DoublePropertyController extends PropertyController {
 277         public DoublePropertyController(PlaygroundProperty playgroundProperty, Object object, Property<Number> prop) {
 278             super(playgroundProperty);
 279             Slider slider = new Slider();
 280             slider.setMin(getProperty(playgroundProperty, "min", 0));
 281             slider.setMax(getProperty(playgroundProperty, "max", 100));
 282             double step = getProperty(playgroundProperty, "step", 0);
 283             if (step > 0) {
 284                 slider.setMajorTickUnit(step);
 285                 slider.setMinorTickCount(0);
 286                 slider.setSnapToTicks(true);
 287             }
 288             slider.valueProperty().bindBidirectional(prop);
 289             setController(slider);
 290 
 291             TextField preview = new TextField();
 292             preview.setPrefColumnCount(4);
 293             preview.textProperty().bindBidirectional(prop, new StringConverter<Number>() {
 294 
 295                 @Override
 296                 public String toString(Number number) {
 297                     return DecimalFormat.getInstance().format((Double) number);
 298                 }
 299 
 300                 @Override
 301                 public Number fromString(String string) {
 302                     try {
 303                         Number number = DecimalFormat.getInstance().parse(string);
 304                         return number;
 305                     } catch (Exception e) {
 306                         return 0;
 307                     }
 308                 }
 309             });
 310             setPreview(preview);
 311         }
 312     }
 313 
 314     private class IntegerPropertyController extends PropertyController {
 315         public IntegerPropertyController(PlaygroundProperty playgroundProperty, Object object, Property<Number> prop) {
 316             super(playgroundProperty);
 317             Slider slider = new Slider();
 318             slider.setMin(getProperty(playgroundProperty, "min", 0));
 319             slider.setMax(getProperty(playgroundProperty, "max", 100));
 320             slider.setSnapToTicks(true);
 321             slider.setMajorTickUnit(1);
 322             slider.valueProperty().bindBidirectional(prop);
 323             setController(slider);
 324 
 325             TextField preview = new TextField();
 326             preview.setPrefWidth(30);
 327             preview.textProperty().bindBidirectional(prop, new StringConverter<Number>() {
 328 
 329                 @Override
 330                 public String toString(Number number) {
 331                     return DecimalFormat.getInstance().format((Integer) number);
 332                 }
 333 
 334                 @Override
 335                 public Number fromString(String string) {
 336                     try {
 337                         Number number = DecimalFormat.getInstance().parse(string);
 338                         return number;
 339                     } catch (Exception e) {
 340                         return 0;
 341                     }
 342                 }
 343             });
 344             setPreview(preview);
 345         }
 346     }
 347 
 348     private class BooleanPropertyController extends PropertyController {
 349         public BooleanPropertyController(PlaygroundProperty playgroundProperty, Object object, Property<Boolean> prop) {
 350             super(playgroundProperty);
 351             CheckBox checkbox = new CheckBox();
 352             checkbox.selectedProperty().bindBidirectional(prop);
 353             setController(checkbox);
 354         }
 355     }
 356 
 357     private class StringPropertyController extends PropertyController {
 358 
 359         public StringPropertyController(PlaygroundProperty playgroundProperty, Object object, Property<String> prop) {
 360             super(playgroundProperty);
 361             TextField textField = new TextField();
 362             textField.textProperty().bindBidirectional(prop);
 363             setController(textField);
 364         }
 365     }
 366 
 367     private class ColorPropertyController extends PropertyController {
 368 
 369         public ColorPropertyController(PlaygroundProperty playgroundProperty, Object object, final Property<Paint> prop) {
 370             super(playgroundProperty);
 371 
 372             final Rectangle colorRect = new Rectangle(20, 20, (Color) prop.getValue());
 373             colorRect.setStroke(Color.GRAY);
 374             final Label valueLabel = new Label(formatWebColor((Color) prop.getValue()));
 375             valueLabel.setGraphic(colorRect);
 376             valueLabel.setContentDisplay(ContentDisplay.LEFT);
 377             setPreview(valueLabel);
 378 
 379             final SimpleHSBColorPicker colorPicker = new SimpleHSBColorPicker();
 380             colorPicker.getColor().addListener(new InvalidationListener() {
 381                 @Override
 382                 public void invalidated(Observable valueModel) {
 383                     Color c = colorPicker.getColor().get();
 384                     prop.setValue(c);
 385                     valueLabel.setText(formatWebColor(c));
 386                     colorRect.setFill(c);
 387                 }
 388             });
 389             setController(colorPicker);
 390         }
 391 
 392         private String formatWebColor(Color c) {
 393             String r = Integer.toHexString((int) (c.getRed() * 255));
 394             if (r.length() == 1) {
 395                 r = "0" + r;
 396             }
 397             String g = Integer.toHexString((int) (c.getGreen() * 255));
 398             if (g.length() == 1) {
 399                 g = "0" + g;
 400             }
 401             String b = Integer.toHexString((int) (c.getBlue() * 255));
 402             if (b.length() == 1) {
 403                 b = "0" + b;
 404             }
 405             return "#" + r + g + b;
 406         }
 407     }
 408 
 409     private class EnumPropertyController extends PropertyController {
 410 
 411         public EnumPropertyController(PlaygroundProperty playgroundProperty, Object object, Property prop, final Enum enumeration) {
 412             super(playgroundProperty);
 413 
 414             final ChoiceBox choiceBox = new ChoiceBox();
 415             choiceBox.setItems(FXCollections.observableArrayList(enumeration.getClass().getEnumConstants()));
 416             choiceBox.getSelectionModel().select(prop.getValue());
 417             prop.bind(choiceBox.getSelectionModel().selectedItemProperty());
 418             setController(choiceBox);
 419         }
 420     }
 421 
 422     private class StrokeDashArrayPropertyController extends PropertyController {
 423 
 424         public StrokeDashArrayPropertyController(final PlaygroundProperty playgroundProperty, Object object, final ObservableList<Double> list) {
 425             super(playgroundProperty, "strokeDashArray");
 426 
 427             final ComboBox<ObservableList<Double>> comboBox = new ComboBox<>();
 428             comboBox.setEditable(true);
 429             comboBox.setItems(FXCollections.observableArrayList(
 430                     FXCollections.<Double>observableArrayList(100d, 50d),
 431                     FXCollections.<Double>observableArrayList(0d, 20d),
 432                     FXCollections.<Double>observableArrayList(20d, 20d),
 433                     FXCollections.<Double>observableArrayList(30d, 15d, 0d, 15d)
 434             ));
 435             comboBox.setConverter(new StringConverter<ObservableList<Double>>() {
 436                 @Override public String toString(ObservableList<Double> t) {
 437                     if (t == null || t.isEmpty()) {
 438                         return null;
 439                     }
 440                     StringBuilder sb = new StringBuilder();
 441                     for (Double d : t) {
 442                         String str = String.valueOf(d);
 443                         if (str.endsWith(".0")) {
 444                             str = str.substring(0, str.length() - 2);
 445                         }
 446                         sb.append(str).append(' ');
 447                     }
 448                     return sb.substring(0, sb.length() - 1);
 449                 }
 450 
 451                 @Override public ObservableList<Double> fromString(String string) {
 452                     String[] values = string.trim().split(" +");
 453                     ObservableList<Double> res = FXCollections.observableArrayList();
 454                     double sum = 0;
 455                     for (String value : values) {
 456                         try {
 457                             double val = Math.min(Math.max(Double.parseDouble(value), 0), 1000);
 458                             res.add(val);
 459                             sum += val;
 460                             if (sum > 5000) {
 461                                 break;
 462                             }
 463                         } catch (Exception ignored) {
 464                         }
 465                     }
 466                     if (sum == 0) {
 467                         res.clear();
 468                     }
 469                     return res;
 470                 }
 471             });
 472             comboBox.valueProperty().addListener(new ChangeListener() {
 473                 @Override public void changed(ObservableValue ov, Object t, Object key) {
 474                     ObservableList<Double> value = comboBox.getValue();
 475                     list.setAll(value);
 476 
 477                     if (value != null && !value.isEmpty() && comboBox.getItems().indexOf(value) == -1) {
 478                         comboBox.getItems().add(value);
 479                     }
 480                 }
 481             });
 482             setController(comboBox);
 483         }
 484     }
 485 }