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 }