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 javafx.scene.control;
  26 
  27 import javafx.beans.InvalidationListener;
  28 import javafx.beans.Observable;
  29 import javafx.beans.property.DoubleProperty;
  30 import javafx.beans.property.ObjectProperty;
  31 import javafx.beans.property.SimpleDoubleProperty;
  32 import javafx.beans.property.SimpleObjectProperty;
  33 import javafx.beans.property.SimpleStringProperty;
  34 import javafx.beans.property.StringProperty;
  35 import javafx.collections.FXCollections;
  36 import javafx.collections.ObservableList;
  37 import javafx.scene.Node;
  38 import javafx.scene.layout.HBox;
  39 
  40 import com.sun.javafx.util.Utils;
  41 import com.sun.javafx.scene.control.skin.ButtonBarSkin;
  42 import com.sun.javafx.scene.traversal.Algorithm;
  43 import com.sun.javafx.scene.traversal.Direction;
  44 import com.sun.javafx.scene.traversal.ParentTraversalEngine;
  45 import com.sun.javafx.scene.traversal.TraversalContext;
  46 import javafx.beans.value.WritableValue;
  47 import javafx.css.StyleableProperty;
  48 
  49 import java.util.Map;
  50 
  51 /**
  52  * A ButtonBar is essentially a {@link HBox}, with the additional functionality
  53  * for operating system specific button placement. In other words, any Node may
  54  * be annotated (via the {@link ButtonBar#setButtonData(Node, ButtonData)}
  55  * method, placed inside a ButtonBar (via the {@link #getButtons()} list), and will
  56  * then be positioned relative to all other nodes in the button list based on their
  57  * annotations, as well as the overarching
  58  * {@link #buttonOrderProperty() button order} specified for the ButtonBar.
  59  * 
  60  * <strong>Uniform button sizing</strong>
  61  * <p>By default all buttons are uniformly sized in a ButtonBar, meaning that all
  62  * buttons take the width of the widest button. It is possible to opt-out of this
  63  * on a per-button basis, but calling the {@link #setButtonUniformSize(Node, boolean)} method with
  64  * a boolean value of false.
  65  * 
  66  * <p>If a button is excluded from uniform sizing, it is both excluded from
  67  * being resized away from its preferred size, and also excluded from the
  68  * measuring process, so its size will not influence the maximum size calculated
  69  * for all buttons in the ButtonBar.
  70  * 
  71  * <h3>Screenshots</h3>
  72  * <p>Because a ButtonBar comes with built-in support for Windows, Mac OS
  73  * and Linux, there are three screenshots shown below, with the same buttons
  74  * laid out on each of the three operating systems.
  75  * 
  76  * <p>
  77  * <strong>Windows:</strong><br/><img src="doc-files/buttonBar-windows.png" /><br>
  78  * <strong>Mac OS:</strong><br/><img src="doc-files/buttonBar-mac.png" /><br>
  79  * <strong>Linux:</strong><br/><img src="doc-files/buttonBar-linux.png" /><br>
  80  * 
  81  * <h3>Code Samples</h3>
  82  * <p>Instantiating and using the ButtonBar is simple, simply do the following:
  83  * 
  84  * <pre>
  85  * {@code
  86  * // Create the ButtonBar instance 
  87  * ButtonBar buttonBar = new ButtonBar();
  88  * 
  89  * // Create the buttons to go into the ButtonBar
  90  * Button yesButton = new Button("Yes");
  91  * ButtonBar.setButtonData(yesButton, ButtonData.YES); 
  92  * 
  93  * Button noButton = new Button("No");
  94  * ButtonBar.setButtonData(noButton, ButtonData.NO);
  95  * 
  96  * // Add buttons to the ButtonBar
  97  * buttonBar.getButtons().addAll(yesButton, noButton);
  98  * }</pre>
  99  * 
 100  * <p>The code sample above will position the Yes and No buttons relative to the
 101  * users operating system. This means that on Windows and Linux the Yes button 
 102  * will come before the No button, whereas on Mac OS it'll be No and then Yes.
 103  * 
 104  * <p>In most cases the OS-specific layout is the best choice, but in cases 
 105  * where you want a custom layout, this is achieved be modifying the 
 106  * {@link #buttonOrderProperty() button order property}. These are cryptic-looking
 107  * strings that are shorthand representations for the button order. The built-in
 108  * orders for Windows, Mac OS and Linux are:
 109  * 
 110  * <table border="0">
 111  *   <tr>
 112  *     <td width="75"><strong>Windows:</strong></td>
 113  *     <td>L_E+U+FBXI_YNOCAH_R</td>
 114  *   </tr>
 115  *   <tr>
 116  *     <td><strong>Mac OS:</strong></td>
 117  *     <td>L_HE+U+FBIX_NCYOA_R</td>
 118  *   </tr>
 119  *   <tr>
 120  *     <td><strong>Linux:</strong></td>
 121  *     <td>L_HE+UNYACBXIO_R</td>
 122  *   </tr>
 123  * </table>
 124  *   
 125  * <p>You should refer to the {@link ButtonData} enumeration for a description of
 126  * what each of these characters mean. However, if your ButtonBar only consisted
 127  * of {@link ButtonData#YES} and {@link ButtonData#NO} buttons, you always
 128  * wanted the yes buttons before the no buttons, and you wanted the buttons to
 129  * be {@link ButtonData#BIG_GAP right-aligned}, you could do the following:
 130  * 
 131  * <pre>
 132  * {@code
 133  * // Create the ButtonBar instance 
 134  * ButtonBar buttonBar = new ButtonBar();
 135  * 
 136  * // Set the custom button order
 137  * buttonBar.setButtonOrder("+YN"); 
 138  * }</pre>
 139  * 
 140  * @see ButtonData
 141  * @since JavaFX 8u40
 142  */
 143 public class ButtonBar extends Control {
 144     
 145     // TODO add support for BUTTON_ORDER_NONE
 146     // TODO test and document what happens with unexpected button order strings
 147 
 148     /**************************************************************************
 149      * 
 150      * Static fields
 151      * 
 152      **************************************************************************/
 153 
 154     /**
 155      * The default button ordering on Windows.
 156      */
 157     public static final String BUTTON_ORDER_WINDOWS = "L_E+U+FBXI_YNOCAH_R"; //$NON-NLS-1$
 158 
 159     /**
 160      * The default button ordering on Mac OS.
 161      */
 162     public static final String BUTTON_ORDER_MAC_OS  = "L_HE+U+FBIX_NCYOA_R"; //$NON-NLS-1$
 163 
 164     /**
 165      * The default button ordering on Linux (specifically, GNOME).
 166      */
 167     public static final String BUTTON_ORDER_LINUX   = "L_HE+UNYACBXIO_R"; //$NON-NLS-1$
 168     
 169     /**
 170      * A button ordering string that specifies there is no button ordering. In 
 171      * other words, buttons will be placed in the order that exist in the 
 172      * {@link #getButtons()} list. The only aspect of layout that makes this
 173      * different than using an HBox is that the buttons are right-aligned.
 174      */
 175     public static final String BUTTON_ORDER_NONE   = ""; //$NON-NLS-1$
 176 
 177 
 178 
 179     /**************************************************************************
 180      * 
 181      * Static enumerations
 182      * 
 183      **************************************************************************/
 184 
 185     /**
 186      * An enumeration of all available button data annotations. By annotating
 187      * every button in a {@link ButtonBar} with one of these annotations, the
 188      * buttons will be appropriately positioned relative to all other buttons in
 189      * the ButtonBar.
 190      * 
 191      * <p>For details on the button order code for each ButtonData, refer to
 192      * the javadoc comment.
 193      *
 194      * @since JavaFX 8u40
 195      */
 196     public static enum ButtonData {
 197         /**
 198          * Buttons with this style tag will statically end up on the left end of the bar.
 199          * 
 200          * <p><strong>Button order code:</strong> L
 201          */
 202         LEFT("L",false,false), //$NON-NLS-1$
 203 
 204         /**
 205          * Buttons with this style tag will statically end up on the right end of the bar.
 206          * 
 207          * <p><strong>Button order code:</strong> R 
 208          */
 209         RIGHT("R", false, false), //$NON-NLS-1$
 210 
 211         /**
 212          * A tag for the "help" button that normally is supposed to be on the right.
 213          * 
 214          * <p><strong>Button order code:</strong> H
 215          */
 216         HELP("H", false, false ), //$NON-NLS-1$
 217 
 218         /**
 219          * A tag for the "help2" button that normally is supposed to be on the left.
 220          * 
 221          * <p><strong>Button order code:</strong> E
 222          */
 223         HELP_2("E", false, false), //$NON-NLS-1$
 224 
 225         /**
 226          * A tag for the "yes" button.
 227          * 
 228          * <p><strong>Is default button:</strong> True
 229          * <p><strong>Button order code:</strong> Y
 230          */
 231         YES("Y", false, true), //$NON-NLS-1$
 232 
 233         /**
 234          * A tag for the "no" button.
 235          * 
 236          * <p><strong>Is cancel button:</strong> True
 237          * <p><strong>Button order code:</strong> N
 238          */
 239         NO("N", true, false), //$NON-NLS-1$
 240 
 241         /**
 242          * A tag for the "next" or "forward" button.
 243          * 
 244          * <p><strong>Is default button:</strong> True 
 245          * <p><strong>Button order code:</strong> X
 246          */
 247         NEXT_FORWARD("X", false, true), //$NON-NLS-1$
 248 
 249         /**
 250          * A tag for the "back" or "previous" button.
 251          * 
 252          * <p><strong>Button order code:</strong> B
 253          */
 254         BACK_PREVIOUS("B", false, false), //$NON-NLS-1$
 255 
 256         /**
 257          * A tag for the "finish".
 258          * 
 259          * <p><strong>Is default button:</strong> True
 260          * <p><strong>Button order code:</strong> I
 261          */
 262         FINISH("I", false, true), //$NON-NLS-1$
 263 
 264         /**
 265          * A tag for the "apply" button.
 266          * 
 267          * <p><strong>Button order code:</strong> A
 268          */
 269         APPLY("A", false, false), //$NON-NLS-1$
 270 
 271         /**
 272          * A tag for the "cancel" or "close" button.
 273          * 
 274          * <p><strong>Is cancel button:</strong> True
 275          * <p><strong>Button order code:</strong> C
 276          */
 277         CANCEL_CLOSE("C", true, false), //$NON-NLS-1$
 278 
 279         /**
 280          * A tag for the "ok" or "done" button.
 281          * 
 282          * <p><strong>Is default button:</strong> True
 283          * <p><strong>Button order code:</strong> O
 284          */
 285         OK_DONE("O", false, true), //$NON-NLS-1$
 286 
 287         /**
 288          * All Uncategorized, Other, or "Unknown" buttons. Tag will be "other".
 289          * 
 290          * <p><strong>Button order code:</strong> U
 291          */
 292         OTHER("U", false, false), //$NON-NLS-1$
 293 
 294 
 295         /**
 296          * A glue push gap that will take as much space as it can and at least 
 297          * an "unrelated" gap. (Platform dependent)
 298          * 
 299          * <p><strong>Button order code:</strong> +
 300          */
 301         BIG_GAP("+", false, false), //$NON-NLS-1$
 302 
 303         /**
 304          * An "unrelated" gap. (Platform dependent)
 305          * 
 306          * <p><strong>Button order code:</strong> _ (underscore)
 307          */
 308         SMALL_GAP("_", false, false); //$NON-NLS-1$
 309 
 310         private final String typeCode;
 311         
 312         private final boolean cancelButton;
 313         private final boolean defaultButton;
 314         
 315         private ButtonData(String type, boolean cancelButton, boolean defaultButton) {
 316             this.typeCode = type;
 317             this.cancelButton = cancelButton;
 318             this.defaultButton = defaultButton;
 319         }
 320 
 321         /**
 322          * Returns the single character code used to represent the ButtonData
 323          * annotation in the {@link ButtonBar#buttonOrderProperty() button order}
 324          * string.
 325          */
 326         public String getTypeCode() {
 327             return typeCode;
 328         }
 329         
 330         /**
 331          * Indicates whether buttons created from the ButtonData enumeration
 332          * should be the 'cancel' button in the user interface. This typically
 333          * means that the button will respond to the escape key press, even if
 334          * the button does not have focus.
 335          * 
 336          * <p>ButtonData enumeration values that can be the cancel button have a
 337          * comment stating this in their javadoc.
 338          */
 339         public final boolean isCancelButton() {
 340             return cancelButton;
 341         }
 342         
 343         /**
 344          * Indicates whether buttons created from the ButtonData enumeration
 345          * should be the 'default' button in the user interface. This typically
 346          * means that the button will respond to enter key presses, even if the
 347          * button does not have focus.
 348          * 
 349          * <p>ButtonData enumeration values that can be the default button have
 350          * a comment stating this in their javadoc.
 351          */
 352         public final boolean isDefaultButton() {
 353             return defaultButton;
 354         }
 355     }
 356     
 357 
 358     /**
 359      * Sets the given ButtonData on the given button. If this button is
 360      * subsequently placed in a {@link ButtonBar} it will be placed in the 
 361      * correct position relative to all other buttons in the bar.
 362      * 
 363      * @param button The button to annotate with the given {@link ButtonData} value.
 364      * @param buttonData The ButtonData to designate the button as.
 365      */
 366     public static void setButtonData(Node button, ButtonData buttonData) {
 367         final Map<Object,Object> properties = button.getProperties();
 368         final ObjectProperty<ButtonData> property =
 369                 (ObjectProperty<ButtonData>) properties.getOrDefault(
 370                         ButtonBarSkin.BUTTON_DATA_PROPERTY,
 371                         new SimpleObjectProperty<>(button, "buttonData", buttonData));
 372 
 373         property.set(buttonData);
 374         properties.putIfAbsent(ButtonBarSkin.BUTTON_DATA_PROPERTY, property);
 375     }
 376     
 377     /**
 378      * Returns the previously set ButtonData property on the given button. If this
 379      * was never set, this method will return null. 
 380      *       
 381      * @param button The button to return the previously set ButtonData for.
 382      */
 383     public static ButtonData getButtonData(Node button) {
 384         final Map<Object,Object> properties = button.getProperties();
 385         if (properties.containsKey(ButtonBarSkin.BUTTON_DATA_PROPERTY)) {
 386             ObjectProperty<ButtonData> property = (ObjectProperty<ButtonData>) properties.get(ButtonBarSkin.BUTTON_DATA_PROPERTY);
 387             return property == null ? null : property.get();
 388         }
 389         return null;
 390     }
 391 
 392     /**
 393      * By default all buttons are uniformly sized in a ButtonBar, meaning that all
 394      * buttons take the width of the widest button. It is possible to opt-out of this
 395      * on a per-button basis, but calling the setButtonUniformSize method with
 396      * a boolean value of false.
 397      * 
 398      * <p>If a button is excluded from uniform sizing, it is both excluded from
 399      * being resized away from its preferred size, and also excluded from the
 400      * measuring process, so its size will not influence the maximum size calculated
 401      * for all buttons in the ButtonBar.
 402      * 
 403      * @param button The button to include / exclude from uniform sizing.
 404      * @param uniformSize Boolean true to force uniform sizing on the button, 
 405      *        false to exclude the button from uniform sizing.
 406      */
 407     public static void setButtonUniformSize(Node button, boolean uniformSize) {
 408         // we store the false, but remove the true (as the isButtonUniformSize 
 409         // method returns true by default)
 410         if (uniformSize) {
 411             button.getProperties().remove(ButtonBarSkin.BUTTON_SIZE_INDEPENDENCE);
 412         } else {
 413             button.getProperties().put(ButtonBarSkin.BUTTON_SIZE_INDEPENDENCE, uniformSize);
 414         }
 415     }
 416     
 417     /**
 418      * Returns whether the given node is part of the uniform sizing calculations
 419      * or not. By default all nodes that have not opted out (via 
 420      * {@link #setButtonUniformSize(Node, boolean)}) will return true here.
 421      */
 422     public static boolean isButtonUniformSize(Node button) {
 423         return (boolean) button.getProperties().getOrDefault(ButtonBarSkin.BUTTON_SIZE_INDEPENDENCE, true);
 424     }
 425 
 426 
 427 
 428     /**************************************************************************
 429      * 
 430      * Private fields
 431      * 
 432      **************************************************************************/
 433 
 434     private ObservableList<Node> buttons = FXCollections.<Node>observableArrayList();
 435 
 436 
 437 
 438     /**************************************************************************
 439      * 
 440      * Constructors
 441      * 
 442      **************************************************************************/
 443 
 444     /**
 445      * Creates a default ButtonBar instance using the default properties for
 446      * the users operating system.
 447      */
 448     public ButtonBar() {
 449         this(null);
 450     }
 451 
 452     /**
 453      * Creates a ButtonBar with the given button order (refer to 
 454      * {@link #buttonOrderProperty()} for more information).
 455      * 
 456      * @param buttonOrder The button order to use in this button bar instance.
 457      */
 458     public ButtonBar(final String buttonOrder) {
 459         getStyleClass().add("button-bar"); //$NON-NLS-1$
 460 
 461         // we allow for the buttons inside the ButtonBar to be focus traversable,
 462         // but the ButtonBar itself is not.
 463         // focusTraversable is styleable through css. Calling setFocusTraversable
 464         // makes it look to css like the user set the value and css will not 
 465         // override. Initializing focusTraversable by calling set on the 
 466         // CssMetaData ensures that css will be able to override the value.
 467         ((StyleableProperty<Boolean>)(WritableValue<Boolean>)focusTraversableProperty()).applyStyle(null, Boolean.FALSE);
 468 
 469         final boolean buttonOrderEmpty = buttonOrder == null || buttonOrder.isEmpty();
 470         
 471         if (Utils.isMac()) {
 472             setButtonOrder(buttonOrderEmpty ? BUTTON_ORDER_MAC_OS : buttonOrder);
 473             setButtonMinWidth(70);
 474         } else if (Utils.isUnix()) {
 475             setButtonOrder(buttonOrderEmpty ? BUTTON_ORDER_LINUX : buttonOrder);
 476             setButtonMinWidth(85);
 477         } else {
 478             // windows by default
 479             setButtonOrder(buttonOrderEmpty ? BUTTON_ORDER_WINDOWS : buttonOrder);
 480             setButtonMinWidth(75); 
 481         }
 482     }
 483 
 484 
 485 
 486     /**************************************************************************
 487      * 
 488      * Public API
 489      * 
 490      **************************************************************************/
 491 
 492     /**
 493      * {@inheritDoc}
 494      */
 495     @Override protected Skin<?> createDefaultSkin() {
 496         return new ButtonBarSkin(this);
 497     }
 498 
 499     /**
 500      * Placing buttons inside this ObservableList will instruct the ButtonBar
 501      * to position them relative to each other based on their specified
 502      * {@link ButtonData}. To set the ButtonData for a button, simply call
 503      * {@link ButtonBar#setButtonData(Node, ButtonData)}, passing in the 
 504      * relevant ButtonData.
 505      *  
 506      * @return A list containing all buttons currently in the button bar, and 
 507      *      allowing for further buttons to be added or removed.
 508      */
 509     public final ObservableList<Node> getButtons() {
 510         return buttons;
 511     }
 512 
 513 
 514 
 515     /**************************************************************************
 516      * 
 517      * Properties
 518      * 
 519      **************************************************************************/
 520 
 521     // --- Button order
 522     /**
 523      * The order for the typical buttons in a standard button bar. It is 
 524      * one letter per {@link ButtonData} enumeration value. Default button orders
 525      * for operating systems are also available: {@link #BUTTON_ORDER_WINDOWS},
 526      * {@link #BUTTON_ORDER_MAC_OS}, and {@link #BUTTON_ORDER_LINUX}.
 527      */
 528     public final StringProperty buttonOrderProperty() {
 529         return buttonOrderProperty;
 530     }
 531     private final StringProperty buttonOrderProperty = 
 532             new SimpleStringProperty(this, "buttonOrder"); //$NON-NLS-1$
 533 
 534     /**
 535      * Sets the {@link #buttonOrderProperty() button order}
 536      * @param buttonOrder The currently set button order, which by default will
 537      *      be the OS-specific button order.
 538      */
 539     public final void setButtonOrder(String buttonOrder) {
 540         buttonOrderProperty.set(buttonOrder);
 541     }
 542 
 543     /**
 544      * Returns the current {@link #buttonOrderProperty() button order}.
 545      * @return The current {@link #buttonOrderProperty() button order}.
 546      */
 547     public final String getButtonOrder() {
 548         return buttonOrderProperty.get();
 549     }
 550 
 551 
 552     // --- button min width
 553     /**
 554      * Specifies the minimum width of all buttons placed in this button bar.
 555      */
 556     public final DoubleProperty buttonMinWidthProperty() {
 557         return buttonMinWidthProperty;
 558     }
 559     private final DoubleProperty buttonMinWidthProperty =
 560             new SimpleDoubleProperty(this, "buttonMinWidthProperty"); //$NON-NLS-1$
 561 
 562     /**
 563      * Sets the minimum width of all buttons placed in this button bar.
 564      */
 565     public final void setButtonMinWidth(double value) {
 566         buttonMinWidthProperty.set(value);
 567     }
 568 
 569     /**
 570      * Returns the minimum width of all buttons placed in this button bar.
 571      */
 572     public final double getButtonMinWidth() {
 573         return buttonMinWidthProperty.get();
 574     }
 575 
 576 
 577 
 578     /**************************************************************************
 579      * 
 580      * Implementation
 581      * 
 582      **************************************************************************/
 583 
 584     /**
 585       * Most Controls return true for focusTraversable, so Control overrides
 586       * this method to return true, but ButtonBar returns false for
 587       * focusTraversable's initial value; hence the override of the override. 
 588       * This method is called from CSS code to get the correct initial value.
 589       * @treatAsPrivate implementation detail
 590       */
 591     @Deprecated @Override
 592     protected /*do not make final*/ Boolean impl_cssGetFocusTraversableInitialValue() {
 593         return Boolean.FALSE;
 594     }
 595 
 596 
 597 
 598     /**************************************************************************
 599      * 
 600      * Support classes / enums
 601      * 
 602      **************************************************************************/
 603 
 604 }