/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Copyright (c) 2012-15 The Processing Foundation
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2
as published by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software Foundation,
Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package processing.app.ui;
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;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Window;
import java.awt.datatransfer.Clipboard;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.image.ImageObserver;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
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.Locale;
import java.util.regex.Pattern;
import javax.swing.Action;
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;
import processing.app.Language;
import processing.app.Messages;
import processing.app.Platform;
import processing.app.Preferences;
import processing.app.Util;
/**
* Utility functions for base that require a java.awt.Toolkit object. These
* are broken out from Base as we start moving toward the possibility of the
* code running in headless mode.
* @author fry
*/
public class Toolkit {
/*
static public final String PROMPT_YES = Language.text("prompt.yes");
static public final String PROMPT_NO = Language.text("prompt.no");
static public final String PROMPT_CANCEL = Language.text("prompt.cancel");
static public final String PROMPT_OK = Language.text("prompt.ok");
static public final String PROMPT_BROWSE = Language.text("prompt.browse");
*/
static final java.awt.Toolkit awtToolkit =
java.awt.Toolkit.getDefaultToolkit();
/** Command on Mac OS X, Ctrl on Windows and Linux */
static final int SHORTCUT_KEY_MASK =
awtToolkit.getMenuShortcutKeyMask();
/** Command-W on Mac OS X, Ctrl-W on Windows and Linux */
public static final KeyStroke WINDOW_CLOSE_KEYSTROKE =
KeyStroke.getKeyStroke('W', SHORTCUT_KEY_MASK);
/** Command-Option on Mac OS X, Ctrl-Alt on Windows and Linux */
static final int SHORTCUT_ALT_KEY_MASK =
ActionEvent.ALT_MASK | SHORTCUT_KEY_MASK;
/** Command-Shift on Mac OS X, Ctrl-Shift on Windows and Linux */
static final int SHORTCUT_SHIFT_KEY_MASK =
ActionEvent.SHIFT_MASK | SHORTCUT_KEY_MASK;
/**
* Standardized width for buttons. Mac OS X 10.3 wants 70 as its default,
* Windows XP needs 66, and my Ubuntu machine needs 80+, so 80 seems proper.
* This is now stored in the languages file since this may need to be larger
* for languages that are consistently wider than English.
*/
static public int getButtonWidth() {
// Made into a method so that calling Toolkit methods doesn't require
// the languages to be loaded, and with that, Base initialized completely
return Integer.parseInt(Language.text("preferences.button.width"));
}
/**
* A software engineer, somewhere, needs to have their abstraction
* taken away. Who crafts the sort of API that would require a
* five-line helper function just to set the shortcut key for a
* menu item?
*/
static public JMenuItem newJMenuItem(String title, int what) {
JMenuItem menuItem = new JMenuItem(title);
int modifiers = awtToolkit.getMenuShortcutKeyMask();
menuItem.setAccelerator(KeyStroke.getKeyStroke(what, modifiers));
return menuItem;
}
/**
* @param action: use an Action, which sets the title, reaction
* and enabled-ness all by itself.
*/
static public JMenuItem newJMenuItem(Action action, int what) {
JMenuItem menuItem = new JMenuItem(action);
int modifiers = awtToolkit.getMenuShortcutKeyMask();
menuItem.setAccelerator(KeyStroke.getKeyStroke(what, modifiers));
return menuItem;
}
/**
* Like newJMenuItem() but adds shift as a modifier for the shortcut.
*/
static public JMenuItem newJMenuItemShift(String title, int what) {
JMenuItem menuItem = new JMenuItem(title);
int modifiers = awtToolkit.getMenuShortcutKeyMask();
modifiers |= ActionEvent.SHIFT_MASK;
menuItem.setAccelerator(KeyStroke.getKeyStroke(what, modifiers));
return menuItem;
}
/**
* Like newJMenuItem() but adds shift as a modifier for the shortcut.
*/
static public JMenuItem newJMenuItemShift(Action action, int what) {
JMenuItem menuItem = new JMenuItem(action);
int modifiers = awtToolkit.getMenuShortcutKeyMask();
modifiers |= ActionEvent.SHIFT_MASK;
menuItem.setAccelerator(KeyStroke.getKeyStroke(what, modifiers));
return menuItem;
}
/**
* Same as newJMenuItem(), but adds the ALT (on Linux and Windows)
* or OPTION (on Mac OS X) key as a modifier. This function should almost
* never be used, because it's bad for non-US keyboards that use ALT in
* strange and wondrous ways.
*/
static public JMenuItem newJMenuItemAlt(String title, int what) {
JMenuItem menuItem = new JMenuItem(title);
menuItem.setAccelerator(KeyStroke.getKeyStroke(what, SHORTCUT_ALT_KEY_MASK));
return menuItem;
}
static public JCheckBoxMenuItem newJCheckBoxMenuItem(String title, int what) {
JCheckBoxMenuItem menuItem = new JCheckBoxMenuItem(title);
int modifiers = awtToolkit.getMenuShortcutKeyMask();
menuItem.setAccelerator(KeyStroke.getKeyStroke(what, modifiers));
return menuItem;
}
static public void addDisabledItem(JMenu menu, String title) {
JMenuItem item = new JMenuItem(title);
item.setEnabled(false);
menu.add(item);
}
/**
* Removes all mnemonics, then sets a mnemonic for each menu and menu item
* recursively by these rules:
*
* - It tries to assign one of
* KDE's defaults.
* - 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)
* - Ditto, but with lowercase.
* - Next, it tries the second ASCII character, if its width >= half the width of
* 'A'.
* - 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.)
* - Failing that, it will loop left-to-right for an available digit. This is a last
* resort because the normal setMnemonic dislikes them.
* - If that doesn't work, it doesn't assign a mnemonic.
*
*
* As a special case, strings starting "sketchbook \u2192 " have that bit ignored
* because otherwise the Recent menu looks awful. However, the name "sketchbook \u2192
* Sketch", 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.
* menu 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.
*/
static public void setMenuMnemonics(JMenuItem... menu) {
if (Platform.isMacOS()) return;
if (menu.length == 0) return;
// The English is http://techbase.kde.org/Projects/Usability/HIG/Keyboard_Accelerators,
// made lowercase.
// Nothing but [a-z] except for '&' before mnemonics and regexes for changable text.
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", "©",
"&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", "&suchen", // de
"&preferncias", "&sair", // PreferĂȘncias; pt
"&rechercher" }; // fr
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());
break;
}
}
if (fmTmp == null) return; // All null menuitems; would fail.
final FontMetrics fm = fmTmp; // Hack for accessing variable in comparator.
final Comparator charComparator = new Comparator() {
char[] baddies = "qypgjaeiouQAEIOU".toCharArray();
public int compare(Character ch1, Character ch2) {
// Discriminates against descenders 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 taken = new ArrayList(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 all mnemonics.
String asciiName = nonAAlpha.matcher(jmi.getText()).replaceAll("");
String lAsciiName = asciiName.toLowerCase();
for (int i = 0; i < kdePreDefStrs.length; i++) {
if (kdePreDefPats[i].matcher(lAsciiName).matches()) {
char mnem = asciiName.charAt(kdePreDefStrs[i].indexOf("&"));
jmi.setMnemonic(mnem);
jmi.setDisplayedMnemonicIndex(jmi.getText().indexOf(mnem));
taken.add((char)(mnem | 32)); // to lowercase
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 banned = new ArrayList();
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'