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 }