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