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