package edu.oregonstate.cartography.gui; import java.awt.Frame; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.List; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.JDialog; import javax.swing.SwingUtilities; import javax.swing.SwingWorker; import javax.swing.Timer; import javax.swing.border.EmptyBorder; /** * * @author Bernhard Jenny, Institute of Cartography, ETH Zurich. * @param <T> */ public abstract class SwingWorkerWithProgressIndicatorDialog<T> extends SwingWorker<T, Integer> implements ProgressIndicator { /** * The GUI. Must be accessed by the Swing thread only. */ protected ProgressPanel progressPanel; /** * The dialog to display the progressPanel. Must be accessed by the Swing * thread only. */ protected JDialog dialog; /** * The owner frame of the dialog */ protected Frame owner; /** * The number of tasks to execute. The default is 1. */ private int totalTasksCount = 1; /** * The ID of the current task. */ private int currentTask = 1; /** * If an operation takes less time than maxTimeWithoutDialog, no dialog is * shown. Unit: milliseconds. */ private int maxTimeWithoutDialog = 2000; /** * Time in milliseconds when the operation started. */ private long startTime; /** * Must be called in the Event Dispatching Thread. * * @param owner * @param dialogTitle * @param message * @param blockOwner */ public SwingWorkerWithProgressIndicatorDialog(Frame owner, String dialogTitle, String message, boolean blockOwner) { assert (SwingUtilities.isEventDispatchThread()); this.owner = owner; // prepare the dialog dialog = new JDialog(owner); dialog.setTitle(dialogTitle); dialog.setModal(blockOwner); dialog.setResizable(false); dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); Action cancelAction = new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { cancel(); } }; progressPanel = new ProgressPanel(); progressPanel.setMessage(message); progressPanel.setCancelAction(cancelAction); progressPanel.setBorder(new EmptyBorder(10, 10, 10, 10)); dialog.setContentPane(progressPanel); dialog.pack(); dialog.setLocationRelativeTo(owner); dialog.setAlwaysOnTop(true); } /** * Initialize the dialog. Can be called from any thread. */ @Override public void start() { synchronized (this) { this.startTime = System.currentTimeMillis(); } // start a timer task that will show the dialog a little later // in case the client does not call progress() for a longer period. new ShowDialogTask().start(); // initialize the GUI in the event dispatching thread SwingUtilities.invokeLater(new Runnable() { @Override public void run() { progressPanel.progress(0); progressPanel.setIndeterminate(false); } }); } /** * Stop the operation. The dialog will be hidden and the operation will stop * as soon as possible. This might not happen synchronously. Can be called * from any thread. The client must invoke SwingWorker.isCancelled at short * intervals to test for cancellation. */ @Override public void cancel() { synchronized (this) { cancel(false); } SwingUtilities.invokeLater(new Runnable() { @Override public void run() { dialog.setVisible(false); } }); } /** * Inform the dialog that the operation has completed and it can be hidden. */ @Override public void completeProgress() { progress(100); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { dialog.setVisible(false); dialog.dispose(); progressPanel.removeActionListeners(); } }); } /** * Update the progress indicator. * * @param percentage A value between 0 and 100. * @return True if the operation should continue, false otherwise. */ @Override public boolean progress(int percentage) { setProgress(percentage); publish(percentage); return !isCancelled(); } /** * Enable or disable button to cancel the operation * @param cancellable If true, the button is enabled. */ @Override public void setCancellable(final boolean cancellable) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { progressPanel.setCancellable(cancellable); } }); } @Override public void setMessage(final String msg) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { progressPanel.setMessage(msg); } }); } public void removeMessageField() { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { progressPanel.removeMessageField(); } }); } /** * Invoked when the task's publish() method is called. Update the value * displayed by the progress dialog. This is called from within the event * dispatching thread. * * @param progressList */ @Override protected void process(List<Integer> progressList) { if (isCancelled()) { return; } int progress = progressList.get(progressList.size() - 1); progress = progress / totalTasksCount + (currentTask - 1) * 100 / totalTasksCount; if (dialog.isVisible()) { progressPanel.progress(progress); } else { // make the dialog visible if necessary // Don't show the dialog for short operations. // Only show it when half of maxTimeWithoutDialog has passed // and the operation has not yet completed half of its task. final long currentTime = System.currentTimeMillis(); if (currentTime - startTime > maxTimeWithoutDialog / 2 && progress < 50) { // show the dialog dialog.pack(); dialog.setVisible(true); } } } /** * ShowDialogTask is a timer that makes sure the progress dialog is shown if * progress() is not called for a long time. In this case, the dialog would * never become visible. */ private class ShowDialogTask implements ActionListener { private ShowDialogTask() { } /** * Start a timer that will show the dialog in maxTimeWithoutDialog * milliseconds if the dialog is not visible until then. */ private void start() { Timer timer = new Timer(getMaxTimeWithoutDialog(), this); timer.setRepeats(false); timer.start(); } /** * Show the dialog if it is not visible yet and if the operation has not * yet finished. */ @Override public void actionPerformed(ActionEvent e) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { // only make the dialog visible if the operation is not // yet finished if (isCancelled() || dialog.isVisible() || getProgress() == 100) { return; } // don't know how long the operation will take. progressPanel.setIndeterminate(true); // show the dialog dialog.pack(); dialog.setVisible(true); } }); } } public int getMaxTimeWithoutDialog() { return maxTimeWithoutDialog; } public void setMaxTimeWithoutDialogMilliseconds(int maxTimeWithoutDialog) { this.maxTimeWithoutDialog = maxTimeWithoutDialog; } /** * Sets the number of tasks. Each task has a progress between 0 and 100. If * the number of tasks is larger than 1, progress of task 1 will be rescaled * to 0..50. * * @param tasksCount The total number of tasks. */ @Override public void setTotalTasksCount(int tasksCount) { synchronized (this) { this.totalTasksCount = tasksCount; } } /** * Returns the total numbers of tasks for this progress indicator. * * @return The total numbers of tasks. */ @Override public int getTotalTasksCount() { synchronized (this) { return this.totalTasksCount; } } /** * Switch to the next task. */ @Override public void nextTask() { synchronized (this) { ++this.currentTask; if (this.currentTask > this.totalTasksCount) { this.totalTasksCount = this.currentTask; } // set progress to 0 for the new task, otherwise the progress bar would // jump to the end of this new task and jump back when setProgress(0) // is called this.setProgress(0); } } @Override public void nextTask(String message) { this.nextTask(); this.setMessage(message); } /** * Returns the ID of the current task. The first task has ID 1 (and not 0). * * @return The ID of the current task. */ @Override public int currentTask() { synchronized (this) { return this.currentTask; } } public void setIndeterminate(final boolean indeterminate) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { progressPanel.setIndeterminate(indeterminate); } }); } }