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