Merge pull request #2382 from GKFX/mnem-patch

Dynamically add menu mnemonics to all menus
This commit is contained in:
Ben Fry
2014-11-18 17:11:23 -05:00
4 changed files with 300 additions and 45 deletions
+26 -28
View File
@@ -51,8 +51,8 @@ public abstract class Editor extends JFrame implements RunnerListener {
protected EditorState state;
protected Mode mode;
// otherwise, if the window is resized with the message label
// set to blank, it's preferredSize() will be fukered
// Otherwise, if the window is resized with the message label
// set to blank, its preferredSize() will be fuckered
static protected final String EMPTY =
" " +
" " +
@@ -66,7 +66,7 @@ public abstract class Editor extends JFrame implements RunnerListener {
private PageFormat pageFormat;
private PrinterJob printerJob;
// file and sketch menus for re-inserting items
// File and sketch menus for re-inserting items
private JMenu fileMenu;
// private JMenuItem saveMenuItem;
// private JMenuItem saveAsMenuItem;
@@ -148,7 +148,10 @@ public abstract class Editor extends JFrame implements RunnerListener {
// fileMenu.insert(base.getSketchbookMenu(), 2);
fileMenu.insert(base.getRecentMenu(), 2);
// fileMenu.insert(mode.getExamplesMenu(), 3);
Toolkit.setMenuMnemsInside(fileMenu);
sketchMenu.insert(mode.getImportMenu(), 4);
Toolkit.setMenuMnemsInside(sketchMenu);
mode.insertToolbarRecentMenu();
}
@@ -398,7 +401,6 @@ public abstract class Editor extends JFrame implements RunnerListener {
public void actionPerformed(ActionEvent e) {
if (!sketch.isModified()) {
base.changeMode(m);
} else {
Base.showWarning("Save",
"Please save the sketch before changing the mode.",
@@ -433,6 +435,8 @@ public abstract class Editor extends JFrame implements RunnerListener {
}
});
modeMenu.add(addLib);
Toolkit.setMenuMnemsInside(modeMenu);
}
@@ -557,7 +561,6 @@ public abstract class Editor extends JFrame implements RunnerListener {
protected void buildMenuBar() {
JMenuBar menubar = new JMenuBar();
menubar = new JMenuBar();
fileMenu = buildFileMenu();
menubar.add(fileMenu);
menubar.add(buildEditMenu());
@@ -573,6 +576,7 @@ public abstract class Editor extends JFrame implements RunnerListener {
menubar.add(getToolMenu());
menubar.add(buildHelpMenu());
Toolkit.setMenuMnemonics(menubar);
setJMenuBar(menubar);
}
@@ -734,11 +738,11 @@ public abstract class Editor extends JFrame implements RunnerListener {
menu.addSeparator();
cutItems = Toolkit.newJMenuItem("Cut", 'X');
cutItems = Toolkit.newJMenuItem(Language.text("menu.edit.cut"), 'X');
cutItems.addActionListener(cutAction = new CutAction());
menu.add(cutItems);
copyItems = Toolkit.newJMenuItem("Copy", 'C');
copyItems = Toolkit.newJMenuItem(Language.text("menu.edit.copy"), 'C');
copyItems.addActionListener(copyAction = new CopyAction());
menu.add(copyItems);
@@ -933,7 +937,9 @@ public abstract class Editor extends JFrame implements RunnerListener {
});
sketchMenu.add(item);
sketchMenu.addSeparator();
if (runItems != null && runItems.length != 0) {
sketchMenu.addSeparator();
}
// final Editor editorName = this;
@@ -957,7 +963,6 @@ public abstract class Editor extends JFrame implements RunnerListener {
// Action listener to bring the appropriate sketch in front
item.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
editor.setState(Frame.NORMAL);
@@ -967,6 +972,7 @@ public abstract class Editor extends JFrame implements RunnerListener {
});
sketchMenu.add(item);
menuList.add(item);
Toolkit.setMenuMnemsInside(sketchMenu);
}
}
@@ -1296,7 +1302,7 @@ public abstract class Editor extends JFrame implements RunnerListener {
class CutAction extends AbstractAction {
public CutAction() {
super("Cut");
super(Language.text("menu.edit.cut"));
this.setEnabled(false);
}
@@ -1306,11 +1312,7 @@ public abstract class Editor extends JFrame implements RunnerListener {
}
public void updateCutState() {
if (canCut()) {
cutItems.setEnabled(true);
} else {
cutItems.setEnabled(false);
}
cutItems.setEnabled(canCut());
}
public boolean canCut() {
@@ -1320,7 +1322,7 @@ public abstract class Editor extends JFrame implements RunnerListener {
class CopyAction extends AbstractAction {
public CopyAction() {
super("Copy");
super(Language.text("menu.edit.copy"));
this.setEnabled(false);
}
@@ -1329,11 +1331,7 @@ public abstract class Editor extends JFrame implements RunnerListener {
}
public void updateCopyState() {
if (canCopy()) {
copyItems.setEnabled(true);
} else {
copyItems.setEnabled(false);
}
copyItems.setEnabled(canCopy());
}
public boolean canCopy() {
@@ -2933,11 +2931,9 @@ public abstract class Editor extends JFrame implements RunnerListener {
* Returns the edit popup menu.
*/
class TextAreaPopup extends JPopupMenu {
JMenuItem cutItem;
JMenuItem copyItem;
JMenuItem discourseItem;
JMenuItem referenceItem;
JMenuItem cutItem, copyItem, discourseItem, pasteItem,
selectAllItem, commUncommItem, incIndItem, decIndItem,
referenceItem;
public TextAreaPopup() {
JMenuItem item;
@@ -2992,7 +2988,7 @@ public abstract class Editor extends JFrame implements RunnerListener {
});
this.add(item);
item = new JMenuItem("\u2192 "+Language.text("menu.edit.increase_indent"));
item = new JMenuItem("\u2192 " + Language.text("menu.edit.increase_indent"));
item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
handleIndentOutdent(true);
@@ -3000,7 +2996,7 @@ public abstract class Editor extends JFrame implements RunnerListener {
});
this.add(item);
item = new JMenuItem("\u2190 "+Language.text("menu.edit.decrease_indent"));
item = new JMenuItem("\u2190 " + Language.text("menu.edit.decrease_indent"));
item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
handleIndentOutdent(false);
@@ -3017,6 +3013,8 @@ public abstract class Editor extends JFrame implements RunnerListener {
}
});
this.add(referenceItem);
Toolkit.setMenuMnemonics(this);
}
// if no text is selected, disable copy and cut menu items
+2
View File
@@ -615,6 +615,8 @@ public class EditorHeader extends JComponent {
menu.add(item);
}
}
Toolkit.setMenuMnemonics(menu);
}
+251
View File
@@ -21,9 +21,11 @@
package processing.app;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontFormatException;
import java.awt.FontMetrics;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Graphics2D;
@@ -45,14 +47,18 @@ import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.regex.Pattern;
import javax.swing.ImageIcon;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JComponent;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JRootPane;
import javax.swing.KeyStroke;
@@ -130,6 +136,251 @@ public class Toolkit {
}
/**
* Removes all mnemonics, then sets a mnemonic for each menu and menu item
* recursively by these rules:
* <ol>
* <li> It tries to assign one of <a href="http://techbase.kde.org/Projects/Usability/HIG/Keyboard_Accelerators">
* KDE's defaults</a>.</li>
* <li> Failing that, it loops through the first letter of each word, where a word
* is a block of Unicode "alphabetical" chars, looking for an upper-case ASCII mnemonic
* that is not taken. This is to try to be relevant, by using a letter well-associated
* with the command. (MS guidelines) </li>
* <li> Ditto, but with lowercase. </li>
* <li> Next, it tries the second ASCII character, if its width &gt;= half the width of
* 'A'. </li>
* <li> If the first letters are all taken/non-ASCII, then it loops through the
* ASCII letters in the item, widest to narrowest, seeing if any of them is not taken.
* To improve readability, it discriminates against decenders (qypgj), imagining they
* have 2/3 their actual width. (MS guidelines: avoid decenders). It also discriminates
* against vowels, imagining they have 2/3 their actual width. (MS and Gnome guidelines:
* avoid vowels. </li>
* <li>Failing that, it will loop left-to-right for an available digit. This is a last
* resort because the normal setMnemonic dislikes them.</li>
* <li> If that doesn't work, it doesn't assign a mnemonic. </li>
* </ol>
*
* As a special case, strings starting "sketchbook \u2192 " have that bit ignored
* because otherwise the Recent menu looks awful. However, the name <tt>"sketchbook \u2192
* Sketch"</tt>, for example, will have the 'S' of "Sketch" chosen, but the 's' of 'sketchbook
* will get underlined.
* No letter by an underscore will be assigned.
* Disabled on Mac, per Apple guidelines.
* <tt>menu</tt> may contain nulls.
*
* Author: George Bateman. Initial work Myer Nore.
* @param menu
* A menu, a list of menus or an array of menu items to set mnemonics for.
*/
public static void setMenuMnemonics(JMenuItem... menu) {
if (Base.isMacOS()) return;
if (menu.length == 0) return;
// This list is (mostly) the contents of http://techbase.kde.org/Projects/Usability/HIG/
// Keyboard_Accelerators, made lowercase, with nothing but letters left except
// for ampersands before mnemonics and ".+" for changable text. (They are regexs.)
// Note that every ampersand MUST be followed by a lowercase ASCII letter.
final String[] kdePreDefStrs = { "&file", "&new", "&open", "open&recent", "&save",
"save&as", "saveacop&y", "saveas&template", "savea&ll", "reloa&d", "&print",
"printpre&view", "&import", "e&xport", "&closefile", "clos&eallfiles", "&quit",
"&edit", "&undo", "re&do", "cu&t&", "&copy", "&paste", "&delete", "select&all",
"dese&lect", "&find", "find&next", "findpre&vious", "&replace", "&gotoline",
"&view", "&newview", "close&allviews", "&splitview", "&removeview",
"splitter&orientation", "&horizontal", "&vertical", "view&mode", "&fullscreenmode",
"&zoom", "zoom&in", "zoom&out", "zoomtopage&width", "zoomwhole&page", "zoom&factor",
"&insert", "&format", "&go", "&up", "&back", "&forward", "&home", "&go", "&previouspage",
"&nextpage", "&firstpage", "&lastpage", "read&updocument", "read&downdocument", "&back",
"&forward", "&gotopage", "&bookmarks", "&addbookmark", "bookmark&tabsasfolder",
"&editbookmarks", "&newbookmarksfolder", "&tools", "&settings", "&toolbars",
"configure&shortcuts", "configuretool&bars", "&configure*", "&help", ".+&handbook",
"&whatsthis", "report&bug", "&aboutprocessing", "about&kde", "&beenden" };
Pattern[] kdePreDefPats = new Pattern[kdePreDefStrs.length];
for (int i = 0; i < kdePreDefStrs.length; i++)
kdePreDefPats[i] = Pattern.compile(kdePreDefStrs[i].replace("&",""));
final Pattern nonAAlpha = Pattern.compile("[^A-Za-z]");
FontMetrics fmTmp = null;
for (JMenuItem m : menu) {
if (m != null) fmTmp = m.getFontMetrics(m.getFont());
}
if (fmTmp == null) return; // All null menuitems; comparator would fail.
final FontMetrics fm = fmTmp; // Hack for accessing variable in comparator.
final Comparator<Character> charComparator = new Comparator<Character>() {
char[] baddies = "qypgjaeiouQYPGJAEIOU".toCharArray();
public int compare(Character ch1, Character ch2) {
// Descriminates against decenders for readability, per MS
// Human Interface Guide, and vowels per MS and Gnome.
float w1 = fm.charWidth(ch1), w2 = fm.charWidth(ch2);
for (char bad : baddies) {
if (bad == ch1) w1 *= 0.66f;
if (bad == ch2) w2 *= 0.66f;
}
return (int)Math.signum(w2 - w1);
}
};
// Holds only [0-9a-z], not uppercase.
// Prevents X != x, so "Save" and "Save As" aren't both given 'a'.
final List<Character> taken = new ArrayList<Character>(menu.length);
char firstChar;
char[] cleanChars;
Character[] cleanCharas;
// METHOD 1: attempt to assign KDE defaults.
for (JMenuItem jmi : menu) {
if (jmi == null) continue;
if (jmi.getText() == null) continue;
jmi.setMnemonic(0); // Reset.
for (int i = 0; i < kdePreDefStrs.length; i++) {
// To ASCII lowercase letters.
String lASCIIName = nonAAlpha.matcher(jmi.getText()).replaceAll("").toLowerCase();
if (kdePreDefPats[i].matcher(lASCIIName).matches()) {
char mnem = kdePreDefStrs[i].charAt(1+kdePreDefStrs[i].indexOf("&"));
jmi.setMnemonic(mnem);
taken.add(mnem);
break;
}
}
}
// Where KDE defaults fail, use an algorithm.
algorithmicAssignment:
for (JMenuItem jmi : menu) {
if (jmi == null) continue;
if (jmi.getText() == null) continue;
if (jmi.getMnemonic() != 0) continue; // Already assigned.
// The string can't be made lower-case as that would spoil
// the width comparison.
String cleanString = jmi.getText();
if (cleanString.startsWith("sketchbook \u2192 "))
cleanString = cleanString.substring(13);
if (cleanString.length() == 0) continue;
// First, ban letters by underscores.
final List<Character> banned = new ArrayList<Character>();
for (int i = 0; i < cleanString.length(); i++) {
if (cleanString.charAt(i) == '_') {
if (i > 0)
banned.add(Character.toLowerCase(cleanString.charAt(i-1)));
if (i+1 < cleanString.length())
banned.add(Character.toLowerCase(cleanString.charAt(i+1)));
}
}
// METHOD 2: Uppercase starts of words.
// Splitting into blocks of ASCII letters wouldn't work
// because there could be non-ASCII letters in a word.
for (String wd : cleanString.split("[^\\p{IsAlphabetic}]")) {
if (wd.length() == 0) continue;
firstChar = wd.charAt(0);
if (taken.contains(Character.toLowerCase(firstChar))) continue;
if (banned.contains(Character.toLowerCase(firstChar))) continue;
if ('A' <= firstChar && firstChar <= 'Z') {
jmi.setMnemonic(firstChar);
taken.add((char)(firstChar | 32)); // tolowercase
continue algorithmicAssignment;
}
}
// METHOD 3: Lowercase starts of words.
for (String wd : cleanString.split("[^\\p{IsAlphabetic}]")) {
if (wd.length() == 0) continue;
firstChar = wd.charAt(0);
if (taken.contains(Character.toLowerCase(firstChar))) continue;
if (banned.contains(Character.toLowerCase(firstChar))) continue;
if ('a' <= firstChar && firstChar <= 'z') {
jmi.setMnemonic(firstChar);
taken.add(firstChar); // is lowercase
continue algorithmicAssignment;
}
}
// METHOD 4: Second wide-enough ASCII letter.
cleanString = nonAAlpha.matcher(jmi.getText()).replaceAll("");
if (cleanString.length() >= 2) {
char ascii2nd = cleanString.charAt(1);
if (!taken.contains((char)(ascii2nd|32)) &&
!banned.contains((char)(ascii2nd|32)) &&
fm.charWidth('A') <= 2*fm.charWidth(ascii2nd)) {
jmi.setMnemonic(ascii2nd);
taken.add((char)(ascii2nd|32));
continue algorithmicAssignment;
}
}
// METHOD 5: charComparator over all ASCII letters.
cleanChars = cleanString.toCharArray();
cleanCharas = new Character[cleanChars.length];
for (int i = 0; i < cleanChars.length; i++) {
cleanCharas[i] = new Character(cleanChars[i]);
}
Arrays.sort(cleanCharas, charComparator); // sorts in increasing order
for (char mnem : cleanCharas) {
if (taken.contains(Character.toLowerCase(mnem))) continue;
if (banned.contains(Character.toLowerCase(mnem))) continue;
// NB: setMnemonic(char) doesn't want [^A-Za-z]
jmi.setMnemonic(mnem);
taken.add(Character.toLowerCase(mnem));
continue algorithmicAssignment;
}
// METHOD 6: Digits as last resort.
for (char digit : jmi.getText().replaceAll("[^0-9]", "").toCharArray()) {
if (taken.contains(digit)) continue;
if (banned.contains(digit)) continue;
jmi.setMnemonic(KeyEvent.VK_0 + digit - '0');
taken.add(digit);
continue algorithmicAssignment;
}
}
// Finally, RECURSION.
for (JMenuItem jmi : menu) {
if (jmi instanceof JMenu) setMenuMnemsInside((JMenu) jmi);
}
}
/**
* As setMenuMnemonics(JMenuItem...).
*/
public static void setMenuMnemonics(JMenuBar menubar) {
JMenuItem[] items = new JMenuItem[menubar.getMenuCount()];
for (int i = 0; i < items.length; i++) {
items[i] = menubar.getMenu(i);
}
setMenuMnemonics(items);
}
/**
* As setMenuMnemonics(JMenuItem...).
*/
public static void setMenuMnemonics(JPopupMenu menu) {
ArrayList<JMenuItem> items = new ArrayList<JMenuItem>();
for (Component c : menu.getComponents()) {
if (c instanceof JMenuItem) items.add((JMenuItem)c);
}
setMenuMnemonics(items.toArray(new JMenuItem[items.size()]));
}
/**
* Calls setMenuMnemonics(JMenuItem...) on the sub-elements only.
*/
public static void setMenuMnemsInside(JMenu menu) {
JMenuItem[] items = new JMenuItem[menu.getItemCount()];
for (int i = 0; i < items.length; i++) {
items[i] = menu.getItem(i);
}
setMenuMnemonics(items);
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
@@ -250,6 +250,8 @@ public class DebugEditor extends JavaEditor implements ActionListener {
// }
// });
Toolkit.setMenuMnemonics(ta.getRightClickPopup());
// load settings from theme.txt
ExperimentalMode theme = dmode;
breakpointColor = theme.getThemeColor("breakpoint.bgcolor", breakpointColor);
@@ -377,26 +379,27 @@ public class DebugEditor extends JavaEditor implements ActionListener {
* Writes all error messages to a csv file.
* For analytics purposes only.
*/
private void writeErrorsToFile(){
if (errorCheckerService.tempErrorLog.size() == 0)
return;
try {
System.out.println("Writing errors");
StringBuilder sb = new StringBuilder();
private void writeErrorsToFile() {
if (errorCheckerService.tempErrorLog.size() == 0) return;
try {
System.out.println("Writing errors");
StringBuilder sb = new StringBuilder();
sb.append("Sketch: " + getSketch().getFolder() + ", "
+ new java.sql.Timestamp(new java.util.Date().getTime())
+ new java.sql.Timestamp(new java.util.Date().getTime())
+ "\nComma in error msg is substituted with ^ symbol\nFor separating arguments in error args | symbol is used\n");
sb.append("ERROR TYPE, ERROR ARGS, ERROR MSG\n");
for (String errMsg : errorCheckerService.tempErrorLog.keySet()) {
IProblem ip = errorCheckerService.tempErrorLog.get(errMsg);
if(ip != null){
sb.append(ErrorMessageSimplifier.getIDName(ip.getID()));
sb.append(',');
sb.append("{");
for (int i = 0; i < ip.getArguments().length; i++) {
sb.append(ip.getArguments()[i]);
if(i < ip.getArguments().length - 1)
sb.append("| ");
for (String errMsg : errorCheckerService.tempErrorLog.keySet()) {
IProblem ip = errorCheckerService.tempErrorLog.get(errMsg);
if (ip != null) {
sb.append(ErrorMessageSimplifier.getIDName(ip.getID()));
sb.append(',');
sb.append("{");
for (int i = 0; i < ip.getArguments().length; i++) {
sb.append(ip.getArguments()[i]);
if (i < ip.getArguments().length-1)
sb.append("| ");
}
sb.append("}");
sb.append(',');
@@ -1005,6 +1008,7 @@ public class DebugEditor extends JavaEditor implements ActionListener {
return saved;
}
private boolean viewingAutosaveBackup;
/**