package org.limewire.ui.swing.search.resultpanel.list;
import static org.limewire.ui.swing.search.resultpanel.list.ListViewRowHeightRule.RowDisplayConfig.HeadingSubHeadingAndMetadata;
import static org.limewire.ui.swing.util.I18n.tr;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
import java.util.EventObject;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.AbstractCellEditor;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JEditorPane;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
import javax.swing.event.HyperlinkEvent.EventType;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.text.html.HTMLDocument;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.StyleSheet;
import net.miginfocom.swing.MigLayout;
import org.jdesktop.application.Resource;
import org.jdesktop.swingx.JXPanel;
import org.limewire.logging.Log;
import org.limewire.logging.LogFactory;
import org.limewire.ui.swing.components.HTMLLabel;
import org.limewire.ui.swing.components.IconButton;
import org.limewire.ui.swing.components.RemoteHostWidget;
import org.limewire.ui.swing.components.RemoteHostWidgetFactory;
import org.limewire.ui.swing.components.RemoteHostWidget.RemoteWidgetType;
import org.limewire.ui.swing.downloads.MainDownloadPanel;
import org.limewire.ui.swing.library.LibraryMediator;
import org.limewire.ui.swing.listener.MousePopupListener;
import org.limewire.ui.swing.nav.Navigator;
import org.limewire.ui.swing.properties.FileInfoDialogFactory;
import org.limewire.ui.swing.properties.FileInfoDialog.FileInfoType;
import org.limewire.ui.swing.search.model.BasicDownloadState;
import org.limewire.ui.swing.search.model.VisualSearchResult;
import org.limewire.ui.swing.search.resultpanel.DownloadHandler;
import org.limewire.ui.swing.search.resultpanel.ResultsTable;
import org.limewire.ui.swing.search.resultpanel.SearchHeading;
import org.limewire.ui.swing.search.resultpanel.SearchHeadingDocumentBuilder;
import org.limewire.ui.swing.search.resultpanel.SearchResultMenu;
import org.limewire.ui.swing.search.resultpanel.SearchResultMenuFactory;
import org.limewire.ui.swing.search.resultpanel.SearchResultTruncator;
import org.limewire.ui.swing.search.resultpanel.SearchResultTruncator.FontWidthResolver;
import org.limewire.ui.swing.search.resultpanel.list.ListViewRowHeightRule.PropertyMatch;
import org.limewire.ui.swing.search.resultpanel.list.ListViewRowHeightRule.RowDisplayConfig;
import org.limewire.ui.swing.search.resultpanel.list.ListViewRowHeightRule.RowDisplayResult;
import org.limewire.ui.swing.table.TransparentCellTableRenderer;
import org.limewire.ui.swing.util.CategoryIconManager;
import org.limewire.ui.swing.util.GuiUtils;
import org.limewire.ui.swing.util.I18n;
import ca.odell.glazedlists.swing.DefaultEventTableModel;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
/**
* This class is responsible for rendering an individual SearchResult
* in "List View".
*/
public class ListViewTableEditorRenderer extends AbstractCellEditor implements TableCellEditor, TableCellRenderer {
private static final Log LOG = LogFactory.getLog(ListViewTableEditorRenderer.class);
private static final int LEFT_COLUMN_WIDTH = 450;
private final CategoryIconManager categoryIconManager;
private static final String HTML = "<html>";
private static final String CLOSING_HTML_TAG = "</html>";
@Resource private Icon similarResultsIcon;
@Resource private Color subHeadingLabelColor;
@Resource private Color metadataLabelColor;
@Resource private Color downloadSourceCountColor;
@Resource private Color similarResultsBackgroundColor;
@Resource private Color surplusRowLimitColor;
@Resource private String headingColor;
@Resource private Font headingFont;
@Resource private Font subHeadingFont;
@Resource private Font metadataFont;
@Resource private Font downloadSourceCountFont;
@Resource private Font surplusRowLimitFont;
@Resource private Icon spamIcon;
@Resource private Icon downloadingIcon;
@Resource private Icon libraryIcon;
@Resource private Icon propertiesPressedIcon;
@Resource private Icon propertiesHoverIcon;
@Resource private Icon propertiesIcon;
@Resource private Icon propertiesSimilarShownPressedIcon;
@Resource private Icon propertiesSimilarShownHoverIcon;
@Resource private Icon propertiesSimilarShownIcon;
@Resource private Icon similarHiddenIcon;
@Resource private Icon similarHiddenPressedIcon;
@Resource private Icon similarHiddenHoverIcon;
@Resource private Icon similarShownIcon;
@Resource private Icon similarShownPressedIcon;
@Resource private Icon similarShownHoverIcon;
@Resource private Icon dividerIcon;
private final Provider<SearchHeadingDocumentBuilder> headingBuilder;
private final ListViewRowHeightRule rowHeightRule;
private final ListViewDisplayedRowsLimit displayLimit;
private final Provider<SearchResultTruncator> truncator;
private final HeadingFontWidthResolver headingFontWidthResolver = new HeadingFontWidthResolver();
private final FileInfoDialogFactory fileInfoFactory;
private RemoteHostWidget fromWidget;
private JButton itemIconButton;
private IconButton similarButton = new IconButton();
private IconButton propertiesButton = new IconButton();
private JEditorPane heading = new JEditorPane();
private JLabel subheadingLabel = new NoDancingHtmlLabel();
private JLabel metadataLabel = new NoDancingHtmlLabel();
private JLabel downloadSourceCount = new TransparentCellTableRenderer();
private JXPanel editorComponent;
private VisualSearchResult vsr;
private JTable table;
private JComponent similarResultIndentation;
private JPanel lastRowPanel;
private final JPanel emptyPanel = new JPanel();
private JXPanel searchResultTextPanel;
private JLabel lastRowMessage;
private DownloadHandler downloadHandler;
/**
* cached width used for text truncation
*/
private int textPanelWidth;
private final SearchResultMenuFactory searchResultMenuFactory;
private final MainDownloadPanel mainDownloadPanel;
@Inject
ListViewTableEditorRenderer(
CategoryIconManager categoryIconManager,
RemoteHostWidgetFactory fromWidgetFactory,
Navigator navigator,
final @Assisted DownloadHandler downloadHandler,
Provider<SearchHeadingDocumentBuilder> headingBuilder,
@Assisted ListViewRowHeightRule rowHeightRule,
final @Assisted ListViewDisplayedRowsLimit displayLimit,
LibraryMediator libraryMediator,
Provider<SearchResultTruncator> truncator, FileInfoDialogFactory fileInfoFactory,
SearchResultMenuFactory searchResultMenuFactory,
MainDownloadPanel mainDownloadPanel) {
this.categoryIconManager = categoryIconManager;
this.headingBuilder = headingBuilder;
this.rowHeightRule = rowHeightRule;
this.displayLimit = displayLimit;
this.truncator = truncator;
this.downloadHandler = downloadHandler;
this.fileInfoFactory = fileInfoFactory;
this.searchResultMenuFactory = searchResultMenuFactory;
this.mainDownloadPanel = mainDownloadPanel;
GuiUtils.assignResources(this);
fromWidget = fromWidgetFactory.create(RemoteWidgetType.SEARCH_LIST);
makePanel(navigator, libraryMediator);
setupButtons();
layoutEditorComponent();
}
private void makePanel(Navigator navigator, LibraryMediator libraryMediator) {
initializeComponents();
makeIndentation();
setupListeners(navigator, libraryMediator);
lastRowPanel = new JPanel(new MigLayout("insets 10 30 0 0", "[]", "[]"));
lastRowPanel.setOpaque(false);
lastRowMessage = new JLabel();
lastRowMessage.setFont(surplusRowLimitFont);
lastRowMessage.setForeground(surplusRowLimitColor);
lastRowPanel.add(lastRowMessage);
emptyPanel.setOpaque(false);
}
private void setupButtons(){
similarButton.setFocusable(false);
similarButton.setIcon(similarHiddenIcon);
similarButton.setPressedIcon(similarHiddenPressedIcon);
similarButton.setRolloverIcon(similarHiddenHoverIcon);
similarButton.setToolTipText(I18n.tr("Show Similar Files"));
propertiesButton.setFocusable(false);
propertiesButton.setIcon(propertiesIcon);
propertiesButton.setPressedIcon(propertiesPressedIcon);
propertiesButton.setRolloverIcon(propertiesHoverIcon);
propertiesButton.setToolTipText(I18n.tr("View File Info"));
itemIconButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (isDownloadEligible(vsr)) {
downloadHandler.download(vsr);
table.editingStopped(new ChangeEvent(table));
}
}
});
similarButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
vsr.toggleChildrenVisibility();
if(vsr.isChildrenVisible()) {
similarButton.setIcon(similarShownIcon);
similarButton.setPressedIcon(similarShownPressedIcon);
similarButton.setRolloverIcon(similarShownHoverIcon);
similarButton.setToolTipText(I18n.tr("Hide Similar Files"));
propertiesButton.setIcon(propertiesSimilarShownIcon);
propertiesButton.setPressedIcon(propertiesSimilarShownPressedIcon);
propertiesButton.setRolloverIcon(propertiesSimilarShownHoverIcon);
} else {
similarButton.setIcon(similarHiddenIcon);
similarButton.setPressedIcon(similarHiddenPressedIcon);
similarButton.setRolloverIcon(similarHiddenHoverIcon);
similarButton.setToolTipText(I18n.tr("Show Similar Files"));
propertiesButton.setIcon(propertiesIcon);
propertiesButton.setPressedIcon(propertiesPressedIcon);
propertiesButton.setRolloverIcon(propertiesHoverIcon);
}
table.editingStopped(new ChangeEvent(table));
}
});
propertiesButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
fileInfoFactory.createFileInfoDialog(vsr, FileInfoType.REMOTE_FILE);
}
});
}
private void layoutEditorComponent(){
searchResultTextPanel.setLayout(new MigLayout("nogrid, ins 0 0 0 0, gap 0! 0!, novisualpadding"));
searchResultTextPanel.setOpaque(false);
searchResultTextPanel.add(heading, "left, shrinkprio 200, growx, wmax pref, hidemode 3, wrap");
searchResultTextPanel.add(subheadingLabel, "left, shrinkprio 200, growx, hidemode 3, wrap");
searchResultTextPanel.add(metadataLabel, "left, shrinkprio 200, hidemode 3");
editorComponent.setLayout(new MigLayout("ins 0 0 0 0, gap 0! 0!, novisualpadding"));
editorComponent.add(similarResultIndentation, "growy, hidemode 3, shrinkprio 0");
editorComponent.add(itemIconButton, "left, aligny 50%, gapleft 4, shrinkprio 0");
editorComponent.add(searchResultTextPanel, "left, , aligny 50%, gapleft 4, growx, shrinkprio 200, growprio 200, push");
editorComponent.add(downloadSourceCount, "gapbottom 3, gapright 2, shrinkprio 0");
editorComponent.add(new JLabel(dividerIcon), "shrinkprio 0");
//TODO: better number for wmin
editorComponent.add(fromWidget, "wmin 90, left, shrinkprio 0");
//Tweaked the width of the icon because display gets clipped otherwise
editorComponent.add(similarButton, "gapright 4, hidemode 0, hmax 25, wmax 27, shrinkprio 0");
editorComponent.add(propertiesButton, "gapright 4, hmax 25, wmax 27, shrinkprio 0");
}
@Override
public Component getTableCellRendererComponent(
JTable table, Object value,
boolean isSelected, boolean hasFocus,
int row, int column) {
return getTableCellEditorComponent(
table, value, isSelected, row, column);
}
@Override
public Component getTableCellEditorComponent(
final JTable table, Object value, boolean isSelected, int row, final int col) {
vsr = (VisualSearchResult) value;
this.table = table;
editorComponent.setBackground(table.getBackground());
if (value == null) {
return emptyPanel;
}
LOG.debugf("row: {0} shouldIndent: {1}", row, vsr.getSimilarityParent() != null);
if (row == displayLimit.getLastDisplayedRow()) {
lastRowMessage.setText(tr("Not showing {0} results. Narrow results to see more.",
(displayLimit.getTotalResultsReturned() - displayLimit.getLastDisplayedRow())));
return lastRowPanel;
}
update(vsr);
return editorComponent;
}
private void update(VisualSearchResult vsr) {
fromWidget.setPeople(vsr.getSources());
propertiesButton.setIcon(vsr.isChildrenVisible() ? propertiesSimilarShownIcon : propertiesIcon);
similarButton.setVisible(vsr.getSimilarResults().size() > 0);
similarButton.setIcon(vsr.isChildrenVisible() ? similarShownIcon : similarHiddenIcon);
similarResultIndentation.setVisible(vsr.getSimilarityParent() != null);
itemIconButton.setIcon(getIcon(vsr));
itemIconButton.setCursor(getIconCursor(vsr));
RowDisplayResult result = rowHeightRule.getDisplayResult(vsr);
setLabelVisibility(result.getConfig());
populateHeading(result, vsr.getDownloadState());
populateSubheading(result);
populateMetadata(result);
}
private Icon getIcon(VisualSearchResult vsr) {
if (vsr.isSpam()) {
return spamIcon;
}
switch (vsr.getDownloadState()) {
case DOWNLOADING:
return downloadingIcon;
case DOWNLOADED:
case LIBRARY:
return libraryIcon;
}
return categoryIconManager.getIcon(vsr);
}
private Cursor getIconCursor(VisualSearchResult vsr) {
boolean useDefaultCursor = !isDownloadEligible(vsr);
return Cursor.getPredefinedCursor(useDefaultCursor ? Cursor.DEFAULT_CURSOR : Cursor.HAND_CURSOR);
}
private void setLabelVisibility(RowDisplayConfig config) {
switch(config) {
case HeadingOnly:
subheadingLabel.setVisible(false);
metadataLabel.setVisible(false);
break;
case HeadingAndSubheading:
subheadingLabel.setVisible(true);
metadataLabel.setVisible(false);
break;
case HeadingAndMetadata:
subheadingLabel.setVisible(false);
metadataLabel.setVisible(true);
break;
case HeadingSubHeadingAndMetadata:
subheadingLabel.setVisible(true);
metadataLabel.setVisible(true);
}
}
private void populateHeading(final RowDisplayResult result, BasicDownloadState downloadState) {
//the visible rect width is always 0 for renderers so getVisibleRect() won't work here
//Width is zero the first time editorpane is rendered - use a wide default (roughly width of left column)
int width = textPanelWidth == 0 ? LEFT_COLUMN_WIDTH : textPanelWidth;
//Make the width seem a little smaller than usual to trigger a more hungry truncation
//otherwise the JEditorPane word-wrapping logic kicks in and the edge word just disappears
final int fudgeFactorPixelWidth = width - 10;
SearchHeading searchHeading = new SearchHeading() {
@Override
public String getText() {
String headingText = result.getHeading();
String truncatedHeading = truncator.get().truncateHeading(headingText, fudgeFactorPixelWidth, headingFontWidthResolver);
handleHeadingTooltip(headingText, truncatedHeading);
return truncatedHeading;
}
/**
* Sets a tooltip for the heading field only if the text has been truncated.
*/
private void handleHeadingTooltip(String headingText, String truncatedHeading) {
String toolTipText = HTML + headingText + CLOSING_HTML_TAG;
editorComponent.setToolTipText(toolTipText);
heading.setToolTipText(toolTipText);
}
@Override
public String getText(String adjoiningFragment) {
int adjoiningTextPixelWidth = headingFontWidthResolver.getPixelWidth(adjoiningFragment);
String headingText = result.getHeading();
String truncatedHeading = truncator.get().truncateHeading(headingText,
fudgeFactorPixelWidth - adjoiningTextPixelWidth, headingFontWidthResolver);
handleHeadingTooltip(headingText, truncatedHeading);
return truncatedHeading;
}
};
this.heading.setText(headingBuilder.get().getHeadingDocument(searchHeading, downloadState, result.isSpam()));
this.downloadSourceCount.setText(Integer.toString(vsr.getSources().size()));
}
private void populateSubheading(RowDisplayResult result) {
subheadingLabel.setText(result.getSubheading());
}
private void populateMetadata(RowDisplayResult result) {
metadataLabel.setText(null);
RowDisplayConfig config = result.getConfig();
if (config != HeadingSubHeadingAndMetadata && config != RowDisplayConfig.HeadingAndMetadata) {
return;
}
PropertyMatch pm = result.getMetadata();
if (pm != null) {
String html = pm.getHighlightedValue();
// Insert the following: the key, a colon and a space after the html start tag, then the closing tags.
html = html.replace(HTML, "").replace(CLOSING_HTML_TAG, "");
html = HTML + pm.getKey() + ":" + html + CLOSING_HTML_TAG;
metadataLabel.setText(html);
}
}
private boolean isDownloadEligible(VisualSearchResult vsr) {
return !vsr.isSpam() && vsr.getDownloadState() == BasicDownloadState.NOT_STARTED;
}
private void initializeComponents() {
searchResultTextPanel = new JXPanel(){
@Override
public void paint(Graphics g){
super.paint(g);
//the visible rect width is always 0 for renderers so we cache the width here
textPanelWidth = getSize().width;
}
};
editorComponent = new JXPanel();
itemIconButton = new IconButton();
heading.setContentType("text/html");
heading.setEditable(false);
heading.setCaretPosition(0);
heading.setSelectionColor(HTMLLabel.TRANSPARENT_COLOR);
heading.setOpaque(false);
heading.setFocusable(false);
StyleSheet mainStyle = ((HTMLDocument)heading.getDocument()).getStyleSheet();
String rules = "body { font-family: " + headingFont.getFamily() + "; }" +
".title { color: " + headingColor + "; font-size: " + headingFont.getSize() + "; }" +
"a { color: " + headingColor + "; }";
StyleSheet newStyle = new StyleSheet();
newStyle.addRule(rules);
mainStyle.addStyleSheet(newStyle);
heading.setMaximumSize(new Dimension(Integer.MAX_VALUE, 22));
subheadingLabel.setForeground(subHeadingLabelColor);
subheadingLabel.setFont(subHeadingFont);
metadataLabel.setForeground(metadataLabelColor);
metadataLabel.setFont(metadataFont);
downloadSourceCount.setForeground(downloadSourceCountColor);
downloadSourceCount.setFont(downloadSourceCountFont);
}
private void makeIndentation() {
similarResultIndentation = new JPanel(new BorderLayout());
similarResultIndentation.add(new JLabel(similarResultsIcon), BorderLayout.CENTER);
similarResultIndentation.setBackground(similarResultsBackgroundColor);
}
private void setupListeners(final Navigator navigator, final LibraryMediator libraryMediator) {
heading.addHyperlinkListener(new HyperlinkListener() {
@Override
public void hyperlinkUpdate(HyperlinkEvent e) {
if (EventType.ACTIVATED == e.getEventType()) {
if (e.getDescription().equals("#download")) {
downloadHandler.download(vsr);
table.editingStopped(new ChangeEvent(table));
} else if (e.getDescription().equals("#downloading")) {
mainDownloadPanel.selectAndScrollTo(vsr.getUrn());
} else if (e.getDescription().equals("#library")) {
libraryMediator.selectInLibrary(vsr.getUrn());
}
}
}
});
Component[] listenerComponents = new Component[]{editorComponent, heading, subheadingLabel, metadataLabel,
similarResultIndentation, searchResultTextPanel, downloadSourceCount, itemIconButton};
MousePopupListener popupListener = new MousePopupListener() {
@Override
public void handlePopupMouseEvent(MouseEvent e) {
// Update selection if mouse is not in selected row.
if (table.isEditing()) {
int editRow = table.getEditingRow();
if (!table.isRowSelected(editRow)) {
updateSelection(e);
}
}
// Create list of selected results.
List<VisualSearchResult> selectedResults = new ArrayList<VisualSearchResult>();
DefaultEventTableModel model = ((ResultsTable) table).getEventTableModel();
int[] selectedRows = table.getSelectedRows();
for (int row : selectedRows) {
Object element = model.getElementAt(row);
if (element instanceof VisualSearchResult) {
selectedResults.add((VisualSearchResult) element);
}
}
// If nothing selected, use current result.
if (selectedResults.size() == 0) {
selectedResults.add(vsr);
}
// Display context menu.
SearchResultMenu searchResultMenu = searchResultMenuFactory.create(downloadHandler, selectedResults, SearchResultMenu.ViewType.List);
searchResultMenu.show(e.getComponent(), e.getX()+3, e.getY()+3);
}
};
addMouseListener(popupListener, listenerComponents);
// Add listener to update table selection and start downloads.
MouseListener selectionDownloadAdapter = new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
if (SwingUtilities.isLeftMouseButton(e)) {
updateSelection(e);
}
}
@Override
public void mouseClicked(MouseEvent e) {
if (vsr != null && !vsr.isSpam() && e.getClickCount() == 2 &&
SwingUtilities.isLeftMouseButton(e)) {
downloadHandler.download(vsr);
}
}
};
addMouseListener(selectionDownloadAdapter, listenerComponents);
}
private void addMouseListener(MouseListener listener, Component... components) {
for (Component c : components){
c.addMouseListener(listener);
}
}
@Override
public boolean shouldSelectCell(EventObject anEvent) {
return false;
}
@Override
public Object getCellEditorValue() {
return vsr;
}
/**
* Updates the table row selection based on the specified mouse event.
*/
private void updateSelection(MouseEvent e) {
if (table.isEditing()) {
// Get cell being edited by this editor.
int editRow = table.getEditingRow();
int editCol = table.getEditingColumn();
// Update the selection. We also prepare the editor to apply
// the selection colors to the current editor component.
if (editRow > -1 && editRow < table.getRowCount()) {
table.changeSelection(editRow, editCol, e.isControlDown(), e.isShiftDown());
table.prepareEditor(ListViewTableEditorRenderer.this, editRow, editCol);
}
}
// Request focus so Enter key can be handled.
e.getComponent().requestFocusInWindow();
}
private class HeadingFontWidthResolver implements FontWidthResolver {
private static final String EMPTY_STRING = "";
//finds <b>foo</b> or <a href="#foo"> or {1} patterns
//**NOTE** This does not account for all HTML sanitizing, just for HTML
//**NOTE** that would have been added by search result display code
private final Pattern findHTMLTagsOrReplacementTokens = Pattern.compile("([<][/]?[\\w =\"#]*[>])|([{][\\d]*[}])");
private final Matcher matcher = findHTMLTagsOrReplacementTokens.matcher("");
@Override
public int getPixelWidth(String text) {
HTMLEditorKit editorKit = (HTMLEditorKit) heading.getEditorKit();
StyleSheet css = editorKit.getStyleSheet();
FontMetrics fontMetrics = css.getFontMetrics(headingFont);
matcher.reset(text);
text = matcher.replaceAll(EMPTY_STRING);
return fontMetrics.stringWidth(text);
}
}
/**
* A label that does not appear to dance up and down when displaying HTML in a table.
*
* The text inside JLabels wraps when displaying HTML that doesn't fit on one line. Only the first line is displayed but
* when the label is used in a table renderer or editor, it has weird mouse over behavior where the lines dance up and down.
* NoDancingHtmlLabel prevents this behavior.
*/
private static class NoDancingHtmlLabel extends TransparentCellTableRenderer {
public NoDancingHtmlLabel(){
//prevents strange movement on mouseover
setVerticalAlignment(JLabel.TOP);
}
@Override
public void setText(String text){
super.setText(text);
//prevent our little friend from dancing up and down on mouse over
setMaximumSize(new Dimension(Integer.MAX_VALUE, getPreferredSize().height));
}
}
}