blob: b66b3117d6d30cd67320b9f822096f05dd37fe82 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2005 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*
*******************************************************************************/
package org.eclipse.wst.dtd.ui.internal.projection;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentExtension;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.ITextInputListener;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.source.projection.IProjectionListener;
import org.eclipse.jface.text.source.projection.ProjectionAnnotation;
import org.eclipse.jface.text.source.projection.ProjectionAnnotationModel;
import org.eclipse.jface.text.source.projection.ProjectionViewer;
import org.eclipse.swt.graphics.FontMetrics;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.wst.dtd.core.internal.DTDFile;
import org.eclipse.wst.dtd.core.internal.DTDNode;
import org.eclipse.wst.dtd.core.internal.TopLevelNode;
import org.eclipse.wst.dtd.core.internal.document.DTDModelImpl;
import org.eclipse.wst.dtd.core.internal.event.IDTDFileListener;
import org.eclipse.wst.dtd.core.internal.event.NodesEvent;
import org.eclipse.wst.dtd.ui.internal.Logger;
import org.eclipse.wst.sse.core.StructuredModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
import org.eclipse.wst.sse.ui.internal.projection.IStructuredTextFoldingProvider;
import org.w3c.dom.Node;
/**
* Updates the projection model of a structured model for DTD.
*/
public class StructuredTextFoldingProviderDTD implements IStructuredTextFoldingProvider, IProjectionListener, IDTDFileListener, ITextInputListener {
private final static boolean debugProjectionPerf = "true".equalsIgnoreCase(Platform.getDebugOption("org.eclipse.wst.dtd.ui/projectionperf")); //$NON-NLS-1$ //$NON-NLS-2$
private class TagProjectionAnnotation extends ProjectionAnnotation {
private boolean fIsVisible = false; /* workaround for BUG85874 */
private Node fNode;
public TagProjectionAnnotation(Node node, boolean isCollapsed) {
super(isCollapsed);
fNode = node;
}
public Node getNode() {
return fNode;
}
public void setNode(Node node) {
fNode = node;
}
/**
* Does not paint hidden annotations. Annotations are hidden when they
* only span one line.
*
* @see ProjectionAnnotation#paint(org.eclipse.swt.graphics.GC,
* org.eclipse.swt.widgets.Canvas,
* org.eclipse.swt.graphics.Rectangle)
*/
public void paint(GC gc, Canvas canvas, Rectangle rectangle) {
/* workaround for BUG85874 */
/*
* only need to check annotations that are expanded because hidden
* annotations should never have been given the chance to
* collapse.
*/
if (!isCollapsed()) {
// working with rectangle, so line height
FontMetrics metrics = gc.getFontMetrics();
if (metrics != null) {
// do not draw annotations that only span one line and
// mark them as not visible
if ((rectangle.height / metrics.getHeight()) <= 1) {
fIsVisible = false;
return;
}
}
}
fIsVisible = true;
super.paint(gc, canvas, rectangle);
}
/*
* (non-Javadoc)
*
* @see org.eclipse.jface.text.source.projection.ProjectionAnnotation#markCollapsed()
*/
public void markCollapsed() {
/* workaround for BUG85874 */
// do not mark collapsed if annotation is not visible
if (fIsVisible)
super.markCollapsed();
}
}
/**
* Listens to document to be aware of when to update the projection
* annotation model.
*/
class DocumentListener implements IDocumentListener {
public void documentAboutToBeChanged(DocumentEvent event) {
if (fDocument == event.getDocument())
fIsDocumentChanging = true;
}
public void documentChanged(DocumentEvent event) {
// register a post notification replace so that projection
// annotation model will be updated after all documentChanged
// listeners have been notified
IDocument document = event.getDocument();
if (document instanceof IDocumentExtension && fDocument == document) {
if (fViewer != null && fQueuedNodeChanges != null && !fQueuedNodeChanges.isEmpty()) {
((IDocumentExtension) document).registerPostNotificationReplace(this, new PostDocumentChangedListener());
}
}
}
}
/**
* Essentially a post document changed listener because it is called after
* documentchanged has been fired.
*/
class PostDocumentChangedListener implements IDocumentExtension.IReplace {
public void perform(IDocument document, IDocumentListener owner) {
applyAnnotationModelChanges();
fIsDocumentChanging = false;
}
}
/**
* Contains node and an indication on how it changed
*/
class NodeChange {
static final int ADD = 1;
static final int REMOVE = 2;
private Node fNode;
private int fChangeType;
public NodeChange(Node node, int changeType) {
fNode = node;
fChangeType = changeType;
}
public Node getNode() {
return fNode;
}
public int getChangeType() {
return fChangeType;
}
}
IDocument fDocument;
ProjectionViewer fViewer;
private boolean fProjectionNeedsToBeEnabled = false;
/**
* Listener to fProjectionViewer's document
*/
private IDocumentListener fDocumentListener;
/**
* Indicates whether or not document is in the middle of changing
*/
boolean fIsDocumentChanging = false;
/**
* List of changed nodes that need to be recalculated
*/
List fQueuedNodeChanges = null;
/**
* Processes all the queued node changes and updates projection annotation
* model.
*/
void applyAnnotationModelChanges() {
if (fViewer != null && fQueuedNodeChanges != null && !fQueuedNodeChanges.isEmpty()) {
ProjectionAnnotationModel annotationModel = fViewer.getProjectionAnnotationModel();
// go through all the pending annotation changes and apply them to
// the projection annotation model
while (!fQueuedNodeChanges.isEmpty()) {
NodeChange changes = (NodeChange) fQueuedNodeChanges.remove(0);
if (changes.getChangeType() == NodeChange.ADD) {
// add
Node node = changes.getNode();
Position newPos = createProjectionPosition(node);
if (newPos != null) {
TagProjectionAnnotation newAnnotation = new TagProjectionAnnotation(node, false);
// add to map containing annotations to add
try {
annotationModel.addAnnotation(newAnnotation, newPos);
}
catch (Exception e) {
// if anything goes wrong, log it and continue
Logger.log(Logger.WARNING_DEBUG, e.getMessage(), e);
}
}
}
else if (changes.getChangeType() == NodeChange.REMOVE) {
// remove
Node node = changes.getNode();
TagProjectionAnnotation anno = findExistingAnnotation(node);
if (anno != null) {
try {
annotationModel.removeAnnotation(anno);
}
catch (Exception e) {
// if anything goes wrong, log it and continue
Logger.log(Logger.WARNING_DEBUG, e.getMessage(), e);
}
}
}
}
fQueuedNodeChanges = null;
}
}
/**
* Goes through every node creates projection annotation if needed
*
* @param DTDFile
* assumes file is not null
*/
private void addAllAnnotations(DTDFile file) {
long start = System.currentTimeMillis();
List nodes = file.getNodes();
Iterator it = nodes.iterator();
while (it.hasNext()) {
DTDNode node = (DTDNode) it.next();
Position newPos = createProjectionPosition(node);
if (newPos != null) {
TagProjectionAnnotation newAnnotation = new TagProjectionAnnotation(node, false);
// add to map containing annotations to add
fViewer.getProjectionAnnotationModel().addAnnotation(newAnnotation, newPos);
}
}
if (debugProjectionPerf) {
long end = System.currentTimeMillis();
System.out.println("StructuredTextFoldingProviderDTD.addAllAnnotations: " + (end - start)); //$NON-NLS-1$
}
}
/**
* Create a projection position from the given node. Able to get
* projection position if node isNodeProjectable.
*
* @param node
* @return null if no projection position possible, a Position otherwise
*/
private Position createProjectionPosition(Node node) {
Position pos = null;
if (isNodeProjectable(node) && node instanceof IndexedRegion) {
IDocument document = fViewer.getDocument();
if (document != null) {
IndexedRegion inode = (IndexedRegion) node;
int start = inode.getStartOffset();
int end = inode.getEndOffset();
if (start >= 0 && start < end) {
pos = new Position(start, end - start);
}
}
}
return pos;
}
/**
* Searches through projection annotation model and retrieves
* TagProjectionAnnotation for node
*
* @param node
* @return TagProjectionAnnotation for node or null if could not be found
*/
private TagProjectionAnnotation findExistingAnnotation(Node node) {
TagProjectionAnnotation anno = null;
if (node != null) {
Iterator it = fViewer.getProjectionAnnotationModel().getAnnotationIterator();
while (it.hasNext() && anno == null) {
TagProjectionAnnotation a = (TagProjectionAnnotation) it.next();
if (node.equals(a.getNode()))
anno = a;
}
}
return anno;
}
/**
* Get the dtd file for the fDocument
*
* @return DTDFile if it exists, null otherwise
*/
private DTDFile getDTDFile() {
DTDFile dtdFile = null;
if (fDocument != null) {
IStructuredModel sModel = null;
try {
sModel = StructuredModelManager.getModelManager().getExistingModelForRead(fDocument);
if (sModel instanceof DTDModelImpl) {
dtdFile = ((DTDModelImpl) sModel).getDTDFile();
}
}
finally {
if (sModel != null) {
sModel.releaseFromRead();
}
}
}
return dtdFile;
}
/**
* Initialize this provider with the correct document. Assumes projection
* is enabled. (otherwise, only install would have been called)
*/
public void initialize() {
if (!isInstalled())
return;
// remove old info
projectionDisabled();
fDocument = fViewer.getDocument();
DTDFile file = getDTDFile();
if (fDocument != null && file != null && fViewer.getProjectionAnnotationModel() != null) {
if (fDocumentListener == null)
fDocumentListener = new DocumentListener();
fDocument.addDocumentListener(fDocumentListener);
// add dtd file listener to new dtd file
file.addDTDFileListener(this);
addAllAnnotations(file);
}
fProjectionNeedsToBeEnabled = false;
}
/**
* Associate a ProjectionViewer with this IStructuredTextFoldingProvider
*
* @param viewer
*/
public void install(ProjectionViewer viewer) {
// uninstall before trying to install new viewer
if (isInstalled()) {
uninstall();
}
fViewer = viewer;
fViewer.addProjectionListener(this);
fViewer.addTextInputListener(this);
}
private boolean isInstalled() {
return fViewer != null;
}
/**
* Returns true if node is a node type able to fold
*
* @param node
* @return boolean true if node is projectable, false otherwise
*/
private boolean isNodeProjectable(Node node) {
if (node != null) {
if (node instanceof TopLevelNode)
return true;
}
return false;
}
public void nodeChanged(DTDNode node) {
/*
* Don't believe this is necessary (used to need it to only add
* projection annotations to elements that span more than one line,
* but now just always add projection annotation)
*/
// long start = System.currentTimeMillis();
// // recalculate projection annotations for node
// // check if this was even a projectable node to start with
// if (isNodeProjectable(node)) {
// // find the existing annotation
// TagProjectionAnnotation anno = findExistingAnnotation(node);
// // if able to project node see if projection annotation was
// // already created and create new if needed
// Position newPos = createProjectionPosition(node);
// if (newPos != null && anno == null) {
// TagProjectionAnnotation newAnnotation = new
// TagProjectionAnnotation(node, false);
// // add to map containing annotations to add
// fViewer.getProjectionAnnotationModel().addAnnotation(newAnnotation,
// newPos);
// }
// // if not able to project node see if projection annotation was
// // already created and remove it
// if (newPos == null && anno != null) {
// fViewer.getProjectionAnnotationModel().removeAnnotation(anno);
// }
// }
//
// long end = System.currentTimeMillis();
// if (debugProjectionPerf) {
// String nodeName = node != null ? node.getNodeName() : "null";
// //$NON-NLS-1$
// System.out.println("StructuredTextFoldingProviderDTD.nodeChanged ("
// + nodeName + "):" + (end - start)); //$NON-NLS-1$ //$NON-NLS-2$
// }
}
public void nodesAdded(NodesEvent event) {
long start = System.currentTimeMillis();
processNodesEvent(event, NodeChange.ADD);
if (debugProjectionPerf) {
long end = System.currentTimeMillis();
System.out.println("StructuredTextFoldingProviderDTD.nodesAdded: " + (end - start)); //$NON-NLS-1$
}
}
/**
* Goes through every changed node in event and add to queue of node
* changes that will be recalculated for projection annotation model.
*
* @param event
* @param changeType
*/
private void processNodesEvent(NodesEvent event, int changeType) {
List nodes = event.getNodes();
Iterator it = nodes.iterator();
while (it.hasNext()) {
DTDNode node = (DTDNode) it.next();
if (isNodeProjectable(node)) {
if (fQueuedNodeChanges == null) {
fQueuedNodeChanges = new ArrayList();
}
int existingIndex = fQueuedNodeChanges.indexOf(node);
if (existingIndex > -1) {
// node is already queued up
NodeChange existingChange = (NodeChange) fQueuedNodeChanges.remove(existingIndex);
// don't add if added then removed node or vice versa
if (existingChange.getChangeType() == changeType) {
NodeChange newChange = new NodeChange(node, changeType);
fQueuedNodeChanges.add(newChange);
}
}
else {
// not queued up yet, so queue node
NodeChange newChange = new NodeChange(node, changeType);
fQueuedNodeChanges.add(newChange);
}
}
}
// if document isn't changing, go ahead and apply it
if (!fIsDocumentChanging) {
applyAnnotationModelChanges();
}
}
public void nodesRemoved(NodesEvent event) {
long start = System.currentTimeMillis();
processNodesEvent(event, NodeChange.REMOVE);
if (debugProjectionPerf) {
long end = System.currentTimeMillis();
System.out.println("StructuredTextFoldingProviderDTD.nodesRemoved: " + (end - start)); //$NON-NLS-1$
}
}
public void projectionDisabled() {
DTDFile file = getDTDFile();
if (file != null) {
file.removeDTDFileListener(this);
}
// remove document listener
if (fDocumentListener != null && fDocument != null) {
fDocument.removeDocumentListener(fDocumentListener);
fDocument = null;
// clear out list of queued changes since it may no longer be
// accurate
if (fQueuedNodeChanges != null) {
fQueuedNodeChanges.clear();
fQueuedNodeChanges = null;
}
}
fDocument = null;
fProjectionNeedsToBeEnabled = false;
}
public void projectionEnabled() {
initialize();
}
public void inputDocumentAboutToBeChanged(IDocument oldInput, IDocument newInput) {
// if folding is enabled and new document is going to be a totally
// different document, disable projection
if (fDocument != null && fDocument != newInput) {
// disable projection and disconnect everything
projectionDisabled();
fProjectionNeedsToBeEnabled = true;
}
}
public void inputDocumentChanged(IDocument oldInput, IDocument newInput) {
// if projection was previously enabled before input document changed
// and new document is different than old document
if (fProjectionNeedsToBeEnabled && fDocument == null && newInput != null) {
projectionEnabled();
fProjectionNeedsToBeEnabled = false;
}
}
/**
* Disconnect this IStructuredTextFoldingProvider from projection viewer
*/
public void uninstall() {
if (isInstalled()) {
projectionDisabled();
fViewer.removeProjectionListener(this);
fViewer.addTextInputListener(this);
fViewer = null;
}
}
}