diff --git a/src/main/java/cz/kamma/kfmanager/ui/FileComparisonDialog.java b/src/main/java/cz/kamma/kfmanager/ui/FileComparisonDialog.java new file mode 100644 index 0000000..1702420 --- /dev/null +++ b/src/main/java/cz/kamma/kfmanager/ui/FileComparisonDialog.java @@ -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 lines1 = new ArrayList<>(); + private List 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 readLines(File f) throws IOException { + List lines = new ArrayList<>(); + try (BufferedReader br = new BufferedReader(new FileReader(f))) { + String line; + while ((line = br.readLine()) != null) { + lines.add(line); + } + } + return lines; + } +} diff --git a/src/main/java/cz/kamma/kfmanager/ui/MainWindow.java b/src/main/java/cz/kamma/kfmanager/ui/MainWindow.java index 592c39b..cd8c23c 100644 --- a/src/main/java/cz/kamma/kfmanager/ui/MainWindow.java +++ b/src/main/java/cz/kamma/kfmanager/ui/MainWindow.java @@ -893,6 +893,9 @@ public class MainWindow extends JFrame { JMenuItem selectWildcardItem = new JMenuItem("Select by wildcard..."); selectWildcardItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, 0)); selectWildcardItem.addActionListener(e -> showWildcardSelectDialog()); + + JMenuItem compareItem = new JMenuItem("Compare Files"); + compareItem.addActionListener(e -> compareFiles()); JMenuItem refreshItem = new JMenuItem("Refresh"); 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(selectAllItem); fileMenu.add(selectWildcardItem); + fileMenu.add(compareItem); fileMenu.add(refreshItem); fileMenu.add(queueItem); fileMenu.addSeparator(); @@ -2159,6 +2163,60 @@ public class MainWindow extends JFrame { }); editor.setVisible(true); } + + /** + * Compare files from both panels + */ + private void compareFiles() { + if (leftPanel == null || rightPanel == null) return; + + List leftSelection = leftPanel.getSelectedItems(); + List 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.