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.EditorPlatform; 36 import com.oracle.javafx.scenebuilder.kit.editor.EditorPlatform.Theme; 37 import com.oracle.javafx.scenebuilder.kit.editor.i18n.I18N; 38 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMInstance; 39 import com.oracle.javafx.scenebuilder.kit.metadata.property.ValuePropertyMetadata; 40 import com.oracle.javafx.scenebuilder.kit.util.CssInternal; 41 import com.oracle.javafx.scenebuilder.kit.util.URLUtils; 42 43 import java.io.File; 44 import java.io.IOException; 45 import java.net.URISyntaxException; 46 import java.util.ArrayList; 47 import java.util.Iterator; 48 import java.util.List; 49 import java.util.Map; 50 import java.util.Set; 51 52 import javafx.beans.value.ChangeListener; 53 import javafx.collections.FXCollections; 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.MenuButton; 61 import javafx.scene.control.MenuItem; 62 import javafx.scene.control.SeparatorMenuItem; 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 'styleClass' property. It may contain several css classes, that 70 * have their dedicated class (StyleClassItem). 71 * 72 * 73 */ 74 public class StyleClassEditor extends InlineListEditor { 75 76 private Set<FXOMInstance> selectedInstances; 77 private Map<String, String> cssClassesMap; 78 private List<String> themeClasses; 79 private EditorController editorController; 80 81 public StyleClassEditor(ValuePropertyMetadata propMeta, Set<Class<?>> selectedClasses, 82 Set<FXOMInstance> selectedInstances, EditorController editorController) { 83 super(propMeta, selectedClasses); 84 initialize(selectedInstances, editorController); 85 } 86 87 private void initialize(Set<FXOMInstance> selectedInstances, EditorController editorController) { 88 this.selectedInstances = selectedInstances; 89 this.editorController = editorController; 90 setLayoutFormat(PropertyEditor.LayoutFormat.DOUBLE_LINE); 91 themeClasses = CssInternal.getThemeStyleClasses(editorController.getTheme()); 92 addItem(getNewStyleClassItem()); 93 94 // On Theme change, update the themeClasses 95 editorController.themeProperty().addListener((ChangeListener<Theme>) (ov, t, t1) -> themeClasses = CssInternal.getThemeStyleClasses(StyleClassEditor.this.editorController.getTheme())); 96 } 97 98 private StyleClassItem getNewStyleClassItem() { 99 if (cssClassesMap == null) { 100 cssClassesMap = CssInternal.getStyleClassesMap(editorController, selectedInstances); 101 // We don't want the theme classes to be suggested: remove them from the list 102 for (String themeClass : themeClasses) { 103 cssClassesMap.remove(themeClass); 104 } 105 } 106 return new StyleClassItem(this, cssClassesMap); 107 } 108 109 @Override 110 public Object getValue() { 111 List<String> value = FXCollections.observableArrayList(); 112 // Group all the item values in a list 113 for (EditorItem styleItem : getEditorItems()) { 114 String itemValue = EditorUtils.toString(styleItem.getValue()); 115 if (itemValue.isEmpty()) { 116 continue; 117 } 118 value.add(itemValue); 119 } 120 if (value.isEmpty()) { 121 // no style class 122 return super.getPropertyMeta().getDefaultValueObject(); 123 } else { 124 return value; 125 } 126 } 127 128 @SuppressWarnings("unchecked") 129 @Override 130 public void setValue(Object value) { 131 setValueGeneric(value); 132 if (value == null) { 133 reset(); 134 return; 135 } 136 assert value instanceof List; 137 // Warning : value is the editing list. 138 // We do not want to set the valueProperty() to editing list 139 setValueGeneric(value); 140 if (isSetValueDone()) { 141 return; 142 } 143 144 Iterator<EditorItem> itemsIter = new ArrayList<>(getEditorItems()).iterator(); 145 for (String item : (List<String>) value) { 146 item = item.trim(); 147 if (item.isEmpty()) { 148 continue; 149 } 150 151 // We don't want to show the default theme classes 152 // (e.g. combo-box, combo-box-base for ComboBox) 153 Object defaultValue = getPropertyMeta().getDefaultValueObject(); 154 assert defaultValue instanceof List; 155 List<String> defaultClasses = (List<String>) defaultValue; 156 if (defaultClasses.contains(item)) { 157 continue; 158 } 159 160 EditorItem editorItem; 161 if (itemsIter.hasNext()) { 162 // re-use the current items first 163 editorItem = itemsIter.next(); 164 } else { 165 // additional items required 166 editorItem = addItem(getNewStyleClassItem()); 167 } 168 editorItem.setValue(item); 169 } 170 // Empty the remaining items, if needed 171 while (itemsIter.hasNext()) { 172 EditorItem editorItem = itemsIter.next(); 173 removeItem(editorItem); 174 } 175 } 176 177 public void reset(ValuePropertyMetadata propMeta, Set<Class<?>> selectedClasses, 178 Set<FXOMInstance> selectedInstances, EditorController editorController) { 179 super.reset(propMeta, selectedClasses); 180 this.selectedInstances = selectedInstances; 181 this.editorController = editorController; 182 cssClassesMap = null; 183 // add an empty item 184 addItem(getNewStyleClassItem()); 185 } 186 187 @Override 188 public void requestFocus() { 189 EditorItem firstItem = getEditorItems().get(0); 190 assert firstItem instanceof StyleClassItem; 191 ((StyleClassItem) firstItem).requestFocus(); 192 } 193 194 /** 195 *************************************************************************** 196 * 197 * StyleClass item : styleClass text fields, and +/action buttons. 198 * 199 *************************************************************************** 200 */ 201 private class StyleClassItem extends AutoSuggestEditor implements EditorItem { 202 203 @FXML 204 private Button plusBt; 205 @FXML 206 private MenuButton actionMb; 207 @FXML 208 private MenuItem removeMi; 209 @FXML 210 private MenuItem moveUpMi; 211 @FXML 212 private MenuItem moveDownMi; 213 @FXML 214 private MenuItem openMi; 215 @FXML 216 private MenuItem revealMi; 217 @FXML 218 private StackPane styleClassSp; 219 220 private Parent root; 221 private TextField styleClassTf; 222 private String currentValue; 223 private Map<String, String> cssClassesMap; 224 private EditorItemDelegate editor; 225 226 public StyleClassItem(EditorItemDelegate editor, Map<String, String> cssClassesMap) { 227 // System.out.println("New StyleClassItem."); 228 // It is an AutoSuggestEditor without MenuButton 229 super("", "", new ArrayList<>(cssClassesMap.keySet()), false); //NOI18N 230 initialize(editor, cssClassesMap); 231 } 232 233 // Method to please FindBugs 234 private void initialize(EditorItemDelegate editor, Map<String, String> cssClassesMap) { 235 this.editor = editor; 236 this.cssClassesMap = cssClassesMap; 237 root = EditorUtils.loadFxml("StyleClassEditorItem.fxml", this);//NOI18N 238 239 // Add the AutoSuggest text field in the scene graph 240 styleClassSp.getChildren().add(super.getRoot()); 241 242 styleClassTf = super.getTextField(); 243 EventHandler<ActionEvent> onActionListener = event -> { 244 // System.out.println("StyleClassItem : onActionListener"); 245 if (getValue().equals(currentValue)) { 246 // no change 247 return; 248 } 249 if (styleClassTf.getText().isEmpty()) { 250 remove(null); 251 } 252 // System.out.println("StyleEditorItem : COMMIT"); 253 editor.commit(StyleClassItem.this); 254 if ((event != null) && event.getSource() instanceof TextField) { 255 ((TextField) event.getSource()).selectAll(); 256 } 257 updateButtons(); 258 currentValue = EditorUtils.toString(getValue()); 259 }; 260 261 ChangeListener<String> textPropertyChange = (ov, prevText, newText) -> { 262 if (prevText.isEmpty() || newText.isEmpty()) { 263 // Text changed FROM empty value, or TO empty value: buttons status change 264 updateButtons(); 265 } 266 }; 267 styleClassTf.textProperty().addListener(textPropertyChange); 268 updateButtons(); 269 270 setTextEditorBehavior(styleClassTf, onActionListener, false); 271 ChangeListener<Boolean> focusListener = (observable, oldValue, newValue) -> { 272 if (!newValue) { 273 // focus lost: commit 274 editor.editing(false, onActionListener); 275 } else { 276 // got focus 277 editor.editing(true, onActionListener); 278 } 279 }; 280 styleClassTf.focusedProperty().addListener(focusListener); 281 282 // Initialize menu items text 283 removeMi.setText(I18N.getString("inspector.list.remove")); 284 moveUpMi.setText(I18N.getString("inspector.list.moveup")); 285 moveDownMi.setText(I18N.getString("inspector.list.movedown")); 286 287 // Add suggested classes in the already existing action menu button, 288 // since we do not use the AutoSuggestEditor menu button for this editor. 289 if (!cssClassesMap.isEmpty()) { 290 actionMb.getItems().add(new SeparatorMenuItem()); 291 } 292 for (String className : cssClassesMap.keySet()) { 293 // css classes menu items 294 MenuItem menuItem = new MenuItem(className); 295 menuItem.setMnemonicParsing(false); 296 menuItem.setOnAction(t -> { 297 styleClassTf.setText(className); 298 StyleClassItem.this.getCommitListener().handle(null); 299 }); 300 actionMb.getItems().add(menuItem); 301 } 302 303 } 304 305 @Override 306 public final Node getNode() { 307 return root; 308 } 309 310 @Override 311 public Object getValue() { 312 return EditorUtils.getPlainString(styleClassTf.getText()).trim(); 313 } 314 315 @Override 316 public void setValue(Object styleClass) { 317 styleClassTf.setText(EditorUtils.toString(styleClass).trim()); 318 updateButtons(); 319 currentValue = EditorUtils.toString(getValue()); 320 } 321 322 @Override 323 public void reset() { 324 styleClassTf.setText(""); //NOI18N 325 } 326 327 // Please findBugs 328 @Override 329 public void requestFocus() { 330 super.requestFocus(); 331 } 332 333 @Override 334 public void setValueAsIndeterminate() { 335 handleIndeterminate(styleClassTf); 336 } 337 338 @Override 339 public MenuItem getMoveUpMenuItem() { 340 return moveUpMi; 341 } 342 343 @Override 344 public MenuItem getMoveDownMenuItem() { 345 return moveDownMi; 346 } 347 348 @Override 349 public MenuItem getRemoveMenuItem() { 350 return removeMi; 351 } 352 353 @Override 354 public Button getPlusButton() { 355 return plusBt; 356 } 357 358 @Override 359 public Button getMinusButton() { 360 // not used here 361 return null; 362 } 363 364 @FXML 365 void add(ActionEvent event) { 366 StyleClassEditor.StyleClassItem styleClassItem = getNewStyleClassItem(); 367 editor.add(this, styleClassItem); 368 styleClassItem.requestFocus(); 369 370 } 371 372 @FXML 373 void remove(ActionEvent event) { 374 editor.remove(this); 375 } 376 377 @FXML 378 void up(ActionEvent event) { 379 editor.up(this); 380 } 381 382 @FXML 383 void down(ActionEvent event) { 384 editor.down(this); 385 } 386 387 @FXML 388 void plusBtTyped(KeyEvent event) { 389 if (event.getCode() == KeyCode.ENTER) { 390 editor.add(this, getNewStyleClassItem()); 391 } 392 } 393 394 @FXML 395 void open(ActionEvent event) { 396 String urlStr = cssClassesMap.get(getValue()); 397 if (urlStr == null) { 398 return; 399 } 400 try { 401 EditorPlatform.open(urlStr); 402 } catch (IOException ex) { 403 editorController.getMessageLog().logWarningMessage( 404 "inspector.stylesheet.cannotopen", urlStr); //NOI18N 405 } 406 } 407 408 @FXML 409 void reveal(ActionEvent event) { 410 String urlStr = cssClassesMap.get(getValue()); 411 if (urlStr == null) { 412 return; 413 } 414 try { 415 File file = URLUtils.getFile(urlStr); 416 if (file == null) { // urlStr is not a file URL 417 return; 418 } 419 EditorPlatform.revealInFileBrowser(file); 420 } catch (URISyntaxException | IOException ex) { 421 editorController.getMessageLog().logWarningMessage( 422 "inspector.stylesheet.cannotreveal", urlStr); //NOI18N 423 } 424 } 425 426 private void updateButtons() { 427 if (styleClassTf.getText().isEmpty()) { 428 // if no content, disable plus 429 plusBt.setDisable(true); 430 removeMi.setDisable(false); 431 } else { 432 // enable plus and minus 433 plusBt.setDisable(false); 434 removeMi.setDisable(false); 435 } 436 // set text of open / reveal menu items 437 String stylesheetUrl = cssClassesMap.get(getValue()); 438 if (stylesheetUrl == null) { 439 // className is unknown: open / reveal should not be visible 440 openMi.setVisible(false); 441 revealMi.setVisible(false); 442 } else { 443 openMi.setVisible(true); 444 revealMi.setVisible(true); 445 String stylesheet = EditorUtils.getSimpleFileName(stylesheetUrl); 446 openMi.setText(I18N.getString("inspector.list.open", stylesheet)); 447 if (EditorPlatform.IS_MAC) { 448 revealMi.setText(I18N.getString("inspector.list.reveal.finder", stylesheet)); 449 } else { 450 revealMi.setText(I18N.getString("inspector.list.reveal.explorer", stylesheet)); 451 } 452 } 453 } 454 455 @SuppressWarnings("unused") 456 protected void disablePlusButton(boolean disable) { 457 plusBt.setDisable(disable); 458 } 459 460 @SuppressWarnings("unused") 461 protected void disableRemove(boolean disable) { 462 removeMi.setDisable(disable); 463 } 464 } 465 }