/* * Sun Public License Notice * * The contents of this file are subject to the Sun Public License * Version 1.0 (the "License"). You may not use this file except in * compliance with the License. A copy of the License is available at * http://www.sun.com/ * * The Original Code is NetBeans. The Initial Developer of the Original * Code is Sun Microsystems, Inc. Portions Copyright 1997-2000 Sun * Microsystems, Inc. All Rights Reserved. */ package org.openide.text; import java.io.*; import java.util.*; import javax.swing.text.*; import javax.swing.event.*; import javax.swing.SwingUtilities; import org.openide.ErrorManager; import org.openide.loaders.DataObject; import org.openide.util.WeakListener; import org.openide.text.EnhancedChangeEvent; /** Implementation of a line in a {@link StyledDocument}. * One object * of this class represents a line in the document by holding * a {@link PositionRef}, which can represent a position in an open or * closed document. * * @author Jaroslav Tulach, David Konecny */ public abstract class DocumentLine extends Line { /** reference to one position on the line */ protected PositionRef pos; /** is breakpoint there - presistent state @deprecated since 1.20 */ private boolean breakpoint; /** error line - transient state @deprecated since 1.20 */ private transient boolean error; /** current line - transient state @deprecated since 1.20 */ private transient boolean current; /** listener for changes of state of the document */ private transient LR listener; /** weak document listener assigned to the document or null */ private transient DocumentListener docL; /** weak map that assignes to editor supports whether they have current or error line * selected. (EditorSupport, DocumentLine[2]), where Line[0] is current and Line[1] is error */ private static WeakHashMap assigned = new WeakHashMap (5); /** List of Line.Part which exist for this line*/ private List lineParts = new ArrayList(3); static final long serialVersionUID =3213776466939427487L; /** Constructor. * @param obj data object we belong to * @param pos position on the line */ public DocumentLine (DataObject obj, PositionRef pos) { super (obj); this.pos = pos; } /** Init listeners */ void init () { listener = new LR (); pos.getCloneableEditorSupport ().addChangeListener (WeakListener.change (listener, pos.getCloneableEditorSupport ())); } /* Get the line number. * The number may change if the * text is modified. * * @return Returns current line number. */ public int getLineNumber () { try { return pos.getLine (); } catch (IOException ex) { // what else? return 0; } } /* Shows the line. * @param kind one of SHOW_XXX constants. * @column the column of this line which should be selected */ public abstract void show(int kind, int column); /* Sets the breakpoint. */ public void setBreakpoint(boolean b) { if (breakpoint != b) { breakpoint = b; refreshState (); } } /* Tests if the breakpoint is set. */ public boolean isBreakpoint () { return breakpoint; } /* Marks the error. */ public void markError () { DocumentLine previous = registerLine (1, this); if (previous != null) { previous.error = false; previous.refreshState (); } error = true; refreshState (); } /* Unmarks error at this line. */ public void unmarkError () { error = false; registerLine (1, null); refreshState (); } /* Marks this line as current. */ public void markCurrentLine () { DocumentLine previous = registerLine (0, this); if (previous != null) { previous.current = false; previous.refreshState (); } current = true; refreshState (); } /* Unmarks this line as current. */ public void unmarkCurrentLine () { current = false; registerLine (0, null); refreshState (); } /** Refreshes the current line. * * @deprecated since 1.20. */ synchronized void refreshState () { StyledDocument doc = pos.getCloneableEditorSupport ().getDocument (); if (doc != null) { // the document is in memory, mark the state if (docL != null) { doc.removeDocumentListener (docL); } // error line if (error) { NbDocument.markError (doc, pos.getOffset ()); doc.addDocumentListener (docL = WeakListener.document (listener, doc)); return; } // current line if (current) { NbDocument.markCurrent (doc, pos.getOffset ()); return; } // breakpoint line if (breakpoint) { NbDocument.markBreakpoint (doc, pos.getOffset ()); return; } NbDocument.markNormal (doc, pos.getOffset ()); return; } } public int hashCode () { return pos.getCloneableEditorSupport ().hashCode (); } public boolean equals (Object o) { if (o instanceof DocumentLine) { DocumentLine dl = (DocumentLine)o; if (dl.pos.getCloneableEditorSupport () == pos.getCloneableEditorSupport ()) { return dl.getLineNumber () == getLineNumber (); } } return false; } // // Work with global hash table // /** Register this line as the one stored * under indx-index (0 = current, 1 = error). * * @param indx index to register * @param line value to add (this or null) * @return the previous value * * @deprecated since 1.20 */ private DocumentLine registerLine (int indx, DocumentLine line) { DocumentLine prev; CloneableEditorSupport es = pos.getCloneableEditorSupport (); DocumentLine[] arr = (DocumentLine[])assigned.get (es); if (arr != null) { // remember the previous prev = arr[indx]; } else { // create new array arr = new DocumentLine[2]; assigned.put (es, arr); prev = null; } arr[indx] = line; return prev; } // // Serialization // /** Write fields. */ private void writeObject (ObjectOutputStream oos) throws IOException { // do not do default read/write object oos.writeObject (pos); oos.writeBoolean (breakpoint); } /** Read important fields. */ private void readObject (ObjectInputStream ois) throws IOException, ClassNotFoundException { pos = (PositionRef)ois.readObject (); setBreakpoint (ois.readBoolean ()); lineParts = new ArrayList(3); } /** Register line. */ Object readResolve() throws ObjectStreamException { // return Set.registerLine (this); //Set.registerPendingLine(this); return this.pos.getCloneableEditorSupport().getLineSet().registerLine(this); } /** Add annotation to this Annotatable class * @param anno annotation which will be attached to this class */ protected void addAnnotation(Annotation anno) { super.addAnnotation(anno); StyledDocument doc = pos.getCloneableEditorSupport ().getDocument (); // document is not opened and so the annotation will be added to document later if (doc == null) return; pos.getCloneableEditorSupport().prepareDocument().waitFinished(); try { if (!anno.isInDocument()) { anno.setInDocument(true); NbDocument.addAnnotation (doc, pos.getPosition(), -1, anno); } } catch (IOException ex) { ErrorManager.getDefault ().notify ( ErrorManager.EXCEPTION, ex); } } /** Remove annotation to this Annotatable class * @param anno annotation which will be detached from this class */ protected void removeAnnotation(Annotation anno) { super.removeAnnotation(anno); StyledDocument doc = pos.getCloneableEditorSupport ().getDocument (); // document is not opened and so no annotation is attached to it if (doc == null) return; pos.getCloneableEditorSupport().prepareDocument().waitFinished(); if (anno.isInDocument()) { anno.setInDocument(false); NbDocument.removeAnnotation(doc, anno); } } /** When document is opened or closed the annotations must be added or * removed. * @since 1.27 */ void attachDetachAnnotations(StyledDocument doc, boolean closing) { java.util.List list = getAnnotations(); for (int i=0; i<list.size(); i++) { Annotation anno = (Annotation)list.get(i); if (!closing) { try { if (!anno.isInDocument()) { anno.setInDocument(true); NbDocument.addAnnotation (doc, pos.getPosition(), -1, anno); } } catch (IOException ex) { ErrorManager.getDefault ().notify ( ErrorManager.EXCEPTION, ex); } } else { if (anno.isInDocument()) { anno.setInDocument(false); NbDocument.removeAnnotation(doc, anno); } } } // notify also all Line.Part attached to this Line for (int i=0; i<lineParts.size(); i++) { ((DocumentLine.Part)lineParts.get(i)).attachDetachAnnotations(doc, closing); } } public String getText() { StyledDocument doc = pos.getCloneableEditorSupport ().getDocument (); // document is not opened if (doc == null) return null; int lineNumber = getLineNumber(); int lineStart = NbDocument.findLineOffset(doc, lineNumber); // #24434: Check whether the next line exists // (the current one could be the last one). int lineEnd; if((lineNumber + 1) >= NbDocument.findLineRootElement(doc).getElementCount()) { lineEnd = doc.getLength(); } else { lineEnd = NbDocument.findLineOffset(doc, lineNumber + 1); } try { return doc.getText(lineStart, lineEnd - lineStart); } catch (BadLocationException ex) { ErrorManager.getDefault ().notify ( ErrorManager.EXCEPTION, ex); return null; } } /** Attach created Line.Part to the parent Line */ void addLinePart(DocumentLine.Part linePart) { lineParts.add(linePart); } /** Move Line.Part from this Line to a new one*/ void moveLinePart(DocumentLine.Part linePart, DocumentLine newLine) { lineParts.remove(linePart); newLine.addLinePart(linePart); linePart.changeLine(newLine); } /** Notify Line.Part(s) that content of the line was changed and that Line.Part(s) may be affected by that*/ void notifyChange(DocumentEvent p0, DocumentLine.Set set, StyledDocument doc) { DocumentLine.Part part; for (int i=0; i<lineParts.size(); ) { part = (DocumentLine.Part)lineParts.get(i); // notify Line.Part about the change part.handleDocumentChange(p0); // if necessary move Line.Part to new Line if (NbDocument.findLineNumber(doc, part.getOffset()) != part.getLine().getLineNumber()) { DocumentLine line = (DocumentLine)set.getCurrent(NbDocument.findLineNumber(doc, part.getOffset())); moveLinePart(part, line); } else { i++; } } } /** Notify Line.Part(s) that line was moved. */ void notifyMove() { updatePositionRef(); for (int i=0; i<lineParts.size(); i++) { ((DocumentLine.Part)lineParts.get(i)).firePropertyChange(Line.Part.PROP_LINE, null, null); } } /** Updates <code>pos</code> the way it points at the start of line. */ private void updatePositionRef() { CloneableEditorSupport support = pos.getCloneableEditorSupport(); int startOffset = NbDocument.findLineOffset(support.getDocument(), getLineNumber()); if(pos.getOffset() != startOffset) { pos = new PositionRef( support.getPositionManager(), startOffset, Position.Bias.Forward ); } } /** Implementation of Line.Part abstract class*/ static class Part extends Line.Part { /** Reference of this part to the document*/ private PositionRef position; /** Reference to Line to which this part belongs*/ private Line line; /** Length of the annotated text*/ private int length; /** Offset of this Part before the modification. This member is used in * listener on document changes and it is updated after each change. */ private int previousOffset; public Part (Line line, PositionRef position, int length) { this.position = position; this.line = line; this.length = length; previousOffset = position.getOffset(); } /** Start column of annotation */ public int getColumn() { try { return position.getColumn(); } catch (IOException ex) { return 0; //TODO: change this } } /** Length of the annotated text. The length does not cross line end. If the annotated text is * split during the editing, the annotation is shorten till the end of the line. Modules can listen on * changes of this value*/ public int getLength() { return length; } /** Line can change during editting*/ public Line getLine() { return line; } /** Offset of the Line.Part*/ int getOffset() { return position.getOffset(); } /** Line can change during editting*/ void changeLine(Line line) { this.line = line; // TODO: check whether there is really some change firePropertyChange (PROP_LINE_NUMBER, null, line); } /** Add annotation to this Annotatable class * @param anno annotation which will be attached to this class */ protected void addAnnotation(Annotation anno) { super.addAnnotation(anno); StyledDocument doc = position.getCloneableEditorSupport ().getDocument (); // document is not opened and so the annotation will be added to document later if (doc == null) return; position.getCloneableEditorSupport().prepareDocument().waitFinished(); try { if (!anno.isInDocument()) { anno.setInDocument(true); NbDocument.addAnnotation(doc, position.getPosition(), length, anno); } } catch (IOException ex) { ErrorManager.getDefault ().notify ( ErrorManager.EXCEPTION, ex); } } /** Remove annotation to this Annotatable class * @param anno annotation which will be detached from this class */ protected void removeAnnotation(Annotation anno) { super.removeAnnotation(anno); StyledDocument doc = position.getCloneableEditorSupport ().getDocument (); // document is not opened and so no annotation is attached to it if (doc == null) return; position.getCloneableEditorSupport().prepareDocument().waitFinished(); if (anno.isInDocument()) { anno.setInDocument(false); NbDocument.removeAnnotation(doc, anno); } } public String getText() { StyledDocument doc = position.getCloneableEditorSupport ().getDocument (); // document is not opened if (doc == null) return null; try { return doc.getText(position.getOffset(), getLength()); } catch (BadLocationException ex) { ErrorManager.getDefault ().notify ( ErrorManager.EXCEPTION, ex); return null; } } /** When document is opened or closed the annotations must be added or * removed.*/ void attachDetachAnnotations(StyledDocument doc, boolean closing) { java.util.List list = getAnnotations(); for (int i=0; i<list.size(); i++) { Annotation anno = (Annotation)list.get(i); if (!closing) { try { if (!anno.isInDocument()) { anno.setInDocument(true); NbDocument.addAnnotation (doc, position.getPosition(), getLength(), anno); } } catch (IOException ex) { ErrorManager.getDefault ().notify ( ErrorManager.EXCEPTION, ex); } } else { if (anno.isInDocument()) { anno.setInDocument(false); NbDocument.removeAnnotation(doc, anno); } } } } /** Handle DocumentChange event. If the change affect this Part, fire * the PROP_TEXT event. */ void handleDocumentChange(DocumentEvent p0) { if (p0.getType().equals(DocumentEvent.EventType.INSERT)) { if (p0.getOffset() >= previousOffset && p0.getOffset() < (previousOffset+getLength()) ) { firePropertyChange(Annotatable.PROP_TEXT, null, null); } } if (p0.getType().equals(DocumentEvent.EventType.REMOVE)) { if ( (p0.getOffset() >= previousOffset && p0.getOffset() < previousOffset+getLength()) || (p0.getOffset() < previousOffset && p0.getOffset()+p0.getLength() > previousOffset) ) { firePropertyChange(Annotatable.PROP_TEXT, null, null); } } if ((p0.getType().equals(DocumentEvent.EventType.INSERT) || p0.getType().equals(DocumentEvent.EventType.REMOVE)) && p0.getOffset() < previousOffset) { firePropertyChange(Line.Part.PROP_COLUMN, null, null); } previousOffset = position.getOffset(); } } /** Definition of actions performed in Listener */ private final class LR implements Runnable, ChangeListener, DocumentListener { private static final int REFRESH = 0; private static final int UNMARK = 1; private static final int ATTACH_DETACH = 2; private int actionId; private EnhancedChangeEvent ev; public LR() {} public LR (int actionId) { this.actionId = actionId; } public LR (EnhancedChangeEvent ev) { this.actionId = ATTACH_DETACH; this.ev = ev; } public void run () { switch (actionId) { case REFRESH: refreshState (); break; case UNMARK: unmarkError (); break; case ATTACH_DETACH: attachDetachAnnotations(ev.getDocument(), ev.isClosingDocument()); ev = null; break; } } private void invoke(int op) { SwingUtilities.invokeLater(new LR(op)); } private void invoke(EnhancedChangeEvent ev) { SwingUtilities.invokeLater(new LR(ev)); } public void stateChanged (ChangeEvent ev) { invoke(REFRESH); invoke((EnhancedChangeEvent)ev); } public void removeUpdate(final javax.swing.event.DocumentEvent p0) { invoke(UNMARK); } public void insertUpdate(final javax.swing.event.DocumentEvent p0) { invoke(UNMARK); } public void changedUpdate(final javax.swing.event.DocumentEvent p0) { } } /** Abstract implementation of {@link Line.Set}. * Defines * ways to obtain a line set for documents following * NetBeans conventions. */ public static abstract class Set extends Line.Set { /** listener on document changes */ private final LineListener listener; /** all lines in the set or null */ private java.util.List list; /** Constructor. * @param doc document to work on */ public Set (StyledDocument doc) { this(doc, null); } Set (StyledDocument doc, CloneableEditorSupport support) { listener = new LineListener (doc, support); } /** Find the line given as parameter in list of all lines attached to this set * and if the line exist in the list, notify it about being edited. */ void linesChanged(int startLineNumber, int endLineNumber, DocumentEvent p0) { List changedLines = getLinesFromRange(startLineNumber, endLineNumber); for(Iterator it = changedLines.iterator(); it.hasNext(); ) { Line line = (Line)it.next(); line.firePropertyChange(Annotatable.PROP_TEXT, null, null); // revalidate all parts attached to this line // that they are still part of the line if(line instanceof DocumentLine) { ((DocumentLine)line).notifyChange(p0, this, listener.doc); } } } /** Find the line given as parameter in list of all lines attached to this set * and if the line exist in the list, notify it about being moved. */ void linesMoved(int startLineNumber, int endLineNumber) { List movedLines = getLinesFromRange(startLineNumber, endLineNumber); for(Iterator it = movedLines.iterator(); it.hasNext(); ) { Line line = (Line)it.next(); line.firePropertyChange(Line.PROP_LINE_NUMBER, null, null); // notify all parts attached to this line // that they were moved if (line instanceof DocumentLine) { ((DocumentLine)line).notifyMove(); } } } /** Gets the lines with line number whitin the range inclusive. * @return <code>List</code> of lines from range inclusive */ private List getLinesFromRange(int startLineNumber, int endLineNumber) { List linesInRange = new ArrayList(10); synchronized(lines) { for(Iterator it = lines.keySet().iterator(); it.hasNext(); ) { Line line = (Line)it.next(); int lineNumber = line.getLineNumber(); if(startLineNumber <= lineNumber && lineNumber <= endLineNumber) { linesInRange.add(line); } } } return linesInRange; } /* Returns an unmodifiable set of Lines sorted by their * line numbers that contains all lines holded by this * Line.Set. * * @return list of Line objects */ public java.util.List getLines () { if (list == null) { int cnt = listener.getOriginalLineCount (); java.util.List l = new java.util.LinkedList (); for (int i = 0; i < cnt; i++) { l.add (getOriginal (i)); } list = l; } return list; } /* Finder method that for the given line number finds right * Line object that represent as closely as possible the line number * in the time when the Line.Set has been created. * * @param line is a number of the line (text line) we want to acquire * @exception IndexOutOfBoundsException if <code>line</code> is invalid. */ public Line getOriginal (int line) throws IndexOutOfBoundsException { int newLine = listener.getLine (line); int offset = NbDocument.findLineOffset (listener.doc, newLine); return this.registerLine(createLine(offset)); } /* Creates current line. * * @param line is a number of the line (text line) we want to acquire * @exception IndexOutOfBoundsException if <code>line</code> is invalid. */ public Line getCurrent (int line) throws IndexOutOfBoundsException { int offset = NbDocument.findLineOffset (listener.doc, line); return this.registerLine(createLine(offset)); } /** Creates a {@link Line} for a given offset. * @param offset the beginning offset of the line * @return line object representing the line at this offset */ protected abstract Line createLine (int offset); } }