/*- * Copyright (C) 2007-2014 Erik Larsson * * This program 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. * * This program 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 org.catacombae.hfsexplorer; import java.awt.Color; import java.util.Date; import java.awt.Component; import java.awt.Graphics; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.text.DateFormat; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Enumeration; import java.util.LinkedList; import java.util.List; import java.util.Queue; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JTextField; import javax.swing.JTree; import javax.swing.ListSelectionModel; import javax.swing.SwingConstants; import javax.swing.event.ChangeEvent; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.event.TableColumnModelEvent; import javax.swing.event.TableColumnModelListener; import javax.swing.event.TreeExpansionEvent; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.event.TreeWillExpandListener; import javax.swing.table.DefaultTableModel; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.ExpandVetoException; import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; import org.catacombae.hfsexplorer.gui.FilesystemBrowserPanel; import org.catacombae.util.Util.Pair; /** * A generalization of the file system browser into a very autonomous component with very few * dependencies, so that it can be easily reused in the future. * * @author <a href="http://www.catacombae.org/" target="_top">Erik Larsson</a> */ public class FileSystemBrowser<A> { private static final boolean DEBUG = Util.booleanEnabledByProperties(false, "org.catacombae.debug", "org.catacombae.hfsexplorer.debug", "org.catacombae.hfsexplorer." + FileSystemBrowser.class.getSimpleName() + ".debug"); private final FileSystemProvider<A> controller; private final FilesystemBrowserPanel viewComponent; private final JTextField addressField; private final JButton upButton; private final JButton extractButton; private final JButton infoButton; private final JButton goButton; private final JLabel statusLabel; private final JTable fileTable; private final JScrollPane fileTableScroller; private final JTree dirTree; //private final JPopupMenu treeNodePopupMenu; //private final JPopupMenu tableNodePopupMenu; private final DefaultTableModel tableModel; // Focus timestamps (for determining what to extract) private long fileTableLastFocus = 0; private long dirTreeLastFocus = 0; /** For determining the standard layout size of the columns in the table. */ private int totalColumnWidth = 0; /** Used for formatting byte size strings, like 234,12 MiB. */ private final DecimalFormat sizeFormat = new DecimalFormat("0.00"); // Communication between adjustColumnsWidths and the column listener private final boolean[] disableColumnListener = { false }; private final ObjectContainer<int[]> lastWidths = new ObjectContainer<int[]>(null); private DefaultTreeModel treeModel; private final GenericPlaceholder<A> genericPlaceholder = new GenericPlaceholder<A>(); private TreePath lastTreeSelectionPath = null; public FileSystemBrowser(FileSystemProvider<A> iController) { this.controller = iController; this.viewComponent = new FilesystemBrowserPanel(); this.addressField = viewComponent.addressField; this.upButton = viewComponent.upButton; this.infoButton = viewComponent.infoButton; this.extractButton = viewComponent.extractButton; this.goButton = viewComponent.goButton; this.statusLabel = viewComponent.statusLabel; this.fileTable = viewComponent.fileTable; this.fileTableScroller = viewComponent.fileTableScroller; this.dirTree = viewComponent.dirTree; upButton.addActionListener(new ActionListener() { /* @Override */ public void actionPerformed(ActionEvent e) { actionGotoParentDir(); } }); extractButton.addActionListener(new ActionListener() { /* @Override */ public void actionPerformed(ActionEvent e) { actionExtractToDir(); } }); infoButton.addActionListener(new ActionListener() { /* @Override */ public void actionPerformed(ActionEvent e) { actionGetInfo(); } }); goButton.addActionListener(new ActionListener() { /* @Override */ public void actionPerformed(ActionEvent e) { actionGotoDir(); } }); addressField.addActionListener(new ActionListener() { /* @Override */ public void actionPerformed(ActionEvent e) { actionGotoDir(); } }); /* addressField.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if(e.getKeyCode() == KeyEvent.VK_ENTER) actionGotoDir(); } }); */ //this.treeNodePopupMenu = controller.createTreeNodePopupMenu(); //this.tableNodePopupMenu = controller.createTableNodePopupMenu(); final Class objectClass = new Object().getClass(); final Object[] colNames = { "Name", "Size", "Type", "Date Modified", "", }; tableModel = new DefaultTableModel(colNames, 0) { @Override public boolean isCellEditable(int rowIndex, int columnIndex) { return false; } }; fileTable.setModel(tableModel); fileTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); // AUTO_RESIZE_SUBSEQUENT_COLUMNS AUTO_RESIZE_OFF AUTO_RESIZE_LAST_COLUMN fileTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); final int COLUMN_WIDTH_NAME = 180; final int COLUMN_WIDTH_SIZE = 96; final int COLUMN_WIDTH_TYPE = 120; final int COLUMN_WIDTH_DATE = 130; fileTable.getColumnModel().getColumn(0).setPreferredWidth(COLUMN_WIDTH_NAME); fileTable.getColumnModel().getColumn(1).setPreferredWidth(COLUMN_WIDTH_SIZE); fileTable.getColumnModel().getColumn(2).setPreferredWidth(COLUMN_WIDTH_TYPE); fileTable.getColumnModel().getColumn(3).setPreferredWidth(COLUMN_WIDTH_DATE); fileTable.getColumnModel().getColumn(4).setPreferredWidth(0); totalColumnWidth = COLUMN_WIDTH_NAME + COLUMN_WIDTH_SIZE + COLUMN_WIDTH_TYPE + COLUMN_WIDTH_DATE; fileTable.getColumnModel().getColumn(4).setMinWidth(0); fileTable.getColumnModel().getColumn(4).setResizable(false); if(Java6Util.isJava6OrHigher()) { Comparator<?> c = new ComparableComparator(); ArrayList<Comparator<?>> rowComparators = new ArrayList<Comparator<?>>(5); for(int i = 0; i < 5; ++i) // 5 rows currently rowComparators.add(c); Java6Util.addRowSorter(fileTable, tableModel, 4, rowComparators); } TableColumnModelListener columnListener = new TableColumnModelListener() { private boolean locked = false; private int[] w1 = null; //public int[] lastWidths = null; /* @Override */ public void columnAdded(TableColumnModelEvent e) { /*System.out.println("columnAdded");*/ } /* @Override */ public void columnMarginChanged(ChangeEvent e) { if (disableColumnListener[0]) { return; } synchronized (this) { if (!locked) locked = true; else { // System.err.println(" BOUNCING!"); return; } } // System.err.print("columnMarginChanged"); // System.err.print(" Width diff:"); int columnCount = fileTable.getColumnModel().getColumnCount(); TableColumn lastColumn = fileTable.getColumnModel().getColumn(columnCount - 1); if (lastWidths.o == null) { lastWidths.o = new int[columnCount]; } if (w1 == null || w1.length != columnCount) { w1 = new int[columnCount]; } int diffSum = 0; int currentWidth = 0; for (int i = 0; i < w1.length; ++i) { w1[i] = fileTable.getColumnModel().getColumn(i).getWidth(); currentWidth += w1[i]; int diff = (w1[i] - lastWidths.o[i]); // System.err.print(" " + (w1[i] - lastWidths.o[i])); if (i < w1.length - 1) { diffSum += diff; } } int lastDiff = (w1[columnCount - 1] - lastWidths.o[columnCount - 1]); // System.err.print(" Diff sum: " + diffSum); // System.err.println(" Last diff: " + (w1[columnCount-1] - lastWidths.o[columnCount-1])); if (lastDiff != -diffSum) { int importantColsWidth = currentWidth - w1[columnCount - 1]; //int newLastColumnWidth = lastWidths.o[columnCount-1] - diffSum; int newLastColumnWidth = totalColumnWidth - importantColsWidth; int nextTotalWidth = importantColsWidth + newLastColumnWidth; // System.err.println(" totalColumnWidth=" + totalColumnWidth + " currentWidth=" + currentWidth + " nextTotalWidth=" + nextTotalWidth + " newLast..=" + newLastColumnWidth); if (newLastColumnWidth >= 0) { if ((nextTotalWidth <= totalColumnWidth || diffSum > 0)) { //if(currentWidth > totalColumnWidth) // System.err.println(" (1)Adjusting last column from " + w1[columnCount-1] + " to " + newLastColumnWidth + "!"); lastColumn.setPreferredWidth(newLastColumnWidth); lastColumn.setWidth(newLastColumnWidth); //fileTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); // System.err.println(" (1)Last column width: " + lastColumn.getWidth() + " revalidating..."); fileTableScroller.invalidate(); fileTableScroller.validate(); // System.err.println(" (1)Adjustment complete. Final last column width: " + lastColumn.getWidth()); } // else // System.err.println(" Outside bounds. Idling."); } else { if (lastColumn.getWidth() != 0) { // System.err.println(" (2)Adjusting last column from " + w1[columnCount-1] + " to zero!"); lastColumn.setPreferredWidth(0); lastColumn.setWidth(0); //fileTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); // System.err.println(" (2)Last column width: " + lastColumn.getWidth() + " revalidating..."); fileTableScroller.invalidate(); fileTableScroller.validate(); // System.err.println(" (2)Adjustment complete. Final last column width: " + lastColumn.getWidth()); } } } for (int i = 0; i < w1.length; ++i) { w1[i] = fileTable.getColumnModel().getColumn(i).getWidth(); } int[] usedArray = lastWidths.o; lastWidths.o = w1; w1 = usedArray; // Switch arrays. synchronized (this) { locked = false; /*System.err.println();*/ } } /* @Override */ public void columnMoved(TableColumnModelEvent e) { /*System.out.println("columnMoved");*/ } /* @Override */ public void columnRemoved(TableColumnModelEvent e) { /*System.out.println("columnRemoved");*/ } /* @Override */ public void columnSelectionChanged(ListSelectionEvent e) { /*System.out.println("columnSelectionChanged");*/ } }; fileTable.getColumnModel().addColumnModelListener(columnListener); final TableCellRenderer objectRenderer = fileTable.getDefaultRenderer(objectClass); fileTable.setDefaultRenderer(objectClass, new TableCellRenderer() { private JLabel theOne = new JLabel(); private JLabel theTwo = new JLabel("", SwingConstants.RIGHT); private ImageIcon documentIcon = new ImageIcon(ClassLoader.getSystemResource("res/emptydocument.png")); private ImageIcon folderIcon = new ImageIcon(ClassLoader.getSystemResource("res/folder.png")); private ImageIcon emptyIcon = new ImageIcon(ClassLoader.getSystemResource("res/nothing.png")); /* @Override */ public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, final int row, final int column) { if(value instanceof RecordContainer) { final Component objectComponent = objectRenderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); final JLabel jl = theOne; Record rec = ((RecordContainer) value).getRecord(genericPlaceholder); switch(rec.getType()) { case FOLDER: case FOLDER_LINK: jl.setIcon(folderIcon); break; case FILE: case FILE_LINK: jl.setIcon(documentIcon); break; case BROKEN_LINK: jl.setIcon(emptyIcon); break; default: throw new RuntimeException("Unhandled RecordType: " + rec.getType()); } jl.setVisible(true); final boolean compressed = rec.isCompressed(); Component c = new Component() { final Color tableForeground; { jl.setSize(jl.getPreferredSize()); jl.setLocation(0, 0); objectComponent.setSize(objectComponent.getPreferredSize()); objectComponent.setLocation(jl.getWidth(), 0); setSize(jl.getWidth() + objectComponent.getWidth(), Math.max(jl.getHeight(), objectComponent.getHeight())); javax.swing.UIDefaults uidefs = javax.swing.UIManager.getLookAndFeelDefaults(); final Color lfTableForeground = uidefs.getColor("Table.foreground"); if(lfTableForeground != null) { tableForeground = lfTableForeground; } else { /* Fall back on the colour black, which is at * least more often than not the foreground * colour. */ tableForeground = Color.BLACK; } } @Override public void paint(Graphics g) { jl.paint(g); int translatex = jl.getWidth(); g.translate(translatex, 0); final Color objectComponentOriginalForeground; if(compressed) { final Color curForeground = objectComponent.getForeground(); /* We only change the foreground colour to blue * when the original foreground colour is equal * to the L&F's Table foreground colour. * This is due to painting issues when restoring * the original background colour in (at least) * the Mac OS X Swing implementation / L&F. */ if(curForeground.equals(tableForeground)) { objectComponent.setForeground(Color.BLUE); objectComponentOriginalForeground = curForeground; } else { objectComponentOriginalForeground = null; } } else { objectComponentOriginalForeground = null; } objectComponent.paint(g); if(objectComponentOriginalForeground != null) { objectComponent.setForeground( objectComponentOriginalForeground); } g.translate(-translatex, 0); } }; return c; } else if(column == 1) { theTwo.setText(value.toString()); return theTwo; } else{ return objectRenderer.getTableCellRendererComponent(table, value, false, false, row, column); } } }); fileTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() { /* @Override */ public void valueChanged(ListSelectionEvent e) { /* When the selection in the file table changes, update the * selection status field with the new selection count and * selection size. */ int[] selection = fileTable.getSelectedRows(); //Object[] selection = fileTable.getSelection(); long selectionSize = 0; for(int selectedRow : selection) { Object o = fileTable.getValueAt(selectedRow, 0); if(o instanceof RecordContainer) { Record rec = ((RecordContainer) o).getRecord(genericPlaceholder); if(rec.getType() == RecordType.FILE || rec.getType() == RecordType.FILE_LINK) selectionSize += rec.getSize(); } } setSelectionStatus(selection.length, selectionSize); } }); fileTableScroller.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { /* If we click outside the table, i.e. in the JScrollPane, * clear selection in table. */ int row = fileTable.rowAtPoint(e.getPoint()); if(row == -1) fileTable.clearSelection(); } }); fileTable.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if(e.getButton() == MouseEvent.BUTTON3) { /* When the user clicks the secondary mouse button * (usually the right mouse button) in the table, * possibly open a JPopupMenu with some options. */ int row = fileTable.rowAtPoint(e.getPoint()); int col = fileTable.columnAtPoint(e.getPoint()); if(col == 0 && row >= 0) { /* These lines are here because right-clicking * doesn't change focus or selection. */ int[] currentSelection = fileTable.getSelectedRows(); if(!Util.contains(currentSelection, row)) { fileTable.clearSelection(); fileTable.changeSelection(row, col, false, false); } fileTable.requestFocus(); List<Record<A>> selection = getTableSelection(); List<Record<A>> selectionParentPath = getRecordPath(lastTreeSelectionPath); /*if(selection.size() != 1) throw new RuntimeException("Right click selection with more than " + "one entry! (" + selection.size() + " entries)");*/ JPopupMenu jpm = controller.getRightClickRecordPopupMenu(selectionParentPath, selection); jpm.show(fileTable, e.getX(), e.getY()); } } else if(e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) { /* When the user double-clicks using the primary mouse * button, send the event on to the controller, which * may handle it as it likes. */ int row = fileTable.rowAtPoint(e.getPoint()); int col = fileTable.columnAtPoint(e.getPoint()); if(col == 0 && row >= 0) { //System.err.println("Double click at (" + row + "," + col + ")"); Object colValue = fileTable.getValueAt(row, col); //System.err.println(" Value class: " + colValue.getClass()); if(colValue instanceof RecordContainer) { Record<A> rec = ((RecordContainer) colValue).getRecord(genericPlaceholder); if(rec.getType() == RecordType.FILE || rec.getType() == RecordType.FILE_LINK) { List<Record<A>> dirPath = getRecordPath(lastTreeSelectionPath); ArrayList<Record<A>> completePath = new ArrayList<Record<A>>(dirPath.size()+1); completePath.addAll(dirPath); completePath.add(rec); controller.actionDoubleClickFile(completePath); } else if(rec.getType() == RecordType.FOLDER || rec.getType() == RecordType.FOLDER_LINK) actionChangeDir(rec); } else throw new RuntimeException("Invalid type in column 0 in fileTable!"); } } } }); dirTree.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { if(e.getButton() == MouseEvent.BUTTON3 && controller.isFileSystemLoaded()) { TreePath tp = dirTree.getPathForLocation(e.getX(), e.getY()); if(tp != null) { dirTree.clearSelection(); dirTree.setSelectionPath(tp); dirTree.requestFocus(); List<Record<A>> recList = Collections.singletonList(getTreeSelection()); List<Record<A>> selectionParentPath = getRecordPath(lastTreeSelectionPath.getParentPath()); controller.getRightClickRecordPopupMenu(selectionParentPath, recList).show(dirTree, e.getX(), e.getY()); } } } }); setRoot(null); dirTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); dirTree.addTreeSelectionListener(new TreeSelectionListener() { /* @Override */ public void valueChanged(TreeSelectionEvent e) { TreePath tp = e.getPath(); actionTreeNodeSelected(tp); } }); dirTree.addTreeWillExpandListener(new TreeWillExpandListener() { /* @Override */ public void treeWillExpand(TreeExpansionEvent e) throws ExpandVetoException { //System.out.println("Tree will expand!"); actionExpandDirTreeNode(e.getPath()); } /* @Override */ public void treeWillCollapse(TreeExpansionEvent e) {} }); // Focus monitoring fileTable.addFocusListener(new FocusListener() { /* @Override */ public void focusGained(FocusEvent e) { //System.err.println("fileTable gained focus!"); fileTableLastFocus = System.nanoTime(); //dirTree.clearSelection(); } /* @Override */ public void focusLost(FocusEvent e) {} }); dirTree.addFocusListener(new FocusListener() { /* @Override */ public void focusGained(FocusEvent e) { //System.err.println("dirTree gained focus!"); dirTreeLastFocus = System.nanoTime(); //fileTable.clearSelection(); // I'm unsure whether this behaviour is desired } /* @Override */ public void focusLost(FocusEvent e) {} }); fileTableScroller.addComponentListener(new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { //System.err.println("Component resized"); adjustTableWidth(); } }); } /** * Action code for the action "go to parent directory" in the file system browser. */ private void actionGotoParentDir() { if(ensureFileSystemLoaded()) { if(lastTreeSelectionPath.getPathCount() > 1) { TreePath parentPath = lastTreeSelectionPath.getParentPath(); selectInTree(parentPath); } } } /** * Action code for the action "extract selection to directory" in the file system browser. */ private void actionExtractToDir() { if(ensureFileSystemLoaded()) { controller.actionExtractToDir(getSelectionParentPath(), getSelection()); } } private void actionChangeDir(Record<A> subDir) { TreePath currentTreeSelection = lastTreeSelectionPath; Object objectToPopulate = lastTreeSelectionPath.getLastPathComponent(); FolderTreeNode nodeToPopulate; if(objectToPopulate instanceof FolderTreeNode) { nodeToPopulate = (FolderTreeNode) objectToPopulate; List<Record<A>> recordPath = getRecordPath(currentTreeSelection); // First make sure we have updated contents populateTreeNodeFromPath(nodeToPopulate, recordPath); dirTree.expandPath(lastTreeSelectionPath); int childCount = treeModel.getChildCount(nodeToPopulate); Object finalChild = null; for(int i = 0; i < childCount; ++i) { Object curChild = treeModel.getChild(nodeToPopulate, i); if(curChild instanceof FolderTreeNode && ((FolderTreeNode) curChild).getRecordContainer() .getRecord(genericPlaceholder).getName().equals(subDir.getName())) { TreePath childPath = lastTreeSelectionPath.pathByAddingChild(curChild); //dirTree.expandPath(childPath); selectInTree(childPath); finalChild = curChild; break; } } if(finalChild == null) throw new RuntimeException("Selection path to leaf child not found!"); } /* String[] rawPath = new String[recordPath.size()-1+1]; int i = 0; for(Record<A> rec : recordPath) { if(i > 0) rawPath[i-1] = rec.getName(); ++i; } rawPath[i] = subDir.getName(); setCurrentDirectory(rawPath); * */ } /** * Action code for the action "get info about selection" in the file system browser. */ private void actionGetInfo() { if(ensureFileSystemLoaded()) { final List<Record<A>> selection = getSelection(); if(selection != null) { controller.actionGetInfo(getSelectionParentPath(), selection); } } } /** * Action code for the action "go to specified directory" in the file system browser. */ private void actionGotoDir() { if(ensureFileSystemLoaded()) { String targetAddress = addressField.getText(); String[] addressComponents = controller.parseAddressPath(targetAddress); if(addressComponents != null) setCurrentDirectory(addressComponents); else JOptionPane.showMessageDialog(viewComponent, "Invalid pathname.", "Error", JOptionPane.ERROR_MESSAGE); } } private void actionExpandDirTreeNode(TreePath targetNodePath) { if(ensureFileSystemLoaded()) { try { final FolderTreeNode nodeToPopulate; { Object objToExpand = targetNodePath.getLastPathComponent(); if(objToExpand instanceof FolderTreeNode) { nodeToPopulate = (FolderTreeNode) objToExpand; } else { throw new RuntimeException("Unexpected node class in tree: " + objToExpand.getClass()); } } List<Record<A>> recordPath = getRecordPath(targetNodePath); populateTreeNodeFromPath(nodeToPopulate, recordPath); } catch(Throwable e) { displayUnhandledException(e); } } } private void actionTreeNodeSelected(TreePath selectionPath) { // System.err.println("actionTreeNodeSelected(" + selectionPath.toString() + ");"); // System.err.println(" path count: " + selectionPath.getPathCount()); // System.err.println(" type of last component: " + selectionPath.getLastPathComponent().getClass()); // If we have selected another node type than FolderTreeNode, we don't do anything. if(selectionPath.getLastPathComponent() instanceof FolderTreeNode) { if(ensureFileSystemLoaded()) { try { List<Record<A>> recordPath = getRecordPath(selectionPath); populateTableFromPath(recordPath); lastTreeSelectionPath = selectionPath; } catch(Throwable e) { displayUnhandledException(e); } } } } private void displayUnhandledException(Throwable e) { e.printStackTrace(); JOptionPane.showMessageDialog(viewComponent, e.getClass() + " while populating " + "tree node:\n " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); } private List<Record<A>> getRecordPath(TreePath tp) { if(tp == null) return null; List<Record<A>> recordPath = new ArrayList<Record<A>>(tp.getPathCount()); for(Object obj : tp.getPath()) { if(obj instanceof FolderTreeNode) { FolderTreeNode noLeafMutableTreeNode = (FolderTreeNode) obj; Object userObj = noLeafMutableTreeNode.getUserObject(); if(userObj instanceof RecordContainer) { Record<A> rec = ((RecordContainer) userObj).getRecord(genericPlaceholder); if(rec.getType() == RecordType.FOLDER || rec.getType() == RecordType.FOLDER_LINK) { recordPath.add(rec); } else { throw new RuntimeException("Unexpected record type in tree: " + rec.getType()); } } else { throw new RuntimeException("Unexpected user object class in tree: " + userObj.getClass()); } } else { throw new RuntimeException("Unexpected node class in tree: " + obj.getClass()); } } return recordPath; } private void adjustTableWidth() { //System.err.println("adjustTableWidth()"); int columnCount = fileTable.getColumnModel().getColumnCount(); int[] w1 = new int[columnCount]; for(int i = 0; i < w1.length; ++i) w1[i] = fileTable.getColumnModel().getColumn(i).getPreferredWidth(); // System.err.print(" Widths before ="); // for(int width : w1) // System.err.print(" " + width); // System.err.println(); disableColumnListener[0] = true; fileTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN); fileTableScroller.invalidate(); //fileTable.invalidate(); //fileTable.validate(); fileTableScroller.validate(); int[] w2 = new int[columnCount]; int newTotalWidth = 0; for(int i = 0; i < columnCount; ++i) { w2[i] = fileTable.getColumnModel().getColumn(i).getWidth(); newTotalWidth += w2[i]; } totalColumnWidth = newTotalWidth; // For telling marginChanged what size to adjust to // System.err.println(" totalColumnWidth=" + totalColumnWidth); int newLastColumnWidth = newTotalWidth; for(int i = 0; i < w1.length-1; ++i) newLastColumnWidth -= w1[i]; if(newLastColumnWidth < 0) newLastColumnWidth = 0; fileTable.getColumnModel().getColumn(columnCount-1).setPreferredWidth(newLastColumnWidth); fileTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); fileTableScroller.invalidate(); fileTableScroller.validate(); // System.err.print(" Widths after ="); // for(int i = 0; i < columnCount; ++i) // System.err.print(" " + fileTable.getColumnModel().getColumn(i).getPreferredWidth()); // System.err.println(); lastWidths.o = null; disableColumnListener[0] = false; } private void populateTreeNodeFromPath(FolderTreeNode nodeToPopulate, List<Record<A>> recordPath) { List<Record<A>> childRecords = controller.getFolderContents(recordPath); populateTreeNodeFromContents(nodeToPopulate, childRecords); } private void populateTreeNodeFromContents(FolderTreeNode nodeToPopulate, List<Record<A>> childRecords) { //System.err.println("populateTreeNodeFromContents called for " + nodeToPopulate.getUserObject().toString()); final Queue<Record<A>> remainingQueue; { // Initialize remainingQueue final LinkedList<Record<A>> remainingRecords = new LinkedList<Record<A>>(); for(Record<A> childRecord : childRecords) { if(childRecord.getType() == RecordType.FOLDER || childRecord.getType() == RecordType.FOLDER_LINK) { remainingRecords.add(childRecord); } } remainingQueue = remainingRecords; } final List<FolderTreeNode> currentNodes; { // Initialize currentNodes currentNodes = new ArrayList<FolderTreeNode>(nodeToPopulate.getChildCount()); Enumeration en = nodeToPopulate.children(); while(en.hasMoreElements()) { Object o = en.nextElement(); if(o instanceof FolderTreeNode) { currentNodes.add((FolderTreeNode) o); } else { throw new RuntimeException("Unexpected child type: " + o.getClass()); } } } // Sort out all nodes to remove, add or change LinkedList<Pair<FolderTreeNode, Record<A>>> nodesToUpdate = new LinkedList<Pair<FolderTreeNode, Record<A>>>(); LinkedList<FolderTreeNode> nodesToRemove = new LinkedList<FolderTreeNode>(); LinkedList<Integer> insertedRecordIndices = new LinkedList<Integer>(); int currentIndex = 0; for(FolderTreeNode node : currentNodes) { String nodeName = node.getRecordContainer().getRecord(genericPlaceholder).getName(); Record<A> firstRemainingRecord = remainingQueue.peek(); while(firstRemainingRecord != null && firstRemainingRecord.getName().compareTo(nodeName) < 0) { //recordsToInsert.add(remainingRecords.removeFirst()); FolderTreeNode newNode = new FolderTreeNode(new RecordContainer(remainingQueue.remove())); insertedRecordIndices.add(currentIndex); nodeToPopulate.insert(newNode, currentIndex++); firstRemainingRecord = remainingQueue.peek(); } if(firstRemainingRecord != null && firstRemainingRecord.getName().compareTo(nodeName) == 0) { nodesToUpdate.add(new Pair<FolderTreeNode, Record<A>>(node, remainingQueue.remove())); } else { nodesToRemove.add(node); } ++currentIndex; } while(remainingQueue.peek() != null) { FolderTreeNode newNode = new FolderTreeNode(new RecordContainer(remainingQueue.remove())); insertedRecordIndices.add(currentIndex); nodeToPopulate.insert(newNode, currentIndex++); } int[] insertedRecordIndicesArray = new int[insertedRecordIndices.size()]; { int i = 0; for(int index : insertedRecordIndices) { insertedRecordIndicesArray[i++] = index; } } //System.err.println("nodesWereInserted: " + insertedRecordIndicesArray.length); if(insertedRecordIndicesArray.length > 0) { treeModel.nodesWereInserted(nodeToPopulate, insertedRecordIndicesArray); // 1. Remove those nodes that should be removed } { FolderTreeNode[] removedChildren = new FolderTreeNode[nodesToRemove.size()]; int[] removedIndices = new int[removedChildren.length]; int index = 0; for(FolderTreeNode node : nodesToRemove) { removedChildren[index] = node; removedIndices[index] = nodeToPopulate.getIndex(node); if(removedIndices[index] < 0) { throw new RuntimeException("INTERNAL ERROR: Can't find node in nodeToPopulate!"); } ++index; } for(int i : removedIndices) { nodeToPopulate.remove(i); } //System.err.println("nodesWereRemoved: " + removedIndices.length); if(removedIndices.length > 0) { treeModel.nodesWereRemoved(nodeToPopulate, removedIndices, removedChildren); } } // 2. Update those nodes that should be updated { int[] updatedIndices = new int[nodesToUpdate.size()]; int index = 0; for(Pair<FolderTreeNode, Record<A>> p : nodesToUpdate) { p.getA().setUserObject(new RecordContainer(p.getB())); updatedIndices[index] = nodeToPopulate.getIndex(p.getA()); if(updatedIndices[index] < 0) { throw new RuntimeException("INTERNAL ERROR: Can't find node in nodeToPopulate!"); } ++index; } //System.err.println("nodesChanged: " + updatedIndices.length); if(updatedIndices.length > 0) { treeModel.nodesChanged(nodeToPopulate, updatedIndices); } } } private List<String> asNameList(List<Record<A>> recordList) { ArrayList<String> res = new ArrayList<String>(); for(Record<A> rec : recordList) res.add(rec.getName()); return res; } private void populateTableFromPath(List<Record<A>> folderRecordPath) { List<Record<A>> childRecords = controller.getFolderContents(folderRecordPath); List<String> nameList = asNameList(folderRecordPath.subList(1, folderRecordPath.size())); String displayPath = controller.getAddressPath(nameList); populateTableFromContents(childRecords, displayPath); } private void populateTableFromContents(List<Record<A>> contents, String displayPath) { while(tableModel.getRowCount() > 0) { tableModel.removeRow(tableModel.getRowCount()-1); } DateFormat dti = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT); int i = 0; for(Record<A> rec : contents) { final Object[] currentRow = { new RecordContainer(rec), new SizeEntry(rec.getSize()), new RecordTypeEntry(rec.getType()), new DateEntry(rec.getModifyDate(), dti), new IndexEntry(i++), }; tableModel.addRow(currentRow); } adjustTableWidth(); fileTableScroller.getVerticalScrollBar().setValue(0); addressField.setText(displayPath); } /** * Returns the JComponent that can be used to display the FileSystemBrowser. * * @return the JComponent that can be used to display the FileSystemBrowser. */ public JComponent getViewComponent() { return viewComponent; } /** * Returns the current user selection as a list of the user objects * contained within the records, rather than a list of the records * themselves. This is a convenience method. * * @return the current user selection as a list of user objects. */ public List<A> getUserObjectSelection() { List<Record<A>> recs = getSelection(); ArrayList<A> result = new ArrayList<A>(recs.size()); for(Record<A> rec : recs) { result.add(rec.getUserObject()); } return result; } /** * Returns the current user selection for the file system browser. The * selection may be from the tree, in which case there is only one object, * or from the table, in which case there can be several. Which one to * choose when there is a selection in both the table and the tree depends * on which component last had focus. * * @return the current user selection for the file system browser. */ public List<Record<A>> getSelection() { final List<Record<A>> result; if(dirTreeLastFocus >= fileTableLastFocus) { Record<A> treeSelection = getTreeSelection(); if(treeSelection != null) { result = new ArrayList<Record<A>>(1); result.add(treeSelection); } else { result = null; } } else { result = getTableSelection(); } return result; } public List<Record<A>> getSelectionParentPath() { if(dirTreeLastFocus >= fileTableLastFocus) return getRecordPath(lastTreeSelectionPath.getParentPath()); else return getRecordPath(lastTreeSelectionPath); } /** * Returns the current user selection for the folder tree. * * @return the current user selection for the folder tree. */ private Record<A> getTreeSelection() { //List<Record<A>> result; Record<A> result; Object o = lastTreeSelectionPath.getLastPathComponent(); if(o == null) { JOptionPane.showMessageDialog(viewComponent, "No file or folder selected.", "Information", JOptionPane.INFORMATION_MESSAGE); result = null; } else if(o instanceof DefaultMutableTreeNode) { Object o2 = ((DefaultMutableTreeNode) o).getUserObject(); if(o2 instanceof RecordContainer) { Record<A> rec = ((RecordContainer) o2).getRecord(genericPlaceholder); //result = new ArrayList<Record<A>>(1); //result.add(rec); result = rec; } else { JOptionPane.showMessageDialog(viewComponent, "[getTreeSelection()] Unexpected data in tree model: " + o2.getClass() + ". (Internal error, report to " + "developer)", "Error", JOptionPane.ERROR_MESSAGE); result = null; } } else { JOptionPane.showMessageDialog(viewComponent, "[getTreeSelection()] Unexpected tree node type: " + o.getClass() + "! (Internal error, report to developer)", "Error", JOptionPane.ERROR_MESSAGE); result = null; } return result; } /* private List<Record<A>> getTreeSelectionPath() { //List<Record<A>> result; TreePath tmpLastTreeSelectionPath = lastTreeSelectionPath; ArrayList<Record<A>> result = new ArrayList<Record<A>>(tmpLastTreeSelectionPath.getPathCount()); for(Object o : tmpLastTreeSelectionPath.getPath()) { if(o == null) { JOptionPane.showMessageDialog(viewComponent, "No file or folder selected.", "Information", JOptionPane.INFORMATION_MESSAGE); result = null; break; } else if(o instanceof DefaultMutableTreeNode) { Object o2 = ((DefaultMutableTreeNode) o).getUserObject(); if(o2 instanceof RecordContainer) { Record<A> rec = ((RecordContainer) o2).getRecord(genericPlaceholder); //result = new ArrayList<Record<A>>(1); //result.add(rec); result.add(rec); } else { JOptionPane.showMessageDialog(viewComponent, "[getTreeSelection()] Unexpected data in tree model: " + o2.getClass() + ". (Internal error, report to " + "developer)", "Error", JOptionPane.ERROR_MESSAGE); result = null; break; } } else { JOptionPane.showMessageDialog(viewComponent, "[getTreeSelection()] Unexpected tree node type: " + o.getClass() + "! (Internal error, report to developer)", "Error", JOptionPane.ERROR_MESSAGE); result = null; break; } } return result; }*/ /** * Returns the current user selection for the folder contents table. * * @return the current user selection for the folder contents table. */ private List<Record<A>> getTableSelection() { List<Record<A>> result; int[] selectedRows = fileTable.getSelectedRows(); if(selectedRows.length == 0) { JOptionPane.showMessageDialog(viewComponent, "No file selected.", "Information", JOptionPane.INFORMATION_MESSAGE); result = null; } else { ArrayList<Record<A>> actualResult = new ArrayList<Record<A>>(selectedRows.length); for(int i = 0; i < selectedRows.length; ++i) { Object o = fileTable.getValueAt(selectedRows[i], 0); if(o instanceof RecordContainer) { Record<A> rekk = ((RecordContainer) o).getRecord(genericPlaceholder); actualResult.add(rekk); } else { JOptionPane.showMessageDialog(viewComponent, "[getTableSelection()] Unexpected data in " + "table model. (Internal error, report to " + "developer)", "Error", JOptionPane.ERROR_MESSAGE); actualResult = null; break; } } result = actualResult; } return result; } /** * Returns whether or not a file system is loaded by the controller. If a * file system is not loaded, an error message dialog is displayed to notify * the user that the operation it requested could not be performed, and then * <code>false</code> is returned. * * @return whether or not a file system is loaded by the controller. */ private boolean ensureFileSystemLoaded() { if(controller.isFileSystemLoaded()) { return true; } else { //new Exception().printStackTrace(); JOptionPane.showMessageDialog(viewComponent, "No file system " + "loaded.", "Error", JOptionPane.ERROR_MESSAGE); return false; } } public void setRoot(Record<A> rootRecord) { final TreeNode rootNode; final List<Record<A>> rootRecordPath; if(rootRecord != null) { rootRecordPath = new ArrayList<Record<A>>(1); rootRecordPath.add(rootRecord); FolderTreeNode rootTreeNode = new FolderTreeNode(new RecordContainer(rootRecord)); populateTreeNodeFromPath(rootTreeNode, rootRecordPath); rootNode = rootTreeNode; } else { rootRecordPath = null; rootNode = new NoLeafMutableTreeNode("No file system loaded"); } treeModel = new DefaultTreeModel(rootNode); // System.err.print("Setting tree model..."); dirTree.setModel(treeModel); // System.err.println("done!"); lastTreeSelectionPath = new TreePath(rootNode); // System.err.print("Doing select in tree..."); selectInTree(lastTreeSelectionPath); // System.err.println("done!"); if(rootRecordPath != null) { populateTableFromPath(rootRecordPath); } else populateTableFromContents(new ArrayList<Record<A>>(0), ""); // System.err.print("Setting selection status..."); setSelectionStatus(0, 0); // System.err.println("done!"); } private void selectInTree(TreePath childPath) { if(childPath.getPathCount() > 1) dirTree.expandPath(childPath.getParentPath()); dirTree.setSelectionPath(childPath); dirTree.scrollPathToVisible(childPath); } /* * Notes on changing the current directory. * * Directories can be changed by: * - Clicking on the requested directory * - Typing the address in the address bar and pressing enter or pushing the "Go"-button * - Double-clicking a directory entry in the directory contents table * * Expected reaction: * - The directory contents table is populated with the contents of the requested directory * - The tree components leading up to the selected directory are expanded. * - The correct node in the directory tree is selected. No automatic expansion should take * place, except if there are no subdirectories to this node, in which case the node should * be expanded to remove the expansion sign. * - The address field is updated to reflect the currently selected directory. * * Action entry points: * - actionChangeDir - triggered by a double-click in the contents table * - * * What we need to do: * * Because of how events are triggered, * - Look up the required Record<A> entry for the directory * 1. For * - Look up the contents of the requested directory * * When a change directory-event is triggered, the following should take place: * * - */ private void setCurrentDirectory(String[] pathnameComponents) { if(DEBUG) { System.err.println("setCurrentDirectory(): printing " + "pathnameComponents"); for(int i = 0; i < pathnameComponents.length; ++i) { System.err.println(" [" + i + "]: " + pathnameComponents[i]); } } Object rootObj = treeModel.getRoot(); FolderTreeNode curNode; if(rootObj instanceof FolderTreeNode) { curNode = (FolderTreeNode) rootObj; } else throw new RuntimeException("Unexpected root node class: " + rootObj.getClass()); LinkedList<Record<A>> dirStack = new LinkedList<Record<A>>(); //LinkedList<FolderTreeNode> nodeStack = new LinkedList<FolderTreeNode>(); //nodeStack.addLast(curNode); TreePath treePath = new TreePath(curNode); for(String currentComponent : pathnameComponents) { //FolderTreeNode curNode = (FolderTreeNode) curObj; dirStack.addLast(curNode.getRecordContainer().getRecord(genericPlaceholder)); populateTreeNodeFromPath(curNode, dirStack); dirTree.expandPath(treePath); int childCount = treeModel.getChildCount(curNode); FolderTreeNode requestedNode = null; for(int i = 0; i < childCount; ++i) { Object curChild = treeModel.getChild(curNode, i); if(curChild instanceof FolderTreeNode) { FolderTreeNode curChildNode = (FolderTreeNode) curChild; Record<A> rec = curChildNode.getRecordContainer().getRecord(genericPlaceholder); if(rec.getName().equals(currentComponent)) { requestedNode = curChildNode; break; } } else { throw new RuntimeException("Unexpected tree node class: " + curChild.getClass()); } } if(requestedNode != null) { curNode = requestedNode; //nodeStack.addLast(curNode); treePath = treePath.pathByAddingChild(curNode); } else { //String dir = controller.getAddressPath(Arrays.asList(pathnameComponents)); JOptionPane.showMessageDialog(viewComponent, "No such directory.", "Error", JOptionPane.ERROR_MESSAGE); return; } } //TreePath tp = new TreePath(nodeStack.toArray(new FolderTreeNode[nodeStack.size()])); if(DEBUG) { System.err.println("setCurrentDirectory(): selecting the " + "following path in tree:"); for(Object o : treePath.getPath()) { System.err.print(" \"" + o.toString() + "\""); } System.err.println(); } selectInTree(treePath); } /** * This method is called each time the user makes/changes a selection in * the right pane. The resulting text is supposed to be printed somewhere * below the file system browser, but it's up to the controller to decide * where to display it.<br> * The text will look something like "3 objects selected (11,39 KiB)". * * @param selectedFilesCount number of files currently selected. * @param totalSize the total size of the selection. */ private void setSelectionStatus(long selectedFilesCount, long selectionSize) { String sizeString; if(selectionSize >= 1024) sizeString = SpeedUnitUtils.bytesToBinaryUnit(selectionSize, sizeFormat); else sizeString = selectionSize + " bytes"; statusLabel.setText(selectedFilesCount + ((selectedFilesCount==1)?" object":" objects") + " selected (" + sizeString + ")"); } public static enum RecordType { FILE, FOLDER, FILE_LINK, FOLDER_LINK, BROKEN_LINK; } public static class Record<A> { private RecordType type; private String name; private long size; private Date modifyDate; private boolean compressed; private A userObject; public Record(RecordType iType, String iName, long iSize, Date iModifyDate, boolean compressed, A iUserObject) { this.type = iType; this.name = iName; this.size = iSize; this.modifyDate = iModifyDate; this.compressed = compressed; this.userObject = iUserObject; } public RecordType getType() { return type; } public String getName() { return name; } public long getSize() { return size; } public Date getModifyDate() { return modifyDate; } public boolean isCompressed() { return compressed; } public A getUserObject() { return userObject; } } public static interface FileSystemProvider<A> { public void actionDoubleClickFile(List<Record<A>> fileRecordPath); public void actionExtractToDir(List<Record<A>> parentPath, List<Record<A>> recordList); public void actionGetInfo(List<Record<A>> parentPath, List<Record<A>> recordList); public JPopupMenu getRightClickRecordPopupMenu(List<Record<A>> parentPath, List<Record<A>> selectedRecords); public boolean isFileSystemLoaded(); public List<Record<A>> getFolderContents(List<Record<A>> folderRecordPath); public String getAddressPath(List<String> pathComponents); /** * Parses the string <code>targetAddress</code> as a path specifier in the context of the * current file system. For example, in a unix-like file system environment you would want * to parse the string "/usr/bin" to <code>{ "usr", "bin" }</code>, and for a Windows file * system you can choose to parse the path "\Windows\System32" or "C:\Windows\System32" as * <code>{ "Windows", "System32" }</code>. The parsing must be consistent with the result of * <code>getAddressPath</code>. * * @param targetAddress * @return the components of the address path if the parsing was successful, or * <code>null</code> if the target address string was invalid. */ public String[] parseAddressPath(String targetAddress); } /** Aggregation class for storage in the first column of fileTable. */ private static class RecordContainer implements Comparable { private Record rec; private RecordContainer() {} public RecordContainer(Record rec) { this.rec = rec; } /*public Record getRecord() { return rec; }*/ @SuppressWarnings("unchecked") public <T> Record<T> getRecord(GenericPlaceholder<T> placeholder) { return (Record<T>)rec; } @Override public String toString() { return rec.getName(); } /* @Override */ public int compareTo(Object o) { if(o instanceof RecordContainer) { RecordContainer rc = (RecordContainer)o; return toString().compareTo(rc.toString()); } else throw new RuntimeException("Can not compare a RecordContainer with a " + o.getClass()); } } /** * Wrapper for the size field in the table. */ private static class SizeEntry implements Comparable { private final String presentedSize; private final long trueSize; public SizeEntry(long trueSize) { this.trueSize = trueSize; this.presentedSize = SpeedUnitUtils.bytesToBinaryUnit(trueSize); } public long getSize() { return trueSize; } /* @Override */ public int compareTo(Object o) { if(o instanceof SizeEntry) { SizeEntry se = (SizeEntry) o; long res = trueSize - se.trueSize; if(res > 0) return 1; else if(res < 0) return -1; else return 0; } else throw new RuntimeException("Can not compare a SizeEntry with a " + o.getClass()); } @Override public String toString() { return presentedSize; } } public static class RecordTypeEntry implements Comparable { private final RecordType recordType; private final String displayString; public RecordTypeEntry(RecordType recordType) { this.recordType = recordType; switch(recordType) { case FILE: displayString = "File"; break; case FOLDER: displayString = "Folder"; break; case FILE_LINK: displayString = "File (symlink)"; break; case FOLDER_LINK: displayString = "Folder (symlink)"; break; case BROKEN_LINK: displayString = "Broken link"; break; default: throw new RuntimeException("INTERNAL ERROR: Encountered " + "unexpected record type (" + recordType + ")"); } } public RecordType getRecordType() { return recordType; } @Override public String toString() { return displayString; } private int getPriority() { switch(recordType) { case FOLDER: return 0; case FOLDER_LINK: return 1; case FILE: return 2; case FILE_LINK: return 3; case BROKEN_LINK: return 4; default: throw new RuntimeException("INTERNAL ERROR: Encountered " + "unexpected record type (" + recordType + ")"); } } /* @Override */ public int compareTo(Object o) { if(o instanceof RecordTypeEntry) { RecordTypeEntry rte = (RecordTypeEntry)o; return getPriority()-rte.getPriority(); } else throw new RuntimeException("Can not compare a RecordTypeEntry to a " + o.getClass()); } } private static class DateEntry implements Comparable { private final Date date; private final String displayString; public DateEntry(Date date, DateFormat formatter) { this.date = date; this.displayString = formatter.format(date); } public Date getDate() { return date; } @Override public String toString() { return displayString; } /* @Override */ public int compareTo(Object o) { if(o instanceof DateEntry) { DateEntry de = (DateEntry)o; return date.compareTo(de.date); } else throw new RuntimeException("Can not compare a DateEntry to a " + o.getClass()); } } private static class IndexEntry implements Comparable<IndexEntry> { private final int index; public IndexEntry(int index) { this.index = index; } /* @Override */ public int compareTo(IndexEntry o) { return index-o.index; } @Override public String toString() { return ""; } } private static class ComparableComparator implements Comparator<Comparable<Comparable>> { /* @Override */ public int compare(Comparable<Comparable> o1, Comparable<Comparable> o2) { //System.err.println("ComparableComparator comparing a " + o1.getClass() + " and a " + o2.getClass()); //if(o1 instanceof Comparable && o2 instanceof Comparable) { return o1.compareTo(o2); /*} else throw new UnsupportedOperationException("Trying to compare non-Comparables.");*/ } } public static class NoLeafMutableTreeNode extends DefaultMutableTreeNode { public NoLeafMutableTreeNode(Object userObject) { super(userObject); } /** Hack to avoid that JTree paints leaf nodes. We have no leafs, only dirs. */ @Override public boolean isLeaf() { return false; } } private static class FolderTreeNode extends NoLeafMutableTreeNode { private final RecordContainer rc; public FolderTreeNode(RecordContainer o) { super(o); rc = o; } public RecordContainer getRecordContainer() { return rc; } } private static class ObjectContainer<A> { public A o; public ObjectContainer(A o) { this.o = o; } } private static class GenericPlaceholder<A> {} }