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 }