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 }