package org.thdl.quilldriver; import org.jdom.Document; import org.jdom.Element; import org.jdom.Attribute; import org.jdom.Text; import org.jdom.DocType; import java.awt.Color; import java.awt.Cursor; import java.awt.event.KeyEvent; import java.awt.event.ActionEvent; import java.awt.event.MouseEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseListener; import java.awt.event.MouseMotionAdapter; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.util.List; import java.util.Set; import java.util.Iterator; import java.util.Map; import java.util.HashMap; import java.util.Hashtable; import java.util.EventObject; import java.util.EventListener; import javax.swing.JTextPane; import javax.swing.text.JTextComponent; import javax.swing.Action; import javax.swing.AbstractAction; import javax.swing.KeyStroke; import javax.swing.text.Keymap; import javax.swing.text.Position; import javax.swing.text.StyleConstants; import javax.swing.text.StyledDocument; import javax.swing.text.AttributeSet; import javax.swing.text.SimpleAttributeSet; import javax.swing.text.BadLocationException; import javax.swing.text.DefaultEditorKit; import javax.swing.event.DocumentListener; import javax.swing.event.DocumentEvent; import javax.swing.event.CaretListener; import javax.swing.event.CaretEvent; import javax.swing.event.EventListenerList; public class XMLEditor { private EventListenerList listenerList = new EventListenerList(); private Document xml; private JTextPane pane; private StyledDocument doc; private DocumentListener docListen; private Map startOffsets, endOffsets; private final float indentIncrement = 15.0F; private final Color tagColor = Color.magenta; private final Color attColor = Color.pink; private final Color textColor = Color.darkGray; private Cursor textCursor; private Cursor defaultCursor; private boolean isEditing = false; private Object editingNode = null; private boolean hasChanged = false; private Hashtable actions; private CaretListener editabilityTracker; private XMLTagInfo tagInfo; public XMLEditor(Document xmlDoc, JTextPane textPane, XMLTagInfo tagInfo) { xml = xmlDoc; pane = textPane; this.tagInfo = tagInfo; startOffsets = new HashMap(); endOffsets = new HashMap(); docListen = new DocumentListener() { public void changedUpdate(DocumentEvent e) { hasChanged = true; } public void insertUpdate(DocumentEvent e) { hasChanged = true; if (getStartOffsetForNode(editingNode) > e.getOffset()) { javax.swing.text.Document d = e.getDocument(); try { startOffsets.put(editingNode, d.createPosition(e.getOffset())); } catch (BadLocationException ble) { ble.printStackTrace(); } } } public void removeUpdate(DocumentEvent e) { hasChanged = true; } }; render(); textCursor = new Cursor(Cursor.TEXT_CURSOR); defaultCursor = new Cursor(Cursor.DEFAULT_CURSOR); pane.addMouseMotionListener(new MouseMotionAdapter() { public void mouseMoved(MouseEvent e) { JTextPane p = (JTextPane)e.getSource(); int offset = p.viewToModel(e.getPoint()); if (isEditable(offset)) p.setCursor(textCursor); else p.setCursor(defaultCursor); } }); MouseListener[] listeners = (MouseListener[])pane.getListeners(MouseListener.class); for (int i=0; i-1); if (dot == -1) return; //what to do? there's nothing to edit in this pane } if (isEditable(dot)) { pane.getCaret().setDot(dot); if (getNodeForOffset(dot) != null) fireStartEditEvent(getNodeForOffset(dot)); } } else if (editingNode == null) //need to start editing because cursor happens to be on an editable node fireStartEditEvent(getNodeForOffset(dot)); } }; Action nextNodeAction = new AbstractAction() { public void actionPerformed(ActionEvent e) { JTextPane p = (JTextPane)e.getSource(); int prePos = p.getCaretPosition(); Object node = getNodeForOffset(prePos); int i = prePos+1; while (i first) p.getCaret().moveDot(offset--); } }; Action selectToNodeEndAction = new AbstractAction() { public void actionPerformed(ActionEvent e) { JTextPane p = (JTextPane)e.getSource(); Object node = getNodeForOffset(p.getCaret().getMark()); if (node != null) { int last = (((Position)endOffsets.get(node)).getOffset()); if (node instanceof Attribute) last--; p.getCaret().moveDot(last); } } }; Action selectToNodeStartAction = new AbstractAction() { public void actionPerformed(ActionEvent e) { JTextPane p = (JTextPane)e.getSource(); int offset = p.getCaretPosition(); Object node = getNodeForOffset(p.getCaret().getMark()); if (node != null) { int first = (((Position)startOffsets.get(node)).getOffset()); p.getCaret().moveDot(first); } } }; Action backwardAction = new AbstractAction() { public void actionPerformed(ActionEvent e) { JTextPane p = (JTextPane)e.getSource(); int prePos = p.getCaretPosition(); int newPos = prePos-1; while (newPos>-1 && !isEditable(newPos)) newPos--; if (newPos != -1) { if (getNodeForOffset(prePos) != getNodeForOffset(newPos)) { fireEndEditEvent(); fireStartEditEvent(getNodeForOffset(newPos)); } p.setCaretPosition(newPos); } } }; Action forwardAction = new AbstractAction() { public void actionPerformed(ActionEvent e) { JTextPane p = (JTextPane)e.getSource(); int prePos = p.getCaretPosition(); int newPos = prePos+1; while (newPos first) { StyledDocument d = p.getStyledDocument(); try { d.remove(offset-1, 1); } catch (BadLocationException ble) { ble.printStackTrace(); } } } }; Action loseFocusAction = new AbstractAction() { public void actionPerformed(ActionEvent e) { JTextPane p = (JTextPane)e.getSource(); p.transferFocus(); //moves focus to next component } }; /* Action selForwardAction = new AbstractAction() { public void actionPerformed(ActionEvent e) { JTextPane p = (JTextPane)e.getSource(); int offset = p.getCaretPosition(); } }; */ createActionTable(pane); JTextPane tp = new JTextPane(); Keymap keymap = tp.addKeymap("KeyBindings", tp.getKeymap()); KeyStroke[] selectAllKeys = keymap.getKeyStrokesForAction(getActionByName(DefaultEditorKit.selectAllAction)); if (selectAllKeys != null) for (int i=0; i0) q=p-1; else q=p+1; } pane.setCaretPosition(q); pane.addCaretListener(editabilityTracker); //shouldn't do if already installed pane.setCaretPosition(p); } else pane.removeCaretListener(editabilityTracker); } public void updateNode(Object node) { System.out.println("updating: " + node.toString()); if (node == null) return; try { if (node instanceof Text) { int p1 = ((Position)startOffsets.get(node)).getOffset(); int p2 = ((Position)endOffsets.get(node)).getOffset(); String val = pane.getDocument().getText(p1, p2-p1).trim(); Text text = (Text)node; text.setText(val); } else if (node instanceof Attribute) { int p1 = ((Position)startOffsets.get(node)).getOffset(); int p2 = ((Position)endOffsets.get(node)).getOffset()-1; //remove right quote String val = pane.getDocument().getText(p1, p2-p1).trim(); Attribute att = (Attribute)node; att.setValue(val); } System.out.println("updated: " + node.toString()); } catch (BadLocationException ble) { ble.printStackTrace(); } } interface NodeEditListener extends EventListener { public void nodeEditPerformed(NodeEditEvent ned); } public void addNodeEditListener(NodeEditListener ned) { listenerList.add(NodeEditListener.class, ned); } public void removeNodeEditListener(NodeEditListener ned) { listenerList.remove(NodeEditListener.class, ned); } class NodeEditEvent extends EventObject { Object node; NodeEditEvent(Object node) { super(node); this.node = node; } public Object getNode() { return node; } } class StartEditEvent extends NodeEditEvent { StartEditEvent(Object node) { super(node); } } class EndEditEvent extends NodeEditEvent { public EndEditEvent(Object node) { super(node); } public boolean hasBeenEdited() { return hasChanged; } } class CantEditEvent extends NodeEditEvent { public CantEditEvent(Object node) { super(node); } } public void fireStartEditEvent(Object node) { //see javadocs on EventListenerList for how following array is structured Object[] listeners = listenerList.getListenerList(); for (int i = listeners.length-2; i>=0; i-=2) { if (listeners[i]==NodeEditListener.class) ((NodeEditListener)listeners[i+1]).nodeEditPerformed(new StartEditEvent(node)); } isEditing = true; editingNode = node; hasChanged = false; } public void fireEndEditEvent() { if (!isEditing) return; //see javadocs on EventListenerList for how following array is structured Object[] listeners = listenerList.getListenerList(); for (int i = listeners.length-2; i>=0; i-=2) { if (listeners[i]==NodeEditListener.class) ((NodeEditListener)listeners[i+1]).nodeEditPerformed(new EndEditEvent(editingNode)); } if (hasChanged) updateNode(editingNode); isEditing = false; editingNode = null; hasChanged = false; } public void fireCantEditEvent(Object node) { //see javadocs on EventListenerList for how following array is structured Object[] listeners = listenerList.getListenerList(); for (int i = listeners.length-2; i>=0; i-=2) { if (listeners[i]==NodeEditListener.class) ((NodeEditListener)listeners[i+1]).nodeEditPerformed(new CantEditEvent(node)); } } public void setXMLDocument(Document d, String doctype_elementName, String doctype_systemID) { xml = d; xml.setDocType(new DocType(doctype_elementName, doctype_systemID)); render(); } public void render() { System.out.println("Rendering the document"); doc = pane.getStyledDocument(); int len = doc.getLength(); try { if (len > 0) doc.remove(0, len); doc.insertString(0, "\n", null); } catch (BadLocationException ble) { ble.printStackTrace(); } startOffsets.clear(); endOffsets.clear(); Element root = xml.getRootElement(); renderElement(root, 0.0F, doc.getLength()); SimpleAttributeSet eColor = new SimpleAttributeSet(); eColor.addAttribute("xmlnode", root); doc.setParagraphAttributes(doc.getLength(), 1, eColor, false); fixOffsets(); doc.addDocumentListener(docListen); pane.setCaretPosition(0); setEditabilityTracker(true); } public void fixOffsets() { //replace Integer values in startOffsets and endOffsets with Positions Set startKeys = startOffsets.keySet(); Iterator iter = startKeys.iterator(); while (iter.hasNext()) { Object key = iter.next(); Object obj = startOffsets.get(key); //if (obj instanceof Position) // startOffsets.put(key, obj); //actually we don't have to do anything here, do we //since the startoffsets are already set!! //else if (obj instanceof Integer) try { Integer val = (Integer)obj; startOffsets.put(key, doc.createPosition(val.intValue())); } catch (BadLocationException ble) { ble.printStackTrace(); } } Set endKeys = endOffsets.keySet(); iter = endKeys.iterator(); while (iter.hasNext()) { Object key = iter.next(); Object obj = endOffsets.get(key); //if (obj instanceof Position) // endOffsets.put(key, obj); //actually we don't have to do anything here, do we //since the endoffsets are already set!! //else if (obj instanceof Integer) try { Integer val = (Integer)obj; endOffsets.put(key, doc.createPosition(val.intValue())); } catch (BadLocationException ble) { ble.printStackTrace(); } } } public int renderElement(Element e, float indent, int insertOffset) { try { Position pos = doc.createPosition(insertOffset); SimpleAttributeSet eAttributes = new SimpleAttributeSet(); StyleConstants.setLeftIndent(eAttributes, indent); SimpleAttributeSet eColor = new SimpleAttributeSet(); //StyleConstants.setLeftIndent(eColor, indent); StyleConstants.setForeground(eColor, tagColor); eColor.addAttribute("xmlnode", e); if (pos.getOffset()>0) { String s = doc.getText(pos.getOffset()-1, 1); if (s.charAt(0)!='\n') { AttributeSet attSet = doc.getCharacterElement(pos.getOffset()-1).getAttributes(); doc.insertString(pos.getOffset(), "\n", attSet); } } int start = pos.getOffset(); startOffsets.put(e, new Integer(start)); String tagDisplay; if (tagInfo == null) tagDisplay = e.getQualifiedName(); else tagDisplay = tagInfo.getTagDisplay(e); doc.insertString(pos.getOffset(), tagDisplay, eColor); //insert element begin tag if (tagInfo == null || tagInfo.areTagContentsForDisplay(e.getQualifiedName())) { List attributes = e.getAttributes(); Iterator iter = attributes.iterator(); while (iter.hasNext()) { Attribute att = (Attribute)iter.next(); if (tagInfo == null || tagInfo.isAttributeForDisplay(att.getQualifiedName(), e.getQualifiedName())) renderAttribute(att, pos.getOffset()); } doc.insertString(pos.getOffset(), ":", eColor); doc.setParagraphAttributes(start, pos.getOffset()-start, eAttributes, false); //doc.insertString(pos.getOffset(), "\n", null); List list = e.getContent(); iter = list.iterator(); while (iter.hasNext()) { Object next = iter.next(); if (next instanceof Element) { Element ne = (Element)next; if (tagInfo == null || tagInfo.isTagForDisplay(ne.getQualifiedName())) renderElement(ne, indent + indentIncrement, pos.getOffset()); } else if (next instanceof Text) { Text t = (Text)next; if (t.getParent().getContent().size() == 1 || t.getTextTrim().length() > 0) renderText(t, indent + indentIncrement, pos.getOffset()); } // Also: Comment ProcessingInstruction CDATA EntityRef } } //start = pos.getOffset(); //doc.insertString(start, "}", eColor); //insert element end tag //doc.setParagraphAttributes(start, pos.getOffset(), eAttributes, false); if (pos.getOffset()>0) { //String s = doc.getText(pos.getOffset()-1, 1); if (doc.getText(pos.getOffset()-1,1).charAt(0)=='\n') endOffsets.put(e, new Integer(pos.getOffset()-1)); else endOffsets.put(e, new Integer(pos.getOffset())); } //endOffsets.put(e, new Integer(pos.getOffset())); return pos.getOffset(); //doc.insertString(pos.getOffset(), "\n", null); } catch (BadLocationException ble) { ble.printStackTrace(); return -1; } } public int renderAttribute(Attribute att, int insertOffset) { try { Position pos = doc.createPosition(insertOffset); SimpleAttributeSet aColor = new SimpleAttributeSet(); StyleConstants.setForeground(aColor, attColor); SimpleAttributeSet tColor = new SimpleAttributeSet(); StyleConstants.setForeground(tColor, textColor); tColor.addAttribute("xmlnode", att); String name = att.getQualifiedName(); String value = att.getValue(); if (pos.getOffset()>0) { String s = doc.getText(pos.getOffset()-1, 1); if (s.charAt(0)!='\n') { AttributeSet attSet = doc.getCharacterElement(pos.getOffset()-1).getAttributes(); doc.insertString(pos.getOffset(), " ", attSet); } } String displayName; if (tagInfo == null) displayName = att.getQualifiedName(); else displayName = tagInfo.getAttributeDisplay(att.getQualifiedName(), att.getParent().getQualifiedName()); doc.insertString(pos.getOffset(), displayName+"=", aColor); startOffsets.put(att, new Integer(pos.getOffset()+1)); //add one so that begin quote is not part of attribute value doc.insertString(pos.getOffset(), "\"" + att.getValue()+"\"", tColor); endOffsets.put(att, new Integer(pos.getOffset())); return pos.getOffset(); } catch (BadLocationException ble) { ble.printStackTrace(); return -1; } } public int renderText(Text t, float indent, int insertOffset) { try { Position pos = doc.createPosition(insertOffset); SimpleAttributeSet tAttributes = new SimpleAttributeSet(); //StyleConstants.setLeftIndent(tAttributes, indent); StyleConstants.setForeground(tAttributes, textColor); tAttributes.addAttribute("xmlnode", t); doc.insertString(pos.getOffset(), " ", tAttributes); //insert space with text attributes so first character has correct color, xmlnode attribute, etc. String s = t.getTextTrim(); int start = pos.getOffset(); startOffsets.put(t, new Integer(start)); doc.insertString(pos.getOffset(), s, tAttributes); //insert text int end = pos.getOffset(); endOffsets.put(t, new Integer(end)); doc.insertString(pos.getOffset(), "\n", tAttributes); return pos.getOffset(); } catch (BadLocationException ble) { ble.printStackTrace(); return -1; } } public void removeNode(Object node) { if (startOffsets.containsKey(node)) { //note: should recursively eliminate all sub-nodes too!! startOffsets.remove(node); endOffsets.remove(node); } } public Object getNodeForOffset(int offset) { AttributeSet attSet = doc.getCharacterElement(offset).getAttributes(); return attSet.getAttribute("xmlnode"); } public int getStartOffsetForNode(Object node) { Position pos = (Position)startOffsets.get(node); if (pos == null) return -1; else return pos.getOffset(); } public int getEndOffsetForNode(Object node) { Position pos = (Position)endOffsets.get(node); if (pos == null) return -1; else return pos.getOffset(); } public boolean isEditable(int offset) { Object node = getNodeForOffset(offset); if ((node instanceof Text) && (offsetgetEndOffsetForNode(node))) return false; else if (node instanceof Attribute && (offsetgetEndOffsetForNode(node)-1)) return false; else return isEditable(node); } public boolean isEditable(Object node) { if (node == null) return false; else if (node instanceof Element) return false; else if (node instanceof Text) return true; else if (node instanceof Attribute) return true; else return false; } public JTextPane getTextPane() { return pane; } public Document getXMLDocument() { return xml; } }