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 }