file comparison draft

This commit is contained in:
Radek Davidek 2026-01-28 22:14:26 +01:00
parent 1c7e5d3e64
commit 4756d4f486
2 changed files with 321 additions and 0 deletions

View File

@ -0,0 +1,263 @@
package cz.kamma.kfmanager.ui;
import cz.kamma.kfmanager.config.AppConfig;
import javax.swing.*;
import javax.swing.text.*;
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.util.ArrayList;
import java.util.List;
/**
* Window for comparing two files side by side
*/
public class FileComparisonDialog extends JFrame {
private final AppConfig config;
private JTextPane textPane1;
private JTextPane textPane2;
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;
public FileComparisonDialog(Window parent, File file1, File file2, AppConfig config) {
super("Compare Files: " + file1.getName() + " vs " + file2.getName());
this.config = config;
try {
this.lines1 = readLines(file1);
this.lines2 = readLines(file2);
} catch (IOException e) {
JOptionPane.showMessageDialog(this, "Error reading files: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
}
initComponents();
updateDisplay();
setSize(1000, 700);
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
public void actionPerformed(ActionEvent e) {
dispose();
}
});
}
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);
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);
// 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
applyAppearance();
}
private class TextPaneListener extends java.awt.event.MouseAdapter {
@Override
public void mousePressed(java.awt.event.MouseEvent e) {
handleSelection(e);
if (e.isPopupTrigger()) showMenu(e);
}
@Override
public void mouseReleased(java.awt.event.MouseEvent e) {
handleSelection(e);
if (e.isPopupTrigger()) showMenu(e);
}
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) {
try {
Element root = pane.getDocument().getDefaultRootElement();
int lineIdx = root.getElementIndex(pos);
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
}
}
}
}
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());
}
}
private void synchronizeFromHere() {
try {
int line1 = selectedLine1;
int line2 = selectedLine2;
if (line1 < line2) {
int diff = line2 - line1;
for (int i = 0; i < diff; i++) {
lines1.add(line1, "");
}
} else if (line2 < line1) {
int diff = line1 - line2;
for (int i = 0; i < diff; i++) {
lines2.add(line2, "");
}
}
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 applyAppearance() {
Color bg = config.getBackgroundColor();
if (bg != null) {
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);
}
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);
}
}
private boolean isDark(Color c) {
return (0.299 * c.getRed() + 0.587 * c.getGreen() + 0.114 * c.getBlue()) / 255 < 0.5;
}
private void updateDisplay() {
updateDisplay(true);
}
private void updateDisplay(boolean resetCaret) {
try {
textPane1.setText("");
textPane2.setText("");
StyledDocument doc1 = textPane1.getStyledDocument();
StyledDocument doc2 = textPane2.getStyledDocument();
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);
int maxLines = Math.max(lines1.size(), lines2.size());
for (int i = 0; i < maxLines; i++) {
String l1 = i < lines1.size() ? lines1.get(i) : "";
String l2 = i < lines2.size() ? lines2.get(i) : "";
boolean different = !l1.equals(l2);
Style s = different ? diffStyle : null;
doc1.insertString(doc1.getLength(), l1 + "\n", s);
doc2.insertString(doc2.getLength(), l2 + "\n", s);
}
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<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);
}
}
return lines;
}
}

View File

@ -894,6 +894,9 @@ public class MainWindow extends JFrame {
selectWildcardItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, 0)); selectWildcardItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, 0));
selectWildcardItem.addActionListener(e -> showWildcardSelectDialog()); selectWildcardItem.addActionListener(e -> showWildcardSelectDialog());
JMenuItem compareItem = new JMenuItem("Compare Files");
compareItem.addActionListener(e -> compareFiles());
JMenuItem refreshItem = new JMenuItem("Refresh"); JMenuItem refreshItem = new JMenuItem("Refresh");
refreshItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F5, InputEvent.CTRL_DOWN_MASK)); refreshItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F5, InputEvent.CTRL_DOWN_MASK));
refreshItem.addActionListener(e -> refreshPanels()); refreshItem.addActionListener(e -> refreshPanels());
@ -909,6 +912,7 @@ public class MainWindow extends JFrame {
fileMenu.add(searchItem); fileMenu.add(searchItem);
fileMenu.add(selectAllItem); fileMenu.add(selectAllItem);
fileMenu.add(selectWildcardItem); fileMenu.add(selectWildcardItem);
fileMenu.add(compareItem);
fileMenu.add(refreshItem); fileMenu.add(refreshItem);
fileMenu.add(queueItem); fileMenu.add(queueItem);
fileMenu.addSeparator(); fileMenu.addSeparator();
@ -2160,6 +2164,60 @@ public class MainWindow extends JFrame {
editor.setVisible(true); editor.setVisible(true);
} }
/**
* Compare files from both panels
*/
private void compareFiles() {
if (leftPanel == null || rightPanel == null) return;
List<FileItem> leftSelection = leftPanel.getSelectedItems();
List<FileItem> rightSelection = rightPanel.getSelectedItems();
File leftFile = null;
File rightFile = null;
if (leftSelection.size() == 1) {
leftFile = leftSelection.get(0).getFile();
} else if (leftSelection.size() > 1) {
JOptionPane.showMessageDialog(this, "Please select only one file in the left panel.", "Compare Files", JOptionPane.WARNING_MESSAGE);
return;
}
if (rightSelection.size() == 1) {
rightFile = rightSelection.get(0).getFile();
} else if (rightSelection.size() > 1) {
JOptionPane.showMessageDialog(this, "Please select only one file in the right panel.", "Compare Files", JOptionPane.WARNING_MESSAGE);
return;
}
// If nothing explicitly selected (marked) in a panel, use the focused item
if (leftFile == null) {
FileItem focused = leftPanel.getFocusedItem();
if (focused != null && !focused.getName().equals("..")) {
leftFile = focused.getFile();
}
}
if (rightFile == null) {
FileItem focused = rightPanel.getFocusedItem();
if (focused != null && !focused.getName().equals("..")) {
rightFile = focused.getFile();
}
}
if (leftFile == null || rightFile == null) {
JOptionPane.showMessageDialog(this, "Please select a file in both panels to compare.", "Compare Files", JOptionPane.WARNING_MESSAGE);
return;
}
if (leftFile.isDirectory() || rightFile.isDirectory()) {
JOptionPane.showMessageDialog(this, "Comparison of directories is not supported.", "Compare Files", JOptionPane.WARNING_MESSAGE);
return;
}
FileComparisonDialog dlg = new FileComparisonDialog(this, leftFile, rightFile, config);
dlg.setVisible(true);
}
/** /**
* Refresh both panels while preserving selection and active panel focus. * Refresh both panels while preserving selection and active panel focus.
*/ */