/*
* %W% %E%
*
* Copyright (c) 2006, Oracle and/or its affiliates. All rights reserved.
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*/
package com.link_intersystems.swing;
import static org.apache.commons.lang3.time.DurationFormatUtils.formatDuration;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dialog;
import java.awt.Font;
import java.awt.Frame;
import java.awt.HeadlessException;
import java.awt.Window;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.SpringLayout;
import javax.swing.UIManager;
import com.link_intersystems.math.IncrementalAverage;
public class ProgressDialog {
private ProgressDialogState dialogState;
/**
* Constructs a graphic object that shows progress, typically by filling in
* a rectangular bar as the process nears completion.
*
* @param parentComponent
* the parent component for the dialog box
* @param message
* a descriptive message that will be shown to the user to
* indicate what operation is being monitored. This does not
* change as the operation progresses. See the message parameters
* to methods in {@link JOptionPane#message} for the range of
* values.
* @param note
* a short note describing the state of the operation. As the
* operation progresses, you can call setNote to change the note
* displayed. This is used, for example, in operations that
* iterate through a list of files to show the name of the file
* being processes. If note is initially null, there will be no
* note line in the dialog box and setNote will be ineffective
* @param min
* the lower bound of the range
* @param max
* the upper bound of the range
* @see JDialog
* @see JOptionPane
*/
public ProgressDialog(Component parentComponent, Object message, int min,
int max) {
this.dialogState = new ProgressDialogHidden();
this.dialogState.setMinimum(min);
this.dialogState.setMaximum(max);
this.dialogState
.setParentComponent(getWindowForComponent(parentComponent));
this.dialogState.setMessage(message);
this.dialogState = dialogState.nextState();
}
static Window getWindowForComponent(Component parentComponent)
throws HeadlessException {
if (parentComponent == null)
return JOptionPane.getRootFrame();
if (parentComponent instanceof Frame
|| parentComponent instanceof Dialog)
return (Window) parentComponent;
return getWindowForComponent(parentComponent.getParent());
}
/**
* Indicate the progress of the operation being monitored. If the specified
* value is >= the maximum, the progress monitor is closed.
*
* @param nv
* an int specifying the current value, between the maximum and
* minimum specified for this component
* @see #setMinimum
* @see #setMaximum
* @see #close
*/
public void setProgress(int nv) {
dialogState.setProgress(nv);
dialogState = dialogState.nextState();
}
/**
* Indicate that the operation is complete. This happens automatically when
* the value set by setProgress is >= max, but it may be called earlier if
* the operation ends early.
*/
public void close() {
dialogState.close();
}
/**
* Returns the minimum value -- the lower end of the progress value.
*
* @return an int representing the minimum value
* @see #setMinimum
*/
public int getMinimum() {
return dialogState.getMinimum();
}
/**
* Specifies the minimum value.
*
* @param m
* an int specifying the minimum value
* @see #getMinimum
*/
public void setMinimum(int m) {
dialogState.setMinimum(m);
}
/**
* Returns the maximum value -- the higher end of the progress value.
*
* @return an int representing the maximum value
* @see #setMaximum
*/
public int getMaximum() {
return dialogState.getMaximum();
}
/**
* Specifies the maximum value.
*
* @param m
* an int specifying the maximum value
* @see #getMaximum
*/
public void setMaximum(int m) {
dialogState.setMaximum(m);
}
/**
* Returns true if the user hits the Cancel button in the progress dialog.
*/
public boolean isCanceled() {
return dialogState.isCanceled();
}
/**
* Specifies the amount of time to wait before deciding whether or not to
* popup a progress monitor.
*
* @param millisToDecideToPopup
* an int specifying the time to wait, in milliseconds
* @see #getMillisToDecideToPopup
*/
public void setMillisToDecideToPopup(int millisToDecideToPopup) {
dialogState.setMillisToDecideToPopup(millisToDecideToPopup);
}
/**
* Returns the amount of time this object waits before deciding whether or
* not to popup a progress monitor.
*
* @see #setMillisToDecideToPopup
*/
public int getMillisToDecideToPopup() {
return dialogState.getMillisToDecideToPopup();
}
/**
* Specifies the amount of time it will take for the popup to appear. (If
* the predicted time remaining is less than this time, the popup won't be
* displayed.)
*
* @param millisToPopup
* an int specifying the time in milliseconds
* @see #getMillisToPopup
*/
public void setMillisToPopup(int millisToPopup) {
dialogState.setMillisToPopup(millisToPopup);
}
/**
* Returns the amount of time it will take for the popup to appear.
*
* @see #setMillisToPopup
*/
public int getMillisToPopup() {
return dialogState.getMillisToPopup();
}
public void setMessage(Object message) {
dialogState.setMessage(message);
}
public void setRemainingTimeEnabled(boolean remainingTimeEnabled) {
this.dialogState.setRemainingTimeEnabled(remainingTimeEnabled);
}
private static abstract class ProgressDialogState {
public abstract void setMessage(Object message);
public abstract void setRemainingTimeEnabled(
boolean remainingTimeEnabled);
public abstract int getMillisToPopup();
public abstract void setMillisToPopup(int millisToPopup);
public abstract int getMillisToDecideToPopup();
public abstract void setMillisToDecideToPopup(int millisToDecideToPopup);
public abstract boolean isCanceled();
public abstract int getMaximum();
public abstract void setMaximum(int max);
public abstract int getMinimum();
public abstract void setMinimum(int min);
public void close() {
}
public ProgressDialogState nextState() {
return this;
}
public abstract void setParentComponent(Component parentComponent);
public abstract void setProgress(int nv);
}
private class ProgressDialogHidden extends ProgressDialogState {
private IncrementalAverage workAverage = new IncrementalAverage();
private Object message;
private Component parentComponent;
private int progress;
private int min;
private int max;
private int millisToDecideToPopup = 500;
private int millisToPopup;
private long T0 = System.currentTimeMillis();
boolean remainingTimeEnabled;
@Override
public void setMessage(Object message) {
this.message = message;
}
@Override
public void setParentComponent(Component parentComponent) {
this.parentComponent = parentComponent;
}
@Override
public void setProgress(int nv) {
this.progress = nv;
}
@Override
public ProgressDialogState nextState() {
ProgressDialogState nextState = this;
if (max < 0) {
nextState = new ProgressDialogVisible(this);
} else {
nextState = timeToPopup(nextState);
}
return nextState;
}
private ProgressDialogState timeToPopup(ProgressDialogState nextState) {
long T = System.currentTimeMillis();
long dT = (int) (T - T0);
if (dT >= millisToDecideToPopup) {
int predictedCompletionTime;
if (progress > min) {
predictedCompletionTime = (int) ((long) dT * (max - min) / (progress - min));
} else {
predictedCompletionTime = millisToPopup;
}
if (predictedCompletionTime >= millisToPopup) {
nextState = new ProgressDialogVisible(this);
}
}
return nextState;
}
@Override
public void setMinimum(int min) {
this.min = min;
}
@Override
public int getMinimum() {
return min;
}
@Override
public void setMaximum(int max) {
this.max = max;
}
@Override
public int getMaximum() {
return max;
}
@Override
public boolean isCanceled() {
return false;
}
@Override
public void setMillisToDecideToPopup(int millisToDecideToPopup) {
this.millisToDecideToPopup = millisToDecideToPopup;
}
@Override
public int getMillisToDecideToPopup() {
return millisToDecideToPopup;
}
@Override
public void setMillisToPopup(int millisToPopup) {
this.millisToPopup = millisToPopup;
}
@Override
public int getMillisToPopup() {
return millisToPopup;
}
@Override
public void setRemainingTimeEnabled(boolean remainingTimeEnabled) {
this.remainingTimeEnabled = remainingTimeEnabled;
}
}
private class ProgressDialogVisible extends ProgressDialogState {
private static final String UNKNOWN_ETA = "ETA --:--:--";
private static final String UNKNOWN_PROGRESS = "--/--";
private JOptionPane pane;
private JProgressBar myBar = new JProgressBar();
private ProgressTextPanel progressTextPanel = new ProgressTextPanel();
private JDialog dialog;
private ProgressDialogHidden hiddenState;
private long last = Long.MIN_VALUE;
private long elapsedTimeStart = 0;
private long etaTime = -1;
private Timer timer;
private TimerTask updateProgressTextTask = new TimerTask() {
@Override
public void run() {
updateProgressStatus();
}
};
private Object[] cancelOption = new Object[] { UIManager
.getString("OptionPane.cancelButtonText") };
public ProgressDialogVisible(ProgressDialogHidden hiddenState) {
this.hiddenState = hiddenState;
int max = hiddenState.getMaximum();
myBar.setMinimum(hiddenState.getMinimum());
myBar.setMaximum(max);
myBar.setIndeterminate(max < 0);
pane = new ProgressOptionPane(getOptionPaneMessage());
dialog = pane.createDialog(hiddenState.parentComponent,
UIManager.getString("ProgressMonitor.progressText"));
dialog.setVisible(true);
}
@Override
public void setMessage(Object message) {
hiddenState.setMessage(message);
pane.setMessage(getOptionPaneMessage());
}
private Object[] getOptionPaneMessage() {
List<Object> messageElements = new ArrayList<Object>();
messageElements.add(hiddenState.message);
messageElements.add(myBar);
if (hiddenState.remainingTimeEnabled) {
updateProgressStatus();
messageElements.add(progressTextPanel);
}
return (Object[]) messageElements
.toArray(new Object[messageElements.size()]);
}
@Override
public void setParentComponent(Component parentComponent) {
dialog.dispose();
dialog = pane.createDialog(parentComponent,
UIManager.getString("ProgressMonitor.progressText"));
dialog.setVisible(true);
}
@Override
public void setProgress(int nv) {
if (nv >= 0) {
int oldValue = myBar.getValue();
etaTime = calculateRemainingTime(oldValue, nv);
if (hiddenState.remainingTimeEnabled) {
updateProgressStatus();
}
myBar.setValue(nv);
} else {
myBar.setIndeterminate(true);
}
}
private void updateProgressStatus() {
String timeString = null;
String progressString = null;
int nv = myBar.getValue();
if (myBar.isIndeterminate()) {
long elapsedTime = calculateElapsedTime();
timeString = "ELAPS " + formatDuration(elapsedTime, "HH:mm:ss");
progressString = "---/---";
} else {
timeString = UNKNOWN_ETA;
progressString = UNKNOWN_PROGRESS;
if (etaTime >= 0) {
timeString = "ETA " + formatDuration(etaTime, "HH:mm:ss");
}
progressString = String.format("%s/%s", nv, getMaximum());
}
progressTextPanel.getTimeDocument().setText(timeString);
progressTextPanel.getProgressDocument().setText(progressString);
}
private long calculateElapsedTime() {
return System.currentTimeMillis() - elapsedTimeStart;
}
private long calculateRemainingTime(int oldValue, int newValue) {
long actual = System.currentTimeMillis();
if (last == Long.MIN_VALUE) {
last = actual;
}
long diff = actual - last;
if (diff > 0) {
double averagePerWork = (double) diff
/ (double) (newValue - oldValue);
hiddenState.workAverage.addValue(averagePerWork);
}
last = actual;
long etaTimeMs = -1;
Double value = hiddenState.workAverage.getValue();
if (value != 0.0) {
int maximum = getMaximum();
int remaining = maximum - newValue;
etaTimeMs = (long) (value * remaining);
}
return etaTimeMs;
}
@Override
public void close() {
dialog.setVisible(false);
dialog.dispose();
dialog = null;
pane = null;
myBar = null;
if (timer != null) {
timer.cancel();
}
}
@Override
public void setMinimum(int min) {
myBar.setMinimum(min);
}
@Override
public int getMinimum() {
return myBar.getMinimum();
}
@Override
public void setMaximum(int max) {
myBar.setMaximum(max);
if (max < 0) {
elapsedTimeStart = System.currentTimeMillis();
myBar.setIndeterminate(true);
timer = new Timer(true);
timer.schedule(updateProgressTextTask, 0, 1000);
} else {
timer.cancel();
myBar.setIndeterminate(false);
hiddenState.workAverage = new IncrementalAverage();
etaTime = -1;
}
updateProgressStatus();
}
@Override
public int getMaximum() {
return myBar.getMaximum();
}
@Override
public boolean isCanceled() {
Object v = pane.getValue();
return ((v != null) && (cancelOption.length == 1) && (v
.equals(cancelOption[0])));
}
@Override
public void setMillisToDecideToPopup(int millisToDecideToPopup) {
hiddenState.setMillisToDecideToPopup(millisToDecideToPopup);
}
@Override
public int getMillisToDecideToPopup() {
return hiddenState.getMillisToDecideToPopup();
}
@Override
public int getMillisToPopup() {
return hiddenState.getMillisToPopup();
}
@Override
public void setMillisToPopup(int millisToPopup) {
hiddenState.setMillisToPopup(millisToPopup);
}
@Override
public void setRemainingTimeEnabled(boolean remainingTimeEnabled) {
this.hiddenState.setRemainingTimeEnabled(remainingTimeEnabled);
pane.setMessage(getOptionPaneMessage());
}
private class ProgressOptionPane extends JOptionPane {
private static final long serialVersionUID = 6465617362433967869L;
ProgressOptionPane(Object messageList) {
super(messageList, JOptionPane.PLAIN_MESSAGE,
JOptionPane.DEFAULT_OPTION, null,
ProgressDialogVisible.this.cancelOption, null);
}
public int getMaxCharactersPerLineCount() {
return 60;
}
// Equivalent to JOptionPane.createDialog,
// but create a modeless dialog.
// This is necessary because the Solaris implementation doesn't
// support Dialog.setModal yet.
public JDialog createDialog(Component parentComponent, String title) {
final JDialog dialog;
Window window = getWindowForComponent(parentComponent);
if (window instanceof Frame) {
dialog = new JDialog((Frame) window, title, false);
} else {
dialog = new JDialog((Dialog) window, title, false);
}
Container contentPane = dialog.getContentPane();
contentPane.setLayout(new BorderLayout());
contentPane.add(this, BorderLayout.CENTER);
dialog.pack();
dialog.setLocationRelativeTo(parentComponent);
dialog.addWindowListener(new WindowAdapter() {
boolean gotFocus = false;
public void windowClosing(WindowEvent we) {
setValue(cancelOption[0]);
}
public void windowActivated(WindowEvent we) {
// Once window gets focus, set initial focus
if (!gotFocus) {
selectInitialValue();
gotFocus = true;
}
}
});
addPropertyChangeListener(new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent event) {
if (dialog.isVisible()
&& event.getSource() == ProgressOptionPane.this
&& (event.getPropertyName().equals(
VALUE_PROPERTY) || event
.getPropertyName().equals(
INPUT_VALUE_PROPERTY))) {
dialog.setVisible(false);
dialog.dispose();
}
}
});
return dialog;
}
}
}
private static class ProgressTextPanel extends JPanel {
private static final long serialVersionUID = 3905254906266079708L;
private JLabel progressLabel = new JLabel();
private JLabelDocumentAdapter progressLabelDocumentAdapter = new JLabelDocumentAdapter(
progressLabel);
private PlainSimpleDocument progressDocument = new PlainSimpleDocument();
private JLabel etaLabel = new JLabel();
private JLabelDocumentAdapter etaLabelDocumentAdapter = new JLabelDocumentAdapter(
etaLabel);
private PlainSimpleDocument etaDocument = new PlainSimpleDocument();
public ProgressTextPanel() {
setLayout(new SpringLayout());
progressLabelDocumentAdapter.setDocument(progressDocument);
etaLabelDocumentAdapter.setDocument(etaDocument);
Font oldFont = etaLabel.getFont();
Font newFont = new Font("monospaced", Font.PLAIN, oldFont.getSize());
etaLabel.setFont(newFont);
progressLabel.setFont(newFont);
add(etaLabel);
add(progressLabel);
SpringUtilities.makeCompactGrid(this, 1, 2, 0, 0, 15, 3);
}
public PlainSimpleDocument getProgressDocument() {
return progressDocument;
}
public PlainSimpleDocument getTimeDocument() {
return etaDocument;
}
}
}