package org.limewire.ui.swing.advanced.connection; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Point; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.awt.event.MouseEvent; import java.awt.event.MouseMotionAdapter; import java.util.Arrays; import javax.swing.BorderFactory; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JTable; import javax.swing.JToolTip; import javax.swing.event.HyperlinkEvent; import javax.swing.event.HyperlinkListener; import javax.swing.event.HyperlinkEvent.EventType; import javax.swing.table.AbstractTableModel; import javax.swing.table.DefaultTableCellRenderer; import net.miginfocom.swing.MigLayout; import org.limewire.collection.glazedlists.GlazedListsFactory; import org.limewire.core.api.connection.ConnectionItem; import org.limewire.core.api.connection.FWTStatusReason; import org.limewire.core.api.connection.FirewallStatus; import org.limewire.core.api.connection.FirewallStatusEvent; import org.limewire.core.api.connection.FirewallTransferStatus; import org.limewire.core.api.connection.FirewallTransferStatusEvent; import org.limewire.core.api.connection.GnutellaConnectionManager; import org.limewire.listener.EventBean; import org.limewire.ui.swing.advanced.connection.PopupManager.PopupProvider; import org.limewire.ui.swing.components.HTMLLabel; import org.limewire.ui.swing.settings.SwingUiSettings; import org.limewire.ui.swing.util.I18n; import ca.odell.glazedlists.EventList; import ca.odell.glazedlists.TransformedList; import com.google.inject.Inject; /** * Display panel for the connection summary. */ public class ConnectionSummaryPanel extends JPanel { private static final String IS_ULTRAPEER = I18n.tr("You are an Ultrapeer node"); private static final String IS_LEAF = I18n.tr("You are a Leaf node"); private static final String IS_NOT_FIREWALLED = I18n.tr("You are not behind a firewall"); private static final String IS_FIREWALLED_TRANSFERS = I18n.tr("You are behind a firewall and support firewall transfers"); private static final String IS_FIREWALLED_NO_TRANSFERS = I18n.tr("You are behind a firewall and do not support firewall transfers"); private static final String WHY = I18n.tr("why"); private static final String CONNECTED_TO = I18n.tr("Connected to:"); private static final String RESOLVE = I18n.tr("Show hostnames of connected peers"); private static final String CONNECTING = I18n.tr("Connecting"); private static final String LEAVES = I18n.tr("Leaves"); private static final String PEERS = I18n.tr("Peers"); private static final String STANDARD = I18n.tr("Standard"); private static final String ULTRAPEERS = I18n.tr("Ultrapeers"); /** Manager instance for connection data. */ private final GnutellaConnectionManager gnutellaConnectionManager; /** Bean instance for firewall status. */ private final EventBean<FirewallStatusEvent> firewallStatusBean; /** Bean instance for firewall transfer status. */ private final EventBean<FirewallTransferStatusEvent> firewallTransferBean; /** List of connections. */ private TransformedList<ConnectionItem, ConnectionItem> connectionList; /** Popup manager for transfer status reason. */ private final PopupManager reasonPopupManager; private JLabel nodeLabel = new JLabel(); private FirewallLabel firewallLabel = new FirewallLabel(); private JLabel summaryLabel = new JLabel(); private JTable summaryTable = new JTable(); private SummaryTableModel summaryTableModel = new SummaryTableModel(); private JCheckBox resolveCheckBox = new JCheckBox(RESOLVE); /** * Constructs the ConnectionDetailPanel to display connections details. */ @Inject public ConnectionSummaryPanel(GnutellaConnectionManager gnutellaConnectionManager, EventBean<FirewallStatusEvent> firewallStatusBean, EventBean<FirewallTransferStatusEvent> firewallTransferBean) { this.gnutellaConnectionManager = gnutellaConnectionManager; this.firewallStatusBean = firewallStatusBean; this.firewallTransferBean = firewallTransferBean; this.reasonPopupManager = new PopupManager(firewallLabel); setBorder(BorderFactory.createTitledBorder("")); setLayout(new MigLayout("insets 0 0 0 0,fill", "[left]", // col constraints "[top][top][bottom][top,fill]")); // row constraints setPreferredSize(new Dimension(120, 120)); setOpaque(false); firewallLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0)); firewallLabel.setOpaque(false); firewallLabel.setOpenUrlsNatively(false); firewallLabel.addHyperlinkListener(new HyperlinkListener() { @Override public void hyperlinkUpdate(HyperlinkEvent e) { if (e.getEventType() == EventType.ACTIVATED) { Point location = firewallLabel.getPopupLocation(); reasonPopupManager.showTimedPopup(firewallLabel, location.x + 18, location.y + 10); } } }); summaryLabel.setText(CONNECTED_TO); summaryTable.setModel(summaryTableModel); summaryTable.setSelectionForeground(Color.BLACK); summaryTable.setPreferredSize(new Dimension(120, 120)); summaryTable.setShowGrid(false); summaryTable.setFocusable(false); // Set column widths. summaryTable.getColumnModel().getColumn(0).setPreferredWidth(24); summaryTable.getColumnModel().getColumn(1).setPreferredWidth(96); // Install renderer to align summary value. summaryTable.getColumnModel().getColumn(0).setCellRenderer(new SummaryCellRenderer()); resolveCheckBox.setContentAreaFilled(false); boolean resolve = SwingUiSettings.RESOLVE_CONNECTION_HOSTNAMES.getValue(); resolveCheckBox.setSelected(resolve); resolveCheckBox.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { boolean resolve = resolveCheckBox.isSelected(); SwingUiSettings.RESOLVE_CONNECTION_HOSTNAMES.setValue(resolve); } }); add(nodeLabel , "cell 0 0"); add(firewallLabel , "cell 0 1, growx 100"); add(summaryLabel , "cell 0 2"); add(summaryTable , "cell 0 3"); add(resolveCheckBox, "cell 0 4"); } @Override public void setBackground(Color bgColor) { super.setBackground(bgColor); if (summaryTable != null) { summaryTable.setBackground(bgColor); summaryTable.setSelectionBackground(bgColor); } } /** * Initializes the data models in the container. */ public void initData() { if (connectionList == null) { // Create connection list for Swing. We wrap the actual list in a // Swing list to ensure that all events are fired on the UI thread. connectionList = GlazedListsFactory.swingThreadProxyEventList( gnutellaConnectionManager.getConnectionList()); // Set node description. boolean ultrapeer = gnutellaConnectionManager.isUltrapeer(); nodeLabel.setText(ultrapeer ? IS_ULTRAPEER : IS_LEAF); // Set firewall status. updateFirewallStatus(); } } /** * Clears the data models in the container. */ public void clearData() { if (connectionList != null) { connectionList.dispose(); connectionList = null; } } /** * Triggers a refresh of the data being displayed. */ public void refresh() { updateFirewallStatus(); summaryTableModel.update(connectionList); } /** * Updates the firewall status fields. */ private void updateFirewallStatus() { // Get firewall status. FirewallStatus firewallStatus = firewallStatusBean.getLastEvent().getData(); if (firewallStatus == FirewallStatus.FIREWALLED) { // Get firewall transfer status and reason. FirewallTransferStatusEvent event = firewallTransferBean.getLastEvent(); FirewallTransferStatus transferStatus = event.getData(); FWTStatusReason transferReason = event.getType(); // Set firewall status and reason. if (transferStatus == FirewallTransferStatus.DOES_NOT_SUPPORT_FWT) { firewallLabel.setStatusText(IS_FIREWALLED_NO_TRANSFERS, getReasonText(transferReason)); } else { firewallLabel.setStatusText(IS_FIREWALLED_TRANSFERS, null); } } else { // Not firewalled so clear transfer status and reason. firewallLabel.setStatusText(IS_NOT_FIREWALLED, null); } } /** * Returns the display text for the specified firewall transfer status * reason. */ private String getReasonText(FWTStatusReason reason) { switch (reason) { case INVALID_EXTERNAL_ADDRESS: return I18n.tr("LimeWire has not been able to determine the external IP address of your NAT or firewall"); case NO_SOLICITED_INCOMING_MESSAGES: return I18n.tr("LimeWire has not received any incoming UDP messages"); case REUSING_STATUS_FROM_PREVIOUS_SESSION: return I18n.tr("LimeWire was not able to support firewall transfers in a previous session"); case PORT_UNSTABLE: return I18n.tr("LimeWire is behind a NAT or firewall that assigns a different external port to each connection"); case UNKNOWN: default: return I18n.tr("Unknown"); } } /** * Label to display firewall transfer status and reason. FirewallLabel * displays a hyperlink when the reason is not blank; the reason is * displayed in a popup window when the link is clicked. */ private class FirewallLabel extends HTMLLabel implements PopupProvider { private Point popupLocation; private String reasonText; public FirewallLabel() { addMouseMotionListener(new MouseMotionAdapter() { @Override public void mouseMoved(MouseEvent e) { popupLocation = e.getPoint(); } }); } @Override public Component getPopupContent() { if ((reasonText != null) && (reasonText.length() > 0)) { // Return tooltip component for popup. JToolTip toolTip = createToolTip(); toolTip.setTipText(reasonText); return toolTip; } else { return null; } } public Point getPopupLocation() { return popupLocation; } public void setStatusText(String statusText, String reasonText) { this.reasonText = reasonText; if ((reasonText != null) && (reasonText.length() > 0)) { StringBuilder builder = new StringBuilder(); builder.append(statusText); builder.append(" (<a href=\"#\">").append(WHY).append("</a>)"); setText(builder.toString()); } else { setText(statusText); } } } /** * Table cell renderer for connection count values in the summary table. * Values are right-aligned with a right margin. */ private static class SummaryCellRenderer extends DefaultTableCellRenderer { @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { Component renderer = super.getTableCellRendererComponent( table, value, isSelected, hasFocus, row, column); if (renderer instanceof JLabel) { ((JLabel) renderer).setHorizontalAlignment(JLabel.RIGHT); ((JLabel) renderer).setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 3)); } return renderer; } } /** * Table model for the connection summary table. */ private static class SummaryTableModel extends AbstractTableModel { /** A 5-element array containing the number of connections with the * following states: connecting, ultrapeer, peer, leaf, standard. */ private int[] connectCounts = new int[5]; @Override public int getColumnCount() { return 2; } @Override public int getRowCount() { return 5; } @Override public Object getValueAt(int rowIndex, int columnIndex) { if (columnIndex == 0) { // Return summary counts. This maps the table rows to the // elements in the connectCounts array. Note that table row 3 // for 'connecting' maps to array index 0. switch (rowIndex) { case 0: return connectCounts[1]; case 1: return connectCounts[2]; case 2: return connectCounts[3]; case 3: return connectCounts[0]; case 4: return connectCounts[4]; default: return null; } } else if (columnIndex == 1) { // Return labels. switch (rowIndex) { case 0: return ULTRAPEERS; case 1: return PEERS; case 2: return LEAVES; case 3: return CONNECTING; case 4: return STANDARD; default: return null; } } return null; } /** * Updates the data model using the specified connection list. */ public void update(EventList<ConnectionItem> connectionList) { // The data model is a 5-element array containing the number of // connections with the following states: connecting, ultrapeer, // peer, leaf, standard. Arrays.fill(connectCounts, 0); // Update connection counts. for (int i = 0; i < connectionList.size(); i++) { ConnectionItem item = connectionList.get(i); if (!item.isConnected()) { connectCounts[0]++; } else if (item.isUltrapeer()) { connectCounts[1]++; } else if (item.isPeer()) { connectCounts[2]++; } else if (item.isLeaf()) { connectCounts[3]++; } else { connectCounts[4]++; } } // Fire event to update table. fireTableDataChanged(); } } }