1 /* 2 * Copyright (c) 2010, 2016, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package javafx.scene.control.skin; 27 28 import com.sun.javafx.scene.ParentHelper; 29 import com.sun.javafx.scene.control.FakeFocusTextField; 30 import com.sun.javafx.scene.control.Properties; 31 import com.sun.javafx.scene.control.behavior.TextInputControlBehavior; 32 import com.sun.javafx.scene.input.ExtendedInputMethodRequests; 33 import com.sun.javafx.scene.traversal.Algorithm; 34 import com.sun.javafx.scene.traversal.Direction; 35 import com.sun.javafx.scene.traversal.ParentTraversalEngine; 36 import com.sun.javafx.scene.traversal.TraversalContext; 37 import javafx.beans.InvalidationListener; 38 import javafx.beans.value.ObservableValue; 39 import javafx.css.Styleable; 40 import javafx.event.EventHandler; 41 import javafx.geometry.Bounds; 42 import javafx.geometry.HPos; 43 import javafx.geometry.Point2D; 44 import javafx.geometry.VPos; 45 import javafx.scene.AccessibleAttribute; 46 import javafx.scene.Node; 47 import javafx.scene.control.ComboBoxBase; 48 import javafx.scene.control.PopupControl; 49 import javafx.scene.control.Skin; 50 import javafx.scene.control.Skinnable; 51 import javafx.scene.control.TextField; 52 import javafx.scene.input.DragEvent; 53 import javafx.scene.input.KeyCode; 54 import javafx.scene.input.KeyEvent; 55 import javafx.scene.input.MouseEvent; 56 import javafx.scene.layout.Region; 57 import javafx.stage.WindowEvent; 58 import javafx.util.StringConverter; 59 60 /** 61 * An abstract class that extends the functionality of {@link ComboBoxBaseSkin} 62 * to include API related to showing ComboBox-like controls as popups. 63 * 64 * @param <T> The type of the ComboBox-like control. 65 * @since 9 66 */ 67 public abstract class ComboBoxPopupControl<T> extends ComboBoxBaseSkin<T> { 68 69 /*************************************************************************** 70 * * 71 * Private fields * 72 * * 73 **************************************************************************/ 74 75 PopupControl popup; 76 77 private boolean popupNeedsReconfiguring = true; 78 79 private final ComboBoxBase<T> comboBoxBase; 80 private TextField textField; 81 82 private String initialTextFieldValue = null; 83 84 85 86 /*************************************************************************** 87 * * 88 * TextField Listeners * 89 * * 90 **************************************************************************/ 91 92 private EventHandler<MouseEvent> textFieldMouseEventHandler = event -> { 93 ComboBoxBase<T> comboBoxBase = getSkinnable(); 94 if (!event.getTarget().equals(comboBoxBase)) { 95 comboBoxBase.fireEvent(event.copyFor(comboBoxBase, comboBoxBase)); 96 event.consume(); 97 } 98 }; 99 private EventHandler<DragEvent> textFieldDragEventHandler = event -> { 100 ComboBoxBase<T> comboBoxBase = getSkinnable(); 101 if (!event.getTarget().equals(comboBoxBase)) { 102 comboBoxBase.fireEvent(event.copyFor(comboBoxBase, comboBoxBase)); 103 event.consume(); 104 } 105 }; 106 107 108 109 /*************************************************************************** 110 * * 111 * Constructors * 112 * * 113 **************************************************************************/ 114 115 /** 116 * Creates a new instance of ComboBoxPopupControl, although note that this 117 * instance does not handle any behavior / input mappings - this needs to be 118 * handled appropriately by subclasses. 119 * 120 * @param control The control that this skin should be installed onto. 121 */ 122 public ComboBoxPopupControl(ComboBoxBase<T> control) { 123 super(control); 124 this.comboBoxBase = control; 125 126 // editable input node 127 this.textField = getEditor() != null ? getEditableInputNode() : null; 128 129 // Fix for RT-29565. Without this the textField does not have a correct 130 // pref width at startup, as it is not part of the scenegraph (and therefore 131 // has no pref width until after the first measurements have been taken). 132 if (this.textField != null) { 133 getChildren().add(textField); 134 } 135 136 // move fake focus in to the textfield if the comboBox is editable 137 comboBoxBase.focusedProperty().addListener((ov, t, hasFocus) -> { 138 if (getEditor() != null) { 139 // Fix for the regression noted in a comment in RT-29885. 140 ((FakeFocusTextField)textField).setFakeFocus(hasFocus); 141 } 142 }); 143 144 comboBoxBase.addEventFilter(KeyEvent.ANY, ke -> { 145 if (textField == null || getEditor() == null) { 146 handleKeyEvent(ke, false); 147 } else { 148 // This prevents a stack overflow from our rebroadcasting of the 149 // event to the textfield that occurs in the final else statement 150 // of the conditions below. 151 if (ke.getTarget().equals(textField)) return; 152 153 switch (ke.getCode()) { 154 case ESCAPE: 155 case F10: 156 // Allow to bubble up. 157 break; 158 159 case ENTER: 160 handleKeyEvent(ke, true); 161 break; 162 163 default: 164 // Fix for the regression noted in a comment in RT-29885. 165 // This forwards the event down into the TextField when 166 // the key event is actually received by the ComboBox. 167 textField.fireEvent(ke.copyFor(textField, textField)); 168 ke.consume(); 169 } 170 } 171 }); 172 173 // RT-38978: Forward input method events to TextField if editable. 174 if (comboBoxBase.getOnInputMethodTextChanged() == null) { 175 comboBoxBase.setOnInputMethodTextChanged(event -> { 176 if (textField != null && getEditor() != null && comboBoxBase.getScene().getFocusOwner() == comboBoxBase) { 177 if (textField.getOnInputMethodTextChanged() != null) { 178 textField.getOnInputMethodTextChanged().handle(event); 179 } 180 } 181 }); 182 } 183 184 // Fix for RT-36902, where focus traversal was getting stuck inside the ComboBox 185 ParentHelper.setTraversalEngine(comboBoxBase, 186 new ParentTraversalEngine(comboBoxBase, new Algorithm() { 187 188 @Override public Node select(Node owner, Direction dir, TraversalContext context) { 189 return null; 190 } 191 192 @Override public Node selectFirst(TraversalContext context) { 193 return null; 194 } 195 196 @Override public Node selectLast(TraversalContext context) { 197 return null; 198 } 199 })); 200 201 updateEditable(); 202 } 203 204 205 206 /*************************************************************************** 207 * * 208 * Public API * 209 * * 210 **************************************************************************/ 211 212 /** 213 * This method should return the Node that will be displayed when the user 214 * clicks on the ComboBox 'button' area. 215 * @return the Node that will be displayed when the user clicks on the 216 * ComboBox 'button' area 217 */ 218 protected abstract Node getPopupContent(); 219 220 /** 221 * Subclasses are responsible for getting the editor. This will be removed 222 * in FX 9 when the editor property is moved up to ComboBoxBase with 223 * JDK-8130354 224 * 225 * Note: ComboBoxListViewSkin should return null if editable is false, even 226 * if the ComboBox does have an editor set. 227 * @return the editor 228 */ 229 protected abstract TextField getEditor(); 230 231 /** 232 * Subclasses are responsible for getting the converter. This will be 233 * removed in FX 9 when the converter property is moved up to ComboBoxBase 234 * with JDK-8130354. 235 * @return the string converter 236 */ 237 protected abstract StringConverter<T> getConverter(); 238 239 /** {@inheritDoc} */ 240 @Override public void show() { 241 if (getSkinnable() == null) { 242 throw new IllegalStateException("ComboBox is null"); 243 } 244 245 Node content = getPopupContent(); 246 if (content == null) { 247 throw new IllegalStateException("Popup node is null"); 248 } 249 250 if (getPopup().isShowing()) return; 251 252 positionAndShowPopup(); 253 } 254 255 /** {@inheritDoc} */ 256 @Override public void hide() { 257 if (popup != null && popup.isShowing()) { 258 popup.hide(); 259 } 260 } 261 262 263 264 /*************************************************************************** 265 * * 266 * Private implementation * 267 * * 268 **************************************************************************/ 269 270 PopupControl getPopup() { 271 if (popup == null) { 272 createPopup(); 273 } 274 return popup; 275 } 276 277 TextField getEditableInputNode() { 278 if (textField == null && getEditor() != null) { 279 textField = getEditor(); 280 textField.setFocusTraversable(false); 281 textField.promptTextProperty().bind(comboBoxBase.promptTextProperty()); 282 textField.tooltipProperty().bind(comboBoxBase.tooltipProperty()); 283 284 // Fix for JDK-8145515 - in short the ComboBox was firing the event down to 285 // the TextField, and then the TextField was firing it back up to the 286 // ComboBox, resulting in stack overflows. 287 textField.getProperties().put(TextInputControlBehavior.DISABLE_FORWARD_TO_PARENT, true); 288 289 // Fix for RT-21406: ComboBox do not show initial text value 290 initialTextFieldValue = textField.getText(); 291 // End of fix (see updateDisplayNode below for the related code) 292 } 293 294 return textField; 295 } 296 297 void setTextFromTextFieldIntoComboBoxValue() { 298 if (getEditor() != null) { 299 StringConverter<T> c = getConverter(); 300 if (c != null) { 301 T oldValue = comboBoxBase.getValue(); 302 T value = oldValue; 303 String text = textField.getText(); 304 305 // conditional check here added due to RT-28245 306 if (oldValue == null && (text == null || text.isEmpty())) { 307 value = null; 308 } else { 309 try { 310 value = c.fromString(text); 311 } catch (Exception ex) { 312 // Most likely a parsing error, such as DateTimeParseException 313 } 314 } 315 316 if ((value != null || oldValue != null) && (value == null || !value.equals(oldValue))) { 317 // no point updating values needlessly if they are the same 318 comboBoxBase.setValue(value); 319 } 320 321 updateDisplayNode(); 322 } 323 } 324 } 325 326 void updateDisplayNode() { 327 if (textField != null && getEditor() != null) { 328 T value = comboBoxBase.getValue(); 329 StringConverter<T> c = getConverter(); 330 331 if (initialTextFieldValue != null && ! initialTextFieldValue.isEmpty()) { 332 // Remainder of fix for RT-21406: ComboBox do not show initial text value 333 textField.setText(initialTextFieldValue); 334 initialTextFieldValue = null; 335 // end of fix 336 } else { 337 String stringValue = c.toString(value); 338 if (value == null || stringValue == null) { 339 textField.setText(""); 340 } else if (! stringValue.equals(textField.getText())) { 341 textField.setText(stringValue); 342 } 343 } 344 } 345 } 346 347 void updateEditable() { 348 TextField newTextField = getEditor(); 349 350 if (getEditor() == null) { 351 // remove event filters 352 if (textField != null) { 353 textField.removeEventFilter(MouseEvent.DRAG_DETECTED, textFieldMouseEventHandler); 354 textField.removeEventFilter(DragEvent.ANY, textFieldDragEventHandler); 355 356 comboBoxBase.setInputMethodRequests(null); 357 } 358 } else if (newTextField != null) { 359 // add event filters 360 361 // Fix for RT-31093 - drag events from the textfield were not surfacing 362 // properly for the ComboBox. 363 newTextField.addEventFilter(MouseEvent.DRAG_DETECTED, textFieldMouseEventHandler); 364 newTextField.addEventFilter(DragEvent.ANY, textFieldDragEventHandler); 365 366 // RT-38978: Forward input method requests to TextField. 367 comboBoxBase.setInputMethodRequests(new ExtendedInputMethodRequests() { 368 @Override public Point2D getTextLocation(int offset) { 369 return newTextField.getInputMethodRequests().getTextLocation(offset); 370 } 371 372 @Override public int getLocationOffset(int x, int y) { 373 return newTextField.getInputMethodRequests().getLocationOffset(x, y); 374 } 375 376 @Override public void cancelLatestCommittedText() { 377 newTextField.getInputMethodRequests().cancelLatestCommittedText(); 378 } 379 380 @Override public String getSelectedText() { 381 return newTextField.getInputMethodRequests().getSelectedText(); 382 } 383 384 @Override public int getInsertPositionOffset() { 385 return ((ExtendedInputMethodRequests)newTextField.getInputMethodRequests()).getInsertPositionOffset(); 386 } 387 388 @Override public String getCommittedText(int begin, int end) { 389 return ((ExtendedInputMethodRequests)newTextField.getInputMethodRequests()).getCommittedText(begin, end); 390 } 391 392 @Override public int getCommittedTextLength() { 393 return ((ExtendedInputMethodRequests)newTextField.getInputMethodRequests()).getCommittedTextLength(); 394 } 395 }); 396 } 397 398 textField = newTextField; 399 } 400 401 private Point2D getPrefPopupPosition() { 402 return com.sun.javafx.util.Utils.pointRelativeTo(getSkinnable(), getPopupContent(), HPos.CENTER, VPos.BOTTOM, 0, 0, true); 403 } 404 405 private void positionAndShowPopup() { 406 final PopupControl _popup = getPopup(); 407 _popup.getScene().setNodeOrientation(getSkinnable().getEffectiveNodeOrientation()); 408 409 410 final Node popupContent = getPopupContent(); 411 sizePopup(); 412 413 Point2D p = getPrefPopupPosition(); 414 415 popupNeedsReconfiguring = true; 416 reconfigurePopup(); 417 418 final ComboBoxBase<T> comboBoxBase = getSkinnable(); 419 _popup.show(comboBoxBase.getScene().getWindow(), 420 snapPositionX(p.getX()), 421 snapPositionY(p.getY())); 422 423 popupContent.requestFocus(); 424 425 // second call to sizePopup here to enable proper sizing _after_ the popup 426 // has been displayed. See RT-37622 for more detail. 427 sizePopup(); 428 } 429 430 private void sizePopup() { 431 final Node popupContent = getPopupContent(); 432 433 if (popupContent instanceof Region) { 434 // snap to pixel 435 final Region r = (Region) popupContent; 436 437 // 0 is used here for the width due to RT-46097 438 double prefHeight = snapSizeY(r.prefHeight(0)); 439 double minHeight = snapSizeY(r.minHeight(0)); 440 double maxHeight = snapSizeY(r.maxHeight(0)); 441 double h = snapSizeY(Math.min(Math.max(prefHeight, minHeight), Math.max(minHeight, maxHeight))); 442 443 double prefWidth = snapSizeX(r.prefWidth(h)); 444 double minWidth = snapSizeX(r.minWidth(h)); 445 double maxWidth = snapSizeX(r.maxWidth(h)); 446 double w = snapSizeX(Math.min(Math.max(prefWidth, minWidth), Math.max(minWidth, maxWidth))); 447 448 popupContent.resize(w, h); 449 } else { 450 popupContent.autosize(); 451 } 452 } 453 454 private void createPopup() { 455 popup = new PopupControl() { 456 @Override public Styleable getStyleableParent() { 457 return ComboBoxPopupControl.this.getSkinnable(); 458 } 459 { 460 setSkin(new Skin<Skinnable>() { 461 @Override public Skinnable getSkinnable() { return ComboBoxPopupControl.this.getSkinnable(); } 462 @Override public Node getNode() { return getPopupContent(); } 463 @Override public void dispose() { } 464 }); 465 } 466 }; 467 popup.getStyleClass().add(Properties.COMBO_BOX_STYLE_CLASS); 468 popup.setConsumeAutoHidingEvents(false); 469 popup.setAutoHide(true); 470 popup.setAutoFix(true); 471 popup.setHideOnEscape(true); 472 popup.setOnAutoHide(e -> getBehavior().onAutoHide(popup)); 473 popup.addEventHandler(MouseEvent.MOUSE_CLICKED, t -> { 474 // RT-18529: We listen to mouse input that is received by the popup 475 // but that is not consumed, and assume that this is due to the mouse 476 // clicking outside of the node, but in areas such as the 477 // dropshadow. 478 getBehavior().onAutoHide(popup); 479 }); 480 popup.addEventHandler(WindowEvent.WINDOW_HIDDEN, t -> { 481 // Make sure the accessibility focus returns to the combo box 482 // after the window closes. 483 getSkinnable().notifyAccessibleAttributeChanged(AccessibleAttribute.FOCUS_NODE); 484 }); 485 486 // Fix for RT-21207 487 InvalidationListener layoutPosListener = o -> { 488 popupNeedsReconfiguring = true; 489 reconfigurePopup(); 490 }; 491 getSkinnable().layoutXProperty().addListener(layoutPosListener); 492 getSkinnable().layoutYProperty().addListener(layoutPosListener); 493 getSkinnable().widthProperty().addListener(layoutPosListener); 494 getSkinnable().heightProperty().addListener(layoutPosListener); 495 496 // RT-36966 - if skinnable's scene becomes null, ensure popup is closed 497 getSkinnable().sceneProperty().addListener(o -> { 498 if (((ObservableValue)o).getValue() == null) { 499 hide(); 500 } 501 }); 502 503 } 504 505 void reconfigurePopup() { 506 // RT-26861. Don't call getPopup() here because it may cause the popup 507 // to be created too early, which leads to memory leaks like those noted 508 // in RT-32827. 509 if (popup == null) return; 510 511 final boolean isShowing = popup.isShowing(); 512 if (! isShowing) return; 513 514 if (! popupNeedsReconfiguring) return; 515 popupNeedsReconfiguring = false; 516 517 final Point2D p = getPrefPopupPosition(); 518 519 final Node popupContent = getPopupContent(); 520 final double minWidth = popupContent.prefWidth(Region.USE_COMPUTED_SIZE); 521 final double minHeight = popupContent.prefHeight(Region.USE_COMPUTED_SIZE); 522 523 if (p.getX() > -1) popup.setAnchorX(p.getX()); 524 if (p.getY() > -1) popup.setAnchorY(p.getY()); 525 if (minWidth > -1) popup.setMinWidth(minWidth); 526 if (minHeight > -1) popup.setMinHeight(minHeight); 527 528 final Bounds b = popupContent.getLayoutBounds(); 529 final double currentWidth = b.getWidth(); 530 final double currentHeight = b.getHeight(); 531 final double newWidth = currentWidth < minWidth ? minWidth : currentWidth; 532 final double newHeight = currentHeight < minHeight ? minHeight : currentHeight; 533 534 if (newWidth != currentWidth || newHeight != currentHeight) { 535 // Resizing content to resolve issues such as RT-32582 and RT-33700 536 // (where RT-33700 was introduced due to a previous fix for RT-32582) 537 popupContent.resize(newWidth, newHeight); 538 if (popupContent instanceof Region) { 539 ((Region)popupContent).setMinSize(newWidth, newHeight); 540 ((Region)popupContent).setPrefSize(newWidth, newHeight); 541 } 542 } 543 } 544 545 private void handleKeyEvent(KeyEvent ke, boolean doConsume) { 546 // When the user hits the enter or F4 keys, we respond before 547 // ever giving the event to the TextField. 548 if (ke.getCode() == KeyCode.ENTER) { 549 if (ke.isConsumed() || ke.getEventType() != KeyEvent.KEY_RELEASED) { 550 return; 551 } 552 setTextFromTextFieldIntoComboBoxValue(); 553 554 if (doConsume && comboBoxBase.getOnAction() != null) { 555 ke.consume(); 556 } else if (textField != null) { 557 textField.fireEvent(ke); 558 } 559 } else if (ke.getCode() == KeyCode.F4) { 560 if (ke.getEventType() == KeyEvent.KEY_RELEASED) { 561 if (comboBoxBase.isShowing()) comboBoxBase.hide(); 562 else comboBoxBase.show(); 563 } 564 ke.consume(); // we always do a consume here (otherwise unit tests fail) 565 } else if (ke.getCode() == KeyCode.F10 || ke.getCode() == KeyCode.ESCAPE) { 566 // RT-23275: The TextField fires F10 and ESCAPE key events 567 // up to the parent, which are then fired back at the 568 // TextField, and this ends up in an infinite loop until 569 // the stack overflows. So, here we consume these two 570 // events and stop them from going any further. 571 if (doConsume) ke.consume(); 572 } 573 } 574 575 576 577 /*************************************************************************** 578 * * 579 * Support classes * 580 * * 581 **************************************************************************/ 582 583 584 585 586 587 /*************************************************************************** 588 * * 589 * Stylesheet Handling * 590 * * 591 **************************************************************************/ 592 593 }