package net.sourceforge.pmd.eclipse.ui.views.cpd2;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import net.sourceforge.pmd.cpd.Match;
import net.sourceforge.pmd.eclipse.plugin.PMDPlugin;
import net.sourceforge.pmd.eclipse.runtime.PMDRuntimeConstants;
import net.sourceforge.pmd.eclipse.ui.nls.StringKeys;
import net.sourceforge.pmd.util.StringUtil;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.TreeNode;
import org.eclipse.jface.viewers.TreeNodeContentProvider;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.MessageBox;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeColumn;
import org.eclipse.ui.IPropertyListener;
import org.eclipse.ui.IViewSite;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.part.ViewPart;
/**
* An updated view for Cut & Paste Detector that shows the results in a tree table
* with the file matches as columns at the root level with actual code snippets
* that span all columns beneath them. Clicking on the class names brings up
* the relevant sections in the code editor.
*
* @author Brian Remedios
*/
public class CPDView2 extends ViewPart implements IPropertyListener {
private TreeViewer treeViewer;
private TreeNodeContentProvider contentProvider;
private CPDViewLabelProvider2 labelProvider;
private int[] columnWidths;
private Listener measureListener;
private Listener resizeListener;
private Color classColor;
private Color packageColor;
private Map<String, int[]> nameWidthsByName;
private TreeColumn messageColumn; // we adjust the width of this one
private static final int SpanColumnWidth = 50;
private static final int MAX_MATCHES = 100;
private static final int xGap = 6;
private static final String TabEquivalent = " "; // tab char == 4 spaces
public static final int SourceColumnIdx = 1;
private static List<Match> asList(Iterator<Match> matchIter) {
List<Match> matches = new ArrayList<Match>(MAX_MATCHES);
for (int count = 0; matchIter.hasNext() && count < MAX_MATCHES; count++) {
matches.add(matchIter.next());
}
Collections.sort(matches, Match.MATCHES_COMPARATOR);
Collections.reverse(matches);
return matches;
}
public static String[] partsOf(String fullName) {
int pos = fullName.lastIndexOf('.');
return new String[] {
fullName.substring(0, pos+1),
fullName.substring(pos+1)
};
}
public static String[] sourceLinesFrom(Match match, boolean trimLeadingWhitespace) {
final String text = match.getSourceCodeSlice().replaceAll("\t", TabEquivalent);
final StringTokenizer lines = new StringTokenizer(text, "\n");
List<String> sourceLines = new ArrayList<String>();
for (int i=0; lines.hasMoreTokens(); i++) {
String line = lines.nextToken();
sourceLines.add(line);
}
String[] lineArr = new String[sourceLines.size()];
lineArr = sourceLines.toArray(lineArr);
if (trimLeadingWhitespace) {
int trimDepth = StringUtil.maxCommonLeadingWhitespaceForAll(lineArr);
if (trimDepth > 0) {
lineArr = StringUtil.trimStartOn(lineArr, trimDepth);
}
}
return lineArr;
}
/*
* @see org.eclipse.ui.ViewPart#init(org.eclipse.ui.IViewSite)
*/
@Override
public void init(IViewSite site) throws PartInitException {
super.init(site);
contentProvider = new TreeNodeContentProvider();
labelProvider = new CPDViewLabelProvider2();
measureListener = new Listener() {
public void handleEvent(Event event) {
captureColumnWidths();
}
};
resizeListener = new Listener() {
public void handleEvent(Event event) {
int width = treeViewer.getTree().getBounds().width;
messageColumn.setWidth(width - SpanColumnWidth);
captureColumnWidths();
treeViewer.refresh();
}
};
nameWidthsByName = new HashMap<String, int[]>();
}
public int widthOf(int columnIndex) {
if (columnWidths == null) captureColumnWidths();
return columnWidths[columnIndex];
}
private void captureColumnWidths() {
TreeColumn[] columns = treeViewer.getTree().getColumns();
columnWidths = new int[columns.length];
for (int i=0; i<columnWidths.length; i++) {
columnWidths[i] = columns[i].getWidth();
}
}
/*
* @see org.eclipse.ui.part.WorkbenchPart#createPartControl(org.eclipse.swt.widgets.Composite)
*/
@Override
public void createPartControl(Composite parent) {
int treeStyle = SWT.H_SCROLL | SWT.V_SCROLL | SWT.MULTI | SWT.FULL_SELECTION;
treeViewer = new TreeViewer(parent, treeStyle);
treeViewer.setUseHashlookup(true);
Tree tree = treeViewer.getTree();
tree.addListener(SWT.Move, measureListener);
tree.addListener(SWT.Resize, resizeListener);
tree.setHeaderVisible(true);
tree.setLinesVisible(true);
addPainters(tree);
treeViewer.setContentProvider(contentProvider);
treeViewer.setLabelProvider(labelProvider);
addDeleteListener(treeViewer.getControl());
createColumns(tree);
CPDViewTooltipListener2 tooltipListener = new CPDViewTooltipListener2(this);
tree.addListener(SWT.MouseMove, tooltipListener);
tree.addListener(SWT.MouseHover, tooltipListener);
tree.addListener(SWT.MouseDown, tooltipListener);
Display disp = tree.getDisplay();
classColor = disp.getSystemColor(SWT.COLOR_BLUE);
packageColor = disp.getSystemColor(SWT.COLOR_GRAY);
}
protected void addDeleteListener(Control control) {
control.addKeyListener(new KeyAdapter() {
public void keyPressed(KeyEvent ev) {
if (ev.character == SWT.DEL) {
removeSelectedItems();
}
}
});
}
// TODO fix - not deleting 'model' elements?
private void removeSelectedItems() {
IStructuredSelection selection = (IStructuredSelection)treeViewer.getSelection();
Object[] items = selection.toArray();
treeViewer.remove(items);
}
public int inColumn(Point point) {
if (columnWidths == null) return -1;
int pos = 0;
for (int i=0; i<columnWidths.length; i++) {
if (pos < point.x && pos + columnWidths[i] > point.x) {
return i;
}
pos += columnWidths[i];
}
return -1;
}
public int[] widthsFor(String name) {
return nameWidthsByName.get(name);
}
private void paintName(GC gc, int x, int y, String name, int rightEdge, int descent, int cellWidth) {
String[] parts = partsOf(name);
int packageWidth;
int classWidth;
int[] widths = nameWidthsByName.get(name);
if (widths != null) {
packageWidth = widths[0];
classWidth = widths[1];
} else {
gc.setFont( treeViewer.getTree().getFont() );
packageWidth = gc.stringExtent(parts[0]).x;
classWidth = gc.stringExtent(parts[1]).x;
nameWidthsByName.put(name, new int[] {packageWidth, classWidth} );
}
int drawX = x + rightEdge - classWidth - xGap;
// Rectangle clipRect = new Rectangle(x, y, cellWidth, 24);
gc.setForeground(classColor);
// gc.drawRectangle(clipRect);
gc.drawText(parts[1], drawX, y + descent, false);
// gc.setClipping((Rectangle)null);
drawX = x + rightEdge - classWidth - packageWidth - xGap;
// clipRect.x = drawX;
// clipRect.width = packageWidth;
gc.setForeground(packageColor);
// gc.drawRectangle(clipRect);
gc.drawText(parts[0], drawX, y + descent, false);
// gc.setClipping((Rectangle)null);
}
private void addPainters(Tree tree) {
Listener paintListener = new Listener() {
public void handleEvent(Event event) {
if (event.index != SourceColumnIdx) return;
Object item = ((TreeNode)event.item.getData()).getValue();
String[] names;
if (item instanceof Match) {
names = CPDViewLabelProvider2.sourcesFor((Match)item);
} else {
return;
}
int descent = event.gc.getFontMetrics().getDescent();
int colWidth = widthOf(SourceColumnIdx);
int cellWidth = colWidth / names.length;
for (int i=0; i<names.length; i++) {
int rightEdge = colWidth - (cellWidth * i);
paintName(event.gc, event.x, event.y, names[i], rightEdge, descent, cellWidth);
}
};
};
Listener measureListener = new Listener() {
public void handleEvent(Event event) {
if (event.index != SourceColumnIdx) return;
event.width = 400;
event.height = 24;
}
};
tree.addListener(SWT.PaintItem, paintListener);
tree.addListener(SWT.MeasureItem, measureListener);
}
/**
* Creates the columns of the tree.
* @param tree Tree from the treeViewer
*/
private void createColumns(Tree tree) {
// the "+"-sign for expanding packages
TreeColumn plusColumn = new TreeColumn(tree, SWT.RIGHT);
plusColumn.setText("Spans");
plusColumn.setWidth(SpanColumnWidth);
// plusColumn.setResizable(false);
// shows the source
messageColumn = new TreeColumn(tree, SWT.LEFT);
messageColumn.addListener(SWT.Move, measureListener);
messageColumn.setText("Source");
messageColumn.setWidth(500);
}
/**
* @return the tree viewer.
*/
public TreeViewer getTreeViewer() {
return treeViewer;
}
/**
* Helper method to return an NLS string from its key
*/
private String getString(String key) {
return PMDPlugin.getDefault().getStringTable().getString(key);
}
/*
* @see org.eclipse.ui.part.WorkbenchPart#setFocus()
*/
@Override
public void setFocus() {
treeViewer.getTree().setFocus();
}
/**
* Sets input for the table.
* @param matches CPD Command that contain the matches from the CPD
*/
public void setData(Iterator<Match> matches) {
List<TreeNode> elements = new ArrayList<TreeNode>();
if (matches != null) {
for (Match match : asList(matches)) {
// create a treenode for the match and add to the list
TreeNode matchNode = new TreeNode(match); // NOPMD by Sven on 02.11.06 11:27
elements.add(matchNode);
String[] lines = sourceLinesFrom(match, true);
TreeNode[] children = new TreeNode[lines.length];
for (int j=0; j<lines.length; j++) {
String line = lines[j];
if (line == null) {
System.out.println();
}
children[j] = new TreeNode(line);
children[j].setParent(matchNode);
}
matchNode.setChildren(children);
}
}
// set the children of the rootnode: the matches
treeViewer.setInput(elements.toArray(new TreeNode[elements.size()]));
}
/**
* After the CPD command is executed, it will trigger an propertyChanged event.
*/
public void propertyChanged(Object source, int propId) {
if (propId == PMDRuntimeConstants.PROPERTY_CPD && source instanceof Iterator<?>) {
Iterator<Match> iter = (Iterator<Match>) source;
// after setdata(iter) iter.hasNext will always return false
boolean hasResults = iter.hasNext();
setData(iter);
if (!hasResults) {
// no entries
MessageBox box = new MessageBox(this.treeViewer.getControl().getShell());
box.setText(getString(StringKeys.DIALOG_CPD_NORESULTS_HEADER));
box.setMessage(getString(StringKeys.DIALOG_CPD_NORESULTS_BODY));
box.open();
}
}
}
}