/*
* This file is part of muCommander, http://www.mucommander.com
* Copyright (C) 2002-2016 Maxence Bernard
*
* muCommander is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* muCommander is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.mucommander.ui.main.table;
import java.awt.Component;
import java.awt.Font;
import javax.swing.JTable;
import javax.swing.table.TableCellRenderer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.mucommander.commons.file.AbstractFile;
import com.mucommander.ui.icon.CustomFileIconProvider;
import com.mucommander.ui.icon.FileIcons;
import com.mucommander.ui.icon.IconManager;
import com.mucommander.ui.quicksearch.QuickSearch;
import com.mucommander.ui.theme.ColorChangedEvent;
import com.mucommander.ui.theme.FontChangedEvent;
import com.mucommander.ui.theme.Theme;
import com.mucommander.ui.theme.ThemeCache;
import com.mucommander.ui.theme.ThemeListener;
/**
* The custom <code>TableCellRenderer</code> class used by {@link FileTable} to render all table cells.
*
* <p>Quote from Sun's Javadoc : The table class defines a single cell renderer and uses it as a
* as a rubber-stamp for rendering all cells in the table; it renders the first cell,
* changes the contents of that cell renderer, shifts the origin to the new location, re-draws it, and so on.</p>
*
* <p>This <code>TableCellRender</code> is written from scratch instead of overridding <code>DefaultTableCellRender</code>
* to provide a more efficient (and more specialized) implementation: each column is rendered using a dedicated
* {@link com.mucommander.ui.main.table.CellLabel CellLabel} which takes into account the column's specificities.
* Having a dedicated for each column avoids calling the label's <code>set</code> methods (alignment, border, font...)
* each time {@link #getTableCellRendererComponent(javax.swing.JTable, Object, boolean, boolean, int, int)}}
* is invoked, making cell rendering faster.
*
* <p>Contrarily to <code>DefaultTableCellRender</code>, <code>FileTableCellRenderer</code> does not extend JLabel,
* instead the dedicated {@link CellLabel} class is used to render cells, making the implementation
* less confusing IMO.
*
* @author Maxence Bernard, Nicolas Rinaudo
*/
public class FileTableCellRenderer implements TableCellRenderer, ThemeListener {
private static final Logger LOGGER = LoggerFactory.getLogger(FileTableCellRenderer.class);
private FileTable table;
private FileTableModel tableModel;
/** Custom JLabel that render specific column cells */
private CellLabel[] cellLabels = new CellLabel[Column.values().length];
public FileTableCellRenderer(FileTable table) {
this.table = table;
this.tableModel = table.getFileTableModel();
// Create a label for each column
for(Column c : Column.values())
this.cellLabels[c.ordinal()] = new CellLabel();
// Set labels' font.
setCellLabelsFont(ThemeCache.tableFont);
// Set labels' text alignment
cellLabels[Column.EXTENSION.ordinal()].setHorizontalAlignment(CellLabel.CENTER);
cellLabels[Column.NAME.ordinal()].setHorizontalAlignment(CellLabel.LEFT);
cellLabels[Column.SIZE.ordinal()].setHorizontalAlignment(CellLabel.RIGHT);
cellLabels[Column.DATE.ordinal()].setHorizontalAlignment(CellLabel.RIGHT);
cellLabels[Column.PERMISSIONS.ordinal()].setHorizontalAlignment(CellLabel.LEFT);
cellLabels[Column.OWNER.ordinal()].setHorizontalAlignment(CellLabel.LEFT);
cellLabels[Column.GROUP.ordinal()].setHorizontalAlignment(CellLabel.LEFT);
// Listens to certain configuration variables
ThemeCache.addThemeListener(this);
}
/**
* Returns the font used to render all table cells.
*/
public static Font getCellFont() {
return ThemeCache.tableFont;
}
/**
* Sets CellLabels' font to the current one.
*/
private void setCellLabelsFont(Font newFont) {
// Set custom font
for(Column c : Column.values()) {
// No need to set extension label's font as this label renders only icons and no text
if(c==Column.EXTENSION)
continue;
cellLabels[c.ordinal()].setFont(newFont);
}
}
///////////////////////////////
// TableCellRenderer methods //
///////////////////////////////
private static int getColorIndex(int row, AbstractFile file, FileTableModel tableModel) {
// Parent directory.
if(row==0 && tableModel.hasParentFolder())
return ThemeCache.FOLDER;
// Marked file.
if(tableModel.isRowMarked(row))
return ThemeCache.MARKED;
// Symlink.
if(file.isSymlink())
return ThemeCache.SYMLINK;
// Hidden file.
if(file.isHidden())
return ThemeCache.HIDDEN_FILE;
// Directory.
if(file.isDirectory())
return ThemeCache.FOLDER;
// Archive.
if(file.isBrowsable())
return ThemeCache.ARCHIVE;
// Plain file.
return ThemeCache.PLAIN_FILE;
}
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int rowIndex, int columnIndex) {
Column column;
int colorIndex;
int focusedIndex;
int selectedIndex;
CellLabel label;
AbstractFile file;
boolean matches;
QuickSearch search;
// Need to check that row index is not out of bounds because when the folder
// has just been changed, the JTable may try to repaint the old folder and
// ask for a row index greater than the length if the old folder contained more files
if(rowIndex < 0 || rowIndex >= tableModel.getRowCount())
return null;
// Sanity check.
file = tableModel.getCachedFileAtRow(rowIndex);
if(file==null) {
LOGGER.debug("tableModel.getCachedFileAtRow("+ rowIndex +") RETURNED NULL !");
return null;
}
search = this.table.getQuickSearch();
if(!table.hasFocus())
matches = true;
else {
if(search.isActive())
matches = search.matches(this.table.getFileNameAtRow(rowIndex));
else
matches = true;
}
// Retrieves the various indexes of the colors to apply.
// Selection only applies when the table is the active one
selectedIndex = (isSelected && ((FileTable)table).isActiveTable()) ? ThemeCache.SELECTED : ThemeCache.NORMAL;
focusedIndex = table.hasFocus() ? ThemeCache.ACTIVE : ThemeCache.INACTIVE;
colorIndex = getColorIndex(rowIndex, file, tableModel);
column = Column.valueOf(table.convertColumnIndexToModel(columnIndex));
label = cellLabels[column.ordinal()];
// Extension/icon column: return ImageIcon instance
if(column == Column.EXTENSION) {
// Set file icon (parent folder icon if '..' file)
label.setIcon(rowIndex ==0 && tableModel.hasParentFolder()
?IconManager.getIcon(IconManager.FILE_ICON_SET, CustomFileIconProvider.PARENT_FOLDER_ICON_NAME, FileIcons.getScaleFactor())
:FileIcons.getFileIcon(file));
}
// Any other column (name, date or size)
else {
String text = (String)value;
if(matches || isSelected)
label.setForeground(ThemeCache.foregroundColors[focusedIndex][selectedIndex][colorIndex]);
else
label.setForeground(ThemeCache.unmatchedForeground);
// Set the label's text, before calculating it width
label.setText(text);
// If label's width is larger than the column width:
// - truncate the text from the center and equally to the left and right sides, adding an ellipsis ('...')
// where characters have been removed. This allows both the start and end of filename to be visible.
// - set a tooltip text that will display the whole text when mouse is over the label
if (table.getColumnModel().getColumn(columnIndex).getWidth() < label.getPreferredSize().getWidth()) {
String leftText = text.substring(0, text.length()/2);
String rightText = text.substring(text.length()/2, text.length());
while(table.getColumnModel().getColumn(columnIndex).getWidth() < label.getPreferredSize().getWidth()
&& leftText.length()>0 && rightText.length()>0) { // Prevents against going out of bounds
if(leftText.length()>rightText.length())
leftText = leftText.substring(0, leftText.length()-1);
else
rightText = rightText.substring(1, rightText.length());
label.setText(leftText+"..."+rightText);
}
// Set the toop
label.setToolTipText(text);
}
// Have to set it to null otherwise the defaultRender sets the tooltip text to the last one
// specified
else
label.setToolTipText(null);
}
// Set background color depending on whether the row is selected or not, and whether the table has focus or not
if(selectedIndex == ThemeCache.SELECTED)
label.setBackground(ThemeCache.backgroundColors[focusedIndex][ThemeCache.SELECTED], ThemeCache.backgroundColors[focusedIndex][ThemeCache.SECONDARY]);
else if(matches) {
if(table.hasFocus() && search.isActive())
label.setBackground(ThemeCache.backgroundColors[focusedIndex][ThemeCache.NORMAL]);
else
label.setBackground(ThemeCache.backgroundColors[focusedIndex][(rowIndex % 2 == 0) ? ThemeCache.NORMAL : ThemeCache.ALTERNATE]);
}
else
label.setBackground(ThemeCache.unmatchedBackground);
if(selectedIndex == ThemeCache.SELECTED)
label.setOutline(table.hasFocus() ? ThemeCache.activeOutlineColor : ThemeCache.inactiveOutlineColor);
else
label.setOutline(null);
return label;
}
// - Theme listening -------------------------------------------------------------
// -------------------------------------------------------------------------------
/**
* Receives theme color changes notifications.
*/
public void colorChanged(ColorChangedEvent event) {
table.repaint();
}
/**
* Receives theme font changes notifications.
*/
public void fontChanged(FontChangedEvent event) {
if(event.getFontId() == Theme.FILE_TABLE_FONT) {
setCellLabelsFont(ThemeCache.tableFont);
}
}
}