/* * Copyright 2008 Ayman Al-Sairafi ayman.alsairafi@gmail.com * * 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 jsyntaxpane; import java.awt.Color; import java.awt.Container; import java.util.logging.Level; import java.awt.Font; import java.awt.GraphicsEnvironment; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.WeakHashMap; import java.util.logging.Logger; import java.util.regex.Pattern; import javax.swing.Action; import javax.swing.ImageIcon; import javax.swing.JEditorPane; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.KeyStroke; import javax.swing.text.DefaultEditorKit; import javax.swing.text.Document; import javax.swing.text.EditorKit; import javax.swing.text.Element; import javax.swing.text.JTextComponent; import javax.swing.text.Keymap; import javax.swing.text.View; import javax.swing.text.ViewFactory; import jsyntaxpane.actions.DefaultSyntaxAction; import jsyntaxpane.actions.SyntaxAction; import jsyntaxpane.components.SyntaxComponent; import jsyntaxpane.util.Configuration; import jsyntaxpane.util.JarServiceProvider; /** * The DefaultSyntaxKit is the main entry to SyntaxPane. To use the package, just * set the EditorKit of the EditorPane to a new instance of this class. * * You need to pass a proper lexer to the class. * * @author ayman */ public class DefaultSyntaxKit extends DefaultEditorKit implements ViewFactory { public static final String CONFIG_CARETCOLOR = "CaretColor"; public static final String CONFIG_SELECTION = "SelectionColor"; public static final String CONFIG_COMPONENTS = "Components"; public static final String CONFIG_MENU = "PopupMenu"; public static final String CONFIG_MENU_ICONS = "PopupMenuIcons"; public static final String PROPERTY_KEYMAP_JSYNTAXPANE = "jsyntaxpane"; public static final Pattern EQUALS_REGEX = Pattern.compile("\\s*=\\s*"); private static final Pattern ACTION_KEY_PATTERN = Pattern.compile("Action\\.(\\w+)"); private static Font DEFAULT_FONT; private static Set<String> CONTENT_TYPES = new HashSet<String>(); private static Boolean initialized = false; private Lexer lexer; private static final Logger LOG = Logger.getLogger(DefaultSyntaxKit.class.getName()); private Map<JEditorPane, List<SyntaxComponent>> editorComponents = new WeakHashMap<JEditorPane, List<SyntaxComponent>>(); private Map<JEditorPane, JPopupMenu> popupMenu = new WeakHashMap<JEditorPane, JPopupMenu>(); /** * Main Configuration of JSyntaxPane EditorKits */ private static Map<Class <? extends DefaultSyntaxKit>, Configuration> CONFIGS; static { // we only need to initialize once. if (!initialized) { initKit(); } } /** * Create a new Kit for the given language * @param lexer */ public DefaultSyntaxKit(Lexer lexer) { super(); this.lexer = lexer; } /** * Adds UI components to the pane * @param editorPane */ public void addComponents(JEditorPane editorPane) { // install the components to the editor: String[] components = getConfig().getPropertyList(CONFIG_COMPONENTS); for (String c : components) { installComponent(editorPane, c); } } public void installComponent(JEditorPane pane, String classname) { try { @SuppressWarnings(value = "unchecked") Class compClass = Class.forName(classname); SyntaxComponent comp = (SyntaxComponent) compClass.newInstance(); comp.config(getConfig()); comp.install(pane); if (editorComponents.get(pane) == null) { editorComponents.put(pane, new ArrayList<SyntaxComponent>()); } editorComponents.get(pane).add(comp); } catch (InstantiationException ex) { LOG.log(Level.SEVERE, null, ex); } catch (IllegalAccessException ex) { LOG.log(Level.SEVERE, null, ex); } catch (ClassNotFoundException ex) { LOG.log(Level.SEVERE, null, ex); } } public void deinstallComponent(JEditorPane pane, String classname) { for (SyntaxComponent c : editorComponents.get(pane)) { if (c.getClass().getName().equals(classname)) { c.deinstall(pane); editorComponents.remove(c); break; } } } public boolean isComponentInstalled(JEditorPane pane, String classname) { for (SyntaxComponent c : editorComponents.get(pane)) { if (c.getClass().getName().equals(classname)) { return true; } } return false; } public boolean toggleComponent(JEditorPane pane, String classname) { for (SyntaxComponent c : editorComponents.get(pane)) { if (c.getClass().getName().equals(classname)) { c.deinstall(pane); editorComponents.get(pane).remove(c); return false; } } installComponent(pane, classname); return true; } /** * Adds a popup menu to the editorPane if needed. * * @param editorPane */ public void addPopupMenu(JEditorPane editorPane) { String[] menuItems = getConfig().getPropertyList(CONFIG_MENU); if (menuItems == null || menuItems.length == 0) { return; } popupMenu.put(editorPane, new JPopupMenu()); String menuIconsLocation = getConfig().getString( CONFIG_MENU_ICONS, "/META-INF/images/"); JMenu stack = null; for (String menu : menuItems) { String[] menudata = EQUALS_REGEX.split(menu); // String menuText = menudata[0]; // create the Popup menu if (menuText.equals("-")) { popupMenu.get(editorPane).addSeparator(); } else if (menuText.startsWith(">")) { JMenu sub = new JMenu(menuText.substring(1)); popupMenu.get(editorPane).add(sub); stack = sub; } else if (menuText.startsWith("<")) { Container parent = stack.getParent(); if (parent instanceof JMenu) { JMenu jMenu = (JMenu) parent; stack = jMenu; } else { stack = null; } } else { JMenuItem menuItem; menuItem = new JMenuItem(); if (menudata.length < 2) { throw new IllegalArgumentException("Invalid menu item data: " + menu); } String menuAction = menudata[1].trim(); Action action = editorPane.getActionMap().get(menuAction); if (action == null) { throw new IllegalArgumentException("Invalid action for menu item: " + menu); } menuItem.setAction(action); menuItem.setText(menuText); if (menudata.length > 2) { URL loc = this.getClass().getResource(menuIconsLocation + menudata[2]); if (loc == null) { Logger.getLogger(this.getClass().getName()).log(Level.WARNING, "Unable to get icon at: " + menuIconsLocation + menudata[2]); } else { ImageIcon i = new ImageIcon(loc); menuItem.setIcon(i); } } if (stack == null) { popupMenu.get(editorPane).add(menuItem); } else { stack.add(menuItem); } } } editorPane.setComponentPopupMenu(popupMenu.get(editorPane)); } @Override public ViewFactory getViewFactory() { return this; } @Override public View create(Element element) { return new SyntaxView(element, getConfig()); } /** * Install the View on the given EditorPane. This is called by Swing and * can be used to do anything you need on the JEditorPane control. Here * I set some default Actions. * * @param editorPane */ @Override public void install(JEditorPane editorPane) { super.install(editorPane); // get our font String fontName = getProperty("DefaultFont"); Font font = DEFAULT_FONT; if(fontName != null) { font = Font.decode(fontName); } editorPane.setFont(font); Configuration conf = getConfig(); Color caretColor = conf.getColor(CONFIG_CARETCOLOR, Color.BLACK); editorPane.setCaretColor(caretColor); Color selectionColor = getConfig().getColor(CONFIG_SELECTION, new Color(0x99ccff)); editorPane.setSelectionColor(selectionColor); addActions(editorPane); addComponents(editorPane); addPopupMenu(editorPane); } @Override public void deinstall(JEditorPane editorPane) { List<SyntaxComponent> l = editorComponents.get(editorPane); for (SyntaxComponent c : editorComponents.get(editorPane)) { c.deinstall(editorPane); } editorComponents.clear(); // All the Actions were added directly to the editorPane, so we can remove // all of them with one call. The Parents (defaults) will be intact editorPane.getActionMap().clear(); } /** * Add keyboard actions to this control using the Configuration we have * @param editorPane */ public void addActions(JEditorPane editorPane) { // look at all keys that either start with prefix.Action, or // that start with Action. Keymap km_parent = JTextComponent.getKeymap(JTextComponent.DEFAULT_KEYMAP); Keymap km_new = JTextComponent.addKeymap(PROPERTY_KEYMAP_JSYNTAXPANE, km_parent); for (Configuration.StringKeyMatcher m : getConfig().getKeys(ACTION_KEY_PATTERN)) { String[] values = Configuration.COMMA_SEPARATOR.split( m.value); String actionClass = values[0]; String actionName = m.group1; SyntaxAction action = createAction(actionClass); // The configuration keys will need to be prefixed by Action // to make it more readable in the COnfiguration files. action.config(getConfig(), DefaultSyntaxAction.ACTION_PREFIX + actionName); // Add the action to the component also editorPane.getActionMap().put(actionName, action); // Now bind all the keys to the Action we have: for (int i = 1; i < values.length; i++) { String keyStrokeString = values[i]; KeyStroke ks = KeyStroke.getKeyStroke(keyStrokeString); // KeyEvent.VK_QUOTEDBL if (ks == null) { throw new IllegalArgumentException("Invalid KeyStroke: " + keyStrokeString); } km_new.addActionForKeyStroke(ks, action); } } editorPane.setKeymap(km_new); } private SyntaxAction createAction(String actionClassName) { SyntaxAction action = null; try { Class clazz = Class.forName(actionClassName); action = (SyntaxAction) clazz.newInstance(); } catch (InstantiationException ex) { throw new IllegalArgumentException("Cannot create action class: " + actionClassName + ". Ensure it has default constructor.", ex); } catch (IllegalAccessException ex) { throw new IllegalArgumentException("Cannot create action class: " + actionClassName, ex); } catch (ClassNotFoundException ex) { throw new IllegalArgumentException("Cannot create action class: " + actionClassName, ex); } catch (ClassCastException ex) { throw new IllegalArgumentException("Cannot create action class: " + actionClassName, ex); } return action; } /** * This is called by Swing to create a Document for the JEditorPane document * This may be called before you actually get a reference to the control. * We use it here to create a proper lexer and pass it to the * SyntaxDcument we return. * @return */ @Override public Document createDefaultDocument() { return new SyntaxDocument(lexer); } /** * This is called to initialize the list of <code>Lexer</code>s we have. * You can call this at initialization, or it will be called when needed. * The method will also add the appropriate EditorKit classes to the * corresponding ContentType of the JEditorPane. After this is called, * you can simply call the editor.setCOntentType("text/java") on the * control and you will be done. */ public synchronized static void initKit() { // attempt to find a suitable default font String defaultFont = getConfig(DefaultSyntaxKit.class).getString("DefaultFont"); if (defaultFont != null) { DEFAULT_FONT = Font.decode(defaultFont); } else { GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); String[] fonts = ge.getAvailableFontFamilyNames(); Arrays.sort(fonts); if (Arrays.binarySearch(fonts, "Courier New") >= 0) { DEFAULT_FONT = new Font("Courier New", Font.PLAIN, 12); } else if (Arrays.binarySearch(fonts, "Courier") >= 0) { DEFAULT_FONT = new Font("Courier", Font.PLAIN, 12); } else if (Arrays.binarySearch(fonts, "Monospaced") >= 0) { DEFAULT_FONT = new Font("Monospaced", Font.PLAIN, 13); } } // read the Default Kits and their associated types Properties kitsForTypes = JarServiceProvider.readProperties("jsyntaxpane/kitsfortypes"); for (Map.Entry e : kitsForTypes.entrySet()) { String type = e.getKey().toString(); String classname = e.getValue().toString(); registerContentType(type, classname); } initialized = true; } /** * Register the given content type to use the given class name as its kit * When this is called, an entry is added into the private HashMap of the * registered editors kits. This is needed so that the SyntaxPane library * has it's own registration of all the EditorKits * @param type * @param classname */ public static void registerContentType(String type, String classname) { try { // ensure the class is available and that it does supply a no args // constructor. This saves debugging later if the classname is incorrect // or does not behave correctly: Class c = Class.forName(classname); // attempt to create the class, if we cannot with an empty argument // then the class is invalid Object kit = c.newInstance(); if (!(kit instanceof EditorKit)) { throw new IllegalArgumentException("Cannot register class: " + classname + ". It does not extend EditorKit"); } JEditorPane.registerEditorKitForContentType(type, classname); CONTENT_TYPES.add(type); } catch (InstantiationException ex) { throw new IllegalArgumentException("Cannot register class: " + classname + ". Ensure it has Default Constructor.", ex); } catch (IllegalAccessException ex) { throw new IllegalArgumentException("Cannot register class: " + classname, ex); } catch (ClassNotFoundException ex) { throw new IllegalArgumentException("Cannot register class: " + classname, ex); } catch (RuntimeException ex) { throw new IllegalArgumentException("Cannot register class: " + classname, ex); } } /** * Return all the content types supported by this library. This will be the * content types in the file WEB-INF/services/resources/jsyntaxpane/kitsfortypes * @return sorted array of all registered content types */ public static String[] getContentTypes() { String[] types = CONTENT_TYPES.toArray(new String[0]); Arrays.sort(types); return types; } /** * Merges the given properties with the configurations for this Object * * @param config */ public void setConfig(Properties config) { getConfig().putAll(config); } /** * Sets the given property to the given value. If the kit is not * initialized, then calls initKit * @param key * @param value */ public void setProperty(String key, String value) { getConfig().put(key, value); } /** * Return the property with the given key. If the kit is not * initialized, then calls initKit * Be careful when changing property as the default property may be used * @param key * @return value for given key */ public String getProperty(String key) { return getConfig().getString(key); } /** * Get the configuration for this Object * @return */ public Configuration getConfig() { return getConfig(this.getClass()); } /** * Return the Configurations object for a Kit. Perfrom lazy creation of a * Configuration object if nothing is created. * * @param kit * @return */ public static synchronized Configuration getConfig(Class<? extends DefaultSyntaxKit> kit) { if (CONFIGS == null) { CONFIGS = new WeakHashMap<Class<? extends DefaultSyntaxKit>, Configuration>(); Configuration defaultConfig = new Configuration(DefaultSyntaxKit.class); loadConfig(defaultConfig, DefaultSyntaxKit.class); CONFIGS.put(DefaultSyntaxKit.class, defaultConfig); } if (CONFIGS.containsKey(kit)) { return CONFIGS.get(kit); } else { // recursive call until we read the Super duper DefaultSyntaxKit Class superKit = kit.getSuperclass(); @SuppressWarnings("unchecked") Configuration defaults = getConfig(superKit); Configuration mine = new Configuration(kit, defaults); loadConfig(mine, kit); CONFIGS.put(kit, mine); return mine; } } private static void loadConfig(Configuration conf, Class<? extends EditorKit> kit) { String url = kit.getName().replace(".", "/") + "/config"; Properties p = JarServiceProvider.readProperties(url); if (p.size() == 0) { LOG.info("unable to load configuration for: " + kit + " from: " + url + ".properties"); } else { conf.putAll(p); } } }