better file compare
This commit is contained in:
parent
bcfa68f30d
commit
561beb1e3e
@ -1,54 +1,81 @@
|
||||
package cz.kamma.kfmanager.ui;
|
||||
|
||||
import cz.kamma.kfmanager.config.AppConfig;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.text.*;
|
||||
import javax.swing.text.BadLocationException;
|
||||
import javax.swing.BoundedRangeModel;
|
||||
import javax.swing.text.Element;
|
||||
import javax.swing.text.Style;
|
||||
import javax.swing.text.StyleConstants;
|
||||
import javax.swing.text.StyledDocument;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Window for comparing two files side by side
|
||||
* Window for comparing two files side by side.
|
||||
*/
|
||||
public class FileComparisonDialog extends JFrame {
|
||||
private final AppConfig config;
|
||||
private final File file1;
|
||||
private final File file2;
|
||||
|
||||
private final List<String> sourceLines1 = new ArrayList<>();
|
||||
private final List<String> sourceLines2 = new ArrayList<>();
|
||||
|
||||
private List<AlignedLine> alignedLines = new ArrayList<>();
|
||||
private List<Integer> visibleIndices = new ArrayList<>();
|
||||
private List<Integer> differenceVisibleRows = new ArrayList<>();
|
||||
|
||||
private JTextPane textPane1;
|
||||
private JTextPane textPane2;
|
||||
private JTextArea lineNumbers1;
|
||||
private JTextArea lineNumbers2;
|
||||
private JScrollPane scroll1;
|
||||
private JScrollPane scroll2;
|
||||
private List<String> lines1 = new ArrayList<>();
|
||||
private List<String> lines2 = new ArrayList<>();
|
||||
private int selectedLine1 = 0;
|
||||
private int selectedLine2 = 0;
|
||||
|
||||
private JCheckBox smartAlignCheck;
|
||||
private JCheckBox ignoreCaseCheck;
|
||||
private JCheckBox ignoreTrimCheck;
|
||||
private JCheckBox ignoreWhitespaceCheck;
|
||||
private JCheckBox onlyDifferencesCheck;
|
||||
private JLabel statusLabel;
|
||||
|
||||
private int selectedVisibleRow = -1;
|
||||
private int selectedLeftVisibleRow = -1;
|
||||
private int selectedRightVisibleRow = -1;
|
||||
private int selectedDifferencePointer = -1;
|
||||
private int manualAnchorLeftLine = -1;
|
||||
private int manualAnchorRightLine = -1;
|
||||
|
||||
public FileComparisonDialog(Window parent, File file1, File file2, AppConfig config) {
|
||||
super("Compare Files: " + file1.getName() + " vs " + file2.getName());
|
||||
this.file1 = file1;
|
||||
this.file2 = file2;
|
||||
this.config = config;
|
||||
|
||||
try {
|
||||
this.lines1 = readLines(file1);
|
||||
this.lines2 = readLines(file2);
|
||||
performSmartAlignment();
|
||||
sourceLines1.addAll(readLines(file1));
|
||||
sourceLines2.addAll(readLines(file2));
|
||||
} catch (IOException e) {
|
||||
JOptionPane.showMessageDialog(this, "Error reading files: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
|
||||
JOptionPane.showMessageDialog(parent, "Error reading files: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
|
||||
initComponents();
|
||||
updateDisplay();
|
||||
refreshComparison(true);
|
||||
|
||||
setSize(1000, 700);
|
||||
setSize(1100, 760);
|
||||
setLocationRelativeTo(parent);
|
||||
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
|
||||
|
||||
// Close on ESC
|
||||
getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "close");
|
||||
getRootPane().getActionMap().put("close", new AbstractAction() {
|
||||
@Override
|
||||
@ -59,152 +86,373 @@ public class FileComparisonDialog extends JFrame {
|
||||
}
|
||||
|
||||
private void initComponents() {
|
||||
setLayout(new BorderLayout());
|
||||
|
||||
JPanel centerPanel = new JPanel(new GridLayout(1, 2, 5, 0));
|
||||
|
||||
textPane1 = new JTextPane();
|
||||
textPane1.setEditable(false);
|
||||
textPane1.setFont(new Font("Monospaced", Font.PLAIN, 12));
|
||||
TextPaneListener listener1 = new TextPaneListener();
|
||||
textPane1.addMouseListener(listener1);
|
||||
scroll1 = new JScrollPane(textPane1);
|
||||
|
||||
lineNumbers1 = new JTextArea("1");
|
||||
lineNumbers1.setEditable(false);
|
||||
lineNumbers1.setBackground(Color.LIGHT_GRAY);
|
||||
lineNumbers1.setForeground(Color.DARK_GRAY);
|
||||
lineNumbers1.setFont(new Font("Monospaced", Font.PLAIN, 12));
|
||||
lineNumbers1.setMargin(new Insets(0, 5, 0, 5));
|
||||
scroll1.setRowHeaderView(lineNumbers1);
|
||||
|
||||
textPane2 = new JTextPane();
|
||||
textPane2.setEditable(false);
|
||||
textPane2.setFont(new Font("Monospaced", Font.PLAIN, 12));
|
||||
TextPaneListener listener2 = new TextPaneListener();
|
||||
textPane2.addMouseListener(listener2);
|
||||
scroll2 = new JScrollPane(textPane2);
|
||||
|
||||
lineNumbers2 = new JTextArea("1");
|
||||
lineNumbers2.setEditable(false);
|
||||
lineNumbers2.setBackground(Color.LIGHT_GRAY);
|
||||
lineNumbers2.setForeground(Color.DARK_GRAY);
|
||||
lineNumbers2.setFont(new Font("Monospaced", Font.PLAIN, 12));
|
||||
lineNumbers2.setMargin(new Insets(0, 5, 0, 5));
|
||||
scroll2.setRowHeaderView(lineNumbers2);
|
||||
|
||||
// Synchronize scrolling
|
||||
scroll1.getVerticalScrollBar().setModel(scroll2.getVerticalScrollBar().getModel());
|
||||
|
||||
centerPanel.add(scroll1);
|
||||
centerPanel.add(scroll2);
|
||||
|
||||
add(centerPanel, BorderLayout.CENTER);
|
||||
|
||||
JPanel bottomPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||
JButton syncButton = new JButton("Synchronize from here");
|
||||
syncButton.addActionListener(e -> synchronizeFromHere());
|
||||
bottomPanel.add(syncButton);
|
||||
|
||||
JButton closeButton = new JButton("Close");
|
||||
closeButton.addActionListener(e -> dispose());
|
||||
bottomPanel.add(closeButton);
|
||||
add(bottomPanel, BorderLayout.SOUTH);
|
||||
|
||||
// Apply appearance from config
|
||||
setLayout(new BorderLayout(6, 6));
|
||||
add(createTopToolbar(), BorderLayout.NORTH);
|
||||
add(createCenterPanel(), BorderLayout.CENTER);
|
||||
add(createBottomBar(), BorderLayout.SOUTH);
|
||||
applyAppearance();
|
||||
}
|
||||
|
||||
private class TextPaneListener extends java.awt.event.MouseAdapter {
|
||||
private JComponent createTopToolbar() {
|
||||
JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 4));
|
||||
|
||||
smartAlignCheck = new JCheckBox("Smart align", true);
|
||||
ignoreCaseCheck = new JCheckBox("Ignore case", false);
|
||||
ignoreTrimCheck = new JCheckBox("Ignore leading/trailing spaces", false);
|
||||
ignoreWhitespaceCheck = new JCheckBox("Ignore all whitespace changes", false);
|
||||
onlyDifferencesCheck = new JCheckBox("Show only differences", false);
|
||||
|
||||
JButton prevDiff = new JButton("Previous difference");
|
||||
JButton nextDiff = new JButton("Next difference");
|
||||
JButton synchronizeButton = new JButton("Synchronize from selected rows");
|
||||
JButton clearSyncButton = new JButton("Clear sync");
|
||||
JButton reloadButton = new JButton("Reload");
|
||||
|
||||
smartAlignCheck.addActionListener(e -> refreshComparison(false));
|
||||
ignoreCaseCheck.addActionListener(e -> refreshComparison(false));
|
||||
ignoreTrimCheck.addActionListener(e -> refreshComparison(false));
|
||||
ignoreWhitespaceCheck.addActionListener(e -> refreshComparison(false));
|
||||
onlyDifferencesCheck.addActionListener(e -> refreshComparison(true));
|
||||
reloadButton.addActionListener(e -> reloadAndRefresh());
|
||||
|
||||
prevDiff.addActionListener(e -> jumpToDifference(-1));
|
||||
nextDiff.addActionListener(e -> jumpToDifference(1));
|
||||
synchronizeButton.addActionListener(e -> synchronizeFromSelectedRows());
|
||||
clearSyncButton.addActionListener(e -> clearSynchronization());
|
||||
|
||||
panel.add(new JLabel("Options:"));
|
||||
panel.add(smartAlignCheck);
|
||||
panel.add(ignoreCaseCheck);
|
||||
panel.add(ignoreTrimCheck);
|
||||
panel.add(ignoreWhitespaceCheck);
|
||||
panel.add(onlyDifferencesCheck);
|
||||
panel.add(prevDiff);
|
||||
panel.add(nextDiff);
|
||||
panel.add(synchronizeButton);
|
||||
panel.add(clearSyncButton);
|
||||
panel.add(reloadButton);
|
||||
return panel;
|
||||
}
|
||||
|
||||
private JComponent createCenterPanel() {
|
||||
JPanel panel = new JPanel(new GridLayout(1, 2, 6, 0));
|
||||
|
||||
textPane1 = createTextPane(true);
|
||||
textPane2 = createTextPane(false);
|
||||
scroll1 = new JScrollPane(textPane1);
|
||||
scroll2 = new JScrollPane(textPane2);
|
||||
|
||||
lineNumbers1 = createLineNumberArea();
|
||||
lineNumbers2 = createLineNumberArea();
|
||||
scroll1.setRowHeaderView(lineNumbers1);
|
||||
scroll2.setRowHeaderView(lineNumbers2);
|
||||
|
||||
// Keep vertical scrolling synchronized in both directions.
|
||||
BoundedRangeModel sharedModel = scroll1.getVerticalScrollBar().getModel();
|
||||
scroll2.getVerticalScrollBar().setModel(sharedModel);
|
||||
|
||||
panel.add(scroll1);
|
||||
panel.add(scroll2);
|
||||
return panel;
|
||||
}
|
||||
|
||||
private JComponent createBottomBar() {
|
||||
JPanel bar = new JPanel(new BorderLayout(8, 0));
|
||||
statusLabel = new JLabel(" ");
|
||||
bar.add(statusLabel, BorderLayout.CENTER);
|
||||
|
||||
JPanel buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 2));
|
||||
JButton closeButton = new JButton("Close");
|
||||
closeButton.addActionListener(e -> dispose());
|
||||
buttons.add(closeButton);
|
||||
bar.add(buttons, BorderLayout.EAST);
|
||||
return bar;
|
||||
}
|
||||
|
||||
private JTextPane createTextPane(boolean isLeftPane) {
|
||||
JTextPane pane = new JTextPane();
|
||||
pane.setEditable(false);
|
||||
pane.setFont(new Font("Monospaced", Font.PLAIN, 12));
|
||||
pane.addMouseListener(new java.awt.event.MouseAdapter() {
|
||||
@Override
|
||||
public void mousePressed(java.awt.event.MouseEvent e) {
|
||||
handleSelection(e);
|
||||
if (e.isPopupTrigger()) showMenu(e);
|
||||
selectLineAtClick((JTextPane) e.getComponent(), e.getPoint(), isLeftPane);
|
||||
}
|
||||
@Override
|
||||
public void mouseReleased(java.awt.event.MouseEvent e) {
|
||||
handleSelection(e);
|
||||
if (e.isPopupTrigger()) showMenu(e);
|
||||
});
|
||||
return pane;
|
||||
}
|
||||
private void handleSelection(java.awt.event.MouseEvent e) {
|
||||
if (SwingUtilities.isLeftMouseButton(e)) {
|
||||
JTextPane pane = (JTextPane) e.getComponent();
|
||||
@SuppressWarnings("deprecation")
|
||||
int pos = pane.viewToModel(e.getPoint());
|
||||
if (pos >= 0) {
|
||||
|
||||
private JTextArea createLineNumberArea() {
|
||||
JTextArea area = new JTextArea("1");
|
||||
area.setEditable(false);
|
||||
area.setBackground(Color.LIGHT_GRAY);
|
||||
area.setForeground(Color.DARK_GRAY);
|
||||
area.setFont(new Font("Monospaced", Font.PLAIN, 12));
|
||||
area.setMargin(new Insets(0, 5, 0, 5));
|
||||
return area;
|
||||
}
|
||||
|
||||
private void reloadAndRefresh() {
|
||||
sourceLines1.clear();
|
||||
sourceLines2.clear();
|
||||
try {
|
||||
sourceLines1.addAll(readLines(file1));
|
||||
sourceLines2.addAll(readLines(file2));
|
||||
} catch (IOException e) {
|
||||
JOptionPane.showMessageDialog(this, "Error reading files: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
refreshComparison(true);
|
||||
}
|
||||
|
||||
private void synchronizeFromSelectedRows() {
|
||||
if (selectedLeftVisibleRow < 0 || selectedRightVisibleRow < 0) {
|
||||
JOptionPane.showMessageDialog(this,
|
||||
"Select one line in left pane and one line in right pane first.",
|
||||
"Synchronize",
|
||||
JOptionPane.INFORMATION_MESSAGE);
|
||||
return;
|
||||
}
|
||||
if (selectedLeftVisibleRow >= visibleIndices.size() || selectedRightVisibleRow >= visibleIndices.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
AlignedLine leftLine = alignedLines.get(visibleIndices.get(selectedLeftVisibleRow));
|
||||
AlignedLine rightLine = alignedLines.get(visibleIndices.get(selectedRightVisibleRow));
|
||||
|
||||
if (leftLine.leftNumber <= 0 || rightLine.rightNumber <= 0) {
|
||||
JOptionPane.showMessageDialog(this,
|
||||
"Selected rows must contain real lines in both panes (not empty gap rows).",
|
||||
"Synchronize",
|
||||
JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
manualAnchorLeftLine = leftLine.leftNumber;
|
||||
manualAnchorRightLine = rightLine.rightNumber;
|
||||
refreshComparison(false);
|
||||
focusAnchorRow();
|
||||
}
|
||||
|
||||
private void clearSynchronization() {
|
||||
manualAnchorLeftLine = -1;
|
||||
manualAnchorRightLine = -1;
|
||||
refreshComparison(false);
|
||||
}
|
||||
|
||||
private void focusAnchorRow() {
|
||||
if (manualAnchorLeftLine <= 0 || manualAnchorRightLine <= 0) return;
|
||||
for (int visibleRow = 0; visibleRow < visibleIndices.size(); visibleRow++) {
|
||||
AlignedLine line = alignedLines.get(visibleIndices.get(visibleRow));
|
||||
if (line.leftNumber == manualAnchorLeftLine && line.rightNumber == manualAnchorRightLine) {
|
||||
selectedVisibleRow = visibleRow;
|
||||
selectedLeftVisibleRow = visibleRow;
|
||||
selectedRightVisibleRow = visibleRow;
|
||||
render();
|
||||
updateStatus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshComparison(boolean resetSelection) {
|
||||
ComparisonOptions options = getOptions();
|
||||
List<String> comparableLeft = buildComparableLines(sourceLines1, options);
|
||||
List<String> comparableRight = buildComparableLines(sourceLines2, options);
|
||||
alignedLines = alignWithOptionalAnchor(sourceLines1, sourceLines2, comparableLeft, comparableRight, options);
|
||||
|
||||
visibleIndices = new ArrayList<>();
|
||||
differenceVisibleRows = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < alignedLines.size(); i++) {
|
||||
AlignedLine line = alignedLines.get(i);
|
||||
if (!options.onlyDifferences || line.different) {
|
||||
int visibleRow = visibleIndices.size();
|
||||
visibleIndices.add(i);
|
||||
if (line.different) differenceVisibleRows.add(visibleRow);
|
||||
}
|
||||
}
|
||||
|
||||
if (resetSelection) {
|
||||
selectedVisibleRow = -1;
|
||||
selectedLeftVisibleRow = -1;
|
||||
selectedRightVisibleRow = -1;
|
||||
selectedDifferencePointer = differenceVisibleRows.isEmpty() ? -1 : 0;
|
||||
} else if (selectedVisibleRow >= visibleIndices.size()) {
|
||||
selectedVisibleRow = visibleIndices.isEmpty() ? -1 : visibleIndices.size() - 1;
|
||||
selectedLeftVisibleRow = selectedVisibleRow;
|
||||
selectedRightVisibleRow = selectedVisibleRow;
|
||||
}
|
||||
|
||||
render();
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
private ComparisonOptions getOptions() {
|
||||
ComparisonOptions options = new ComparisonOptions();
|
||||
options.smartAlign = smartAlignCheck.isSelected();
|
||||
options.ignoreCase = ignoreCaseCheck.isSelected();
|
||||
options.ignoreTrim = ignoreTrimCheck.isSelected();
|
||||
options.ignoreWhitespace = ignoreWhitespaceCheck.isSelected();
|
||||
options.onlyDifferences = onlyDifferencesCheck.isSelected();
|
||||
return options;
|
||||
}
|
||||
|
||||
private void render() {
|
||||
try {
|
||||
StyledDocument doc1 = textPane1.getStyledDocument();
|
||||
StyledDocument doc2 = textPane2.getStyledDocument();
|
||||
doc1.remove(0, doc1.getLength());
|
||||
doc2.remove(0, doc2.getLength());
|
||||
|
||||
StringBuilder ln1 = new StringBuilder();
|
||||
StringBuilder ln2 = new StringBuilder();
|
||||
|
||||
Color bg = textPane1.getBackground();
|
||||
boolean dark = isDark(bg);
|
||||
Style normal1 = createStyle(textPane1, "normal1", null, null);
|
||||
Style normal2 = createStyle(textPane2, "normal2", null, null);
|
||||
Style changed1 = createStyle(textPane1, "changed1", dark ? new Color(95, 45, 45) : new Color(255, 220, 220), null);
|
||||
Style changed2 = createStyle(textPane2, "changed2", dark ? new Color(95, 45, 45) : new Color(255, 220, 220), null);
|
||||
Style onlyLeft = createStyle(textPane1, "onlyLeft", dark ? new Color(85, 62, 40) : new Color(255, 236, 210), null);
|
||||
Style onlyRight = createStyle(textPane2, "onlyRight", dark ? new Color(45, 78, 55) : new Color(220, 255, 230), null);
|
||||
Style selected1 = createStyle(textPane1, "selected1", dark ? new Color(45, 65, 105) : new Color(210, 230, 255), null);
|
||||
Style selected2 = createStyle(textPane2, "selected2", dark ? new Color(45, 65, 105) : new Color(210, 230, 255), null);
|
||||
|
||||
for (int visibleRow = 0; visibleRow < visibleIndices.size(); visibleRow++) {
|
||||
AlignedLine line = alignedLines.get(visibleIndices.get(visibleRow));
|
||||
boolean selected = visibleRow == selectedVisibleRow
|
||||
|| visibleRow == selectedLeftVisibleRow
|
||||
|| visibleRow == selectedRightVisibleRow;
|
||||
|
||||
Style style1;
|
||||
Style style2;
|
||||
if (selected) {
|
||||
style1 = selected1;
|
||||
style2 = selected2;
|
||||
} else if (!line.different) {
|
||||
style1 = normal1;
|
||||
style2 = normal2;
|
||||
} else if (line.left == null) {
|
||||
style1 = changed1;
|
||||
style2 = onlyRight;
|
||||
} else if (line.right == null) {
|
||||
style1 = onlyLeft;
|
||||
style2 = changed2;
|
||||
} else {
|
||||
style1 = changed1;
|
||||
style2 = changed2;
|
||||
}
|
||||
|
||||
doc1.insertString(doc1.getLength(), (line.left != null ? line.left : "") + "\n", style1);
|
||||
doc2.insertString(doc2.getLength(), (line.right != null ? line.right : "") + "\n", style2);
|
||||
ln1.append(line.leftNumber > 0 ? line.leftNumber : "").append('\n');
|
||||
ln2.append(line.rightNumber > 0 ? line.rightNumber : "").append('\n');
|
||||
}
|
||||
|
||||
lineNumbers1.setText(ln1.toString());
|
||||
lineNumbers2.setText(ln2.toString());
|
||||
|
||||
if (selectedVisibleRow < 0 && !visibleIndices.isEmpty()) {
|
||||
scrollToVisibleRow(0);
|
||||
} else if (selectedVisibleRow >= 0) {
|
||||
scrollToVisibleRow(selectedVisibleRow);
|
||||
} else {
|
||||
textPane1.setCaretPosition(0);
|
||||
textPane2.setCaretPosition(0);
|
||||
}
|
||||
} catch (BadLocationException e) {
|
||||
JOptionPane.showMessageDialog(this, "Error updating display: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
private Style createStyle(JTextPane pane, String name, Color background, Color foreground) {
|
||||
Style style = pane.addStyle(name, null);
|
||||
if (background != null) {
|
||||
StyleConstants.setBackground(style, background);
|
||||
}
|
||||
if (foreground != null) {
|
||||
StyleConstants.setForeground(style, foreground);
|
||||
} else {
|
||||
StyleConstants.setForeground(style, pane.getForeground());
|
||||
}
|
||||
return style;
|
||||
}
|
||||
|
||||
private void jumpToDifference(int direction) {
|
||||
if (differenceVisibleRows.isEmpty()) {
|
||||
Toolkit.getDefaultToolkit().beep();
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedDifferencePointer < 0) {
|
||||
selectedDifferencePointer = 0;
|
||||
} else {
|
||||
int size = differenceVisibleRows.size();
|
||||
selectedDifferencePointer = (selectedDifferencePointer + direction + size) % size;
|
||||
}
|
||||
selectedVisibleRow = differenceVisibleRows.get(selectedDifferencePointer);
|
||||
selectedLeftVisibleRow = selectedVisibleRow;
|
||||
selectedRightVisibleRow = selectedVisibleRow;
|
||||
render();
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
private void selectLineAtClick(JTextPane pane, Point point, boolean isLeftPane) {
|
||||
int pos = pane.viewToModel2D(point);
|
||||
if (pos < 0) return;
|
||||
Element root = pane.getDocument().getDefaultRootElement();
|
||||
int lineIdx = root.getElementIndex(pos);
|
||||
int lineIndex = root.getElementIndex(pos);
|
||||
if (lineIndex < 0 || lineIndex >= visibleIndices.size()) return;
|
||||
|
||||
if (pane == textPane1) selectedLine1 = lineIdx;
|
||||
else if (pane == textPane2) selectedLine2 = lineIdx;
|
||||
|
||||
Element line = root.getElement(lineIdx);
|
||||
int start = line.getStartOffset();
|
||||
int end = Math.min(line.getEndOffset(), pane.getDocument().getLength());
|
||||
|
||||
// Use invokeLater to ensure selection happens after default UI behavior
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
pane.requestFocusInWindow();
|
||||
pane.setCaretPosition(start);
|
||||
pane.moveCaretPosition(end);
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
// ignore
|
||||
selectedVisibleRow = lineIndex;
|
||||
if (isLeftPane) {
|
||||
selectedLeftVisibleRow = lineIndex;
|
||||
} else {
|
||||
selectedRightVisibleRow = lineIndex;
|
||||
}
|
||||
selectedDifferencePointer = -1;
|
||||
for (int i = 0; i < differenceVisibleRows.size(); i++) {
|
||||
if (differenceVisibleRows.get(i) == selectedVisibleRow) {
|
||||
selectedDifferencePointer = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private void showMenu(java.awt.event.MouseEvent e) {
|
||||
JPopupMenu menu = new JPopupMenu();
|
||||
JMenuItem syncItem = new JMenuItem("Synchronize from here");
|
||||
syncItem.addActionListener(event -> synchronizeFromHere());
|
||||
menu.add(syncItem);
|
||||
menu.show(e.getComponent(), e.getX(), e.getY());
|
||||
}
|
||||
render();
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
private void synchronizeFromHere() {
|
||||
private void scrollToVisibleRow(int row) {
|
||||
if (row < 0) return;
|
||||
try {
|
||||
int line1 = selectedLine1;
|
||||
int line2 = selectedLine2;
|
||||
Element root1 = textPane1.getDocument().getDefaultRootElement();
|
||||
Element root2 = textPane2.getDocument().getDefaultRootElement();
|
||||
if (row >= root1.getElementCount() || row >= root2.getElementCount()) return;
|
||||
|
||||
if (line1 < line2) {
|
||||
int diff = line2 - line1;
|
||||
for (int i = 0; i < diff; i++) {
|
||||
lines1.add(line1, null);
|
||||
}
|
||||
} else if (line2 < line1) {
|
||||
int diff = line1 - line2;
|
||||
for (int i = 0; i < diff; i++) {
|
||||
lines2.add(line2, null);
|
||||
Element line1 = root1.getElement(row);
|
||||
Element line2 = root2.getElement(row);
|
||||
int pos1 = line1.getStartOffset();
|
||||
int pos2 = line2.getStartOffset();
|
||||
|
||||
textPane1.setCaretPosition(pos1);
|
||||
textPane2.setCaretPosition(pos2);
|
||||
} catch (Exception ignored) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
updateDisplay(false);
|
||||
|
||||
// Restore selection and scroll to the newly synced line
|
||||
int newLineIdx = Math.max(line1, line2);
|
||||
selectedLine1 = newLineIdx;
|
||||
selectedLine2 = newLineIdx;
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
Element newRoot = textPane1.getDocument().getDefaultRootElement();
|
||||
if (newLineIdx < newRoot.getElementCount()) {
|
||||
Element lineElem = newRoot.getElement(newLineIdx);
|
||||
int start = lineElem.getStartOffset();
|
||||
int end = Math.min(lineElem.getEndOffset(), textPane1.getDocument().getLength());
|
||||
|
||||
textPane1.requestFocusInWindow();
|
||||
textPane1.setCaretPosition(start);
|
||||
textPane1.moveCaretPosition(end);
|
||||
|
||||
textPane2.setCaretPosition(start);
|
||||
textPane2.moveCaretPosition(end);
|
||||
}
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
private void updateStatus() {
|
||||
int different = 0;
|
||||
for (AlignedLine line : alignedLines) {
|
||||
if (line.different) different++;
|
||||
}
|
||||
String alignMode = smartAlignCheck.isSelected() ? "smart" : "by position";
|
||||
String visibleInfo = onlyDifferencesCheck.isSelected()
|
||||
? "showing only differences"
|
||||
: "showing all lines";
|
||||
String syncInfo = (manualAnchorLeftLine > 0 && manualAnchorRightLine > 0)
|
||||
? " | sync L" + manualAnchorLeftLine + " -> R" + manualAnchorRightLine
|
||||
: "";
|
||||
statusLabel.setText("Differences: " + different + " | " + visibleInfo + " | align: " + alignMode + syncInfo);
|
||||
}
|
||||
|
||||
private void applyAppearance() {
|
||||
@ -213,28 +461,24 @@ public class FileComparisonDialog extends JFrame {
|
||||
textPane1.setBackground(bg);
|
||||
textPane2.setBackground(bg);
|
||||
boolean dark = isDark(bg);
|
||||
textPane1.setForeground(dark ? Color.WHITE : Color.BLACK);
|
||||
textPane2.setForeground(dark ? Color.WHITE : Color.BLACK);
|
||||
textPane1.setCaretColor(dark ? Color.WHITE : Color.BLACK);
|
||||
textPane2.setCaretColor(dark ? Color.WHITE : Color.BLACK);
|
||||
Color fg = dark ? Color.WHITE : Color.BLACK;
|
||||
textPane1.setForeground(fg);
|
||||
textPane2.setForeground(fg);
|
||||
textPane1.setCaretColor(fg);
|
||||
textPane2.setCaretColor(fg);
|
||||
|
||||
if (lineNumbers1 != null) {
|
||||
lineNumbers1.setBackground(dark ? bg.brighter() : bg.darker());
|
||||
lineNumbers1.setForeground(dark ? Color.LIGHT_GRAY : Color.DARK_GRAY);
|
||||
}
|
||||
if (lineNumbers2 != null) {
|
||||
lineNumbers2.setBackground(dark ? bg.brighter() : bg.darker());
|
||||
lineNumbers2.setForeground(dark ? Color.LIGHT_GRAY : Color.DARK_GRAY);
|
||||
}
|
||||
}
|
||||
Font f = config.getGlobalFont();
|
||||
if (f != null) {
|
||||
// Use monospaced variant of the font if possible, or just the same size
|
||||
Font mono = new Font("Monospaced", Font.PLAIN, f.getSize());
|
||||
textPane1.setFont(mono);
|
||||
textPane2.setFont(mono);
|
||||
if (lineNumbers1 != null) lineNumbers1.setFont(mono);
|
||||
if (lineNumbers2 != null) lineNumbers2.setFont(mono);
|
||||
lineNumbers1.setFont(mono);
|
||||
lineNumbers2.setFont(mono);
|
||||
}
|
||||
}
|
||||
|
||||
@ -242,128 +486,183 @@ public class FileComparisonDialog extends JFrame {
|
||||
return (0.299 * c.getRed() + 0.587 * c.getGreen() + 0.114 * c.getBlue()) / 255 < 0.5;
|
||||
}
|
||||
|
||||
private void updateDisplay() {
|
||||
updateDisplay(true);
|
||||
private List<AlignedLine> alignWithOptionalAnchor(List<String> left, List<String> right,
|
||||
List<String> comparableLeft, List<String> comparableRight,
|
||||
ComparisonOptions options) {
|
||||
boolean validAnchor =
|
||||
manualAnchorLeftLine > 0
|
||||
&& manualAnchorRightLine > 0
|
||||
&& manualAnchorLeftLine <= left.size()
|
||||
&& manualAnchorRightLine <= right.size();
|
||||
if (!validAnchor) {
|
||||
return options.smartAlign
|
||||
? alignSmart(left, right, comparableLeft, comparableRight)
|
||||
: alignByPosition(left, right, comparableLeft, comparableRight);
|
||||
}
|
||||
|
||||
private void updateDisplay(boolean resetCaret) {
|
||||
try {
|
||||
textPane1.setText("");
|
||||
textPane2.setText("");
|
||||
StyledDocument doc1 = textPane1.getStyledDocument();
|
||||
StyledDocument doc2 = textPane2.getStyledDocument();
|
||||
StringBuilder ln1 = new StringBuilder();
|
||||
StringBuilder ln2 = new StringBuilder();
|
||||
int leftAnchorIndex = manualAnchorLeftLine - 1;
|
||||
int rightAnchorIndex = manualAnchorRightLine - 1;
|
||||
|
||||
Style diffStyle = textPane1.addStyle("diff", null);
|
||||
Color bg = textPane1.getBackground();
|
||||
boolean dark = isDark(bg);
|
||||
StyleConstants.setBackground(diffStyle, dark ? new Color(100, 30, 30) : new Color(255, 200, 200));
|
||||
StyleConstants.setForeground(diffStyle, dark ? Color.WHITE : Color.BLACK);
|
||||
List<AlignedLine> result = new ArrayList<>();
|
||||
List<AlignedLine> prefix = options.smartAlign
|
||||
? alignSmartRange(left, right, comparableLeft, comparableRight, 0, leftAnchorIndex, 0, rightAnchorIndex)
|
||||
: alignByPositionRange(left, right, comparableLeft, comparableRight, 0, leftAnchorIndex, 0, rightAnchorIndex);
|
||||
result.addAll(prefix);
|
||||
|
||||
int maxLines = Math.max(lines1.size(), lines2.size());
|
||||
int counter1 = 1;
|
||||
int counter2 = 1;
|
||||
result.add(new AlignedLine(
|
||||
left.get(leftAnchorIndex),
|
||||
right.get(rightAnchorIndex),
|
||||
manualAnchorLeftLine,
|
||||
manualAnchorRightLine,
|
||||
!equalsComparable(comparableLeft.get(leftAnchorIndex), comparableRight.get(rightAnchorIndex))
|
||||
));
|
||||
|
||||
for (int i = 0; i < maxLines; i++) {
|
||||
String l1 = i < lines1.size() ? lines1.get(i) : null;
|
||||
String l2 = i < lines2.size() ? lines2.get(i) : null;
|
||||
List<AlignedLine> suffix = options.smartAlign
|
||||
? alignSmartRange(left, right, comparableLeft, comparableRight, leftAnchorIndex + 1, left.size(), rightAnchorIndex + 1, right.size())
|
||||
: alignByPositionRange(left, right, comparableLeft, comparableRight, leftAnchorIndex + 1, left.size(), rightAnchorIndex + 1, right.size());
|
||||
result.addAll(suffix);
|
||||
|
||||
boolean different = (l1 == null && l2 != null) || (l1 != null && l2 == null) || (l1 != null && l2 != null && !l1.equals(l2));
|
||||
Style s = different ? diffStyle : null;
|
||||
|
||||
if (l1 != null) {
|
||||
doc1.insertString(doc1.getLength(), l1 + "\n", s);
|
||||
ln1.append(counter1++).append("\n");
|
||||
} else {
|
||||
doc1.insertString(doc1.getLength(), "\n", s);
|
||||
ln1.append("\n");
|
||||
return result;
|
||||
}
|
||||
|
||||
if (l2 != null) {
|
||||
doc2.insertString(doc2.getLength(), l2 + "\n", s);
|
||||
ln2.append(counter2++).append("\n");
|
||||
} else {
|
||||
doc2.insertString(doc2.getLength(), "\n", s);
|
||||
ln2.append("\n");
|
||||
}
|
||||
private List<AlignedLine> alignByPosition(List<String> left, List<String> right, List<String> comparableLeft, List<String> comparableRight) {
|
||||
return alignByPositionRange(left, right, comparableLeft, comparableRight, 0, left.size(), 0, right.size());
|
||||
}
|
||||
|
||||
if (lineNumbers1 != null) lineNumbers1.setText(ln1.toString());
|
||||
if (lineNumbers2 != null) lineNumbers2.setText(ln2.toString());
|
||||
|
||||
if (resetCaret) {
|
||||
textPane1.setCaretPosition(0);
|
||||
textPane2.setCaretPosition(0);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
JOptionPane.showMessageDialog(this, "Error updating display: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
|
||||
private List<AlignedLine> alignByPositionRange(List<String> left, List<String> right, List<String> comparableLeft, List<String> comparableRight,
|
||||
int leftStart, int leftEnd, int rightStart, int rightEnd) {
|
||||
int leftLen = leftEnd - leftStart;
|
||||
int rightLen = rightEnd - rightStart;
|
||||
int max = Math.max(leftLen, rightLen);
|
||||
List<AlignedLine> result = new ArrayList<>(max);
|
||||
for (int i = 0; i < max; i++) {
|
||||
int li = leftStart + i;
|
||||
int ri = rightStart + i;
|
||||
String l = li < leftEnd ? left.get(li) : null;
|
||||
String r = ri < rightEnd ? right.get(ri) : null;
|
||||
String cl = li < leftEnd ? comparableLeft.get(li) : null;
|
||||
String cr = ri < rightEnd ? comparableRight.get(ri) : null;
|
||||
int leftNum = li < leftEnd ? li + 1 : -1;
|
||||
int rightNum = ri < rightEnd ? ri + 1 : -1;
|
||||
result.add(new AlignedLine(l, r, leftNum, rightNum, !equalsComparable(cl, cr)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void performSmartAlignment() {
|
||||
if (lines1.isEmpty() || lines2.isEmpty()) return;
|
||||
private List<AlignedLine> alignSmart(List<String> left, List<String> right, List<String> comparableLeft, List<String> comparableRight) {
|
||||
return alignSmartRange(left, right, comparableLeft, comparableRight, 0, left.size(), 0, right.size());
|
||||
}
|
||||
|
||||
private List<AlignedLine> alignSmartRange(List<String> left, List<String> right, List<String> comparableLeft, List<String> comparableRight,
|
||||
int leftStart, int leftEnd, int rightStart, int rightEnd) {
|
||||
int n = leftEnd - leftStart;
|
||||
int m = rightEnd - rightStart;
|
||||
if (n == 0 && m == 0) return new ArrayList<>();
|
||||
|
||||
// Skip alignment for very large files to avoid O(N^2) memory/time issues
|
||||
int maxLines = config.getMaxCompareLines();
|
||||
if (lines1.size() > maxLines || lines2.size() > maxLines) return;
|
||||
if (n > maxLines || m > maxLines) {
|
||||
return alignByPositionRange(left, right, comparableLeft, comparableRight, leftStart, leftEnd, rightStart, rightEnd);
|
||||
}
|
||||
|
||||
int n = lines1.size();
|
||||
int m = lines2.size();
|
||||
int[][] dp = new int[n + 1][m + 1];
|
||||
int[][] lcs = new int[n + 1][m + 1];
|
||||
|
||||
// LCS computation with similarity check (exact or trimmed match)
|
||||
for (int i = 1; i <= n; i++) {
|
||||
for (int j = 1; j <= m; j++) {
|
||||
String s1 = lines1.get(i - 1);
|
||||
String s2 = lines2.get(j - 1);
|
||||
if (s1.equals(s2) || (!s1.trim().isEmpty() && s1.trim().equals(s2.trim()))) {
|
||||
dp[i][j] = dp[i - 1][j - 1] + 1;
|
||||
if (equalsComparable(comparableLeft.get(leftStart + i - 1), comparableRight.get(rightStart + j - 1))) {
|
||||
lcs[i][j] = lcs[i - 1][j - 1] + 1;
|
||||
} else {
|
||||
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
||||
lcs[i][j] = Math.max(lcs[i - 1][j], lcs[i][j - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<String> aligned1 = new ArrayList<>();
|
||||
List<String> aligned2 = new ArrayList<>();
|
||||
int i = n, j = m;
|
||||
List<AlignedLine> reversed = new ArrayList<>();
|
||||
int i = n;
|
||||
int j = m;
|
||||
while (i > 0 || j > 0) {
|
||||
if (i > 0 && j > 0) {
|
||||
String s1 = lines1.get(i - 1);
|
||||
String s2 = lines2.get(j - 1);
|
||||
if (s1.equals(s2) || (!s1.trim().isEmpty() && s1.trim().equals(s2.trim()))) {
|
||||
aligned1.add(0, s1);
|
||||
aligned2.add(0, s2);
|
||||
if (i > 0 && j > 0 && equalsComparable(comparableLeft.get(leftStart + i - 1), comparableRight.get(rightStart + j - 1))) {
|
||||
int li = leftStart + i - 1;
|
||||
int ri = rightStart + j - 1;
|
||||
String l = left.get(li);
|
||||
String r = right.get(ri);
|
||||
reversed.add(new AlignedLine(l, r, li + 1, ri + 1, !equalsComparable(comparableLeft.get(li), comparableRight.get(ri))));
|
||||
i--;
|
||||
j--;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (j > 0 && (i == 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
||||
aligned1.add(0, null); // Added gap marker
|
||||
aligned2.add(0, lines2.get(j - 1));
|
||||
} else if (j > 0 && (i == 0 || lcs[i][j - 1] >= lcs[i - 1][j])) {
|
||||
int ri = rightStart + j - 1;
|
||||
reversed.add(new AlignedLine(null, right.get(ri), -1, ri + 1, true));
|
||||
j--;
|
||||
} else if (i > 0) {
|
||||
aligned1.add(0, lines1.get(i - 1));
|
||||
aligned2.add(0, null); // Added gap marker
|
||||
int li = leftStart + i - 1;
|
||||
reversed.add(new AlignedLine(left.get(li), null, li + 1, -1, true));
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
this.lines1 = aligned1;
|
||||
this.lines2 = aligned2;
|
||||
List<AlignedLine> result = new ArrayList<>(reversed.size());
|
||||
for (int idx = reversed.size() - 1; idx >= 0; idx--) {
|
||||
result.add(reversed.get(idx));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<String> buildComparableLines(List<String> lines, ComparisonOptions options) {
|
||||
List<String> comparable = new ArrayList<>(lines.size());
|
||||
for (String line : lines) {
|
||||
comparable.add(normalize(line, options));
|
||||
}
|
||||
return comparable;
|
||||
}
|
||||
|
||||
private boolean equalsComparable(String left, String right) {
|
||||
if (left == null || right == null) return left == right;
|
||||
return left.equals(right);
|
||||
}
|
||||
|
||||
private String normalize(String text, ComparisonOptions options) {
|
||||
if (text == null) return "";
|
||||
String normalized = text;
|
||||
if (options.ignoreTrim) {
|
||||
normalized = normalized.trim();
|
||||
}
|
||||
if (options.ignoreWhitespace) {
|
||||
normalized = normalized.replaceAll("\\s+", "");
|
||||
}
|
||||
if (options.ignoreCase) {
|
||||
normalized = normalized.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private List<String> readLines(File f) throws IOException {
|
||||
List<String> lines = new ArrayList<>();
|
||||
try (BufferedReader br = new BufferedReader(new FileReader(f))) {
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
lines.add(line);
|
||||
try {
|
||||
return Files.readAllLines(f.toPath(), StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
return Files.readAllLines(f.toPath());
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
|
||||
private static class ComparisonOptions {
|
||||
boolean smartAlign;
|
||||
boolean ignoreCase;
|
||||
boolean ignoreTrim;
|
||||
boolean ignoreWhitespace;
|
||||
boolean onlyDifferences;
|
||||
}
|
||||
|
||||
private static class AlignedLine {
|
||||
final String left;
|
||||
final String right;
|
||||
final int leftNumber;
|
||||
final int rightNumber;
|
||||
final boolean different;
|
||||
|
||||
AlignedLine(String left, String right, int leftNumber, int rightNumber, boolean different) {
|
||||
this.left = left;
|
||||
this.right = right;
|
||||
this.leftNumber = leftNumber;
|
||||
this.rightNumber = rightNumber;
|
||||
this.different = different;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user