package bjc.utils.gui.panels; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.LinkedHashMap; import java.util.Map; /** * A Swing panel that groups multiple background tasks into collapsible "batches" * using CollapsiblePanel, similar to a download manager. * * Each batch: * - Appears as a CollapsiblePanel with a title. * - Contains a vertical list of task rows. * * Each task: * - Has a description label. * - A progress bar (0..max). * - A small note/status line. * - Optional Cancel button. * * You typically: * * BatchTaskProgressPanel progressPanel = new BatchTaskProgressPanel(); * * BatchTaskProgressPanel.BatchHandle batch = * progressPanel.startBatch("Saving Batch 1"); * * SwingWorker w1 = ...; * batch.monitorSwingWorker(w1, "Collection A", true); * w1.execute(); * * SwingWorker w2 = ...; * batch.monitorSwingWorker(w2, "Collection B", true); * w2.execute(); */ public class BatchTaskProgressPanel extends JPanel { private static final long serialVersionUID = 758145786594145134L; private final JPanel batchesPanel; private final JScrollPane scrollPane; private final JButton clearFinishedButton; // UI for each batch private static class BatchUI { final CollapsiblePanel collapsible; final JPanel taskListPanel; // BoxLayout.Y_AXIS BatchUI(CollapsiblePanel collapsible, JPanel taskListPanel) { this.collapsible = collapsible; this.taskListPanel = taskListPanel; } } private final Map batches = new LinkedHashMap<>(); private final Map taskPanels = new LinkedHashMap<>(); /** * Create a new BatchTaskProgressPanel */ public BatchTaskProgressPanel() { super(new BorderLayout()); batchesPanel = new JPanel(); batchesPanel.setLayout(new BoxLayout(batchesPanel, BoxLayout.Y_AXIS)); scrollPane = new JScrollPane(batchesPanel); scrollPane.setBorder(BorderFactory.createEmptyBorder()); clearFinishedButton = new JButton(new AbstractAction("Clear finished") { @Override public void actionPerformed(ActionEvent e) { clearFinishedTasksAndEmptyBatches(); } }); JPanel header = new JPanel(new BorderLayout()); header.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4)); header.add(new JLabel("Background tasks"), BorderLayout.WEST); header.add(clearFinishedButton, BorderLayout.EAST); add(header, BorderLayout.NORTH); add(scrollPane, BorderLayout.CENTER); } /** * Start a new batch section with a given title. * * @param batchTitle The title for the batch * @return The new batch */ public BatchHandle startBatch(String batchTitle) { BatchHandle handle = new BatchHandle(this); runOnEdt(() -> { CollapsiblePanel collapsible = new CollapsiblePanel(batchTitle); JPanel taskListPanel = new JPanel(); taskListPanel.setLayout(new BoxLayout(taskListPanel, BoxLayout.Y_AXIS)); collapsible.add(taskListPanel); BatchUI batchUI = new BatchUI(collapsible, taskListPanel); batches.put(handle, batchUI); batchesPanel.add(collapsible); batchesPanel.add(Box.createVerticalStrut(4)); batchesPanel.revalidate(); batchesPanel.repaint(); }); return handle; } /** * Remove all finished tasks, and any batches that become empty afterwards. */ public void clearFinishedTasksAndEmptyBatches() { runOnEdt(() -> { // Remove finished tasks from each batch taskPanels.entrySet().removeIf(entry -> { TaskPanel tp = entry.getValue(); if (tp.isFinished()) { Container parent = tp.getParent(); if (parent != null) { parent.remove(tp); } return true; } return false; }); // Now remove empty batches batches.entrySet().removeIf(entry -> { BatchUI batchUI = entry.getValue(); if (batchUI.taskListPanel.getComponentCount() == 0) { batchesPanel.remove(batchUI.collapsible); return true; } return false; }); batchesPanel.revalidate(); batchesPanel.repaint(); }); } // === Batch creation API internal helpers === private TaskHandle createTaskInBatch(BatchHandle batchHandle, String description, int min, int max, Runnable onCancel) { TaskHandle taskHandle = new TaskHandle(this, batchHandle); runOnEdt(() -> { BatchUI batchUI = batches.get(batchHandle); if (batchUI == null) { return; // batch removed or not present } TaskPanel panel = new TaskPanel(taskHandle, description, min, max, onCancel); taskPanels.put(taskHandle, panel); batchUI.taskListPanel.add(panel); batchUI.taskListPanel.revalidate(); batchUI.taskListPanel.repaint(); }); return taskHandle; } private TaskHandle monitorSwingWorkerInBatch(BatchHandle batchHandle, final SwingWorker worker, String description, boolean cancellable) { Runnable onCancel = cancellable ? () -> worker.cancel(true) : null; final TaskHandle handle = createTaskInBatch(batchHandle, description, 0, 100, onCancel); worker.addPropertyChangeListener(new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { switch (evt.getPropertyName()) { case "progress": int value = (Integer) evt.getNewValue(); handle.setProgress(value); break; case "state": SwingWorker.StateValue state = (SwingWorker.StateValue) evt.getNewValue(); if (state == SwingWorker.StateValue.DONE) { handle.done(); } break; default: // ignore other properties } } }); return handle; } // === Internal helpers used by TaskHandle === private void updateProgress(TaskHandle handle, int value) { runOnEdt(() -> { TaskPanel panel = taskPanels.get(handle); if (panel != null) { panel.setProgress(value); } }); } private void updateNote(TaskHandle handle, String note) { runOnEdt(() -> { TaskPanel panel = taskPanels.get(handle); if (panel != null) { panel.setNote(note); } }); } private void markDone(TaskHandle handle) { runOnEdt(() -> { TaskPanel panel = taskPanels.get(handle); if (panel != null) { panel.setFinished(true); } }); } private void markCancelled(TaskHandle handle) { runOnEdt(() -> { TaskPanel panel = taskPanels.get(handle); if (panel != null) { panel.setCancelled(); } }); } /** * Remove a single task row from the UI (used by the "Remove" button), and * remove the batch if it becomes empty. */ private void removeTask(TaskHandle handle) { runOnEdt(() -> { TaskPanel panel = taskPanels.remove(handle); if (panel != null) { Container parent = panel.getParent(); if (parent != null) { parent.remove(panel); parent.revalidate(); parent.repaint(); } } // Remove batch if it has no tasks left BatchUI batchUI = batches.get(handle.getBatch()); if (batchUI != null && batchUI.taskListPanel.getComponentCount() == 0) { batches.remove(handle.getBatch()); batchesPanel.remove(batchUI.collapsible); batchesPanel.revalidate(); batchesPanel.repaint(); } }); } void runOnEdt(Runnable r) { if (SwingUtilities.isEventDispatchThread()) { r.run(); } else { SwingUtilities.invokeLater(r); } } // === Public handles === /** * Handle representing a batch (one CollapsiblePanel section). */ public static class BatchHandle { private final BatchTaskProgressPanel owner; private BatchHandle(BatchTaskProgressPanel owner) { this.owner = owner; } /** * Start a task in this batch * * @param description The description for the task * @param min The minimum progress value * @param max The maximum progress value * @param onCancel The function to run on cancelling * @return A handle to the task */ public TaskHandle startTask(String description, int min, int max, Runnable onCancel) { return owner.createTaskInBatch(this, description, min, max, onCancel); } /** * Start a task in this batch * * @param description The description for the task * @param min The minimum progress value * @param max The maximum progress value * @return A handle to the task */ public TaskHandle startTask(String description, int min, int max) { return startTask(description, min, max, null); } /** * Start a task monitoring a given SwingWorker * * @param worker The SwingWorker to monitor * @param description The description for the task * @param cancellable Is the task cancellable? * @return A handle to the task */ public TaskHandle monitorSwingWorker(SwingWorker worker, String description, boolean cancellable) { return owner.monitorSwingWorkerInBatch(this, worker, description, cancellable); } /** * Set the header component for this batch * @param comp */ public void setHeaderComponent(final Component comp) { owner.runOnEdt(() -> { BatchUI ui = owner.batches.get(this); if (ui != null) { ui.collapsible.setHeaderComponent(comp); } }); } } /** * Handle that your worker code uses to update a specific task. */ public static class TaskHandle { private final BatchTaskProgressPanel owner; private final BatchHandle batch; private volatile boolean cancelled = false; private volatile boolean done = false; private TaskHandle(BatchTaskProgressPanel owner, BatchHandle batch) { this.owner = owner; this.batch = batch; } /** * Return the handle to the underlying batch * @return The underlying batch */ public BatchHandle getBatch() { return batch; } /** * Set the progress for the task * @param value The progress value */ public void setProgress(int value) { if (!done) { owner.updateProgress(this, value); } } /** * Set the note for the task * @param note The note for the task */ public void setNote(String note) { if (!done) { owner.updateNote(this, note); } } /** * Mark the task as done. */ public void done() { done = true; owner.markDone(this); } /** * Mark the task as cancelled. */ public void cancel() { cancelled = true; owner.markCancelled(this); } /** * Is the task cancelled? * * @return If the task is cancelled or not */ public boolean isCancelled() { return cancelled; } /** * Is the task done? * * @return If the task is done or not */ public boolean isDone() { return done; } /** * Called by the "Remove" button to drop this task row from the UI. */ public void dismiss() { owner.removeTask(this); } } // === UI for a single task row === private static class TaskPanel extends JPanel { private static final long serialVersionUID = 7163540519142582817L; private final TaskHandle handle; private final JLabel descriptionLabel; private final JLabel noteLabel; private final JProgressBar progressBar; private final JButton cancelButton; private boolean finished = false; TaskPanel(TaskHandle handle, String description, int min, int max, Runnable onCancel) { this.handle = handle; setBorder(BorderFactory.createCompoundBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, Color.LIGHT_GRAY), BorderFactory.createEmptyBorder(4, 4, 4, 4))); setLayout(new BorderLayout(4, 4)); descriptionLabel = new JLabel(description); descriptionLabel.setFont(descriptionLabel.getFont().deriveFont(Font.BOLD)); noteLabel = new JLabel(" "); noteLabel.setFont(noteLabel.getFont().deriveFont(Font.ITALIC, 11f)); noteLabel.setForeground(Color.DARK_GRAY); progressBar = new JProgressBar(min, max); progressBar.setStringPainted(true); JPanel textPanel = new JPanel(); textPanel.setLayout(new BoxLayout(textPanel, BoxLayout.Y_AXIS)); textPanel.add(descriptionLabel); textPanel.add(Box.createVerticalStrut(2)); textPanel.add(noteLabel); cancelButton = new JButton("Cancel"); if (onCancel == null) { // Non-cancellable: hide until it becomes a "Remove" button cancelButton.setVisible(false); } else { cancelButton.addActionListener(e -> { handle.cancel(); onCancel.run(); }); } JPanel bottomPanel = new JPanel(new BorderLayout(4, 4)); bottomPanel.add(progressBar, BorderLayout.CENTER); bottomPanel.add(cancelButton, BorderLayout.EAST); add(textPanel, BorderLayout.CENTER); add(bottomPanel, BorderLayout.SOUTH); } void setProgress(int value) { progressBar.setValue(value); } void setNote(String note) { noteLabel.setText((note == null || note.isEmpty()) ? " " : note); } void setFinished(boolean finished) { this.finished = finished; progressBar.setIndeterminate(false); progressBar.setValue(progressBar.getMaximum()); noteLabel.setText("Completed"); configureAsDismissButton(); } void setCancelled() { this.finished = true; progressBar.setIndeterminate(false); noteLabel.setText("Cancelled"); progressBar.setForeground(Color.GRAY); configureAsDismissButton(); } boolean isFinished() { return finished; } /** * Re-purpose the Cancel button as a "Remove" button that removes this task row. */ private void configureAsDismissButton() { // Ensure the button is visible cancelButton.setVisible(true); // Remove existing listeners (Cancel behavior) for (ActionListener l : cancelButton.getActionListeners()) { cancelButton.removeActionListener(l); } cancelButton.setText("Remove"); cancelButton.setEnabled(true); cancelButton.addActionListener(e -> handle.dismiss()); } } }