package org.jmeld.ui.diffbar; /* JMeld is a visual diff and merge tool. Copyright (C) 2007 Kees Kuip This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ import org.jmeld.diff.JMChunk; import org.jmeld.diff.JMDelta; import org.jmeld.diff.JMRevision; import org.jmeld.diff.TypeDiff; import org.jmeld.settings.EditorSettings; import org.jmeld.settings.JMeldSettings; import org.jmeld.ui.BufferDiffPanel; import org.jmeld.ui.FilePanel; import org.jmeld.ui.text.BufferDocumentIF; import org.jmeld.ui.util.RevisionUtil; import org.jmeld.util.conf.ConfigurationListenerIF; import javax.swing.*; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.text.BadLocationException; import javax.swing.text.JTextComponent; import java.awt.*; import java.awt.event.*; import java.awt.geom.CubicCurve2D; import java.awt.geom.GeneralPath; import java.awt.geom.Line2D; import java.util.ArrayList; import java.util.List; public class DiffScrollComponent extends JComponent implements ChangeListener, ConfigurationListenerIF { private BufferDiffPanel diffPanel; private int fromPanelIndex; private int toPanelIndex; private List<Command> commands; private Object antiAlias; private boolean leftsideReadonly; private boolean rightsideReadonly; private int curveType; private boolean drawCurves; public DiffScrollComponent(BufferDiffPanel diffPanel, int fromPanelIndex, int toPanelIndex) { this.diffPanel = diffPanel; this.fromPanelIndex = fromPanelIndex; this.toPanelIndex = toPanelIndex; getFromPanel().getScrollPane().getViewport().addChangeListener(this); getToPanel().getScrollPane().getViewport().addChangeListener(this); addMouseListener(getMouseListener()); addMouseWheelListener(getMouseWheelListener()); addKeyListener(getKeyListener()); JMeldSettings.getInstance().addConfigurationListener(this); initSettings(); } public void setCurveType(int curveType) { this.curveType = curveType; } public int getCurveType() { return curveType; } boolean shift; public void setShift(boolean shift) { this.shift = shift; repaint(); } public KeyListener getKeyListener() { return new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_SHIFT) { shift = true; repaint(); } } @Override public void keyReleased(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_SHIFT) { shift = false; repaint(); } } }; } public boolean isDrawCurves() { return drawCurves; } public void setDrawCurves(boolean drawCurves) { this.drawCurves = drawCurves; } private void initSettings() { JMeldSettings settings = JMeldSettings.getInstance(); EditorSettings editorSettings; editorSettings = settings.getEditor(); leftsideReadonly = editorSettings.getLeftsideReadonly(); rightsideReadonly = editorSettings.getRightsideReadonly(); setDrawCurves(settings.getDrawCurves()); setCurveType(settings.getCurveType()); } public void stateChanged(ChangeEvent event) { repaint(); } public void configurationChanged() { initSettings(); repaint(); } private MouseWheelListener getMouseWheelListener() { return new MouseWheelListener() { public void mouseWheelMoved(MouseWheelEvent me) { diffPanel.toNextDelta(me.getWheelRotation() > 0); repaint(); } }; } private MouseListener getMouseListener() { return new MouseAdapter() { @Override public void mouseClicked(MouseEvent me) { requestFocus(); int onmask = MouseEvent.SHIFT_DOWN_MASK /*| MouseEvent.BUTTON1_DOWN_MASK*/; int offmask = MouseEvent.CTRL_DOWN_MASK; //TODO: Se debe pintar antes una flecha sobre o debajo del cambio para entender que se añadira arriba //o abajo y deben desaparecer los de borrar boolean shift = false; if ((me.getModifiersEx() & (onmask | offmask)) == onmask) { shift = true; } executeCommand((double) me.getX(), (double) me.getY(), shift); } }; } public boolean executeCommand(double x, double y, boolean shift) { if (commands == null) { return false; } for (Command command : commands) { if (command.contains(x, y)) { command.execute(shift); return true; } } return false; } @Override public void paintComponent(Graphics g) { Rectangle r; int middle; Graphics2D g2; g2 = (Graphics2D) g; r = g.getClipBounds(); g2.setColor(getBackground()); g2.fill(r); middle = r.height / 2; g2.setColor(Color.LIGHT_GRAY); Stroke oldStroke = g2.getStroke(); // g2.setStroke(new BasicStroke(3.0f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_ROUND, 10.0f, null, 0.0f)); // g2.drawLine(r.x + 20, r.y + middle, r.x + r.width - 20, r.y + middle); // g2.setStroke(oldStroke); paintDiffs(g2); } private void paintDiffs(Graphics2D g2) { JViewport viewportFrom; JViewport viewportTo; JTextComponent editorFrom; JTextComponent editorTo; JMRevision revision; BufferDocumentIF bdFrom; BufferDocumentIF bdTo; int firstLineFrom; int lastLineFrom; int firstLineTo; int lastLineTo; int offset; Rectangle r; Point p; JMChunk original; JMChunk revised; Rectangle viewportRect; Rectangle fromRect; Rectangle toRect; int fromLine; int toLine; int x; int y; int width; int height; Rectangle bounds; int x0; int y0; int x1; int y1; Color color; Color darkerColor; Polygon shape; Rectangle rect; boolean selected; int selectionWidth; FilePanel fromPanel; FilePanel toPanel; bounds = g2.getClipBounds(); g2.setClip(null); revision = diffPanel.getCurrentRevision(); if (revision == null) { return; } commands = new ArrayList<Command>(); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); antiAlias = g2.getRenderingHint(RenderingHints.KEY_ANTIALIASING); // From side: fromPanel = getFromPanel(); viewportFrom = fromPanel.getScrollPane().getViewport(); editorFrom = fromPanel.getEditor(); bdFrom = fromPanel.getBufferDocument(); if (bdFrom == null) { return; } r = viewportFrom.getViewRect(); // Calculate firstLine shown of the first document. p = new Point(r.x, r.y); offset = editorFrom.viewToModel(p); firstLineFrom = bdFrom.getLineForOffset(offset) + 1; // Calculate lastLine shown of the first document. p = new Point(r.x, r.y + r.height); offset = editorFrom.viewToModel(p); bdFrom = fromPanel.getBufferDocument(); lastLineFrom = bdFrom.getLineForOffset(offset) + 1; // To side: toPanel = getToPanel(); viewportTo = toPanel.getScrollPane().getViewport(); editorTo = toPanel.getEditor(); bdTo = toPanel.getBufferDocument(); if (bdTo == null) { return; } r = viewportTo.getViewRect(); // Calculate firstLine shown of the second document. p = new Point(r.x, r.y); offset = editorTo.viewToModel(p); firstLineTo = bdTo.getLineForOffset(offset) + 1; // Calculate lastLine shown of the second document. p = new Point(r.x, r.y + r.height); offset = editorTo.viewToModel(p); lastLineTo = bdTo.getLineForOffset(offset) + 1; try { // Draw only the delta's that have some line's drawn in one of the viewports. for (JMDelta delta : revision.getDeltas()) { original = delta.getOriginal(); revised = delta.getRevised(); // This delta is before the firstLine of the screen: Keep on searching! if (original.getAnchor() + original.getSize() < firstLineFrom && revised.getAnchor() + revised.getSize() < firstLineTo) { continue; } // This delta is after the lastLine of the screen: stop! if (original.getAnchor() > lastLineFrom && revised.getAnchor() > lastLineTo) { break; } selected = (delta == diffPanel.getSelectedDelta()); // OK, this delta has some visible lines. Now draw it! color = RevisionUtil.getColor(delta); darkerColor = RevisionUtil.getDarkerColor(delta); g2.setColor(color); // Draw original chunk: fromLine = original.getAnchor(); toLine = original.getAnchor() + original.getSize(); viewportRect = viewportFrom.getViewRect(); offset = bdFrom.getOffsetForLine(fromLine); if (offset < 0) { continue; } fromRect = editorFrom.modelToView(offset); offset = bdFrom.getOffsetForLine(toLine); if (offset < 0) { continue; } toRect = editorFrom.modelToView(offset); x = 0; y = fromRect.y - viewportRect.y + 1; y = y < 0 ? 0 : y; width = 10; height = 0; // start of diff is before the first visible line. // end of diff is before the last visible line. // (The first part of diff should not be visible) if (fromRect.y <= viewportRect.y && toRect.y <= viewportRect.y + viewportRect.height) { height = original.getSize() * fromRect.height; height -= viewportRect.y - fromRect.y; } // start of diff is after the first visible line. // end of diff is after the last visible line. // (The last part of diff should not be visible) else if (fromRect.y > viewportRect.y && toRect.y > viewportRect.y + viewportRect.height) { height = original.getSize() * fromRect.height; height -= viewportRect.y + viewportRect.height - toRect.y; } // start of diff is after the first visible line. // end of diff is before the last visible line. // (The diff is completely visible) else if (fromRect.y > viewportRect.y && toRect.y <= viewportRect.y + viewportRect.height) { height = original.getSize() * fromRect.height; } // start of diff is before the first visible line. // end of diff is after the last visible line. // (The first part of diff should not be visible) // (The last part of diff should not be visible) else if (fromRect.y <= viewportRect.y && toRect.y > viewportRect.y + viewportRect.height) { height = viewportRect.height; } x0 = x + width; y0 = y; int curveX1 = x; int curveY1 = y; int curveX4 = x; int curveY4 = y + (height > 0 ? height : 0); int xSelFrom = x; int ySelFrom = y; int heightSelFrom = height; // Draw revised chunk: fromLine = revised.getAnchor(); toLine = revised.getAnchor() + revised.getSize(); viewportRect = viewportTo.getViewRect(); offset = bdTo.getOffsetForLine(fromLine); if (offset < 0) { continue; } fromRect = editorTo.modelToView(offset); offset = bdTo.getOffsetForLine(toLine); if (offset < 0) { continue; } toRect = editorTo.modelToView(offset); x = bounds.x + bounds.width - 10; y = fromRect.y - viewportRect.y + 1; y = y < 0 ? 0 : y; width = 10; height = 0; if (fromRect.y <= viewportRect.y && toRect.y <= viewportRect.y + viewportRect.height) { height = revised.getSize() * fromRect.height; height -= viewportRect.y - fromRect.y; } else if (fromRect.y > viewportRect.y && toRect.y > viewportRect.y + viewportRect.height) { height = revised.getSize() * fromRect.height; height -= viewportRect.y + viewportRect.height - toRect.y; } else if (fromRect.y > viewportRect.y && toRect.y <= viewportRect.y + viewportRect.height) { height = revised.getSize() * fromRect.height; } else if (fromRect.y <= viewportRect.y && toRect.y > viewportRect.y + viewportRect.height) { height = viewportRect.height; } x1 = x; y1 = y; if (isDrawCurves()) { int curveX2 = x + width; int curveY2 = y; int curveX3 = x + width; int curveY3 = y + (height > 0 ? height : 0); GeneralPath curve = new GeneralPath(); if (getCurveType() == 0) { curve.append(new Line2D.Float(curveX4, curveY4, curveX1, curveY1), true); curve.append(new Line2D.Float(curveX2, curveY2, curveX3, curveY3), true); } else if (getCurveType() == 1) { int posyOrg = original.getSize() > 0 ? 0 : 1; int posyRev = revised.getSize() > 0 ? 0 : 1; curve.append(new CubicCurve2D.Float(curveX1, curveY1 - posyOrg , curveX1 + ((curveX2 - curveX1) / 2) + 5, curveY1 , curveX1 + ((curveX2 - curveX1) / 2) + 5, curveY2 , curveX2, curveY2 - posyRev) , true); int addHeightCorrection = 0; // if (delta.getType() == TypeDiff.ADD) { // addHeightCorrection = 15; // } curve.append(new CubicCurve2D.Float(curveX3, curveY3 + posyRev/* - addHeightCorrection*/ , curveX3 + ((curveX4 - curveX3) / 2) - 5, curveY3 /*- addHeightCorrection*/ , curveX3 + ((curveX4 - curveX3) / 2) - 5, curveY4 , curveX4, curveY4 + posyOrg) , true); } else if (getCurveType() == 2) { curve.append(new CubicCurve2D.Float(curveX1, curveY1 - 2 , curveX2 + 10, curveY1 , curveX1 + 10, curveY2 , curveX2, curveY2 - 2) , true); curve.append(new CubicCurve2D.Float(curveX3, curveY3 + 2 , curveX4 - 10, curveY3 , curveX3 - 10, curveY4 , curveX4, curveY4 + 2) , true); } g2.setColor(color); g2.fill(curve); g2.setColor(darkerColor); setAntiAlias(g2); g2.draw(curve); resetAntiAlias(g2); } else { if (height > 0) { g2.setColor(color); g2.fillRect(x, y, width, height); } g2.setColor(darkerColor); g2.drawLine(x, y, x + width, y); if (height > 0) { g2.drawLine(x, y + height, x + width, y + height); g2.drawLine(x, y, x, y + height); } //x = x + width + 1; g2.setColor(darkerColor); g2.drawLine(x0, y0, x0 + 15, y0); setAntiAlias(g2); g2.drawLine(x0 + 15, y0, x1 - 15, y1); resetAntiAlias(g2); g2.drawLine(x1 - 15, y1, x1, y1); } boolean showSelected = true; if (showSelected && selected) { if (heightSelFrom > 0) { selectionWidth = 5; g2.setColor(Color.yellow); g2.fillRect(xSelFrom, ySelFrom, selectionWidth, heightSelFrom); g2.setColor(Color.yellow.darker()); g2.drawLine(xSelFrom, ySelFrom, xSelFrom + selectionWidth, ySelFrom); g2.drawLine(xSelFrom + selectionWidth, ySelFrom, xSelFrom + selectionWidth, ySelFrom + heightSelFrom); g2.drawLine(xSelFrom, ySelFrom + heightSelFrom, xSelFrom + selectionWidth, ySelFrom + heightSelFrom); } if (height > 0) { selectionWidth = 5; x += selectionWidth; g2.setColor(Color.yellow); g2.fillRect(x, y, selectionWidth, height); g2.setColor(Color.yellow.darker()); g2.drawLine(x, y, x + selectionWidth, y); g2.drawLine(x, y, x, y + height); g2.drawLine(x, y + height, x + selectionWidth, y + height); g2.setColor(color); } } boolean drawCommandPointers = true; if(drawCommandPointers) { // Draw merge right->left command. if (!leftsideReadonly && !bdFrom.isReadonly()) { if (!shift || revised.getSize() > 0) { shape = createTriangle(x0, y0, true); setAntiAlias(g2); g2.setColor(shift ? Color.black : color); g2.fill(shape); g2.setColor(shift ? Color.black : darkerColor); g2.draw(shape); resetAntiAlias(g2); commands.add(new DiffChangeCommand(shape, delta, toPanelIndex, fromPanelIndex)); } // Draw delete right command if (original.getSize() > 0 && !shift) { g2.setColor(Color.red); g2.drawLine(x0 + 3 - width, y0 + 3, x0 + 7 - width, y0 + 7); g2.drawLine(x0 + 7 - width, y0 + 3, x0 + 3 - width, y0 + 7); rect = new Rectangle(x0 + 2 - width, y0 + 2, 6, 6); commands.add(new DiffDeleteCommand(rect, delta, fromPanelIndex, toPanelIndex)); } } // Draw merge left->right command. if (!rightsideReadonly && !bdTo.isReadonly()) { if (!shift || original.getSize() > 0) { shape = createTriangle(x1, y1, false); setAntiAlias(g2); g2.setColor(shift ? Color.black : color); g2.fillPolygon(shape); g2.setColor(shift ? Color.black : darkerColor); g2.drawPolygon(shape); resetAntiAlias(g2); commands.add(new DiffChangeCommand(shape, delta, fromPanelIndex, toPanelIndex)); } // Draw delete right command if (revised.getSize() > 0 && !shift) { g2.setColor(Color.red); g2.drawLine(x1 + 3, y1 + 3, x1 + 7, y1 + 7); g2.drawLine(x1 + 7, y1 + 3, x1 + 3, y1 + 7); rect = new Rectangle(x1 + 2, y1 + 2, 6, 6); commands.add(new DiffDeleteCommand(rect, delta, toPanelIndex, fromPanelIndex)); } } } } } catch (BadLocationException ex) { ex.printStackTrace(); } resetAntiAlias(g2); } private Polygon createTriangle(int x, int y, boolean toLeft) { Polygon shape; shape = new Polygon(); int posx = 10; shape.addPoint(x + (toLeft ? -posx : posx), y); posx -= 11; shape.addPoint(x + (toLeft ? -posx : posx), y + - 4); shape.addPoint(x + (toLeft ? -posx : posx), y + + 4); return shape; } class DiffChangeCommand extends Command { DiffChangeCommand(Shape shape, JMDelta delta, int fromIndex, int toIndex) { super(shape, delta, fromIndex, toIndex); } @Override public void execute(boolean shift) { diffPanel.setSelectedDelta(delta); diffPanel.runChange(fromIndex, toIndex, shift); } } class DiffDeleteCommand extends Command { DiffDeleteCommand(Shape shape, JMDelta delta, int fromIndex, int toIndex) { super(shape, delta, fromIndex, toIndex); } @Override public void execute(boolean shift) { diffPanel.setSelectedDelta(delta); if (!shift) { //TODO: Con shift no se debería ni ver diffPanel.runDelete(fromIndex, toIndex); } } } //TODO: Hay que hacer un diff add command (AddUpper, AddLower) abstract class Command { Rectangle bounds; JMDelta delta; int fromIndex; int toIndex; Command(Shape shape, JMDelta delta, int fromIndex, int toIndex) { this.bounds = shape.getBounds(); this.delta = delta; this.fromIndex = fromIndex; this.toIndex = toIndex; } boolean contains(double x, double y) { return bounds.contains(x, y); } public abstract void execute(boolean shift); } private void setAntiAlias(Graphics2D g2) { g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); } private void resetAntiAlias(Graphics2D g2) { g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antiAlias); } private FilePanel getFromPanel() { return diffPanel.getFilePanel(fromPanelIndex); } private FilePanel getToPanel() { return diffPanel.getFilePanel(toPanelIndex); } }