package ui; import java.awt.Color; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.StringReader; import java.util.HashMap; import javax.swing.text.AttributeSet; import javax.swing.text.BadLocationException; import javax.swing.text.DefaultStyledDocument; import javax.swing.text.SimpleAttributeSet; import javax.swing.text.StyleConstants; import common.prettyprinter.PrettyStyle; import languages.Language; import languages.LanguageFactory; import languages.LanguageScanner; import languages.LanguageScannerException; import languages.LanguageSymbol; import languages.NoSuchLanguageException; import expressions.Expression; /** * * TODO Add documentation here. * * @author Benedikt Meurer * @version $Id$ */ public class MLStyledDocument extends DefaultStyledDocument { // // Constants // /** * Empty array of language exceptions. */ private static final LanguageScannerException[] EMPTY_ARRAY = new LanguageScannerException[0]; // // Attributes // /** * If any <code>PropertyChangeListeners</code> have been registered, * the <code>changeSupport</code> field describes them. * * @serial * @see #addPropertyChangeListener(PropertyChangeListener) * @see #addPropertyChangeListener(String, PropertyChangeListener) * @see #removePropertyChangeListener(PropertyChangeListener) * @see #removePropertyChangeListener(String, PropertyChangeListener) * @see #firePropertyChange(String, boolean, boolean) * @see #firePropertyChange(String, int, int) * @see #firePropertyChange(String, Object, Object) */ protected PropertyChangeSupport changeSupport; private Language language; private LanguageScannerException exceptions[]; private SimpleAttributeSet normalSet = new SimpleAttributeSet(); private HashMap<PrettyStyle, SimpleAttributeSet> attributes = new HashMap<PrettyStyle, SimpleAttributeSet>(); private static final long serialVersionUID = -1779640687489585648L; // // Constructor // /** * */ public MLStyledDocument() { try { // try to determine the language LanguageFactory languageFactory = LanguageFactory.newInstance(); this.language = languageFactory.getLanguageById("l1"); } catch (NoSuchLanguageException e) { throw new RuntimeException(e); } // setup the normal attribute set StyleConstants.setForeground(this.normalSet, Color.BLACK); StyleConstants.setBold(this.normalSet, false); // setup the comment set SimpleAttributeSet commentSet = new SimpleAttributeSet(); StyleConstants.setForeground(commentSet, new Color(0.0f, 0.6f, 0.0f)); StyleConstants.setItalic(commentSet, true); this.attributes.put(PrettyStyle.COMMENT, commentSet); // setup the constant set SimpleAttributeSet constantSet = new SimpleAttributeSet(); StyleConstants.setForeground(constantSet, new Color(0.0f, 0.0f, 0.6f)); StyleConstants.setBold(constantSet, true); this.attributes.put(PrettyStyle.CONSTANT, constantSet); // setup the keyword set SimpleAttributeSet keywordSet = new SimpleAttributeSet(); StyleConstants.setForeground(keywordSet, new Color(0.6f, 0.0f, 0.0f)); StyleConstants.setBold(keywordSet, true); this.attributes.put(PrettyStyle.KEYWORD, keywordSet); } // // Accessors // /** * Returns the current {@link LanguageScannerException}s that * were detected while trying to interpret the token stream. * * @return the exceptions. */ public LanguageScannerException[] getExceptions() { return (this.exceptions != null) ? this.exceptions : EMPTY_ARRAY; } /** * Returns the exception at the given <code>index</code>. * * @param index the index of the exception to return. * * @return the exception at the <code>index</code>. * * @throws ArrayIndexOutOfBoundsException if the <code>index</code> * is out of bounds. */ public LanguageScannerException getExceptions(int index) { return getExceptions()[index]; } // // Primitives // /** * Returns the {@link Expression} for the program text within * this document. Throws an exception if a parsing error occurred. * * @return the {@link Expression} for the program text. * * @throws Exception */ public Expression getExpression() throws Exception { LanguageFactory languageFactory = LanguageFactory.newInstance(); Language language = languageFactory.getLanguageById("l1"); return language.newParser(new StringReader(getText(0, getLength()))).parse(); } // // Change handling // public void insertString(int offset, String str, AttributeSet set) throws BadLocationException { super.insertString(offset, str, set); processChanged(); } public void remove(int offset, int length) throws BadLocationException { super.remove(offset, length); processChanged(); } public void processChanged() throws BadLocationException { // reset the character attributes setCharacterAttributes(0, getLength(), this.normalSet, true); // allocate a list to collect the exceptions LanguageScannerException[] exceptions = null; try { // start with first character int offset = 0; // determine the document content String content = getText(offset, getLength()); // allocate the scanner (initially) LanguageScanner scanner = this.language.newScanner(new StringReader(content)); // determine the tokens for the content for (;;) { try { // read the next token from the scanner LanguageSymbol symbol = scanner.nextSymbol(); if (symbol == null) break; // check if we have an attribute set for the token SimpleAttributeSet set = this.attributes.get(scanner.getStyleBySymbol(symbol)); if (set == null) set = this.normalSet; // apply the character attribute set setCharacterAttributes(offset + symbol.getLeft(), symbol.getRight() - symbol.getLeft(), set, true); // add the token to the token list (skip comments) /*FIXME:PARSER:if (scanner.getStyleBySymbol(symbol) != PrettyStyle.COMMENT) this.symbols.add(symbol);*/ } catch (LanguageScannerException e) { // setup the error attribute set SimpleAttributeSet errorSet = new SimpleAttributeSet(); StyleConstants.setForeground(errorSet, Color.RED); StyleConstants.setUnderline(errorSet, true); errorSet.addAttribute("exception", e); // apply the error character attribute set to indicate the syntax error setCharacterAttributes(offset + e.getLeft(), e.getRight() - e.getLeft(), errorSet, false); // adjust the offset to point after the error offset += e.getRight(); // skip the problematic characters content = content.substring(e.getRight()); // restart the scanner after the error scanner.restart(new StringReader(content)); // add the exception to our list if (exceptions == null) { exceptions = new LanguageScannerException[] { e }; } else { LanguageScannerException[] newExceptions = new LanguageScannerException[exceptions.length + 1]; System.arraycopy(exceptions, 0, newExceptions, 0, exceptions.length); newExceptions[exceptions.length] = e; exceptions = newExceptions; } } } /*FIXME:PARSER// check if we completed without errors if (numErrors == 0) { try { // try to parse the generated tokens LanguageParser parser = this.language.newParser(new TokenStream(this.symbols)); parser.parse(); } catch (LanguageParserException e) { // setup the error attribute set SimpleAttributeSet errorSet = new SimpleAttributeSet(); StyleConstants.setForeground(errorSet, Color.RED); StyleConstants.setUnderline(errorSet, true); errorSet.addAttribute("exception", e); // determine the error offset and the length int errorOffset = (e.getLeft() < 0) ? getLength() - 1 : e.getLeft(); int errorLength = (e.getRight() < 0) ? 1 : (e.getRight() - errorOffset); // apply the error character attribute set to indicate the syntax error setCharacterAttributes(errorOffset, errorLength, errorSet, false); } }*/ } catch (Exception e) { // FIXME e.printStackTrace(); } // update the exceptions property if necessary if (this.exceptions != exceptions) { LanguageScannerException[] oldExceptions = this.exceptions; this.exceptions = exceptions; firePropertyChange("exceptions", oldExceptions, this.exceptions); } } // // Inner classes // /** * TODO */ /*FIXME:PARSER:private static final class TokenStream extends AbstractLanguageScanner { private Iterator<LanguageSymbol> iterator; TokenStream(LinkedList<LanguageSymbol> symbols) { this.iterator = symbols.iterator(); } @Override protected PrettyStyle getStyleBySymbolId(int id) { return PrettyStyle.NONE; } public LanguageSymbol nextSymbol() throws IOException, LanguageScannerException { return this.iterator.hasNext() ? this.iterator.next() : null; } public void restart(Reader reader) { throw new UnsupportedOperationException("not supported"); } }*/ // // Listener registration // /** * Adds a {@link PropertyChangeListener} to the listener list. The listener * is registered for all bound properties of the derived class. * * If <code>listener</code> is <code>null</code>, no exception is thrown * and no action is performed. * * @param listener the {@link PropertyChangeListener} to be added. * * @see #getPropertyChangeListeners() * @see #removePropertyChangeListener(PropertyChangeListener) */ public synchronized void addPropertyChangeListener(PropertyChangeListener listener) { if (listener == null) { return; } if (this.changeSupport == null) { this.changeSupport = new PropertyChangeSupport(this); } this.changeSupport.addPropertyChangeListener(listener); } /** * Removes a {@link PropertyChangeListener} from the listener list. This method * should be used to remove PropertyChangeListeners that were registered for all * bound properties of this class. * * If <code>listener</code> is <code>null</code>, no exception is thrown and * no action is performed. * * @param listener the {@link PropertyChangeListener} to be removed. * * @see #addPropertyChangeListener(PropertyChangeListener) * @see #getPropertyChangeListeners() */ public synchronized void removePropertyChangeListener(PropertyChangeListener listener) { if (listener == null || this.changeSupport == null) { return; } this.changeSupport.removePropertyChangeListener(listener); } /** * Returns an array of all the property change listeners registered * on this object. * * @return all of this object's {@link PropertyChangeListener}s or an * empty array if no property change listeners are currently * registered. * * @see #addPropertyChangeListener(PropertyChangeListener) * @see #removePropertyChangeListener(PropertyChangeListener) */ public synchronized PropertyChangeListener[] getPropertyChangeListeners() { if (this.changeSupport == null) { return new PropertyChangeListener[0]; } return this.changeSupport.getPropertyChangeListeners(); } /** * Adds a {@link PropertyChangeListener} to the listener list for a specific * property. The specified property may be user-defined, or one of the properties * provided by the object. * * If <code>listener</code> is <code>null</code>, no exception is thrown and * no action is performed. * * @param propertyName one of the property names of the object. * @param listener the {@link PropertyChangeListener} to be added. * * @see #removePropertyChangeListener(String, PropertyChangeListener) * @see #getPropertyChangeListeners(String) */ public synchronized void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) { if (listener == null) { return; } if (this.changeSupport == null) { this.changeSupport = new PropertyChangeSupport(this); } this.changeSupport.addPropertyChangeListener(propertyName, listener); } /** * Removes a {@link PropertyChangeListener} from the listener list for a specific * property. This method should be used to remove {@link PropertyChangeListener}s * that were registered for a specific bound property. * * If <code>listener</code> is <code>null</code>, no exception is thrown and no * action is performed. * * @param propertyName a valid property name. * @param listener the {@link PropertyChangeListener} to be removed. * * @see #addPropertyChangeListener(String, PropertyChangeListener) * @see #getPropertyChangeListeners(String) */ public synchronized void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) { if (listener == null || this.changeSupport == null) { return; } this.changeSupport.removePropertyChangeListener(propertyName, listener); } /** * Returns an array of all the listeners which have been associated * with the named property. * * @param propertyName a valid property name. * * @return all of the {@link PropertyChangeListeners} associated with * the named property or an empty array if no listeners have * been added */ public synchronized PropertyChangeListener[] getPropertyChangeListeners(String propertyName) { if (this.changeSupport == null) { return new PropertyChangeListener[0]; } return this.changeSupport.getPropertyChangeListeners(propertyName); } // // Listener invocation // /** * Support for reporting bound property changes for Object properties. * This method can be called when a bound property has changed and it will * send the appropriate {@link PropertyChangeEvent} to any registered * {@link PropertyChangeListener}s. * * @param propertyName the property whose value has changed. * @param oldValue the property's previous value. * @param newValue the property's new value. */ protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) { PropertyChangeSupport changeSupport = this.changeSupport; if (changeSupport == null) { return; } changeSupport.firePropertyChange(propertyName, oldValue, newValue); } /** * Support for reporting bound property changes for boolean properties. * This method can be called when a bound property has changed and it will * send the appropriate {@link PropertyChangeEvent} to any registered * {@link PropertyChangeListener}s. * * @param propertyName the propery whose value has changed. * @param oldValue the property's previous value. * @param newValue the property's new value. */ protected void firePropertyChange(String propertyName, boolean oldValue, boolean newValue) { PropertyChangeSupport changeSupport = this.changeSupport; if (changeSupport == null) { return; } changeSupport.firePropertyChange(propertyName, oldValue, newValue); } /** * Support for reporting bound property changes for boolean properties. * This method can be called when a bound property has changed and it will * send the appropriate {@link PropertyChangeEvent} to any registered * {@link PropertyChangeListener}s. * * @param propertyName the propery whose value has changed. * @param oldValue the property's previous value. * @param newValue the property's new value. */ protected void firePropertyChange(String propertyName, int oldValue, int newValue) { PropertyChangeSupport changeSupport = this.changeSupport; if (changeSupport == null) { return; } changeSupport.firePropertyChange(propertyName, oldValue, newValue); } }