diff options
| author | Benjamin Culkin <scorpress@gmail.com> | 2025-12-07 17:48:20 -0500 |
|---|---|---|
| committer | Benjamin Culkin <scorpress@gmail.com> | 2025-12-07 17:48:20 -0500 |
| commit | 09cd8befff4c8b9110ae5ad629cecb39fc2624cc (patch) | |
| tree | 0f6d93e0d2c6d711b58faeec1053abc6976b4eb0 | |
| parent | 98d563b55a5f756573d23a4fd82978d17742332c (diff) | |
Add BatchTaskProgressPanel
Adds BatchTaskProgressPanel, a augmented/specialized version of
MultiTaskProgressPanel that uses CollapsiblePanel to allow tracking of
batches of related tasks using SwingWorkers
| -rw-r--r-- | base/src/examples/java/bjc/utils/examples/gui/panels/BatchTaskProgressPanelDemo.java | 62 | ||||
| -rw-r--r-- | base/src/main/java/bjc/utils/gui/panels/BatchTaskProgressPanel.java | 517 |
2 files changed, 579 insertions, 0 deletions
diff --git a/base/src/examples/java/bjc/utils/examples/gui/panels/BatchTaskProgressPanelDemo.java b/base/src/examples/java/bjc/utils/examples/gui/panels/BatchTaskProgressPanelDemo.java new file mode 100644 index 0000000..9d2312c --- /dev/null +++ b/base/src/examples/java/bjc/utils/examples/gui/panels/BatchTaskProgressPanelDemo.java @@ -0,0 +1,62 @@ +package bjc.utils.examples.gui.panels; + +import java.awt.BorderLayout; + +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.SwingUtilities; +import javax.swing.SwingWorker; + +import bjc.utils.gui.panels.BatchTaskProgressPanel; +import bjc.utils.gui.panels.BatchTaskProgressPanel.BatchHandle; + +/** + * Demo for {@link BatchTaskProgressPanel} + */ +public class BatchTaskProgressPanelDemo { + /** + * Main method + * @param args Unused CLI args + */ + public static void main(String[] args) { + SwingUtilities.invokeLater(() -> { + JFrame frame = new JFrame("BatchTaskProgressPanel Demo"); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setLayout(new BorderLayout()); + + BatchTaskProgressPanel panel = new BatchTaskProgressPanel(); + frame.add(panel, BorderLayout.CENTER); + + JButton addBatch = new JButton("Start fake batch"); + addBatch.addActionListener(e -> { + // Simulate a batch with 3 SwingWorkers + BatchHandle batch = panel.startBatch("Save batch at " + System.currentTimeMillis()); + + for (int i = 1; i <= 3; i++) { + final int idx = i; + SwingWorker<Void, Void> worker = new SwingWorker<>() { + @Override + protected Void doInBackground() { + for (int p = 0; p <= 100; p++) { + if (isCancelled()) break; + setProgress(p); + try { + Thread.sleep(30 + idx * 10L); + } catch (InterruptedException ignored) { + } + } + return null; + } + }; + batch.monitorSwingWorker(worker, "Collection " + idx, true); + worker.execute(); + } + }); + + frame.add(addBatch, BorderLayout.SOUTH); + frame.setSize(600, 400); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + }); + } +} diff --git a/base/src/main/java/bjc/utils/gui/panels/BatchTaskProgressPanel.java b/base/src/main/java/bjc/utils/gui/panels/BatchTaskProgressPanel.java new file mode 100644 index 0000000..8d091af --- /dev/null +++ b/base/src/main/java/bjc/utils/gui/panels/BatchTaskProgressPanel.java @@ -0,0 +1,517 @@ +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<Void, Void> w1 = ...;
+ * batch.monitorSwingWorker(w1, "Collection A", true);
+ * w1.execute();
+ *
+ * SwingWorker<Void, Void> 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<BatchHandle, BatchUI> batches = new LinkedHashMap<>();
+ private final Map<TaskHandle, TaskPanel> 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());
+ }
+ }
+}
|
