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 }