Java tutorial
/* Part of the Processing project - http://processing.org Copyright (c) 2004-06 Ben Fry and Casey Reas Copyright (c) 2001-04 Massachusetts Institute of Technology This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. 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; import static processing.app.I18n.format; import static processing.app.I18n.tr; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.MediaTracker; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.SystemColor; import java.awt.Toolkit; import java.awt.font.TextAttribute; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Hashtable; import java.util.Map; import java.util.Properties; import java.util.TreeMap; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import javax.swing.text.StyleContext; import org.apache.batik.transcoder.Transcoder; import org.apache.batik.transcoder.TranscoderException; import org.apache.batik.transcoder.TranscoderInput; import org.apache.batik.transcoder.TranscoderOutput; import org.apache.batik.transcoder.image.PNGTranscoder; import org.apache.commons.compress.utils.IOUtils; import org.apache.commons.lang3.StringUtils; import processing.app.helpers.OSUtils; import processing.app.helpers.PreferencesHelper; import processing.app.helpers.PreferencesMap; /** * Storage class for theme settings. This was separated from the Preferences * class for 1.0 so that the coloring wouldn't conflict with previous releases * and to make way for future ability to customize. */ public class Theme { static final String THEME_DIR = "theme/"; static final String THEME_FILE_NAME = "theme.txt"; static final String NAMESPACE_APP = "app:"; static final String NAMESPACE_USER = "user:"; /** * A theme resource, this is returned instead of {@link File} so that we can * support zip-packaged resources as well as files in the file system */ public static class Resource { // Priority levels used to determine whether one resource should override // another static public final int PRIORITY_DEFAULT = 0; static public final int PRIORITY_USER_ZIP = 1; static public final int PRIORITY_USER_FILE = 2; /** * Priority of this resource. */ private final int priority; /** * Resource name (original name of requested resource, relative path only). */ private final String name; /** * File if this resource represents a file, can be null. */ private final File file; /** * Zip theme if the resource is contained within a zipped theme */ private final ZippedTheme theme; /** * Zip entry if this resource represents a zip entry, can be null. */ private final ZipEntry zipEntry; /** * URL of this resource regardless of type, theoretically shouldn't ever be * null though it might be if a particular resource path can't be * successfully transformed into a URL (eg. {@link Theme#getUrl} traps a * <tt>MalformedURLException</tt>). */ private final URL url; /** * If this resource supercedes a resource with a lower priority, this field * stores a reference to the superceded resource. This allows consumers to * traverse the resource hierarchy if required. */ private Resource parent; /** * ctor for file resources */ Resource(int priority, String name, URL url, File file) { this(priority, name, url, file, null, null); } /** * ctor for zip resources */ Resource(int priority, String name, URL url, ZippedTheme theme, ZipEntry entry) { this(priority, name, url, null, theme, entry); } private Resource(int priority, String name, URL url, File file, ZippedTheme theme, ZipEntry zipEntry) { this.priority = priority; this.name = name; this.file = file; this.theme = theme; this.zipEntry = zipEntry; this.url = url; } public Resource getParent() { return this.parent; } public String getName() { return this.name; } public URL getUrl() { return this.url; } public int getPriority() { return this.priority; } public boolean isUserDefined() { return this.priority > PRIORITY_DEFAULT; } public boolean exists() { return this.zipEntry != null || this.file == null || this.file.exists(); } public InputStream getInputStream() throws IOException { if (this.file != null) { return new FileInputStream(this.file); } if (this.zipEntry != null) { return this.theme.getZip().getInputStream(this.zipEntry); } if (this.url != null) { return this.url.openStream(); } throw new FileNotFoundException(this.name); } public String toString() { return this.name; } Resource withParent(Resource parent) { this.parent = parent; return this; } } /** * Struct which keeps information about a discovered .zip theme file */ public static class ZippedTheme { /** * Configuration key, this key consists of a "namespace" which determines * the root folder the theme was found in without actually storing the path * itself, followed by the file name. */ private final String key; /** * File containing the theme */ private final File file; /** * Zip file handle for retrieving entries */ private final ZipFile zip; /** * Display name, defaulted to filename but can be read from metadata */ private final String name; /** * Version number, plain text string read from metadata */ private final String version; private ZippedTheme(String namespace, File file, ZipFile zip, String name, String version) { this.key = namespace + file.getName(); this.file = file; this.zip = zip; this.name = name; this.version = version; } public String getKey() { return this.key; } public File getFile() { return this.file; } public ZipFile getZip() { return this.zip; } public String getName() { return this.name; } public String getVersion() { return this.version; } public String toString() { String description = String.format("%s %s (%s)", this.getName(), this.getVersion(), this.file.getName()); return StringUtils.abbreviate(description, 40); } /** * Attempts to parse the supplied zip file as a theme file. This is largely * determined by the file being readable and containing a theme.txt entry. * Returns null if the file is unreadable or doesn't contain theme.txt */ static ZippedTheme load(String namespace, File file) { ZipFile zip = null; try { zip = new ZipFile(file); ZipEntry themeTxtEntry = zip.getEntry(THEME_FILE_NAME); if (themeTxtEntry != null) { String name = file.getName().substring(0, file.getName().length() - 4); String version = ""; ZipEntry themePropsEntry = zip.getEntry("theme.properties"); if (themePropsEntry != null) { Properties themeProperties = new Properties(); themeProperties.load(zip.getInputStream(themePropsEntry)); name = themeProperties.getProperty("name", name); version = themeProperties.getProperty("version", version); } return new ZippedTheme(namespace, file, zip, name, version); } } catch (Exception ex) { System.err.println( format(tr("Error loading theme {0}: {1}"), file.getAbsolutePath(), ex.getMessage())); IOUtils.closeQuietly(zip); } return null; } } /** * Copy of the defaults in case the user mangles a preference. */ static PreferencesMap defaults; /** * Table of attributes/values for the theme. */ static PreferencesMap table = new PreferencesMap(); /** * Available zipped themes */ static private final Map<String, ZippedTheme> availableThemes = new TreeMap<>(); /** * Zip file containing user-defined theme elements */ static private ZippedTheme zipTheme; static protected void init() { zipTheme = openZipTheme(); try { loadFromResource(table, THEME_DIR + THEME_FILE_NAME); } catch (Exception te) { Base.showError(null, tr("Could not read color theme settings.\n" + "You'll need to reinstall Arduino."), te); } // other things that have to be set explicitly for the defaults setColor("run.window.bgcolor", SystemColor.control); // clone the hash table defaults = new PreferencesMap(table); } static private ZippedTheme openZipTheme() { refreshAvailableThemes(); String selectedTheme = PreferencesData.get("theme.file", ""); synchronized (availableThemes) { return availableThemes.get(selectedTheme); } } static private void refreshAvailableThemes() { Map<String, ZippedTheme> discoveredThemes = new TreeMap<>(); refreshAvailableThemes(discoveredThemes, NAMESPACE_APP, new File(BaseNoGui.getContentFile("lib"), THEME_DIR)); refreshAvailableThemes(discoveredThemes, NAMESPACE_USER, new File(BaseNoGui.getSketchbookFolder(), THEME_DIR)); synchronized (availableThemes) { availableThemes.clear(); availableThemes.putAll(discoveredThemes); } } static private void refreshAvailableThemes(Map<String, ZippedTheme> discoveredThemes, String namespace, File folder) { if (!folder.isDirectory()) { return; } for (File zipFile : folder.listFiles((dir, name) -> name.endsWith(".zip"))) { ZippedTheme theme = ZippedTheme.load(namespace, zipFile); if (theme != null) { discoveredThemes.put(theme.getKey(), theme); } } } public static Collection<ZippedTheme> getAvailablethemes() { refreshAvailableThemes(); return Collections.unmodifiableCollection(availableThemes.values()); } static public String get(String attribute) { return table.get(attribute); } static public String getDefault(String attribute) { return defaults.get(attribute); } static public void set(String attribute, String value) { table.put(attribute, value); } static public boolean getBoolean(String attribute) { return table.getBoolean(attribute); } static public void setBoolean(String attribute, boolean value) { table.putBoolean(attribute, value); } static public int getInteger(String attribute) { return Integer.parseInt(get(attribute)); } static public void setInteger(String key, int value) { set(key, String.valueOf(value)); } static public int getScale() { try { int scale = PreferencesData.getInteger("gui.scale", -1); if (scale != -1) return scale; } catch (NumberFormatException ignore) { } return BaseNoGui.getPlatform().getSystemDPI() * 100 / 96; } static public int scale(int size) { return size * getScale() / 100; } static public Dimension scale(Dimension dim) { return new Dimension(scale(dim.width), scale(dim.height)); } static public Font scale(Font font) { float size = scale(font.getSize()); // size must be float to call the correct Font.deriveFont(float) // method that is different from Font.deriveFont(int)! Font scaled = font.deriveFont(size); return scaled; } static public Rectangle scale(Rectangle rect) { Rectangle res = new Rectangle(rect); res.x = scale(res.x); res.y = scale(res.y); res.width = scale(res.width); res.height = scale(res.height); return res; } static public Color getColorCycleColor(String name, int i) { int cycleSize = getInteger(name + ".size"); name = String.format("%s.%02d", name, i % cycleSize); return PreferencesHelper.parseColor(get(name)); } static public void setColorCycleColor(String name, int i, Color color) { name = String.format("%s.%02d", name, i); PreferencesHelper.putColor(table, name, color); int cycleSize = getInteger(name + ".size"); setInteger(name + ".size", (i + 1) > cycleSize ? (i + 1) : cycleSize); } static public Color getColor(String name) { return PreferencesHelper.parseColor(get(name)); } static public void setColor(String attr, Color color) { PreferencesHelper.putColor(table, attr, color); } static public Font getFont(String attr) { Font font = PreferencesHelper.getFont(table, attr); if (font == null) { String value = getDefault(attr); set(attr, value); font = PreferencesHelper.getFont(table, attr); if (font == null) { return null; } } return font.deriveFont((float) scale(font.getSize())); } /** * Returns the default font for text areas. * * @return The default font. */ public static final Font getDefaultFont() { // Use StyleContext to get a composite font for better Asian language // support; see Sun bug S282887. StyleContext sc = StyleContext.getDefaultStyleContext(); Font font = null; if (OSUtils.isMacOS()) { // Snow Leopard (1.6) uses Menlo as default monospaced font, // pre-Snow Leopard used Monaco. font = sc.getFont("Menlo", Font.PLAIN, 12); if (!"Menlo".equals(font.getFamily())) { font = sc.getFont("Monaco", Font.PLAIN, 12); if (!"Monaco".equals(font.getFamily())) { // Shouldn't happen font = sc.getFont("Monospaced", Font.PLAIN, 13); } } } else { // Consolas added in Vista, used by VS2010+. font = sc.getFont("Consolas", Font.PLAIN, 13); if (!"Consolas".equals(font.getFamily())) { font = sc.getFont("Monospaced", Font.PLAIN, 13); } } // System.out.println(font.getFamily() + ", " + font.getName()); return font; } public static Map<String, Object> getStyledFont(String what, Font font) { String split[] = get("editor." + what + ".style").split(","); Color color = PreferencesHelper.parseColor(split[0]); String style = split[1]; boolean bold = style.contains("bold"); boolean italic = style.contains("italic"); boolean underlined = style.contains("underlined"); Font styledFont = new Font(font.getFamily(), (bold ? Font.BOLD : 0) | (italic ? Font.ITALIC : 0), font.getSize()); if (underlined) { Map<TextAttribute, Object> attr = new Hashtable<>(); attr.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON); styledFont = styledFont.deriveFont(attr); } Map<String, Object> result = new HashMap<>(); result.put("color", color); result.put("font", styledFont); return result; } /** * Return an Image object from inside the Processing lib folder. */ static public Image getLibImage(String filename, Component who, int width, int height) { Image image = null; // Use vector image when available Resource vectorFile = getThemeResource(filename + ".svg"); if (vectorFile.exists()) { try { image = imageFromSVG(vectorFile.getUrl(), width, height); } catch (Exception e) { System.err.println("Failed to load " + vectorFile + ": " + e.getMessage()); } } Resource bitmapFile = getThemeResource(filename + ".png"); // Otherwise fall-back to PNG bitmaps, allowing user-defined bitmaps to // override built-in svgs if (image == null || bitmapFile.getPriority() > vectorFile.getPriority()) { Resource bitmap2xFile = getThemeResource(filename + "@2x.png"); Resource imageFile; if (((getScale() > 125 && bitmap2xFile.exists()) || !bitmapFile.exists()) && (bitmapFile.isUserDefined() && bitmap2xFile.isUserDefined())) { imageFile = bitmap2xFile; } else { imageFile = bitmapFile; } Toolkit tk = Toolkit.getDefaultToolkit(); image = tk.getImage(imageFile.getUrl()); } MediaTracker tracker = new MediaTracker(who); try { tracker.addImage(image, 0); tracker.waitForAll(); } catch (InterruptedException e) { } if (image.getWidth(null) != width || image.getHeight(null) != height) { image = image.getScaledInstance(width, height, Image.SCALE_SMOOTH); try { tracker.addImage(image, 1); tracker.waitForAll(); } catch (InterruptedException e) { } } return image; } /** * Get an image associated with the current color theme. */ static public Image getThemeImage(String name, Component who, int width, int height) { return getLibImage(THEME_DIR + name, who, width, height); } private static Image imageFromSVG(URL url, int width, int height) throws TranscoderException { Transcoder t = new PNGTranscoder(); t.addTranscodingHint(PNGTranscoder.KEY_WIDTH, new Float(width)); t.addTranscodingHint(PNGTranscoder.KEY_HEIGHT, new Float(height)); TranscoderInput input = new TranscoderInput(url.toString()); ByteArrayOutputStream ostream = new ByteArrayOutputStream(); TranscoderOutput output = new TranscoderOutput(ostream); t.transcode(input, output); byte[] imgData = ostream.toByteArray(); return Toolkit.getDefaultToolkit().createImage(imgData); } static public Graphics2D setupGraphics2D(Graphics graphics) { Graphics2D g = (Graphics2D) graphics; if (PreferencesData.getBoolean("editor.antialias")) { g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); } return g; } /** * Loads the supplied {@link PreferencesMap} from the specified resource, * recursively loading parent resources such that entries are loaded in order * of priority (lowest first). * * @param map preference map to populate * @param name name of resource to load */ static public PreferencesMap loadFromResource(PreferencesMap map, String name) throws IOException { return loadFromResource(map, getThemeResource(name)); } static private PreferencesMap loadFromResource(PreferencesMap map, Resource resource) throws IOException { if (resource != null) { loadFromResource(map, resource.getParent()); map.load(resource.getInputStream()); } return map; } /** * @param name * @return */ static public Resource getThemeResource(String name) { File defaultfile = getDefaultFile(name); Resource resource = new Resource(Resource.PRIORITY_DEFAULT, name, getUrl(defaultfile), defaultfile); ZipEntry themeZipEntry = getThemeZipEntry(name); if (themeZipEntry != null) { resource = new Resource(Resource.PRIORITY_USER_ZIP, name, getUrl(themeZipEntry), zipTheme, themeZipEntry).withParent(resource); } File themeFile = getThemeFile(name); if (themeFile != null) { resource = new Resource(Resource.PRIORITY_USER_FILE, name, getUrl(themeFile), themeFile) .withParent(resource); } return resource; } static private File getThemeFile(String name) { File sketchBookThemeFolder = new File(BaseNoGui.getSketchbookFolder(), THEME_DIR); File themeFile = new File(sketchBookThemeFolder, name); if (themeFile.exists()) { return themeFile; } if (name.startsWith(THEME_DIR)) { themeFile = new File(sketchBookThemeFolder, name.substring(THEME_DIR.length())); if (themeFile.exists()) { return themeFile; } } return null; } static private ZipEntry getThemeZipEntry(String name) { if (zipTheme == null) { return null; } if (name.startsWith(THEME_DIR)) { name = name.substring(THEME_DIR.length()); } return zipTheme.getZip().getEntry(name); } static private File getDefaultFile(String name) { return new File(BaseNoGui.getContentFile("lib"), name); } static URL getUrl(File file) { try { return file.toURI().toURL(); } catch (MalformedURLException ex) { return null; } } static URL getUrl(ZipEntry entry) { try { // Adjust file name for URL format on Windows String zipFile = zipTheme.getZip().getName().replace('\\', '/'); if (!zipFile.startsWith("/")) { zipFile = "/" + zipFile; } // Construct a URL which points to the internal resource URI uri = new URI("jar", "file:" + zipFile + "!/" + entry.getName(), null); return uri.toURL(); } catch (MalformedURLException | URISyntaxException ex) { return null; } } }