/*
* ThumbnailView.java
*
* Created on December 18, 2002, 4:29 PM
*/
package kiyut.swing.shell.shelllistview;
import java.awt.*;
import java.awt.dnd.DropTarget;
import java.awt.event.*;
import java.awt.image.*;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeEvent;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.security.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;
import java.util.TooManyListenersException;
import javax.imageio.*;
import javax.imageio.metadata.*;
import javax.imageio.stream.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.filechooser.FileSystemView;
import org.w3c.dom.*;
import kiyut.imageio.*;
import kiyut.swing.dnd.*;
import kiyut.swing.shell.event.*;
import kiyut.swing.shell.image.ImageUtilities;
import kiyut.swing.shell.util.*;
/** <code>ThumbnailView</code> is a view for <code>ShellListView</code> which display
* the <code>ShellListViewModel</code> in a thumbnail like view.
* It loads the image on the background and using <code>ImageIO</code> to load the image progressively.
* When this component is disabled, it stop the background image loading.
* The chace directory and content is to meet the http://www.freedesktop.org specification regarding thumbnail cache.
* <b>important:<b>when setUseCache to true make sure cache directory is valid.
*
* @version 1.0
* @author tonny
*/
public class ThumbnailView extends JComponent implements Scrollable, ViewComponent {
/** the <code>PropertyChangeListener for this component */
private PropertyChangeListener propertyChangeListener;
/** the <code>FlowLayout</code> of this component, used to arrange the cells. */
private FlowLayout flowLayout;
/** Number of columns to create.*/
private int columnCount = -1;
/** The <code>ListSelectionModel</code> of this component , used to keep track of index selections. */
protected ListSelectionModel selectionModel;
/** The <code>ShellListViewModel</code> of the this component. */
protected ShellListViewModel dataModel;
/** the list of cells */
protected List<ThumbnailCell> cells;
/** the list of file to be loaded */
protected List<File> loadList;
/** the editor for the cells */
protected ThumbnailCellEditor cellEditor;
/** Identifies the index of the cell being edited. */
protected int editingIndex = -1;
/** this component background color */
protected Color background;
/** this component selection background color */
protected Color selectionBackground;
/** this component selection foreground color */
protected Color selectionForeground;
/** this component dragEnabled, initialy false */
protected boolean dragEnabled = false;
/** the thumbnail thread */
protected Thread thumbnailThread;
/** boolean indicating flag use cache for thumbnail */
protected boolean useCache = false;
/** cache directory location */
protected File cacheDirectory = null;
/** md5 generator for file cache */
protected MessageDigest md5;
/** ImageReaderWRiterPreferences */
protected ImageReaderWriterPreferences imageReaderWriterPreferences;
/** Constructs a <code>ThumbnailView</code> using the given data model
* @param dataModel data model for this component
*/
public ThumbnailView(ShellListViewModel dataModel) {
try {
md5 = MessageDigest.getInstance("MD5");
} catch (Exception ex) {}
cells = new ArrayList<ThumbnailCell>();
loadList = Collections.synchronizedList(new ArrayList<File>());
cellEditor = new ThumbnailCellEditor();
cellEditor.addCellEditorListener(new javax.swing.event.CellEditorListener() {
public void editingCanceled(ChangeEvent evt) { ThumbnailView.this.editingCanceled(evt); }
public void editingStopped(ChangeEvent evt) { ThumbnailView.this.editingStopped(evt); }
});
this.dataModel = dataModel;
dataModel.addListDataListener(new javax.swing.event.ListDataListener() {
public void contentsChanged(ListDataEvent evt) { onContentsChanged(evt); }
public void intervalAdded(ListDataEvent evt) { onIntervalAdded(evt); }
public void intervalRemoved(ListDataEvent evt) { onIntervalRemoved(evt); }
});
// DnD Support
setTransferHandler(new TransferHandler(""));
ThumbnailViewDragGestureRecognizer dragRecognizer = new ThumbnailViewDragGestureRecognizer();
addMouseListener(dragRecognizer);
addMouseMotionListener(dragRecognizer);
addComponentListener(new ComponentAdapter() {
public void componentResized(ComponentEvent evt) { calculatePreferredSize(); }
});
addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent evt) { onMouseClicked(evt); }
});
addKeyListener(new KeyAdapter() {
public void keyPressed(KeyEvent evt) { onKeyPressed(evt); }
});
propertyChangeListener = new PropertyChangeHandler();
addPropertyChangeListener(propertyChangeListener);
flowLayout = new FlowLayout();
flowLayout.setAlignment(FlowLayout.LEFT);
setPreferredSize(new Dimension(128, 128));
setLayout(flowLayout);
//setOpaque(true);
setBackground(UIManager.getColor("List.background"));
setForeground(UIManager.getColor("List.foreground"));
setFont(UIManager.getFont("Table.font"));
setSelectionBackground(UIManager.getColor("List.selectionBackground"));
setSelectionForeground(UIManager.getColor("List.selectionForeground"));
thumbnailThread = new ThumbnailThread();
thumbnailThread.start();
}
/** Overriden to provide add Custom dropTargetListener
* {@inheritDoc}
*/
public void setTransferHandler(TransferHandler newHandler) {
super.setTransferHandler(newHandler);
try {
getDropTarget().addDropTargetListener(new ThumbnailViewDropTargetListener());
} catch (TooManyListenersException ex) { }
}
/**
* Returns the background color for selected cells.
*
* @return the <code>Color</code> used for the background of selected list items
* @see #setSelectionBackground
*/
public Color getSelectionBackground() {
return selectionBackground;
}
/**
* Sets the background color for selected cells.
* <p>
* The default value of this property is defined by the look
* and feel implementation.
* <p>
* This is a JavaBeans bound property.
*
* @param selectionBackground the <code>Color</code> to use for the
* background of selected cells
* @see #getSelectionBackground
* @see #setSelectionForeground
* @see #setForeground
* @see #setBackground
* @see #setFont
*/
public void setSelectionBackground(Color selectionBackground) {
Color oldValue = this.selectionBackground;
this.selectionBackground = selectionBackground;
firePropertyChange("selectionBackground", oldValue, selectionBackground);
}
/**
* Returns the Foreground color for selected cells.
*
* @return the <code>Color</code> used for the Foreground of
* selected list items
* @see #setSelectionForeground(Color)
*/
public Color getSelectionForeground() {
return selectionForeground;
}
/**
* Sets the Foreground color for selected cells.
* <p>
* The default value of this property is defined by the look
* and feel implementation.
* <p>
* This is a JavaBeans bound property.
*
* @param selectionForeground the <code>Color</code> to use for the
* foreground of selected cells
* @see #getSelectionBackground
* @see #setSelectionForeground
*/
public void setSelectionForeground(Color selectionForeground) {
Color oldValue = this.selectionForeground;
this.selectionForeground = selectionForeground;
firePropertyChange("selectionForeground", oldValue, selectionForeground);
}
/** Enables or disables this component, depending on the value of the parameter b.
* An enabled component can respond to user input and generate events.
* Components are enabled initially by default.
* When this component is disabled, the background image loading also disabled
* @param b If true, this component is enabled; otherwise this component is disabled
* @see java.awt.Component#setEnabled(boolean)
*/
public void setEnabled(boolean b) {
if (b == true) {
synchronized(loadList) {
loadList.notifyAll();
}
}
super.setEnabled(b);
}
/**
* Sets the <code>dragEnabled</code> property,
* which must be <code>true</code> to enable
* automatic drag handling (the first part of drag and drop)
* on this component.
* The <code>transferHandler</code> property needs to be set
* to a non-<code>null</code> value for the drag to do
* anything. The default value of the <code>dragEnabled</code
* property is <code>false</code>.
* @param b the value to set the <code>dragEnabled</code> property to
* @exception HeadlessException if
* <code>b</code> is <code>true</code> and
* <code>GraphicsEnvironment.isHeadless()</code>
* returns <code>true</code>
* @see java.awt.GraphicsEnvironment#isHeadless
* @see #getDragEnabled
* @see #setTransferHandler
* @see TransferHandler
*/
public void setDragEnabled(boolean b) {
if (b && GraphicsEnvironment.isHeadless()) {
throw new HeadlessException();
}
dragEnabled = b;
}
/**
* Gets the value of the <code>dragEnabled</code> property.
*
* @return the value of the <code>dragEnabled</code> property
*/
public boolean getDragEnabled() {
return dragEnabled;
}
/** refresh the data model */
protected void refresh() {
this.removeAll();
this.repaint();
selectionModel.clearSelection();
cells.clear();
loadList.clear();
FileSystemView fsv = dataModel.getFileSystemView();
for (int i=0; i < dataModel.getSize(); i++) {
File file = (File)dataModel.getFile(i);
ThumbnailCell cell = createCell(file,fsv);
loadList.add(file);
cells.add(cell);
this.add(cell);
}
// set preferredSize
calculatePreferredSize();
revalidate();
repaint();
synchronized(loadList) {
loadList.notifyAll();
}
}
/** Returns an <code>ThumbnailCell</code> instance using the given parameter
* @param file File
* @param fsv FileSystemView
* @return an <code>ThumbailCell</code> instance.
*/
protected ThumbnailCell createCell(File file,FileSystemView fsv) {
ThumbnailCell cell = new ThumbnailCell(this);
updateCell(cell,file,fsv);
return cell;
}
/** update the cell using the given parameter.
* @param cell the cell to be updated
* @param file File
* @param fsv FileSystemView
*/
protected void updateCell(ThumbnailCell cell,File file,FileSystemView fsv) {
cell.setText(fsv.getSystemDisplayName(file));
cell.setImageRescale(1.5, 1.5);
cell.setImage(ImageUtilities.iconToBufferedImage(fsv.getSystemIcon(file)));
}
/** load the specified <code>File</code> image and register it with the cell
* @param cell the cell to receive notification of image loading
* @param file the file to be loaded
*/
private void loadFileImage(ThumbnailCell cell, File file) {
if (isEnabled() == false) {
return;
}
File imgFile = file;
File cacheFile = null;
if (useCache == true) {
// get the file to load whether cached or original file
md5.update(file.toURI().toString().getBytes());
byte[] bytes = md5.digest();
String md5String = ShellUtilities.bytesToHexString(bytes) + ".png";
cacheFile = new File(cacheDirectory,md5String);
try {
if (isCacheValid(imgFile,cacheFile)) {
imgFile = cacheFile;
}
} catch (Exception e) {}
}
ImageReader reader = null;
BufferedImage img = null;
try {
reader = getImageReader(imgFile);
img = cell.readFileImage(reader, null);
// to make the loading faster, don't need to load the whole image
//ImageReadParam param = reader.getDefaultReadParam();
//param.setSourceProgressivePasses(0,4);
//cell.readFileImage(reader, param);
} catch (IOException e) {
} finally {
if (reader != null) {
if (reader.getInput() instanceof ImageInputStream) {
try {
((ImageInputStream)reader.getInput()).close();
} catch (Exception e) { }
}
reader.dispose();
reader = null;
}
}
if (useCache == true && !imgFile.equals(cacheFile) && img != null) {
if (!(img.getWidth() <= 128 && img.getHeight() <= 128)) {
try {
BufferedImage smallImg = cell.getImage();
writeImageCache(smallImg,cacheFile, file.lastModified(), file.toURI());
//ImageIO.write(img,"png",cacheFile);
} catch (Exception ex) {}
}
}
}
/** determine whether the file has cache and the cache is valid
* MTime == lastModified and URI = fileURI.
* It is follow http://www.freedesktop.org thumbnail cache specification.
* @param file file to check
* @param cacheFile file cache to compare
* @return true if valid, otherwise false
* @throw IOException if io error occur
*/
private boolean isCacheValid(File file,File cacheFile) throws IOException {
boolean valid = false;
if (cacheFile.exists() == false) {
return valid;
}
ImageInputStream iis = ImageIO.createImageInputStream(cacheFile);
Iterator readers = ImageIO.getImageReadersBySuffix("png");
ImageReader reader = (ImageReader)readers.next();
reader.setInput(iis, true);
String cacheMTime = null;
String cacheURI = null;
IIOMetadata metadata = reader.getImageMetadata(0);
IIOMetadataNode root = (IIOMetadataNode)metadata.getAsTree(metadata.getNativeMetadataFormatName());
NodeList nodeList = root.getElementsByTagName("tEXtEntry");
for (int i=0; i<nodeList.getLength(); i++) {
IIOMetadataNode node = (IIOMetadataNode)nodeList.item(i);
if (node.getAttribute("keyword").equals("Thumb::MTime")) {
cacheMTime = node.getAttribute("value");
}
if (node.getAttribute("keyword").equals("Thumb::URI")) {
cacheURI = node.getAttribute("value");
}
}
try {
iis.close();
} catch (Exception e) { }
if (cacheMTime == null || cacheURI == null) {
return valid;
}
String mTime = Long.toString(file.lastModified());
String uri = file.toURI().toString();
if (cacheURI.equals(uri) && cacheMTime.equals(mTime)) {
valid = true;
}
return valid;
}
/** Write image to cache
* It is follow http://www.freedesktop.org thumbnail cache specification.
*@param bi the <code>BufferedImage</code> to write
*@param cacheFile the cache File
*@param lastModified the last modified to put into Thumb:MTime
*@param uri the uri to put into Thumb:URI
*@throws IOException if io error occur
*/
private void writeImageCache(BufferedImage bi, File cacheFile, long lastModified, URI uri) throws IOException {
ImageWriter writer = null;
if (imageReaderWriterPreferences != null) {
writer = imageReaderWriterPreferences.getPreferredImageWriterByFormatName("png");
}
if (writer == null) {
Iterator writers = ImageIO.getImageWritersByFormatName("png");
writer = (ImageWriter)writers.next();
}
// set up metadata
IIOMetadata metadata = writer.getDefaultImageMetadata(ImageTypeSpecifier.createFromRenderedImage(bi),null);
Node node = metadata.getAsTree(metadata.getNativeMetadataFormatName());
Node tEXtNode = new IIOMetadataNode("tEXt");
node.appendChild(tEXtNode);
Element entryElt;
entryElt = new IIOMetadataNode("tEXtEntry");
entryElt.setAttribute("keyword", "Software");
entryElt.setAttribute("value", "Kiyut Ekspos Image Viewer");
tEXtNode.appendChild(entryElt);
entryElt = new IIOMetadataNode("tEXtEntry");
entryElt.setAttribute("keyword", "Thumb::MTime");
entryElt.setAttribute("value", Long.toString(lastModified));
tEXtNode.appendChild(entryElt);
entryElt = new IIOMetadataNode("tEXtEntry");
entryElt.setAttribute("keyword", "Thumb::URI");
entryElt.setAttribute("value", uri.toString());
tEXtNode.appendChild(entryElt);
metadata.mergeTree(metadata.getNativeMetadataFormatName(), node);
// create temp file
File parentFile = cacheFile.getParentFile();
File tmpFile = new File(parentFile, "temp" + Runtime.getRuntime().hashCode() + ".png");
// write to temp file
ImageOutputStream ios = ImageIO.createImageOutputStream(tmpFile);
writer.setOutput(ios);
IIOImage iioImage = new IIOImage(bi,null,metadata);
writer.write(null,iioImage,null);
ios.close();
writer.dispose();
// rename temp file to cache file
tmpFile.renameTo(cacheFile);
}
/** return the <code>ImageReader</code> use to read the specified <code>file</code>
* @return the <code>ImageReader</code>
* @throws IOException If an I/O error occurs
*/
private ImageReader getImageReader(File file) throws IOException {
ImageInputStream iis = ImageIO.createImageInputStream(file);
String suffix = ShellUtilities.getFileSuffix(file);
ImageReader reader = null;
if (imageReaderWriterPreferences != null) {
reader = imageReaderWriterPreferences.getPreferredImageReaderBySuffix(suffix);
}
if (reader == null) {
Iterator readers = ImageIO.getImageReadersBySuffix(suffix);
reader = (ImageReader)readers.next();
}
reader.setInput(iis,true);
return reader;
}
/** {@inheritDoc} */
public ShellListViewModel getViewModel() {
return dataModel;
}
/**
* Sets the index selection model for this component to <code>selectionModel</code>
* and registers for listener notifications from the new selection model.
*
* @param selectionModel the new selection model
* @exception IllegalArgumentException if <code>newModel</code> is <code>null</code>
* @see #getSelectionModel()
*/
public void setSelectionModel(ListSelectionModel selectionModel) {
if (selectionModel == null) {
throw new IllegalArgumentException("selectionModel must be non null");
}
this.selectionModel = selectionModel;
this.selectionModel.addListSelectionListener(new javax.swing.event.ListSelectionListener() {
public void valueChanged(ListSelectionEvent e) {
onListSelectionChanged(e);
}
});
}
/**
* Returns the value of the current selection model. The selection
* model handles the task of making single selections, selections
* of contiguous ranges, and non-contiguous selections.
*
* @return the <code>ListSelectionModel</code> that implements
* list selections
*/
public ListSelectionModel getSelectionModel() {
return selectionModel;
}
/**
* Returns true if the specified index is selected.
* This is a convenience method that just delegates to the
* <code>selectionModel</code>.
*
* @param index index to be queried for selection state
* @return true if the specified index is selected
* @see ListSelectionModel#isSelectedIndex
*/
public boolean isSelectedIndex(int index) {
return getSelectionModel().isSelectedIndex(index);
}
/** calculates the preferred size to arrange the icon
*/
protected void calculatePreferredSize() {
if ( (cells.size() > 0) && (this.isVisible()) ) {
ThumbnailCell cell = cells.get(0);
double width = getVisibleRect().getWidth();
columnCount = (int)(width / (cell.getPreferredSize().getWidth() + flowLayout.getVgap()));
if (columnCount > 0) {
int row = (int)(cells.size()/columnCount) + 1;
double height = row * (cell.getPreferredSize().getHeight() + flowLayout.getHgap());
Dimension dim = new Dimension((int)width,(int)height);
setPreferredSize(dim);
}
}
}
/** Returns true if a cell is being edited.
* @return true if editing a cell
*/
public boolean isEditing() {
boolean b = false;
ShellListViewModel dataModel = getViewModel();
if (dataModel.getState() == ShellListViewModel.EDIT) { b = true; }
return b;
}
/** Returns the <code>ThumbnailCellEditor</code> used by this component to edit values for the cells.
* @return <code>ThumbnailCellEditor</code> used to edit values for the cells
*/
public ThumbnailCellEditor getCellEditor() {
return cellEditor;
}
/** set editing index
* @param index the editing index
* @see #getEditingIndex
*/
private void setEditingIndex(int index) {
editingIndex = index;
}
/** Returns the index of the model that contains the cell currently being edited.
* If nothing is being edited, returns -1.
* @return index or -1
*/
public int getEditingIndex() {
return editingIndex;
}
/** start editing cell programatically
* @param index the index of data model to be edited
* @return true if editing is started; false otherwise
* @throws IndexOutOfBoundsException if an invalid index was given
*/
public boolean editCellAt(int index) {
boolean b = false;
if ( (index < 0) || (index >= dataModel.getRowCount()) ) {
throw new IndexOutOfBoundsException("invalid index");
}
selectionModel.clearSelection();
selectionModel.setSelectionInterval(index,index);
setEditingIndex(index);
ShellListViewModel dataModel = getViewModel();
Object value = dataModel.getFile(index);
JComponent editorComp = cellEditor.getThumbnailCellEditorComponent(this, value, true, index);
ThumbnailCell cell = cells.get(index);
cell.setCellEditor(editorComp);
cell.startCellEditing();
cell.validate();
cell.repaint();
b = true;
return b;
}
/**Discards the editor object and frees the real estate it used for
* cell rendering.
*/
public void removeEditor() {
if ((editingIndex>=0) && (editingIndex<cells.size())) {
ThumbnailCell cell = cells.get(editingIndex);
cell.stopCellEditing();
cell.removeCellEditor();
editingIndex = -1;
cell.validate();
}
}
/** Invoked when editing is canceled.
* @param evt the event received
* @see DetailView#editingCanceled(ChangeEvent)
*/
public void editingCanceled(ChangeEvent evt) {
ShellListViewModel dataModel = getViewModel();
dataModel.setState(ShellListViewModel.BROWSE);
removeEditor();
}
/** Invoked when editing is stopped.
* @param evt the event received
* @see DetailView#editingStopped(ChangeEvent)
*/
public void editingStopped(ChangeEvent evt) {
ShellListViewModel dataModel = getViewModel();
dataModel.setState(ShellListViewModel.BROWSE);
removeEditor();
}
/** Returns the preferred size of the viewport for a view component.
* For example the preferredSize of a JList component is the size
* required to accommodate all of the cells in its list however the
* value of preferredScrollableViewportSize is the size required for
* JList.getVisibleRowCount() rows. A component without any properties
* that would effect the viewport size should just return
* getPreferredSize() here.
*
* @return The preferredSize of a JViewport whose view is this Scrollable.
* @see JViewport#getPreferredSize
*
*/
public Dimension getPreferredScrollableViewportSize() {
return getPreferredSize();
}
/** Components that display logical rows or columns should compute
* the scroll increment that will completely expose one block
* of rows or columns, depending on the value of orientation.
* <p>
* Scrolling containers, like JScrollPane, will use this method
* each time the user requests a block scroll.
*
* @param visibleRect The view area visible within the viewport
* @param orientation Either SwingConstants.VERTICAL or SwingConstants.HORIZONTAL.
* @param direction Less than zero to scroll up/left, greater than zero for down/right.
* @return The "block" increment for scrolling in the specified direction.
* This value should always be positive.
* @see JScrollBar#setBlockIncrement
*
*/
public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
int i = 64;
return i;
}
/** Return true if a viewport should always force the height of this
* Scrollable to match the height of the viewport. For example a
* columnar text view that flowed text in left to right columns
* could effectively disable vertical scrolling by returning
* true here.
* <p>
* Scrolling containers, like JViewport, will use this method each
* time they are validated.
*
* @return True if a viewport should force the Scrollables height to match its own.
*
*/
public boolean getScrollableTracksViewportHeight() {
return false;
}
/** Return true if a viewport should always force the width of this
* <code>Scrollable</code> to match the width of the viewport.
* For example a normal
* text view that supported line wrapping would return true here, since it
* would be undesirable for wrapped lines to disappear beyond the right
* edge of the viewport. Note that returning true for a Scrollable
* whose ancestor is a JScrollPane effectively disables horizontal
* scrolling.
* <p>
* Scrolling containers, like JViewport, will use this method each
* time they are validated.
*
* @return True if a viewport should force the Scrollables width to match its own.
*
*/
public boolean getScrollableTracksViewportWidth() {
return true;
}
/** Components that display logical rows or columns should compute
* the scroll increment that will completely expose one new row
* or column, depending on the value of orientation. Ideally,
* components should handle a partially exposed row or column by
* returning the distance required to completely expose the item.
* <p>
* Scrolling containers, like JScrollPane, will use this method
* each time the user requests a unit scroll.
*
* @param visibleRect The view area visible within the viewport
* @param orientation Either SwingConstants.VERTICAL or SwingConstants.HORIZONTAL.
* @param direction Less than zero to scroll up/left, greater than zero for down/right.
* @return The "unit" increment for scrolling in the specified direction.
* This value should always be positive.
* @see JScrollBar#setUnitIncrement
*
*/
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
return getScrollableBlockIncrement(visibleRect,orientation,direction);
}
/** Sets a flag indicating whether a cache file should be used for storing thumbnail
* if useCache is true make sure getCacheDirectory is not null by setCacheDirectory
*@param useCache boolean indicating whether a cache file should be used.
*@see #isUseCache
*@see #setCacheDirectory(File)
*@see #getCacheDirectory
*/
public void setUseCache(boolean useCache) {
this.useCache = useCache;
}
/** Returns the current value set by setUseCache.
*@return true if use cache, otherwise false
*@see #setUseCache(boolean)
*@see #setCacheDirectory(File)
*@see #getCacheDirectory
*/
public boolean isUseCache() {
return this.useCache;
}
/** Sets the directory where cache files are to be created.
*If getUseCache returns false, this value is ignored.
*@param cacheDirectory a File specifying a directory.
*@throw IllegalArgumentException if cacheDirectory is not a directory.
*@see #setUseCache(boolean)
*@see #isUseCache
*@see #getCacheDirectory
*/
public void setCacheDirectory(File cacheDirectory) {
if (cacheDirectory.isDirectory() == false) {
throw new IllegalArgumentException(cacheDirectory.toString() + " is not a directory.");
}
this.cacheDirectory = cacheDirectory;
}
/** Returns the current value set by setCacheDirectory, or null if no explicit setting has been made.
*@return a File indicating the directory where cache files will be created
*@see #setUseCache(boolean)
*@see #isUseCache
*@see #setCacheDirectory(boolean)
*/
public File getCacheDirectory() {
return this.cacheDirectory;
}
/** set ImageReaderWriterPreferences for thumbnail encoding and decoding
* @param prefs ImageReaderWriterPreferences
* @see #getImageReaderWriterPreferences(ImageReaderWriterPreferences)
* @see kiyut.imageio.ImageReaderWriterPreferences
*/
public void setImageReaderWriterPreferences(ImageReaderWriterPreferences prefs) {
this.imageReaderWriterPreferences = prefs;
}
/** Return ImageReaderWriterPreferences or null if not set
* @return ImageReaderWriterPreferences or null
* @see #setImageReaderWriterPreferences(ImageReaderWriterPreferences)
* @see kiyut.imageio.ImageReaderWriterPreferences
*/
public ImageReaderWriterPreferences getImageReaderWriterPreferences() {
return imageReaderWriterPreferences;
}
//////////////////////////
// event handling
/////////////////////////
/** Invoked when the mouse button has been clicked (pressed and released) on this component.
* @param evt a <code>MouseEvent</code> object
*/
private void onMouseClicked(MouseEvent evt) {
if (isEnabled() == false) { return; }
requestFocusInWindow();
if (evt.getClickCount() > 1) {
return;
}
Component cell = getComponentAt(evt.getX(),evt.getY());
if ( (evt.getClickCount() == 1) && (cell != null) ) {
int index = cells.indexOf(cell);
if (evt.isShiftDown()) {
selectionModel.addSelectionInterval(selectionModel.getLeadSelectionIndex(),index);
} else if (evt.isControlDown()) {
selectionModel.addSelectionInterval(index,index);
} else {
//selectionModel.clearSelection();
selectionModel.setSelectionInterval(index, index);
}
}
}
/** Invoked when a key has been pressed on this component
* @param evt a <code>KeyEvent</code> object
*/
private void onKeyPressed(KeyEvent evt) {
ShellListViewModel dataModel = getViewModel();
if (dataModel.getState()!=ShellListViewModel.BROWSE) { return; }
int index = getSelectionModel().getMaxSelectionIndex();
int keyCode = evt.getKeyCode();
if (keyCode==KeyEvent.VK_RIGHT) {
index = index + 1;
} else if (keyCode==KeyEvent.VK_LEFT) {
index = index - 1;
} else if (keyCode==KeyEvent.VK_UP) {
index = index - columnCount;
} else if (keyCode==KeyEvent.VK_DOWN) {
index = index + columnCount;
} else {
return;
}
if ((index<0) || (index >= cells.size())) {
return;
}
Component cell = (Component)cells.get(index);
MouseEvent mouseEvent = new MouseEvent(this,MouseEvent.MOUSE_CLICKED,
evt.getWhen(),evt.getModifiers(),cell.getX(),cell.getY(),1,false,MouseEvent.BUTTON1);
onMouseClicked(mouseEvent);
}
/** Sent when the contents of the data model has changed in
* a way that's too complex to characterize.
* For example, this is sent when an item has been replaced.
* Index0 and index1 bracket the change.
* @param e a <code>ListDataEvent</code> encapsulating the event information
*/
private void onContentsChanged(ListDataEvent e) {
// changeAll
if ((e.getIndex0()==ShellListViewModel.ALL_INDEX) && (e.getIndex1()==ShellListViewModel.ALL_INDEX) ){
refresh();
return;
}
FileSystemView fsv = dataModel.getFileSystemView();
for(int i=e.getIndex0(); i<=e.getIndex1(); i++) {
File file = (File)dataModel.getFile(i);
ThumbnailCell cell = cells.get(i);
updateCell(cell,file,fsv);
loadList.add(file);
}
/*int index = e.getIndex1();
if (dataModel.getSize() >= index) {
getSelectionModel().setSelectionInterval(index,index);
}*/
synchronized(loadList) {
loadList.notifyAll();
}
}
/** Sent after the indices in the index0,index1 interval have been inserted in the data model.
* The new interval includes both index0 and index1.
* @param e a <code>ListDataEvent</code> encapsulating the event information
*/
private void onIntervalAdded(ListDataEvent e) {
FileSystemView fsv = dataModel.getFileSystemView();
for(int i=e.getIndex0(); i<=e.getIndex1(); i++) {
File file = (File)dataModel.getFile(i);
ThumbnailCell cell = createCell(file,fsv);
this.add(cell,i);
cells.add(i, cell);
}
calculatePreferredSize();
revalidate();
repaint();
}
/** Sent after the indices in the index0,index1 interval have been removed from the data model.
* The interval includes both index0 and index1.
* @param e a <code>ListDataEvent</code> encapsulating the event information
*/
private void onIntervalRemoved(ListDataEvent e) {
for(int i=e.getIndex0(); i<=e.getIndex1(); i++) {
ThumbnailCell cell = cells.remove(i);
this.remove(cell);
}
calculatePreferredSize();
revalidate();
repaint();
}
/** Called whenever the value of the selection changes.
* @param e the event that characterizes the change.
*/
private void onListSelectionChanged(ListSelectionEvent e) {
cellEditor.cancelCellEditing();
if (!selectionModel.isSelectionEmpty()) {
Component comp = (Component)cells.get(selectionModel.getMaxSelectionIndex());
scrollRectToVisible(comp.getBounds());
}
for (int i=0; i<cells.size(); i++) {
ThumbnailCell cell = cells.get(i);
if (selectionModel.isSelectedIndex(i)) {
cell.setCellHasFocus(true);
} else {
cell.setCellHasFocus(false);
}
cell.repaint();
}
}
/** thumbnail thread
*/
private class ThumbnailThread extends Thread {
/** run this thread */
public void run() {
while(true) {
try {
synchronized(loadList) {
while ( (loadList.size()==0) || (!isEnabled()) )
loadList.wait();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
while ((loadList.size() > 0) && (isEnabled())) {
try {
File file = (File)loadList.remove(0);
int indexOf = dataModel.indexOf(file);
if (indexOf != -1) {
ThumbnailCell cell = cells.get(indexOf);
if (ImageUtilities.isFileImage(file)) {
loadFileImage(cell,file);
this.yield();
}
}
} catch(Exception e) {}
}
}
}
}
/** Property Change Handler for this component */
private class PropertyChangeHandler implements PropertyChangeListener {
/** This method gets called when a bound property is changed.
* @param e A <code>PropertyChangeEvent</code> object describing the event source
* and the property that has changed.
*/
public void propertyChange(PropertyChangeEvent e) {
String propertyName = e.getPropertyName();
if (propertyName.equals("selectionBackground") || propertyName.equals("selectionForeground") ) {
repaint();
}
}
}
protected class ThumbnailViewDragGestureRecognizer extends CustomDragGestureRecognizer {
/** {@inheritDoc} */
protected boolean isDragPossible(MouseEvent evt) {
if (super.isDragPossible(evt)) {
ThumbnailView comp = (ThumbnailView)evt.getSource();
if (comp.getDragEnabled()) {
Component cell = getComponentAt(evt.getX(),evt.getY());
int index = cells.indexOf(cell);
if ((index != -1) && comp.isSelectedIndex(index)) {
return true;
}
}
}
return false;
}
}
protected class ThumbnailViewDropTargetListener extends CustomDropTargetListener {
private int[] selectedIndices;
/** {@inheritDoc} */
protected void saveComponentState(JComponent comp) {
ThumbnailView viewComp = (ThumbnailView)comp;
ShellListViewSelectionModel sm = (ShellListViewSelectionModel)viewComp.getSelectionModel();
selectedIndices = sm.getSelectedIndices();
}
/** {@inheritDoc} */
protected void restoreComponentState(JComponent comp) {
ThumbnailView viewComp = (ThumbnailView)comp;
ShellListViewSelectionModel sm = (ShellListViewSelectionModel)viewComp.getSelectionModel();
sm.setSelectedIndices(selectedIndices);
}
/** {@inheritDoc} */
protected void updateInsertionLocation(JComponent comp, Point p) {
ThumbnailView viewComp = (ThumbnailView)comp;
Component cell = getComponentAt((int)p.getX(),(int)p.getY());
int index = cells.indexOf(cell);
if (index != -1) {
viewComp.getSelectionModel().setSelectionInterval(index, index);
}
}
}
}