package bjc.utils.gui.panels; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.LinkedHashMap; import java.util.Map; /** * A Swing component that manages multiple long-running tasks and shows them * in a scrollable list, similar to a download manager. * * Each task is represented by a row with: * - A description label * - An optional note label * - A progress bar * - An optional Cancel button * * Usage (manual): * MultiTaskProgressPanel panel = new MultiTaskProgressPanel(); * TaskHandle handle = panel.startTask("Saving collection A", 0, totalItems); * // from worker thread: * for (int i = 0; i < totalItems; i++) { * if (handle.isCancelled()) break; * // ... do work ... * handle.setProgress(i + 1); * handle.setNote("Processed " + (i + 1) + " of " + totalItems); * } * handle.done(); * * Usage with SwingWorker: * SwingWorker worker = new SwingWorker<>() { ... }; * TaskHandle handle = panel.monitorSwingWorker(worker, "Saving collection A", true); * worker.execute(); */ public class MultiTaskProgressPanel extends JPanel { private static final long serialVersionUID = -2849291684349270649L; private final JPanel listPanel; private final JScrollPane scrollPane; private final JButton clearFinishedButton; private final Map taskPanels = new LinkedHashMap<>(); /** * Create a new multi-task progress panel */ public MultiTaskProgressPanel() { super(new BorderLayout()); listPanel = new JPanel(); listPanel.setLayout(new BoxLayout(listPanel, BoxLayout.Y_AXIS)); scrollPane = new JScrollPane(listPanel); scrollPane.setBorder(BorderFactory.createEmptyBorder()); clearFinishedButton = new JButton(new AbstractAction("Clear finished") { @Override public void actionPerformed(ActionEvent e) { clearFinishedTasks(); } }); 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); } /** * Starts a new task with the given description and progress range. * * @param description human-readable description of the task * @param min minimum progress value (typically 0) * @param max maximum progress value (e.g. number of items) * @return a TaskHandle that can be used to update and complete the task */ public TaskHandle startTask(String description, int min, int max) { return startTask(description, min, max, null); } /** * Starts a new task with optional cancellation. * * @param description human-readable description of the task * @param min minimum progress value (typically 0) * @param max maximum progress value (e.g. number of items) * @param onCancel called when the user clicks Cancel; may be null NOTE: * onCancel is invoked on the EDT; if it does heavy work, * delegate that to a worker thread. * @return a TaskHandle for controlling the task */ public TaskHandle startTask(String description, int min, int max, Runnable onCancel) { TaskHandle handle = new TaskHandle(this); runOnEdt(() -> { TaskPanel panel = new TaskPanel(handle, description, min, max, onCancel); taskPanels.put(handle, panel); listPanel.add(panel); listPanel.revalidate(); listPanel.repaint(); }); return handle; } /** * Convenience helper: attach a SwingWorker to this panel. It listens to the * worker's "progress" and "state" properties. * * Call worker.setProgress(0..100) inside doInBackground() as usual. * * @param worker The SwingWorker to monitor * @param description The description of the SwingWorker * @param cancellable Whether the SwingWorker is cancellable or not * @return The handle to the task */ public TaskHandle monitorSwingWorker(final SwingWorker worker, String description, boolean cancellable) { Runnable onCancel = cancellable ? () -> worker.cancel(false) : null; final TaskHandle handle = startTask(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 } } }); return handle; } /** * Removes all tasks that are marked as finished. */ public void clearFinishedTasks() { runOnEdt(() -> { taskPanels.entrySet().removeIf(entry -> { TaskPanel panel = entry.getValue(); if (panel.isFinished()) { listPanel.remove(panel); return true; } return false; }); listPanel.revalidate(); listPanel.repaint(); }); } // === internal API 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(); } }); } private static void runOnEdt(Runnable r) { if (SwingUtilities.isEventDispatchThread()) { r.run(); } else { SwingUtilities.invokeLater(r); } } // === TaskHandle: what your worker code interacts with === /** * The handle worker code uses to interact with this component */ public static class TaskHandle { private final MultiTaskProgressPanel owner; private volatile boolean cancelled = false; private volatile boolean done = false; private TaskHandle(MultiTaskProgressPanel owner) { this.owner = owner; } /** * Updates the progress value (between the task's min and max). Can be called * from any thread. * * @param value The progress value for the task */ public void setProgress(int value) { if (!done) { owner.updateProgress(this, value); } } /** * Updates the note (small status text under the description). Can be called * from any thread. * * @param note The note for the task */ public void setNote(String note) { if (!done) { owner.updateNote(this, note); } } /** * Marks the task as finished. Can be called from any thread. */ public void done() { done = true; owner.markDone(this); } /** * Requests cancellation. This is invoked by the UI when the user presses the * Cancel button, but you can also call it manually. */ public void cancel() { cancelled = true; owner.markCancelled(this); } /** * Returns true if the user has requested cancellation. Check this periodically * in your worker loop. * * @return If the task has been cancelled */ public boolean isCancelled() { return cancelled; } /** * Returns true if done() has been called. * * @return If the task is done */ public boolean isDone() { return done; } } // === UI representation of a single task === private static class TaskPanel extends JPanel { private static final long serialVersionUID = 1164358308895796582L; 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) { 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) { cancelButton.setVisible(false); } else { cancelButton.addActionListener(e -> { // Mark handle as cancelled and call onCancel on EDT. 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"); cancelButton.setEnabled(false); } void setCancelled() { this.finished = true; progressBar.setIndeterminate(false); noteLabel.setText("Cancelled"); progressBar.setForeground(Color.GRAY); cancelButton.setEnabled(false); } boolean isFinished() { return finished; } } }