package org.limewire.ui.swing.properties;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.EventObject;
import java.util.List;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import javax.swing.SwingConstants;
import javax.swing.event.CellEditorListener;
import javax.swing.event.ChangeEvent;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import net.miginfocom.swing.MigLayout;
import org.jdesktop.application.Resource;
import org.jdesktop.swingx.decorator.SortKey;
import org.limewire.bittorrent.Torrent;
import org.limewire.bittorrent.TorrentEvent;
import org.limewire.bittorrent.TorrentEventType;
import org.limewire.bittorrent.TorrentFileEntry;
import org.limewire.collection.glazedlists.GlazedListsFactory;
import org.limewire.core.api.library.PropertiableFile;
import org.limewire.listener.EventListener;
import org.limewire.ui.swing.action.AbstractAction;
import org.limewire.ui.swing.components.HyperlinkButton;
import org.limewire.ui.swing.table.AbstractTableFormat;
import org.limewire.ui.swing.table.DefaultLimeTableCellRenderer;
import org.limewire.ui.swing.table.FileSizeRenderer;
import org.limewire.ui.swing.table.MouseableTable;
import org.limewire.ui.swing.table.TableCellHeaderRenderer;
import org.limewire.ui.swing.util.EventListJXTableSorting;
import org.limewire.ui.swing.util.EventListTableSortFormat;
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.SwingUtils;
import org.limewire.util.Objects;
import ca.odell.glazedlists.BasicEventList;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.GlazedLists;
import ca.odell.glazedlists.ObservableElementList;
import ca.odell.glazedlists.SortedList;
import ca.odell.glazedlists.gui.AdvancedTableFormat;
import ca.odell.glazedlists.swing.DefaultEventTableModel;
public class FileInfoBittorrentPanel implements FileInfoPanel, EventListener<TorrentEvent> {
@Resource private Font selectFont;
public static final String TORRENT_FILE_ENTRY_SELECTED = "torrentFileEntrySelected";
private static final int DONT_DOWNLOAD = 0;
private static final int LOWEST_PRIORITY = 1;
private static final int NORMAL_PRIORITY = 2;
private static final int HIGHEST_PRIORITY = 3;
private final Torrent torrent;
private final JPanel component;
private BitTorrentTable table;
private EventListJXTableSorting tableSorting;
/**
* Items in the eventList are expected to be in the order that they are
* returned from the Torrent instance. This is so we can pull items out by
* the matching index in the TorrentFileEntry.
*/
private EventList<TorrentFileEntryWrapper> eventList;
private PropertyChangeSupport support = new PropertyChangeSupport(this);
public FileInfoBittorrentPanel(Torrent torrent) {
GuiUtils.assignResources(this);
this.torrent = torrent;
component = new JPanel(new MigLayout("fill", "[grow]", "[][grow]"));
init();
}
@Override
public JComponent getComponent() {
return component;
}
@Override
public void updatePropertiableFile(PropertiableFile file) {
// do nothing
}
private void init() {
component.setOpaque(false);
ObservableElementList.Connector<TorrentFileEntryWrapper> torrentFileEntryConnector = GlazedLists
.beanConnector(TorrentFileEntryWrapper.class);
eventList = GlazedListsFactory.observableElementList(
new BasicEventList<TorrentFileEntryWrapper>(), torrentFileEntryConnector);
List<TorrentFileEntry> fileEntries = torrent.getTorrentFileEntries();
for (TorrentFileEntry entry : fileEntries) {
eventList.add(new TorrentFileEntryWrapper(entry));
}
// NOTE: this sortedList should never be used for iterating over torrent values, its strictly used to
// sort the table
SortedList<TorrentFileEntryWrapper> sortedList = GlazedListsFactory.sortedList(eventList, null);
BitTorrentTableFormat tableFormat = new BitTorrentTableFormat();
table = new BitTorrentTable(new DefaultEventTableModel<TorrentFileEntryWrapper>(sortedList, tableFormat));
tableSorting = EventListJXTableSorting.install(table, sortedList, tableFormat);
JLabel selectLabel = new JLabel(I18n.tr("Select"));
selectLabel.setFont(selectFont);
HyperlinkButton allButton = new HyperlinkButton(new SelectAll(I18n.tr("all")));
allButton.setFont(selectFont);
HyperlinkButton noneButton = new HyperlinkButton(new SelectNone(I18n.tr("none")));
noneButton.setFont(selectFont);
// only show add/remove all buttons if is downloadng
if(!torrent.isFinished()) {
component.add(selectLabel, "gapleft 5, gaptop 2, split 3");
component.add(allButton, "gapleft 6, gaptop 2");
component.add(noneButton, "gapleft 6, gaptop 2, wrap");
}
JScrollPane scrollPane = new JScrollPane(table);
configureEnclosingScrollPane(scrollPane);
component.add(scrollPane, "grow, gaptop 7");
torrent.addListener(this);
}
@Override
public boolean hasChanged() {
boolean hasChanged = false;
for (TorrentFileEntryWrapper wrapper : eventList) {
if (wrapper.hasChanged()) {
hasChanged = true;
break;
}
}
return hasChanged;
}
@Override
public void save() {
if (!torrent.isEditable()) {
return;
}
torrent.getLock().lock();
try {
if (hasChanged() && !torrent.isFinished() && torrent.isValid()) {
for (TorrentFileEntryWrapper wrapper : eventList) {
torrent.setTorrenFileEntryPriority(wrapper.getTorrentFileEntry(), wrapper
.getPriority());
}
}
} finally {
torrent.getLock().unlock();
}
}
@Override
public void dispose() {
tableSorting.uninstall();
torrent.removeListener(this);
}
/**
* Fills in the top right corner if a scrollbar appears with an empty table
* header.
*/
protected void configureEnclosingScrollPane(JScrollPane scrollPane) {
JTableHeader th = new JTableHeader();
th.setDefaultRenderer(new TableCellHeaderRenderer());
// Put a dummy header in the upper-right corner.
final Component renderer = th.getDefaultRenderer().getTableCellRendererComponent(null, "", false, false, -1, -1);
JPanel cornerComponent = new JPanel(new BorderLayout());
cornerComponent.add(renderer, BorderLayout.CENTER);
scrollPane.setCorner(JScrollPane.UPPER_RIGHT_CORNER, cornerComponent);
}
private class BitTorrentTable extends MouseableTable {
boolean torrentPartSelected = true;
public BitTorrentTable(final DefaultEventTableModel<TorrentFileEntryWrapper> model) {
super(model);
setShowHorizontalLines(false);
setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
setColumnSelectionAllowed(false);
final CheckBoxRendererEditor checkBoxEditor = new CheckBoxRendererEditor();
checkBoxEditor.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (checkBoxEditor.getCellEditorValue() != null) {
checkBoxEditor.getCellEditorValue().setPriority(checkBoxEditor.isSelected() ? 1 : 0);
checkBoxEditor.cancelCellEditing();
}
validateSelection();
BitTorrentTable.this.repaint();
}
});
getColumn(BitTorrentTableFormat.DOWNLOAD_INDEX).setCellRenderer(new CheckBoxRendererEditor());
getColumn(BitTorrentTableFormat.DOWNLOAD_INDEX).setCellEditor(checkBoxEditor);
getColumn(BitTorrentTableFormat.SIZE_INDEX).setCellRenderer(new FileSizeRenderer());
getColumn(BitTorrentTableFormat.SIZE_INDEX).setMaxWidth(72);
getColumn(BitTorrentTableFormat.PERCENT_INDEX).setCellRenderer(new PercentRenderer());
getColumn(BitTorrentTableFormat.PERCENT_INDEX).setMaxWidth(20);
getColumn(BitTorrentTableFormat.NAME_INDEX).setCellRenderer(new DefaultLimeTableCellRenderer());
getColumn(BitTorrentTableFormat.NAME_INDEX).setMinWidth(140);
final PriorityRendererEditor editor = new PriorityRendererEditor();
editor.getButton().addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
int oldPriority = editor.getCellEditorValue().getPriority();
if (oldPriority != DONT_DOWNLOAD) {
editor.getCellEditorValue().setPriority(
oldPriority + 1 > HIGHEST_PRIORITY ? LOWEST_PRIORITY
: (oldPriority + 1));
editor.cancelCellEditing();
BitTorrentTable.this.repaint();
}
}
});
getColumn(BitTorrentTableFormat.PRIORITY_INDEX).setCellRenderer(
new PriorityRendererEditor());
getColumn(BitTorrentTableFormat.PRIORITY_INDEX).setCellEditor(editor);
getColumnExt(BitTorrentTableFormat.DOWNLOAD_INDEX).setMaxWidth(30);
getColumnExt(BitTorrentTableFormat.DOWNLOAD_INDEX).setMinWidth(30);
getColumnExt(BitTorrentTableFormat.PERCENT_INDEX).setMaxWidth(50);
getColumnExt(BitTorrentTableFormat.PERCENT_INDEX).setMinWidth(50);
getColumnExt(BitTorrentTableFormat.PRIORITY_INDEX).setMaxWidth(60);
getColumnExt(BitTorrentTableFormat.PRIORITY_INDEX).setMinWidth(60);
boolean editable = torrent.isEditable();
getColumnExt(BitTorrentTableFormat.PRIORITY_INDEX).setVisible(editable);
getColumnExt(BitTorrentTableFormat.PERCENT_INDEX).setVisible(editable);
getColumnExt(BitTorrentTableFormat.DOWNLOAD_INDEX).setVisible(editable);
}
private void validateSelection() {
if ( isAnyTorrentPartSelected() != torrentPartSelected ) {
torrentPartSelected = !torrentPartSelected;
support.firePropertyChange(TORRENT_FILE_ENTRY_SELECTED, !torrentPartSelected, torrentPartSelected);
}
}
@SuppressWarnings("unchecked")
private boolean isAnyTorrentPartSelected() {
DefaultEventTableModel<TorrentFileEntryWrapper> model = (DefaultEventTableModel<TorrentFileEntryWrapper>)getModel();
for (int counter = 0; counter < model.getRowCount(); counter++) {
TorrentFileEntryWrapper torrentFile = model.getElementAt(counter);
if (torrentFile.getPriority() != 0)
return true;
}
return false;
}
}
private class BitTorrentTableFormat extends AbstractTableFormat<TorrentFileEntryWrapper> implements EventListTableSortFormat, AdvancedTableFormat<TorrentFileEntryWrapper> {
private static final int DOWNLOAD_INDEX = 0;
private static final int NAME_INDEX = 1;
private static final int SIZE_INDEX = 2;
private static final int PERCENT_INDEX = 3;
private static final int PRIORITY_INDEX = 4;
public BitTorrentTableFormat() {
super(I18n.tr("DL"), I18n.tr("Name"), I18n.tr("Size"), I18n.tr("%"), I18n.tr("Priority"));
}
@Override
public Object getColumnValue(TorrentFileEntryWrapper baseObject, int column) {
switch (column) {
case DOWNLOAD_INDEX:
return baseObject;
case NAME_INDEX:
return baseObject.getPath();
case SIZE_INDEX:
return baseObject.getSize();
case PERCENT_INDEX:
return baseObject;
case PRIORITY_INDEX:
return baseObject;
}
throw new IllegalStateException("Unknown column:" + column);
}
@Override
public Comparator getColumnComparator(int column) {
switch(column) {
case DOWNLOAD_INDEX:
return new SelectedComparator();
case NAME_INDEX:
return new NameComparator();
case SIZE_INDEX:
return Objects.getComparator(true);
case PERCENT_INDEX:
return new PercentComparator();
case PRIORITY_INDEX:
return new PriorityComparator();
}
throw new IllegalStateException("Unknown column:" + column);
}
@Override
public List<SortKey> getDefaultSortKeys() {
return Collections.emptyList();
}
@Override
public List<SortKey> getPreSortColumns() {
return Collections.emptyList();
}
@Override
public List<Integer> getSecondarySortColumns(int column) {
return Collections.emptyList();
}
@Override
public Class getColumnClass(int column) {
switch(column) {
case DOWNLOAD_INDEX:
return TorrentFileEntryWrapper.class;
case NAME_INDEX:
return String.class;
case SIZE_INDEX:
return Long.class;
case PERCENT_INDEX:
return TorrentFileEntryWrapper.class;
case PRIORITY_INDEX:
return TorrentFileEntryWrapper.class;
}
throw new IllegalStateException("Unknown column:" + column);
}
}
/**
* Compares whether two TorrentFileEntries are selected or not.
*/
private class SelectedComparator implements Comparator<TorrentFileEntryWrapper> {
@Override
public int compare(TorrentFileEntryWrapper o1, TorrentFileEntryWrapper o2) {
boolean o1isSelected = getIsSelected(o1);
boolean o2isSelected = getIsSelected(o2);
if(o1isSelected && o2isSelected)
return 0;
else if(o1isSelected)
return 1;
else
return -1;
}
private boolean getIsSelected(TorrentFileEntryWrapper wrapper) {
if (torrent.isFinished()) {
return wrapper.getProgress() == 1.0f && wrapper.getPriority() > DONT_DOWNLOAD;
} else if (wrapper.getProgress() == 1.0f) {
return true;
} else {
return wrapper.getPriority() != DONT_DOWNLOAD;
}
}
}
/**
* Compares the Percent complete of two TorrentFileEntries. If the file is
* not being downloaded, any percent completed is ignored.
*/
private class PercentComparator implements Comparator<TorrentFileEntryWrapper> {
@Override
public int compare(TorrentFileEntryWrapper o1, TorrentFileEntryWrapper o2) {
int o1Percent = (int) o1.getProgress() * 100;
int o2Percent = (int) o2.getProgress() * 100;
if(o1.getPriority() == DONT_DOWNLOAD)
o1Percent = -1;
if(o2.getPriority() == DONT_DOWNLOAD)
o2Percent = -1;
return o1Percent - o2Percent;
}
}
/**
* Compares the Priority of two TorrentFileEntries. Completed torrents
* are considered higher priority than the max priority setting.
*/
private class PriorityComparator implements Comparator<TorrentFileEntryWrapper> {
@Override
public int compare(TorrentFileEntryWrapper o1, TorrentFileEntryWrapper o2) {
int o1Priority = getPriority(o1);
int o2Priority = getPriority(o2);
return o1Priority - o2Priority;
}
private int getPriority(TorrentFileEntryWrapper wrapper) {
if (wrapper.getProgress() == 1.0f) {
return 4;
} else {
return wrapper.getPriority();
}
}
}
/**
* Comapares the path/filename of two TorrentFileEntries.
*/
private class NameComparator implements Comparator<String> {
@Override
public int compare(String o1, String o2) {
return o1.compareToIgnoreCase(o2);
}
}
private class CheckBoxRendererEditor extends JCheckBox implements TableCellRenderer,
TableCellEditor {
/** List of cell editor listeners. */
private final List<CellEditorListener> listenerList = new ArrayList<CellEditorListener>();
private TorrentFileEntryWrapper currentWrapper;
public CheckBoxRendererEditor() {
setHorizontalAlignment(SwingConstants.CENTER);
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column) {
return getTableCellComponent(table, value, isSelected, column, column);
}
@Override
public Component getTableCellEditorComponent(JTable table, Object value,
boolean isSelected, int row, int column) {
return getTableCellComponent(table, value, isSelected, column, column);
}
private Component getTableCellComponent(JTable table, Object value, boolean isSelected,
int row, int column) {
if (value instanceof TorrentFileEntryWrapper) {
currentWrapper = (TorrentFileEntryWrapper) value;
if (torrent.isFinished()) {
setEnabled(false);
setSelected(currentWrapper.getProgress() == 1.0f
&& currentWrapper.getPriority() > DONT_DOWNLOAD);
} else if (currentWrapper.getProgress() == 1.0f) {
setSelected(true);
setEnabled(false);
} else {
setSelected(((TorrentFileEntryWrapper) value).getPriority() != DONT_DOWNLOAD);
setEnabled(!torrent.isFinished());
}
} else {
currentWrapper = null;
setSelected(false);
}
return this;
}
@Override
public void addCellEditorListener(CellEditorListener l) {
if (!listenerList.contains(l)) {
listenerList.add(l);
}
}
@Override
public void cancelCellEditing() {
ChangeEvent event = new ChangeEvent(this);
for (int i = 0, size = listenerList.size(); i < size; i++) {
listenerList.get(i).editingCanceled(event);
}
}
@Override
public TorrentFileEntryWrapper getCellEditorValue() {
return currentWrapper;
}
@Override
public boolean isCellEditable(EventObject anEvent) {
return true;
}
@Override
public void removeCellEditorListener(CellEditorListener l) {
listenerList.remove(l);
}
@Override
public boolean shouldSelectCell(EventObject anEvent) {
return true;
}
@Override
public boolean stopCellEditing() {
cancelCellEditing();
return true;
}
}
private class PercentRenderer extends DefaultLimeTableCellRenderer {
@Override
public Component getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column) {
super.getTableCellRendererComponent(table, value, isSelected, false, row, column);
if (value instanceof TorrentFileEntryWrapper) {
float percent = ((TorrentFileEntryWrapper) value).getProgress();
if (torrent.isFinished()) {
if (percent == 1.0f
&& ((TorrentFileEntryWrapper) value).getPriority() > DONT_DOWNLOAD) {
setText(I18n.tr("Done"));
} else {
setText("");
}
} else if (((TorrentFileEntryWrapper) value).getPriority() == DONT_DOWNLOAD) {
setText("");
} else {
setText((int) (percent * 100) + "%");
}
} else {
setText("");
}
return this;
}
}
private class PriorityRendererEditor extends JPanel implements TableCellRenderer,
TableCellEditor {
/** List of cell editor listeners. */
private final List<CellEditorListener> listenerList = new ArrayList<CellEditorListener>();
@Resource
private Icon lowestPriorityIcon;
@Resource
private Icon normalPriorityIcon;
@Resource
private Icon highestPriorityIcon;
@Resource
private Font textFont;
@Resource
private Color fontColor;
private final JButton button;
private TorrentFileEntryWrapper currentWrapper;
public PriorityRendererEditor() {
super(new MigLayout("align 50%"));
GuiUtils.assignResources(this);
button = new JButton();
button.setBorderPainted(false);
button.setContentAreaFilled(false);
button.setFont(textFont);
button.setFocusPainted(false);
button.setForeground(fontColor);
FontUtils.underline(button);
add(button);
}
public JButton getButton() {
return button;
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column) {
return getTableCellComponent(table, value, isSelected, row, column);
}
@Override
public Component getTableCellEditorComponent(JTable table, Object value,
boolean isSelected, int row, int column) {
return getTableCellComponent(table, value, isSelected, row, column);
}
private Component getTableCellComponent(JTable table, Object value, boolean isSelected,
int row, int column) {
if (value instanceof TorrentFileEntryWrapper) {
currentWrapper = (TorrentFileEntryWrapper) value;
int priority = ((TorrentFileEntryWrapper) value).getPriority();
if (((TorrentFileEntryWrapper) value).getProgress() == 1.0f) {
button.setIcon(null);
button.setText("");
// button.setText(I18n.tr("delete"));
} else if (priority == DONT_DOWNLOAD) {
button.setIcon(null);
button.setText("");
} else if (priority == LOWEST_PRIORITY) {
button.setIcon(lowestPriorityIcon);
button.setText("");
} else if (priority == NORMAL_PRIORITY) {
button.setIcon(normalPriorityIcon);
button.setText("");
} else if (priority == HIGHEST_PRIORITY) {
button.setIcon(highestPriorityIcon);
button.setText("");
}
} else {
currentWrapper = null;
button.setIcon(null);
button.setText("");
}
return this;
}
@Override
public void addCellEditorListener(CellEditorListener l) {
if (!listenerList.contains(l)) {
listenerList.add(l);
}
}
@Override
public void cancelCellEditing() {
ChangeEvent event = new ChangeEvent(this);
for (int i = 0, size = listenerList.size(); i < size; i++) {
listenerList.get(i).editingCanceled(event);
}
}
@Override
public TorrentFileEntryWrapper getCellEditorValue() {
return currentWrapper;
}
@Override
public boolean isCellEditable(EventObject anEvent) {
return true;
}
@Override
public void removeCellEditorListener(CellEditorListener l) {
listenerList.remove(l);
}
@Override
public boolean shouldSelectCell(EventObject anEvent) {
return true;
}
@Override
public boolean stopCellEditing() {
cancelCellEditing();
return true;
}
}
public void addPropertyChangeListener( PropertyChangeListener listener )
{
support.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener( PropertyChangeListener listener )
{
support.removePropertyChangeListener(listener);
}
@Override
public void handleEvent(TorrentEvent event) {
if (event.getType() == TorrentEventType.STATUS_CHANGED || event.getType() == TorrentEventType.COMPLETED) {
SwingUtils.invokeNowOrLater(new Runnable() {
@Override
public void run() {
List<TorrentFileEntry> fileEntries = torrent.getTorrentFileEntries();
for (TorrentFileEntry newEntry : fileEntries) {
TorrentFileEntryWrapper wrapper = eventList.get(newEntry.getIndex());
wrapper.setTorrentFileEntry(newEntry);
}
}
});
}
}
private final class SelectNone extends AbstractAction {
private SelectNone(String name) {
super(name);
}
@Override
public void actionPerformed(ActionEvent e) {
torrent.getLock().lock();
try {
if(!torrent.isFinished()) {
for (TorrentFileEntryWrapper wrapper : eventList) {
if(!(wrapper.getProgress() == 1.0f)) {
wrapper.setPriority(DONT_DOWNLOAD);
}
}
}
} finally {
torrent.getLock().unlock();
}
table.repaint();
table.validateSelection();
}
}
private final class SelectAll extends AbstractAction {
private SelectAll(String name) {
super(name);
}
@Override
public void actionPerformed(ActionEvent e) {
torrent.getLock().lock();
try {
if(!torrent.isFinished()) {
for (TorrentFileEntryWrapper wrapper : eventList) {
if(wrapper.getPriority() == DONT_DOWNLOAD) {
wrapper.setPriority(LOWEST_PRIORITY);
}
}
}
} finally {
torrent.getLock().unlock();
}
table.repaint();
table.validateSelection();
}
}
}