From 8608cb13602d8df3cd3d2f96b1cc3a14d488fe7d Mon Sep 17 00:00:00 2001 From: Benjamin Culkin Date: Sun, 7 Dec 2025 17:31:40 -0500 Subject: Add MultiTaskProgressPanel Adds MultiTaskProgressPanel. This is a Swing component usable for monitoring background tasks similar to the way ProgressMonitor works, but instead of opening a dialog for each tasks, it displays them like a browser download manager --- .../gui/panels/MultiTaskProgressPanelDemo.java | 55 ++++ .../utils/gui/panels/MultiTaskProgressPanel.java | 365 +++++++++++++++++++++ 2 files changed, 420 insertions(+) create mode 100644 base/src/examples/java/bjc/utils/examples/gui/panels/MultiTaskProgressPanelDemo.java create mode 100644 base/src/main/java/bjc/utils/gui/panels/MultiTaskProgressPanel.java diff --git a/base/src/examples/java/bjc/utils/examples/gui/panels/MultiTaskProgressPanelDemo.java b/base/src/examples/java/bjc/utils/examples/gui/panels/MultiTaskProgressPanelDemo.java new file mode 100644 index 0000000..f417903 --- /dev/null +++ b/base/src/examples/java/bjc/utils/examples/gui/panels/MultiTaskProgressPanelDemo.java @@ -0,0 +1,55 @@ +package bjc.utils.examples.gui.panels; + +import java.awt.BorderLayout; + +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.SwingUtilities; + +import bjc.utils.gui.panels.MultiTaskProgressPanel; +import bjc.utils.gui.panels.MultiTaskProgressPanel.TaskHandle; + +/** + * Demo for {@link MultiTaskProgressPanel} + */ +public class MultiTaskProgressPanelDemo { + /** + * Main method + * @param args Unused CLI args + */ + public static void main(String[] args) { + SwingUtilities.invokeLater(() -> { + JFrame frame = new JFrame("MultiTaskProgressPanel demo"); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + MultiTaskProgressPanel panel = new MultiTaskProgressPanel(); + frame.add(panel, BorderLayout.CENTER); + + JButton addTask = new JButton("Start fake task"); + addTask.addActionListener(e -> { + TaskHandle handle = panel.startTask("Saving large collection", + 0, 100, null); + + // Simulate background work + new Thread(() -> { + for (int i = 0; i <= 100; i++) { + if (handle.isCancelled()) break; + handle.setProgress(i); + handle.setNote("Step " + i + " of 100"); + try { + Thread.sleep(50); + } catch (InterruptedException ignored) { + } + } + handle.done(); + }, "FakeWorker").start(); + }); + + frame.add(addTask, BorderLayout.SOUTH); + + frame.setSize(500, 400); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + }); + } +} diff --git a/base/src/main/java/bjc/utils/gui/panels/MultiTaskProgressPanel.java b/base/src/main/java/bjc/utils/gui/panels/MultiTaskProgressPanel.java new file mode 100644 index 0000000..578623a --- /dev/null +++ b/base/src/main/java/bjc/utils/gui/panels/MultiTaskProgressPanel.java @@ -0,0 +1,365 @@ +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; + } + } +} -- cgit v1.2.3