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.popupeditors;
  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.editor.panel.inspector.editors.AutoSuggestEditor;
  37 import com.oracle.javafx.scenebuilder.kit.editor.panel.inspector.editors.EditorUtils;
  38 import com.oracle.javafx.scenebuilder.kit.metadata.property.ValuePropertyMetadata;
  39 
  40 import java.lang.reflect.Field;
  41 import java.util.ArrayList;
  42 import java.util.Arrays;
  43 import java.util.Collections;
  44 import java.util.Comparator;
  45 import java.util.List;
  46 import java.util.Locale;
  47 import java.util.Objects;
  48 import java.util.Set;
  49 
  50 import javafx.beans.value.ChangeListener;
  51 import javafx.event.ActionEvent;
  52 import javafx.event.Event;
  53 import javafx.event.EventHandler;
  54 import javafx.fxml.FXML;
  55 import javafx.scene.Node;
  56 import javafx.scene.Parent;
  57 import javafx.scene.control.Button;
  58 import javafx.scene.control.ChoiceBox;
  59 import javafx.scene.control.Label;
  60 import javafx.scene.input.KeyCharacterCombination;
  61 import javafx.scene.input.KeyCode;
  62 import javafx.scene.input.KeyCodeCombination;
  63 import javafx.scene.input.KeyCombination;
  64 import javafx.scene.input.KeyCombination.Modifier;
  65 import javafx.scene.layout.GridPane;
  66 import javafx.scene.layout.StackPane;
  67 import javafx.util.StringConverter;
  68 
  69 /**
  70  * KeyCombination popup editor (for keyboard shortcuts).
  71  *
  72  */
  73 public class KeyCombinationPopupEditor extends PopupEditor {
  74 
  75     @FXML
  76     StackPane mainKeySp;
  77     @FXML
  78     Button clearAllBt;
  79 
  80     private GridPane gridPane;
  81     private static final int NB_MODIFIERS_MAX = 5;
  82     private final ArrayList<ModifierRow> modifierRows = new ArrayList<>();
  83     private KeyCombination.ModifierValue alt;
  84     private KeyCombination.ModifierValue control;
  85     private KeyCombination.ModifierValue meta;
  86     private KeyCombination.ModifierValue shift;
  87     private KeyCombination.ModifierValue shortcut;
  88     private MainKey mainKey;
  89     private EditorController editorController;
  90     private final KeyCombination.Modifier[] keyCombinationModifiers = {
  91         KeyCombination.ALT_ANY, KeyCombination.ALT_DOWN,
  92         KeyCombination.CONTROL_ANY, KeyCombination.CONTROL_DOWN,
  93         KeyCombination.META_ANY, KeyCombination.META_DOWN,
  94         KeyCombination.SHIFT_ANY, KeyCombination.SHIFT_DOWN,
  95         KeyCombination.SHORTCUT_ANY, KeyCombination.SHORTCUT_DOWN};
  96 
  97     public KeyCombinationPopupEditor(ValuePropertyMetadata propMeta, Set<Class<?>> selectedClasses,
  98             EditorController editorController) {
  99         super(propMeta, selectedClasses);
 100         initialize(editorController);
 101     }
 102 
 103     private void initialize(EditorController editorController) {
 104         this.editorController = editorController;
 105     }
 106 
 107     //
 108     // Interface from PopupEditor.
 109     // Methods called by PopupEditor.
 110     //
 111     @Override
 112     public void initializePopupContent() {
 113         Parent root = EditorUtils.loadPopupFxml("KeyCombinationPopupEditor.fxml", this); //NOI18N
 114         assert root instanceof GridPane;
 115         gridPane = (GridPane) root;
 116         // Build suggested key code list
 117         List<Field> keyCodes = Arrays.asList(KeyCode.class.getFields());
 118         List<String> keyCodesStr = new ArrayList<>();
 119         for (Field keyCode : keyCodes) {
 120             keyCodesStr.add(keyCode.getName());
 121         }
 122 
 123         mainKey = new MainKey(keyCodesStr, editorController);
 124         mainKeySp.getChildren().add(mainKey.getNode());
 125 
 126         clearAllBt.setText(I18N.getString("inspector.keycombination.clear"));
 127         clearAllBt.setOnAction(t -> {
 128             resetState();
 129             buildUI();
 130             commit(null);
 131         });
 132 
 133         buildUI();
 134     }
 135 
 136     @Override
 137     public String getPreviewString(Object value) {
 138         if (value == null) {
 139             return I18N.getString("inspector.keycombination.null");
 140         }
 141         assert value instanceof KeyCombination;
 142         KeyCombination keyCombinationVal = (KeyCombination) value;
 143         String valueAsString;
 144         if (isIndeterminate()) {
 145             valueAsString = "-"; //NOI18N
 146         } else {
 147             valueAsString = keyCombinationVal.toString();
 148         }
 149         return valueAsString;
 150     }
 151 
 152     @Override
 153     public void setPopupContentValue(Object value) {
 154 
 155         if (value != null) {
 156             // Empty the editor
 157             resetState();
 158             resetUI();
 159             assert value instanceof KeyCombination;
 160             // Apply the new keyCombination
 161             buildContent((KeyCombination) value);
 162         } else {
 163             resetState();
 164             buildUI();
 165         }
 166     }
 167 
 168     @Override
 169     public Node getPopupContentNode() {
 170         return gridPane;
 171     }
 172 
 173     public void reset(ValuePropertyMetadata propMeta, Set<Class<?>> selectedClasses, EditorController editorController) {
 174         super.reset(propMeta, selectedClasses);
 175         this.editorController = editorController;
 176     }
 177 
 178     private void resetState() {
 179         modifierRows.clear();
 180         mainKey.setKeyCode(null);
 181     }
 182 
 183     private void resetUI() {
 184         gridPane.getChildren().clear();
 185         gridPane.getRowConstraints().clear();
 186     }
 187 
 188     private void buildContent(KeyCombination keyCombination) {
 189         assert keyCombination != null;
 190 
 191         // Build the modifiers rows
 192         modifierRows.clear();
 193         KeyCombination.Modifier modifier1 = null;
 194         alt = keyCombination.getAlt();
 195         if (alt != KeyCombination.ModifierValue.UP) {
 196             if (alt == KeyCombination.ModifierValue.DOWN) {
 197                 modifier1 = KeyCombination.ALT_DOWN;
 198             } else if (alt == KeyCombination.ModifierValue.ANY) {
 199                 modifier1 = KeyCombination.ALT_ANY;
 200             }
 201             createModifierRow(modifier1);
 202         }
 203 
 204         KeyCombination.Modifier modifier2 = null;
 205         control = keyCombination.getControl();
 206         if (control != KeyCombination.ModifierValue.UP) {
 207             if (control == KeyCombination.ModifierValue.DOWN) {
 208                 modifier2 = KeyCombination.CONTROL_DOWN;
 209             } else if (control == KeyCombination.ModifierValue.ANY) {
 210                 modifier2 = KeyCombination.CONTROL_ANY;
 211             }
 212             createModifierRow(modifier2);
 213         }
 214 
 215         KeyCombination.Modifier modifier3 = null;
 216         meta = keyCombination.getMeta();
 217         if (meta != KeyCombination.ModifierValue.UP) {
 218             if (meta == KeyCombination.ModifierValue.DOWN) {
 219                 modifier3 = KeyCombination.META_DOWN;
 220             } else if (meta == KeyCombination.ModifierValue.ANY) {
 221                 modifier3 = KeyCombination.META_ANY;
 222             }
 223             createModifierRow(modifier3);
 224         }
 225 
 226         KeyCombination.Modifier modifier4 = null;
 227         shift = keyCombination.getShift();
 228         if (shift != KeyCombination.ModifierValue.UP) {
 229             if (shift == KeyCombination.ModifierValue.DOWN) {
 230                 modifier4 = KeyCombination.SHIFT_DOWN;
 231             } else if (shift == KeyCombination.ModifierValue.ANY) {
 232                 modifier4 = KeyCombination.SHIFT_ANY;
 233             }
 234             createModifierRow(modifier4);
 235         }
 236 
 237         KeyCombination.Modifier modifier5 = null;
 238         shortcut = keyCombination.getShortcut();
 239         if (shortcut != KeyCombination.ModifierValue.UP) {
 240             if (shortcut == KeyCombination.ModifierValue.DOWN) {
 241                 modifier5 = KeyCombination.SHORTCUT_DOWN;
 242             } else if (shortcut == KeyCombination.ModifierValue.ANY) {
 243                 modifier5 = KeyCombination.SHORTCUT_ANY;
 244             }
 245             createModifierRow(modifier5);
 246         }
 247 
 248         // Handle the main key
 249         KeyCode keyCode = null;
 250         if (keyCombination instanceof KeyCodeCombination) {
 251             keyCode = ((KeyCodeCombination) keyCombination).getCode();
 252         } else if (keyCombination instanceof KeyCharacterCombination) {
 253             keyCode = KeyCode.getKeyCode(((KeyCharacterCombination) keyCombination).getCharacter());
 254         }
 255         mainKey.setKeyCode(keyCode);
 256 
 257         // Build the UI
 258         buildUI();
 259 
 260         commit(keyCombination);
 261     }
 262 
 263     private void commit(KeyCombination keyCombination) {
 264         commitValue(keyCombination);
 265     }
 266 
 267     private KeyCombination createKeyCombination() {
 268         if (mainKey.isEmpty()) {
 269             return null;
 270         }
 271         KeyCodeCombination keyComb = null;
 272         List<KeyCombination.Modifier> modifiers = new ArrayList<>();
 273         for (ModifierRow modifier : modifierRows) {
 274             if (!modifier.isEmpty()) {
 275                 if (modifiers.contains(modifier.getModifier())) {
 276                     // doublon: invalid
 277                     return null;
 278                 }
 279                 modifiers.add(modifier.getModifier());
 280             }
 281         }
 282         if (modifiers.isEmpty()) {
 283             // no modifier: invalid
 284             return null;
 285         }
 286         try {
 287             keyComb = new KeyCodeCombination(mainKey.getKeyCode(), modifiers.toArray(new KeyCombination.Modifier[1]));
 288         } catch (IllegalArgumentException | NullPointerException ex) {
 289             System.out.println("Invalid key combination" + ex); //NOI18N
 290         } catch (RuntimeException ex) {
 291             System.out.println(ex.getMessage() + ex);
 292         }
 293         return keyComb;
 294     }
 295 
 296     private List<KeyCombination.Modifier> getModifierConstants() {
 297         ArrayList<KeyCombination.Modifier> mods = new ArrayList<>();
 298         for (KeyCombination.Modifier modifier : keyCombinationModifiers) {
 299             boolean alreadyUsed = false;
 300             for (ModifierRow row : modifierRows) {
 301                 if (!row.isEmpty()) {
 302                     if (row.getModifier().getKey().equals(modifier.getKey())) {
 303                         // modifier already used
 304                         alreadyUsed = true;
 305                         break;
 306                     }
 307                 }
 308             }
 309             if (!alreadyUsed) {
 310                 mods.add(modifier);
 311             }
 312         }
 313         mods.add(null);
 314         Collections.sort(mods, new ModifierComparator());
 315         return mods;
 316     }
 317 
 318     private static class ModifierComparator implements Comparator<KeyCombination.Modifier> {
 319 
 320         @Override
 321         public int compare(KeyCombination.Modifier o1, KeyCombination.Modifier o2) {
 322             if (o1 == null || o2 == null) {
 323                 return -1;
 324             }
 325             String str1 = o1.getKey().toString() + o1.getValue().toString();
 326             String str2 = o2.getKey().toString() + o2.getValue().toString();
 327             return str1.compareTo(str2);
 328         }
 329     }
 330 
 331     private ChoiceBox<KeyCombination.Modifier> createModifierChoiceBox(KeyCombination.Modifier modifier) {
 332         final ChoiceBox<KeyCombination.Modifier> modifierChoiceBox = new ChoiceBox<>();
 333         EditorUtils.makeWidthStretchable(modifierChoiceBox);
 334         modifierChoiceBox.setConverter(new ModifierConverter());
 335         modifierChoiceBox.getItems().setAll(getModifierConstants());
 336         if (modifier != null) {
 337             modifierChoiceBox.getSelectionModel().select(modifier);
 338         }
 339 
 340         modifierChoiceBox.getSelectionModel().selectedItemProperty()
 341                 .addListener((ChangeListener<Modifier>) (observable, oldValue, newValue) -> {
 342                     if (!mainKey.isEmpty()) {
 343                         KeyCombination kc = createKeyCombination();
 344                         if (kc != null) {
 345                             commit(kc);
 346                         }
 347                     }
 348                     buildUI();
 349                 });
 350         // Workaround for RT-37679
 351         modifierChoiceBox.addEventHandler(ActionEvent.ACTION, (Event event) -> {
 352             event.consume();
 353         });
 354         return modifierChoiceBox;
 355     }
 356 
 357     private void buildUI() {
 358         resetUI();
 359 
 360         // Cleanup: remove empty rows
 361         ArrayList<ModifierRow> emptyRows = new ArrayList<>();
 362         for (ModifierRow row : modifierRows) {
 363             if (row.isEmpty()) {
 364                 emptyRows.add(row);
 365             }
 366         }
 367         modifierRows.removeAll(emptyRows);
 368 
 369         int lineIndex = 0;
 370         for (ModifierRow row : modifierRows) {
 371             addModifierRow(row, lineIndex);
 372             lineIndex++;
 373         }
 374 
 375         // add an empty row
 376         boolean added = false;
 377         if (modifierRows.size() < NB_MODIFIERS_MAX) {
 378             added = addEmptyModifierIfNeeded();
 379         }
 380         if (added) {
 381             lineIndex++;
 382         }
 383 
 384         // add mainKey
 385         Label mainKeyLabel = new Label(I18N.getString("inspector.keycombination.mainkey"));
 386         gridPane.add(mainKeyLabel, 0, lineIndex);
 387         gridPane.add(mainKey.getNode(), 1, lineIndex);
 388         lineIndex++;
 389 
 390         // add reset button
 391         gridPane.add(clearAllBt, 1, lineIndex);
 392     }
 393 
 394     private boolean addEmptyModifierIfNeeded() {
 395         for (ModifierRow row : modifierRows) {
 396             if (row.isEmpty()) {
 397                 return false;
 398             }
 399         }
 400         addModifierRow(createModifierRow(null), modifierRows.size() - 1);
 401         return true;
 402     }
 403 
 404     private void addModifierRow(ModifierRow row, int lineIndex) {
 405         row.getLabel().setText(I18N.getString("inspector.keycombination.modifier")
 406                 + " " + (lineIndex + 1)); //NOI18N
 407         gridPane.add(row.getLabel(), 0, lineIndex);
 408         gridPane.add(row.getChoiceBox(), 1, lineIndex);
 409     }
 410 
 411     private ModifierRow createModifierRow(KeyCombination.Modifier modifier) {
 412         ChoiceBox<KeyCombination.Modifier> choiceBox = createModifierChoiceBox(modifier);
 413         ModifierRow row = new ModifierRow(choiceBox);
 414         modifierRows.add(row);
 415         return row;
 416     }
 417 
 418     private class MainKey extends AutoSuggestEditor {
 419 
 420         private EditorController editorController;
 421         String mainKey = null;
 422 
 423         public MainKey(List<String> suggestedKeys, EditorController editorController) {
 424             super("", null, suggestedKeys); //NOI18N
 425             initialize(editorController);
 426         }
 427 
 428         private void initialize(EditorController editorController) {
 429             this.editorController = editorController;
 430             EventHandler<ActionEvent> onActionListener = t -> {
 431                 if (Objects.equals(mainKey, getTextField().getText())) {
 432                     // no change
 433                     return;
 434                 }
 435                 mainKey = getTextField().getText();
 436                 if (!mainKey.isEmpty()) {
 437                     KeyCombination kc = createKeyCombination();
 438                     if (kc != null) {
 439                         commit(kc);
 440                     }
 441                 }
 442             };
 443 
 444             setTextEditorBehavior(this, getTextField(), onActionListener);
 445             commitOnFocusLost(this);
 446         }
 447 
 448         public Node getNode() {
 449             return getValueEditor();
 450         }
 451 
 452         public void setKeyCode(KeyCode keyCode) {
 453             setValue((keyCode != null) ? keyCode.toString() : "");//NOI18N
 454         }
 455 
 456         public KeyCode getKeyCode() {
 457             String valStr = getTextField().getText();
 458             if (valStr.isEmpty()) {
 459                 return null;
 460             }
 461             // Put the string in uppercase for convenience (all kycode are uppercase)
 462             valStr = valStr.toUpperCase(Locale.ROOT);
 463 
 464             KeyCode keyCode = null;
 465             try {
 466                 keyCode = KeyCode.valueOf(valStr);
 467             } catch (Exception ex) {
 468                 editorController.getMessageLog().logWarningMessage(
 469                         "inspector.keycombination.invalidkeycode", valStr); //NOI18N
 470             }
 471             return keyCode;
 472         }
 473 
 474         public boolean isEmpty() {
 475             return getKeyCode() == null;
 476         }
 477     }
 478 
 479     private static void commitOnFocusLost(AutoSuggestEditor autoSuggestEditor) {
 480         autoSuggestEditor.getTextField().focusedProperty().addListener((ChangeListener<Boolean>) (ov, oldVal, newVal) -> {
 481             if (!newVal) {
 482                 // Focus lost
 483                 autoSuggestEditor.getCommitListener().handle(null);
 484             }
 485         });
 486     }
 487 
 488     private static class ModifierRow {
 489 
 490         private Label label;
 491         private ChoiceBox<KeyCombination.Modifier> choiceBox;
 492 
 493         public ModifierRow(ChoiceBox<KeyCombination.Modifier> choiceBox) {
 494             this.label = new Label();
 495             this.choiceBox = choiceBox;
 496         }
 497 
 498         public Label getLabel() {
 499             return label;
 500         }
 501 
 502         @SuppressWarnings("unused")
 503         public void setLabel(Label label) {
 504             this.label = label;
 505         }
 506 
 507         public ChoiceBox<KeyCombination.Modifier> getChoiceBox() {
 508             return choiceBox;
 509         }
 510 
 511         @SuppressWarnings("unused")
 512         public void setChoiceBox(ChoiceBox<KeyCombination.Modifier> choiceBox) {
 513             this.choiceBox = choiceBox;
 514         }
 515 
 516         public KeyCombination.Modifier getModifier() {
 517             return choiceBox.getSelectionModel().getSelectedItem();
 518         }
 519 
 520         public boolean isEmpty() {
 521             return getModifier() == null;
 522         }
 523     }
 524 
 525     private static class ModifierConverter extends StringConverter<KeyCombination.Modifier> {
 526 
 527         @Override
 528         public String toString(KeyCombination.Modifier object) {
 529             if (object == null) {
 530                 return I18N.getString("inspector.keycombination.none");
 531             }
 532             return object.getKey() + "_" + object.getValue(); //NOI18N
 533         }
 534 
 535         @Override
 536         public KeyCombination.Modifier fromString(String string) {
 537             if (string.equals(I18N.getString("inspector.keycombination.none"))) {
 538                 return null;
 539             }
 540             if (string.startsWith(KeyCode.ALT.getName())) {
 541                 if (string.endsWith(KeyCombination.ModifierValue.DOWN.name())) {
 542                     return KeyCombination.ALT_DOWN;
 543                 } else if (string.endsWith(KeyCombination.ModifierValue.ANY.name())) {
 544                     return KeyCombination.ALT_ANY;
 545                 }
 546             }
 547             if (string.startsWith(KeyCode.CONTROL.getName())) {
 548                 if (string.endsWith(KeyCombination.ModifierValue.DOWN.name())) {
 549                     return KeyCombination.CONTROL_DOWN;
 550                 } else if (string.endsWith(KeyCombination.ModifierValue.ANY.name())) {
 551                     return KeyCombination.CONTROL_ANY;
 552                 }
 553             }
 554             if (string.startsWith(KeyCode.META.getName())) {
 555                 if (string.endsWith(KeyCombination.ModifierValue.DOWN.name())) {
 556                     return KeyCombination.META_DOWN;
 557                 } else if (string.endsWith(KeyCombination.ModifierValue.ANY.name())) {
 558                     return KeyCombination.META_ANY;
 559                 }
 560             }
 561             if (string.startsWith(KeyCode.SHIFT.getName())) {
 562                 if (string.endsWith(KeyCombination.ModifierValue.DOWN.name())) {
 563                     return KeyCombination.SHIFT_DOWN;
 564                 } else if (string.endsWith(KeyCombination.ModifierValue.ANY.name())) {
 565                     return KeyCombination.SHIFT_ANY;
 566                 }
 567             }
 568             if (string.startsWith(KeyCode.SHORTCUT.getName())) {
 569                 if (string.endsWith(KeyCombination.ModifierValue.DOWN.name())) {
 570                     return KeyCombination.SHORTCUT_DOWN;
 571                 } else if (string.endsWith(KeyCombination.ModifierValue.ANY.name())) {
 572                     return KeyCombination.SHORTCUT_ANY;
 573                 }
 574             }
 575             return null;
 576         }
 577     }
 578 
 579 }