package edu.oregonstate.cartography.gui;
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.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.Timer;
/**
*
* @author Bernhard Jenny, Institute of Cartography, ETH Zurich.
* @param <T>
*/
public abstract class SwingWorkerWithProgressIndicatorPanel<T> extends SwingWorker<T, Integer>
implements ProgressIndicator {
/**
* The GUI. Must be accessed by the Swing thread only.
*/
protected final ProgressPanel progressPanel;
/**
* 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 = 1000;
/**
* Time in milliseconds when the operation started.
*/
private long startTime;
/**
* A timer that shows the GUI after maxTimeWithoutDialog milliseconds.
*/
ShowTimer showTimer = new ShowTimer();
/**
* Must be called in the Event Dispatching Thread.
*
* @param progressPanel Panel to show progress.
*/
public SwingWorkerWithProgressIndicatorPanel(ProgressPanel progressPanel) {
assert (SwingUtilities.isEventDispatchThread());
assert (progressPanel != null);
this.progressPanel = progressPanel;
Action cancelAction = new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
cancel();
}
};
progressPanel.removeActionListeners();
progressPanel.setCancelAction(cancelAction);
}
/**
* Initialize the dialog. Can be called from any thread.
*/
@Override
public void start() {
synchronized (this) {
this.startTime = System.currentTimeMillis();
}
// initialize the GUI in the event dispatching thread
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
// start a timer task that will show the GUI a little later
// in case the client does not call progress() for a longer period.
showTimer.startTimer();
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() {
showTimer.stopTimer();
progressPanel.progress(0);
progressPanel.setVisible(false);
}
});
}
/**
* Inform the dialog that the operation has completed and it can be hidden.
*/
@Override
public void completeProgress() {
assert (SwingUtilities.isEventDispatchThread());
showTimer.stopTimer();
progress(100);
progressPanel.setVisible(false);
}
/**
* 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 (progressPanel.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 progress bar;
progressPanel.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 ShowTimer implements ActionListener {
private final Timer timer;
private ShowTimer() {
assert (SwingUtilities.isEventDispatchThread());
timer = new Timer(0, this);
timer.setRepeats(false);
}
private void stopTimer() {
assert (SwingUtilities.isEventDispatchThread());
if (timer != null) {
timer.stop();
}
}
/**
* Start a timer that will show the dialog in maxTimeWithoutDialog
* milliseconds if the dialog is not visible until then.
*/
private void startTimer() {
assert (SwingUtilities.isEventDispatchThread());
timer.stop();
timer.setInitialDelay(getMaxTimeWithoutDialog());
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) {
// this is guaranteed to be called in the event dispatch thread.
// only make the dialog visible if the operation is not
// yet finished
if (isCancelled() || progressPanel.isVisible() || getProgress() == 100) {
return;
}
// don't know how long the operation will take.
progressPanel.setIndeterminate(true);
// show the progress bar
progressPanel.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);
}
});
}
}