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 }