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.collections.ListChangeListener;
  34 import javafx.geometry.Pos;
  35 import javafx.scene.Node;
  36 import javafx.scene.control.ButtonBar;
  37 import javafx.scene.control.ButtonBar.ButtonData;
  38 import javafx.scene.layout.HBox;
  39 import javafx.scene.layout.Pane;
  40 import javafx.scene.layout.Priority;
  41 import javafx.scene.layout.Region;
  42 
  43 import com.sun.javafx.scene.control.behavior.BehaviorBase;
  44 import com.sun.javafx.scene.control.behavior.KeyBinding;
  45 import com.sun.javafx.scene.control.skin.BehaviorSkinBase;
  46 
  47 public class ButtonBarSkin extends BehaviorSkinBase<ButtonBar, BehaviorBase<ButtonBar>> {
  48     
  49     /**************************************************************************
  50      * 
  51      * Static fields
  52      * 
  53      **************************************************************************/
  54 
  55     private static final double GAP_SIZE = 10; 
  56     
  57     private static final String CATEGORIZED_TYPES = "LRHEYNXBIACO"; //$NON-NLS-1$
  58     
  59     // represented as a ButtonType
  60     public static final String BUTTON_DATA_PROPERTY  = "controlfx.button.type"; //$NON-NLS-1$
  61     
  62     // allows to exclude button from uniform resizing
  63     public static final String BUTTON_SIZE_INDEPENDENCE = "controlfx.button.size.indepenence"; //$NON-NLS-1$
  64     
  65     // pick an arbitrary number
  66     private static final double DO_NOT_CHANGE_SIZE = Double.MAX_VALUE - 100;
  67     
  68     
  69     /**************************************************************************
  70      * 
  71      * fields
  72      * 
  73      **************************************************************************/
  74     
  75     private HBox layout;
  76     
  77     
  78     
  79     /**************************************************************************
  80      * 
  81      * Constructors
  82      * 
  83      **************************************************************************/
  84 
  85     public ButtonBarSkin(final ButtonBar control) {
  86         super(control, new BehaviorBase<>(control, Collections.<KeyBinding> emptyList()));
  87         
  88         this.layout = new HBox(GAP_SIZE) {
  89             @Override
  90             protected void layoutChildren() {
  91                 // has to be called first or layout is not correct somtimes 
  92                 resizeButtons();
  93                 super.layoutChildren();
  94             }
  95         };
  96         this.layout.setAlignment(Pos.CENTER);
  97         this.layout.getStyleClass().add("container");
  98         getChildren().add(layout);
  99         
 100         layoutButtons();
 101         
 102         control.getButtons().addListener(new ListChangeListener<Node>() {
 103             @Override public void onChanged(ListChangeListener.Change<? extends Node> change) {
 104                 layoutButtons();
 105             }
 106         });
 107         
 108         registerChangeListener(control.buttonOrderProperty(), "BUTTON_ORDER"); //$NON-NLS-1$
 109         registerChangeListener(control.buttonMinWidthProperty(), "BUTTON_MIN_WIDTH"); //$NON-NLS-1$
 110     }
 111     
 112     
 113     /**************************************************************************
 114      * 
 115      * Overriding public API
 116      * 
 117      **************************************************************************/
 118     
 119     @Override protected void handleControlPropertyChanged(String p) {
 120         super.handleControlPropertyChanged(p);
 121         
 122         if ("BUTTON_ORDER".equals(p)) { //$NON-NLS-1$
 123             layoutButtons();
 124         } else if ("BUTTON_MIN_WIDTH".equals(p)) { //$NON-NLS-1$
 125 //            layoutButtons();
 126             resizeButtons();
 127         }
 128     }
 129     
 130     
 131     
 132     /**************************************************************************
 133      * 
 134      * Implementation
 135      * 
 136      **************************************************************************/
 137     
 138     private void layoutButtons() {
 139         final ButtonBar buttonBar = getSkinnable();
 140         final List<? extends Node> buttons = buttonBar.getButtons();
 141         final double buttonMinWidth = buttonBar.getButtonMinWidth();
 142         
 143         Map<String, List<Node>> buttonMap = buildButtonMap(buttons);
 144         String buttonOrder = getSkinnable().getButtonOrder();
 145         
 146         if (buttonOrder == null || buttonOrder.isEmpty()) {
 147             throw new IllegalStateException("ButtonBar buttonOrder string can not be null or empty"); //$NON-NLS-1$
 148         }
 149          
 150         char[] buttonOrderArr = buttonOrder.toCharArray();
 151         layout.getChildren().clear();
 152         
 153         int buttonIndex = 0; // to determine edge cases
 154         Spacer spacer = Spacer.NONE;
 155         
 156         for (int i = 0; i < buttonOrderArr.length; i++) {
 157             char type = buttonOrderArr[i];
 158             boolean edgeCase = buttonIndex <= 0 && buttonIndex >= buttons.size()-1;
 159             boolean hasChildren = ! layout.getChildren().isEmpty();
 160             if (type == '+') {
 161                 spacer = spacer.replace(Spacer.DYNAMIC);
 162             } else if (type == '_' && hasChildren) {
 163                 spacer = spacer.replace(Spacer.FIXED);
 164             } else {
 165                 List<Node> buttonList = buttonMap.get(String.valueOf(type).toUpperCase());
 166                 if (buttonList != null) {
 167                     
 168                     spacer.add(layout,edgeCase);
 169                     
 170                     for (Node btn: buttonList) {
 171                         sizeButton(btn, buttonMinWidth, DO_NOT_CHANGE_SIZE, Double.MAX_VALUE);
 172                         
 173                         layout.getChildren().add(btn);
 174                         HBox.setHgrow(btn, Priority.NEVER);
 175                         buttonIndex++;
 176                     }
 177                     spacer = spacer.replace(Spacer.NONE);
 178                 } 
 179             }
 180         }
 181         
 182     }
 183     
 184     // Button sizing. If buttonUniformSize is true button size = max(buttonMinSize, max(all button pref sizes))
 185     // otherwise button size = max(buttonBar.buttonMinSize, button pref size)
 186     private void resizeButtons() {
 187         final ButtonBar buttonBar = getSkinnable();
 188         double buttonMinWidth = buttonBar.getButtonMinWidth();
 189         final List<? extends Node> buttons = buttonBar.getButtons();
 190 
 191         // determine the widest button
 192         double widest = buttonMinWidth;
 193         for (Node button : buttons) {
 194             if (!ButtonBar.isButtonUniformSize(button)) {
 195                widest = Math.max(button.prefWidth(-1), widest);
 196             }
 197         }
 198         
 199         // set the width of all buttons
 200         for (Node button : buttons) {
 201             if (!ButtonBar.isButtonUniformSize(button)) {
 202                 sizeButton(button, DO_NOT_CHANGE_SIZE, widest, DO_NOT_CHANGE_SIZE);
 203             }
 204         }
 205     }
 206     
 207     private void sizeButton(Node btn, double min, double pref, double max) {
 208         if (btn instanceof Region) {
 209             Region regionBtn = (Region)btn;
 210             
 211             if (min != DO_NOT_CHANGE_SIZE) {
 212                 regionBtn.setMinWidth(min);
 213             }
 214             if (pref != DO_NOT_CHANGE_SIZE) {
 215                 regionBtn.setPrefWidth(pref);
 216             }
 217             if (max != DO_NOT_CHANGE_SIZE)
 218             regionBtn.setMaxWidth(max);
 219         }
 220     }
 221     
 222     private String getButtonType(Node btn) {
 223         ButtonData buttonType =  (ButtonData) btn.getProperties().get(BUTTON_DATA_PROPERTY);
 224         
 225         if (buttonType == null) {
 226             // just assume it is ButtonType.OTHER
 227             buttonType = ButtonData.OTHER;
 228         }
 229         
 230         String typeCode = buttonType.getTypeCode();
 231         typeCode = typeCode.length() > 0? typeCode.substring(0,1): ""; //$NON-NLS-1$
 232         return CATEGORIZED_TYPES.contains(typeCode.toUpperCase())? typeCode : ButtonData.OTHER.getTypeCode(); 
 233     }
 234     
 235     private Map<String, List<Node>> buildButtonMap( List<? extends Node> buttons ) {
 236         Map<String, List<Node>> buttonMap = new HashMap<>();
 237         for (Node btn : buttons) {
 238             if ( btn == null ) continue;
 239             String type =  getButtonType(btn); 
 240             List<Node> typedButtons = buttonMap.get(type);
 241             if ( typedButtons == null ) {
 242                 typedButtons = new ArrayList<Node>();
 243                 buttonMap.put(type, typedButtons);
 244             }
 245             typedButtons.add( btn );
 246         }
 247         return buttonMap;
 248     }
 249     
 250     
 251     
 252     /**************************************************************************
 253      * 
 254      * Support classes / enums
 255      * 
 256      **************************************************************************/
 257     
 258     private enum Spacer {
 259         FIXED {
 260             @Override protected Node create(boolean edgeCase) {
 261                 if ( edgeCase ) return null;
 262                 Region spacer = new Region();
 263                 spacer.setMinWidth(GAP_SIZE);
 264                 HBox.setHgrow(spacer, Priority.NEVER);
 265                 return spacer;
 266             }
 267         },
 268         DYNAMIC {
 269             @Override protected Node create(boolean edgeCase) {
 270                 Region spacer = new Region();
 271                 spacer.setMinWidth(edgeCase ? 0 : GAP_SIZE);
 272                 HBox.setHgrow(spacer, Priority.ALWAYS);
 273                 return spacer;
 274             }
 275 
 276             @Override public Spacer replace(Spacer spacer) {
 277                 return FIXED == spacer? this: spacer;
 278             }
 279         },
 280         NONE;
 281         
 282         protected Node create(boolean edgeCase) {
 283             return null;
 284         }
 285         
 286         public Spacer replace(Spacer spacer) {
 287             return spacer;
 288         }
 289         
 290         public void add(Pane pane, boolean edgeCase) {
 291             Node spacer = create(edgeCase);
 292             if (spacer != null) {
 293                 pane.getChildren().add(spacer);
 294             }
 295         }
 296     }
 297 }