import javax.swing.*; import javax.swing.border.EmptyBorder; import javax.swing.border.TitledBorder; import javax.swing.filechooser.FileSystemView; import javax.swing.tree.*; import java.awt.*; import java.awt.event.*; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Hashtable; import java.util.List; /** * DuplicateFinderGUI - Swing GUI wrapper around DuplicateFinder (CLI core). * * Enhanced: tree view + selective delete. * - Uses Hashtable> md5 -> files core * - JTree with checkboxes for selective deletion * - Auto-select (keep first), clear, delete, open folder, expand/collapse * - Background SwingWorker scan with progress */ public class DuplicateFinderGUI extends JFrame { private JTextField txtDirectory; private JButton btnBrowse; private JButton btnFind; private JButton btnCancel; private JButton btnClear; private JCheckBox chkRecursive; private JProgressBar progressBar; private JLabel lblStatus; private JLabel lblSummary; // Tree private JTree tree; private DefaultTreeModel treeModel; private DefaultMutableTreeNode rootNode; private JScrollPane treeScroll; // Tree actions private JButton btnAutoSelect; private JButton btnClearSelection; private JButton btnDeleteSelected; private JButton btnExpandAll; private JButton btnCollapseAll; private JButton btnOpenFolder; private JLabel lblCheckedInfo; private JFileChooser dirChooser; private ScanWorker currentWorker; // Data holder for tree nodes private static class CheckedNode { String text; // display text String filePath; // null for group, absolute path for file String md5; // for group long fileSize; // for file boolean isGroup; boolean checked; CheckedNode(String text, String filePath, String md5, long fileSize, boolean isGroup) { this.text = text; this.filePath = filePath; this.md5 = md5; this.fileSize = fileSize; this.isGroup = isGroup; this.checked = false; } @Override public String toString() { return text; } } public DuplicateFinderGUI() { super("Duplicate File Finder - MD5 Hashtable"); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setSize(1000, 700); setLocationRelativeTo(null); initComponents(); } private void initComponents() { try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (Exception ignored) {} // Top panel - directory selection JPanel pnlTop = new JPanel(new GridBagLayout()); pnlTop.setBorder(new EmptyBorder(10, 10, 10, 10)); GridBagConstraints gbc = new GridBagConstraints(); JLabel lblDir = new JLabel("Directory:"); txtDirectory = new JTextField(40); txtDirectory.setToolTipText("Enter or browse to the directory to scan"); txtDirectory.setText(new File(System.getProperty("user.home")).getAbsolutePath()); btnBrowse = new JButton("Browse..."); chkRecursive = new JCheckBox("Recursive (include subdirectories)", true); btnFind = new JButton("Find Duplicates"); btnFind.setFont(btnFind.getFont().deriveFont(Font.BOLD)); btnCancel = new JButton("Cancel"); btnCancel.setEnabled(false); btnClear = new JButton("Clear Results"); gbc.insets = new Insets(5,5,5,5); gbc.gridx = 0; gbc.gridy = 0; gbc.anchor = GridBagConstraints.WEST; pnlTop.add(lblDir, gbc); gbc.gridx = 1; gbc.gridy = 0; gbc.fill = GridBagConstraints.HORIZONTAL; gbc.weightx = 1.0; pnlTop.add(txtDirectory, gbc); gbc.gridx = 2; gbc.gridy = 0; gbc.fill = GridBagConstraints.NONE; gbc.weightx = 0; pnlTop.add(btnBrowse, gbc); gbc.gridx = 0; gbc.gridy = 1; gbc.gridwidth = 3; gbc.anchor = GridBagConstraints.WEST; pnlTop.add(chkRecursive, gbc); JPanel pnlButtons = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0)); pnlButtons.add(btnFind); pnlButtons.add(btnCancel); pnlButtons.add(btnClear); gbc.gridx = 0; gbc.gridy = 2; gbc.gridwidth = 3; gbc.fill = GridBagConstraints.NONE; pnlTop.add(pnlButtons, gbc); // Progress progressBar = new JProgressBar(0, 100); progressBar.setStringPainted(true); progressBar.setString("Idle"); lblStatus = new JLabel("Ready. Select a directory and press Find Duplicates."); lblSummary = new JLabel(" "); JPanel pnlProgress = new JPanel(new BorderLayout(5,5)); pnlProgress.setBorder(new EmptyBorder(0, 10, 10, 10)); pnlProgress.add(progressBar, BorderLayout.NORTH); pnlProgress.add(lblStatus, BorderLayout.CENTER); pnlProgress.add(lblSummary, BorderLayout.SOUTH); JPanel pnlNorth = new JPanel(new BorderLayout()); pnlNorth.add(pnlTop, BorderLayout.NORTH); pnlNorth.add(pnlProgress, BorderLayout.SOUTH); // Tree rootNode = new DefaultMutableTreeNode(new CheckedNode("No scan yet - press Find Duplicates", null, null, 0, true)); treeModel = new DefaultTreeModel(rootNode); tree = new JTree(treeModel); tree.setRootVisible(false); tree.setShowsRootHandles(true); tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); tree.setCellRenderer(new CheckBoxTreeCellRenderer()); tree.setRowHeight(22); tree.setToggleClickCount(2); // double click to expand, single click handled for checkbox // Mouse listener for checkbox toggling tree.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { int row = tree.getRowForLocation(e.getX(), e.getY()); TreePath path = tree.getPathForLocation(e.getX(), e.getY()); if (row == -1 || path == null) return; DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); Object obj = node.getUserObject(); if (!(obj instanceof CheckedNode)) return; CheckedNode data = (CheckedNode) obj; // Only toggle checkbox if click is on checkbox area OR on file/group node with single click // For simplicity toggle on any click on the node, but let expand/collapse handle via toggleClickCount=2 // Determine if click was on expand handle? JTree handles that before us, but we can still toggle. // We'll check if click is on the left ~30px where checkbox is, otherwise ignore for group expand convenience // But many users expect click on label toggles checkbox too - so we toggle anyway except when double-click expand? // Use: if the path is already selected and click is single, toggle. Simpler: always toggle. Rectangle bounds = tree.getRowBounds(row); if (bounds == null) return; // Allow toggle if click is within checkbox+label area (not far right) // For usability, toggle on any click inside row boolean newState = !data.checked; setNodeChecked(node, newState); // Update parent group state to reflect children? If all children checked, group checked; if none, unchecked updateParentGroupState(node); tree.repaint(); updateCheckedInfo(); } }); // Space bar toggles checkbox of selected node tree.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_SPACE) { TreePath path = tree.getSelectionPath(); if (path != null) { DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); Object obj = node.getUserObject(); if (obj instanceof CheckedNode) { CheckedNode data = (CheckedNode) obj; setNodeChecked(node, !data.checked); updateParentGroupState(node); tree.repaint(); updateCheckedInfo(); e.consume(); } } } } }); // Selection listener to show file details / enable open folder tree.addTreeSelectionListener(e -> { TreePath path = tree.getSelectionPath(); if (path != null) { DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); Object obj = node.getUserObject(); if (obj instanceof CheckedNode) { CheckedNode data = (CheckedNode) obj; if (!data.isGroup && data.filePath != null) { btnOpenFolder.setEnabled(true); lblStatus.setText(data.filePath + " (" + data.fileSize + " bytes)"); } else if (data.isGroup) { btnOpenFolder.setEnabled(false); lblStatus.setText(data.text); } else { btnOpenFolder.setEnabled(false); } } } else { btnOpenFolder.setEnabled(false); } }); treeScroll = new JScrollPane(tree); treeScroll.setBorder(new TitledBorder("Duplicate Groups (check files to delete)")); // Tree action buttons btnAutoSelect = new JButton("Auto-Select (keep 1 per group)"); btnAutoSelect.setToolTipText("Checks all but the first file in each duplicate group - safe default to keep one copy"); btnAutoSelect.setEnabled(false); btnClearSelection = new JButton("Clear Checks"); btnClearSelection.setEnabled(false); btnDeleteSelected = new JButton("Delete Checked Files"); btnDeleteSelected.setEnabled(false); btnDeleteSelected.setBackground(new Color(255, 200, 200)); btnExpandAll = new JButton("Expand All"); btnExpandAll.setEnabled(false); btnCollapseAll = new JButton("Collapse All"); btnCollapseAll.setEnabled(false); btnOpenFolder = new JButton("Open Location"); btnOpenFolder.setEnabled(false); btnOpenFolder.setToolTipText("Open containing folder of selected file"); lblCheckedInfo = new JLabel("No files checked"); lblCheckedInfo.setFont(lblCheckedInfo.getFont().deriveFont(Font.ITALIC)); JPanel pnlTreeActions = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 5)); pnlTreeActions.add(btnAutoSelect); pnlTreeActions.add(btnClearSelection); pnlTreeActions.add(btnDeleteSelected); pnlTreeActions.add(btnOpenFolder); pnlTreeActions.add(new JSeparator(SwingConstants.VERTICAL)); pnlTreeActions.add(btnExpandAll); pnlTreeActions.add(btnCollapseAll); pnlTreeActions.add(lblCheckedInfo); JPanel pnlCenter = new JPanel(new BorderLayout()); pnlCenter.add(treeScroll, BorderLayout.CENTER); pnlCenter.add(pnlTreeActions, BorderLayout.SOUTH); // Popup menu JPopupMenu popup = new JPopupMenu(); JMenuItem miDelete = new JMenuItem("Delete Checked Files"); JMenuItem miAuto = new JMenuItem("Auto-Select (keep 1)"); JMenuItem miClear = new JMenuItem("Clear Checks"); JMenuItem miOpen = new JMenuItem("Open Location"); JMenuItem miCopyPath = new JMenuItem("Copy Path"); popup.add(miAuto); popup.add(miClear); popup.addSeparator(); popup.add(miDelete); popup.addSeparator(); popup.add(miOpen); popup.add(miCopyPath); tree.setComponentPopupMenu(popup); // Popup actions miDelete.addActionListener(e -> deleteCheckedFiles()); miAuto.addActionListener(e -> autoSelectKeepOne()); miClear.addActionListener(e -> clearChecks()); miOpen.addActionListener(e -> openSelectedFolder()); miCopyPath.addActionListener(e -> copySelectedPath()); // Menu bar JMenuBar menuBar = new JMenuBar(); JMenu mnuFile = new JMenu("File"); JMenuItem miBrowse = new JMenuItem("Browse..."); JMenuItem miExit = new JMenuItem("Exit"); mnuFile.add(miBrowse); mnuFile.addSeparator(); mnuFile.add(miExit); JMenu mnuEdit = new JMenu("Edit"); JMenuItem miSelectAll = new JMenuItem("Auto-Select Keep One"); JMenuItem miDeleteMenu = new JMenuItem("Delete Checked..."); mnuEdit.add(miSelectAll); mnuEdit.add(miDeleteMenu); JMenu mnuView = new JMenu("View"); JMenuItem miExpand = new JMenuItem("Expand All"); JMenuItem miCollapse = new JMenuItem("Collapse All"); mnuView.add(miExpand); mnuView.add(miCollapse); JMenu mnuHelp = new JMenu("Help"); JMenuItem miAbout = new JMenuItem("About"); mnuHelp.add(miAbout); menuBar.add(mnuFile); menuBar.add(mnuEdit); menuBar.add(mnuView); menuBar.add(mnuHelp); setJMenuBar(menuBar); dirChooser = new JFileChooser(FileSystemView.getFileSystemView()); dirChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); dirChooser.setAcceptAllFileFilterUsed(false); dirChooser.setDialogTitle("Select Directory to Scan"); setLayout(new BorderLayout()); add(pnlNorth, BorderLayout.NORTH); add(pnlCenter, BorderLayout.CENTER); // Listeners btnBrowse.addActionListener(e -> browse()); miBrowse.addActionListener(e -> browse()); miExit.addActionListener(e -> dispose()); miAbout.addActionListener(e -> JOptionPane.showMessageDialog(this, "Duplicate File Finder\n\nFinds duplicate files by MD5 Hashtable.\nHashtable> - groups with >1 file are duplicates.\n\nTree view: check unwanted duplicates, then Delete.\nSafe auto-select keeps 1 file per group.\n\nCLI: java DuplicateFinder \nGUI: java DuplicateFinderGUI\n\nJava " + System.getProperty("java.version"), "About", JOptionPane.INFORMATION_MESSAGE)); btnFind.addActionListener(e -> startScan()); btnCancel.addActionListener(e -> cancelScan()); btnClear.addActionListener(e -> clearResults()); txtDirectory.addActionListener(e -> startScan()); btnAutoSelect.addActionListener(e -> autoSelectKeepOne()); miSelectAll.addActionListener(e -> autoSelectKeepOne()); btnClearSelection.addActionListener(e -> clearChecks()); btnDeleteSelected.addActionListener(e -> deleteCheckedFiles()); miDeleteMenu.addActionListener(e -> deleteCheckedFiles()); btnExpandAll.addActionListener(e -> expandAll()); btnCollapseAll.addActionListener(e -> collapseAll()); miExpand.addActionListener(e -> expandAll()); miCollapse.addActionListener(e -> collapseAll()); btnOpenFolder.addActionListener(e -> openSelectedFolder()); } // Renderer with checkbox + label private class CheckBoxTreeCellRenderer extends JPanel implements TreeCellRenderer { private JCheckBox checkBox = new JCheckBox(); private JLabel iconLabel = new JLabel(); private JLabel textLabel = new JLabel(); private JLabel sizeLabel = new JLabel(); CheckBoxTreeCellRenderer() { setLayout(new FlowLayout(FlowLayout.LEFT, 4, 0)); setOpaque(false); checkBox.setOpaque(false); textLabel.setOpaque(false); sizeLabel.setOpaque(false); sizeLabel.setFont(sizeLabel.getFont().deriveFont(Font.ITALIC, 11f)); sizeLabel.setForeground(Color.GRAY); add(checkBox); add(iconLabel); add(textLabel); add(sizeLabel); } @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { DefaultMutableTreeNode node = (DefaultMutableTreeNode) value; Object obj = node.getUserObject(); if (!(obj instanceof CheckedNode)) { textLabel.setText(obj.toString()); checkBox.setVisible(false); iconLabel.setIcon(null); sizeLabel.setText(""); return this; } CheckedNode data = (CheckedNode) obj; // Root invisible case if (node == rootNode && rootNode.getChildCount()==0) { // shouldn't happen } checkBox.setVisible(true); checkBox.setSelected(data.checked); checkBox.setEnabled(tree.isEnabled()); textLabel.setText(data.text); // Icon: group vs file, also indicate checked for deletion (red?) if (data.isGroup) { iconLabel.setIcon(UIManager.getIcon("FileView.directoryIcon")); // Show md5 truncated - guard against null (placeholder root nodes) if (data.md5 != null && data.md5.length() >= 8) { sizeLabel.setText(" [" + data.md5.substring(0, 8) + "...]"); } else if (data.md5 != null && !data.md5.isEmpty()) { sizeLabel.setText(" [" + data.md5 + "]"); } else { sizeLabel.setText(""); } textLabel.setFont(textLabel.getFont().deriveFont(Font.BOLD)); } else { iconLabel.setIcon(UIManager.getIcon("FileView.fileIcon")); textLabel.setFont(textLabel.getFont().deriveFont(Font.PLAIN)); sizeLabel.setText(" (" + data.fileSize + " bytes)"); } // Selection coloring if (selected) { setBackground(UIManager.getColor("Tree.selectionBackground")); setOpaque(true); textLabel.setForeground(UIManager.getColor("Tree.selectionForeground")); sizeLabel.setForeground(UIManager.getColor("Tree.selectionForeground")); } else { setOpaque(false); textLabel.setForeground(UIManager.getColor("Tree.textForeground")); sizeLabel.setForeground(Color.GRAY); if (data.checked && !data.isGroup) { textLabel.setForeground(new Color(180, 0, 0)); } } // Strike through for checked? Could add but keep simple return this; } } private void setNodeChecked(DefaultMutableTreeNode node, boolean checked) { Object obj = node.getUserObject(); if (!(obj instanceof CheckedNode)) return; CheckedNode data = (CheckedNode) obj; data.checked = checked; // If group, propagate to children if (data.isGroup) { for (int i = 0; i < node.getChildCount(); i++) { DefaultMutableTreeNode child = (DefaultMutableTreeNode) node.getChildAt(i); setNodeChecked(child, checked); } } } private void updateParentGroupState(DefaultMutableTreeNode changedNode) { // Update ancestors: group checked if all children checked, unchecked if none, otherwise? We treat as unchecked if not all. DefaultMutableTreeNode parent = (DefaultMutableTreeNode) changedNode.getParent(); if (parent == null || parent == rootNode) return; Object pObj = parent.getUserObject(); if (!(pObj instanceof CheckedNode)) return; CheckedNode pData = (CheckedNode) pObj; if (!pData.isGroup) return; boolean allChecked = true; boolean anyChecked = false; for (int i = 0; i < parent.getChildCount(); i++) { DefaultMutableTreeNode child = (DefaultMutableTreeNode) parent.getChildAt(i); CheckedNode cData = (CheckedNode) child.getUserObject(); if (cData.checked) anyChecked = true; else allChecked = false; } // If all children checked, group checked; else unchecked (could be indeterminate but we use unchecked) pData.checked = allChecked; // Recurse up updateParentGroupState(parent); } private void browse() { String current = txtDirectory.getText().trim(); if (!current.isEmpty()) { File f = new File(current); if (f.exists()) { dirChooser.setCurrentDirectory(f.isDirectory() ? f : f.getParentFile()); } } int ret = dirChooser.showOpenDialog(this); if (ret == JFileChooser.APPROVE_OPTION) { File sel = dirChooser.getSelectedFile(); txtDirectory.setText(sel.getAbsolutePath()); } } private void startScan() { String dirPath = txtDirectory.getText().trim(); if (dirPath.isEmpty()) { JOptionPane.showMessageDialog(this, "Please enter a directory path.", "Error", JOptionPane.ERROR_MESSAGE); return; } File root = new File(dirPath); if (!root.exists()) { JOptionPane.showMessageDialog(this, "Path does not exist:\n" + dirPath, "Error", JOptionPane.ERROR_MESSAGE); return; } if (!root.isDirectory() && !root.isFile()) { JOptionPane.showMessageDialog(this, "Not a directory or file:\n" + dirPath, "Error", JOptionPane.ERROR_MESSAGE); return; } boolean recursive = chkRecursive.isSelected(); btnFind.setEnabled(false); btnBrowse.setEnabled(false); btnCancel.setEnabled(true); btnClear.setEnabled(false); btnAutoSelect.setEnabled(false); btnClearSelection.setEnabled(false); btnDeleteSelected.setEnabled(false); btnExpandAll.setEnabled(false); btnCollapseAll.setEnabled(false); // Clear tree placeholder rootNode.removeAllChildren(); rootNode.setUserObject(new CheckedNode("Scanning: " + root.getAbsolutePath() + " ...", null, null, 0, true)); treeModel.reload(); progressBar.setIndeterminate(true); progressBar.setString("Scanning..."); lblStatus.setText("Collecting files..."); lblSummary.setText(" "); lblCheckedInfo.setText("Scanning..."); currentWorker = new ScanWorker(root, recursive); currentWorker.execute(); } private void cancelScan() { if (currentWorker != null && !currentWorker.isDone()) { currentWorker.cancel(true); lblStatus.setText("Cancelling..."); } } private void clearResults() { rootNode.removeAllChildren(); rootNode.setUserObject(new CheckedNode("No scan yet - press Find Duplicates", null, null, 0, true)); treeModel.reload(); lblStatus.setText("Cleared."); lblSummary.setText(" "); lblCheckedInfo.setText("No files checked"); progressBar.setValue(0); progressBar.setString("Idle"); btnAutoSelect.setEnabled(false); btnClearSelection.setEnabled(false); btnDeleteSelected.setEnabled(false); btnExpandAll.setEnabled(false); btnCollapseAll.setEnabled(false); btnOpenFolder.setEnabled(false); btnClear.setEnabled(false); } // Tree actions private void autoSelectKeepOne() { // For each group, check all but first file (sorted) int totalChecked = 0; for (int i = 0; i < rootNode.getChildCount(); i++) { DefaultMutableTreeNode groupNode = (DefaultMutableTreeNode) rootNode.getChildAt(i); for (int j = 0; j < groupNode.getChildCount(); j++) { DefaultMutableTreeNode fileNode = (DefaultMutableTreeNode) groupNode.getChildAt(j); CheckedNode data = (CheckedNode) fileNode.getUserObject(); boolean shouldCheck = (j != 0); // keep first data.checked = shouldCheck; if (shouldCheck) totalChecked++; } // Update group checkbox to reflect not all checked (so unchecked) CheckedNode gData = (CheckedNode) groupNode.getUserObject(); gData.checked = false; } tree.repaint(); updateCheckedInfo(); if (totalChecked > 0) { lblStatus.setText("Auto-selected " + totalChecked + " files (keeping 1 per group). Review and press Delete if correct."); } } private void clearChecks() { for (int i = 0; i < rootNode.getChildCount(); i++) { DefaultMutableTreeNode groupNode = (DefaultMutableTreeNode) rootNode.getChildAt(i); setNodeChecked(groupNode, false); } tree.repaint(); updateCheckedInfo(); lblStatus.setText("All checks cleared."); } private void expandAll() { for (int i = 0; i < tree.getRowCount(); i++) { tree.expandRow(i); } } private void collapseAll() { for (int i = tree.getRowCount() - 1; i >= 0; i--) { tree.collapseRow(i); } } private void openSelectedFolder() { TreePath path = tree.getSelectionPath(); if (path == null) return; DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); CheckedNode data = (CheckedNode) node.getUserObject(); String filePath = data.filePath; if (filePath == null && data.isGroup && node.getChildCount() > 0) { // use first child DefaultMutableTreeNode first = (DefaultMutableTreeNode) node.getChildAt(0); CheckedNode fData = (CheckedNode) first.getUserObject(); filePath = fData.filePath; } if (filePath == null) return; File f = new File(filePath); File dir = f.getParentFile(); if (dir == null || !dir.exists()) return; try { if (Desktop.isDesktopSupported()) { Desktop.getDesktop().open(dir); } else { // Fallback for Windows Runtime.getRuntime().exec(new String[]{"explorer", dir.getAbsolutePath()}); } } catch (IOException ex) { JOptionPane.showMessageDialog(this, "Cannot open folder:\n" + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); } } private void copySelectedPath() { TreePath path = tree.getSelectionPath(); if (path == null) return; DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); CheckedNode data = (CheckedNode) node.getUserObject(); String toCopy = data.filePath != null ? data.filePath : data.text; Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new java.awt.datatransfer.StringSelection(toCopy), null); lblStatus.setText("Copied to clipboard: " + toCopy); } private List getCheckedFiles() { List checked = new ArrayList(); for (int i = 0; i < rootNode.getChildCount(); i++) { DefaultMutableTreeNode groupNode = (DefaultMutableTreeNode) rootNode.getChildAt(i); for (int j = 0; j < groupNode.getChildCount(); j++) { DefaultMutableTreeNode fileNode = (DefaultMutableTreeNode) groupNode.getChildAt(j); CheckedNode data = (CheckedNode) fileNode.getUserObject(); if (data.checked && !data.isGroup && data.filePath != null) { checked.add(new File(data.filePath)); } } } return checked; } private void updateCheckedInfo() { List checked = getCheckedFiles(); long totalBytes = 0; for (File f : checked) totalBytes += f.length(); if (checked.isEmpty()) { lblCheckedInfo.setText("No files checked"); btnDeleteSelected.setEnabled(false); btnClearSelection.setEnabled(false); } else { lblCheckedInfo.setText(checked.size() + " file(s) checked, " + totalBytes + " bytes to free"); btnDeleteSelected.setEnabled(true); btnClearSelection.setEnabled(true); } } private void deleteCheckedFiles() { List toDelete = getCheckedFiles(); if (toDelete.isEmpty()) { JOptionPane.showMessageDialog(this, "No files checked for deletion.\nCheck files in the tree (click checkbox) first.", "No selection", JOptionPane.INFORMATION_MESSAGE); return; } // Warn if deleting all files in any group List warnings = new ArrayList(); for (int i = 0; i < rootNode.getChildCount(); i++) { DefaultMutableTreeNode groupNode = (DefaultMutableTreeNode) rootNode.getChildAt(i); int checkedInGroup = 0; for (int j = 0; j < groupNode.getChildCount(); j++) { DefaultMutableTreeNode fileNode = (DefaultMutableTreeNode) groupNode.getChildAt(j); CheckedNode d = (CheckedNode) fileNode.getUserObject(); if (d.checked) checkedInGroup++; } if (checkedInGroup == groupNode.getChildCount() && groupNode.getChildCount() > 0) { CheckedNode gData = (CheckedNode) groupNode.getUserObject(); String shortMd5 = (gData.md5 != null && gData.md5.length() >= 8) ? gData.md5.substring(0,8) : (gData.md5 != null ? gData.md5 : "unknown"); warnings.add("Group " + (i+1) + " (" + shortMd5 + "): ALL " + checkedInGroup + " files checked - no copy will remain!"); } } // Build confirmation message long totalBytes = 0; for (File f : toDelete) totalBytes += f.length(); StringBuilder msg = new StringBuilder(); msg.append("Permanently delete ").append(toDelete.size()).append(" checked file(s)?\n"); msg.append("(").append(totalBytes).append(" bytes will be freed)\n\n"); int showMax = Math.min(toDelete.size(), 15); for (int i = 0; i < showMax; i++) { msg.append("- ").append(toDelete.get(i).getAbsolutePath()).append("\n"); } if (toDelete.size() > showMax) { msg.append("... and ").append(toDelete.size() - showMax).append(" more\n"); } if (!warnings.isEmpty()) { msg.append("\nWARNINGS:\n"); for (String w : warnings) msg.append("! ").append(w).append("\n"); msg.append("\nConsider keeping at least one per group.\n"); } msg.append("\nThis cannot be undone. Continue?"); int confirm = JOptionPane.showConfirmDialog(this, msg.toString(), "Confirm Delete", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); if (confirm != JOptionPane.YES_OPTION) return; // Perform deletion int success = 0; int failed = 0; List failedFiles = new ArrayList(); // Need to collect nodes to remove after file deletion to avoid concurrent modification List nodesToRemove = new ArrayList(); // Map file path -> node for (int i = 0; i < rootNode.getChildCount(); i++) { DefaultMutableTreeNode groupNode = (DefaultMutableTreeNode) rootNode.getChildAt(i); for (int j = 0; j < groupNode.getChildCount(); j++) { DefaultMutableTreeNode fileNode = (DefaultMutableTreeNode) groupNode.getChildAt(j); CheckedNode d = (CheckedNode) fileNode.getUserObject(); if (d.checked) { File f = new File(d.filePath); try { // Extra safety: ensure file still exists if (!f.exists()) { failed++; failedFiles.add(d.filePath + " (not found)"); continue; } if (f.delete()) { success++; nodesToRemove.add(fileNode); } else { // Try writable check f.setWritable(true); if (f.delete()) { success++; nodesToRemove.add(fileNode); } else { failed++; failedFiles.add(d.filePath + " (delete returned false)"); } } } catch (SecurityException ex) { failed++; failedFiles.add(d.filePath + " (" + ex.getMessage() + ")"); } } } } // Remove nodes from tree model for (DefaultMutableTreeNode n : nodesToRemove) { treeModel.removeNodeFromParent(n); } // Remove now-empty/singular groups (no longer duplicates) List emptyGroups = new ArrayList(); for (int i = 0; i < rootNode.getChildCount(); i++) { DefaultMutableTreeNode groupNode = (DefaultMutableTreeNode) rootNode.getChildAt(i); if (groupNode.getChildCount() < 2) { emptyGroups.add(groupNode); } } for (DefaultMutableTreeNode g : emptyGroups) { treeModel.removeNodeFromParent(g); } // If no groups left, show placeholder if (rootNode.getChildCount() == 0) { lblSummary.setText("All duplicate groups cleared."); } else { // Update summary int remainingGroups = rootNode.getChildCount(); int remainingFiles = 0; for (int i = 0; i < rootNode.getChildCount(); i++) { remainingFiles += ((DefaultMutableTreeNode) rootNode.getChildAt(i)).getChildCount(); } lblSummary.setText(remainingGroups + " group(s) remaining, " + remainingFiles + " files"); } tree.repaint(); updateCheckedInfo(); // Result dialog StringBuilder result = new StringBuilder(); result.append("Deleted ").append(success).append(" file(s).\n"); if (failed > 0) { result.append("Failed to delete ").append(failed).append(" file(s):\n"); for (String s : failedFiles) result.append("- ").append(s).append("\n"); JOptionPane.showMessageDialog(this, result.toString(), "Delete Result - Partial Failure", JOptionPane.WARNING_MESSAGE); } else { result.append("Successfully freed ").append(totalBytes).append(" bytes (estimated).\n"); if (success > 0) { // Show info but not too annoying if many deletes lblStatus.setText("Deleted " + success + " file(s)."); } // Only show dialog if success and not too frequent? Show lightweight if (success <= 5) { JOptionPane.showMessageDialog(this, result.toString(), "Delete Complete", JOptionPane.INFORMATION_MESSAGE); } } // Update checked info already done } private class ScanWorker extends SwingWorker>, String> { private final File root; private final boolean recursive; private volatile boolean cancelled = false; private int totalFiles = 0; ScanWorker(File root, boolean recursive) { this.root = root; this.recursive = recursive; } @Override protected Hashtable> doInBackground() throws Exception { List allFiles = DuplicateFinder.collectFiles(root, recursive); totalFiles = allFiles.size(); if (totalFiles == 0) { return new Hashtable>(); } SwingUtilities.invokeLater(() -> { progressBar.setIndeterminate(false); progressBar.setMaximum(totalFiles); progressBar.setValue(0); }); DuplicateFinder.ProgressCallback cb = new DuplicateFinder.ProgressCallback() { @Override public void onProgress(int current, int total, String file) { if (isCancelled()) { cancelled = true; return; } final int cur = current; SwingUtilities.invokeLater(() -> { progressBar.setValue(cur); progressBar.setString(cur + " / " + total); lblStatus.setText("Hashing: " + file); }); } @Override public boolean isCancelled() { return ScanWorker.this.isCancelled() || cancelled; } }; Hashtable> fullTable = DuplicateFinder.buildHashTableWithProgress(root, recursive, cb); if (isCancelled() || cancelled) return new Hashtable>(); return DuplicateFinder.filterDuplicates(fullTable); } @Override protected void done() { btnFind.setEnabled(true); btnBrowse.setEnabled(true); btnCancel.setEnabled(false); btnClear.setEnabled(true); progressBar.setIndeterminate(false); if (isCancelled()) { lblStatus.setText("Cancelled."); rootNode.removeAllChildren(); rootNode.setUserObject(new CheckedNode("Cancelled", null, null, 0, true)); treeModel.reload(); progressBar.setString("Cancelled"); return; } try { Hashtable> dupes = get(); displayResultsTree(dupes, totalFiles); } catch (Exception ex) { Throwable cause = ex.getCause() != null ? ex.getCause() : ex; lblStatus.setText("Error: " + cause.getMessage()); JOptionPane.showMessageDialog(DuplicateFinderGUI.this, "Error during scan:\n" + cause.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); progressBar.setString("Error"); cause.printStackTrace(); } } } private void displayResultsTree(Hashtable> dupes, int totalScanned) { rootNode.removeAllChildren(); if (dupes.isEmpty()) { rootNode.setUserObject(new CheckedNode("No duplicates found (" + totalScanned + " files scanned)", null, null, 0, true)); treeModel.reload(); lblStatus.setText("Done - no duplicates found."); lblSummary.setText("Scanned " + totalScanned + " files - no duplicates."); progressBar.setValue(progressBar.getMaximum()); progressBar.setString("Done"); btnAutoSelect.setEnabled(false); btnExpandAll.setEnabled(false); btnCollapseAll.setEnabled(false); return; } List hashes = new ArrayList(dupes.keySet()); Collections.sort(hashes); int groupIdx = 1; int totalDupFiles = 0; for (String hash : hashes) { List files = dupes.get(hash); Collections.sort(files); totalDupFiles += files.size(); // Determine common size (all duplicates have same content so same size) long size = new File(files.get(0)).length(); String groupText = "Group " + groupIdx + " - " + files.size() + " files - " + size + " bytes each - MD5: " + hash; CheckedNode groupData = new CheckedNode(groupText, null, hash, size, true); DefaultMutableTreeNode groupNode = new DefaultMutableTreeNode(groupData); for (String path : files) { File f = new File(path); String fileText = f.getName() + " [" + f.getParent() + "]"; CheckedNode fileData = new CheckedNode(fileText, path, hash, f.length(), false); DefaultMutableTreeNode fileNode = new DefaultMutableTreeNode(fileData); groupNode.add(fileNode); } rootNode.add(groupNode); groupIdx++; } // Root label is summary rootNode.setUserObject(new CheckedNode("Duplicates: " + dupes.size() + " groups, " + totalDupFiles + " files", null, null, 0, true)); treeModel.reload(); // Expand all groups expandAll(); lblStatus.setText("Done - found " + dupes.size() + " duplicate group(s). Check unwanted files and delete."); lblSummary.setText("Scanned " + totalScanned + " files, " + totalDupFiles + " files in " + dupes.size() + " duplicate group(s)."); progressBar.setValue(progressBar.getMaximum()); progressBar.setString("Done"); btnAutoSelect.setEnabled(true); btnExpandAll.setEnabled(true); btnCollapseAll.setEnabled(true); // enable clear selection only after something checked, but enable button itself? Keep disabled until checked updateCheckedInfo(); // select first group if (rootNode.getChildCount() > 0) { TreePath path = new TreePath(((DefaultMutableTreeNode) rootNode.getChildAt(0)).getPath()); tree.setSelectionPath(path); } } public static void main(String[] args) { final String startDir = (args.length > 0) ? args[0] : null; SwingUtilities.invokeLater(() -> { DuplicateFinderGUI gui = new DuplicateFinderGUI(); if (startDir != null && !startDir.trim().isEmpty()) { gui.txtDirectory.setText(startDir); } gui.setVisible(true); }); } }