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 }