package org.rr.jeborker.gui.cell;
import static org.rr.commons.utils.StringUtil.EMPTY;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.image.BufferedImage;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTable;
import javax.swing.JTextArea;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;
import javax.swing.table.TableCellRenderer;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.jsoup.Jsoup;
import org.jsoup.safety.Whitelist;
import org.rr.commons.collection.VolatileHashMap;
import org.rr.commons.log.LoggerFactory;
import org.rr.commons.mufs.IResourceHandler;
import org.rr.commons.mufs.ResourceHandlerFactory;
import org.rr.commons.swing.SwingUtils;
import org.rr.commons.swing.components.StarRater;
import org.rr.commons.utils.CommonUtils;
import org.rr.commons.utils.HTMLEntityConverter;
import org.rr.commons.utils.ListUtils;
import org.rr.commons.utils.ReflectionFailureException;
import org.rr.commons.utils.ReflectionUtils;
import org.rr.commons.utils.StringUtil;
import org.rr.jeborker.app.BasePathList;
import org.rr.jeborker.app.JeboorkerConstants;
import org.rr.jeborker.app.JeboorkerConstants.SUPPORTED_MIMES;
import org.rr.jeborker.app.preferences.APreferenceStore;
import org.rr.jeborker.app.preferences.PreferenceStoreFactory;
import org.rr.jeborker.db.IDBObject;
import org.rr.jeborker.db.item.EbookPropertyItem;
import org.rr.jeborker.db.item.EbookPropertyItemUtils;
import org.rr.jeborker.gui.MainController;
import org.rr.pm.image.IImageProvider;
import org.rr.pm.image.ImageProviderFactory;
import org.rr.pm.image.ImageUtils;
public class EbookTableCellRenderer implements TableCellRenderer, Serializable {
private static final long serialVersionUID = -4684790158985895647L;
class RendererComponent extends JPanel {
public RendererComponent() {
init();
}
private JLabel imageLabel;
private JLabel firstLineLabel;
private JLabel secondLineLabel;
private JTextArea thirdLineTextArea;
private JLabel dataFormatLabel;
private StarRater starRater;
private JPanel basePathColorIndicator;
private boolean labelSetupComplete = false;
public String toString() {
return new ToStringBuilder(this)
.append("label1", firstLineLabel.getText())
.append("label2", secondLineLabel.getText())
.toString();
}
public void init() {
GridBagLayout gridBagLayout = new GridBagLayout();
gridBagLayout.columnWidths = new int[]{0, 0, 0, 0, 0};
gridBagLayout.rowHeights = new int[]{0, 0, 0};
gridBagLayout.columnWeights = new double[]{0.0, 1.0, 0.0, 0.0, Double.MIN_VALUE};
gridBagLayout.rowWeights = new double[]{0.0, 1.0, Double.MIN_VALUE};
setLayout(gridBagLayout);
imageLabel = new JLabel(EMPTY);
imageLabel.setOpaque(false);
imageLabel.setVerticalAlignment(SwingConstants.TOP);
GridBagConstraints gbc_imageLabel = new GridBagConstraints();
gbc_imageLabel.insets = new Insets(0, 0, 0, 0);
gbc_imageLabel.gridheight = 5;
gbc_imageLabel.gridx = 0;
gbc_imageLabel.gridy = 1;
add(imageLabel, gbc_imageLabel);
firstLineLabel = new JLabel(EMPTY);
firstLineLabel.setOpaque(false);
firstLineLabel.setVerticalAlignment(SwingConstants.TOP);
GridBagConstraints gbc_firstLineLabel = new GridBagConstraints();
gbc_firstLineLabel.insets = new Insets(0, 0, 0, 0);
gbc_firstLineLabel.anchor = GridBagConstraints.WEST;
gbc_firstLineLabel.gridx = 1;
gbc_firstLineLabel.gridy = 1;
add(firstLineLabel, gbc_firstLineLabel);
dataFormatLabel = new JLabel(EMPTY);
dataFormatLabel.setOpaque(false);
dataFormatLabel.setVerticalAlignment(SwingConstants.TOP);
GridBagConstraints gbc_label = new GridBagConstraints();
gbc_label.anchor = GridBagConstraints.NORTHEAST;
gbc_label.insets = new Insets(2, 0, 0, 0);
gbc_label.gridx = 2;
gbc_label.gridy = 2;
add(dataFormatLabel, gbc_label);
starRater = new StarRater();
starRater.setMinimumSize(new Dimension(85,27));
GridBagConstraints gbc_starRater = new GridBagConstraints();
gbc_starRater.insets = new Insets(3, 0, 0, 0);
gbc_starRater.gridx = 2;
gbc_starRater.gridy = 1;
starRater.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if(starRater.getSelection() > 0) {
MainController.getController().setRatingToSelectedEntry(starRater.getSelection() * 2);
}
}
});
add(starRater, gbc_starRater);
secondLineLabel = new JLabel(EMPTY);
secondLineLabel.setOpaque(false);
secondLineLabel.setVerticalAlignment(SwingConstants.TOP);
GridBagConstraints gbc_secondLineLabel = new GridBagConstraints();
gbc_secondLineLabel.insets = new Insets(0, 0, 0, 0);
gbc_secondLineLabel.gridwidth = 3;
gbc_secondLineLabel.anchor = GridBagConstraints.WEST;
gbc_secondLineLabel.gridx = 1;
gbc_secondLineLabel.gridy = 2;
add(secondLineLabel, gbc_secondLineLabel);
thirdLineTextArea = new JTextArea(EMPTY);
thirdLineTextArea.setOpaque(false);
thirdLineTextArea.setLineWrap(true);
thirdLineTextArea.setWrapStyleWord(true);
thirdLineTextArea.setEnabled(false);
thirdLineTextArea.setBorder(BorderFactory.createEmptyBorder());
GridBagConstraints gbc_thirdLineTextArea = new GridBagConstraints();
gbc_thirdLineTextArea.insets = new Insets(0, 0, 0, 0);
gbc_thirdLineTextArea.gridwidth = 3;
gbc_thirdLineTextArea.anchor = GridBagConstraints.WEST;
gbc_thirdLineTextArea.gridx = 1;
gbc_thirdLineTextArea.gridy = 3;
add(thirdLineTextArea, gbc_thirdLineTextArea);
basePathColorIndicator = new JPanel();
basePathColorIndicator.setOpaque(true);
GridBagConstraints gbc_color = new GridBagConstraints();
gbc_color.insets = new Insets(0, 0, 0, 0);
gbc_color.gridheight = 5;
gbc_color.gridx = 4;
gbc_color.gridy = 1;
add(basePathColorIndicator, gbc_color);
this.setOpaque(true);
}
/**
* take sure that the labels have a constant allocation.
*/
void completeLabelSetup(final JTable table, RendererComponent renderer) {
if(!labelSetupComplete) {
Font f = renderer.firstLineLabel.getFont();
renderer.firstLineLabel.setFont(f.deriveFont(f.getStyle() ^ Font.BOLD));
final int oneLineHeight = 19;
final int lastLabelHeight = table.getRowHeight() - oneLineHeight - oneLineHeight;
renderer.firstLineLabel.setBorder(new EmptyBorder(0,0,0,0));
renderer.firstLineLabel.setMinimumSize(new Dimension(table.getWidth(), oneLineHeight));
renderer.dataFormatLabel.setBorder(new EmptyBorder(0,0,0,5));
renderer.dataFormatLabel.setForeground(Color.GRAY);
renderer.secondLineLabel.setMinimumSize(new Dimension(table.getWidth(), oneLineHeight));
renderer.secondLineLabel.setBorder(new EmptyBorder(0,0,0,0));
renderer.thirdLineTextArea.setMinimumSize(new Dimension(table.getWidth(), lastLabelHeight));
renderer.thirdLineTextArea.addMouseListener(new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent e) {
// dispatch the mouse event in the text field to the table's popup menu.
popupMouseListener.mouseReleased(SwingUtilities.convertMouseEvent(thirdLineTextArea, e, table));
}
});
renderer.imageLabel.setMinimumSize(new Dimension(50, table.getRowHeight()));
renderer.imageLabel.setMaximumSize(new Dimension(50, table.getRowHeight()));
renderer.imageLabel.setSize(new Dimension(50, table.getRowHeight()));
renderer.imageLabel.setPreferredSize(new Dimension(50, table.getRowHeight()));
renderer.basePathColorIndicator.setMinimumSize(new Dimension(5, table.getRowHeight()));
labelSetupComplete = true;
}
}
void setStarRaterSelection(int sel) {
starRater.setSelection(sel);
}
int getStarRaterSelection() {
return starRater.getSelection();
}
}
private static final VolatileHashMap<String, ImageIcon> thumbnailCache = new VolatileHashMap<String, ImageIcon>(20, 20);
private final APreferenceStore preferenceStore = PreferenceStoreFactory.getPreferenceStore(PreferenceStoreFactory.DB_STORE);
private Dimension thumbnailDimension;
private final MouseListener popupMouseListener;
private boolean singletonComponent = false;
private RendererComponent rendererComponent;
public EbookTableCellRenderer(MouseListener popupMouseListener) {
this.popupMouseListener = popupMouseListener;
}
public EbookTableCellRenderer(MouseListener popupMouseListener, boolean singletonComponent) {
this.popupMouseListener = popupMouseListener;
this.singletonComponent = singletonComponent;
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
RendererComponent renderer = this.getTableCellComponent(table, value, isSelected, hasFocus, row, column);
setStripedColorSetupToRenderer(isSelected, row, renderer);
return renderer;
}
private void setStripedColorSetupToRenderer(boolean isSelected, int row, RendererComponent renderer) {
if(isSelected) {
renderer.setBackground(SwingUtils.getBrighterColor(SwingUtils.getSelectionBackgroundColor(), 20));
renderer.setForeground(SwingUtils.getSelectionForegroundColor());
} else {
if(row % 2 == 0) {
renderer.setBackground(SwingUtils.getStripeBackgroundColor());
} else {
renderer.setBackground(SwingUtils.getBackgroundColor());
}
renderer.setForeground(SwingUtils.getForegroundColor());
}
}
RendererComponent getTableCellComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, final int column) {
final EbookPropertyItem item = (EbookPropertyItem) value;
final Color foregroundColor = SwingUtils.getForegroundColor();
final Color selectionForegroundColor = SwingUtils.getSelectionForegroundColor();
final Color brighterColor = SwingUtils.getBrighterColor(SwingUtils.getSelectionBackgroundColor(), 20);
final Color backgroundColor = SwingUtils.getBackgroundColor();
// seems there is an openjdk issue with linux which cause getTableCellComponent could be invoked
// before the painting process is finished.
// Avoid these and create always a new renderer instance.
RendererComponent renderer = createTableCellComponent(item);
setCommonColorRendererComponentSetup(isSelected, foregroundColor, selectionForegroundColor, brighterColor, backgroundColor, renderer);
renderer.imageLabel.setIcon(getImageIconCover(table, item));
renderer.completeLabelSetup(table, renderer);
//title
renderer.firstLineLabel.setText(this.getTitle(item));
//gray light file format
renderer.dataFormatLabel.setText(getDataFormat(item));
setStarRatingRendererComponentSetup(item, renderer);
//second line author and order values.
renderer.secondLineLabel.setText(getAuthorAndOrderValues(item));
//third line description
setThirdLineRendererComponentSetup(item, renderer);
//base path color hint
setBasePathColorIndicator(item, renderer);
return renderer;
}
protected void setBasePathColorIndicator(final EbookPropertyItem item, RendererComponent renderer) {
BasePathList basePath = preferenceStore.getBasePath();
Color color = basePath != null && item != null ? basePath.getColor(item.getBasePath()) : null;
if(color != null) {
renderer.basePathColorIndicator.setBackground(preferenceStore.getBasePath().getColor(item.getBasePath()));
} else {
renderer.basePathColorIndicator.setBackground(SwingUtils.getBackgroundColor());
}
}
private void setThirdLineRendererComponentSetup(final EbookPropertyItem item, RendererComponent renderer) {
if(item != null && item.getDescription() != null) {
//attach html for a multiline label but previously strip all possible html from the description.
String strippedDescription = cleanString(item != null ? item.getDescription() : EMPTY);
renderer.thirdLineTextArea.setText(strippedDescription);
} else {
renderer.thirdLineTextArea.setText(EMPTY);
}
}
private void setStarRatingRendererComponentSetup(final EbookPropertyItem item, RendererComponent renderer) {
final float starRatingValue = this.getStarRatingValue(item);
if(starRatingValue < 0) {
renderer.starRater.setRating(0);
} else {
renderer.starRater.setRating(starRatingValue);
}
}
private void setCommonColorRendererComponentSetup(boolean isSelected, final Color foregroundColor, final Color selectionForegroundColor,
final Color brighterColor, final Color backgroundColor, RendererComponent renderer) {
if(isSelected) {
renderer.firstLineLabel.setForeground(selectionForegroundColor);
renderer.secondLineLabel.setForeground(selectionForegroundColor);
renderer.thirdLineTextArea.setForeground(selectionForegroundColor);
renderer.setForeground(selectionForegroundColor);
renderer.setBackground(brighterColor);
} else {
renderer.firstLineLabel.setForeground(foregroundColor);
renderer.secondLineLabel.setForeground(foregroundColor);
renderer.thirdLineTextArea.setForeground(foregroundColor);
renderer.setForeground(foregroundColor);
renderer.setBackground(backgroundColor);
}
}
protected RendererComponent createTableCellComponent(EbookPropertyItem item) {
if(singletonComponent) {
if(rendererComponent == null) {
rendererComponent = new RendererComponent();
}
return rendererComponent;
}
return new RendererComponent();
}
/**
* Get the five star rating value or -1 if not rating is available.
* @param item The item containing the rating.
* @return The rating value.
*/
private float getStarRatingValue(final EbookPropertyItem item) {
if(item == null) {
return 0f;
}
final Integer rating = item.getRating();
if(rating == null || rating.intValue() < 0) {
return -1f;
} else {
return ((float)rating) / 2f;
}
}
/**
* Gets the thumbnail image to be displayed in the renderer.
* @param table The JTable instance.
* @param item The item to be rendered.
* @return The thumbnail image to be displayed in the renderer.
*/
private ImageIcon getImageIconCover(final JTable table, final EbookPropertyItem item) {
if(item == null) {
return null;
}
byte[] coverThumbnail = EbookPropertyItemUtils.getCoverThumbnailBytes(item.getResourceHandler());
if(item != null && coverThumbnail != null && coverThumbnail.length > 0) {
final String coverThumbnailCRC32 = String.valueOf(CommonUtils.calculateCrc(coverThumbnail));
if(thumbnailCache.containsKey(coverThumbnailCRC32)) {
return thumbnailCache.get(coverThumbnailCRC32);
}
try {
final byte[] coverData = coverThumbnail;
if(coverData != null) {
final IResourceHandler virtualImageResourceLoader = ResourceHandlerFactory.getVirtualResourceHandler("TableCellRendererImageData", coverData);
final IImageProvider imageProvider = ImageProviderFactory.getImageProvider(virtualImageResourceLoader);
final BufferedImage image = imageProvider.getImage();
if(image != null) {
BufferedImage scaleToMatch = ImageUtils.scaleToMatch(imageProvider.getImage(), getThumbnailDimension(table), false);
ImageIcon imageIcon = new ImageIcon(scaleToMatch);
thumbnailCache.put(coverThumbnailCRC32, imageIcon);
return imageIcon;
} else {
return null;
}
}
} catch (Exception e) {
LoggerFactory.logInfo(this, "Could not render thumbnail", e);
}
}
return null;
}
/**
* Gets the dimension for the thumbnail in the view.
* @param table The tabel which shows the thumbnail.
* @return The dimension for the thumbnail.
*/
private Dimension getThumbnailDimension(final JTable table) {
if(thumbnailDimension == null) {
thumbnailDimension = new Dimension((int) (table.getRowHeight()*0.7), table.getRowHeight());
}
return thumbnailDimension;
}
/**
* Get the author and the sort order column values.
* @param item The item containing the desired values.
* @return The author and order string. Never returns <code>null</code>.
*/
private String getAuthorAndOrderValues(EbookPropertyItem item) {
if(item == null) {
return EMPTY;
}
final StringBuilder result = new StringBuilder();
final List<Field> selectedFields = MainController.getController().getSelectedSortColumnFields();
for (Field field : selectedFields) {
//do not add the folowing ones.
if(field.getName().equalsIgnoreCase("author")) {
continue;
} else if(field.getName().equalsIgnoreCase("authorSort")) {
continue;
} else if(field.getName().equalsIgnoreCase("title")) {
continue;
} else if(field.getName().equalsIgnoreCase("description")) {
continue;
}
try {
Object fieldValueObject = ReflectionUtils.getFieldValue(item, field.getName(), true);
if(field.getName().equals("file")) {
fieldValueObject = item.getResourceHandler().getName();
}
final String fieldValueString = StringUtil.toString(fieldValueObject);
if(StringUtil.isNotEmpty(fieldValueString)) {
if(StringUtil.isNotEmpty(result)) {
result.append(", ");
}
if(fieldValueObject instanceof Date) {
result.append(SimpleDateFormat.getDateInstance().format(fieldValueObject));
} else {
result.append(fieldValueString);
}
}
} catch (ReflectionFailureException e) {
LoggerFactory.logWarning(this, "No field named " + field.getName(), e);
}
}
//prepend the author to the result string
if(StringUtil.isNotEmpty(result)) {
result.insert(0, ", ");
}
List<String> authors;
if(item.getAuthor() == null) {
authors = Collections.emptyList();
} else {
authors = item.getAuthor() != null ? ListUtils.split(item.getAuthor(), IDBObject.LIST_SEPARATOR_CHAR) : new ArrayList<String>();
}
if(!authors.isEmpty()) {
StringBuilder b = new StringBuilder();
for(String author : authors) {
if(b.length() != 0) {
b.append(", ");
}
b.append(author);
}
if(StringUtil.isNotEmpty(b)) {
result.insert(0, b);
} else {
result.insert(0, "<"+Bundle.getString("EbookTableCellComponent.noAuthor")+">");
}
} else {
result.insert(0, "<"+Bundle.getString("EbookTableCellComponent.noAuthor")+">");
}
return result.toString();
}
/**
* Creates the title of the document.
* @param item The item where the title should be created from.
* @return The desired book title.
*/
private String getTitle(EbookPropertyItem item) {
if(item != null && StringUtil.isEmpty(item.getTitle())) {
//if there is no title, just use the file name but without file extension
final String fileName = StringUtil.substringBefore(item.getFileName(), ".", false);
return fileName;
} else {
return cleanString(item != null ? item.getTitle() : EMPTY);
}
}
/**
* Remove all html tags an decode entities.
* @param toClean The text to be cleaned.
* @return
*/
private String cleanString(String toClean) {
if(toClean == null) {
return EMPTY;
}
if(toClean.indexOf('&')!=-1) {
toClean = new HTMLEntityConverter(toClean, HTMLEntityConverter.ENCODE_EIGHT_BIT_ASCII).decodeEntities();
}
if(toClean.indexOf('<')!=-1) {
toClean = Jsoup.clean(toClean, Whitelist.none());
}
return toClean.trim();
}
/**
* Gets the string value for the file format to be displayed in the renderer.
* For example "pdf" if it's a pdf file.
* @param item The item where the string should be evaluated for.
* @return The detected file format string or an empty string if the format could not
* be detected. Never returns <code>null</code>.
*/
private String getDataFormat(EbookPropertyItem item) {
if (item != null) {
for(SUPPORTED_MIMES mime : JeboorkerConstants.SUPPORTED_MIMES.values()) {
if(mime.getMime().equals(item.getMimeType())) {
return mime.getName();
}
}
IResourceHandler resourceHandler = item.getResourceHandler();
String mimeType = resourceHandler != null ? resourceHandler.getMimeType(true) : null;
if(mimeType != null) {
return mimeType;
}
}
return EMPTY;
}
}