jsyntaxpane.DefaultSyntaxKit Maven / Gradle / Ivy
/*
* Copyright 2008 Ayman Al-Sairafi [email protected]
*
* 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.awt.Toolkit;
import java.awt.event.KeyEvent;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
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.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JEditorPane;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JToolBar;
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.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_TOOLBAR = "Toolbar";
public static final String CONFIG_TOOLBAR_ROLLOVER = "Toolbar.Buttons.Rollover";
public static final String CONFIG_TOOLBAR_BORDER = "Toolbar.Buttons.BorderPainted";
public static final String CONFIG_TOOLBAR_OPAQUE = "Toolbar.Buttons.Opaque";
public static final String CONFIG_TOOLBAR_BORDER_SIZE = "Toolbar.Buttons.BorderSize";
private static final Pattern ACTION_KEY_PATTERN = Pattern.compile("Action\\.((\\w|-)+)");
private static final Pattern DEFAULT_ACTION_PATTERN = Pattern.compile("(DefaultAction.((\\w|-)+)).*");
private static Font DEFAULT_FONT;
private static Set CONTENT_TYPES = new HashSet();
private static Boolean initialized = false;
private static Map abbrvs;
private static String MENU_MASK_STRING = "control ";
private Lexer lexer;
private static final Logger LOG = Logger.getLogger(DefaultSyntaxKit.class.getName());
private Map> editorComponents =
new WeakHashMap>();
private Map popupMenu =
new WeakHashMap();
/**
* Main Configuration of JSyntaxPane EditorKits
*/
private static Map, Configuration> CONFIGS;
static {
// we only need to initialize once.
if (!initialized) {
initKit();
}
int menuMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
if(menuMask == KeyEvent.ALT_DOWN_MASK) {
MENU_MASK_STRING = "alt ";
}
}
private static final String ACTION_MENU_TEXT = "MenuText";
/**
* 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);
}
}
/**
* Creates a SyntaxComponent of the the given classname and installs
* it on the pane
* @param pane
* @param classname
*/
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());
}
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);
}
}
/**
* Find the SyntaxCOmponent with given classname that is installed
* on the given pane, then deinstalls and removes it fom the
* editorComponents list
* @param pane
* @param classname
*/
public void deinstallComponent(JEditorPane pane, String classname) {
for (SyntaxComponent c : editorComponents.get(pane)) {
if (c.getClass().getName().equals(classname)) {
c.deinstall(pane);
editorComponents.get(pane).remove(c);
break;
}
}
}
/**
* Checks if the component with given classname is installed on the
* pane.
* @param pane
* @param classname
* @return true if component is installed, false otherwise
*/
public boolean isComponentInstalled(JEditorPane pane, String classname) {
for (SyntaxComponent c : editorComponents.get(pane)) {
if (c.getClass().getName().equals(classname)) {
return true;
}
}
return false;
}
/**
* Toggles the component with given classname. If component is found
* and installed, then it is deinstalled. Otherwise a new one is
* installed
* @param pane
* @param classname
* @return true if component was installed, false if it was removed
*/
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());
JMenu stack = null;
for (String menuString : menuItems) {
// create the Popup menu
if (menuString.equals("-")) {
popupMenu.get(editorPane).addSeparator();
} else if (menuString.startsWith(">")) {
JMenu sub = new JMenu(menuString.substring(1));
popupMenu.get(editorPane).add(sub);
stack = sub;
} else if (menuString.startsWith("<")) {
Container parent = stack.getParent();
if (parent instanceof JMenu) {
JMenu jMenu = (JMenu) parent;
stack = jMenu;
} else {
stack = null;
}
} else {
Action action = editorPane.getActionMap().get(menuString);
if (action != null) {
JMenuItem menuItem;
if (action.getValue(Action.SELECTED_KEY) != null) {
menuItem = new JCheckBoxMenuItem(action);
} else {
menuItem = new JMenuItem(action);
}
// Use our own property if it was set for the menu text
if (action.getValue(ACTION_MENU_TEXT) != null) {
menuItem.setText((String) action.getValue(ACTION_MENU_TEXT));
}
if (stack == null) {
popupMenu.get(editorPane).add(menuItem);
} else {
stack.add(menuItem);
}
}
}
}
editorPane.setComponentPopupMenu(popupMenu.get(editorPane));
}
/**
* Add all pop-up menu items to a Toolbar. You need to call the validate method
* on the toolbar after this is done to layout the buttons.
* Only Actions which have a SMALL_ICON property will be added to the toolbar
* There are three Configuration Keys that affect the appearance of the added buttons:
* CONFIG_TOOLBAR_ROLLOVER, CONFIG_TOOLBAR_BORDER, CONFIG_TOOLBAR_OPAQUE
*
* @param editorPane
* @param toolbar
*/
public void addToolBarActions(JEditorPane editorPane, JToolBar toolbar) {
String[] toolBarItems = getConfig().getPropertyList(CONFIG_TOOLBAR);
if (toolBarItems == null || toolBarItems.length == 0) {
toolBarItems = getConfig().getPropertyList(CONFIG_MENU);
if (toolBarItems == null || toolBarItems.length == 0) {
return;
}
}
boolean btnRolloverEnabled = getConfig().getBoolean(CONFIG_TOOLBAR_ROLLOVER, true);
boolean btnBorderPainted = getConfig().getBoolean(CONFIG_TOOLBAR_BORDER, false);
boolean btnOpaque = getConfig().getBoolean(CONFIG_TOOLBAR_OPAQUE, false);
int btnBorderSize = getConfig().getInteger(CONFIG_TOOLBAR_BORDER_SIZE, 2);
for (String menuString : toolBarItems) {
if (menuString.equals("-") ||
menuString.startsWith("<") ||
menuString.startsWith(">")) {
toolbar.addSeparator();
} else {
Action action = editorPane.getActionMap().get(menuString);
if (action != null && action.getValue(Action.SMALL_ICON) != null) {
JButton b = toolbar.add(action);
b.setRolloverEnabled(btnRolloverEnabled);
b.setBorderPainted(btnBorderPainted);
b.setOpaque(btnOpaque);
b.setFocusable(false);
b.setBorder(BorderFactory.createEmptyBorder(btnBorderSize,
btnBorderSize, btnBorderSize, btnBorderSize));
}
}
}
}
@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 l = editorComponents.get(editorPane);
for (SyntaxComponent c : editorComponents.get(editorPane)) {
c.deinstall(editorPane);
}
editorComponents.clear();
editorPane.getInputMap().clear();
editorPane.getActionMap().clear();
}
/**
* Add keyboard actions to this control using the Configuration we have
* This is revised to properly use InputMap and ActionMap of the component
* instead of using the KeyMaps directly.
* @param editorPane
*/
public void addActions(JEditorPane editorPane) {
InputMap imap = new InputMap();
imap.setParent(editorPane.getInputMap());
ActionMap amap = new ActionMap();
amap.setParent(editorPane.getActionMap());
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
amap.put(actionName, action);
// Now bind all the keys to the Action we have using the InputMap
for (int i = 1; i < values.length; i++) {
String keyStrokeString = values[i].replace("menu ", MENU_MASK_STRING);
KeyStroke ks = KeyStroke.getKeyStroke(keyStrokeString);
// we may have more than onr value ( for key action ), but we will use the
// last one in the single value here. This will display the key in the
// popup menus. Pretty neat.
if (ks == null) {
throw new IllegalArgumentException("Invalid KeyStroke: " +
keyStrokeString);
}
action.putValue(Action.ACCELERATOR_KEY, ks);
imap.put(ks, actionName);
}
}
// Now configure the Default actions for better display in the popup menu
for (Configuration.StringKeyMatcher m : getConfig().getKeys(DEFAULT_ACTION_PATTERN)) {
String name = m.matcher.group(2);
Action action = editorPane.getActionMap().get(name);
if (action != null) {
configActionProperties(action, name, m.group1);
}
// The below commented block does find the keys for the default Actions
// using InputMap, however there are multiple bound keys for the
// default actions that displaying them in the menu will probably not
// be the most obvious binding
/*
for (KeyStroke key : imap.allKeys()) {
Object o = imap.get(key);
if(name.equals(o)) {
action.putValue(Action.ACCELERATOR_KEY, key);
break;
}
}
*/
}
editorPane.setActionMap(amap);
editorPane.setInputMap(JTextComponent.WHEN_FOCUSED, imap);
}
private void configActionProperties(Action action, String actionName, String configKey) {
// if we have an icon, then load it:
String iconLoc = getConfig().getString(configKey + ".SmallIcon", actionName + ".png");
URL loc = this.getClass().getResource(DefaultSyntaxAction.SMALL_ICONS_LOC_PREFIX + iconLoc);
if (loc != null) {
ImageIcon i = new ImageIcon(loc);
action.putValue(Action.SMALL_ICON, i);
}
// Set the menu text. Use the Action.NAME property, unless it is
// already set.
// The NAME would be set for default actions, and we should not change those names.
// so we will put another property and use it for the menu text
String name = getProperty(configKey + ".MenuText");
if (action.getValue(Action.NAME) == null) {
action.putValue(Action.NAME, name);
} else {
action.putValue(ACTION_MENU_TEXT, name);
}
// Set the menu tooltips
String shortDesc = getProperty(configKey + ".ToolTip");
if (shortDesc != null) {
action.putValue(Action.SHORT_DESCRIPTION, shortDesc);
} else {
action.putValue(Action.SHORT_DESCRIPTION, name);
}
}
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 Lexer
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, 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;
}
}
public Map getAbbreviations() {
// if we have not loaded the abbreviations, then load them now:
if (abbrvs == null) {
String cl = this.getClass().getName().replace('.', '/').toLowerCase();
abbrvs = JarServiceProvider.readStringsMap(cl + "/abbreviations.properties");
}
return abbrvs;
}
/**
* Adds an abbrevisation to this kit's abbreviations.
* @param abbr
* @param template
*/
public static void addAbbreviation(String abbr, String template) {
if (abbrvs == null) {
abbrvs = new HashMap();
}
abbrvs.put(abbr, template);
}
/**
* Get the template for the given abbreviation
* @param abbr
* @return
*/
public static String getAbbreviation(String abbr) {
return abbrvs == null ? null : abbrvs.get(abbr);
}
private static void loadConfig(Configuration conf, Class extends EditorKit> kit) {
String url = kit.getName().replace(".", "/") + "/config";
Properties p = JarServiceProvider.readProperties(url, Locale.getDefault());
if (p.size() == 0) {
LOG.log(Level.FINE, "unable to load configuration for: {0} from: {1}.properties",
new Object[]{kit, url});
} else {
conf.putAll(p);
}
}
@Override
public String getContentType() {
return "text/" + this.getClass().getSimpleName().replace("SyntaxKit", "").toLowerCase();
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy