1 /*
   2  * Copyright (c) 2012, 2015, 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 com.oracle.javafx.scenebuilder.kit.editor.panel.inspector.editors;
  33 
  34 import com.oracle.javafx.scenebuilder.kit.editor.EditorController;
  35 import com.oracle.javafx.scenebuilder.kit.editor.i18n.I18N;
  36 import com.oracle.javafx.scenebuilder.kit.metadata.property.ValuePropertyMetadata;
  37 import com.oracle.javafx.scenebuilder.kit.util.CssInternal;
  38 import javafx.css.CssParser;
  39 
  40 import java.util.ArrayList;
  41 import java.util.Collections;
  42 import java.util.Iterator;
  43 import java.util.List;
  44 import java.util.Set;
  45 
  46 import javafx.beans.value.ChangeListener;
  47 import javafx.collections.FXCollections;
  48 import javafx.collections.ListChangeListener;
  49 import javafx.collections.ObservableList;
  50 import javafx.collections.ObservableSet;
  51 import javafx.css.CssMetaData;
  52 import javafx.css.PseudoClass;
  53 import javafx.css.Styleable;
  54 import javafx.event.ActionEvent;
  55 import javafx.event.EventHandler;
  56 import javafx.fxml.FXML;
  57 import javafx.scene.Node;
  58 import javafx.scene.Parent;
  59 import javafx.scene.control.Button;
  60 import javafx.scene.control.MenuItem;
  61 import javafx.scene.control.TextField;
  62 import javafx.scene.input.KeyCode;
  63 import javafx.scene.input.KeyEvent;
  64 import javafx.scene.layout.StackPane;
  65 
  66 /**
  67  * Editor of the 'style' property. It may contain several css rules, that have
  68  * their dedicated class (StyleItem).
  69  *
  70  *
  71  */
  72 public class StyleEditor extends InlineListEditor {
  73 
  74     private List<String> cssProperties;
  75     private Set<Class<?>> selectedClasses;
  76     private EditorController editorController;
  77 
  78     public StyleEditor(ValuePropertyMetadata propMeta, Set<Class<?>> selectedClasses, EditorController editorController) {
  79         super(propMeta, selectedClasses);
  80         initialize(selectedClasses, editorController);
  81     }
  82 
  83     private void initialize(Set<Class<?>> selectedClasses, EditorController editorController) {
  84         this.selectedClasses = selectedClasses;
  85         this.editorController = editorController;
  86         setLayoutFormat(LayoutFormat.DOUBLE_LINE);
  87         addItem(getNewStyleItem());
  88     }
  89 
  90     private StyleItem getNewStyleItem() {
  91         if (cssProperties == null) {
  92             cssProperties = CssInternal.getCssProperties(selectedClasses);
  93         }
  94         return new StyleItem(this, cssProperties);
  95     }
  96 
  97     @Override
  98     public void commit(EditorItem source) {
  99         try {
 100             userUpdateValueProperty(getValue());
 101         } catch (Exception ex) {
 102             editorController.getMessageLog().logWarningMessage(
 103                     "inspector.style.valuetypeerror", ex.getMessage());
 104         }
 105     }
 106 
 107     @Override
 108     public Object getValue() {
 109         // Concatenate all the item values
 110         String value = null;
 111         for (EditorItem styleItem : getEditorItems()) {
 112             String itemValue = EditorUtils.toString(styleItem.getValue());
 113             if (itemValue.isEmpty()) {
 114                 continue;
 115             }
 116             if (value == null) {
 117                 value = ""; //NOI18N
 118             }
 119             assert styleItem instanceof StyleItem;
 120             if (((StyleItem) styleItem).hasParsingError()) {
 121                 editorController.getMessageLog().logWarningMessage(
 122                         "inspector.style.parsingerror", itemValue);
 123             }
 124             value += itemValue + " "; //NOI18N
 125         }
 126         if (value != null) {
 127             value = value.trim();
 128         }
 129         if (value == null) {
 130             // no style
 131             return super.getPropertyMeta().getDefaultValueObject();
 132         } else {
 133             return value;
 134         }
 135     }
 136 
 137     @Override
 138     public void setValue(Object value) {
 139         setValueGeneric(value);
 140         if (isSetValueDone()) {
 141             return;
 142         }
 143         if (value == null) {
 144             reset();
 145             return;
 146         }
 147         assert value instanceof String;
 148         String[] itemArray = ((String) value).split(";");
 149         Iterator<EditorItem> itemsIter = new ArrayList<>(getEditorItems()).iterator();
 150         for (String item : itemArray) {
 151             item = item.trim();
 152             if (item.isEmpty()) {
 153                 continue;
 154             }
 155             EditorItem editorItem;
 156             if (itemsIter.hasNext()) {
 157                 // re-use the current items first
 158                 editorItem = itemsIter.next();
 159             } else {
 160                 // additional items required
 161                 editorItem = addItem(getNewStyleItem());
 162             }
 163             editorItem.setValue(item);
 164         }
 165         // Empty the remaining items, if needed
 166         while (itemsIter.hasNext()) {
 167             EditorItem editorItem = itemsIter.next();
 168             removeItem(editorItem);
 169         }
 170     }
 171 
 172     @Override
 173     boolean isValueChanged(Object value) {
 174         if (((value == null) && (valueProperty().getValue() != null))
 175                 || ((value != null) && (valueProperty().getValue() == null))) {
 176             return true;
 177         }
 178 
 179         if (value != null) {
 180             // Compare the values without spaces, since the fxml file could have
 181             // a different formatting than the one we generate.
 182             assert value instanceof String;
 183             assert valueProperty().getValue() instanceof String;
 184             String oldNoSpace = ((String) valueProperty().getValue()).replaceAll("\\s", "");
 185             String newNoSpace = ((String) value).replaceAll("\\s", "");
 186             if (!oldNoSpace.equals(newNoSpace)) {
 187                 return true;
 188             }
 189         }
 190         return false;
 191     }
 192 
 193     public void reset(ValuePropertyMetadata propMeta, Set<Class<?>> selectedClasses, EditorController editorController) {
 194         super.reset(propMeta, selectedClasses);
 195         this.selectedClasses = selectedClasses;
 196         this.editorController = editorController;
 197         cssProperties = null;
 198         // add an empty item
 199         addItem(getNewStyleItem());
 200     }
 201 
 202     @Override
 203     public void requestFocus() {
 204         EditorItem firstItem = getEditorItems().get(0);
 205         assert firstItem instanceof StyleItem;
 206         ((StyleItem) firstItem).requestFocus();
 207     }
 208 
 209     /**
 210      ***************************************************************************
 211      *
 212      * Style item : property + value text fields, and +/action buttons.
 213      *
 214      ***************************************************************************
 215      */
 216     private class StyleItem extends AutoSuggestEditor implements EditorItem {
 217 
 218         @FXML
 219         private Button plusBt;
 220         @FXML
 221         private MenuItem removeMi;
 222         @FXML
 223         private MenuItem moveUpMi;
 224         @FXML
 225         private MenuItem moveDownMi;
 226         @FXML
 227         private TextField valueTf;
 228         @FXML
 229         private StackPane propertySp;
 230 
 231         private Parent root;
 232         private TextField propertyTf;
 233         private String currentValue;
 234         private EditorItemDelegate editor;
 235         private boolean parsingError = false;
 236         private ListChangeListener<CssParser.ParseError> errorListener;
 237 
 238         public StyleItem(EditorItemDelegate editor, List<String> suggestedList) {
 239 //            System.out.println("New StyleItem.");
 240             // It is an AutoSuggestEditor without MenuButton
 241             super("", "", suggestedList, false);
 242             initialize(editor);
 243         }
 244 
 245         // Method to please FindBugs
 246         private void initialize(EditorItemDelegate editor) {
 247             this.editor = editor;
 248             root = EditorUtils.loadFxml("StyleEditorItem.fxml", this);
 249 
 250             // Add the AutoSuggest text field in the scene graph
 251             propertySp.getChildren().add(super.getRoot());
 252 
 253             propertyTf = super.getTextField();
 254             EventHandler<ActionEvent> onActionListener = event -> {
 255 //                    System.out.println("StyleItem : onActionListener");
 256                 if (getValue().equals(currentValue)) {
 257                     // no change
 258                     return;
 259                 }
 260                 if (!propertyTf.getText().isEmpty() && !valueTf.getText().isEmpty()) {
 261 //                        System.out.println("StyleEditorItem : COMMIT");
 262                     editor.commit(StyleItem.this);
 263                     if (event != null && event.getSource() instanceof TextField) {
 264                         ((TextField) event.getSource()).selectAll();
 265                     }
 266                 }
 267                 if (propertyTf.getText().isEmpty() && valueTf.getText().isEmpty()) {
 268                     remove(null);
 269                 }
 270 
 271                 updateButtons();
 272                 currentValue = EditorUtils.toString(getValue());
 273             };
 274 
 275             ChangeListener<String> textPropertyChange = (ov, prevText, newText) -> {
 276                 if (prevText.isEmpty() || newText.isEmpty()) {
 277                     // Text changed FROM empty value, or TO empty value: buttons status change
 278                     updateButtons();
 279                 }
 280             };
 281 
 282             propertyTf.textProperty().addListener(textPropertyChange);
 283             valueTf.textProperty().addListener(textPropertyChange);
 284             updateButtons();
 285 
 286             // Do not add a generic focus listener on each of the text fields,
 287             // but implement a specific one.
 288             setTextEditorBehavior(propertyTf, onActionListener, false);
 289             setTextEditorBehavior(valueTf, onActionListener, false);
 290             ChangeListener<Boolean> focusListener = (observable, oldValue, newValue) -> {
 291                 if (!newValue) {
 292                     // focus lost: commit
 293                     editor.editing(false, onActionListener);
 294                 } else {
 295                     // got focus
 296                     editor.editing(true, onActionListener);
 297                 }
 298             };
 299             propertyTf.focusedProperty().addListener(focusListener);
 300             valueTf.focusedProperty().addListener(focusListener);
 301 
 302             // Initialize menu items text
 303             removeMi.setText(I18N.getString("inspector.list.remove"));
 304             moveUpMi.setText(I18N.getString("inspector.list.moveup"));
 305             moveDownMi.setText(I18N.getString("inspector.list.movedown"));
 306 
 307             errorListener = change -> {
 308                 while (change.next()) {
 309                     if (change.wasAdded()) {
 310                         for (CssParser.ParseError error : change.getAddedSubList()) {
 311                             if ("InlineStyleParsingError".equals(error.getClass().getSimpleName())) {
 312                                 parsingError = true;
 313                                 break;
 314                             }
 315                         }
 316                     }
 317                 }
 318             };
 319 
 320         }
 321 
 322         @Override
 323         public final Node getNode() {
 324             return root;
 325         }
 326 
 327         @Override
 328         public Object getValue() {
 329             String value;
 330             if (propertyTf.getText().isEmpty() && valueTf.getText().isEmpty()) {
 331                 return ""; //NOI18N
 332             } else {
 333                 String propertyVal = EditorUtils.getPlainString(propertyTf.getText()).trim();
 334                 String valueVal = EditorUtils.getPlainString(valueTf.getText()).trim();
 335                 value = propertyVal + ": " + valueVal + ";"; //NOI18N
 336             }
 337 
 338             // Parse the style, and set the parsingError boolean if any error
 339             parsingError = false;
 340             CssParser.errorsProperty().addListener(errorListener);
 341             new CssParser().parseInlineStyle(new StyleableStub(value));
 342             CssParser.errorsProperty().removeListener(errorListener);
 343 
 344             return value;
 345         }
 346 
 347         public boolean hasParsingError() {
 348             return parsingError;
 349         }
 350 
 351         @Override
 352         public void setValue(Object style) {
 353             String styleStr = EditorUtils.toString(style);
 354             // remove last ';' if any
 355             if (styleStr.endsWith(";")) { //NOI18N
 356                 styleStr = styleStr.substring(0, styleStr.length() - 1);
 357             }
 358             // split in property and value
 359             int dotIndex = styleStr.indexOf(':');
 360             String propertyStr;
 361             String valueStr = ""; //NOI18N
 362             if (dotIndex != -1) {
 363                 propertyStr = styleStr.substring(0, dotIndex);
 364                 valueStr = styleStr.substring(dotIndex + 1);
 365             } else {
 366                 propertyStr = styleStr;
 367             }
 368             propertyTf.setText(propertyStr);
 369             valueTf.setText(valueStr);
 370             updateButtons();
 371             currentValue = EditorUtils.toString(getValue());
 372         }
 373 
 374         @Override
 375         public void reset() {
 376             propertyTf.setText(""); //NOI18N
 377             valueTf.setText(""); //NOI18N
 378             propertyTf.setPromptText(null);
 379             valueTf.setPromptText(null);
 380         }
 381 
 382         // Please findBugs
 383         @Override
 384         public void requestFocus() {
 385             super.requestFocus();
 386         }
 387 
 388         @Override
 389         public void setValueAsIndeterminate() {
 390             handleIndeterminate(propertyTf);
 391             handleIndeterminate(valueTf);
 392         }
 393 
 394         @Override
 395         public MenuItem getMoveUpMenuItem() {
 396             return moveUpMi;
 397         }
 398 
 399         @Override
 400         public MenuItem getMoveDownMenuItem() {
 401             return moveDownMi;
 402         }
 403 
 404         @Override
 405         public MenuItem getRemoveMenuItem() {
 406             return removeMi;
 407         }
 408 
 409         @Override
 410         public Button getPlusButton() {
 411             return plusBt;
 412         }
 413 
 414         @Override
 415         public Button getMinusButton() {
 416             // Not used here
 417             return null;
 418         }
 419 
 420         @FXML
 421         void add(ActionEvent event) {
 422             StyleItem styleItem = getNewStyleItem();
 423             editor.add(this, styleItem);
 424             styleItem.requestFocus();
 425         }
 426 
 427         @FXML
 428         void remove(ActionEvent event) {
 429             editor.remove(this);
 430         }
 431 
 432         @FXML
 433         void up(ActionEvent event) {
 434             editor.up(this);
 435         }
 436 
 437         @FXML
 438         void down(ActionEvent event) {
 439             editor.down(this);
 440         }
 441 
 442         @FXML
 443         void plusBtTyped(KeyEvent event) {
 444             if (event.getCode() == KeyCode.ENTER) {
 445                 StyleItem styleItem = getNewStyleItem();
 446                 editor.add(this, styleItem);
 447                 styleItem.requestFocus();
 448             }
 449         }
 450 
 451         private void updateButtons() {
 452             if (propertyTf.getText().isEmpty() && valueTf.getText().isEmpty()) {
 453                 // if no field has content, disable plus
 454                 plusBt.setDisable(true);
 455                 removeMi.setDisable(false);
 456             } else if (!propertyTf.getText().isEmpty() && !valueTf.getText().isEmpty()) {
 457                 // if both fields have content, enable plus and minus
 458                 plusBt.setDisable(false);
 459                 removeMi.setDisable(false);
 460             } else if (!propertyTf.getText().isEmpty() || !valueTf.getText().isEmpty()) {
 461                 // if either field has content, disable plus and enable minus
 462                 plusBt.setDisable(true);
 463                 removeMi.setDisable(false);
 464             }
 465         }
 466 
 467         @SuppressWarnings("unused")
 468         protected void disablePlusButton(boolean disable) {
 469             plusBt.setDisable(disable);
 470         }
 471 
 472         @SuppressWarnings("unused")
 473         protected void disableRemove(boolean disable) {
 474             removeMi.setDisable(disable);
 475         }
 476     }
 477 
 478     // Stub for style parsing
 479     private static class StyleableStub implements Styleable {
 480 
 481         private final String style;
 482 
 483         private StyleableStub(String style) {
 484             this.style = style;
 485         }
 486 
 487         @Override
 488         public String getTypeSelector() {
 489             return null;
 490         }
 491 
 492         @Override
 493         public String getId() {
 494             return null;
 495         }
 496 
 497         @Override
 498         public ObservableList<String> getStyleClass() {
 499             return FXCollections.emptyObservableList();
 500         }
 501 
 502         @Override
 503         public String getStyle() {
 504             return style;
 505         }
 506 
 507         @Override
 508         public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
 509             return Collections.emptyList();
 510         }
 511 
 512         @Override
 513         public Styleable getStyleableParent() {
 514             return null;
 515         }
 516 
 517         @Override
 518         public ObservableSet<PseudoClass> getPseudoClassStates() {
 519             return FXCollections.emptyObservableSet();
 520         }
 521     }
 522 
 523 }