summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--base/src/examples/java/bjc/utils/examples/gui/panels/MultiTaskProgressPanelDemo.java55
-rw-r--r--base/src/main/java/bjc/utils/gui/panels/MultiTaskProgressPanel.java365
2 files changed, 420 insertions, 0 deletions
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<Void, Void> 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<TaskHandle, TaskPanel> 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;
+ }
+ }
+}