001: /*
002: * XmlTree.java
003: *
004: * Copyright (C) 2000-2003 Peter Graves
005: * $Id: XmlTree.java,v 1.9 2003/07/23 15:46:59 piso Exp $
006: *
007: * This program is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU General Public License
009: * as published by the Free Software Foundation; either version 2
010: * of the License, or (at your option) any later version.
011: *
012: * This program is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
015: * GNU General Public License for more details.
016: *
017: * You should have received a copy of the GNU General Public License
018: * along with this program; if not, write to the Free Software
019: * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
020: */
021:
022: package org.armedbear.j;
023:
024: import java.awt.Color;
025: import java.awt.Component;
026: import java.awt.Graphics;
027: import java.awt.Point;
028: import java.awt.event.InputEvent;
029: import java.awt.event.KeyEvent;
030: import java.awt.event.KeyListener;
031: import java.awt.event.MouseEvent;
032: import java.awt.event.MouseListener;
033: import java.awt.event.MouseMotionListener;
034: import java.io.StringReader;
035: import java.util.Enumeration;
036: import javax.swing.JTree;
037: import javax.swing.SwingUtilities;
038: import javax.swing.event.TreeSelectionEvent;
039: import javax.swing.event.TreeSelectionListener;
040: import javax.swing.tree.DefaultMutableTreeNode;
041: import javax.swing.tree.DefaultTreeCellRenderer;
042: import javax.swing.tree.TreeModel;
043: import javax.swing.tree.TreePath;
044: import javax.swing.tree.TreeSelectionModel;
045:
046: public final class XmlTree extends JTree implements Constants,
047: NavigationComponent, TreeSelectionListener, MouseListener,
048: MouseMotionListener, KeyListener {
049: private final Editor editor;
050: private final Buffer buffer;
051: private String parserClassName;
052: private boolean aelfred;
053: private boolean xp;
054: private int modificationCount = -1;
055: private boolean disabled;
056:
057: public XmlTree(Editor editor, TreeModel model) {
058: super (model);
059: this .editor = editor;
060: this .buffer = editor.getBuffer();
061: getSelectionModel().setSelectionMode(
062: TreeSelectionModel.SINGLE_TREE_SELECTION);
063: addTreeSelectionListener(this );
064: addMouseListener(this );
065: addMouseMotionListener(this );
066: addKeyListener(this );
067: setCellRenderer(new XmlTreeCellRenderer(this ));
068: }
069:
070: public final String getLabelText() {
071: return buffer.getFile() != null ? buffer.getFile().getName()
072: : null;
073: }
074:
075: public void setParserClassName(String className) {
076: parserClassName = className;
077: aelfred = false;
078: xp = false;
079: if (parserClassName.equals("org.armedbear.j.aelfred.SAXDriver"))
080: aelfred = true;
081: else if (parserClassName.equals("com.jclark.xml.sax.Driver"))
082: xp = true;
083: }
084:
085: public final Editor getEditor() {
086: return editor;
087: }
088:
089: public synchronized void refresh() {
090: if (!SwingUtilities.isEventDispatchThread())
091: Debug
092: .bug("XmlTree.refresh() called from background thread!");
093: if (disabled)
094: return;
095: if (modificationCount == buffer.getModCount())
096: return;
097: final XmlParserImpl parser = new XmlParserImpl(buffer);
098: if (!parser.initialize())
099: return;
100: modificationCount = buffer.getModCount();
101: try {
102: final String text = buffer.getText();
103: if (text.length() < 7) // "<a></a>"
104: return;
105: parser.setReader(new StringReader(text));
106: } catch (OutOfMemoryError e) {
107: outOfMemory();
108: return;
109: }
110: Runnable parseBufferRunnable = new Runnable() {
111: public void run() {
112: try {
113: parser.run();
114: } catch (OutOfMemoryError e) {
115: outOfMemory();
116: return;
117: }
118: if (parser.getException() == null) {
119: final TreeModel treeModel = parser.getTreeModel();
120: if (treeModel != null) {
121: setParserClassName(parser.getParserClassName());
122: Runnable r = new Runnable() {
123: public void run() {
124: setModel(treeModel);
125: if (editor.getBuffer() == buffer)
126: XmlMode.ensureCurrentNodeIsVisible(
127: editor, XmlTree.this );
128: }
129: };
130: SwingUtilities.invokeLater(r);
131: }
132: }
133: }
134: };
135: new Thread(parseBufferRunnable).start();
136: }
137:
138: // Update the selected node in the tree, based on the position of dot in
139: // the edit buffer.
140: public void updatePosition() {
141: if (disabled)
142: return;
143: Position dot = editor.getDot();
144: if (dot == null)
145: return;
146: final Line dotLine = dot.getLine();
147: final int dotOffset = dot.getOffset();
148:
149: // Our line numbers are zero-based.
150: final int dotLineNumber = editor.getDotLineNumber();
151:
152: int rowToBeSelected = -1;
153: final int limit = getRowCount();
154: for (int row = 0; row < limit; row++) {
155: TreePath path = getPathForRow(row);
156: if (path != null) {
157: DefaultMutableTreeNode node = (DefaultMutableTreeNode) path
158: .getLastPathComponent();
159: if (node != null) {
160: XmlTreeElement treeElement = (XmlTreeElement) node
161: .getUserObject();
162: // Tree element line numbers are one-based, so subtract 1.
163: final int lineNumber = treeElement.getLineNumber() - 1;
164: if (lineNumber == dotLineNumber) {
165: // Tree element column numbers are one-based, so
166: // subtract 1.
167: int columnNumber = treeElement
168: .getColumnNumber();
169: if (columnNumber > 0) {
170: // Tree element column numbers are one-based, so
171: // subtract 1.
172: --columnNumber;
173: }
174: int index;
175: if (columnNumber < 0) {
176: // Crimson always reports -1 ("maintaining column
177: // numbers hurts performance").
178: index = findStartTag(treeElement.getName(),
179: dotLine, 0);
180: } else if (xp) {
181: // Position reported by XP is '<' of start tag.
182: index = columnNumber;
183: } else {
184: // Position reported by parser is next char after
185: // '>' of start tag. Start reverse search 1 back
186: // from there.
187: index = reverseFindStartTag(treeElement
188: .getName(), dotLine, columnNumber);
189: }
190: if (aelfred && index < 0) {
191: // Aelfred's locator is very sloppy. Try forward
192: // search.
193: index = findStartTag(treeElement.getName(),
194: dotLine, columnNumber);
195: }
196: // Make sure index is sane (the tree may need refreshing).
197: if (index < 0)
198: index = 0;
199: else if (index > dotLine.length())
200: index = dotLine.length();
201: if (dotOffset == index) {
202: rowToBeSelected = row;
203: break;
204: } else if (dotOffset < index) {
205: // If dot is in whitespace at the beginning of the
206: // line, immediately to the left of the current
207: // node's start tag, we want the current node.
208: if (Utilities.isWhitespace(dotLine
209: .substring(0, index)))
210: rowToBeSelected = row;
211: break;
212: }
213: } else if (lineNumber > dotLineNumber)
214: break;
215: rowToBeSelected = row;
216: }
217: }
218: }
219:
220: if (rowToBeSelected >= 0) {
221: setSelectionRow(rowToBeSelected);
222: scrollRowToVisible(rowToBeSelected);
223: } else
224: clearSelection();
225:
226: repaint();
227: }
228:
229: private void outOfMemory() {
230: disabled = true;
231: treeModel = null;
232: MessageDialog.showMessageDialog(
233: "Not enough memory to display tree", "XML Mode");
234: }
235:
236: public void valueChanged(TreeSelectionEvent e) {
237: if (editor.getFocusedComponent() != this )
238: return;
239: if (editor.getStatusBar() == null)
240: return;
241: String statusText = "";
242: DefaultMutableTreeNode node = (DefaultMutableTreeNode) getLastSelectedPathComponent();
243: if (node != null) {
244: XmlTreeElement treeElement = (XmlTreeElement) node
245: .getUserObject();
246: statusText = treeElement.getStatusText();
247: }
248: editor.status(statusText);
249: }
250:
251: public void keyPressed(KeyEvent e) {
252: final int keyCode = e.getKeyCode();
253: final int modifiers = e.getModifiers();
254: switch (keyCode) {
255: // Ignore modifier keystrokes.
256: case KeyEvent.VK_SHIFT:
257: case KeyEvent.VK_CONTROL:
258: case KeyEvent.VK_ALT:
259: case KeyEvent.VK_META:
260: return;
261: case KeyEvent.VK_ENTER: {
262: e.consume();
263: TreePath path = getSelectionPath();
264: if (path != null) {
265: DefaultMutableTreeNode node = (DefaultMutableTreeNode) path
266: .getLastPathComponent();
267: if (node != null)
268: moveDotToNode(node);
269: }
270: editor.setFocusToDisplay();
271: if (modifiers == KeyEvent.ALT_MASK)
272: editor.toggleSidebar();
273: return;
274: }
275: case KeyEvent.VK_TAB:
276: e.consume();
277: if (modifiers == 0) {
278: if (editor.getSidebar().getBufferList() != null)
279: editor
280: .setFocus(editor.getSidebar()
281: .getBufferList());
282: }
283: return;
284: case KeyEvent.VK_ESCAPE:
285: e.consume();
286: editor.getSidebar().setBuffer();
287: editor.getSidebar().updatePosition();
288: editor.setFocusToDisplay();
289: return;
290: }
291: editor.getDispatcher().setEnabled(false);
292: }
293:
294: public void keyReleased(KeyEvent e) {
295: e.consume();
296: editor.getDispatcher().setEnabled(true);
297: }
298:
299: public void keyTyped(KeyEvent e) {
300: e.consume();
301: }
302:
303: public void mousePressed(MouseEvent e) {
304: LocationBar.cancelInput();
305: editor.ensureActive();
306: final int modifiers = e.getModifiers();
307: if (modifiers == InputEvent.BUTTON1_MASK
308: || modifiers == InputEvent.BUTTON2_MASK) {
309: editor.setFocus(this );
310: if (modifiers == InputEvent.BUTTON2_MASK) {
311: int row = getRowForLocation(e.getX(), e.getY());
312: if (row >= 0)
313: setSelectionRow(row);
314: }
315: } else
316: editor.setFocusToDisplay();
317: }
318:
319: public void mouseReleased(MouseEvent e) {
320: }
321:
322: public void mouseClicked(MouseEvent e) {
323: final int modifiers = e.getModifiers();
324: if (modifiers == InputEvent.BUTTON1_MASK
325: || modifiers == InputEvent.BUTTON2_MASK) {
326: Point point = e.getPoint();
327: moveDotToNodeAtPoint(point);
328: }
329: editor.setFocusToDisplay();
330: }
331:
332: public void mouseMoved(MouseEvent e) {
333: if (editor.getStatusBar() == null)
334: return;
335: String statusText = "";
336: Point point = e.getPoint();
337: TreePath path = getPathForLocation(point.x, point.y);
338: if (path != null) {
339: DefaultMutableTreeNode node = (DefaultMutableTreeNode) path
340: .getLastPathComponent();
341: if (node != null) {
342: XmlTreeElement treeElement = (XmlTreeElement) node
343: .getUserObject();
344: statusText = treeElement.getStatusText();
345: }
346: editor.status(statusText);
347: }
348: }
349:
350: public void mouseEntered(MouseEvent e) {
351: }
352:
353: public void mouseExited(MouseEvent e) {
354: editor.setFocusToDisplay();
355: if (editor.getStatusBar() != null) {
356: editor.getStatusBar().setText(null);
357: editor.getStatusBar().repaintNow();
358: }
359: }
360:
361: public void mouseDragged(MouseEvent e) {
362: }
363:
364: private void moveDotToNode(DefaultMutableTreeNode node) {
365: if (node == null)
366: return;
367: XmlTreeElement treeElement = (XmlTreeElement) node
368: .getUserObject();
369: String name = treeElement.getName();
370:
371: // Subtract 1 since our line numbers are zero-based.
372: int lineNumber = treeElement.getLineNumber() - 1;
373:
374: Editor editor = Editor.currentEditor();
375: Line line = editor.getBuffer().getLine(lineNumber);
376: if (line != null) {
377: int offset;
378: if (treeElement.getColumnNumber() < 0) {
379: // Crimson always reports -1.
380: offset = findStartTag(name, line, 0);
381: } else if (xp) {
382: // Position reported by XP is '<' of start tag.
383: offset = treeElement.getColumnNumber() - 1;
384: } else {
385: offset = 0;
386:
387: // The line and column numbers stored in the tree element
388: // refer (in theory) to the position just past the end of the
389: // start tag. Subtract 1 since our offsets are zero-based.
390: int endOfStartTag = treeElement.getColumnNumber() - 1;
391:
392: if (endOfStartTag >= 0) {
393: if (aelfred) {
394: // Aelfred.
395: // Look for start of start tag.
396: int startOfStartTag = reverseFindStartTag(name,
397: line, endOfStartTag - 1);
398: if (startOfStartTag < 0)
399: startOfStartTag = findStartTag(name, line,
400: endOfStartTag);
401: if (startOfStartTag >= 0)
402: offset = startOfStartTag;
403: } else {
404: // Not Aelfred.
405: // Look for start of start tag.
406: int startOfStartTag = reverseFindStartTag(name,
407: line, endOfStartTag - 1);
408: while (startOfStartTag < 0) {
409: // Not found on current line. Look at previous line.
410: if (line.previous() == null)
411: break;
412: line = line.previous();
413: startOfStartTag = reverseFindStartTag(name,
414: line, line.length());
415: }
416: if (startOfStartTag >= 0)
417: offset = startOfStartTag;
418: }
419: }
420: }
421: editor.addUndo(SimpleEdit.MOVE);
422: if (editor.getMark() != null) {
423: editor.setMark(null);
424: editor.setUpdateFlag(REPAINT);
425: }
426: editor.update(editor.getDotLine());
427: // Make sure offset is sane.
428: if (offset < 0)
429: offset = 0;
430: else if (offset > line.length())
431: offset = line.length();
432: editor.setDot(line, offset);
433: editor.update(editor.getDotLine());
434:
435: // Make sure end tag is visible if possible.
436: Position end = findMatchingEndTagOnSameLine(name, editor
437: .getDot());
438:
439: if (end != null)
440: end.skip(name.length() + 3); // "</" + name + ">"
441: else
442: end = new Position(line, line.length() - 1);
443: int absCol = buffer.getCol(end);
444: editor.getDisplay().ensureColumnVisible(line, absCol);
445: editor.moveCaretToDotCol();
446: editor.updateDisplay();
447: }
448: }
449:
450: private void moveDotToNodeAtPoint(Point point) {
451: TreePath path = getPathForLocation(point.x, point.y);
452: if (path != null) {
453: DefaultMutableTreeNode node = (DefaultMutableTreeNode) path
454: .getLastPathComponent();
455: moveDotToNode(node);
456: }
457: }
458:
459: public DefaultMutableTreeNode getNodeAtPos(Position where) {
460: if (treeModel == null)
461: return null;
462: DefaultMutableTreeNode root = (DefaultMutableTreeNode) treeModel
463: .getRoot();
464: if (root == null)
465: return null;
466:
467: // Search backwards from starting point to find nearest '<' (start of
468: // current node).
469: Position pos = new Position(where);
470: while (pos.getChar() != '<')
471: if (!pos.prev())
472: break;
473:
474: // Skip past '<'.
475: pos.next();
476:
477: // One more for good measure. (Aelfred is sloppy!)
478: pos.next();
479:
480: // The starting location reported by the parser and stored in the tree
481: // element refers (in theory) to the position just past the end of the
482: // start tag. We want to find the node whose reported starting
483: // location is after pos, but closest to it.
484:
485: // Our line numbers and offsets are zero-based.
486: int targetLineNumber = pos.lineNumber() + 1;
487: int targetColumnNumber = pos.getOffset() + 1;
488:
489: DefaultMutableTreeNode currentNode = null;
490: int currentLineDelta = Integer.MAX_VALUE;
491: int currentColumnDelta = Integer.MAX_VALUE;
492: Enumeration nodes = root.depthFirstEnumeration();
493: while (nodes.hasMoreElements()) {
494: DefaultMutableTreeNode node = (DefaultMutableTreeNode) nodes
495: .nextElement();
496: XmlTreeElement treeElement = (XmlTreeElement) node
497: .getUserObject();
498: int lineDelta = treeElement.getLineNumber()
499: - targetLineNumber;
500:
501: // We want the smallest lineDelta >= 0.
502: if (lineDelta >= 0 && lineDelta < currentLineDelta) {
503: currentNode = node;
504: currentLineDelta = lineDelta;
505: }
506: if (lineDelta == 0 && currentLineDelta == 0) {
507: int columnDelta = treeElement.getColumnNumber()
508: - targetColumnNumber;
509:
510: // We want the smallest columnDelta >= 0.
511: if (columnDelta >= 0
512: && columnDelta < currentColumnDelta) {
513: currentNode = node;
514: currentColumnDelta = columnDelta;
515: }
516: }
517: }
518: return currentNode;
519: }
520:
521: private int findStartTag(String name, Line line, int start) {
522: final String lookFor = '<' + name;
523: final int length = lookFor.length();
524: while (start + length < line.length()) {
525: int index = line.getText().indexOf(lookFor, start);
526: if (index < 0)
527: return index; // Not found.
528: int end = index + length;
529: if (end < line.length()) {
530: char c = line.charAt(end);
531: if (c == '/' || c == '>' || c <= ' ')
532: return index;
533: }
534: start = index + 1;
535: }
536: return -1; // Not found.
537: }
538:
539: private int reverseFindStartTag(String name, Line line, int start) {
540: final String lookFor = '<' + name;
541: final int length = lookFor.length();
542: while (start >= 0) {
543: int index = line.getText().lastIndexOf(lookFor, start);
544: if (index < 0)
545: return index; // Not found.
546: int end = index + length;
547: if (end < line.length()) {
548: char c = line.charAt(end);
549: if (c == '/' || c == '>' || c <= ' ')
550: return index;
551: } else
552: return index;
553: start = index - lookFor.length();
554: }
555: return -1; // Not found.
556: }
557:
558: private static final String COMMENT_START = "<!--";
559: private static final String COMMENT_END = "-->";
560:
561: private Position findMatchingEndTagOnSameLine(String name,
562: Position start) {
563: String toBeMatched = "<" + name;
564: String match = "</" + name + ">";
565: int count = 1;
566: Position pos = new Position(start);
567: pos.skip(toBeMatched.length());
568: int limit = pos.getLineLength();
569: while (pos.getOffset() < limit) {
570: if (pos.lookingAt(COMMENT_START)) {
571: pos.skip(COMMENT_START.length());
572: while (pos.getOffset() < limit) {
573: if (pos.lookingAt(COMMENT_END)) {
574: pos.skip(COMMENT_END.length());
575: break;
576: }
577: pos.skip(1);
578: }
579: } else if (pos.lookingAtIgnoreCase(toBeMatched)) {
580: pos.skip(toBeMatched.length());
581: char c = pos.getChar();
582: if (c <= ' ' || c == '>') {
583: ++count;
584: pos.skip(1);
585: }
586: } else if (pos.lookingAtIgnoreCase(match)) {
587: --count;
588: if (count == 0)
589: return pos;
590: pos.skip(match.length());
591: } else
592: pos.skip(1);
593: }
594: return null;
595: }
596:
597: private static class XmlTreeCellRenderer extends
598: DefaultTreeCellRenderer {
599: private XmlTree tree;
600: private Editor editor;
601:
602: private static Color noFocusSelectionBackground = new Color(
603: 208, 208, 208);
604:
605: private Color oldBackgroundSelectionColor;
606:
607: public XmlTreeCellRenderer(XmlTree tree) {
608: super ();
609: this .tree = tree;
610: editor = tree.getEditor();
611: oldBackgroundSelectionColor = getBackgroundSelectionColor();
612: setOpenIcon(Utilities.getIconFromFile("branch.png"));
613: setClosedIcon(Utilities.getIconFromFile("branch.png"));
614: setLeafIcon(Utilities.getIconFromFile("leaf.png"));
615: }
616:
617: public Component getTreeCellRendererComponent(JTree tree,
618: Object value, boolean selected, boolean expanded,
619: boolean leaf, int row, boolean hasFocus) {
620: super .getTreeCellRendererComponent(tree, value, selected,
621: expanded, leaf, row, hasFocus);
622: if (selected)
623: super .setForeground(getTextSelectionColor());
624: else
625: super .setForeground(getTextNonSelectionColor());
626: if (editor.getFocusedComponent() == tree)
627: setBackgroundSelectionColor(oldBackgroundSelectionColor);
628: else
629: setBackgroundSelectionColor(noFocusSelectionBackground);
630: return this ;
631: }
632:
633: public void paintComponent(Graphics g) {
634: Display.setRenderingHints(g);
635: super.paintComponent(g);
636: }
637: }
638: }
|