file comparison draft
This commit is contained in:
parent
1c7e5d3e64
commit
4756d4f486
263
src/main/java/cz/kamma/kfmanager/ui/FileComparisonDialog.java
Normal file
263
src/main/java/cz/kamma/kfmanager/ui/FileComparisonDialog.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -893,6 +893,9 @@ public class MainWindow extends JFrame {
|
|||||||
JMenuItem selectWildcardItem = new JMenuItem("Select by wildcard...");
|
JMenuItem selectWildcardItem = new JMenuItem("Select by wildcard...");
|
||||||
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));
|
||||||
@ -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();
|
||||||
@ -2159,6 +2163,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.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user