/* * JTreeViewTable.java * Copyright 2010 Connor Petty <cpmeister@users.sourceforge.net> * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * Created on May 13, 2010, 11:53:50 AM */ package pcgen.gui2.util; import java.awt.Container; import java.awt.event.ActionEvent; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import javax.swing.AbstractAction; import javax.swing.BorderFactory; import javax.swing.ButtonGroup; import javax.swing.JCheckBoxMenuItem; import javax.swing.JLabel; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.JRadioButtonMenuItem; import javax.swing.JScrollPane; import javax.swing.JViewport; import javax.swing.ScrollPaneConstants; import javax.swing.event.TableColumnModelEvent; import javax.swing.table.TableColumn; import javax.swing.table.TableColumnModel; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.TreePath; import pcgen.facade.util.ListFacade; import pcgen.facade.util.event.ListEvent; import pcgen.facade.util.event.ListListener; import pcgen.gui2.UIPropertyContext; import pcgen.gui2.tools.PrefTableColumnModel; import pcgen.gui2.util.event.DynamicTableColumnModelListener; import pcgen.gui2.util.table.DynamicTableColumnModel; import pcgen.gui2.util.treeview.DataView; import pcgen.gui2.util.treeview.DataViewColumn; import pcgen.gui2.util.treeview.DataViewColumn.Visibility; import pcgen.gui2.util.treeview.TreeView; import pcgen.gui2.util.treeview.TreeViewModel; import pcgen.gui2.util.treeview.TreeViewTableModel; import pcgen.system.PropertyContext; import pcgen.util.CollectionMaps; import pcgen.util.ListMap; /** * JTreeViewTable is a subclass of JTreeTable that uses a TreeViewModel instead * of a a TreeTableModel. The TreeViewModel is a oriented towards displaying * arbitrary objects as a tree instead of TreeTableNodes. In addition, the * TreeViewModel supports multiple viewing methods and column visibility * controls. * <br> * <br>Node: Methods whose usage would endanger the integrity of this class are * the following: * <br>getModel() * <br>setModel(TableModel) * <br>getTreeTableModel() * <br>setTreeTableModel(TreeTableModel) * <br>setTableHeader(JTableHeader) * <br>setAutoCreateColumnsFromModel(boolean); * * @author Connor Petty <cpmeister@users.sourceforge.net> */ @SuppressWarnings("serial") public class JTreeViewTable<T> extends JTreeTable { /** * Preferences key for the width of the tree view column. */ private static final String TREE_VIEW_COL_PREFS_KEY = "TreeView"; /** * The preferences key for the selected tree view index. */ private static final String VIEW_INDEX_PREFS_KEY = "viewIdx"; private final JTableMenuButton cornerButton; private DynamicTableColumnModel dynamicColumnModel = null; protected TreeViewTableModel<T> treetableModel; private TreeViewModel<T> viewModel; protected CornerButtonPopupMenu cornerPopupMenu = new CornerButtonPopupMenu(); private static final PropertyContext baseContext = UIPropertyContext.createContext("tablePrefs"); /** * Create a new instance of JTreeViewTable */ public JTreeViewTable() { setAutoCreateColumnsFromModel(false); setAutoCreateRowSorter(false); getTree().setLargeModel(true); this.cornerButton = new JTableMenuButton(this, cornerPopupMenu); } protected <TM> TreeViewTableModel<TM> createDefaultTreeViewTableModel(DataView<TM> dataView) { return new TreeViewTableModel<>(dataView); } private JCheckBoxMenuItem createMenuItem(TableColumn column) { JCheckBoxMenuItem item = new JCheckBoxMenuItem(); boolean visible = dynamicColumnModel.isVisible(column); item.setSelected(visible); item.setAction(new MenuAction(column, visible)); return item; } private DynamicTableColumnModel createTableColumnModel(TreeView<?> startingView, DataView<?> dataView) { @SuppressWarnings("unchecked") ListMap<Visibility, TableColumn, List<TableColumn>> listMap = CollectionMaps.createListMap(HashMap.class, ArrayList.class); int index = 1; for (DataViewColumn column : dataView.getDataColumns()) { TableColumn tableColumn = new TableColumn(index++); tableColumn.setHeaderValue(column.getName()); Visibility vis = column.getVisibility(); listMap.add(vis, tableColumn); } List<TableColumn> columns = listMap.get(Visibility.ALWAYS_VISIBLE); if (columns == null) { columns = Collections.emptyList(); } PrefTableColumnModel model = new PrefTableColumnModel(this.viewModel.getDataView().getPrefsKey(), columns.size() + 1); TableColumn viewColumn = new TableColumn(); viewColumn.setHeaderValue(startingView.getViewName()); viewColumn.setIdentifier(TREE_VIEW_COL_PREFS_KEY); model.addColumn(viewColumn, true, 150); for (TableColumn column : columns) { model.addColumn(column, true, 75); } columns = listMap.get(Visibility.INITIALLY_VISIBLE); if (columns != null) { for (TableColumn column : columns) { model.addColumn(column, true, 75); } } columns = listMap.get(Visibility.INITIALLY_INVISIBLE); if (columns != null) { for (TableColumn column : columns) { model.addColumn(column, false, 75); } } return model; } @Override protected void configureEnclosingScrollPane() { super.configureEnclosingScrollPane(); 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() != this) { return; } scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); scrollPane.setCorner(ScrollPaneConstants.UPPER_RIGHT_CORNER, cornerButton); } } } @Override protected void unconfigureEnclosingScrollPane() { super.unconfigureEnclosingScrollPane(); 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() != this) { return; } scrollPane.setCorner(ScrollPaneConstants.UPPER_RIGHT_CORNER, null); } } } /** * This returns data that is currently highlighted by the user. This may * include branch nodes which are of type Object and not the type managed by * the table model. Hence we cannot use <T> here. * * @return A list of selected leaf and branch rows. */ public List<Object> getSelectedData() { TreePath[] paths = getTree().getSelectionPaths(); if (paths == null) { return Collections.emptyList(); } List<Object> data = new ArrayList<>(paths.length); for (TreePath path : paths) { DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); data.add(node.getUserObject()); } return data; } /** * Returns the currently selected object, if any. * * @return the selected object or null if none selected. */ public Object getSelectedObject() { int selectedRow = getSelectedRow(); if (selectedRow != -1) { return getModel().getValueAt(selectedRow, 0); } return null; } public void refreshModelData() { if (treetableModel != null) { treetableModel.refreshData(); } resizeAndRepaint(); } /** * React to a non structural change in model data by repainting the table. * Will not collapse the tree or change which rows are displayed and will * not be sufficient if rows have been added or removed. */ public void updateDisplay() { resizeAndRepaint(); } @Override public void setColumnModel(TableColumnModel columnModel) { if (this.dynamicColumnModel != null) { this.dynamicColumnModel.removeDynamicTableColumnModelListener(cornerPopupMenu); cornerButton.setVisible(false); } super.setColumnModel(columnModel); } public void setColumnModel(DynamicTableColumnModel columnModel) { if (this.dynamicColumnModel != null) { this.dynamicColumnModel.removeDynamicTableColumnModelListener(cornerPopupMenu); } this.dynamicColumnModel = columnModel; columnModel.addDynamicTableColumnModelListener(cornerPopupMenu); super.setColumnModel(columnModel); cornerPopupMenu.resetComponents(); } protected void setTreeView(TreeView<? super T> view) { TableColumn viewColumn = getColumn(TREE_VIEW_COL_PREFS_KEY); treetableModel.setSelectedTreeView(view); viewColumn.setHeaderValue(view.getViewName()); sortModel(); getTableHeader().repaint(); PropertyContext context = baseContext.createChildContext( this.viewModel.getDataView().getPrefsKey()); int index = getIndex(viewModel.getTreeViews(), view); if (index >= 0) { context.setInt(VIEW_INDEX_PREFS_KEY, index); //$NON-NLS-1$ } } /** * get the index of the view. * * @param treeViews The list of tree views. * @param view The view to be found * @return The index or -1 if not found. */ private int getIndex(ListFacade<? extends TreeView<T>> treeViews, TreeView<? super T> view) { for (int i = 0; i < treeViews.getSize(); i++) { TreeView<T> treeView = treeViews.getElementAt(i); if (treeView.equals(view)) { return i; } } // If not found it is most likely the text search view, return -1; } public TreeViewModel<?> getTreeViewModel() { return viewModel; } public void setTreeViewModel(TreeViewModel<T> viewModel) { ListFacade<? extends TreeView<T>> views = viewModel.getTreeViews(); PropertyContext context = baseContext.createChildContext( viewModel.getDataView().getPrefsKey()); int viewIndex = context.initInt(VIEW_INDEX_PREFS_KEY, viewModel.getDefaultTreeViewIndex()); TreeView<? super T> startingView = views.getElementAt(viewIndex); DataView<T> dataView = viewModel.getDataView(); final TreeViewTableModel<T> model = createDefaultTreeViewTableModel(dataView); this.treetableModel = model; if (this.viewModel != null) { this.viewModel.getTreeViews().removeListListener(cornerPopupMenu); } this.viewModel = viewModel; model.setDataModel(viewModel.getDataModel()); model.setSelectedTreeView(startingView); setTreeTableModel(model); setColumnModel(createTableColumnModel(startingView, dataView)); cornerPopupMenu.resetComponents(); this.viewModel.getTreeViews().addListListener(cornerPopupMenu); } /** * @return The currently selected tree view. */ public TreeView<? super T> getSelectedTreeView() { return treetableModel.getSelectedTreeView(); } /** * Find the named view. * * @param views The list of TreeViews. * @param viewName The name of the desired view. * @return The matching view, or the first one if none match. */ private TreeView<? super T> findViewByName( ListFacade<? extends TreeView<T>> views, String viewName) { for (TreeView<T> treeView : views) { if (treeView.getViewName().equals(viewName)) { return treeView; } } return views.getElementAt(0); } /** * This is the popup menu for the CornerButton which allows selection of the * selected tree view as well as the visible columns for the table. */ protected class CornerButtonPopupMenu extends JPopupMenu implements ListListener<TreeView<T>>, DynamicTableColumnModelListener { private boolean treeViewsEnabled = true; private boolean tableColumnsEnabled = true; @Override public void availableColumnAdded(TableColumnModelEvent event) { resetComponents(); } @Override public void availableColumnRemove(TableColumnModelEvent event) { resetComponents(); } @Override public void elementAdded(ListEvent<TreeView<T>> e) { resetComponents(); } @Override public void elementRemoved(ListEvent<TreeView<T>> e) { resetComponents(); } public void setTreeViewsEnabled(boolean enabled) { this.treeViewsEnabled = enabled; resetComponents(); } public void setTableColumnsEnabled(boolean enabled) { this.tableColumnsEnabled = enabled; resetComponents(); } @Override public void elementsChanged(ListEvent<TreeView<T>> e) { resetComponents(); } @Override public void elementModified(ListEvent<TreeView<T>> e) { } public void resetComponents() { ListFacade<? extends TreeView<T>> views = viewModel.getTreeViews(); PropertyContext context = baseContext.createChildContext(viewModel.getDataView() .getPrefsKey()); int viewIndex = context.initInt(VIEW_INDEX_PREFS_KEY, viewModel.getDefaultTreeViewIndex()); ButtonGroup group = new ButtonGroup(); TreeView<? super T> startingView = views.getElementAt(viewIndex); removeAll(); JLabel treeLabel = new JLabel("Tree Views"); treeLabel.setBorder(BorderFactory.createEmptyBorder(0, 4, 0, 0)); add(treeLabel); for (TreeView<T> treeview : views) { JMenuItem item = new JRadioButtonMenuItem(new ChangeViewAction(treeview)); item.setSelected(startingView == treeview); group.add(item); item.setEnabled(treeViewsEnabled); add(item); } addSeparator(); JLabel columnLabel = new JLabel("Columns"); columnLabel.setBorder(BorderFactory.createEmptyBorder(0, 4, 0, 0)); add(columnLabel); List<TableColumn> columns = dynamicColumnModel.getAvailableColumns(); for (TableColumn column : columns) { JMenuItem item = createMenuItem(column); item.setEnabled(tableColumnsEnabled); add(item); } cornerButton.setVisible(!columns.isEmpty() || !views.isEmpty()); } } private class MenuAction extends AbstractAction { private boolean visible; private TableColumn column; public MenuAction(TableColumn column, boolean visible) { super(column.getHeaderValue().toString()); this.visible = visible; this.column = column; } @Override public void actionPerformed(ActionEvent e) { dynamicColumnModel.setVisible(column, visible = !visible); } } private class ChangeViewAction extends AbstractAction { private TreeView<T> view; public ChangeViewAction(TreeView<T> view) { super(view.getViewName()); this.view = view; } @Override public void actionPerformed(ActionEvent e) { setTreeView(view); } } }