/*
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
*/
package org.jmeld.ui;
import org.jmeld.diff.JMChunk;
import org.jmeld.diff.JMDelta;
import org.jmeld.diff.JMRevision;
import org.jmeld.settings.JMeldSettings;
import org.jmeld.ui.search.SearchCommand;
import org.jmeld.ui.search.SearchHit;
import org.jmeld.ui.search.SearchHits;
import org.jmeld.ui.swing.*;
import org.jmeld.ui.text.BufferDocumentChangeListenerIF;
import org.jmeld.ui.text.BufferDocumentIF;
import org.jmeld.ui.text.JMDocumentEvent;
import org.jmeld.ui.util.FontUtil;
import org.jmeld.ui.util.ImageUtil;
import org.jmeld.util.StringUtil;
import org.jmeld.util.conf.ConfigurationListenerIF;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Highlighter;
import javax.swing.text.PlainDocument;
import java.awt.*;
import java.awt.event.*;
public class FilePanel implements BufferDocumentChangeListenerIF, ConfigurationListenerIF {
private static final int MAXSIZE_CHANGE_DIFF = 1000;
private BufferDiffPanel diffPanel;
private String name;
private int position;
private DiffLabel fileLabel;
private JComboBox fileBox;
private JScrollPane scrollPane;
private JTextArea editor;
private BufferDocumentIF bufferDocument;
private JButton saveButton;
private Timer timer;
private SearchHits searchHits;
private boolean selected;
private FilePanelBar filePanelBar;
FilePanel(BufferDiffPanel diffPanel, String name, int position) {
this.diffPanel = diffPanel;
this.name = name;
this.position = position;
init();
}
private void init() {
ImageIcon icon;
editor = new JTextArea();
editor.setDragEnabled(true);
editor.setHighlighter(new JMHighlighter());
editor.addFocusListener(getFocusListener());
editor.addCaretListener(getCaretListener());
DefaultContextMenu contextMenu = new DefaultContextMenu();
contextMenu.add(editor);
scrollPane = new JScrollPane(editor);
// scrollPane.getViewport().setScrollMode(JViewport.SIMPLE_SCROLL_MODE);
scrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE);
if (BufferDocumentIF.ORIGINAL.equals(name)) {
// Dirty trick to have the scrollbar on the other side!
LeftScrollPaneLayout layout;
layout = new LeftScrollPaneLayout();
scrollPane.setLayout(layout);
layout.syncWithScrollPane(scrollPane);
// Normally the leftside is not painted of a scrollbar that is
// NOT freestanding.
scrollPane.getVerticalScrollBar().putClientProperty(
"JScrollBar.isFreeStanding", Boolean.TRUE);
}
fileBox = new JComboBox();
fileBox.addActionListener(getFileBoxAction());
fileLabel = new DiffLabel();
saveButton = new JButton();
saveButton.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
saveButton.setContentAreaFilled(false);
icon = ImageUtil.getSmallImageIcon("stock_save");
saveButton.setIcon(icon);
saveButton.setDisabledIcon(ImageUtil.createTransparentIcon(icon));
saveButton.addActionListener(getSaveButtonAction());
timer = new Timer(100, refresh());
timer.setRepeats(false);
initConfiguration();
getConfiguration().addConfigurationListener(this);
}
FilePanelBar getFilePanelBar() {
if (filePanelBar == null) {
filePanelBar = new FilePanelBar(this);
}
return filePanelBar;
}
JComboBox getFileBox() {
return fileBox;
}
DiffLabel getFileLabel() {
return fileLabel;
}
public JScrollPane getScrollPane() {
return scrollPane;
}
public JTextArea getEditor() {
return editor;
}
public BufferDocumentIF getBufferDocument() {
return bufferDocument;
}
JButton getSaveButton() {
return saveButton;
}
public void setBufferDocument(BufferDocumentIF bd) {
Document previousDocument;
Document document;
String fileName;
try {
if (bufferDocument != null) {
bufferDocument.removeChangeListener(this);
previousDocument = bufferDocument.getDocument();
if (previousDocument != null) {
previousDocument.removeUndoableEditListener(diffPanel
.getUndoHandler());
}
}
bufferDocument = bd;
document = bufferDocument.getDocument();
if (document != null) {
editor.setDocument(document);
editor.setTabSize(getConfiguration().getEditor().getTabSize());
bufferDocument.addChangeListener(this);
document.addUndoableEditListener(diffPanel.getUndoHandler());
}
fileName = bufferDocument.getName();
fileBox.addItem(fileName);
fileBox.setSelectedItem(fileName);
fileLabel.setText(fileName);
checkActions();
initConfiguration();
} catch (Exception ex) {
ex.printStackTrace();
JOptionPane.showMessageDialog(diffPanel, "Could not read file: "
+ bufferDocument.getName()
+ "\n" + ex.getMessage(),
"Error opening file", JOptionPane.ERROR_MESSAGE);
return;
}
}
void updateFileLabel(String name1, String name2) {
fileLabel.setText(name1, name2);
}
void doStopSearch() {
searchHits = null;
reDisplay();
}
private void checkSearch() {
doSearch();
}
SearchHits doSearch() {
int numberOfLines;
BufferDocumentIF doc;
String text;
int index, fromIndex;
boolean regularExpression;
String searchText;
SearchHit searchHit;
int offset;
int length;
SearchCommand searchCommand;
searchCommand = diffPanel.getSearchCommand();
if (searchCommand == null) {
return null;
}
searchText = searchCommand.getSearchText();
regularExpression = searchCommand.isRegularExpression();
doc = getBufferDocument();
numberOfLines = doc.getNumberOfLines();
searchHits = new SearchHits();
if (!StringUtil.isEmpty(searchText)) {
for (int line = 0; line < numberOfLines; line++) {
text = doc.getLineText(line);
if (!regularExpression) {
fromIndex = 0;
while ((index = text.indexOf(searchText, fromIndex)) != -1) {
offset = bufferDocument.getOffsetForLine(line);
if (offset < 0) {
continue;
}
searchHit = new SearchHit(line, offset + index, searchText.length());
searchHits.add(searchHit);
fromIndex = index + searchHit.getSize() + 1;
}
}
}
}
reDisplay();
return getSearchHits();
}
void setShowLineNumbers(boolean showLineNumbers) {
Border originalBorder;
String propertyName;
propertyName = "JMeld.originalBorder";
originalBorder = (Border) editor.getClientProperty(propertyName);
if (showLineNumbers) {
if (originalBorder == null) {
originalBorder = editor.getBorder();
editor.setBorder(new LineNumberBorder(this));
editor.putClientProperty(propertyName, originalBorder);
}
} else {
if (originalBorder != null) {
editor.setBorder(originalBorder);
editor.putClientProperty(propertyName, null);
}
}
}
SearchHits getSearchHits() {
return searchHits;
}
public void reDisplay() {
getHighlighter().setDoNotRepaint(true);
removeHighlights();
paintSearchHighlights();
paintRevisionHighlights();
getHighlighter().setDoNotRepaint(false);
getHighlighter().repaint();
}
private void paintSearchHighlights() {
if (searchHits != null) {
for (SearchHit sh : searchHits.getSearchHits()) {
setHighlight(JMHighlighter.LAYER2, sh.getFromOffset(),
sh.getToOffset(),
searchHits.isCurrent(sh)
? JMHighlightPainter.CURRENT_SEARCH: JMHighlightPainter.SEARCH);
}
}
}
private void paintRevisionHighlights() {
if (bufferDocument == null) {
return;
}
JMRevision revision = diffPanel.getCurrentRevision();
if (revision == null) {
return;
}
for (JMDelta delta : revision.getDeltas()) {
if (BufferDocumentIF.ORIGINAL.equals(name)) {
new HighlightOriginal(delta).highlight();
} else if (BufferDocumentIF.REVISED.equals(name)) {
new HighlightRevised(delta).highlight();
}
}
}
abstract class AbstractHighlight {
protected JMDelta delta;
public AbstractHighlight(JMDelta delta) {
this.delta = delta;
}
protected void highlight() {
int fromOffset;
int toOffset;
JMRevision changeRev;
JMChunk changeOriginal;
int fromOffset2;
int toOffset2;
fromOffset = bufferDocument.getOffsetForLine(getPrimaryChunk().getAnchor());
if (fromOffset < 0) {
return;
}
toOffset = bufferDocument.getOffsetForLine(getPrimaryChunk().getAnchor() + getPrimaryChunk().getSize());
if (toOffset < 0) {
return;
}
boolean isEndAndIsLastNewLine = isEndAndIsLastNewLine(toOffset);
JMHighlightPainter highlight = null;
if (delta.isChange()) {
if (delta.getOriginal().getSize() < MAXSIZE_CHANGE_DIFF
&& delta.getRevised().getSize() < MAXSIZE_CHANGE_DIFF) {
changeRev = delta.getChangeRevision();
if (changeRev != null) {
for (JMDelta changeDelta : changeRev.getDeltas()) {
changeOriginal = getPrimaryChunk(changeDelta);
if (changeOriginal.getSize() <= 0) {
continue;
}
fromOffset2 = fromOffset + changeOriginal.getAnchor();
toOffset2 = fromOffset2 + changeOriginal.getSize();
setHighlight(JMHighlighter.LAYER1, fromOffset2, toOffset2,
JMHighlightPainter.CHANGED_LIGHTER);
}
}
}
highlight = isEndAndIsLastNewLine ? JMHighlightPainter.CHANGED_NEWLINE : JMHighlightPainter.CHANGED;
} else {
if (isEmptyLine()) {
toOffset = fromOffset + 1;
}
if (delta.isAdd()) {
highlight = getAddedHighlightPainter(isOriginal(), isEndAndIsLastNewLine);
} else if (delta.isDelete()) {
highlight = getDeleteHighlightPainter(!isOriginal(), isEndAndIsLastNewLine);
}
}
setHighlight(fromOffset, toOffset, highlight);
}
/**
* If last change reach the end and has a newline as final char, final line is
* virtual. Document has no line for it, since newline stars that line but it
* contains nothing yet.
*
* @param toOffset last offset of change
*
* @see org.jmeld.ui.swing.JMHighlightPainter#ADDED_NEWLINE
* @see org.jmeld.ui.swing.JMHighlightPainter#CHANGED_NEWLINE
* @see org.jmeld.ui.swing.JMHighlightPainter#DELETED_NEWLINE
* @return
*/
private boolean isEndAndIsLastNewLine(int toOffset) {
boolean isEndAndIsLastNewLine = false;
try {
PlainDocument document = bufferDocument.getDocument();
int endOffset = toOffset - 1;
boolean changeReachEnd = endOffset == document.getLength();
boolean lastCharIsNewLine = "\n".equals(document.getText(endOffset, 1));
isEndAndIsLastNewLine = changeReachEnd && lastCharIsNewLine;
} catch (BadLocationException e) {
e.printStackTrace();
}
return isEndAndIsLastNewLine;
}
private JMChunk getPrimaryChunk() {
return getPrimaryChunk(delta);
}
private boolean isOriginal() {
return delta.getOriginal() == getPrimaryChunk();
}
private JMHighlightPainter getAddedHighlightPainter(boolean line, boolean isLastNewLine) {
return line
? JMHighlightPainter.ADDED_LINE
: isLastNewLine
? JMHighlightPainter.ADDED_NEWLINE
: JMHighlightPainter.ADDED;
}
private JMHighlightPainter getDeleteHighlightPainter(boolean line, boolean isLastNewLine) {
return line
? JMHighlightPainter.DELETED_LINE
: isLastNewLine
? JMHighlightPainter.DELETED_NEWLINE
: JMHighlightPainter.DELETED;
}
protected abstract JMChunk getPrimaryChunk(JMDelta changeDelta);
public abstract boolean isEmptyLine();
}
class HighlightOriginal extends AbstractHighlight {
public HighlightOriginal(JMDelta delta) {
super(delta);
}
public boolean isEmptyLine() {
return delta.isAdd();
}
protected JMChunk getPrimaryChunk(JMDelta changeDelta) {
return changeDelta.getOriginal();
}
}
class HighlightRevised extends AbstractHighlight {
public HighlightRevised(JMDelta delta) {
super(delta);
}
public boolean isEmptyLine() {
return delta.isDelete();
}
protected JMChunk getPrimaryChunk(JMDelta changeDelta) {
return changeDelta.getRevised();
}
}
private JMHighlighter getHighlighter() {
return (JMHighlighter) editor.getHighlighter();
}
private void removeHighlights() {
JMHighlighter jmhl;
jmhl = getHighlighter();
jmhl.removeHighlights(JMHighlighter.LAYER0);
jmhl.removeHighlights(JMHighlighter.LAYER1);
jmhl.removeHighlights(JMHighlighter.LAYER2);
}
private void setHighlight(int offset, int size,
Highlighter.HighlightPainter highlight) {
setHighlight(JMHighlighter.LAYER0, offset, size, highlight);
}
private void setHighlight(Integer layer, int offset, int size,
Highlighter.HighlightPainter highlight) {
try {
getHighlighter().addHighlight(layer, offset, size, highlight);
} catch (BadLocationException ex) {
ex.printStackTrace();
}
}
public ActionListener getSaveButtonAction() {
return new ActionListener() {
public void actionPerformed(ActionEvent ae) {
try {
bufferDocument.write();
} catch (Exception ex) {
JOptionPane.showMessageDialog(SwingUtilities.getRoot(editor),
"Could not save file: " + bufferDocument.getName() + "\n"
+ ex.getMessage(), "Error saving file",
JOptionPane.ERROR_MESSAGE);
}
}
};
}
public ActionListener getFileBoxAction() {
return new ActionListener() {
public void actionPerformed(ActionEvent ae) {
//System.out.println("fileBox: " + fileBox.getSelectedItem());
}
};
}
public void documentChanged(JMDocumentEvent de) {
if (de.getStartLine() == -1 && de.getDocumentEvent() == null) {
// Refresh the diff of whole document.
timer.restart();
} else {
// Try to update the revision instead of doing a full diff.
if (!diffPanel.revisionChanged(de)) {
timer.restart();
}
}
checkSearch();
checkActions();
}
private void checkActions() {
if (saveButton.isEnabled() != isDocumentChanged()) {
saveButton.setEnabled(isDocumentChanged());
}
diffPanel.checkActions();
}
boolean isDocumentChanged() {
return bufferDocument != null ? bufferDocument.isChanged() : false;
}
private ActionListener refresh() {
return new ActionListener() {
public void actionPerformed(ActionEvent ae) {
diffPanel.diff();
}
};
}
public FocusListener getFocusListener() {
return new FocusAdapter() {
@Override
public void focusGained(FocusEvent fe) {
diffPanel.setSelectedPanel(FilePanel.this);
}
};
}
public CaretListener getCaretListener() {
return new CaretListener() {
public void caretUpdate(CaretEvent fe) {
updateFilePanelBar();
}
};
}
public void setSelected(boolean selected) {
this.selected = selected;
updateFilePanelBar();
checkSearch();
}
private void updateFilePanelBar() {
if (filePanelBar != null) {
filePanelBar.update();
}
}
public boolean isSelected() {
return selected;
}
public void configurationChanged() {
initConfiguration();
}
private void initConfiguration() {
JMeldSettings settings;
boolean readonly;
Font font;
FontMetrics fm;
settings = getConfiguration();
setShowLineNumbers(settings.getEditor().getShowLineNumbers());
font = settings.getEditor().isCustomFontEnabled() ? settings.getEditor().getFont() : null;
font = font != null ? font : FontUtil.defaultTextAreaFont;
editor.setFont(font);
fm = editor.getFontMetrics(font);
scrollPane.getHorizontalScrollBar().setUnitIncrement(fm.getHeight());
getEditor().setTabSize(settings.getEditor().getTabSize());
readonly = false;
if (position == BufferDiffPanel.LEFT) {
readonly = settings.getEditor().getLeftsideReadonly();
} else if (position == BufferDiffPanel.RIGHT) {
readonly = settings.getEditor().getRightsideReadonly();
}
if (bufferDocument != null && bufferDocument.isReadonly()) {
readonly = true;
}
editor.setEditable(!readonly);
}
private JMeldSettings getConfiguration() {
return JMeldSettings.getInstance();
}
public String getSelectedText() {
return editor.getSelectedText();
}
}