Java tutorial
/******************************************************************************* * Copyright 2008, 2013 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation * *******************************************************************************/ package org.eclipse.edt.ide.rui.visualeditor.internal.util; import java.lang.Character.UnicodeBlock; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.swt.custom.CTabFolder; import org.eclipse.swt.custom.CTabItem; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Combo; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.MenuItem; import org.eclipse.swt.widgets.TabFolder; import org.eclipse.swt.widgets.TabItem; import org.eclipse.swt.widgets.Text; /** * Instances of this class may be used to supply mnemonics to the * text for controls. * There are preferences which can be set by products to control how * these mnemonics are generated and applied. * <p> * There are two types of mnemonics which can be added to a label: * embedded mnemonics and appended mnemonics. An embedded mnemonic uses * an existing letter in the label for the mnemonic. An appended mnemonic * is added to the end of the label (but prior to any punctuation or accelerators) * and is of the form (X). * <p> * The org.eclipse.rse.ui/MNEMONICS_POLICY preference establishes the * desire to generated embedded mnemonics using letters that already * exist in the text of the controls and/or to generate appended mnemonics * if an embedded mnemonic cannot be found or is not desired. * The policy is composed of bit flags. * See {@link #EMBED_MNEMONICS} and {@link #APPEND_MNEMONICS} for the flag values. * See {@link #POLICY_DEFAULT} for the default policy value. * A policy value of 0 will disable the generation of all mnemonics. * <p> * The org.eclipse.rse.ui/APPEND_MNEMONICS_PATTERN preference is used to * further qualify the appending behavior by the current locale. If the * current locale name matches this pattern then appending can be performed. * See {@link #APPEND_MNEMONICS_PATTERN_DEFAULT} for the default pattern. * <p> * Mnemonics on menus are allowed to have duplicates. Attempts are made to find the * least used mnemonic when finding a duplicate. * @noextend This class is not intended to be subclassed by clients. */ public class Mnemonics { /** * An option bit mask - value 1. * If on, specifies whether or not to insert mnemonic indications into * the current text of a label. * If off, all other options are ignored. */ public static final int EMBED_MNEMONICS = 1; /** * An option bit mask - value 2. * If on, specifies to generate mnemonics of the form (X) at the end of labels for * those languages matching the locale pattern. * If off, then only characters from the label will be used as mnemonics. */ public static final int APPEND_MNEMONICS = 2; /** * The simple name of the preference that holds the pattern to be used for matching * against the locale to determine if APPEND_MNEMONICS option applies. */ public static final String APPEND_MNEMONICS_PATTERN_PREFERENCE = "APPEND_MNEMONICS_PATTERN"; //$NON-NLS-1$ /** * Some products will to append mnemonics only for certain locales. * The following provides the default pattern for matching the locale. * The default pattern matches Chinese, Japanese, and Korean. */ public static final String APPEND_MNEMONICS_PATTERN_DEFAULT = "zh.*|ja.*|ko.*"; //$NON-NLS-1$ /** * The simple name of the preference that determines the policy to be used when applying mnemonics to menus and composites. */ public static final String POLICY_PREFERENCE = "MNEMONICS_POLICY"; //$NON-NLS-1$ /** * The default mnemonics policy. If no policy is specified in a call to generate * mnemonics this policy will be used. Can be overridden by the * org.eclipse.rse.ui/MNEMONICS_POLICY preference. */ public static final int POLICY_DEFAULT = EMBED_MNEMONICS | APPEND_MNEMONICS; private static final char MNEMONIC_CHAR = '&'; /* * Interesting ISO 639-1 language codes. */ private static final String LC_GREEK = "el"; //$NON-NLS-1$ private static final String LC_RUSSIAN = "ru"; //$NON-NLS-1$ /* * Known valid mnemonic candidates */ private static final String GREEK_MNEMONICS = "\u0391\u0392\u0393\u0394\u0395\u0396\u0397\u0398\u0399\u039a\u039b\u039c\u039d\u039e\u039f\u03a0\u03a1\u03a3\u03a4\u03a5\u03a6\u03a7\u03a8\u03a9"; //$NON-NLS-1$ private static final String RUSSIAN_MNEMONICS = "\u0410\u0411\u0412\u0413\u0414\u0145\u0401\u0416\u0417\u0418\u0419\u041a\u041b\u041c\u041d\u041e\u041f\u0420\u0421\u0422\u0423\u0424\u0425\u0426\u0427\u0428\u0429\u042a\u042b\u042c\u042d\u042e\u042f"; //$NON-NLS-1$ private static final String LATIN_MNEMONICS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; //$NON-NLS-1$ private final static Pattern TRANSPARENT_ENDING_PATTERN = Pattern .compile("(\\s|\\.\\.\\.|>|<|:|\uff0e\uff0e\uff0e|\uff1e|\uff1c|\uff1a|\\t.*)+$"); //$NON-NLS-1$ private boolean applyMnemonicsToPrecedingLabels = true; private Map usedCharacters = new HashMap(); /** * Helper method to return the mnemonic from a string. * Helpful when it is necessary to know the mnemonic assigned so it can be reassigned, * such as is necessary for buttons which toggle their text. * @param text the label from which to extract the mnemonic * @return the mnemonic if assigned, else a blank character. */ public static char getMnemonic(String text) { int idx = text.indexOf(MNEMONIC_CHAR); if (idx >= 0 && idx < (text.length() - 1)) return text.charAt(idx + 1); else return ' '; } /** * Given a label and mnemonic, this applies that mnemonic to the label. * Not normally called from other classes, but rather by the setUniqueMnemonic * methods in this class. * @param label String to which to apply the mnemonic * @param mnemonicChar the character that is to be the mnemonic character * @return input String with '&' inserted in front of the given character, * or with "(c)" appended to the label at a proper position in case the * character c is not part of the label. */ public static String applyMnemonic(String label, char mnemonicChar) { int labelLen = label.length(); if (labelLen == 0) return label; StringBuffer newLabel = new StringBuffer(label); int mcharPos = label.indexOf(mnemonicChar); if (mcharPos != -1) newLabel.insert(mcharPos, MNEMONIC_CHAR); else { String addedMnemonic = new String("(" + MNEMONIC_CHAR + mnemonicChar + ")"); //$NON-NLS-1$ //$NON-NLS-2$ int p = getEndingPosition(label); newLabel.insert(p, addedMnemonic); } return newLabel.toString(); } /** * Helper method to strip the mnemonic from a string. * Useful if using Eclipse supplied labels. * @param text the label from which to strip the mnemonic * @return the label with the mnemonic stripped */ public static String removeMnemonic(String text) { String[] parts = text.split("\\(\\&.\\)", 2); //$NON-NLS-1$ if (parts.length == 1) { parts = text.split("\\&", 2); //$NON-NLS-1$ } if (parts.length == 2) { text = parts[0] + parts[1]; } return text; } /** * Finds the point at which to insert a mnemonic of the form (&X). * Checks for transparent endings. * @param label the label to check * @return the position at which a mnemonic of the form (&X) can be inserted. */ private static int getEndingPosition(String label) { Matcher m = TRANSPARENT_ENDING_PATTERN.matcher(label); int result = m.find() ? m.start() : label.length(); return result; } /** * Clear the list of used mnemonic characters */ public void clear() { usedCharacters.clear(); } /** * Resets the list of used mnemonic characters to those in the string. * * @param usedMnemonics A String listing the characters to mark used as * mnemonics. Each character will be considered in a case * insensitive manner. */ public void clear(String usedMnemonics) { clear(); makeUsed(usedMnemonics); } /** * Sets a mnemonic in the given string and returns the result. * Functions according to the default policy as specified in * Sets the mnemonic according to the org.eclipse.rse.ui/MNEMONICS_POLICY preference. * Not normally called from other classes, but rather by the setMnemonic * methods in this class. * @param label The string to which to apply the mnemonic * @return the result string with the mnemonic embedded or appended */ public String setUniqueMnemonic(String label) { String localePattern = APPEND_MNEMONICS_PATTERN_DEFAULT; String result = setUniqueMnemonic(label, EMBED_MNEMONICS, localePattern, false); return result; } /** * Given a string, this starts at the first character and iterates until * it finds a character not already used as a mnemonic. * Not normally called from other classes, but rather by the setMnemonic * methods in this class. * If the label already has a mnemonic it is not touched. * @param label String to which to apply a mnemonic * @param flags The policy bit field composed of the following options * EMBED_MNEMONICS and APPEND_MNEMONICS * @param allowDuplicates true if duplicates can be allowed. Typically used only * when assigning mnemonics to menu items. If true, it will attempt to assign the * least used duplicate mnemonic for the string from the context established so far. * @return input String with '&' inserted in front of the mnemonic character */ private String setUniqueMnemonic(String label, int flags, String localePattern, boolean allowDuplicates) { // determine the cases where the label does not need a mnemonic if (flags == 0 || label == null || label.trim().length() == 0 || label.equals("?")) { //$NON-NLS-1$ return label; } StringBuffer buffer = new StringBuffer(label); char mn = getMnemonic(label); if (mn == ' ' && ((flags & EMBED_MNEMONICS) > 0)) { // no mnemonic exists, try embedding int p = findUniqueMnemonic(label); if (p >= 0) { // a character in the label can be used as the mnemonic mn = label.charAt(p); buffer.insert(p, MNEMONIC_CHAR); } } if (mn == ' ' && allowDuplicates) { // no mnemonic exists, try a duplicate int n = getEndingPosition(label); int m = 999; int p = -1; for (int i = 0; i < n; i++) { char ch = label.charAt(i); if (isAcceptable(ch) && timesUsed(ch) < m) { m = timesUsed(ch); p = i; } } if (p >= 0) { mn = label.charAt(p); buffer.insert(p, MNEMONIC_CHAR); } } // if (mn == ' ' && ((flags & APPEND_MNEMONICS) > 0)) { // no mnemonic exists, try appending a mnemonic // String localeName = ULocale.getDefault().getName(); // if (localeName.matches(localePattern)) { // String candidates = getCandidates(); // int p = findUniqueMnemonic(candidates); // if (p >= 0) { // mn = candidates.charAt(p); // String mnemonicString = "(" + MNEMONIC_CHAR + mn + ")"; //$NON-NLS-1$ //$NON-NLS-2$ // p = getEndingPosition(label); // buffer.insert(p, mnemonicString); // } // } // } makeUsed(mn); return buffer.toString(); } /** * Determine if given char is a unique mnemonic. * @param ch the character to test. * @return true if the character has not yet been used. */ public boolean isUniqueMnemonic(char ch) { return timesUsed(ch) == 0; } private String getLocalePattern() { return APPEND_MNEMONICS_PATTERN_DEFAULT; } private int getPolicy() { return EMBED_MNEMONICS; } private boolean isEmbedding() { return (getPolicy() & EMBED_MNEMONICS) > 0; } private boolean isAppending() { return (getPolicy() & APPEND_MNEMONICS) > 0; } /** * @return a string of acceptable mnemonic candidates for the language * of the current locale. */ private String getCandidates() { /* * This is a coarse-grained approach and uses the 2-letter language codes from ISO 639-1. * This should be quite sufficient for mnemonic generation. */ // String language = ULocale.getDefault().getLanguage(); // if (language.equals(LC_GREEK)) return GREEK_MNEMONICS; // if (language.equals(LC_RUSSIAN)) return RUSSIAN_MNEMONICS; return LATIN_MNEMONICS; } /** * Find a unique mnemonic char in given string. * @param label the string in which to search for the best mnemonic character * @return index position of unique character in input string, or -1 if none found. */ private int findUniqueMnemonic(String label) { int uniqueIndex = -1; char ch = label.charAt(0); for (int i = 0; (i < label.length()) && (uniqueIndex == -1); i++) { ch = label.charAt(i); if (ch == '\t') { // stop at accelerators too break; } if (timesUsed(ch) == 0 && isAcceptable(ch)) { uniqueIndex = i; } } return uniqueIndex; } private boolean isAcceptable(char ch) { UnicodeBlock block = UnicodeBlock.of(ch); boolean result = (isAcceptable(block) && Character.isLetter(ch)); // the character is a letter return result; } private boolean isAcceptable(UnicodeBlock block) { if (block == UnicodeBlock.BASIC_LATIN) return true; if (block == UnicodeBlock.LATIN_1_SUPPLEMENT) return true; if (block == UnicodeBlock.LATIN_EXTENDED_A) return true; if (block == UnicodeBlock.LATIN_EXTENDED_B) return true; // if (block == UnicodeBlock.LATIN_EXTENDED_C) return true; // if (block == UnicodeBlock.LATIN_EXTENDED_D) return true; if (block == UnicodeBlock.GREEK) return true; if (block == UnicodeBlock.CYRILLIC) return true; if (block == UnicodeBlock.HEBREW) return true; if (block == UnicodeBlock.ARABIC) return true; return false; } /** * Returns the number of times a given character is used as a mnemonic in this * context. * @param ch the character to examine * @return the number of times it has been reported as being used. */ private int timesUsed(char ch) { // TODO if we are guaranteed java 1.5 we can use Character.valueOf(ch) int result = 0; Integer count = (Integer) usedCharacters.get(new Character(ch)); if (count != null) { result = count.intValue(); } return result; } private void makeUsed(char ch) { // TODO if we are guaranteed java 1.5 we can use Character.valueOf(ch) if (ch != ' ') { makeUsed(new Character(Character.toLowerCase(ch))); makeUsed(new Character(Character.toUpperCase(ch))); } } private void makeUsed(Character ch) { Integer count = (Integer) usedCharacters.get(ch); if (count == null) { count = new Integer(1); } usedCharacters.put(ch, count); } private void makeUsed(String s) { for (int i = 0; i < s.length(); i++) { makeUsed(s.charAt(i)); } } /** * Returns a string with the mnemonics for a given array of strings. * @param strings the array of strings. * @return a string containing the mnemonics. */ private String getMnemonicsFromStrings(String[] strings) { StringBuffer result = new StringBuffer(); for (int i = 0; i < strings.length; i++) { int idx = strings[i].indexOf(MNEMONIC_CHAR); if (idx != -1) { result.append(strings[i].charAt(idx + 1)); } } return result.toString(); } /** * Set whether to apply mnemonics to labels preceding text fields, combos and inheritable entry fields. * This is for consistency with Eclipse. Only set to <code>false</code> if it does not work * in your dialog, wizard, preference or property page, i.e. you have labels preceding these * widgets that do not necessarily refer to them. * @param apply <code>true</code> to apply mnemonic to preceding labels, <code>false</code> otherwise. * @return this instance, for convenience */ public Mnemonics setApplyMnemonicsToPrecedingLabels(boolean apply) { this.applyMnemonicsToPrecedingLabels = apply; return this; } /** * Adds mnemonic to a tab folder */ public void setMnemonics(CTabFolder tabFolder) { CTabItem[] tabItems = tabFolder.getItems(); for (int i = 0; i < tabItems.length; i++) { String text = tabItems[i].getText(); if ((text != null) && (text.trim().length() > 0)) { String newText = setUniqueMnemonic(text); if (!text.equals(newText)) { tabItems[i].setText(newText); } } } for (int i = 0; i < tabItems.length; i++) { if (tabItems[i].getControl() instanceof Composite) setMnemonics((Composite) tabItems[i].getControl()); } } /** * Adds mnemonic to a tab folder */ public void setMnemonics(TabFolder tabFolder) { TabItem[] tabItems = tabFolder.getItems(); for (int i = 0; i < tabItems.length; i++) { String text = tabItems[i].getText(); if ((text != null) && (text.trim().length() > 0)) { String newText = setUniqueMnemonic(text); if (!text.equals(newText)) { tabItems[i].setText(newText); } } } for (int i = 0; i < tabItems.length; i++) { if (tabItems[i].getControl() instanceof Composite) setMnemonics((Composite) tabItems[i].getControl()); } } /** * Adds a mnemonic to an SWT Button such that the user can select it via Ctrl/Alt+mnemonic. * Note a mnemonic unique to this window is chosen. * @param button the button to equip with a mnemonic * @return <code>true</code> if the button was actually changed */ public boolean setMnemonic(Button button) { boolean changed = false; String text = button.getText(); if ((text != null) && (text.trim().length() > 0)) { String newText = setUniqueMnemonic(text); if (!text.equals(newText)) { button.setText(newText); changed = true; } } return changed; } /** * Given a menu, this method walks all the items and assigns each a mnemonic. * Note that menu item mnemonics do not have to be unique. * The mnemonics used on cascaded menus are independent of those of the parent. * Handles cascading menus. * Call this after populating the menu. * @param menu the menu to examine */ public void setMnemonics(Menu menu) { // this set will contain menu items without mnemonics in order of length of their text Collection embeddingItems = new TreeSet(new Comparator() { public int compare(Object o1, Object o2) { String t1 = ((MenuItem) o1).getText(); String t2 = ((MenuItem) o2).getText(); int l1 = getEndingPosition(t1); int l2 = getEndingPosition(t2); if (l1 < l2) return -1; if (l1 > l2) return 1; return t1.compareTo(t2); } }); Collection appendingItems = new ArrayList(10); // handle cascades, populate the set, record existing mnemonics MenuItem[] menuItems = menu.getItems(); for (int i = 0; i < menuItems.length; i++) { MenuItem menuItem = menuItems[i]; Menu cascade = menuItem.getMenu(); if (cascade != null) { Mnemonics context = new Mnemonics(); context.setMnemonics(cascade); } String text = menuItem.getText(); if (text.length() > 0) { char ch = getMnemonic(text); if (ch == ' ') { embeddingItems.add(menuItem); appendingItems.add(menuItem); } else { makeUsed(ch); } } } // assign mnemonics to the items of the set String localePattern = getLocalePattern(); if (isEmbedding()) { processMenuItems(embeddingItems, EMBED_MNEMONICS, localePattern); } if (isAppending()) { processMenuItems(appendingItems, APPEND_MNEMONICS, localePattern); } } private void processMenuItems(Collection collection, int flags, String localePattern) { for (Iterator z = collection.iterator(); z.hasNext();) { MenuItem menuItem = (MenuItem) z.next(); String text = menuItem.getText(); String newText = setUniqueMnemonic(text, flags, localePattern, true); if (!text.equals(newText)) { Image image = menuItem.getImage(); menuItem.setText(newText); if (image != null) { menuItem.setImage(image); } } } } /** * Given a Composite, this method walks all the children recursively and * and sets the mnemonics uniquely for each child control where a * mnemonic makes sense (eg, buttons). * The letter/digit chosen for the mnemonic is unique for this Composite, * so you should call this on as high a level of a composite as possible * per window. * Call this after populating your controls. * @param parent the parent control to examine. */ public void setMnemonics(Composite parent) { setMnemonics(parent, new HashSet()); } /** * Given a Composite, this method walks all the children recursively and * and sets the mnemonics uniquely for each child control where a * mnemonic makes sense (for example, buttons). * The letter/digit chosen for the mnemonic is unique for this Composite, * so you should call this on as high a level of a composite as possible * per window. * Call this after populating your controls. * @param parent the parent control to examine. * @param ignoredControls the set of controls in which to not set mnemonics. * If the controls are composites, their children are also not examined. */ public void setMnemonics(Composite parent, Set ignoredControls) { gatherCompositeMnemonics(parent); boolean mustLayout = setCompositeMnemonics(parent, ignoredControls); if (mustLayout) { parent.layout(true); } } private boolean setCompositeMnemonics(Composite parent, Set ignoredControls) { Control children[] = parent.getChildren(); Control currentChild = null; boolean mustLayout = false; for (int i = 0; i < children.length; i++) { Control previousChild = currentChild; currentChild = children[i]; if (!ignoredControls.contains(currentChild)) { if (currentChild instanceof Combo) { if (applyMnemonicsToPrecedingLabels && previousChild instanceof Label) { Label label = (Label) previousChild; String text = label.getText(); if ((text != null) && (text.trim().length() > 0)) { String newText = setUniqueMnemonic(text); if (!text.equals(newText)) { label.setText(newText); mustLayout = true; } } } } else if (currentChild instanceof Button) { mustLayout |= setMnemonic((Button) currentChild); } else if (currentChild instanceof Composite) { /* * d54732: (KM) we test Composites last since we don't want to recurse if it is a Combo. * For a combo, we want to check if there is a preceding label. * It's meaningless for a combo to have children. */ mustLayout |= setCompositeMnemonics((Composite) currentChild, ignoredControls); } // ignore other controls } } return mustLayout; } private void gatherCompositeMnemonics(Composite parent) { Control children[] = parent.getChildren(); Control currentChild = null; for (int i = 0; i < children.length; i++) { Control previousChild = currentChild; currentChild = children[i]; String childText = null; if (currentChild instanceof Combo || currentChild instanceof Text) { if (applyMnemonicsToPrecedingLabels && previousChild instanceof Label) { Label label = (Label) previousChild; childText = label.getText(); } } else if (currentChild instanceof Button) { childText = ((Button) currentChild).getText(); } else if (currentChild instanceof Composite) { gatherCompositeMnemonics((Composite) currentChild); } // ignore other controls if (childText != null) { char ch = getMnemonic(childText); makeUsed(ch); } } } /** * Set if the mnemonics are for a preference page * Preference pages already have a few buttons with mnemonics set by Eclipse * We have to make sure we do not use the ones they use */ public Mnemonics setOnPreferencePage(boolean page) { if (page) { String[] labels = JFaceResources.getStrings(new String[] { "defaults", "apply" }); //$NON-NLS-1$ //$NON-NLS-2$ String used = getMnemonicsFromStrings(labels).toUpperCase(); makeUsed(used); } return this; } /** * Set if the mnemonics are for a wizard page * Wizard pages already have a few buttons with mnemonics set by Eclipse * We have to make sure we do not use the ones they use */ public Mnemonics setOnWizardPage(boolean page) { if (page) { String[] labels = new String[] { IDialogConstants.BACK_LABEL, IDialogConstants.NEXT_LABEL, IDialogConstants.FINISH_LABEL }; String used = getMnemonicsFromStrings(labels).toUpperCase(); makeUsed(used); } return this; } }