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 javafx.scene.control.skin; 27 28 import com.sun.javafx.scene.control.ContextMenuContent; 29 import com.sun.javafx.scene.control.behavior.BehaviorBase; 30 import javafx.beans.WeakInvalidationListener; 31 import javafx.scene.Node; 32 import javafx.scene.control.Accordion; 33 import javafx.scene.control.Button; 34 import javafx.scene.control.Control; 35 import javafx.scene.control.SkinBase; 36 import javafx.util.StringConverter; 37 import javafx.beans.InvalidationListener; 38 import javafx.collections.ListChangeListener; 39 import javafx.collections.ObservableList; 40 import javafx.geometry.HPos; 41 import javafx.geometry.Side; 42 import javafx.geometry.VPos; 43 import javafx.scene.control.ChoiceBox; 44 import javafx.scene.control.ContextMenu; 45 import javafx.scene.control.Label; 46 import javafx.scene.control.MenuItem; 47 import javafx.scene.control.RadioMenuItem; 48 import javafx.scene.control.SelectionModel; 49 import javafx.scene.control.Separator; 50 import javafx.scene.control.SeparatorMenuItem; 51 import javafx.scene.control.ToggleGroup; 52 import javafx.scene.layout.StackPane; 53 import javafx.scene.text.Text; 54 55 import com.sun.javafx.scene.control.behavior.ChoiceBoxBehavior; 56 import javafx.collections.WeakListChangeListener; 57 58 59 /** 60 * Default skin implementation for the {@link ChoiceBox} control. 61 * 62 * @see ChoiceBox 63 * @since 9 64 */ 65 public class ChoiceBoxSkin<T> extends SkinBase<ChoiceBox<T>> { 66 67 /*************************************************************************** 68 * * 69 * Private fields * 70 * * 71 **************************************************************************/ 72 73 private ObservableList<T> choiceBoxItems; 74 75 private ContextMenu popup; 76 77 // The region that shows the "arrow" box portion 78 private StackPane openButton; 79 80 private final ToggleGroup toggleGroup = new ToggleGroup(); 81 82 /* 83 * Watch for if the user changes the selected index, and if so, we toggle 84 * the selection in the toggle group (so the check shows in the right place) 85 */ 86 private SelectionModel<T> selectionModel; 87 88 private Label label; 89 90 private final BehaviorBase<ChoiceBox<T>> behavior; 91 92 93 94 /*************************************************************************** 95 * * 96 * Listeners * 97 * * 98 **************************************************************************/ 99 100 private final ListChangeListener<T> choiceBoxItemsListener = new ListChangeListener<T>() { 101 @Override public void onChanged(Change<? extends T> c) { 102 while (c.next()) { 103 if (c.getRemovedSize() > 0 || c.wasPermutated()) { 104 toggleGroup.getToggles().clear(); 105 popup.getItems().clear(); 106 int i = 0; 107 for (T obj : c.getList()) { 108 addPopupItem(obj, i); 109 i++; 110 } 111 } else { 112 for (int i = c.getFrom(); i < c.getTo(); i++) { 113 final T obj = c.getList().get(i); 114 addPopupItem(obj, i); 115 } 116 } 117 } 118 updateSelection(); 119 getSkinnable().requestLayout(); // RT-18052 resize of choicebox should happen immediately. 120 } 121 }; 122 123 private final WeakListChangeListener<T> weakChoiceBoxItemsListener = 124 new WeakListChangeListener<T>(choiceBoxItemsListener); 125 126 private final InvalidationListener itemsObserver; 127 128 129 130 /*************************************************************************** 131 * * 132 * Constructors * 133 * * 134 **************************************************************************/ 135 136 /** 137 * Creates a new ChoiceBoxSkin instance, installing the necessary child 138 * nodes into the Control {@link Control#getChildren() children} list, as 139 * well as the necessary input mappings for handling key, mouse, etc events. 140 * 141 * @param control The control that this skin should be installed onto. 142 */ 143 public ChoiceBoxSkin(ChoiceBox<T> control) { 144 super(control); 145 146 // install default input map for the ChoiceBox control 147 behavior = new ChoiceBoxBehavior<>(control); 148 // control.setInputMap(behavior.getInputMap()); 149 150 initialize(); 151 152 itemsObserver = observable -> updateChoiceBoxItems(); 153 control.itemsProperty().addListener(new WeakInvalidationListener(itemsObserver)); 154 155 control.requestLayout(); 156 registerChangeListener(control.selectionModelProperty(), e -> updateSelectionModel()); 157 registerChangeListener(control.showingProperty(), e -> { 158 if (getSkinnable().isShowing()) { 159 MenuItem item = null; 160 161 SelectionModel sm = getSkinnable().getSelectionModel(); 162 if (sm == null) return; 163 164 long currentSelectedIndex = sm.getSelectedIndex(); 165 int itemInControlCount = choiceBoxItems.size(); 166 boolean hasSelection = currentSelectedIndex >= 0 && currentSelectedIndex < itemInControlCount; 167 if (hasSelection) { 168 item = popup.getItems().get((int) currentSelectedIndex); 169 if (item != null && item instanceof RadioMenuItem) ((RadioMenuItem)item).setSelected(true); 170 } else { 171 if (itemInControlCount > 0) item = popup.getItems().get(0); 172 } 173 174 // This is a fix for RT-9071. Ideally this won't be necessary in 175 // the long-run, but for now at least this resolves the 176 // positioning 177 // problem of ChoiceBox inside a Cell. 178 getSkinnable().autosize(); 179 // -- End of RT-9071 fix 180 181 double y = 0; 182 183 if (popup.getSkin() != null) { 184 ContextMenuContent cmContent = (ContextMenuContent)popup.getSkin().getNode(); 185 if (cmContent != null && currentSelectedIndex != -1) { 186 y = -(cmContent.getMenuYOffset((int)currentSelectedIndex)); 187 } 188 } 189 190 popup.show(getSkinnable(), Side.BOTTOM, 2, y); 191 } else { 192 popup.hide(); 193 } 194 }); 195 registerChangeListener(control.itemsProperty(), e -> { 196 updateChoiceBoxItems(); 197 updatePopupItems(); 198 updateSelectionModel(); 199 updateSelection(); 200 if(selectionModel != null && selectionModel.getSelectedIndex() == -1) { 201 label.setText(""); // clear label text when selectedIndex is -1 202 } 203 }); 204 registerChangeListener(control.getSelectionModel().selectedItemProperty(), e -> { 205 if (getSkinnable().getSelectionModel() != null) { 206 int index = getSkinnable().getSelectionModel().getSelectedIndex(); 207 if (index != -1) { 208 MenuItem item = popup.getItems().get(index); 209 if (item instanceof RadioMenuItem) ((RadioMenuItem)item).setSelected(true); 210 } 211 } 212 }); 213 registerChangeListener(control.converterProperty(), e -> { 214 updateChoiceBoxItems(); 215 updatePopupItems(); 216 }); 217 } 218 219 220 221 /*************************************************************************** 222 * * 223 * Public API * 224 * * 225 **************************************************************************/ 226 227 /** {@inheritDoc} */ 228 @Override public void dispose() { 229 super.dispose(); 230 231 if (behavior != null) { 232 behavior.dispose(); 233 } 234 } 235 236 /** {@inheritDoc} */ 237 @Override protected void layoutChildren(final double x, final double y, 238 final double w, final double h) { 239 // open button width/height 240 double obw = openButton.prefWidth(-1); 241 242 ChoiceBox<T> control = getSkinnable(); 243 label.resizeRelocate(x, y, w, h); 244 openButton.resize(obw, openButton.prefHeight(-1)); 245 positionInArea(openButton, (x+w) - obw, 246 y, obw, h, /*baseline ignored*/0, HPos.CENTER, VPos.CENTER); 247 } 248 249 /** {@inheritDoc} */ 250 @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 251 final double boxWidth = label.minWidth(-1) + openButton.minWidth(-1); 252 final double popupWidth = popup.minWidth(-1); 253 return leftInset + Math.max(boxWidth, popupWidth) + rightInset; 254 } 255 256 /** {@inheritDoc} */ 257 @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 258 final double displayHeight = label.minHeight(-1); 259 final double openButtonHeight = openButton.minHeight(-1); 260 return topInset + Math.max(displayHeight, openButtonHeight) + bottomInset; 261 } 262 263 /** {@inheritDoc} */ 264 @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 265 final double boxWidth = label.prefWidth(-1) 266 + openButton.prefWidth(-1); 267 double popupWidth = popup.prefWidth(-1); 268 if (popupWidth <= 0) { // first time: when the popup has not shown yet 269 if (popup.getItems().size() > 0){ 270 popupWidth = (new Text(((MenuItem)popup.getItems().get(0)).getText())).prefWidth(-1); 271 } 272 } 273 return (popup.getItems().size() == 0) ? 50 : leftInset + Math.max(boxWidth, popupWidth) 274 + rightInset; 275 } 276 277 /** {@inheritDoc} */ 278 @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 279 final double displayHeight = label.prefHeight(-1); 280 final double openButtonHeight = openButton.prefHeight(-1); 281 return topInset 282 + Math.max(displayHeight, openButtonHeight) 283 + bottomInset; 284 } 285 286 /** {@inheritDoc} */ 287 @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 288 return getSkinnable().prefHeight(width); 289 } 290 291 /** {@inheritDoc} */ 292 @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 293 return getSkinnable().prefWidth(height); 294 } 295 296 297 298 /*************************************************************************** 299 * * 300 * Private implementation * 301 * * 302 **************************************************************************/ 303 304 private void initialize() { 305 updateChoiceBoxItems(); 306 307 label = new Label(); 308 label.setMnemonicParsing(false); // ChoiceBox doesn't do Mnemonics 309 310 openButton = new StackPane(); 311 openButton.getStyleClass().setAll("open-button"); 312 313 StackPane region = new StackPane(); 314 region.getStyleClass().setAll("arrow"); 315 openButton.getChildren().clear(); 316 openButton.getChildren().addAll(region); 317 318 popup = new ContextMenu(); 319 // When popup is hidden by autohide - the ChoiceBox Showing property needs 320 // to be updated. So we listen to when autohide happens. Calling hide() 321 // there after causes Showing to be set to false 322 popup.showingProperty().addListener((o, ov, nv) -> { 323 if (!nv) { 324 getSkinnable().hide(); 325 } 326 }); 327 // This is used as a way of accessing the context menu within the ChoiceBox. 328 popup.setId("choice-box-popup-menu"); 329 // popup.getItems().clear(); 330 // popup.getItems().addAll(popupItems); 331 // popup.setManaged(false); 332 // popup.visibleProperty().addListener(new InvalidationListener() { 333 // @Override public void invalidated(ObservableValue valueModel) { 334 // if (popup.isVisible() { 335 //// RadioMenuItem selected = (RadioMenuItem) toggleGroup.getSelectedToggle(); 336 //// if (selected != null) selected.requestFocus(); 337 // } else { 338 // getBehavior().close(); 339 // } 340 // } 341 // }); 342 getChildren().setAll(label, openButton); 343 344 updatePopupItems(); 345 346 updateSelectionModel(); 347 updateSelection(); 348 if(selectionModel != null && selectionModel.getSelectedIndex() == -1) { 349 label.setText(""); // clear label text when selectedIndex is -1 350 } 351 } 352 353 private void updateChoiceBoxItems() { 354 if (choiceBoxItems != null) { 355 choiceBoxItems.removeListener(weakChoiceBoxItemsListener); 356 } 357 choiceBoxItems = getSkinnable().getItems(); 358 if (choiceBoxItems != null) { 359 choiceBoxItems.addListener(weakChoiceBoxItemsListener); 360 } 361 } 362 363 // Test only purpose 364 String getChoiceBoxSelectedText() { 365 return label.getText(); 366 } 367 368 private void addPopupItem(final T o, int i) { 369 MenuItem popupItem = null; 370 if (o instanceof Separator) { 371 // We translate the Separator into a SeparatorMenuItem... 372 popupItem = new SeparatorMenuItem(); 373 } else if (o instanceof SeparatorMenuItem) { 374 popupItem = (SeparatorMenuItem) o; 375 } else { 376 StringConverter<T> c = getSkinnable().getConverter(); 377 String displayString = (c == null) ? ((o == null) ? "" : o.toString()) : c.toString(o); 378 final RadioMenuItem item = new RadioMenuItem(displayString); 379 item.setId("choice-box-menu-item"); 380 item.setToggleGroup(toggleGroup); 381 item.setOnAction(e -> { 382 if (selectionModel == null) return; 383 int index = getSkinnable().getItems().indexOf(o); 384 selectionModel.select(index); 385 item.setSelected(true); 386 }); 387 popupItem = item; 388 } 389 popupItem.setMnemonicParsing(false); // ChoiceBox doesn't do Mnemonics 390 popup.getItems().add(i, popupItem); 391 } 392 393 private void updatePopupItems() { 394 toggleGroup.getToggles().clear(); 395 popup.getItems().clear(); 396 toggleGroup.selectToggle(null); 397 398 for (int i = 0; i < choiceBoxItems.size(); i++) { 399 T o = choiceBoxItems.get(i); 400 addPopupItem(o, i); 401 } 402 } 403 404 private void updateSelectionModel() { 405 if (selectionModel != null) { 406 selectionModel.selectedIndexProperty().removeListener(selectionChangeListener); 407 } 408 this.selectionModel = getSkinnable().getSelectionModel(); 409 if (selectionModel != null) { 410 selectionModel.selectedIndexProperty().addListener(selectionChangeListener); 411 } 412 } 413 414 private InvalidationListener selectionChangeListener = observable -> { 415 updateSelection(); 416 }; 417 418 private void updateSelection() { 419 if (selectionModel == null || selectionModel.isEmpty()) { 420 toggleGroup.selectToggle(null); 421 label.setText(""); 422 } else { 423 int selectedIndex = selectionModel.getSelectedIndex(); 424 if (selectedIndex == -1 || selectedIndex > popup.getItems().size()) { 425 label.setText(""); // clear label text 426 return; 427 } 428 if (selectedIndex < popup.getItems().size()) { 429 MenuItem selectedItem = popup.getItems().get(selectedIndex); 430 if (selectedItem instanceof RadioMenuItem) { 431 ((RadioMenuItem) selectedItem).setSelected(true); 432 toggleGroup.selectToggle(null); 433 } 434 // update the label 435 label.setText(popup.getItems().get(selectedIndex).getText()); 436 } 437 } 438 } 439 }