/* * Beanfabrics Framework Copyright (C) by Michael Karneim, beanfabrics.org * Use is subject to license terms. See license.txt. */ // TODO javadoc - remove this comment only when the class and all non-public // methods and fields are documented package org.beanfabrics.swing.table; import java.awt.Component; import java.awt.Container; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.HierarchyEvent; import java.awt.event.HierarchyListener; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JViewport; import javax.swing.ListSelectionModel; import javax.swing.event.ChangeEvent; import javax.swing.table.DefaultTableModel; import javax.swing.table.JTableHeader; import javax.swing.table.TableColumn; import javax.swing.table.TableModel; import org.beanfabrics.IModelProvider; import org.beanfabrics.Link; import org.beanfabrics.ModelSubscriber; import org.beanfabrics.Path; import org.beanfabrics.View; import org.beanfabrics.event.ElementsAddedEvent; import org.beanfabrics.event.ElementsRemovedEvent; import org.beanfabrics.event.ListAdapter; import org.beanfabrics.event.WeakListAdapter; import org.beanfabrics.log.Logger; import org.beanfabrics.log.LoggerFactory; import org.beanfabrics.model.AbstractPM; import org.beanfabrics.model.IListPM; import org.beanfabrics.model.PresentationModel; import org.beanfabrics.swing.table.celleditor.BnTableCellEditor; import org.beanfabrics.swing.table.cellrenderer.BnTableCellRenderer; /** * The <code>BnTable</code> is a {@link JTable} that can subscribe to an {@link IListPM}. * <p> * For an example about using BnTable, please see <a href="http://www.beanfabrics.org/index.php/BnTable" * target="parent">http://www.beanfabrics.org/index.php/BnTable</a> * </p> * * @author Michael Karneim * @beaninfo */ @SuppressWarnings({ "serial" }) public class BnTable extends JTable implements View<IListPM<? extends PresentationModel>>, ModelSubscriber { private final static Logger LOG = LoggerFactory.getLogger(BnTable.class); private final ListAdapter listListener = new MyWeakListAdapter(); private class MyWeakListAdapter extends WeakListAdapter implements Serializable { public void elementsAdded(ElementsAddedEvent evt) { cancelCellEditing(); } public void elementsRemoved(ElementsRemovedEvent evt) { cancelCellEditing(); } private void cancelCellEditing() { if (getCellEditor() != null) getCellEditor().cancelCellEditing(); } }; private final Link link = new Link(this); private IListPM<? extends PresentationModel> presentationModel; private List<BnColumn> columns = Collections.emptyList(); private boolean cellEditingAllowed; private boolean sortable; // Extensions private final AutoResizeExtension autoResizeExtension = createAutoResizeExtension(); public BnTable() { setSurrendersFocusOnKeystroke(true); setAutoResizeMode(JTable.AUTO_RESIZE_OFF); setCellEditingAllowed(true); setSortable(true); } /** * Returns whether or not this table allows cell editing. * * @return <code>true</code> if cell editing is allowed */ public boolean isCellEditingAllowed() { return cellEditingAllowed; } /** * Sets whether or not this table allows cell editing. * * @param newValue */ public void setCellEditingAllowed(boolean newValue) { boolean oldValue = this.cellEditingAllowed; this.cellEditingAllowed = newValue; if (isConnected()) { TableModel tblModel = getModel(); if (tblModel instanceof BnTableModel) { ((BnTableModel) tblModel).setCellEditingAllowed(this.cellEditingAllowed); } } this.firePropertyChange("cellEditingAllowed", oldValue, newValue); } /** * Returns wheter or not this table is sortable. * * @return <code>true</code> if this table is sortable */ public boolean isSortable() { return sortable; } /** * Sets whether or not this table is sortable. * * @param newValue */ public void setSortable(boolean newValue) { boolean oldValue = this.sortable; if (oldValue == newValue) { return; } this.sortable = newValue; if (isConnected()) { disconnect(); connect(); } this.firePropertyChange("sortable", oldValue, newValue); } /** {@inheritDoc} */ public IListPM<? extends PresentationModel> getPresentationModel() { return this.presentationModel; } /** {@inheritDoc} */ public void setPresentationModel(IListPM<? extends PresentationModel> newModel) { IListPM<? extends PresentationModel> oldModel = this.presentationModel; if (newModel == oldModel) { return; } disconnect(); this.presentationModel = newModel; connect(); this.firePropertyChange("presentationModel", oldModel, newModel); } /** {@inheritDoc} */ public IModelProvider getModelProvider() { return link.getModelProvider(); } /** {@inheritDoc} */ public void setModelProvider(IModelProvider provider) { this.link.setModelProvider(provider); } /** {@inheritDoc} */ public Path getPath() { return link.getPath(); } /** {@inheritDoc} */ public void setPath(Path path) { this.link.setPath(path); } /** * Returns whether or not this component is connected to the target {@link AbstractPM} to synchronize with. * * @return <code>true</code> if this component is connected */ private boolean isConnected() { return this.columns != null && this.presentationModel != null; } protected void connect() { if (this.columns == null ) { return; } if ( this.presentationModel == null) { // set dummy model, in order to show column headers even if PM is not bound. DefaultTableModel dummyModel = new DefaultTableModel(); for( BnColumn col: columns) { dummyModel.addColumn( col.getColumnName()); } setModel( dummyModel); return; } this.presentationModel.addListListener(listListener); this.setModel(new BnTableModel(presentationModel, this.columns, this.cellEditingAllowed)); if (isSortable()) { installSortingFeature(); } // now install the selection model int currentSelectionMode = getSelectionModel().getSelectionMode(); BnTableSelectionModel newModel = new BnTableSelectionModel(presentationModel); newModel.setSelectionMode(currentSelectionMode); this.setSelectionModel(newModel); this.autoResizeExtension.resizeColumns(); } private void installSortingFeature() { installRowSorter(); JTableHeader header = getTableHeader(); if (header instanceof Java5SortingTableHeader) { Java5SortingTableHeader headerJ5 = (Java5SortingTableHeader) header; headerJ5.setSortable(true); } } private void uninstallSortingFeature() { uninstallRowSorter(); JTableHeader header = getTableHeader(); if (header instanceof Java5SortingTableHeader) { Java5SortingTableHeader headerJ5 = (Java5SortingTableHeader) header; headerJ5.setSortable(false); } } private void installRowSorter() { // When intalling a row sorter in jre1.6 the selection model is cleared // To prevent this to change the presentation model we temporary install a dummy selection model ListSelectionModel oldSelModel = getSelectionModel(); int oldSelectionMode = oldSelModel.getSelectionMode(); setSelectionModel(createDefaultSelectionModel()); setSelectionMode(oldSelectionMode); BnTableRowSorter.install(this); // reset the temporary selection model setSelectionModel(oldSelModel); } private void uninstallRowSorter() { BnTableRowSorter.uninstall(this); } @Override protected void createDefaultRenderers() { super.createDefaultRenderers(); setDefaultRenderer(PresentationModel.class, new BnTableCellRenderer()); } @Override protected void createDefaultEditors() { super.createDefaultEditors(); setDefaultEditor(PresentationModel.class, new BnTableCellEditor()); } protected void disconnect() { if (this.presentationModel != null) { this.presentationModel.removeListListener(listListener); } // process selection model ListSelectionModel selModel = getSelectionModel(); int currentSelectionMode = selModel.getSelectionMode(); if (selModel instanceof BnTableSelectionModel) { ((BnTableSelectionModel) selModel).dismiss(); } setSelectionModel(createDefaultSelectionModel()); getSelectionModel().setSelectionMode(currentSelectionMode); // process table model TableModel tblModel = getModel(); if (tblModel instanceof BnTableModel) { ((BnTableModel) tblModel).dismiss(); } setModel(createDefaultDataModel()); uninstallSortingFeature(); } public BnColumn[] getColumns() { return this.columns.toArray(new BnColumn[this.columns.size()]); } public void addColumn(BnColumn newCol) { if (this.columns != null) { final List<BnColumn> list = new ArrayList<BnColumn>(this.columns); list.add(newCol); this.setColumns(list.toArray(new BnColumn[list.size()])); } else { this.setColumns(new BnColumn[] { newCol }); } } // public void setColumnsE( BnColumn ... newCols) { // (rk) VE won't work // with this param public void setColumns(BnColumn[] newCols) { BnColumn[] old = null; if (this.columns != null) { old = this.columns.toArray(new BnColumn[this.columns.size()]); this.disconnect(); } this.columns = Arrays.asList(newCols); this.connect(); super.firePropertyChange("columns", old, newCols); } @Override public void columnMarginChanged(ChangeEvent e) { super.columnMarginChanged(e); if (autoResizeExtension != null) { autoResizeExtension.columnMarginChanged(e); } } /* ------------ AutoResize ------------- */ protected AutoResizeExtension createAutoResizeExtension() { return new AutoResizeExtension(); } /** * Extension that auto-resizes the columns according to the settings of the columns. * * @author Michael Karneim */ class AutoResizeExtension implements Serializable { private boolean pending_columnMarginChanged = false; AutoResizeExtension() { final ComponentListener parentComponentListener = new ComponentAdapter() { // this method is called whenever an enclosing scrollpane is // resized public void componentResized(ComponentEvent e) { resizeColumns(); } }; BnTable.this.addHierarchyListener(new HierarchyListener() { private JViewport viewport; public void hierarchyChanged(HierarchyEvent e) { if (viewport != null) { viewport.removeComponentListener(parentComponentListener); } viewport = getEnclosingViewport(); if (viewport != null) { viewport.addComponentListener(parentComponentListener); } } }); } /** * This extension is only enabled if the standard auto resize mode is off. * * @return <code>true</code> if this extension is enabled, else <code>false</code> */ private boolean isEnabled() { return getAutoResizeMode() == JTable.AUTO_RESIZE_OFF && columns != null; } public void columnMarginChanged(ChangeEvent e) { if (pending_columnMarginChanged || !isEnabled()) { return; } else { pending_columnMarginChanged = true; try { int toviewindex = getResizingColumnViewIndex(); for (int viewindex = 0; viewindex <= toviewindex; ++viewindex) { int colIndex = convertColumnIndexToModel(viewindex); columns.get(colIndex).setWidth(getColumnModel().getColumn(viewindex).getWidth()); columns.get(colIndex).setWidthFixed(true); } resizeColumns(); } finally { pending_columnMarginChanged = false; } } } int getResizingColumnViewIndex() { int result = -1; if (tableHeader != null) { TableColumn resizingColumn = tableHeader.getResizingColumn(); if (resizingColumn != null) { result = convertColumnIndexToView(resizingColumn.getModelIndex()); } } return result; } public void resizeColumns() { if (!isEnabled()) { return; } // how much space is there to fill? int totalWidth = getTotalWidth(); if (totalWidth <= 0) { return; // no space to distribute } // how much space do all columns prefer in total? int totalPreferredWith = getTotalPreferredWidth(); // is there space left? int spaceLeft = totalWidth - totalPreferredWith; // if so, how much space can we give each column? int additionalSpacePerColumn = 0; if (spaceLeft > 0 && getColumnCount() - getColumnsWithFixedWidth() > 0) { additionalSpacePerColumn = spaceLeft / (getColumnCount() - getColumnsWithFixedWidth()); } // is there still space left? // this space we will give to the last column int stillSpaceLeft = spaceLeft - additionalSpacePerColumn * (getColumnCount() - getColumnsWithFixedWidth()); // now distribute the space over all columns // int resizingColumnViewIndex = getResizingColumnViewIndex(); // TODO (mk) use resizingColumnViewIndex for (int viewIndex = 0; viewIndex < getColumnCount(); ++viewIndex) { TableColumn col = getColumnModel().getColumn(viewIndex); int modelIndex = col.getModelIndex(); int delta = 0; if (viewIndex == getColumnCount() - 1 && stillSpaceLeft > 0) { delta = stillSpaceLeft; } BnColumn colAtIndex = columns.get(modelIndex); if (colAtIndex.isWidthFixed() == false) { int newWidth = colAtIndex.getWidth() + additionalSpacePerColumn + delta; this.setColumnWidth(viewIndex, newWidth); } else { int prefWidth = colAtIndex.getWidth() + delta; this.setColumnWidth(viewIndex, prefWidth); } } } int getTotalWidth() { Component parent = getParent(); if (parent == null || parent instanceof JViewport == false) { return -1; } int result = ((JViewport) parent).getSize().width; return result; } int getColumnsWithFixedWidth() { int count = 0; for (BnColumn col : columns) { if (col.isWidthFixed()) { count++; } } return count; } int getTotalPreferredWidth() { int sum = 0; for (BnColumn col : columns) { sum = sum + col.getWidth(); } return sum; } void setColumnWidth(int columnViewIndex, int width) { final TableColumn col = getColumnModel().getColumn(columnViewIndex); final int minw = col.getMinWidth(); final int maxw = col.getMaxWidth(); if (col.getWidth() == width) { return; } col.setMinWidth(width); col.setMaxWidth(width); col.setWidth(width); if (minw > width) { col.setMinWidth(width); } // default min. of TableColumn = 15 => if width < 15 => display // errors when columns get resized else { col.setMinWidth(minw); } col.setMaxWidth(maxw); } JViewport getEnclosingViewport() { Container p = getParent(); if (p instanceof JViewport) { Container gp = p.getParent(); if (gp instanceof JScrollPane) { JScrollPane scrollPane = (JScrollPane) gp; // Make certain we are the viewPort's view and not, for // example, the rowHeaderView of the scrollPane - // an implementor of fixed columns might do this. JViewport viewport = scrollPane.getViewport(); if (viewport == null || viewport.getView() != BnTable.this) { return null; } return viewport; } } return null; } } }