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 }