package org.limewire.ui.swing.properties;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GradientPaint;
import java.awt.GridBagLayout;
import java.awt.Paint;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.Timer;
import net.miginfocom.swing.MigLayout;
import org.jdesktop.application.Resource;
import org.jdesktop.swingx.JXPanel;
import org.jdesktop.swingx.painter.RectanglePainter;
import org.limewire.bittorrent.Torrent;
import org.limewire.core.api.FilePropertyKey;
import org.limewire.core.api.download.DownloadItem;
import org.limewire.core.api.download.DownloadPiecesInfo;
import org.limewire.core.api.download.DownloadState;
import org.limewire.core.api.download.DownloadItem.DownloadItemType;
import org.limewire.core.api.download.DownloadPiecesInfo.PieceState;
import org.limewire.core.api.library.PropertiableFile;
import org.limewire.ui.swing.util.FontUtils;
import org.limewire.ui.swing.util.GuiUtils;
import org.limewire.ui.swing.util.I18n;
import org.limewire.ui.swing.util.PainterUtils;
import org.limewire.ui.swing.util.ResizeUtils;
public class FileInfoPiecesPanel implements FileInfoPanel {
private static final int NUM_COLUMNS = 10;
private static final int MAX_NUM_ROWS = 10;
private static final int MAX_CELL_HEIGHT = 16;
private static final int MAX_CELL_WIDTH = 80;
private static final PieceIntensity ACTIVE_INTENSITY = new PieceIntensity(PieceState.ACTIVE);
private static final PieceIntensity UNAVAILABLE_INTENSITY = new PieceIntensity(PieceState.UNAVAILABLE);
private final NumberFormat formatter = new DecimalFormat("0.00");
/**
* The shorter the cooler, the shorter the more performance intensive.
*/
private static final int REFRESH_DELAY = 200;
@Resource private Color foreground;
@Resource private Font smallFont;
@Resource private Color legendBackground = PainterUtils.TRANSPARENT;
@Resource private Color downloadedForeground;
@Resource private Color availableForeground;
@Resource private Color activeForeground;
@Resource private Color unavailableForeground;
private final Color partialForegroundInitial;
private final Color partialForegroundFinal;
private final DownloadItem download;
private Torrent torrent = null;
private final JPanel component;
private PiecesGrid grid;
private Timer refresher;
private DownloadPiecesInfo piecesInfo;
private int numPieces = -1;
private int coalesceFactor;
private boolean finishedSuccessfully = false;
private int cachedColumns;
private int cachedRows;
private final JLabel statusLabel;
private JLabel numPiecesLabel;
private JLabel piecesPerCellLabel;
private JLabel piecesSizeLabel;
private JLabel ratioLabel;
private JLabel uploadedLabel;
private JLabel downloadedLabel;
private JLabel piecesCompletedLabel;
private JLabel failedDownloadLabel;
public FileInfoPiecesPanel(final DownloadItem download) {
this.download = download;
GuiUtils.assignResources(this);
partialForegroundInitial = createShade(availableForeground, downloadedForeground, .1);
partialForegroundFinal = createShade(downloadedForeground, availableForeground, .1);
component = new JPanel(new MigLayout("insets 6 0 0 0"));
component.setOpaque(false);
numPiecesLabel = createLabel("?");
piecesPerCellLabel = createLabel("?");
piecesCompletedLabel = createLabel("?");
piecesSizeLabel = createLabel("?");
downloadedLabel = createLabel("?");
failedDownloadLabel = createLabel("?");
grid = new PiecesGrid();
statusLabel = new JLabel(I18n.tr("Preparing View..."));
grid.setLayout(new GridBagLayout());
grid.add(statusLabel);
ResizeUtils.forceSize(grid, new Dimension(MAX_CELL_WIDTH*NUM_COLUMNS,
MAX_CELL_HEIGHT*MAX_NUM_ROWS));
final JPanel infoPanel = new JPanel(new MigLayout("insets 0, gap 0, fill"));
infoPanel.setOpaque(false);
JPanel legendPanel = new JPanel(new MigLayout("insets 8, gap 3"));
legendPanel.setBackground(legendBackground);
legendPanel.add(createLegendBox(activeForeground));
legendPanel.add(createLabel(I18n.tr("Active")), "gapright 5");
legendPanel.add(createLegendBox(availableForeground));
legendPanel.add(createLabel(I18n.tr("Available")), "wrap");
legendPanel.add(createLegendBox(downloadedForeground));
legendPanel.add(createLabel(I18n.tr("Done")), "gapright 5");
legendPanel.add(createLegendBox(unavailableForeground));
legendPanel.add(createLabel(I18n.tr("Unavailable")), "wrap");
legendPanel.add(createLegendBox(new GradientPaint(0, 0, partialForegroundInitial, 0, 1, partialForegroundFinal)));
legendPanel.add(createLabel(I18n.tr("Partially Done")), "wrap");
JPanel bottomLegend = new JPanel(new MigLayout("insets 0, gap 6, fillx"));
bottomLegend.setOpaque(false);
bottomLegend.add(createLabel(I18n.tr("Pieces per Cell:")));
bottomLegend.add(piecesPerCellLabel);
legendPanel.add(bottomLegend, "gaptop 5, span, alignx 50%");
JPanel rightPanel = new JPanel(new MigLayout("fillx, gap 0, insets 0"));
rightPanel.setOpaque(false);
rightPanel.add(legendPanel);
infoPanel.add(rightPanel, "dock east");
infoPanel.add(createBoldLabel(I18n.tr("Number of Pieces:")), "split 2");
infoPanel.add(numPiecesLabel, "wrap");
infoPanel.add(createBoldLabel(I18n.tr("Pieces Completed:")), "split 2");
infoPanel.add(piecesCompletedLabel, "wrap");
infoPanel.add(createBoldLabel(I18n.tr("Piece Size:")), "split 2");
infoPanel.add(piecesSizeLabel, "wrap");
infoPanel.add(createBoldLabel(I18n.tr("Downloaded:")), "split 2");
infoPanel.add(downloadedLabel, "wrap");
infoPanel.add(createBoldLabel(I18n.tr("Failed Download:")), "split 2");
infoPanel.add(failedDownloadLabel, "wrap");
if (download.getDownloadItemType() == DownloadItemType.BITTORRENT) {
torrent = (Torrent) download.getProperty(FilePropertyKey.TORRENT);
uploadedLabel = createLabel("");
ratioLabel = createLabel("");
infoPanel.add(createBoldLabel(I18n.tr("Uploaded:")), "split 2");
infoPanel.add(uploadedLabel, "wrap");
infoPanel.add(createBoldLabel(I18n.tr("Ratio:")), "split 2");
infoPanel.add(ratioLabel, "wrap");
}
component.add(grid, "wrap");
component.add(infoPanel, "growx");
refresher = new Timer(REFRESH_DELAY, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (isFinished()) {
refresher.stop();
if (download.getState() == DownloadState.CANCELLED) {
grid.setAlpha(.5f);
statusLabel.setText(I18n.tr("Download Cancelled!"));
statusLabel.setVisible(true);
grid.repaint();
} else {
finishedSuccessfully = true;
if (numPieces > 0) {
for ( int i=0 ; i<grid.getCellCount() ; i++ ) {
grid.setCellFillPaint(i, downloadedForeground);
}
} else {
statusLabel.setText(I18n.tr("Download Already Finished!"));
}
}
}
else {
if (calculatePieceData() > 0) {
updateTable();
}
}
grid.repaint();
// Hack to get the legends margin to line up with the table edge despite
// resizing due to rounding. NOTE: there will be a lag of one refresh cycle.
if (grid.isMarginUpdated()) {
int margin = grid.getInnerMargin();
if (margin != 0) {
infoPanel.setBorder(BorderFactory.createEmptyBorder(0, margin, 0, margin));
}
}
updateDownloadDetails();
}
private boolean isFinished() {
DownloadState state = download.getState();
// TODO: use DownloadState.isFinished() ? doesn't match properly right now though.
return state == DownloadState.DONE || state == DownloadState.FINISHING
|| state == DownloadState.CANCELLED || state == DownloadState.DANGEROUS
|| state == DownloadState.SCAN_FAILED
|| state == DownloadState.THREAT_FOUND;
}
});
refresher.setInitialDelay(0);
refresher.start();
}
private int calculatePieceData() {
piecesInfo = download.getPiecesInfo();
if (piecesInfo == null) {
return -1;
}
int newNum = piecesInfo.getNumPieces();
if(newNum != numPieces && newNum != 0) {
// Hide any previous status messages
statusLabel.setVisible(false);
// If the number of pieces has changed, resize the grid.
numPieces = newNum;
setupGrid();
}
return newNum;
}
private void setupGrid() {
int requiredRows = (int)Math.ceil((double)numPieces / NUM_COLUMNS);
int numRows = requiredRows;
coalesceFactor = 1;
if (requiredRows > MAX_NUM_ROWS) {
coalesceFactor = (int)Math.ceil((double)numPieces / (MAX_NUM_ROWS*NUM_COLUMNS));
numRows = (int)Math.ceil((double)numPieces / (coalesceFactor*NUM_COLUMNS));
piecesPerCellLabel.setText(""+coalesceFactor);
}
grid.resizeGrid(numRows, NUM_COLUMNS);
grid.setAlignmentX(Component.CENTER_ALIGNMENT);
grid.setAlignmentY(Component.CENTER_ALIGNMENT);
ResizeUtils.forceSize(grid, new Dimension(MAX_CELL_WIDTH*NUM_COLUMNS,
MAX_CELL_HEIGHT*numRows));
}
private static Component createLegendBox(Paint foreground) {
JXPanel panel = new JXPanel();
RectanglePainter<JXPanel> painter = new RectanglePainter<JXPanel>();
painter.setPaintStretched(true);
painter.setFillPaint(foreground);
painter.setBorderPaint(Color.BLACK);
painter.setAntialiasing(true);
painter.setCacheable(true);
panel.setBackgroundPainter(painter);
return panel;
}
private JLabel createLabel(String text) {
JLabel label = new JLabel(text);
label.setOpaque(false);
label.setForeground(foreground);
label.setFont(smallFont);
return label;
}
private JLabel createBoldLabel(String text) {
JLabel label = createLabel(text);
FontUtils.bold(label);
return label;
}
private void updateDownloadDetails() {
long currentSize = download.getCurrentSize();
long verifiedSize = download.getAmountVerified();
String downloadString = GuiUtils.formatUnitFromBytes(currentSize);
// Don't show verification unless the numbers deviate more than one byte
if (currentSize - verifiedSize > 1000) {
downloadString += " " + I18n.tr("({0} verified)", GuiUtils.formatUnitFromBytes(verifiedSize));
}
downloadedLabel.setText(downloadString);
if (numPieces > 0) {
numPiecesLabel.setText("" + numPieces);
}
if (piecesInfo != null) {
int completed = piecesInfo.getNumPiecesCompleted();
String completedText;
if (finishedSuccessfully) {
completedText = "" + numPieces;
}
else if (completed < 0) {
completedText = "?";
}
else {
completedText = "" + completed;
}
piecesCompletedLabel.setText(completedText);
piecesSizeLabel.setText(GuiUtils.formatUnitFromBytes(piecesInfo.getPieceSize()));
}
failedDownloadLabel.setText(GuiUtils.formatUnitFromBytes(download.getAmountLost()));
if (torrent != null) {
uploadedLabel.setText(GuiUtils.formatUnitFromBytes(torrent.getTotalUploaded()));
ratioLabel.setText(formatter.format(torrent.getSeedRatio()));
}
}
private void updateTable() {
// Correction if per chance the number of cells or rows is changed.
if (cachedRows != grid.getRows() || cachedColumns != grid.getColumns()) {
cachedRows = grid.getRows();
cachedColumns = grid.getColumns();
int cellsAvailable = grid.getCellCount();
coalesceFactor = (int)Math.ceil((double)numPieces / cellsAvailable);
piecesPerCellLabel.setText(""+coalesceFactor);
}
int gridSlot = 0;
for ( int i=0 ; i<numPieces ; i+=coalesceFactor ) {
Paint pieceForeground = Color.BLACK;
int numPiecesLeft = numPieces-coalesceFactor*gridSlot;
int piecesToCoalesce = coalesceFactor;
if (numPiecesLeft < coalesceFactor) {
piecesToCoalesce = numPiecesLeft;
}
PieceIntensity cumulativePieceIntensity = coalescePieceStates(piecesInfo, i, piecesToCoalesce);
PieceState cumulativeState = cumulativePieceIntensity.getState();
switch (cumulativeState) {
case ACTIVE :
pieceForeground = activeForeground;
break;
case PARTIAL :
pieceForeground = createShade(partialForegroundInitial, partialForegroundFinal,
cumulativePieceIntensity.getIntensity());
break;
case AVAILABLE :
pieceForeground = availableForeground;
break;
case DOWNLOADED :
pieceForeground = downloadedForeground;
break;
case UNAVAILABLE :
pieceForeground = unavailableForeground;
break;
default:
throw new IllegalStateException(cumulativeState.toString());
}
grid.setCellFillPaint(gridSlot++, pieceForeground);
}
}
@Override
public JComponent getComponent() {
return component;
}
@Override
public boolean hasChanged() {
return false;
}
@Override
public void save() {
}
@Override
public void updatePropertiableFile(PropertiableFile file) {
}
@Override
public void dispose() {
refresher.stop();
}
private static PieceIntensity coalescePieceStates(DownloadPiecesInfo piecesInfo, int startIndex, int piecesToCoalesce) {
// +1 for partial, +2 for done, 0 for available
int completedScore = 0;
PieceState workingState = null;
for ( int i=startIndex ; i < startIndex+piecesToCoalesce ; i++ ) {
PieceState state = piecesInfo.getPieceState(i);
if (state == PieceState.ACTIVE) {
return ACTIVE_INTENSITY;
}
if (state == PieceState.UNAVAILABLE) {
return UNAVAILABLE_INTENSITY;
}
switch (state) {
case PARTIAL :
completedScore+=1;
break;
case DOWNLOADED :
completedScore+=2;
break;
}
if (workingState != null && workingState != state) {
workingState = PieceState.PARTIAL;
}
else {
workingState = state;
}
}
if (workingState == PieceState.PARTIAL) {
int completedScoreMax = piecesToCoalesce*2;
return new PieceIntensity(workingState, (double)completedScore/completedScoreMax);
} else {
return new PieceIntensity(workingState);
}
}
private static class PieceIntensity {
private final PieceState state;
private final double intensity;
public PieceIntensity(PieceState state) {
this.state = state;
this.intensity = 1;
}
public PieceIntensity(PieceState state, double intensity) {
this.state = state;
this.intensity = intensity;
}
public PieceState getState() {
return state;
}
public double getIntensity() {
return intensity;
}
}
private static Color createShade(Color initialShade, Color finalShade, double intensity) {
int redDelta = finalShade.getRed() - initialShade.getRed();
int greenDelta = finalShade.getGreen() - initialShade.getGreen();
int blueDelta = finalShade.getBlue() - initialShade.getBlue();
redDelta *= intensity;
greenDelta *= intensity;
blueDelta *= intensity;
return new Color(initialShade.getRed() + redDelta,
initialShade.getGreen() + greenDelta,
initialShade.getBlue() + blueDelta);
}
}