1 /*
   2  * Copyright (c) 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 package com.sun.javafx.scene.control.skin;
  26 
  27 import java.util.ArrayList;
  28 import java.util.Collections;
  29 import java.util.HashMap;
  30 import java.util.List;
  31 import java.util.Map;
  32 
  33 import javafx.beans.InvalidationListener;
  34 import javafx.beans.property.ObjectProperty;
  35 import javafx.collections.ListChangeListener;
  36 import javafx.geometry.Pos;
  37 import javafx.scene.Node;
  38 import javafx.scene.control.Button;
  39 import javafx.scene.control.ButtonBar;
  40 import javafx.scene.control.ButtonBar.ButtonData;
  41 import javafx.scene.layout.HBox;
  42 import javafx.scene.layout.Pane;
  43 import javafx.scene.layout.Priority;
  44 import javafx.scene.layout.Region;
  45 
  46 import com.sun.javafx.scene.control.behavior.BehaviorBase;
  47 import com.sun.javafx.scene.control.behavior.KeyBinding;
  48 
  49 public class ButtonBarSkin extends BehaviorSkinBase<ButtonBar, BehaviorBase<ButtonBar>> {
  50     
  51     /**************************************************************************
  52      * 
  53      * Static fields
  54      * 
  55      **************************************************************************/
  56 
  57     private static final double GAP_SIZE = 10; 
  58     
  59     private static final String CATEGORIZED_TYPES = "LRHEYNXBIACO"; //$NON-NLS-1$
  60     
  61     // represented as a ButtonData
  62     public static final String BUTTON_DATA_PROPERTY  = "javafx.scene.control.ButtonBar.ButtonData"; //$NON-NLS-1$
  63     
  64     // allows to exclude button from uniform resizing
  65     public static final String BUTTON_SIZE_INDEPENDENCE = "javafx.scene.control.ButtonBar.independentSize"; //$NON-NLS-1$
  66     
  67     // pick an arbitrary number
  68     private static final double DO_NOT_CHANGE_SIZE = Double.MAX_VALUE - 100;
  69     
  70     
  71     /**************************************************************************
  72      * 
  73      * fields
  74      * 
  75      **************************************************************************/
  76     
  77     private HBox layout;
  78 
  79     private InvalidationListener buttonDataListener = o -> layoutButtons();
  80     
  81     
  82     
  83     /**************************************************************************
  84      * 
  85      * Constructors
  86      * 
  87      **************************************************************************/
  88 
  89     public ButtonBarSkin(final ButtonBar control) {
  90         super(control, new BehaviorBase<>(control, Collections.<KeyBinding> emptyList()));
  91         
  92         this.layout = new HBox(GAP_SIZE) {
  93             @Override
  94             protected void layoutChildren() {
  95                 // has to be called first or layout is not correct sometimes
  96                 resizeButtons();
  97                 super.layoutChildren();
  98             }
  99         };
 100         this.layout.setAlignment(Pos.CENTER);
 101         this.layout.getStyleClass().add("container");
 102         getChildren().add(layout);
 103         
 104         layoutButtons();
 105 
 106         updateButtonListeners(control.getButtons(), true);
 107         control.getButtons().addListener((ListChangeListener<Node>) c -> {
 108             while (c.next()) {
 109                 updateButtonListeners(c.getRemoved(), false);
 110                 updateButtonListeners(c.getAddedSubList(), true);
 111             }
 112             layoutButtons();
 113         });
 114         
 115         registerChangeListener(control.buttonOrderProperty(), "BUTTON_ORDER"); //$NON-NLS-1$
 116         registerChangeListener(control.buttonMinWidthProperty(), "BUTTON_MIN_WIDTH"); //$NON-NLS-1$
 117     }
 118 
 119     private void updateButtonListeners(List<? extends Node> list, boolean buttonsAdded) {
 120         if (list != null) {
 121             for (Node n : list) {
 122                 final Map<Object, Object> properties = n.getProperties();
 123                 if (properties.containsKey(ButtonBarSkin.BUTTON_DATA_PROPERTY)) {
 124                     ObjectProperty<ButtonData> property = (ObjectProperty<ButtonData>) properties.get(ButtonBarSkin.BUTTON_DATA_PROPERTY);
 125                     if (property != null) {
 126                         if (buttonsAdded) {
 127                             property.addListener(buttonDataListener);
 128                         } else {
 129                             property.removeListener(buttonDataListener);
 130                         }
 131                     }
 132                 }
 133             }
 134         }
 135     }
 136 
 137 
 138     /**************************************************************************
 139      * 
 140      * Overriding public API
 141      * 
 142      **************************************************************************/
 143     
 144     @Override protected void handleControlPropertyChanged(String p) {
 145         super.handleControlPropertyChanged(p);
 146         
 147         if ("BUTTON_ORDER".equals(p)) { //$NON-NLS-1$
 148             layoutButtons();
 149         } else if ("BUTTON_MIN_WIDTH".equals(p)) { //$NON-NLS-1$
 150 //            layoutButtons();
 151             resizeButtons();
 152         }
 153     }
 154     
 155     
 156     
 157     /**************************************************************************
 158      * 
 159      * Implementation
 160      * 
 161      **************************************************************************/
 162     
 163     private void layoutButtons() {
 164         final ButtonBar buttonBar = getSkinnable();
 165         final List<? extends Node> buttons = buttonBar.getButtons();
 166         final double buttonMinWidth = buttonBar.getButtonMinWidth();
 167         
 168         String buttonOrder = getSkinnable().getButtonOrder();
 169 
 170         layout.getChildren().clear();
 171 
 172         // empty is valid, because it is BUTTON_ORDER_NONE
 173         if (buttonOrder == null) {
 174             throw new IllegalStateException("ButtonBar buttonOrder string can not be null"); //$NON-NLS-1$
 175         }
 176 
 177         if (buttonOrder == ButtonBar.BUTTON_ORDER_NONE) {
 178             // when using BUTTON_ORDER_NONE, we just lay out the buttons in the
 179             // order they are specified, but we do right-align the buttons by
 180             // inserting a dynamic spacer.
 181             Spacer.DYNAMIC.add(layout, true);
 182             for (Node btn: buttons) {
 183                 sizeButton(btn, buttonMinWidth, DO_NOT_CHANGE_SIZE, Double.MAX_VALUE);
 184                 layout.getChildren().add(btn);
 185                 HBox.setHgrow(btn, Priority.NEVER);
 186             }
 187         } else {
 188             doButtonOrderLayout(buttonOrder);
 189         }
 190     }
 191 
 192     private void doButtonOrderLayout(String buttonOrder) {
 193         final ButtonBar buttonBar = getSkinnable();
 194         final List<? extends Node> buttons = buttonBar.getButtons();
 195         final double buttonMinWidth = buttonBar.getButtonMinWidth();
 196         Map<String, List<Node>> buttonMap = buildButtonMap(buttons);
 197 
 198         char[] buttonOrderArr = buttonOrder.toCharArray();
 199 
 200         int buttonIndex = 0; // to determine edge cases
 201         Spacer spacer = Spacer.NONE;
 202 
 203         for (int i = 0; i < buttonOrderArr.length; i++) {
 204             char type = buttonOrderArr[i];
 205             boolean edgeCase = buttonIndex <= 0 && buttonIndex >= buttons.size()-1;
 206             boolean hasChildren = ! layout.getChildren().isEmpty();
 207             if (type == '+') {
 208                 spacer = spacer.replace(Spacer.DYNAMIC);
 209             } else if (type == '_' && hasChildren) {
 210                 spacer = spacer.replace(Spacer.FIXED);
 211             } else {
 212                 List<Node> buttonList = buttonMap.get(String.valueOf(type).toUpperCase());
 213                 if (buttonList != null) {
 214                     spacer.add(layout,edgeCase);
 215 
 216                     for (Node btn: buttonList) {
 217                         sizeButton(btn, buttonMinWidth, DO_NOT_CHANGE_SIZE, Double.MAX_VALUE);
 218 
 219                         layout.getChildren().add(btn);
 220                         HBox.setHgrow(btn, Priority.NEVER);
 221                         buttonIndex++;
 222                     }
 223                     spacer = spacer.replace(Spacer.NONE);
 224                 }
 225             }
 226         }
 227 
 228         // now that all buttons have been placed, we need to ensure focus is
 229         // set on the correct button. Firstly, we check to see if any button
 230         // is of type Button (which is typically the case), and of these, if
 231         // any is a default button. If so, we request focus onto this default
 232         // button.
 233         // If there is no Button that is a default button, we subsequently look
 234         // at the ButtonData for each node and request focus on the first one
 235         // that returns true for isDefaultButton()
 236         boolean isDefaultSet = false;
 237         final int childrenCount = buttons.size();
 238         for (int i = 0; i < childrenCount; i++) {
 239             Node btn = buttons.get(i);
 240 
 241             if (btn instanceof Button && ((Button) btn).isDefaultButton()) {
 242                 btn.requestFocus();
 243                 isDefaultSet = true;
 244                 break;
 245             }
 246         }
 247         if (!isDefaultSet) {
 248             for (int i = 0; i < childrenCount; i++) {
 249                 Node btn = buttons.get(i);
 250                 ButtonData btnData = ButtonBar.getButtonData(btn);
 251 
 252                 if (btnData != null && btnData.isDefaultButton()) {
 253                     btn.requestFocus();
 254                     isDefaultSet = true;
 255                     break;
 256                 }
 257             }
 258         }
 259     }
 260     
 261     private void resizeButtons() {
 262         final ButtonBar buttonBar = getSkinnable();
 263         double buttonMinWidth = buttonBar.getButtonMinWidth();
 264         final List<? extends Node> buttons = buttonBar.getButtons();
 265 
 266         // determine the widest button
 267         double widest = buttonMinWidth;
 268         for (Node button : buttons) {
 269             if (ButtonBar.isButtonUniformSize(button)) {
 270                widest = Math.max(button.prefWidth(-1), widest);
 271             }
 272         }
 273         
 274         // set the width of all buttons
 275         for (Node button : buttons) {
 276             if (ButtonBar.isButtonUniformSize(button)) {
 277                 sizeButton(button, DO_NOT_CHANGE_SIZE, widest, DO_NOT_CHANGE_SIZE);
 278             }
 279         }
 280     }
 281     
 282     private void sizeButton(Node btn, double min, double pref, double max) {
 283         if (btn instanceof Region) {
 284             Region regionBtn = (Region)btn;
 285             
 286             if (min != DO_NOT_CHANGE_SIZE) {
 287                 regionBtn.setMinWidth(min);
 288             }
 289             if (pref != DO_NOT_CHANGE_SIZE) {
 290                 regionBtn.setPrefWidth(pref);
 291             }
 292             if (max != DO_NOT_CHANGE_SIZE) {
 293                 regionBtn.setMaxWidth(max);
 294             }
 295         }
 296     }
 297     
 298     private String getButtonType(Node btn) {
 299         ButtonData buttonType = ButtonBar.getButtonData(btn);
 300         
 301         if (buttonType == null) {
 302             // just assume it is ButtonType.OTHER
 303             buttonType = ButtonData.OTHER;
 304         }
 305         
 306         String typeCode = buttonType.getTypeCode();
 307         typeCode = typeCode.length() > 0? typeCode.substring(0,1): ""; //$NON-NLS-1$
 308         return CATEGORIZED_TYPES.contains(typeCode.toUpperCase())? typeCode : ButtonData.OTHER.getTypeCode(); 
 309     }
 310     
 311     private Map<String, List<Node>> buildButtonMap( List<? extends Node> buttons ) {
 312         Map<String, List<Node>> buttonMap = new HashMap<>();
 313         for (Node btn : buttons) {
 314             if ( btn == null ) continue;
 315             String type =  getButtonType(btn); 
 316             List<Node> typedButtons = buttonMap.get(type);
 317             if ( typedButtons == null ) {
 318                 typedButtons = new ArrayList<Node>();
 319                 buttonMap.put(type, typedButtons);
 320             }
 321             typedButtons.add( btn );
 322         }
 323         return buttonMap;
 324     }
 325     
 326     
 327     
 328     /**************************************************************************
 329      * 
 330      * Support classes / enums
 331      * 
 332      **************************************************************************/
 333     
 334     private enum Spacer {
 335         FIXED {
 336             @Override protected Node create(boolean edgeCase) {
 337                 if ( edgeCase ) return null;
 338                 Region spacer = new Region();
 339                 ButtonBar.setButtonData(spacer, ButtonData.SMALL_GAP);
 340                 spacer.setMinWidth(GAP_SIZE);
 341                 HBox.setHgrow(spacer, Priority.NEVER);
 342                 return spacer;
 343             }
 344         },
 345         DYNAMIC {
 346             @Override protected Node create(boolean edgeCase) {
 347                 Region spacer = new Region();
 348                 ButtonBar.setButtonData(spacer, ButtonData.BIG_GAP);
 349                 spacer.setMinWidth(edgeCase ? 0 : GAP_SIZE);
 350                 HBox.setHgrow(spacer, Priority.ALWAYS);
 351                 return spacer;
 352             }
 353 
 354             @Override public Spacer replace(Spacer spacer) {
 355                 return FIXED == spacer? this: spacer;
 356             }
 357         },
 358         NONE;
 359         
 360         protected Node create(boolean edgeCase) {
 361             return null;
 362         }
 363         
 364         public Spacer replace(Spacer spacer) {
 365             return spacer;
 366         }
 367         
 368         public void add(Pane pane, boolean edgeCase) {
 369             Node spacer = create(edgeCase);
 370             if (spacer != null) {
 371                 pane.getChildren().add(spacer);
 372             }
 373         }
 374     }
 375 }