// Copyright (C) 2001-2003 Jon A. Maxwell (JAM)
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
package net.sourceforge.jnlp.cache;
import static net.sourceforge.jnlp.runtime.Translator.R;
import java.awt.*;
import java.awt.event.*;
import java.net.*;
import java.util.*;
import java.util.List;
import javax.swing.*;
import javax.swing.Timer;
import javax.jnlp.*;
import net.sourceforge.jnlp.runtime.*;
import net.sourceforge.jnlp.util.ImageResources;
import net.sourceforge.jnlp.util.ScreenFinder;
/**
* Show the progress of downloads.
*
* @author Jon A. Maxwell (JAM) - initial author
* @version $Revision: 1.3 $
*/
public class DefaultDownloadIndicator implements DownloadIndicator {
// todo: rewrite this to cut down on size/complexity; smarter
// panels (JList, renderer) understand resources instead of
// nested panels and grid-bag mess.
// todo: fix bug where user closes download box and it
// never(?) reappears.
// todo: UI for user to cancel/restart download
// todo: this should be synchronized at some point but conflicts
// aren't very likely.
private static String downloading = R("CDownloading");
private static String complete = R("CComplete");
/** time to wait after completing but before window closes */
private static final int CLOSE_DELAY = 750;
/** the display window */
private static JFrame frame;
private static final Object frameMutex = new Object();
/** shared constraint */
static GridBagConstraints vertical;
static GridBagConstraints verticalNoClean;
static GridBagConstraints verticalIndent;
static {
vertical = new GridBagConstraints();
vertical.gridwidth = GridBagConstraints.REMAINDER;
vertical.weightx = 1.0;
vertical.fill = GridBagConstraints.HORIZONTAL;
vertical.anchor = GridBagConstraints.WEST;
verticalNoClean = new GridBagConstraints();
verticalNoClean.weightx = 1.0;
verticalIndent = (GridBagConstraints) vertical.clone();
verticalIndent.insets = new Insets(0, 10, 3, 0);
}
/**
* Return the update rate.
*/
public int getUpdateRate() {
return 150; //ms
}
/**
* Return the initial delay before obtaining a listener.
*/
public int getInitialDelay() {
return 300; //ms
}
/**
* Return a download service listener that displays the progress
* in a shared download info window.
*
* @param app the downloading application, or null if N/A
* @param downloadName name identifying the download to the user
* @param resources initial urls to display (not required)
*/
public DownloadServiceListener getListener(ApplicationInstance app, String downloadName, URL resources[]) {
DownloadPanel result = new DownloadPanel(downloadName);
synchronized (frameMutex) {
if (frame == null) {
frame=createDownloadIndicatorFrame(true);
}
if (resources != null) {
for (URL url : resources) {
result.addProgressPanel(url, null);
}
}
frame.getContentPane().add(result, vertical);
frame.pack();
placeFrameToLowerRight();
result.addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
placeFrameToLowerRight();
}
});
frame.setVisible(true);
return result;
}
}
public static JFrame createDownloadIndicatorFrame(boolean undecorated) throws HeadlessException {
JFrame f = new JFrame(downloading + "...");
f.setUndecorated(undecorated);
f.setIconImages(ImageResources.INSTANCE.getApplicationImages());
f.getContentPane().setLayout(new GridBagLayout());
return f;
}
/**
* This places indicator to lower right corner of active monitor.
*/
private static void placeFrameToLowerRight() throws HeadlessException {
Rectangle bounds = ScreenFinder.getCurrentScreenSizeWithoutBounds();
frame.setLocation(bounds.width+bounds.x - frame.getWidth(),
bounds.height+bounds.y - frame.getHeight());
}
/**
* Remove a download service listener that was obtained by
* calling the getDownloadListener method from the shared
* download info window.
*/
public void disposeListener(final DownloadServiceListener listener) {
if (!(listener instanceof DownloadPanel))
return;
ActionListener hider = new ActionListener() {
public void actionPerformed(ActionEvent evt) {
synchronized(frameMutex) {
frame.getContentPane().remove((DownloadPanel) listener);
frame.pack();
if (frame.getContentPane().getComponentCount() == 0) {
frame.setVisible(false);
frame.dispose();
frame = null;
}
}
}
};
Timer timer = new Timer(CLOSE_DELAY, hider);
timer.setRepeats(false);
timer.start();
}
/**
* Groups the url progress in a panel.
*/
static class DownloadPanel extends JPanel implements DownloadServiceListener {
private final DownloadPanel self;
private static enum States{
ONE_JAR, COLLAPSED, DETAILED;
}
private static final String DETAILS=R("ButShowDetails");
private static final String HIDE_DETAILS=R("ButHideDetails");
/** the download name */
private String downloadName;
/** Downloading part: */
private JLabel header = new JLabel();
/** Show/hide detailsButton button: */
private final JButton detailsButton;
private static final URL magnifyGlassUrl = ClassLoader.getSystemResource("net/sourceforge/jnlp/resources/showDownloadDetails.png");
private static final URL redCrossUrl = ClassLoader.getSystemResource("net/sourceforge/jnlp/resources/hideDownloadDetails.png");
private static final Icon magnifyGlassIcon = new ImageIcon(magnifyGlassUrl);
private static final Icon redCrossIcon = new ImageIcon(redCrossUrl);
/** used instead of detailsButton button in case of one jar*/
private JLabel delimiter = new JLabel("");
/** all already created progress bars*/
private List progressPanels = new ArrayList();
private States state=States.ONE_JAR;
private ProgressPanel mainProgressPanel;
/** list of URLs being downloaded */
private List urls = new ArrayList();
/** list of ProgressPanels */
private List panels = new ArrayList();
/**
* Create a new download panel for with the specified download
* name.
*/
protected DownloadPanel(String downloadName) {
self = this;
setLayout(new GridBagLayout());
this.downloadName = downloadName;
this.add(header, verticalNoClean);
header.setFont(header.getFont().deriveFont(Font.BOLD));
this.add(delimiter, vertical);
detailsButton = new JButton(magnifyGlassIcon);
int w = magnifyGlassIcon.getIconWidth();
int h = magnifyGlassIcon.getIconHeight();
detailsButton.setPreferredSize(new Dimension(w + 2, h + 2));
detailsButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (state == States.DETAILED) {
state = States.COLLAPSED;
detailsButton.setToolTipText(DETAILS);
detailsButton.setIcon(magnifyGlassIcon);
for (ProgressPanel progressPanel : progressPanels) {
remove(progressPanel);
}
add(mainProgressPanel, verticalIndent);
recreateFrame(true);
} else {
state = States.DETAILED;
detailsButton.setToolTipText(HIDE_DETAILS);
detailsButton.setIcon(redCrossIcon);
remove(mainProgressPanel);
for (ProgressPanel progressPanel : progressPanels) {
add(progressPanel, verticalIndent);
}
recreateFrame(false);
}
}
public void recreateFrame(boolean undecorated) throws HeadlessException {
JFrame oldFrame = frame;
frame = createDownloadIndicatorFrame(undecorated);
frame.getContentPane().add(self, vertical);
synchronized (frameMutex) {
frame.pack();
placeFrameToLowerRight();
}
frame.setVisible(true);
oldFrame.dispose();
}
});
setOverallPercent(0);
}
/**
* Add a ProgressPanel for a URL.
*/
protected void addProgressPanel(URL url, String version) {
if (!urls.contains(url)) {
ProgressPanel panel = new ProgressPanel(url, version);
if (state != States.COLLAPSED) {
add(panel, verticalIndent);
}
progressPanels.add(panel);
urls.add(url);
panels.add(panel);
//download indicator does not know about added jars
//When only one jar is added to downlaod queue then its progress is
//shown, and there is no show detail button.
//When second one is added, then it already knows that there will
//be two or more jars, so it swap to collapsed state in count of two.
//no later, no sooner
if (panels.size() == 2){
remove(panels.get(0));
remove(panels.get(1));
remove(delimiter);
add(detailsButton,vertical);
mainProgressPanel=new ProgressPanel();
add(mainProgressPanel, verticalIndent);
state=States.COLLAPSED;
}
synchronized (frameMutex) {
frame.pack();
placeFrameToLowerRight();
}
}
}
/**
* Update the download progress of a url.
*/
protected void update(final URL url, final String version,
final long readSoFar, final long total,
final int overallPercent) {
Runnable r = new Runnable() {
public void run() {
if (!urls.contains(url))
addProgressPanel(url, version);
setOverallPercent(overallPercent);
ProgressPanel panel = panels.get(urls.indexOf(url));
panel.setProgress(readSoFar, total);
panel.repaint();
}
};
SwingUtilities.invokeLater(r);
}
/**
* Sets the overall percent completed.
* should be called via invokeLater
*/
public void setOverallPercent(int percent) {
// don't get whole string from resource and sub in
// values because it'll be doing a MessageFormat for
// each update.
header.setText(downloading + " " + downloadName + ": " + percent + "% " + complete + ".");
Container c = header.getParent();
//we need to adapt both panels and also frame to new length of header text
while (c != null) {
c.invalidate();
c.validate();
if (c instanceof Window){
((Window) c).pack();
}
c=c.getParent();
}
if (mainProgressPanel != null) {
mainProgressPanel.setProgress(percent, 100);
mainProgressPanel.repaint();
}
}
/**
* Called when a download failed.
*/
public void downloadFailed(URL url, String version) {
update(url, version, -1, -1, -1);
}
/**
* Called when a download has progressed.
*/
public void progress(URL url, String version, long readSoFar, long total, int overallPercent) {
update(url, version, readSoFar, total, overallPercent);
}
/**
* Called when an archive is patched.
*/
public void upgradingArchive(URL url, String version, int patchPercent, int overallPercent) {
update(url, version, patchPercent, 100, overallPercent);
}
/**
* Called when a download is being validated.
*/
public void validating(URL url, String version, long entry, long total, int overallPercent) {
update(url, version, entry, total, overallPercent);
}
};
/**
* A progress bar with the URL next to it.
*/
static class ProgressPanel extends JPanel {
private JPanel bar = new JPanel();
private long total;
private long readSoFar;
private Dimension size = new Dimension(80, 15);
ProgressPanel() {
bar.setMinimumSize(size);
bar.setPreferredSize(size);
bar.setOpaque(false);
setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
styleGridBagConstraints(gbc);
add(bar, gbc);
}
ProgressPanel(URL url, String version) {
this(" " + url.getHost() + "/" + url.getFile(),version);
}
ProgressPanel(String caption, String version) {
JLabel location = new JLabel(caption);
bar.setMinimumSize(size);
bar.setPreferredSize(size);
bar.setOpaque(false);
setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.weightx = 0.0;
gbc.fill = GridBagConstraints.NONE;
gbc.gridwidth = GridBagConstraints.RELATIVE;
add(bar, gbc);
styleGridBagConstraints(gbc);
add(location, gbc);
}
public void setProgress(long readSoFar, long total) {
this.readSoFar = readSoFar;
this.total = total;
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
int x = bar.getX();
int y = bar.getY();
int h = bar.getHeight();
int w = bar.getWidth();
if (readSoFar <= 0 || total <= 0) {
// make barber pole
} else {
double progress = (double) readSoFar / (double) total;
int divide = (int) (w * progress);
g.setColor(Color.white);
g.fillRect(x, y, w, h);
g.setColor(Color.blue);
g.fillRect(x + 1, y + 1, divide - 1, h - 1);
}
}
private void styleGridBagConstraints(GridBagConstraints gbc) {
gbc.insets = new Insets(0, 3, 0, 0);
gbc.weightx = 1.0;
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.anchor = GridBagConstraints.WEST;
}
};
}