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