001    /*
002     * Copyright 2002-2004 the original author or authors.
003     *
004     * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005     * use this file except in compliance with the License. You may obtain a copy of
006     * the License at
007     *
008     * http://www.apache.org/licenses/LICENSE-2.0
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012     * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013     * License for the specific language governing permissions and limitations under
014     * the License.
015     */
016    package org.springframework.richclient.core;
017    
018    import javax.swing.AbstractButton;
019    import javax.swing.JComponent;
020    import javax.swing.JLabel;
021    import javax.swing.JPanel;
022    
023    import org.apache.commons.logging.Log;
024    import org.apache.commons.logging.LogFactory;
025    import org.springframework.core.style.ToStringCreator;
026    import org.springframework.richclient.util.Assert;
027    import org.springframework.util.StringUtils;
028    
029    /**
030     * An immutable parameter object consisting of the text, mnemonic character and mnemonic character
031     * index that may be associated with a labeled component. This class also acts as a factory for
032     * creating instances of itself based on a string descriptor that adheres to some simple syntax
033     * rules as described in the javadoc for the {@link #valueOf(String)} method.
034     *
035     * <p>
036     * The syntax used for the label info descriptor is just the text to be displayed by the label with
037     * an ampersand (&) optionally inserted before the character that is to be used as a
038     * mnemonic for the label.
039     * </p>
040     *
041     * <p>
042     * Example: To create a label with the text {@code My Label} and the capital L as a mnemonic,
043     * use the following descriptor:
044     * </p>
045     *
046     * <pre>
047     *     <code>My &Label</code>
048     * </pre>
049     *
050     * <p>
051     * A backslash character (\) can be used to escape ampersand characters that are to be displayed as
052     * part of the label's text. For example:
053     * </p>
054     *
055     * <pre>
056     *     <code>Save \& Run</code>
057     * </pre>
058     *
059     * <p>
060     * Only one non-escaped backslash can appear in the label descriptor. Attempting to specify more
061     * than one mnemonic character will result in an exception being thrown.
062     * TODO finish comment regarding backslash chars in props file
063     * Note that for label descriptors provided in properties files, an extra backslash will be required
064     * to avoid the single backslash being interpreted as a special character.
065     * </p>
066     *
067     * @author Keith Donald
068     * @author Peter De Bruycker
069     * @author Kevin Stembridge
070     */
071    public final class LabelInfo {
072    
073        private static final Log logger = LogFactory.getLog(LabelInfo.class);
074    
075        private static final LabelInfo BLANK_LABEL_INFO = new LabelInfo("");
076    
077        private static final int DEFAULT_MNEMONIC = 0;
078    
079        private static final int DEFAULT_MNEMONIC_INDEX = -1;
080    
081        private final String text;
082    
083        private final int mnemonic;
084    
085        private final int mnemonicIndex;
086    
087        /**
088         * Creates a new {@code LabelInfo} instance by parsing the given label descriptor to determine
089         * the label's text and mnemonic character. The syntax rules for the descriptor are as follows:
090         *
091         * <ul>
092         * <li>The descriptor may be null or an empty string, in which case, an instance with no text
093         * or mnemonic will be returned.</li>
094         * <li>The mnemonic character is indicated by a preceding ampersand (&).</li>
095         * <li>A backslash character (\) can be used to escape ampersand characters that are to be
096         * displayed as part of the label's text.</li>
097         * <li>A double backslash (a backslash escaped by a backslash) indicates that a single backslash
098         * is to appear in the label's text.</li>
099         * <li>Only one non-escaped ampersand can appear in the descriptor.</li>
100         * <li>A space character cannot be specified as the mnemonic character.</li>
101         * </ul>
102         *
103         * @param labelDescriptor The label descriptor. The text may be null or empty, in which case a
104         * blank {@code LabelInfo} instance will be returned.
105         *
106         * @return A {@code LabelInfo} instance that is described by the given descriptor.
107         * Never returns null.
108         *
109         * @throws IllegalArgumentException if {@code labelDescriptor} violates any of the syntax rules
110         * described above.
111         */
112        public static LabelInfo valueOf(final String labelDescriptor) {
113    
114            if (logger.isDebugEnabled()) {
115                logger.debug("Creating a new LabelInfo from label descriptor [" + labelDescriptor + "]");
116            }
117    
118            if (!StringUtils.hasText(labelDescriptor)) {
119                return BLANK_LABEL_INFO;
120            }
121    
122            StringBuffer labelText = new StringBuffer();
123            char mnemonicChar = '\0';
124            int mnemonicCharIndex = DEFAULT_MNEMONIC_INDEX;
125            char currentChar;
126    
127            for (int i = 0; i < labelDescriptor.length();) {
128                currentChar = labelDescriptor.charAt(i);
129                int nextCharIndex = i + 1;
130    
131                if (currentChar == '\\') {
132                    //confirm that the next char is a valid escaped char, add the next char to the
133                    //stringbuffer then skip ahead 2 chars.
134                    checkForValidEscapedCharacter(nextCharIndex, labelDescriptor);
135                    labelText.append(labelDescriptor.charAt(nextCharIndex));
136                    i++;
137                    i++;
138                }
139                else if (currentChar == '&') {
140                    //we've found a mnemonic indicator, so...
141    
142                    //confirm that we haven't already found one, ...
143                    if (mnemonicChar != '\0') {
144                        throw new IllegalArgumentException(
145                                "The label descriptor ["
146                                + labelDescriptor
147                                + "] can only contain one non-escaped ampersand.");
148                    }
149    
150                    //...that it isn't the last character, ...
151                    if (nextCharIndex >= labelDescriptor.length()) {
152                        throw new IllegalArgumentException(
153                                "The label descriptor ["
154                                + labelDescriptor
155                                + "] cannot have a non-escaped ampersand as its last character.");
156                    }
157    
158                    //...and that the character that it prefixes is a valid mnemonic character.
159                    mnemonicChar = labelDescriptor.charAt(nextCharIndex);
160                    checkForValidMnemonicChar(mnemonicChar, labelDescriptor);
161    
162                    //...add it to the stringbuffer and set the mnemonic index to the position of
163                    //the newly added char, then skip ahead 2 characters
164                    labelText.append(mnemonicChar);
165                    mnemonicCharIndex = labelText.length() - 1;
166                    i++;
167                    i++;
168    
169                }
170                else {
171                    labelText.append(currentChar);
172                    i++;
173                }
174    
175            }
176    
177            // mnemonics work with VK_XXX (see KeyEvent) and only uppercase letters are used as event
178            return new LabelInfo(labelText.toString(), Character.toUpperCase(mnemonicChar), mnemonicCharIndex);
179    
180        }
181    
182        /**
183         * Confirms that the character at the specified index within the given label descriptor is
184         * a valid 'escapable' character. i.e. either an ampersand or backslash.
185         *
186         * @param index The position within the label descriptor of the character to be checked.
187         * @param labelDescriptor The label descriptor.
188         *
189         * @throws NullPointerException if {@code labelDescriptor} is null.
190         * @throws IllegalArgumentException if the given {@code index} position is beyond the length
191         * of the string or if the character at that position is not an ampersand or backslash.
192         */
193        private static void checkForValidEscapedCharacter(int index, String labelDescriptor) {
194    
195            if (index >= labelDescriptor.length()) {
196                throw new IllegalArgumentException(
197                        "The label descriptor contains an invalid escape sequence. Backslash "
198                        + "characters (\\) must be followed by either an ampersand (&) or another "
199                        + "backslash.");
200            }
201    
202            char escapedChar = labelDescriptor.charAt(index);
203    
204            if (escapedChar != '&' && escapedChar != '\\') {
205                throw new IllegalArgumentException(
206                        "The label descriptor ["
207                        + labelDescriptor
208                        + "] contains an invalid escape sequence. Backslash "
209                        + "characters (\\) must be followed by either an ampersand (&) or another "
210                        + "backslash.");
211            }
212    
213        }
214    
215        /**
216         * Confirms that the given character is allowed to be used as a mnemonic. Currently, only
217         * spaces are disallowed.
218         *
219         * @param mnemonicChar The mnemonic character.
220         * @param labelDescriptor The label descriptor.
221         */
222        private static void checkForValidMnemonicChar(char mnemonicChar, String labelDescriptor) {
223    
224            if (mnemonicChar == ' ') {
225                throw new IllegalArgumentException(
226                        "The mnemonic character cannot be a space. ["
227                        + labelDescriptor
228                        + "]");
229            }
230    
231        }
232    
233        /**
234         * Creates a new {@code LabelInfo} with the given text and no specified mnemonic.
235         *
236         * @param text The text to be displayed by the label. This may be an empty string but
237         * cannot be null.
238         *
239         * @throws IllegalArgumentException if {@code text} is null.
240         */
241        public LabelInfo(String text) {
242            this(text, DEFAULT_MNEMONIC, DEFAULT_MNEMONIC_INDEX);
243        }
244    
245        /**
246         * Creates a new {@code LabelInfo} with the given text and mnemonic character.
247         *
248         * @param text The text to be displayed by the label. This may be an empty string but cannot
249         * be null.
250         * @param mnemonic The character from the label text that acts as a mnemonic.
251         *
252         * @throws IllegalArgumentException if {@code text} is null or if {@code mnemonic} is a
253         * negative value.
254         */
255        public LabelInfo(String text, int mnemonic) {
256            this(text, mnemonic, DEFAULT_MNEMONIC_INDEX);
257        }
258    
259        /**
260         * Creates a new {@code LabelInfo} with the given text, mnemonic character and mnemonic index.
261         *
262         * @param text The text to be displayed by the label. This may be an empty string but cannot
263         * be null.
264         * @param mnemonic The character from the label text that acts as a mnemonic.
265         * @param mnemonicIndex The zero-based index of the mnemonic character within the label text.
266         * If the specified label text is an empty string, this property will be ignored and set to -1.
267         *
268         * @throws IllegalArgumentException if {@code text} is null, if {@code mnemonic} is a negative
269         * value, if {@code mnemonicIndex} is less than -1 or if {@code mnemonicIndex} is outside the
270         * length of {@code text}.
271         */
272        public LabelInfo(String text, int mnemonic, int mnemonicIndex) {
273    
274            Assert.required(text, "text");
275            Assert.isTrue(mnemonic >= 0, "mnemonic must be greater than or equal to 0");
276            Assert.isTrue(mnemonicIndex >= -1, "mnemonicIndex must be greater than or equal to -1");
277    
278            Assert.isTrue(mnemonicIndex < text.length(),
279                          "The mnemonic index must be less than the text length; mnemonicIndex = "
280                          + mnemonicIndex
281                          + ", text length = "
282                          + text.length());
283    
284            this.text = text;
285    
286            if (!StringUtils.hasText(text)) {
287                mnemonicIndex = DEFAULT_MNEMONIC_INDEX;
288            }
289    
290            if (logger.isDebugEnabled()) {
291                logger.debug("Constructing a new LabelInfo instance with properties: text='"
292                             + text
293                             + "', mnemonic="
294                             + mnemonic
295                             + ", mnemonicIndex="
296                             + mnemonicIndex);
297            }
298    
299            this.mnemonic = mnemonic;
300            this.mnemonicIndex = mnemonicIndex;
301    
302        }
303    
304        /**
305         * {@inheritDoc}
306         */
307        public int hashCode() {
308            final int PRIME = 31;
309            int result = 1;
310            result = PRIME * result + this.mnemonic;
311            result = PRIME * result + this.mnemonicIndex;
312            result = PRIME * result + ((this.text == null) ? 0 : this.text.hashCode());
313            return result;
314        }
315    
316        /**
317         * {@inheritDoc}
318         */
319        public boolean equals(Object obj) {
320            if (this == obj)
321                return true;
322            if (obj == null)
323                return false;
324            if (getClass() != obj.getClass())
325                return false;
326            final LabelInfo other = (LabelInfo) obj;
327            if (this.mnemonic != other.mnemonic)
328                return false;
329            if (this.mnemonicIndex != other.mnemonicIndex)
330                return false;
331            if (this.text == null) {
332                if (other.text != null)
333                    return false;
334            } else if (!this.text.equals(other.text))
335                return false;
336            return true;
337        }
338    
339        /**
340         * Configures the given label with the parameters from this instance.
341         *
342         * @param label The label that is to be configured.
343         * @throws IllegalArgumentException if {@code label} is null.
344         */
345        public void configureLabel(JLabel label) {
346    
347            Assert.required(label, "label");
348    
349            label.setText(this.text);
350            label.setDisplayedMnemonic(getMnemonic());
351    
352            if (getMnemonicIndex() >= -1) {
353                label.setDisplayedMnemonicIndex(getMnemonicIndex());
354            }
355    
356        }
357    
358        /**
359         * Configures the given label with the property values described by this instance and then sets
360         * it as the label for the given component.
361         *
362         * @param label The label to be configured.
363         * @param component The component that the label is 'for'.
364         *
365         * @throws IllegalArgumentException if either argument is null.
366         *
367         * @see JLabel#setLabelFor(java.awt.Component)
368         */
369        public void configureLabelFor(JLabel label, JComponent component) {
370    
371            Assert.required(label, "label");
372            Assert.required(component, "component");
373    
374            configureLabel(label);
375    
376            if (!(component instanceof JPanel)) {
377                String labelText = label.getText();
378    
379                if (!labelText.endsWith(":")) {
380    
381                    if (logger.isDebugEnabled()) {
382                        logger.debug("Appending colon to text field label text '" + this.text + "'");
383                    }
384    
385                    label.setText(labelText + ":");
386                }
387    
388            }
389    
390            label.setLabelFor(component);
391    
392        }
393    
394        /**
395         * Configures the given button with the properties held in this instance. Note that this
396         * instance doesn't hold any keystroke accelerator information.
397         *
398         * @param button The button to be configured.
399         *
400         * @throws IllegalArgumentException if {@code button} is null.
401         */
402        public void configureButton(AbstractButton button) {
403            Assert.notNull(button);
404            button.setText(this.text);
405            button.setMnemonic(getMnemonic());
406            button.setDisplayedMnemonicIndex(getMnemonicIndex());
407        }
408    
409        /**
410         * Returns the text to be displayed by the label.
411         * @return The label text, possibly an empty string but never null.
412         */
413        public String getText() {
414            return this.text;
415        }
416    
417        /**
418         * Returns the character that is to be treated as the mnemonic character for the label.
419         *
420         * @return The mnemonic character.
421         */
422        public int getMnemonic() {
423            return this.mnemonic;
424        }
425    
426        /**
427         * Returns the index within the label text of the mnemonic character.
428         * @return The index of the mnemonic character, or -1 if no mnemonic index is specified.
429         */
430        public int getMnemonicIndex() {
431            return this.mnemonicIndex;
432        }
433    
434        /**
435         * {@inheritDoc}
436         */
437        public String toString() {
438            return new ToStringCreator(this)
439                    .append("text", this.text)
440                    .append("mnemonic", this.mnemonic)
441                    .append("mnemonicIndex", this.mnemonicIndex)
442                    .toString();
443        }
444    
445    }