/**
* Copyright (C) 2015 Valkyrie RCP
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.valkyriercp.core.support;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import javax.swing.*;
/**
* An immutable parameter object consisting of the text, mnemonic character and mnemonic character
* index that may be associated with a labeled component. This class also acts as a factory for
* creating instances of itself based on a string descriptor that adheres to some simple syntax
* rules as described in the javadoc for the {@link #valueOf(String)} method.
*
* <p>
* The syntax used for the label info descriptor is just the text to be displayed by the label with
* an ampersand (&) optionally inserted before the character that is to be used as a
* mnemonic for the label.
* </p>
*
* <p>
* Example: To create a label with the text {@code My Label} and the capital L as a mnemonic,
* use the following descriptor:
* </p>
*
* <pre>
* <code>My &Label</code>
* </pre>
*
* <p>
* A backslash character (\) can be used to escape ampersand characters that are to be displayed as
* part of the label's text. For example:
* </p>
*
* <pre>
* <code>Save \& Run</code>
* </pre>
*
* <p>
* Only one non-escaped backslash can appear in the label descriptor. Attempting to specify more
* than one mnemonic character will result in an exception being thrown.
* TODO finish comment regarding backslash chars in props file
* Note that for label descriptors provided in properties files, an extra backslash will be required
* to avoid the single backslash being interpreted as a special character.
* </p>
*
* @author Keith Donald
* @author Peter De Bruycker
* @author Kevin Stembridge
*/
public final class LabelInfo {
private static final Log logger = LogFactory.getLog(LabelInfo.class);
private static final LabelInfo BLANK_LABEL_INFO = new LabelInfo("");
private static final int DEFAULT_MNEMONIC = 0;
private static final int DEFAULT_MNEMONIC_INDEX = -1;
private final String text;
private final int mnemonic;
private final int mnemonicIndex;
/**
* Creates a new {@code LabelInfo} instance by parsing the given label descriptor to determine
* the label's text and mnemonic character. The syntax rules for the descriptor are as follows:
*
* <ul>
* <li>The descriptor may be null or an empty string, in which case, an instance with no text
* or mnemonic will be returned.</li>
* <li>The mnemonic character is indicated by a preceding ampersand (&).</li>
* <li>A backslash character (\) can be used to escape ampersand characters that are to be
* displayed as part of the label's text.</li>
* <li>A double backslash (a backslash escaped by a backslash) indicates that a single backslash
* is to appear in the label's text.</li>
* <li>Only one non-escaped ampersand can appear in the descriptor.</li>
* <li>A space character cannot be specified as the mnemonic character.</li>
* </ul>
*
* @param labelDescriptor The label descriptor. The text may be null or empty, in which case a
* blank {@code LabelInfo} instance will be returned.
*
* @return A {@code LabelInfo} instance that is described by the given descriptor.
* Never returns null.
*
* @throws IllegalArgumentException if {@code labelDescriptor} violates any of the syntax rules
* described above.
*/
public static LabelInfo valueOf(final String labelDescriptor) {
if (logger.isDebugEnabled()) {
logger.debug("Creating a new LabelInfo from label descriptor [" + labelDescriptor + "]");
}
if (!StringUtils.hasText(labelDescriptor)) {
return BLANK_LABEL_INFO;
}
StringBuffer labelText = new StringBuffer();
char mnemonicChar = '\0';
int mnemonicCharIndex = DEFAULT_MNEMONIC_INDEX;
char currentChar;
for (int i = 0; i < labelDescriptor.length();) {
currentChar = labelDescriptor.charAt(i);
int nextCharIndex = i + 1;
if (currentChar == '\\') {
//confirm that the next char is a valid escaped char, add the next char to the
//stringbuffer then skip ahead 2 chars.
checkForValidEscapedCharacter(nextCharIndex, labelDescriptor);
labelText.append(labelDescriptor.charAt(nextCharIndex));
i++;
i++;
}
else if (currentChar == '&') {
//we've found a mnemonic indicator, so...
//confirm that we haven't already found one, ...
if (mnemonicChar != '\0') {
throw new IllegalArgumentException(
"The label descriptor ["
+ labelDescriptor
+ "] can only contain one non-escaped ampersand.");
}
//...that it isn't the last character, ...
if (nextCharIndex >= labelDescriptor.length()) {
throw new IllegalArgumentException(
"The label descriptor ["
+ labelDescriptor
+ "] cannot have a non-escaped ampersand as its last character.");
}
//...and that the character that it prefixes is a valid mnemonic character.
mnemonicChar = labelDescriptor.charAt(nextCharIndex);
checkForValidMnemonicChar(mnemonicChar, labelDescriptor);
//...add it to the stringbuffer and set the mnemonic index to the position of
//the newly added char, then skip ahead 2 characters
labelText.append(mnemonicChar);
mnemonicCharIndex = labelText.length() - 1;
i++;
i++;
}
else {
labelText.append(currentChar);
i++;
}
}
// mnemonics work with VK_XXX (see KeyEvent) and only uppercase letters are used as event
return new LabelInfo(labelText.toString(), Character.toUpperCase(mnemonicChar), mnemonicCharIndex);
}
/**
* Confirms that the character at the specified index within the given label descriptor is
* a valid 'escapable' character. i.e. either an ampersand or backslash.
*
* @param index The position within the label descriptor of the character to be checked.
* @param labelDescriptor The label descriptor.
*
* @throws NullPointerException if {@code labelDescriptor} is null.
* @throws IllegalArgumentException if the given {@code index} position is beyond the length
* of the string or if the character at that position is not an ampersand or backslash.
*/
private static void checkForValidEscapedCharacter(int index, String labelDescriptor) {
if (index >= labelDescriptor.length()) {
throw new IllegalArgumentException(
"The label descriptor contains an invalid escape sequence. Backslash "
+ "characters (\\) must be followed by either an ampersand (&) or another "
+ "backslash.");
}
char escapedChar = labelDescriptor.charAt(index);
if (escapedChar != '&' && escapedChar != '\\') {
throw new IllegalArgumentException(
"The label descriptor ["
+ labelDescriptor
+ "] contains an invalid escape sequence. Backslash "
+ "characters (\\) must be followed by either an ampersand (&) or another "
+ "backslash.");
}
}
/**
* Confirms that the given character is allowed to be used as a mnemonic. Currently, only
* spaces are disallowed.
*
* @param mnemonicChar The mnemonic character.
* @param labelDescriptor The label descriptor.
*/
private static void checkForValidMnemonicChar(char mnemonicChar, String labelDescriptor) {
if (mnemonicChar == ' ') {
throw new IllegalArgumentException(
"The mnemonic character cannot be a space. ["
+ labelDescriptor
+ "]");
}
}
/**
* Creates a new {@code LabelInfo} with the given text and no specified mnemonic.
*
* @param text The text to be displayed by the label. This may be an empty string but
* cannot be null.
*
* @throws IllegalArgumentException if {@code text} is null.
*/
public LabelInfo(String text) {
this(text, DEFAULT_MNEMONIC, DEFAULT_MNEMONIC_INDEX);
}
/**
* Creates a new {@code LabelInfo} with the given text and mnemonic character.
*
* @param text The text to be displayed by the label. This may be an empty string but cannot
* be null.
* @param mnemonic The character from the label text that acts as a mnemonic.
*
* @throws IllegalArgumentException if {@code text} is null or if {@code mnemonic} is a
* negative value.
*/
public LabelInfo(String text, int mnemonic) {
this(text, mnemonic, DEFAULT_MNEMONIC_INDEX);
}
/**
* Creates a new {@code LabelInfo} with the given text, mnemonic character and mnemonic index.
*
* @param text The text to be displayed by the label. This may be an empty string but cannot
* be null.
* @param mnemonic The character from the label text that acts as a mnemonic.
* @param mnemonicIndex The zero-based index of the mnemonic character within the label text.
* If the specified label text is an empty string, this property will be ignored and set to -1.
*
* @throws IllegalArgumentException if {@code text} is null, if {@code mnemonic} is a negative
* value, if {@code mnemonicIndex} is less than -1 or if {@code mnemonicIndex} is outside the
* length of {@code text}.
*/
public LabelInfo(String text, int mnemonic, int mnemonicIndex) {
Assert.notNull(text, "text");
Assert.isTrue(mnemonic >= 0, "mnemonic must be greater than or equal to 0");
Assert.isTrue(mnemonicIndex >= -1, "mnemonicIndex must be greater than or equal to -1");
Assert.isTrue(mnemonicIndex < text.length(),
"The mnemonic index must be less than the text length; mnemonicIndex = "
+ mnemonicIndex
+ ", text length = "
+ text.length());
this.text = text;
if (!StringUtils.hasText(text)) {
mnemonicIndex = DEFAULT_MNEMONIC_INDEX;
}
if (logger.isDebugEnabled()) {
logger.debug("Constructing a new LabelInfo instance with properties: text='"
+ text
+ "', mnemonic="
+ mnemonic
+ ", mnemonicIndex="
+ mnemonicIndex);
}
this.mnemonic = mnemonic;
this.mnemonicIndex = mnemonicIndex;
}
/**
* {@inheritDoc}
*/
public int hashCode() {
final int PRIME = 31;
int result = 1;
result = PRIME * result + this.mnemonic;
result = PRIME * result + this.mnemonicIndex;
result = PRIME * result + ((this.text == null) ? 0 : this.text.hashCode());
return result;
}
/**
* {@inheritDoc}
*/
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final LabelInfo other = (LabelInfo) obj;
if (this.mnemonic != other.mnemonic)
return false;
if (this.mnemonicIndex != other.mnemonicIndex)
return false;
if (this.text == null) {
if (other.text != null)
return false;
} else if (!this.text.equals(other.text))
return false;
return true;
}
/**
* Configures the given label with the parameters from this instance.
*
* @param label The label that is to be configured.
* @throws IllegalArgumentException if {@code label} is null.
*/
public void configureLabel(JLabel label) {
Assert.notNull(label, "label");
label.setText(this.text);
label.setDisplayedMnemonic(getMnemonic());
if (getMnemonicIndex() >= -1) {
label.setDisplayedMnemonicIndex(getMnemonicIndex());
}
}
/**
* Configures the given label with the property values described by this instance and then sets
* it as the label for the given component.
*
* @param label The label to be configured.
* @param component The component that the label is 'for'.
*
* @throws IllegalArgumentException if either argument is null.
*
* @see JLabel#setLabelFor(java.awt.Component)
*/
public void configureLabelFor(JLabel label, JComponent component) {
Assert.notNull(label, "label");
Assert.notNull(component, "component");
configureLabel(label);
if (!(component instanceof JPanel)) {
String labelText = label.getText();
if (!labelText.endsWith(":")) {
if (logger.isDebugEnabled()) {
logger.debug("Appending colon to text field label text '" + this.text + "'");
}
label.setText(labelText + ":");
}
}
label.setLabelFor(component);
}
/**
* Configures the given button with the properties held in this instance. Note that this
* instance doesn't hold any keystroke accelerator information.
*
* @param button The button to be configured.
*
* @throws IllegalArgumentException if {@code button} is null.
*/
public void configureButton(AbstractButton button) {
Assert.notNull(button);
button.setText(this.text);
button.setMnemonic(getMnemonic());
button.setDisplayedMnemonicIndex(getMnemonicIndex());
}
/**
* Returns the text to be displayed by the label.
* @return The label text, possibly an empty string but never null.
*/
public String getText() {
return this.text;
}
/**
* Returns the character that is to be treated as the mnemonic character for the label.
*
* @return The mnemonic character.
*/
public int getMnemonic() {
return this.mnemonic;
}
/**
* Returns the index within the label text of the mnemonic character.
* @return The index of the mnemonic character, or -1 if no mnemonic index is specified.
*/
public int getMnemonicIndex() {
return this.mnemonicIndex;
}
/**
* {@inheritDoc}
*/
public String toString() {
return new ToStringCreator(this)
.append("text", this.text)
.append("mnemonic", this.mnemonic)
.append("mnemonicIndex", this.mnemonicIndex)
.toString();
}
}