View Javadoc

1   /*
2    * Copyright 2002-2004 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5    * use this file except in compliance with the License. You may obtain a copy of
6    * the License at
7    *
8    * http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations under
14   * the License.
15   */
16  package org.springframework.richclient.core;
17  
18  import javax.swing.AbstractButton;
19  import javax.swing.JComponent;
20  import javax.swing.JLabel;
21  import javax.swing.JPanel;
22  
23  import org.apache.commons.logging.Log;
24  import org.apache.commons.logging.LogFactory;
25  import org.springframework.core.style.ToStringCreator;
26  import org.springframework.richclient.util.Assert;
27  import org.springframework.util.StringUtils;
28  
29  /**
30   * An immutable parameter object consisting of the text, mnemonic character and mnemonic character
31   * index that may be associated with a labeled component. This class also acts as a factory for
32   * creating instances of itself based on a string descriptor that adheres to some simple syntax
33   * rules as described in the javadoc for the {@link #valueOf(String)} method.
34   *
35   * <p>
36   * The syntax used for the label info descriptor is just the text to be displayed by the label with
37   * an ampersand (&) optionally inserted before the character that is to be used as a
38   * mnemonic for the label.
39   * </p>
40   *
41   * <p>
42   * Example: To create a label with the text {@code My Label} and the capital L as a mnemonic,
43   * use the following descriptor:
44   * </p>
45   *
46   * <pre>
47   *     <code>My &Label</code>
48   * </pre>
49   *
50   * <p>
51   * A backslash character (\) can be used to escape ampersand characters that are to be displayed as
52   * part of the label's text. For example:
53   * </p>
54   *
55   * <pre>
56   *     <code>Save \& Run</code>
57   * </pre>
58   *
59   * <p>
60   * Only one non-escaped backslash can appear in the label descriptor. Attempting to specify more
61   * than one mnemonic character will result in an exception being thrown.
62   * TODO finish comment regarding backslash chars in props file
63   * Note that for label descriptors provided in properties files, an extra backslash will be required
64   * to avoid the single backslash being interpreted as a special character.
65   * </p>
66   *
67   * @author Keith Donald
68   * @author Peter De Bruycker
69   * @author Kevin Stembridge
70   */
71  public final class LabelInfo {
72  
73      private static final Log logger = LogFactory.getLog(LabelInfo.class);
74  
75      private static final LabelInfo BLANK_LABEL_INFO = new LabelInfo("");
76  
77      private static final int DEFAULT_MNEMONIC = 0;
78  
79      private static final int DEFAULT_MNEMONIC_INDEX = -1;
80  
81      private final String text;
82  
83      private final int mnemonic;
84  
85      private final int mnemonicIndex;
86  
87      /**
88       * Creates a new {@code LabelInfo} instance by parsing the given label descriptor to determine
89       * the label's text and mnemonic character. The syntax rules for the descriptor are as follows:
90       *
91       * <ul>
92       * <li>The descriptor may be null or an empty string, in which case, an instance with no text
93       * or mnemonic will be returned.</li>
94       * <li>The mnemonic character is indicated by a preceding ampersand (&).</li>
95       * <li>A backslash character (\) can be used to escape ampersand characters that are to be
96       * displayed as part of the label's text.</li>
97       * <li>A double backslash (a backslash escaped by a backslash) indicates that a single backslash
98       * is to appear in the label's text.</li>
99       * <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 }