001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: /*
043: * Outline.java
044: *
045: * Created on January 27, 2004, 6:59 PM
046: */
047:
048: package org.netbeans.swing.outline;
049:
050: import java.awt.Font;
051: import java.awt.FontMetrics;
052: import java.awt.Graphics;
053: import java.awt.Insets;
054: import java.awt.Rectangle;
055: import java.awt.event.ActionEvent;
056: import java.awt.event.ActionListener;
057: import java.awt.event.ComponentAdapter;
058: import java.awt.event.ComponentEvent;
059: import java.awt.event.ComponentListener;
060: import java.awt.event.MouseEvent;
061: import java.util.ArrayList;
062: import java.util.EventObject;
063: import java.util.List;
064: import javax.swing.JScrollBar;
065: import javax.swing.JScrollPane;
066: import javax.swing.JTable;
067: import javax.swing.JTree;
068: import javax.swing.JViewport;
069: import javax.swing.ListSelectionModel;
070: import javax.swing.Timer;
071: import javax.swing.UIManager;
072: import javax.swing.event.TableModelEvent;
073: import javax.swing.event.TreeModelEvent;
074: import javax.swing.table.TableCellRenderer;
075: import javax.swing.table.TableModel;
076: import javax.swing.tree.AbstractLayoutCache;
077: import javax.swing.tree.TreePath;
078:
079: /** An Outline, or tree-table component. Takes an instance of OutlineModel,
080: * an interface which merges TreeModel and TableModel.
081: * <p>
082: * Simplest usage:
083: * <ol>
084: * <li>Create a standard tree model for the tree node portion of the outline.</li>
085: * <li>Implement RowModel. RowModel is a subset of TableModel - it is passed
086: * the value in column 0 of the Outline and a column index, and returns the
087: * value in the column in question.</li>
088: * <li>Pass the TreeModel and the RowModel to <code>DefaultOutlineModel.createModel()</code>
089: * </ol>
090: * This will generate an instance of DefaultOutlineModel which will use the
091: * TreeModel for the rows/tree column content, and use the RowModel to provide
092: * the additional table columns.
093: * <p>
094: * It is also useful to provide an implementation of <code>RenderDataProvider</code>
095: * to supply icons and affect text display of cells - this covers most of the
096: * needs for which it is necessary to write a custom cell renderer in JTable/JTree.
097: * <p>
098: * <b>Example usage:</b><br>
099: * Assume FileTreeModel is a model which, given a root directory, will
100: * expose the files and folders underneath it. We will implement a
101: * RowModel to expose the file size and date, and a RenderDataProvider which
102: * will use a gray color for uneditable files and expose the full file path as
103: * a tooltip. Assume the class this is implemented in is a
104: * JPanel subclass or other Swing container.
105: * <br>
106: * XXX todo: clean up formatting & edit for style
107: * <pre>
108: * public void initComponents() {
109: * setLayout (new BorderLayout());
110: * TreeModel treeMdl = new FileTreeModel (someDirectory);
111: *
112: * OutlineModel mdl = DefaultOutlineModel.createOutlineModel(treeMdl,
113: * new FileRowModel(), true);
114: * outline = new Outline();
115: * outline.setRenderDataProvider(new FileDataProvider());
116: * outline.setRootVisible (true);
117: * outline.setModel (mdl);
118: * add (outline, BorderLayout.CENTER);
119: * }
120: * private class FileRowModel implements RowModel {
121: * public Class getColumnClass(int column) {
122: * switch (column) {
123: * case 0 : return Date.class;
124: * case 1 : return Long.class;
125: * default : assert false;
126: * }
127: * return null;
128: * }
129: *
130: * public int getColumnCount() {
131: * return 2;
132: * }
133: *
134: * public String getColumnName(int column) {
135: * return column == 0 ? "Date" : "Size";
136: * }
137: *
138: * public Object getValueFor(Object node, int column) {
139: * File f = (File) node;
140: * switch (column) {
141: * case 0 : return new Date (f.lastModified());
142: * case 1 : return new Long (f.length());
143: * default : assert false;
144: * }
145: * return null;
146: * }
147: *
148: * public boolean isCellEditable(Object node, int column) {
149: * return false;
150: * }
151: *
152: * public void setValueFor(Object node, int column, Object value) {
153: * //do nothing, nothing is editable
154: * }
155: * }
156: *
157: * private class FileDataProvider implements RenderDataProvider {
158: * public java.awt.Color getBackground(Object o) {
159: * return null;
160: * }
161: *
162: * public String getDisplayName(Object o) {
163: * return ((File) o).getName();
164: * }
165: *
166: * public java.awt.Color getForeground(Object o) {
167: * File f = (File) o;
168: * if (!f.isDirectory() && !f.canWrite()) {
169: * return UIManager.getColor ("controlShadow");
170: * }
171: * return null;
172: * }
173: *
174: * public javax.swing.Icon getIcon(Object o) {
175: * return null;
176: * }
177: *
178: * public String getTooltipText(Object o) {
179: * return ((File) o).getAbsolutePath();
180: * }
181: *
182: * public boolean isHtmlDisplayName(Object o) {
183: * return false;
184: * }
185: * }
186: * </pre>
187: *
188: * @author Tim Boudreau
189: */
190: public final class Outline extends JTable {
191: //XXX plenty of methods missing here - add/remove tree expansion listeners,
192: //better path info/queries, etc.
193:
194: private boolean initialized = false;
195: private Boolean cachedRootVisible = null;
196: private RenderDataProvider renderDataProvider = null;
197: private ComponentListener componentListener = null;
198:
199: /** Creates a new instance of Outline */
200: public Outline() {
201: init();
202: }
203:
204: public Outline(OutlineModel mdl) {
205: super (mdl);
206: init();
207: }
208:
209: private void init() {
210: initialized = true;
211: setDefaultRenderer(Object.class,
212: new DefaultOutlineCellRenderer());
213: }
214:
215: /** Always returns the default renderer for Object.class for the tree column */
216: public TableCellRenderer getCellRenderer(int row, int column) {
217: TableCellRenderer result;
218: if (column == 0) {
219: result = getDefaultRenderer(Object.class);
220: } else {
221: result = super .getCellRenderer(row, column);
222: }
223: return result;
224: }
225:
226: /** Get the RenderDataProvider which is providing text, icons and tooltips
227: * for items in the tree column. The default property for this value is
228: * null, in which case standard JTable/JTree object -> icon/string
229: * conventions are used */
230: public RenderDataProvider getRenderDataProvider() {
231: return renderDataProvider;
232: }
233:
234: /** Set the RenderDataProvider which will provide text, icons and tooltips
235: * for items in the tree column. The default is null. If null,
236: * the data displayed will be generated in the standard JTable/JTree way -
237: * calling <code>toString()</code> on objects in the tree model and
238: * using the look and feel's default tree folder and tree leaf icons. */
239: public void setRenderDataProvider(RenderDataProvider provider) {
240: if (provider != renderDataProvider) {
241: RenderDataProvider old = renderDataProvider;
242: renderDataProvider = provider;
243: firePropertyChange("renderDataProvider", old, provider); //NOI18N
244: }
245: }
246:
247: /** Get the TreePathSupport object which manages path expansion for this
248: * Outline. */
249: TreePathSupport getTreePathSupport() {
250: OutlineModel mdl = getOutlineModel();
251: if (mdl != null) {
252: return mdl.getTreePathSupport();
253: } else {
254: return null;
255: }
256: }
257:
258: /** Get the layout cache which manages layout data for the Outline.
259: * <strong>Under no circumstances directly call the methods on the
260: * layout cache which change the expanded state - such changes will not
261: * be propagated into the table model, and will leave the model and
262: * its layout in inconsistent states. Any calls that affect expanded
263: * state must go through <code>getTreePathSupport()</code>.</strong> */
264: AbstractLayoutCache getLayoutCache() {
265: OutlineModel mdl = getOutlineModel();
266: if (mdl != null) {
267: return mdl.getLayout();
268: } else {
269: return null;
270: }
271: }
272:
273: boolean isTreeColumnIndex(int column) {
274: //XXX fixme - this is not true if columns have been dragged
275: return column == 0;
276: }
277:
278: public boolean isVisible(TreePath path) {
279: if (getTreePathSupport() != null) {
280: return getTreePathSupport().isVisible(path);
281: }
282: return false;
283: }
284:
285: /** Overridden to pass the fixed row height to the tree layout cache */
286: public void setRowHeight(int val) {
287: super .setRowHeight(val);
288: if (getLayoutCache() != null) {
289: getLayoutCache().setRowHeight(val);
290: }
291: }
292:
293: /** Set whether or not the root is visible */
294: public void setRootVisible(boolean val) {
295: if (getOutlineModel() == null) {
296: cachedRootVisible = val ? Boolean.TRUE : Boolean.FALSE;
297: }
298: if (val != isRootVisible()) {
299: //TODO - need to force a property change on the model,
300: //the layout cache doesn't have direct listener support
301: getLayoutCache().setRootVisible(val);
302: firePropertyChange("rootVisible", !val, val); //NOI18N
303: }
304: }
305:
306: /** Is the tree root visible. Default value is true. */
307: public boolean isRootVisible() {
308: if (getLayoutCache() == null) {
309: return cachedRootVisible != null ? cachedRootVisible
310: .booleanValue() : true;
311: } else {
312: return getLayoutCache().isRootVisible();
313: }
314: }
315:
316: /** Overridden to throw an exception if the passed model is not an instance
317: * of <code>OutlineModel</code> (with the exception of calls from the
318: * superclass constructor) */
319: public void setModel(TableModel mdl) {
320: if (initialized && (!(mdl instanceof OutlineModel))) {
321: throw new IllegalArgumentException(
322: "Table model for an Outline must be an instance of "
323: + "OutlineModel"); //NOI18N
324: }
325: if (mdl instanceof OutlineModel) {
326: AbstractLayoutCache layout = ((OutlineModel) mdl)
327: .getLayout();
328: if (cachedRootVisible != null) {
329:
330: layout.setRootVisible(cachedRootVisible.booleanValue());
331:
332: }
333:
334: layout.setRowHeight(getRowHeight());
335:
336: if (((OutlineModel) mdl).isLargeModel()) {
337: addComponentListener(getComponentListener());
338: layout.setNodeDimensions(new ND());
339: } else {
340: if (componentListener != null) {
341: removeComponentListener(componentListener);
342: componentListener = null;
343: }
344: }
345: }
346:
347: super .setModel(mdl);
348: }
349:
350: /** Convenience getter for the <code>TableModel</code> as an instance of
351: * OutlineModel. If no OutlineModel has been set, returns null. */
352: public OutlineModel getOutlineModel() {
353: TableModel mdl = getModel();
354: if (mdl instanceof OutlineModel) {
355: return (OutlineModel) getModel();
356: } else {
357: return null;
358: }
359: }
360:
361: /** Expand a tree path */
362: public void expandPath(TreePath path) {
363: getTreePathSupport().expandPath(path);
364: }
365:
366: public void collapsePath(TreePath path) {
367: getTreePathSupport().collapsePath(path);
368: }
369:
370: public Rectangle getPathBounds(TreePath path) {
371: Insets i = getInsets();
372: Rectangle bounds = getLayoutCache().getBounds(path, null);
373:
374: if (bounds != null && i != null) {
375: bounds.x += i.left;
376: bounds.y += i.top;
377: }
378: return bounds;
379: }
380:
381: public TreePath getClosestPathForLocation(int x, int y) {
382: Insets i = getInsets();
383: if (i != null) {
384: return getLayoutCache().getPathClosestTo(x - i.left,
385: y - i.top);
386: } else {
387: return getLayoutCache().getPathClosestTo(x, y);
388: }
389: }
390:
391: public boolean editCellAt(int row, int column, EventObject e) {
392: //If it was on column 0, it may be a request to expand a tree
393: //node - check for that first.
394: if (isTreeColumnIndex(column) && e instanceof MouseEvent) {
395: MouseEvent me = (MouseEvent) e;
396: TreePath path = getLayoutCache().getPathClosestTo(
397: me.getX(), me.getY());
398: if (!getOutlineModel().isLeaf(path.getLastPathComponent())) {
399: int handleWidth = DefaultOutlineCellRenderer
400: .getExpansionHandleWidth();
401: Insets ins = getInsets();
402: int handleStart = ins.left
403: + ((path.getPathCount() - 1) * DefaultOutlineCellRenderer
404: .getNestingWidth());
405: int handleEnd = ins.left + handleStart + handleWidth;
406:
407: //TODO: Translate x/y to position of column if non-0
408:
409: if ((me.getX() > ins.left && me.getX() >= handleStart && me
410: .getX() <= handleEnd)
411: || me.getClickCount() > 1) {
412:
413: boolean expanded = getLayoutCache()
414: .isExpanded(path);
415: if (!expanded) {
416: getTreePathSupport().expandPath(path);
417: } else {
418: getTreePathSupport().collapsePath(path);
419: }
420: return false;
421: }
422: }
423: }
424:
425: return super .editCellAt(row, column, e);
426: }
427:
428: private boolean needCalcRowHeight = true;
429:
430: /** Calculate the height of rows based on the current font. This is
431: * done when the first paint occurs, to ensure that a valid Graphics
432: * object is available. */
433: private void calcRowHeight(Graphics g) {
434: //Users of themes can set an explicit row height, so check for it
435: Integer i = (Integer) UIManager
436: .get("netbeans.outline.rowHeight"); //NOI18N
437:
438: int rowHeight;
439: if (i != null) {
440: rowHeight = i.intValue();
441: } else {
442: //Derive a row height to accomodate the font and expando icon
443: Font f = getFont();
444: FontMetrics fm = g.getFontMetrics(f);
445: rowHeight = Math.max(fm.getHeight() + 3,
446: DefaultOutlineCellRenderer
447: .getExpansionHandleHeight());
448: }
449: //Clear the flag
450: needCalcRowHeight = false;
451: //Set row height. If displayable, this will generate a new call
452: //to paint()
453: setRowHeight(rowHeight);
454: }
455:
456: public void tableChanged(TableModelEvent e) {
457: // System.err.println("Table got tableChanged " + e);
458: super .tableChanged(e);
459: // System.err.println("row count is " + getRowCount());
460: }
461:
462: public void paint(Graphics g) {
463: if (needCalcRowHeight) {
464: calcRowHeight(g);
465: //CalcRowHeight will trigger a repaint
466: return;
467: }
468: super .paint(g);
469: }
470:
471: /** Create a component listener to handle size changes if the table model
472: * is large-model */
473: private ComponentListener getComponentListener() {
474: if (componentListener == null) {
475: componentListener = new SizeManager();
476: }
477: return componentListener;
478: }
479:
480: private JScrollPane getScrollPane() {
481: JScrollPane result = null;
482: if (getParent() instanceof JViewport) {
483: if (((JViewport) getParent()).getParent() instanceof JScrollPane) {
484: result = (JScrollPane) ((JViewport) getParent())
485: .getParent();
486: }
487: }
488: return result;
489: }
490:
491: private void change() {
492: revalidate();
493: repaint();
494: }
495:
496: private class ND extends AbstractLayoutCache.NodeDimensions {
497:
498: public Rectangle getNodeDimensions(Object value, int row,
499: int depth, boolean expanded, Rectangle bounds) {
500:
501: int wid = Outline.this .getColumnModel().getColumn(0)
502: .getPreferredWidth();
503: bounds.setBounds(0, row * getRowHeight(), wid,
504: getRowHeight());
505: return bounds;
506: }
507:
508: }
509:
510: /** A component listener. If we're a large model table, we need
511: * to inform the FixedHeightLayoutCache when the size changes, so it
512: * can update its mapping of visible nodes */
513: private class SizeManager extends ComponentAdapter implements
514: ActionListener {
515: protected Timer timer = null;
516: protected JScrollBar scrollBar = null;
517:
518: public void componentMoved(ComponentEvent e) {
519: if (timer == null) {
520: JScrollPane scrollPane = getScrollPane();
521:
522: if (scrollPane == null) {
523: change();
524: } else {
525: scrollBar = scrollPane.getVerticalScrollBar();
526: if (scrollBar == null
527: || !scrollBar.getValueIsAdjusting()) {
528: // Try the horizontal scrollbar.
529: if ((scrollBar = scrollPane
530: .getHorizontalScrollBar()) != null
531: && scrollBar.getValueIsAdjusting()) {
532:
533: startTimer();
534: } else {
535: change();
536: }
537: } else {
538: startTimer();
539: }
540: }
541: }
542: }
543:
544: protected void startTimer() {
545: if (timer == null) {
546: timer = new Timer(200, this );
547: timer.setRepeats(true);
548: }
549: timer.start();
550: }
551:
552: public void actionPerformed(ActionEvent ae) {
553: if (scrollBar == null || !scrollBar.getValueIsAdjusting()) {
554: if (timer != null)
555: timer.stop();
556: change();
557: timer = null;
558: scrollBar = null;
559: }
560: }
561:
562: public void componentHidden(ComponentEvent e) {
563: }
564:
565: public void componentResized(ComponentEvent e) {
566: }
567:
568: public void componentShown(ComponentEvent e) {
569: }
570: }
571: }
|