       1 /*
       2  * Copyright (c) 2010, 2013, 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
      26 package com.sun.javafx.scene.control.behavior;
      27
      28 import java.util.StringTokenizer;
      29
      30 import javafx.scene.input.KeyCode;
      31
      32 /**
      33  * <p>
      34  * Provides support for embedding a mnemonic, accelerator and text in a single
      35  * string. A mnemonic is a single character that will either give a control
      36  * focus or activate it when pressed in conjunction with the base platform's
      37  * mnemonic modifier (typically the {@code Alt} key). An accelerator is a key
      38  * combination that will activate the object.
      39  * </p>
      40  *
      41  * <p>
      42  * The syntax of the string content is as follows:
      43  * </p>
      44  *
      45  * <ul>
      46  * <li>
      47  * <p>
      48  * <b>Simple mnemonic</b>: the first character preceded by the first {@code _}
      49  * character will be treated as the mnemonic. For example, "E_xit" will cause
      50  * the text to become "Exit" and the mnemonic will be "x". This is the most
      51  * common designation of a mnemonic, and the skin for a control will present the
      52  * mnemonic in the string with an underline. To prevent {@code _} from being
      53  * treated as the mnemonic prefix character, repeat it twice in a row. A
      54  * mnemonic is not required.
      55  * </p>
      56  * </li>
      57  * <li>
      58  * <p>
      59  * <b>Extended mnemonic</b>: an optional representation of a mnemonic is
      60  * {@code _(c)}, where {@code c} is the mnemonic character. For example,
      61  * "Exit_(q)" will cause the text to become "Exit" and the
      62  * {@link #extendedMnemonicText} to become "(q)". This is typically provided in
      63  * translated strings to support mnemonics where the main text does not have any
      64  * characters that map to keyboard keys. In these cases, the skin for the
      65  * control will typically present the mnemonic surrounded by parentheses. The
      66  * skin will also honor the hiding and presentation of the extended mnemonic
      67  * string on platforms where the mnemonic is only displayed when the mnemonic
      68  * modifier key is pressed.
      69  * <li>
      70  * <p>
      71  * <b>Accelerator</b>: an accelerator must be defined as the last element in the
      72  * string. It is delimited from the rest of the string by {@code @}. The
      73  * {@code @} must be followed by any number of modifier designations delimited
      74  * by {@code +} then followed by the accelerator character (e.g.,
      75  * {@code Ctrl+Shift+K}). The legal modifier designations are as follows (case
      76  * insensitive, and not to be translated): {@code Ctrl}, {@code Alt},
      77  * {@code Shift} and {@code Shortcut}. If an {@code @} appears
      78  * twice in a row in the string, it will be treated as a single {@code @} and
      79  * not as the accelerator delimiter.
      80  * <p></li>
      81  * </ul>
      82  *
      83  */
      84 public class TextBinding {
      85
      86     /**
      87      * the marker symbol used when parsing for mnemonics
      88      */
      89     private String MNEMONIC_SYMBOL = "_";
      90
      91     /**
      92      * The text with any markup for the mnemonic and accelerator removed.
      93      */
      94     private String text = null;
      95
      96     /**
      97      * Returns the text with any markup for the mnemonic and accelerator removed
      98      *
      99      * @return the text with any markup for the mnemonic and accelerator removed
     100      */
     101     public String getText() {
     102         return text;
     103     }
     104
     105     /**
     106      * The mnemonic or {@link KeyCode#UNDEFINED} if there is no mnemonic.
     107      */
     108     private KeyCode mnemonic = KeyCode.UNDEFINED;
     109
     110     /**
     111      * Returns the mnemonic or {@link KeyCode#UNDEFINED} if there is no
     112      * mnemonic.
     113      *
     114      * @return the mnemonic or {@link KeyCode#UNDEFINED} if there is no
     115      *         mnemonic
     116      */
     117     public KeyCode getMnemonic() {
     118         return mnemonic;
     119     }
     120
     121     /**
     122      * The index of the mnemonic character in the text property or -1 if there
     123      * is no mnemonic character in the text. This is only non-negative if the
     124      * simple {@code _c} syntax was used to specify the mnemonic.
     125      */
     126     private int mnemonicIndex = -1;
     127
     128     /**
     129      * Returns the index of the mnemonic character in the text property or -1 if
     130      * there is no mnemonic character in the text. This is only non-negative if
     131      * the simple {@code _c} syntax was used to specify the mnemonic.
     132      *
     133      * @return the index of the mnemonic character in the text property or -1 if
     134      *         there is no mnemonic character in the text
     135      */
     136     public int getMnemonicIndex() {
     137         return mnemonicIndex;
     138     }
     139
     140     /**
     141      * The extended mnemonic text (if it exists). This is only non-null if the
     142      * extended mnemonic syntax was used to specify the mnemonic.
     143      */
     144     private String extendedMnemonicText = null;
     145
     146     /**
     147      * Returns the extended mnemonic text (if it exists). This is only non-null
     148      * if the extended mnemonic syntax was used to specify the mnemonic.
     149      *
     150      * @return the extended mnemonic text (if it exists) or null
     151      */
     152     public String getExtendedMnemonicText() {
     153         return extendedMnemonicText;
     154     }
     155
     156     /**
     157      * The KeyBinding that describes the accelerator or null if there is no
     158      * accelerator.
     159      */
     160     private KeyBinding accelerator = null;
     161
     162     /**
     163      * Returns the KeyBinding that describes the accelerator or null if there is
     164      * no accelerator
     165      *
     166      * @return the KeyBinding that describes the accelerator or null if there is
     167      *         no accelerator
     168      */
     169     public KeyBinding getAccelerator() {
     170         return accelerator;
     171     }
     172
     173     /**
     174      * The localized accelerator text for use in presenting in an interface.
     175      */
     176     private String acceleratorText = null;
     177
     178     /**
     179      * Returns the localized accelerator text for use in presenting in an
     180      * interface or null if there is no accelerator text.
     181      *
     182      * @return the localized accelerator text for use in presenting in an
     183      *         interface or null if there is no accelerator text
     184      */
     185     public String getAcceleratorText() {
     186         return acceleratorText;
     187     }
     188
     189
     190     /**
     191      * for accelerator : is the control key needed?
     192      */
     193     private boolean ctrl = false;
     194     public boolean getCtrl() {
     195         return ctrl;
     196     }
     197
     198     /**
     199      * for accelerator : is the shift key needed?
     200      */
     201     private boolean shift = false;
     202     public boolean getShift() {
     203         return shift;
     204     }
     205
     206     /**
     207      * for accelerator : is the control key needed?
     208      */
     209     private boolean alt = false;
     210     public boolean getAlt() {
     211         return alt;
     212     }
     213
     214     /**
     215      * for accelerator : is the shortcut key needed?
     216      */
     217     private boolean shortcut = false;
     218     public boolean getShortcut() {
     219         return shortcut;
     220     }
     221
     222     /**
     223      * Creates a new TextBinding instance from the given string.
     224      *
     225      * @param s the action text string
     226      *
     227      * @see getText
     228      * @see getMnemonic
     229      * @see getMnemonicIndex
     230      * @see getExtendedMnemonicText
     231      * @see getAccelerator
     232      * @see getAcceleratorText
     233      */
     234     public TextBinding(String s) {
     235         parseAndSplit(s);
     236     }
     237
     238     /**
     239      * Parse and split the given string into the appropriate segments.
     240      */
     241     private void parseAndSplit(String s) {
     242         if (s == null || s.length() == 0) {
     243             text = s;
     244             return;
     245         }
     246
     247         // We will use temp as a working copy of the string and will pull
     248         // mnemonic and accelerator text out of it as we find those things.
     249         //
     250         StringBuffer temp = new StringBuffer(s);
     251
     252         // Find the accelerator if it exists.
     253         //
     254         int index = temp.indexOf("@");
     255         while (index >= 0 && index < (temp.length() - 1)) {
     256             if (temp.charAt(index + 1) == '@') {
     257                 temp.delete(index, index + 1); // delete the extra '@'
     258             } else {
     259                 // Only strip out the accelerator text if we can parse it.
     260                 // [[[TODO: WDW - provide a localized form that concatenates
     261                 // the localized strings for the modifier with the key.]]]
     262                 acceleratorText = temp.substring(index + 1);
     263                 accelerator = parseAcceleratorText(acceleratorText);
     264                 if (accelerator != null) {
     265                     temp.delete(index, temp.length());
     266                     break;
     267                 } else {
     268                     acceleratorText = null;
     269                 }
     270             }
     271             index = temp.indexOf("@", index + 1);
     272         }
     273
     274         // Find the mnemonic if it exists.
     275         //
     276         index = temp.indexOf(MNEMONIC_SYMBOL);
     277         while (index >= 0 && index < (temp.length() - 1)) {
     278             // Skip two _'s in a row
     279             if (MNEMONIC_SYMBOL.equals(temp.substring(index + 1, index + 2))) {
     280                 temp.delete(index, index + 1); // delete the extra MNEMONIC_SYMBOL
     281             } else if (temp.charAt(index + 1) != '('
     282                        || index == temp.length() - 2) {
     283                 String mnemonicChar = temp.substring(index + 1, index + 2);
     284                 mnemonic = KeyCode.getKeyCode(mnemonicChar.toUpperCase());
     285                 if (mnemonic != null) {
     286                     mnemonicIndex = index;
     287                 }
     288                 temp.delete(index, index + 1);
     289                 break;
     290             } else {
     291                 int endIndex = temp.indexOf(")", index + 3);
     292                 if (endIndex == -1) { // "(" is actually the mnemonic
     293                     String mnemonicChar = temp.substring(index + 1, index + 2);
     294                     mnemonic = KeyCode.getKeyCode(mnemonicChar.toUpperCase());
     295                     if (mnemonic != null) {
     296                         mnemonicIndex = index;
     297                     }
     298                     temp.delete(index, index + 1);
     299                     break;
     300                 } else if (endIndex == index + 3) {
     301                     String mnemonicChar = temp.substring(index + 2, index + 3);
     302                     mnemonic = KeyCode.getKeyCode(mnemonicChar.toUpperCase());
     303                     extendedMnemonicText = temp.substring(index + 1, index + 4);
     304                     temp.delete(index, endIndex + 3);
     305                     break;
     306                 }
     307             }
     308             index = temp.indexOf(MNEMONIC_SYMBOL, index + 1);
     309         }
     310         text = temp.toString();
     311     }
     312
     313     /**
     314      * Returns a KeyBinding for the given accelerator text or {@code null} if
     315      * the accelerator text cannot be parsed.
     316      *
     317      * @return a KeyBinding for the given accelerator text or {@code null} if
     318      *         the accelerator text cannot be parsed
     319      */
     320     private KeyBinding parseAcceleratorText(String s) {
     321         KeyBinding result = null;
     322         String text = null;
     323         boolean controlDown = false;
     324         boolean altDown = false;
     325         boolean shiftDown = false;
     326         boolean shortcutDown = false;
     327         boolean parseFail = false;
     328         StringTokenizer tokenizer = new StringTokenizer(s, "+");
     329         while (!parseFail && tokenizer.hasMoreTokens()) {
     330             String token = tokenizer.nextToken();
     331             if (!tokenizer.hasMoreTokens()) {
     332                 text = token;
     333             } else {
     334                 KeyCode code = KeyCode.getKeyCode(token.toUpperCase());
     335                 if (code != null) {
     336                     switch (code) {
     337                       case CONTROL:
     338                         controlDown = true;
     339                         break;
     340                       case ALT:
     341                         altDown = true;
     342                         break;
     343                       case SHIFT:
     344                         shiftDown = true;
     345                         break;
     346                       case SHORTCUT:
     347                         shortcutDown = true;
     348                         break;
     349                       default:
     350                         text = null;
     351                         parseFail = true;
     352                         break;
     353                     }
     354                 } else {
     355                     text = null;
     356                     parseFail = true;
     357                 }
     358             }
     359         }
     360         if (text != null) {
     361             KeyCode code = KeyCode.getKeyCode(text.toUpperCase());
     362             if (code != null) {
     363                 if (code != KeyCode.UNDEFINED) {
     364                     result = new KeyBinding(code, null);
     365                     if (controlDown) {
     366                         ctrl = true;
     367                         result.ctrl();
     368                     }
     369                     if (altDown) {
     370                         alt = true;
     371                         result.alt();
     372                     }
     373                     if (shiftDown) {
     374                         shift = true;
     375                         result.shift();
     376                     }
     377                     if (shortcutDown) {
     378                         shortcut = true;
     379                         result.shortcut();
     380                     }
     381                 }
     382             }
     383         }
     384         return result;
     385     }
     386
     387     @Override public String toString() {
     388         return "TextBinding [text=" + getText() + ",mnemonic=" + getMnemonic()
     389             + ", mnemonicIndex=" + getMnemonicIndex()
     390             + ", extendedMnemonicText=" + getExtendedMnemonicText()
     391             + ", accelerator=" + getAccelerator() + ", acceleratorText="
     392             + getAcceleratorText() + "]";
     393     }
     394 }