/*
* 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 javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.TableColumnModelEvent;
import javax.swing.event.TableColumnModelListener;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.*;
/**
* Used to keep track of a file table's columns position and visibility settings.
* @author Nicolas Rinaudo, Maxence Bernard
*/
public class FileTableColumnModel implements TableColumnModel, PropertyChangeListener {
// - Class constants -----------------------------------------------------------------
// -----------------------------------------------------------------------------------
/** If {@link #widthCache} is set to this, it needs to be recalulated. */
private static final int CACHE_OUT_OF_DATE = -1;
/** Even though we're not using column selection, the table API forces us to return this instance or will crash. */
private static final ListSelectionModel SELECTION_MODEL = new DefaultListSelectionModel();
// - Instance fields -----------------------------------------------------------------
// -----------------------------------------------------------------------------------
/** All registered listeners. */
private WeakHashMap<TableColumnModelListener, ?> listeners = new WeakHashMap<TableColumnModelListener, Object>();
/** Cache for the table's total width. */
private int widthCache = CACHE_OUT_OF_DATE;
/** All available columns. */
private List<TableColumn> columns = new Vector<TableColumn>(Column.values().length);
/** Enabled state of each column. */
private boolean[] enabled = new boolean[Column.values().length];
/** Visibility state of each column. */
private boolean[] visibility = new boolean[Column.values().length];
/** Cache for the number of available columns. */
private int countCache;
/** Whether the column sizes were set already. */
private boolean columnSizesSet;
// - Initialization ------------------------------------------------------------------
// -----------------------------------------------------------------------------------
/**
* Creates a new file table column model.
*/
public FileTableColumnModel(FileTableConfiguration conf) {
TableColumn column; // Buffer for the current column.
int columnIndex;
// The name column is always visible, so we know that the column count is always
// at least 1.
countCache = 1;
// Initializes the columns.
for(Column c : Column.values()) {
columnIndex = c.ordinal();
columns.add(column = new TableColumn(columnIndex));
column.setCellEditor(null);
column.setHeaderValue(c.getLabel());
// Mac OS X 10.5 (Leopard) and up uses JTableHeader properties to render sort indicators on table headers.
// On other platforms, we use a custom table header renderer.
if(!FileTable.usesTableHeaderRenderingProperties()) {
column.setHeaderRenderer(new FileTableHeaderRenderer());
}
column.addPropertyChangeListener(this);
// Sets the column's initial width.
if(conf.getWidth(c) != 0)
column.setWidth(conf.getWidth(c));
// Initialises the column's visibility and minimum width.
if(c == Column.NAME) {
enabled[columnIndex] = true;
}
else {
if((enabled[columnIndex] = conf.isEnabled(c)))
countCache++;
}
column.setMinWidth(c.getMinimumColumnWidth());
// Init visibility state to enabled state, FileTable will adjust the values for conditional columns later
visibility[columnIndex] = enabled[columnIndex];
}
// Sorts the columns.
Collections.sort(columns, new ColumnSorter(conf));
}
// - Configuration -------------------------------------------------------------------
// -----------------------------------------------------------------------------------
public synchronized FileTableConfiguration getConfiguration() {
FileTableConfiguration conf;
TableColumn column;
int modelCIndex;
Column modelC;
conf = new FileTableConfiguration();
for(Column c : Column.values()) {
column = columns.get(c.ordinal());
modelC = Column.valueOf(column.getModelIndex());
modelCIndex = modelC.ordinal();
conf.setEnabled(modelC, enabled[modelCIndex]);
conf.setPosition(modelC, c.ordinal());
conf.setWidth(modelC, column.getWidth());
}
return conf;
}
// - Enabled state -------------------------------------------------------------------
// -----------------------------------------------------------------------------------
/**
* Returns <code>true</code> if the specified column is enabled.
* @param column column, see {@link com.mucommander.ui.main.table.Column} for possible values
* @return true if the column is enabled, false if disabled.
*/
public synchronized boolean isColumnEnabled(Column column) {
return enabled[column.ordinal()];
}
/**
* Sets the specified column's enabled state.
* @param column column, see {@link com.mucommander.ui.main.table.Column} for possible values
* @param enabled true to enable the column, false to disable it.
*/
public synchronized void setColumnEnabled(Column column, boolean enabled) {
this.enabled[column.ordinal()] = enabled;
}
// - Visibility state ----------------------------------------------------------------
// -----------------------------------------------------------------------------------
/**
* Sets the specified column's visibility state.
* @param column column, see {@link com.mucommander.ui.main.table.Column} for possible values
* @param visible whether the column should be visible or not.
*/
synchronized void setColumnVisible(Column column, boolean visible) {
// Ignores calls that won't actually change anything.
int columnVal = column.ordinal();
if(visibility[columnVal] != visible) {
visibility[columnVal] = visible;
widthCache = CACHE_OUT_OF_DATE;
// Adds the column.
if(visible) {
countCache++;
triggerColumnAdded(new TableColumnModelEvent(this, columnVal, columnVal));
}
// Removes the column.
else {
countCache--;
triggerColumnRemoved(new TableColumnModelEvent(this, columnVal, columnVal));
}
}
}
/**
* Returns <code>true</code> if the specified column is visible.
* @param column column, see {@link com.mucommander.ui.main.table.Column} for possible values
* @return <code>true</code> if the specified column is visible, <code>false</code> otherwise.
*/
public synchronized boolean isColumnVisible(Column column) {return visibility[column.ordinal()];}
/**
* Adds the specified column to the model.
* @param column column to add to the model.
*/
public void addColumn(TableColumn column) {setColumnVisible(Column.valueOf(column.getModelIndex()), true);}
/**
* Removes the specified column from the model.
* @param column column to remove from the model.
*/
public void removeColumn(TableColumn column) {setColumnVisible(Column.valueOf(column.getModelIndex()), false);}
// - Column retrieval ----------------------------------------------------------------
// -----------------------------------------------------------------------------------
private synchronized int getInternalIndex(int index) {
int visibleIndex;
TableColumn column;
// Looks for the visible column of index 'index'.
visibleIndex = -1;
for(int i = 0; i < visibility.length; i++) {
column = columns.get(i);
if(visibility[column.getModelIndex()])
if(++visibleIndex == index)
return i;
}
// Index doesn't exist.
throw new ArrayIndexOutOfBoundsException(Integer.toString(index));
}
/**
* Returns the specified column.
* @param index index of the column in the model.
* @return the requested column.
*/
public synchronized TableColumn getColumn(int index) {return columns.get(getInternalIndex(index));}
public synchronized TableColumn getColumnFromId(int id) {return columns.get(id);}
public synchronized int getColumnPosition(int id) {
for(int i = 0; i < visibility.length; i++)
if(columns.get(i).getModelIndex() == id)
return i;
return -1;
}
/**
* Returns the number of columns currently displayed.
* @return the number of columns currently displayed.
*/
public synchronized int getColumnCount() {return countCache;}
/**
* Moves a column.
* @param from index of the column to move.
* @param to where to move the column.
*/
public synchronized void moveColumn(int from, int to) {
// We need to trigger these for the file table to display the 'column dragging' animation.
if(from == to) {
triggerColumnMoved(new TableColumnModelEvent(this, from, to));
return;
}
TableColumn column; // Buffer for the table to remove.
int index; // Used to store the internal index of 'from' and 'to'.
// Locates the internal index of the requested column
// and removes that column
index = getInternalIndex(from);
column = columns.get(index);
columns.remove(index);
// If the column needs to be moved at the end of the set,
// no need to locate its correct index.
if(to == countCache - 1)
columns.add(column);
// Otherwise, finds the column's internal index and inserts
// it there.
else {
index = getInternalIndex(to);
columns.add(index, column);
}
// Notifies listeners and stores the new configuration.
triggerColumnMoved(new TableColumnModelEvent(this, from, to));
}
public int getColumnIndex(Object identifier) {return 0;}
// - Columns width -------------------------------------------------------------------
// -----------------------------------------------------------------------------------
/**
* Returns the index of the column at the specified position.
* @param x position of the column to look for.
* @return the index of the column at the specified position, <code>-1</code> if not found.
*/
public int getColumnIndexAtX(int x) {
int count;
count = getColumnCount();
for(int i = 0; i < count; i++) {
x = x - getColumn(i).getWidth();
if(x < 0)
return i;
}
return -1;
}
/**
* Returns the total width of the table column model.
* @return the total width of the table column model.
*/
public int getTotalColumnWidth() {
if(widthCache == CACHE_OUT_OF_DATE)
computeWidthCache();
return widthCache;
}
/**
* Computes the model's width.
*/
private void computeWidthCache() {
Enumeration<TableColumn> elements;
elements = getColumns();
widthCache = 0;
while(elements.hasMoreElements())
widthCache += elements.nextElement().getWidth();
}
/**
* Invalidates the width cache if a column's width has changed.
*/
public void propertyChange(PropertyChangeEvent event) {
String name;
name = event.getPropertyName();
if(name.equals("width")) {
columnSizesSet = true;
widthCache = CACHE_OUT_OF_DATE;
// Notifies the table that columns width have changed and that it should repaint itself.
triggerColumnMarginChanged(new ChangeEvent(this));
}
}
boolean wereColumnSizesSet() {return columnSizesSet;}
// - Columns margin ------------------------------------------------------------------
// -----------------------------------------------------------------------------------
// Column margin is fixed to 0.
/**
* Returns 0.
* @return 0.
*/
public int getColumnMargin() {return 0;}
/**
* Ignored.
*/
public void setColumnMargin(int margin) {}
// - Listeners -----------------------------------------------------------------------
// -----------------------------------------------------------------------------------
/**
* Registers the specified column model listener.
* @param listener listener to register.
*/
public void addColumnModelListener(TableColumnModelListener listener) {listeners.put(listener, null);}
/**
* Removes the specified column model listener.
* @param listener listener to remove.
*/
public void removeColumnModelListener(TableColumnModelListener listener) {listeners.remove(listener);}
/**
* Calls all registered listeners' <code>columnAdded(event)</code>.
* @param event event to propagate.
*/
private void triggerColumnAdded(TableColumnModelEvent event) {
for(TableColumnModelListener listener: listeners.keySet())
listener.columnAdded(event);
}
/**
* Calls all registered listeners' <code>columnMarginChanged(event)</code>.
* @param event event to propagate.
*/
private void triggerColumnMarginChanged(ChangeEvent event) {
for(TableColumnModelListener listener: listeners.keySet())
listener.columnMarginChanged(event);
}
/**
* Calls all registered listeners' <code>columnMoved(event)</code>.
* @param event event to propagate.
*/
private void triggerColumnMoved(TableColumnModelEvent event) {
for(TableColumnModelListener listener: listeners.keySet())
listener.columnMoved(event);
}
/**
* Calls all registered listeners' <code>columnRemoved(event)</code>.
* @param event event to propagate.
*/
private void triggerColumnRemoved(TableColumnModelEvent event) {
for(TableColumnModelListener listener: listeners.keySet())
listener.columnRemoved(event);
}
// - Column selection ----------------------------------------------------------------
// -----------------------------------------------------------------------------------
// Column selection is not allowed, this methods are ignored or return default values.
/**
* Returns <code>false</code>.
* @return <code>false</code>.
*/
public boolean getColumnSelectionAllowed() {return false;}
/**
* Returns <code>0</code>.
* @return <code>0</code>.
*/
public int getSelectedColumnCount() {return 0;}
/**
* Returns an integer array of size 0.
* @return an integer array of size 0.
*/
public int[] getSelectedColumns() {return new int[0];}
/**
* Ignored.
*/
public void setColumnSelectionAllowed(boolean flag) {}
/**
* Returns a default list selection model.
* <p>
* Ideally, we'd like to return <code>null</code> here, but the table API takes a dim view
* of this and we're forced to keep a useless reference.
* </p>
* @return a default list selection model.
*/
public ListSelectionModel getSelectionModel() {return SELECTION_MODEL;}
/**
* Ignored.
*/
public void setSelectionModel(ListSelectionModel model) {}
// - Column enumeration --------------------------------------------------------------
// -----------------------------------------------------------------------------------
/**
* Returns an enumeration on all visible columns.
* @return an enumeration on all visible columns.
*/
public Enumeration<TableColumn> getColumns() {
return new ColumnEnumeration();
}
public Iterator<TableColumn> getAllColumns() {
return columns.iterator();
}
/**
* Browses through the model's visible columns
* <p>
* This will enumerate all the elements of {@link FileTableColumnModel#columns}, skipping
* over any that's marked as invisible.
* </p>
* @author Nicolas Rinaudo
*/
private class ColumnEnumeration implements Enumeration<TableColumn> {
// - Instance fields -------------------------------------------------------------
// -------------------------------------------------------------------------------
/** Index of the next available element in the enumeration. */
private int nextIndex;
// - Initialisation --------------------------------------------------------------
// -------------------------------------------------------------------------------
/**
* Creates a new column enumeration.
*/
public ColumnEnumeration() {
nextIndex = -1;
findNextElement();
}
/**
* Finds the next visible element in the model.
*/
private void findNextElement() {
TableColumn column;
for(nextIndex++; nextIndex < visibility.length; nextIndex++) {
column = columns.get(nextIndex);
if(visibility[column.getModelIndex()])
break;
}
}
// - Enumeration methods ---------------------------------------------------------
// -------------------------------------------------------------------------------
/**
* Returns <code>true</code> if there's a next element in the enumeration.
* @return <code>true</code> if there's a next element in the enumeration, <code>false</code> otherwise.
*/
public boolean hasMoreElements() {return nextIndex < visibility.length;}
/**
* Returns the next element in the enumeration.
* @return the next element in the enumeration.
* @throws NoSuchElementException if there is no next element in the enumeration.
*/
public TableColumn nextElement() {
// Makes sure we have at least one more element to return.
if(!hasMoreElements())
throw new NoSuchElementException();
// Retrieves the next element.
TableColumn column;
column = columns.get(nextIndex);
// Looks for the next one.
findNextElement();
return column;
}
}
// - Column sorting ------------------------------------------------------------------
// -----------------------------------------------------------------------------------
/**
* Used to sort the model's columns at boot time.
* <p>
* The sort is done by first comparing each column's index as defined in the configuration and,
* if there's a conflict, by comparing each column's identifier.
* </p>
* @author Nicolas Rinaudo
*/
private static class ColumnSorter implements Comparator<TableColumn> {
// - Instance fields -------------------------------------------------------------
// -------------------------------------------------------------------------------
/** Defines the columns order. */
private FileTableConfiguration conf;
// - Initialisation --------------------------------------------------------------
// -------------------------------------------------------------------------------
/**
* Loads the columns order as defined in the configuration.
*/
public ColumnSorter(FileTableConfiguration conf) {this.conf = conf;}
// - Comparator code -------------------------------------------------------------
// -------------------------------------------------------------------------------
/**
* Compares <code>o1</code> and <code>o2</code>.
*/
public int compare(TableColumn tc1, TableColumn tc2) {
int id1; // Identifier of the first column.
int id2; // Identifier of the second column.
int index1; // Index of the first column as defined in the configuration.
int index2; // Index of the second column as defined in the configuration.
// Retrieves the two columns' indexes and identifiers.
index1 = conf.getPosition(Column.valueOf(id1 = tc1.getModelIndex()));
index2 = conf.getPosition(Column.valueOf(id2 = tc2.getModelIndex()));
// Sort by index, then by identifier.
if(index1 < index2)
return -1;
if(index1 == index2) {
if(id1 < id2)
return -1;
if (id1 == id2)
return 0;
}
return 1;
}
}
}