/*
* IconView.java
*
* Created on December 14, 2002, 2:36 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.util.List;
import java.util.ArrayList;
import java.util.Vector;
import java.util.TooManyListenersException;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.filechooser.FileSystemView;
import kiyut.swing.dnd.*;
import kiyut.swing.shell.event.*;
import kiyut.swing.shell.image.ImageUtilities;
import kiyut.swing.shell.util.*;
/** <code>IconView</code> is a view for <code>ShellListView</code> which display
* the <code>ShellListViewModel</code> in an icon like view.
*
* @author tonny
*/
public class IconView 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<IconCell> cells;
/** the editor for the cells */
protected IconCellEditor cellEditor;
/** Identifies the index of the cell being edited. */
protected int editingIndex = -1;
/** 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;
/** Constructs a <code>IconView</code> using the given data model
* @param dataModel data model for this component
*/
public IconView(ShellListViewModel dataModel) {
cells = new ArrayList<IconCell>();
cellEditor = new IconCellEditor();
cellEditor.addCellEditorListener(new javax.swing.event.CellEditorListener() {
public void editingCanceled(ChangeEvent evt) { IconView.this.editingCanceled(evt); }
public void editingStopped(ChangeEvent evt) { IconView.this.editingStopped(evt); }
});
this.dataModel = dataModel;
dataModel.addListDataListener(new ListDataListener() {
public void contentsChanged(ListDataEvent evt) { onContentsChanged(evt); }
public void intervalAdded(ListDataEvent evt) { onIntervalAdded(evt); }
public void intervalRemoved(ListDataEvent evt) { onIntervalRemoved(evt); }
});
addComponentListener(new ComponentAdapter() {
public void componentResized(ComponentEvent evt) { calculatePreferredSize(); }
});
// DnD Support
setTransferHandler(new TransferHandler(""));
IconViewDragGestureRecognizer dragRecognizer = new IconViewDragGestureRecognizer();
addMouseListener(dragRecognizer);
addMouseMotionListener(dragRecognizer);
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"));
}
/** Overriden to provide add Custom dropTargetListener
* {@inheritDoc}
*/
public void setTransferHandler(TransferHandler newHandler) {
super.setTransferHandler(newHandler);
try {
getDropTarget().addDropTargetListener(new IconViewDropTargetListener());
} 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);
}
/** {@inheritDoc} */
public void setDragEnabled(boolean b) {
if (b && GraphicsEnvironment.isHeadless()) {
throw new HeadlessException();
}
dragEnabled = b;
}
/** {@inheritDoc} */
public boolean getDragEnabled() {
return dragEnabled;
}
/** refresh the data model */
protected void refresh() {
this.removeAll();
this.repaint();
selectionModel.clearSelection();
cells.clear();
FileSystemView fsv = dataModel.getFileSystemView();
for (int i=0; i < dataModel.getSize(); i++) {
File file = (File)dataModel.getFile(i);
IconCell cell = createCell(file,fsv);
cells.add(cell);
this.add(cell);
}
// set preferredSize
calculatePreferredSize();
revalidate();
repaint();
}
/** Returns an <code>IconCell</code> instance using the given parameter
* @param file File
* @param fsv FileSystemView
* @return an <code>IconCell</code> instance.
*/
protected IconCell createCell(File file,FileSystemView fsv) {
IconCell cell = new IconCell(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(IconCell cell,File file,FileSystemView fsv) {
cell.setText(fsv.getSystemDisplayName(file));
cell.setImageRescale(1.5, 1.5);
cell.setImage(ImageUtilities.iconToBufferedImage(fsv.getSystemIcon(file)));
}
/** {@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("Cannot set a null SelectionModel");
}
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
* @see #setSelectionModel(ListSelectionModel)
* @see ListSelectionModel
*/
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
*/
private void calculatePreferredSize() {
if ( (cells.size() > 0) && (this.isVisible()) ) {
IconCell 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>IconCellEditor</code> used by this component to edit values for the cells.
* @return <code>IconCellEditor</code> used to edit values for the cells
*/
public IconCellEditor getCellEditor() {
return cellEditor;
}
/** set editing index
*@param index the editing index
*/
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.getIconCellEditorComponent(this, value, true, index);
IconCell 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.
*/
protected void removeEditor() {
if ((editingIndex>=0) && (editingIndex<cells.size())) {
IconCell 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 = 16;
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);
}
//////////////////////////
// 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) {
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 a 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;
}
IconCell cell = 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 evt a <code>ListDataEvent</code> encapsulating the event information
*/
private void onContentsChanged(ListDataEvent evt) {
// changeAll
if ((evt.getIndex0()==ShellListViewModel.ALL_INDEX) && (evt.getIndex1()==ShellListViewModel.ALL_INDEX) ){
refresh();
return;
}
FileSystemView fsv = dataModel.getFileSystemView();
for(int i=evt.getIndex0(); i<=evt.getIndex1(); i++) {
File file = (File)dataModel.getFile(i);
IconCell cell = cells.get(i);
updateCell(cell,file,fsv);
cell.repaint();
}
}
/** 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 evt a <code>ListDataEvent</code> encapsulating the event information
*/
private void onIntervalAdded(ListDataEvent evt) {
FileSystemView fsv = dataModel.getFileSystemView();
for(int i=evt.getIndex0(); i<=evt.getIndex1(); i++) {
File file = (File)dataModel.getFile(i);
IconCell 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 evt a <code>ListDataEvent</code> encapsulating the event information
*/
private void onIntervalRemoved(ListDataEvent evt) {
for(int i=evt.getIndex0(); i<=evt.getIndex1(); i++) {
IconCell cell = cells.remove(i);
this.remove(cell);
}
calculatePreferredSize();
revalidate();
repaint();
}
/** Called whenever the value of the selection changes.
* @param evt the event that characterizes the change.
*/
private void onListSelectionChanged(ListSelectionEvent evt) {
cellEditor.cancelCellEditing();
if (!selectionModel.isSelectionEmpty()) {
Component comp = (Component)cells.get(selectionModel.getMaxSelectionIndex());
scrollRectToVisible(comp.getBounds());
}
for (int i=0; i<cells.size(); i++) {
IconCell cell = cells.get(i);
if (selectionModel.isSelectedIndex(i)) {
cell.setCellHasFocus(true);
} else {
cell.setCellHasFocus(false);
}
cell.repaint();
}
}
/** Property Change Handler for this component */
protected class PropertyChangeHandler implements PropertyChangeListener {
/** {@inheritDoc} */
public void propertyChange(PropertyChangeEvent evt) {
String propertyName = evt.getPropertyName();
if (propertyName.equals("selectionBackground") || propertyName.equals("selectionForeground") ) {
repaint();
}
}
}
protected class IconViewDragGestureRecognizer extends CustomDragGestureRecognizer {
/** {@inheritDoc} */
protected boolean isDragPossible(MouseEvent evt) {
if (super.isDragPossible(evt)) {
IconView comp = (IconView)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 IconViewDropTargetListener extends CustomDropTargetListener {
private int[] selectedIndices;
/** {@inheritDoc} */
protected void saveComponentState(JComponent comp) {
IconView iconView = (IconView)comp;
ShellListViewSelectionModel sm = (ShellListViewSelectionModel)iconView.getSelectionModel();
selectedIndices = sm.getSelectedIndices();
}
/** {@inheritDoc} */
protected void restoreComponentState(JComponent comp) {
IconView iconView = (IconView)comp;
ShellListViewSelectionModel sm = (ShellListViewSelectionModel)iconView.getSelectionModel();
sm.setSelectedIndices(selectedIndices);
}
/** {@inheritDoc} */
protected void updateInsertionLocation(JComponent comp, Point p) {
IconView iconView = (IconView)comp;
Component cell = getComponentAt((int)p.getX(),(int)p.getY());
int index = cells.indexOf(cell);
if (index != -1) {
iconView.getSelectionModel().setSelectionInterval(index, index);
}
}
}
}