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><br/><img src="doc-files/buttonBar-windows.png" /><br>
  73  * <strong>Mac OS:</strong><br/><img src="doc-files/buttonBar-mac.png" /><br>
  74  * <strong>Linux:</strong><br/><img src="doc-files/buttonBar-linux.png" /><br>
  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">
 106  *   <tr>
 107  *     <td width="75"><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          */
 321         public String getTypeCode() {
 322             return typeCode;
 323         }
 324 
 325         /**
 326          * Indicates whether buttons created from the ButtonData enumeration
 327          * should be the 'cancel' button in the user interface. This typically
 328          * means that the button will respond to the escape key press, even if
 329          * the button does not have focus.
 330          *
 331          * <p>ButtonData enumeration values that can be the cancel button have a
 332          * comment stating this in their javadoc.
 333          */
 334         public final boolean isCancelButton() {
 335             return cancelButton;
 336         }
 337 
 338         /**
 339          * Indicates whether buttons created from the ButtonData enumeration
 340          * should be the 'default' button in the user interface. This typically
 341          * means that the button will respond to enter key presses, even if the
 342          * button does not have focus.
 343          *
 344          * <p>ButtonData enumeration values that can be the default button have
 345          * a comment stating this in their javadoc.
 346          */
 347         public final boolean isDefaultButton() {
 348             return defaultButton;
 349         }
 350     }
 351 
 352 
 353     /**
 354      * Sets the given ButtonData on the given button. If this button is
 355      * subsequently placed in a {@link ButtonBar} it will be placed in the
 356      * correct position relative to all other buttons in the bar.
 357      *
 358      * @param button The button to annotate with the given {@link ButtonData} value.
 359      * @param buttonData The ButtonData to designate the button as.
 360      */
 361     public static void setButtonData(Node button, ButtonData buttonData) {
 362         final Map<Object,Object> properties = button.getProperties();
 363         final ObjectProperty<ButtonData> property =
 364                 (ObjectProperty<ButtonData>) properties.getOrDefault(
 365                         Properties.BUTTON_DATA_PROPERTY,
 366                         new SimpleObjectProperty<>(button, "buttonData", buttonData));
 367 
 368         property.set(buttonData);
 369         properties.putIfAbsent(Properties.BUTTON_DATA_PROPERTY, property);
 370     }
 371 
 372     /**
 373      * Returns the previously set ButtonData property on the given button. If this
 374      * was never set, this method will return null.
 375      *
 376      * @param button The button to return the previously set ButtonData for.
 377      */
 378     public static ButtonData getButtonData(Node button) {
 379         final Map<Object,Object> properties = button.getProperties();
 380         if (properties.containsKey(Properties.BUTTON_DATA_PROPERTY)) {
 381             ObjectProperty<ButtonData> property = (ObjectProperty<ButtonData>) properties.get(Properties.BUTTON_DATA_PROPERTY);
 382             return property == null ? null : property.get();
 383         }
 384         return null;
 385     }
 386 
 387     /**
 388      * By default all buttons are uniformly sized in a ButtonBar, meaning that all
 389      * buttons take the width of the widest button. It is possible to opt-out of this
 390      * on a per-button basis, but calling the setButtonUniformSize method with
 391      * a boolean value of false.
 392      *
 393      * <p>If a button is excluded from uniform sizing, it is both excluded from
 394      * being resized away from its preferred size, and also excluded from the
 395      * measuring process, so its size will not influence the maximum size calculated
 396      * for all buttons in the ButtonBar.
 397      *
 398      * @param button The button to include / exclude from uniform sizing.
 399      * @param uniformSize Boolean true to force uniform sizing on the button,
 400      *        false to exclude the button from uniform sizing.
 401      */
 402     public static void setButtonUniformSize(Node button, boolean uniformSize) {
 403         // we store the false, but remove the true (as the isButtonUniformSize
 404         // method returns true by default)
 405         if (uniformSize) {
 406             button.getProperties().remove(Properties.BUTTON_SIZE_INDEPENDENCE);
 407         } else {
 408             button.getProperties().put(Properties.BUTTON_SIZE_INDEPENDENCE, uniformSize);
 409         }
 410     }
 411 
 412     /**
 413      * Returns whether the given node is part of the uniform sizing calculations
 414      * or not. By default all nodes that have not opted out (via
 415      * {@link #setButtonUniformSize(Node, boolean)}) will return true here.
 416      */
 417     public static boolean isButtonUniformSize(Node button) {
 418         return (boolean) button.getProperties().getOrDefault(Properties.BUTTON_SIZE_INDEPENDENCE, true);
 419     }
 420 
 421 
 422 
 423     /**************************************************************************
 424      *
 425      * Private fields
 426      *
 427      **************************************************************************/
 428 
 429     private ObservableList<Node> buttons = FXCollections.<Node>observableArrayList();
 430 
 431 
 432 
 433     /**************************************************************************
 434      *
 435      * Constructors
 436      *
 437      **************************************************************************/
 438 
 439     /**
 440      * Creates a default ButtonBar instance using the default properties for
 441      * the users operating system.
 442      */
 443     public ButtonBar() {
 444         this(null);
 445     }
 446 
 447     /**
 448      * Creates a ButtonBar with the given button order (refer to
 449      * {@link #buttonOrderProperty()} for more information).
 450      *
 451      * @param buttonOrder The button order to use in this button bar instance.
 452      */
 453     public ButtonBar(final String buttonOrder) {
 454         getStyleClass().add("button-bar"); //$NON-NLS-1$
 455 
 456         // we allow for the buttons inside the ButtonBar to be focus traversable,
 457         // but the ButtonBar itself is not.
 458         // focusTraversable is styleable through css. Calling setFocusTraversable
 459         // makes it look to css like the user set the value and css will not
 460         // override. Initializing focusTraversable by calling set on the
 461         // CssMetaData ensures that css will be able to override the value.
 462         ((StyleableProperty<Boolean>)(WritableValue<Boolean>)focusTraversableProperty()).applyStyle(null, Boolean.FALSE);
 463 
 464         final boolean buttonOrderEmpty = buttonOrder == null || buttonOrder.isEmpty();
 465 
 466         if (Utils.isMac()) {
 467             setButtonOrder(buttonOrderEmpty ? BUTTON_ORDER_MAC_OS : buttonOrder);
 468             setButtonMinWidth(70);
 469         } else if (Utils.isUnix()) {
 470             setButtonOrder(buttonOrderEmpty ? BUTTON_ORDER_LINUX : buttonOrder);
 471             setButtonMinWidth(85);
 472         } else {
 473             // windows by default
 474             setButtonOrder(buttonOrderEmpty ? BUTTON_ORDER_WINDOWS : buttonOrder);
 475             setButtonMinWidth(75);
 476         }
 477     }
 478 
 479 
 480 
 481     /**************************************************************************
 482      *
 483      * Public API
 484      *
 485      **************************************************************************/
 486 
 487     /**
 488      * {@inheritDoc}
 489      */
 490     @Override protected Skin<?> createDefaultSkin() {
 491         return new ButtonBarSkin(this);
 492     }
 493 
 494     /**
 495      * Placing buttons inside this ObservableList will instruct the ButtonBar
 496      * to position them relative to each other based on their specified
 497      * {@link ButtonData}. To set the ButtonData for a button, simply call
 498      * {@link ButtonBar#setButtonData(Node, ButtonData)}, passing in the
 499      * relevant ButtonData.
 500      *
 501      * @return A list containing all buttons currently in the button bar, and
 502      *      allowing for further buttons to be added or removed.
 503      */
 504     public final ObservableList<Node> getButtons() {
 505         return buttons;
 506     }
 507 
 508 
 509 
 510     /**************************************************************************
 511      *
 512      * Properties
 513      *
 514      **************************************************************************/
 515 
 516     // --- Button order
 517     /**
 518      * The order for the typical buttons in a standard button bar. It is
 519      * one letter per {@link ButtonData} enumeration value. Default button orders
 520      * for operating systems are also available: {@link #BUTTON_ORDER_WINDOWS},
 521      * {@link #BUTTON_ORDER_MAC_OS}, and {@link #BUTTON_ORDER_LINUX}.
 522      */
 523     public final StringProperty buttonOrderProperty() {
 524         return buttonOrderProperty;
 525     }
 526     private final StringProperty buttonOrderProperty =
 527             new SimpleStringProperty(this, "buttonOrder"); //$NON-NLS-1$
 528 
 529     /**
 530      * Sets the {@link #buttonOrderProperty() button order}
 531      * @param buttonOrder The currently set button order, which by default will
 532      *      be the OS-specific button order.
 533      */
 534     public final void setButtonOrder(String buttonOrder) {
 535         buttonOrderProperty.set(buttonOrder);
 536     }
 537 
 538     /**
 539      * Returns the current {@link #buttonOrderProperty() button order}.
 540      * @return The current {@link #buttonOrderProperty() button order}.
 541      */
 542     public final String getButtonOrder() {
 543         return buttonOrderProperty.get();
 544     }
 545 
 546 
 547     // --- button min width
 548     /**
 549      * Specifies the minimum width of all buttons placed in this button bar.
 550      */
 551     public final DoubleProperty buttonMinWidthProperty() {
 552         return buttonMinWidthProperty;
 553     }
 554     private final DoubleProperty buttonMinWidthProperty =
 555             new SimpleDoubleProperty(this, "buttonMinWidthProperty"); //$NON-NLS-1$
 556 
 557     /**
 558      * Sets the minimum width of all buttons placed in this button bar.
 559      */
 560     public final void setButtonMinWidth(double value) {
 561         buttonMinWidthProperty.set(value);
 562     }
 563 
 564     /**
 565      * Returns the minimum width of all buttons placed in this button bar.
 566      */
 567     public final double getButtonMinWidth() {
 568         return buttonMinWidthProperty.get();
 569     }
 570 
 571 
 572 
 573     /**************************************************************************
 574      *
 575      * Implementation
 576      *
 577      **************************************************************************/
 578 
 579     /**
 580      * Returns the initial focus traversable state of this control, for use
 581      * by the JavaFX CSS engine to correctly set its initial value. This method
 582      * is overridden as by default UI controls have focus traversable set to true,
 583      * but that is not appropriate for this control.
 584      *
 585      * @since 9
 586      */
 587     @Override protected Boolean getInitialFocusTraversable() {
 588         return Boolean.FALSE;
 589     }
 590 
 591 
 592 
 593     /**************************************************************************
 594      *
 595      * Support classes / enums
 596      *
 597      **************************************************************************/
 598 
 599 }