// Copyright 2004-2014 Jim Voris
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package com.qumasoft.guitools.compare;
import com.qumasoft.guitools.qwin.QWinFrame;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.event.MouseInputAdapter;
/**
* Panel that shows where the changes are in a given file.
*
* @author Jim Voris
*/
public class ChangeMarkerPanel extends JPanel {
private static final long serialVersionUID = 8325950361773231444L;
/** Panel width in pixels. */
private static final int PANEL_WIDTH = 10;
private static final int PANEL_HEIGHT = 10;
private Rectangle pastSize;
private final int totalRowCount;
private final int rowHeight;
private final ArrayList<ColoredRowBlock> coloredRowBlocks;
private final ArrayList<ColoredRectangle> coloredRectangles;
private JScrollBar scrollBar;
private double scrollButtonTotalHeight;
private double scrollButtonTopHeight;
private double scrollButtonBottomHeight;
/**
* Construct a ChangeMarkerPanel object.
*
* @param baseFileLinkedList the list of rows that populate the display.
* @param panelNumber this should be a 1 or a 2 for the first descendent or second descendent respectively.
* @param rh the height of a single row.
*/
ChangeMarkerPanel(FileContentsListModel fileContentsListModel, int rh) {
this.scrollButtonBottomHeight = 0.0;
this.scrollButtonTopHeight = 0.0;
this.scrollButtonTotalHeight = 0.0;
this.coloredRectangles = new ArrayList<>();
this.coloredRowBlocks = new ArrayList<>();
setMinimumSize(new Dimension(PANEL_WIDTH, PANEL_HEIGHT));
this.rowHeight = rh;
this.totalRowCount = computeRowBlocks(fileContentsListModel);
this.addMouseListener(new MyMouseListener(this));
}
/**
* Set the scrollbar for this change panel so we can figure out what space to pre-allocate for the
* scroll bar buttons.
*
* @param sb the vertical scroll bar for this change marker panel.
*/
void setScrollBar(final JScrollBar sb) {
this.scrollBar = sb;
}
/**
* Get the vertical scroll bar for this change marker panel.
*
* @return return the scroll bar for this change marker panel.
*/
JScrollBar getScrollBar() {
return scrollBar;
}
@Override
public Dimension getPreferredSize() {
return new Dimension(PANEL_WIDTH, PANEL_HEIGHT);
}
/**
* Scroll the scroll bar so it matches the given y position.
*
* @param y the vertical y coordinate to scroll to.
*/
void scrollToY(int y) {
double adjustedY = y - (int) scrollButtonTotalHeight;
double percentage = adjustedY / (pastSize.height - scrollButtonTotalHeight);
int minimum = getScrollBar().getModel().getMinimum();
int maximum = getScrollBar().getModel().getMaximum();
double range = maximum - minimum;
int modelValue = (int) (percentage * range);
getScrollBar().getModel().setValue(modelValue);
}
@Override
public void paint(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
Rectangle ourRectangle = getBounds();
if ((pastSize == null) || (pastSize.height != ourRectangle.height)) {
pastSize = new Rectangle(ourRectangle);
scaleRectangles(ourRectangle);
}
for (int i = 0; i < coloredRectangles.size(); i++) {
ColoredRectangle coloredRectangle = coloredRectangles.get(i);
g2d.setColor(coloredRectangle.getColor());
g2d.setBackground(coloredRectangle.getColor());
g2d.fillRect(coloredRectangle.getRectangle().x,
coloredRectangle.getRectangle().y,
coloredRectangle.getRectangle().width,
coloredRectangle.getRectangle().height);
}
}
private void computeScrollButtonOffsets(Rectangle ourBoundingRectangle) {
scrollButtonTotalHeight = 0.0;
scrollButtonTopHeight = 0.0;
scrollButtonBottomHeight = 0.0;
Component[] components = scrollBar.getComponents();
if (components.length > 0) {
for (Component component : components) {
// These turn out to be the scroll buttons...
Rectangle rectangle = component.getBounds();
scrollButtonTotalHeight += rectangle.getHeight();
if (rectangle.getY() > 0.0) {
scrollButtonBottomHeight += rectangle.getHeight();
} else {
scrollButtonTopHeight += rectangle.getHeight();
}
}
} else {
// This is for iMac... This is an approximation at best.
Rectangle rectangle = scrollBar.getBounds();
scrollButtonTopHeight = rectangle.getY();
if (rectangle.getHeight() > 0.0) {
scrollButtonBottomHeight = ourBoundingRectangle.getY() + (ourBoundingRectangle.getHeight() - rectangle.getHeight());
}
scrollButtonTotalHeight = scrollButtonTopHeight + scrollButtonBottomHeight;
}
}
/**
* Figure out the array of rectangles that we need to have to provide a summary of where
* the edits are in the given file.
*
* @param ourBoundingRectangle our bounding rectangle. We only really care about the height.
*/
private void scaleRectangles(Rectangle ourBoundingRectangle) {
coloredRectangles.clear();
// Figure out the maximum vertical space needed to display all the rows...
double totalHeightOfAllRows = this.rowHeight * this.totalRowCount;
computeScrollButtonOffsets(ourBoundingRectangle);
// If the total needed to display all rows is less than our bounding rectangle, then we need to use that
// smaller size to figure out the scaling factor per row, so things will be scaled correctly for the
// case where the text does not fill the entire panel.
double totalHeightToUse;
if ((ourBoundingRectangle.getHeight() - scrollButtonTotalHeight) > totalHeightOfAllRows) {
totalHeightToUse = totalHeightOfAllRows;
} else {
totalHeightToUse = ourBoundingRectangle.getHeight() - scrollButtonTotalHeight;
}
double scalingFactorPerRow = totalHeightToUse / this.totalRowCount;
Iterator<ColoredRowBlock> it = coloredRowBlocks.iterator();
int currentY = 0;
int anchoredX = 0;
int adjustRowCount = 0;
// Add a rectangle for the top scroll bar button.
if (scrollButtonTopHeight > 0) {
coloredRectangles.add(new ColoredRectangle(QWinFrame.getQWinFrame().getBackground(),
new Rectangle(anchoredX + 1, currentY, PANEL_WIDTH - 2, (int) scrollButtonTopHeight)));
currentY += (int) scrollButtonTopHeight;
}
while (it.hasNext()) {
ColoredRowBlock coloredRowBlock = it.next();
int scaledHeight = (int) ((double) (adjustRowCount + coloredRowBlock.getRowCount()) * scalingFactorPerRow);
// Make each edit at least one pixel high.
if (scaledHeight <= 0) {
int increaseRowCount = 0;
while (scaledHeight <= 0) {
increaseRowCount++;
scaledHeight = (int) ((double) (increaseRowCount + coloredRowBlock.getRowCount()) * scalingFactorPerRow);
}
// So the next block will be smaller by this amount.
adjustRowCount = -increaseRowCount;
}
coloredRectangles.add(new ColoredRectangle(coloredRowBlock.getColor(),
new Rectangle(anchoredX + 1, currentY, PANEL_WIDTH - 2, scaledHeight)));
currentY += scaledHeight;
}
// Add a rectangle for the bottom scroll bar button(s).
if (scrollButtonBottomHeight > 0) {
coloredRectangles.add(new ColoredRectangle(QWinFrame.getQWinFrame().getBackground(),
new Rectangle(anchoredX + 1, currentY, PANEL_WIDTH - 2, (int) scrollButtonBottomHeight)));
}
}
/**
* Figure out the minimum number of rectangles that will represent the changes in the file.
*
* @param baseFileLinkedList the list of rows in the file.
* @param panelNumber a 1 or a 2 to indicate the changes are for the first (1) or second (2) decendent file.
* @return the total number of rows in the display.
*/
private int computeRowBlocks(FileContentsListModel fileContentsListModel) {
// Make sure to start with an empty list of row blocks.
coloredRowBlocks.clear();
Enumeration it = fileContentsListModel.elements();
int blockSize = 0;
int rowCount = 0;
int rowType = -1;
while (it.hasMoreElements()) {
int currentRowType;
ContentRow contentRow = (ContentRow) it.nextElement();
rowCount++;
if (blockSize == 0) {
rowType = translateRowType(contentRow.getRowType());
currentRowType = rowType;
} else {
currentRowType = translateRowType(contentRow.getRowType());
}
if (currentRowType == rowType) {
blockSize++;
} else {
coloredRowBlocks.add(new ColoredRowBlock(rowType, blockSize));
blockSize = 1;
rowType = currentRowType;
}
}
if (blockSize > 0) {
coloredRowBlocks.add(new ColoredRowBlock(rowType, blockSize));
}
return rowCount;
}
/**
* We only care about inserts and deletes. Everthing else is a 'normal' row.
*
* @param rowType input row type.
* @return translated row type.
*/
private int translateRowType(int rowType) {
int translatedRowType;
switch (rowType) {
case ContentRow.ROWTYPE_INSERT:
translatedRowType = ContentRow.ROWTYPE_INSERT;
break;
case ContentRow.ROWTYPE_DELETE:
translatedRowType = ContentRow.ROWTYPE_DELETE;
break;
case ContentRow.ROWTYPE_REPLACE:
translatedRowType = ContentRow.ROWTYPE_REPLACE;
break;
default:
translatedRowType = 0;
break;
}
return translatedRowType;
}
/**
* Entity class to hold info about a rectangle that represents a block of rows that share the same color.
*/
class ColoredRectangle {
private final Color color;
private final Rectangle rectangle;
ColoredRectangle(Color c, Rectangle r) {
this.color = c;
this.rectangle = r;
}
Color getColor() {
return color;
}
Rectangle getRectangle() {
return rectangle;
}
}
/**
* Entity class to hold info about a block of rows that share the same color.
*/
class ColoredRowBlock {
private Color color;
private final int rowCount;
ColoredRowBlock(int rowType, int rc) {
switch (rowType) {
case ContentRow.ROWTYPE_DELETE:
this.color = FileContentsList.getDeleteColor();
break;
case ContentRow.ROWTYPE_INSERT:
this.color = FileContentsList.getInsertColor();
break;
case ContentRow.ROWTYPE_REPLACE:
this.color = FileContentsList.getReplaceColor();
break;
default:
this.color = QWinFrame.getQWinFrame().getBackground();
break;
}
this.rowCount = rc;
}
Color getColor() {
return color;
}
int getRowCount() {
return rowCount;
}
}
class MyMouseListener extends MouseInputAdapter {
private final ChangeMarkerPanel changeMarkerPanel;
MyMouseListener(ChangeMarkerPanel cmp) {
this.changeMarkerPanel = cmp;
}
@Override
public void mouseClicked(MouseEvent e) {
int y = e.getY();
changeMarkerPanel.scrollToY(y);
}
}
}