001: package net.sourceforge.squirrel_sql.client.session.mainpanel.objecttree;
002:
003: /*
004: * Copyright (C) 2002-2004 Colin Bell
005: * colbell@users.sourceforge.net
006: *
007: * This library is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU Lesser General Public
009: * License as published by the Free Software Foundation; either
010: * version 2.1 of the License, or (at your option) any later version.
011: *
012: * This library is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: *
017: * You should have received a copy of the GNU Lesser General Public
018: * License along with this library; if not, write toS the Free Software
019: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
020: */
021: import java.awt.event.MouseAdapter;
022: import java.awt.event.MouseEvent;
023: import java.io.Serializable;
024: import java.sql.SQLException;
025: import java.util.ArrayList;
026: import java.util.Arrays;
027: import java.util.Comparator;
028: import java.util.Enumeration;
029: import java.util.HashMap;
030: import java.util.Iterator;
031: import java.util.List;
032: import java.util.Map;
033: import java.util.Set;
034:
035: import javax.swing.Action;
036: import javax.swing.JMenu;
037: import javax.swing.JPopupMenu;
038: import javax.swing.JTree;
039: import javax.swing.SwingUtilities;
040: import javax.swing.ToolTipManager;
041: import javax.swing.event.EventListenerList;
042: import javax.swing.event.TreeExpansionEvent;
043: import javax.swing.event.TreeExpansionListener;
044: import javax.swing.event.TreeSelectionEvent;
045: import javax.swing.event.TreeSelectionListener;
046: import javax.swing.tree.DefaultMutableTreeNode;
047: import javax.swing.tree.TreePath;
048:
049: import net.sourceforge.squirrel_sql.client.action.ActionCollection;
050: import net.sourceforge.squirrel_sql.client.session.ISession;
051: import net.sourceforge.squirrel_sql.client.session.action.CopyQualifiedObjectNameAction;
052: import net.sourceforge.squirrel_sql.client.session.action.CopySimpleObjectNameAction;
053: import net.sourceforge.squirrel_sql.client.session.action.DeleteSelectedTablesAction;
054: import net.sourceforge.squirrel_sql.client.session.action.EditWhereColsAction;
055: import net.sourceforge.squirrel_sql.client.session.action.FilterObjectsAction;
056: import net.sourceforge.squirrel_sql.client.session.action.RefreshObjectTreeItemAction;
057: import net.sourceforge.squirrel_sql.client.session.action.RefreshSchemaInfoAction;
058: import net.sourceforge.squirrel_sql.client.session.action.SQLFilterAction;
059: import net.sourceforge.squirrel_sql.client.session.action.SetDefaultCatalogAction;
060: import net.sourceforge.squirrel_sql.fw.gui.CursorChanger;
061: import net.sourceforge.squirrel_sql.fw.gui.GUIUtils;
062: import net.sourceforge.squirrel_sql.fw.id.IIdentifier;
063: import net.sourceforge.squirrel_sql.fw.sql.DatabaseObjectInfo;
064: import net.sourceforge.squirrel_sql.fw.sql.DatabaseObjectType;
065: import net.sourceforge.squirrel_sql.fw.sql.IDatabaseObjectInfo;
066: import net.sourceforge.squirrel_sql.fw.sql.ITableInfo;
067: import net.sourceforge.squirrel_sql.fw.util.EnumerationIterator;
068: import net.sourceforge.squirrel_sql.fw.util.log.ILogger;
069: import net.sourceforge.squirrel_sql.fw.util.log.LoggerController;
070:
071: /**
072: * This is the tree showing the structure of objects in the database.
073: *
074: * @author <A HREF="mailto:colbell@users.sourceforge.net">Colin Bell</A>
075: */
076: class ObjectTree extends JTree {
077: private static final long serialVersionUID = 1L;
078:
079: /** Logger for this class. */
080: private static final ILogger s_log = LoggerController
081: .createLogger(ObjectTree.class);
082:
083: /** Model for this tree. */
084: private final ObjectTreeModel _model;
085:
086: /** Current session. */
087: transient private final ISession _session;
088:
089: /**
090: * Collection of popup menus (<TT>JPopupMenu</TT> instances) for the
091: * object tree. Keyed by node type.
092: */
093: private final Map<IIdentifier, JPopupMenu> _popups = new HashMap<IIdentifier, JPopupMenu>();
094:
095: /**
096: * Global popup menu. This contains items that are to be displayed
097: * in the popup menu no matter what items are selected in the tree.
098: */
099: private final JPopupMenu _globalPopup = new JPopupMenu();
100:
101: private final List<Action> _globalActions = new ArrayList<Action>();
102:
103: /**
104: * Object to synchronize on so that only one node can be expanded at any
105: * one time.
106: */
107: private Object _syncObject = new Object();
108:
109: /**
110: * String representation of the <TT>TreePath</TT> objects that have been
111: * expanded. The key is <TT>Treepath.toString()</TT> and the value
112: * is <TT>null</TT>.
113: */
114: private Map<String, Object> _expandedPathNames = new HashMap<String, Object>();
115:
116: /**
117: * Collection of listeners to this object tree.
118: */
119: private EventListenerList _listenerList = new EventListenerList();
120:
121: private boolean _startExpandInThread = true;
122:
123: /**
124: * ctor specifying session.
125: *
126: * @param session Current session.
127: *
128: * @throws IllegalArgumentException
129: * Thrown if <TT>null</TT> <TT>ISession</TT> passed.
130: */
131: ObjectTree(ISession session) {
132: super (new ObjectTreeModel(session));
133: if (session == null) {
134: throw new IllegalArgumentException("ISession == null");
135: }
136: setRowHeight(getFontMetrics(getFont()).getHeight());
137: _session = session;
138: _model = (ObjectTreeModel) getModel();
139: setModel(_model);
140:
141: addTreeExpansionListener(new NodeExpansionListener());
142:
143: addTreeSelectionListener(new TreeSelectionListener() {
144: public void valueChanged(TreeSelectionEvent e) {
145: if (null != e.getNewLeadSelectionPath()) {
146: scrollPathToVisible(e.getNewLeadSelectionPath());
147: }
148: }
149: });
150:
151: setShowsRootHandles(true);
152:
153: // Add actions to the popup menu.
154: final ActionCollection actions = session.getApplication()
155: .getActionCollection();
156:
157: // Options for global popup menu.
158: addToPopup(actions.get(RefreshSchemaInfoAction.class));
159: addToPopup(actions.get(RefreshObjectTreeItemAction.class));
160:
161: addToPopup(DatabaseObjectType.TABLE, actions
162: .get(EditWhereColsAction.class));
163:
164: addToPopup(DatabaseObjectType.TABLE, actions
165: .get(SQLFilterAction.class));
166: addToPopup(DatabaseObjectType.VIEW, actions
167: .get(SQLFilterAction.class));
168:
169: addToPopup(DatabaseObjectType.TABLE, actions
170: .get(DeleteSelectedTablesAction.class));
171:
172: addToPopup(DatabaseObjectType.SESSION, actions
173: .get(FilterObjectsAction.class));
174:
175: session.getApplication().getThreadPool().addTask(
176: new Runnable() {
177: public void run() {
178: try {
179: // Option to select default catalog only applies to sessions
180: // that support catalogs.
181: if (_session.getSQLConnection()
182: .getSQLMetaData()
183: .supportsCatalogs()) {
184: SwingUtilities
185: .invokeLater(new Runnable() {
186: public void run() {
187: addToPopup(
188: DatabaseObjectType.CATALOG,
189: actions
190: .get(SetDefaultCatalogAction.class));
191: }
192:
193: });
194: }
195: } catch (Throwable th) {
196: // Assume DBMS doesn't support catalogs.
197: s_log.debug(th);
198: }
199:
200: SwingUtilities.invokeLater(new Runnable() {
201: public void run() {
202: addToPopup(actions
203: .get(CopySimpleObjectNameAction.class));
204: addToPopup(actions
205: .get(CopyQualifiedObjectNameAction.class));
206:
207: // Mouse listener used to display popup menu.
208: addMouseListener(new MouseAdapter() {
209: public void mousePressed(
210: MouseEvent evt) {
211: if (evt.isPopupTrigger()) {
212: showPopup(evt.getX(), evt
213: .getY());
214: }
215: }
216:
217: public void mouseReleased(
218: MouseEvent evt) {
219: if (evt.isPopupTrigger()) {
220: showPopup(evt.getX(), evt
221: .getY());
222: }
223: }
224: });
225:
226: setCellRenderer(new ObjectTreeCellRenderer(
227: _model, _session));
228: ObjectTree.this .refresh(false);
229: ObjectTree.this
230: .setSelectionPath(ObjectTree.this
231: .getPathForRow(0));
232: }
233: });
234: }
235: });
236:
237: }
238:
239: /**
240: * Component has been added to its parent.
241: */
242: public void addNotify() {
243: super .addNotify();
244: // Register so that we can display different tooltips depending
245: // which entry in list mouse is over.
246: ToolTipManager.sharedInstance().registerComponent(this );
247: }
248:
249: /**
250: * Component has been removed from its parent.
251: */
252: public void removeNotify() {
253: super .removeNotify();
254:
255: // Don't need tooltips any more.
256: ToolTipManager.sharedInstance().unregisterComponent(this );
257: }
258:
259: /**
260: * Return the name of the object that the mouse is currently
261: * over as the tooltip text.
262: *
263: * @param event Used to determine the current mouse position.
264: */
265: public String getToolTipText(MouseEvent evt) {
266: String tip = null;
267: final TreePath path = getPathForLocation(evt.getX(), evt.getY());
268: if (path != null) {
269: tip = path.getLastPathComponent().toString();
270: } else {
271: tip = getToolTipText();
272: }
273: return tip;
274: }
275:
276: /**
277: * Return the typed data model for this tree.
278: *
279: * @return The typed data model for this tree.
280: */
281: public ObjectTreeModel getTypedModel() {
282: return _model;
283: }
284:
285: /**
286: * Refresh tree.
287: * @param reloadSchemaInfo
288: */
289: public void refresh(final boolean reloadSchemaInfo) {
290: Runnable task = new Runnable() {
291: public void run() {
292: if (reloadSchemaInfo) {
293: _session.getSchemaInfo().reloadAll();
294: }
295:
296: GUIUtils.processOnSwingEventThread(new Runnable() {
297: public void run() {
298: refreshTree();
299: }
300: });
301: }
302: };
303:
304: if (reloadSchemaInfo) {
305: _session.getApplication().getThreadPool().addTask(task);
306: } else {
307: // No need to this in background when SchemaInfo is not reloaded.
308: task.run();
309: }
310: }
311:
312: private void refreshTree() {
313: final TreePath[] selectedPaths = getSelectionPaths();
314: final Map<String, Object> selectedPathNames = new HashMap<String, Object>();
315: if (selectedPaths != null) {
316: for (int i = 0; i < selectedPaths.length; ++i) {
317: selectedPathNames
318: .put(selectedPaths[i].toString(), null);
319: }
320: }
321: ObjectTreeNode root = _model.getRootObjectTreeNode();
322: root.removeAllChildren();
323: fireObjectTreeCleared();
324: startExpandingTree(root, false, selectedPathNames, false, true);
325: fireObjectTreeRefreshed();
326: }
327:
328: /**
329: * Refresh the nodes currently selected in the object tree.
330: */
331: public void refreshSelectedNodes() {
332:
333: final TreePath[] selectedPaths = getSelectionPaths();
334: ObjectTreeNode[] nodes = getSelectedNodes();
335: final Map<String, Object> selectedPathNames = new HashMap<String, Object>();
336: if (selectedPaths != null) {
337: for (int i = 0; i < selectedPaths.length; ++i) {
338: selectedPathNames
339: .put(selectedPaths[i].toString(), null);
340: }
341: }
342: clearSelection();
343:
344: DefaultMutableTreeNode parent = (DefaultMutableTreeNode) nodes[0]
345: .getParent();
346:
347: if (parent != null) {
348: parent.removeAllChildren();
349: startExpandingTree((ObjectTreeNode) parent, false,
350: selectedPathNames, true, true);
351: } else {
352: nodes[0].removeAllChildren();
353: startExpandingTree(nodes[0], false, selectedPathNames,
354: true, true);
355: }
356: }
357:
358: /**
359: * Adds a listener for changes in this cache entry.
360: *
361: * @param lis a IObjectCacheChangeListener that will be notified when
362: * objects are added and removed from this cache entry.
363: */
364: public void addObjectTreeListener(IObjectTreeListener lis) {
365: _listenerList.add(IObjectTreeListener.class, lis);
366: }
367:
368: /**
369: * Removes a listener for changes in this cache entry.
370: *
371: * @param lis a IObjectCacheChangeListener that will be notified when
372: * objects are added and removed from this cache entry.
373: */
374: void removeObjectTreeListener(IObjectTreeListener lis) {
375: _listenerList.remove(IObjectTreeListener.class, lis);
376: }
377:
378: /**
379: * Restore the expansion state of the tree starting at the passed node.
380: * The passed node is always expanded.
381: *
382: * @param node Node to restore expansion state from.
383: *
384: * @throws IllegalArgumentException
385: * Thrown if null ObjectTreeNode passed.
386: */
387: private void restoreExpansionState(ObjectTreeNode node,
388: Map<String, Object> previouslySelectedTreePathNames,
389: List<TreePath> selectedTreePaths) {
390: if (node == null) {
391: throw new IllegalArgumentException("ObjectTreeNode == null");
392: }
393:
394: final TreePath nodePath = new TreePath(node.getPath());
395: if (matchKeyPrefix(previouslySelectedTreePathNames, node,
396: nodePath.toString())) {
397: selectedTreePaths.add(nodePath);
398: }
399:
400: try {
401: _startExpandInThread = false;
402: expandPath(nodePath);
403: } finally {
404: _startExpandInThread = true;
405: }
406:
407: // Go through each child of the parent and see if it was previously
408: // expanded. If it was recursively call this method in order to expand
409: // the child.
410:
411: Enumeration<ObjectTreeNode> childEnumeration = (Enumeration<ObjectTreeNode>) node
412: .children();
413: Iterator<ObjectTreeNode> it = new EnumerationIterator<ObjectTreeNode>(
414: childEnumeration);
415: while (it.hasNext()) {
416: final ObjectTreeNode child = it.next();
417: final TreePath childPath = new TreePath(child.getPath());
418: final String childPathName = childPath.toString();
419:
420: if (matchKeyPrefix(previouslySelectedTreePathNames, child,
421: childPathName)) {
422: selectedTreePaths.add(childPath);
423: }
424:
425: if (_expandedPathNames.containsKey(childPathName)) {
426: restoreExpansionState(child,
427: previouslySelectedTreePathNames,
428: selectedTreePaths);
429: }
430: }
431: }
432:
433: /**
434: * This is to handle the case where the user has enabled showRowCounts and
435: * the table/view name as it appeared before is different only because the
436: * number of rows has changed. For example, suppose a user deletes records
437: * in a table "foo" with 100 rows then refreshes the tree. The tree node
438: * before the delete looks like foo(100) and after looks like foo(0). We
439: * want to strip off the (...) and test to see if the selected path "foo"
440: * is the same before the delete as after. This way, when the user refreshes
441: * "foo(...)", then it is still selected after the refresh.
442: *
443: * @param map
444: * @param pattern
445: * @return
446: */
447: protected boolean matchKeyPrefix(Map<String, Object> map,
448: ObjectTreeNode node, String path) {
449: // We only show row counts for tables and views. Other objects won't
450: // be affected by changing row counts.
451: if (node.getDatabaseObjectType() != DatabaseObjectType.TABLE
452: && node.getDatabaseObjectType() != DatabaseObjectType.VIEW) {
453: return map.containsKey(path);
454: }
455: Set<String> s = map.keySet();
456: Iterator<String> i = s.iterator();
457: String pathPrefix = path;
458: if (path.indexOf("(") != -1) {
459: pathPrefix = path.substring(0, path.lastIndexOf("("));
460: }
461: boolean result = false;
462: while (i.hasNext()) {
463: String key = i.next();
464: String keyPrefix = key;
465: if (key.indexOf("(") != -1) {
466: keyPrefix = key.substring(0, key.lastIndexOf("("));
467: }
468: if (keyPrefix.equals(pathPrefix)) {
469: result = true;
470: break;
471: }
472: }
473: return result;
474: }
475:
476: private void startExpandingTree(ObjectTreeNode node,
477: boolean selectNode, Map<String, Object> selectedPathNames,
478: boolean refreshSchemaInfo, boolean startExpandInThread) {
479: ExpansionController exp = new ExpansionController(node,
480: selectNode, selectedPathNames, refreshSchemaInfo);
481:
482: // This is a potential fix for Bug #1752089. I couldn't fiond any reason for this thread.
483: // If this proves to be right over some time remove the threading stuff.
484: // if (SwingUtilities.isEventDispatchThread() && startExpandInThread)
485: // {
486: // _session.getApplication().getThreadPool().addTask(exp);
487: // }
488: // else
489: {
490: exp.run();
491: }
492: }
493:
494: private void expandNode(ObjectTreeNode node, boolean selectNode) {
495: if (node == null) {
496: throw new IllegalArgumentException("ObjectTreeNode == null");
497: }
498: // If node hasn't already been expanded.
499: if (node.getChildCount() == 0) {
500: // Add together the standard expanders for this node type and any
501: // individual expanders that there are for the node and process them.
502: final DatabaseObjectType dboType = node
503: .getDatabaseObjectType();
504: INodeExpander[] stdExpanders = _model.getExpanders(dboType);
505: INodeExpander[] extraExpanders = node.getExpanders();
506: if (stdExpanders.length > 0 || extraExpanders.length > 0) {
507: INodeExpander[] expanders = null;
508: if (stdExpanders.length > 0
509: && extraExpanders.length == 0) {
510: expanders = stdExpanders;
511: } else if (stdExpanders.length == 0
512: && extraExpanders.length > 0) {
513: expanders = extraExpanders;
514: } else {
515: expanders = new INodeExpander[stdExpanders.length
516: + extraExpanders.length];
517: System.arraycopy(stdExpanders, 0, expanders, 0,
518: stdExpanders.length);
519: System.arraycopy(extraExpanders, 0, expanders,
520: stdExpanders.length, extraExpanders.length);
521: }
522: new TreeLoader(node, expanders, selectNode).execute();
523: }
524: }
525: }
526:
527: /**
528: * Add an item to the popup menu for the specified node type in the object
529: * tree.
530: *
531: * @param dboType Database Object Type.
532: * @param action Action to add to menu.
533: *
534: * @throws IllegalArgumentException
535: * Thrown if a <TT>null</TT> <TT>Action</TT> or
536: * <TT>DatabaseObjectType</TT>thrown.
537: */
538: void addToPopup(DatabaseObjectType dboType, Action action) {
539: if (dboType == null) {
540: throw new IllegalArgumentException(
541: "Null DatabaseObjectType passed");
542: }
543: if (action == null) {
544: throw new IllegalArgumentException("Null Action passed");
545: }
546:
547: final JPopupMenu pop = getPopup(dboType, true);
548: pop.add(action);
549: }
550:
551: /**
552: * Add an item to the popup menu for the all nodes.
553: *
554: * @param action Action to add to menu.
555: *
556: * @throws IllegalArgumentException
557: * Thrown if a <TT>null</TT> <TT>Action</TT> thrown.
558: */
559: void addToPopup(Action action) {
560: if (action == null) {
561: throw new IllegalArgumentException("Null Action passed");
562: }
563: _globalPopup.add(action);
564: _globalActions.add(action);
565:
566: for (Iterator<JPopupMenu> it = _popups.values().iterator(); it
567: .hasNext();) {
568: JPopupMenu pop = it.next();
569: pop.add(action);
570: }
571: }
572:
573: /**
574: * Add an hierarchical menu to the popup menu for the specified database
575: * object type.
576: *
577: * @param dboType Database object type.
578: * @param menu <TT>JMenu</TT> to add to menu.
579: *
580: * @throws IllegalArgumentException
581: * Thrown if a <TT>null</TT> <TT>DatabaseObjectType</TT> or
582: * <TT>JMenu</TT> thrown.
583: */
584: public void addToPopup(DatabaseObjectType dboType, JMenu menu) {
585: if (dboType == null) {
586: throw new IllegalArgumentException(
587: "DatabaseObjectType == null");
588: }
589: if (menu == null) {
590: throw new IllegalArgumentException("JMenu == null");
591: }
592:
593: final JPopupMenu pop = getPopup(dboType, true);
594: pop.add(menu);
595: }
596:
597: /**
598: * Add an hierarchical menu to the popup menu for all node types.
599: *
600: * @param menu <TT>JMenu</TT> to add to menu.
601: *
602: * @throws IllegalArgumentException
603: * Thrown if a <TT>null</TT> <TT>JMenu</TT> thrown.
604: */
605: public void addToPopup(JMenu menu) {
606: if (menu == null) {
607: throw new IllegalArgumentException("JMenu == null");
608: }
609: _globalPopup.add(menu);
610: _globalActions.add(menu.getAction());
611:
612: for (Iterator<JPopupMenu> it = _popups.values().iterator(); it
613: .hasNext();) {
614: JPopupMenu pop = it.next();
615: pop.add(menu);
616: }
617: }
618:
619: /**
620: * Get the popup menu for the passed database object type. If one
621: * doesn't exist then create one if requested to do so.
622:
623: * @param dboType Database Object Type.
624: * @param create If <TT>true</TT> popup will eb created if it
625: * doesn't exist.
626: *
627: * @throws IllegalArgumentException
628: * Thrown if a <TT>null</TT> <TT>Action</TT> or
629: * <TT>DatabaseObjectType</TT>thrown.
630: */
631: private JPopupMenu getPopup(DatabaseObjectType dboType,
632: boolean create) {
633: if (dboType == null) {
634: throw new IllegalArgumentException(
635: "Null DatabaseObjectType passed");
636: }
637: IIdentifier key = dboType.getIdentifier();
638: JPopupMenu pop = _popups.get(key);
639: if (pop == null && create) {
640: pop = new JPopupMenu();
641: _popups.put(key, pop);
642: for (Iterator<Action> it = _globalActions.iterator(); it
643: .hasNext();) {
644: pop.add(it.next());
645: }
646: }
647: return pop;
648: }
649:
650: /**
651: * Return an array of the currently selected nodes. This array is sorted
652: * by the simple name of the database object.
653: *
654: * @return array of <TT>ObjectTreeNode</TT> objects.
655: */
656: ObjectTreeNode[] getSelectedNodes() {
657: TreePath[] paths = getSelectionPaths();
658: List<ObjectTreeNode> list = new ArrayList<ObjectTreeNode>();
659: if (paths != null) {
660: for (int i = 0; i < paths.length; ++i) {
661: Object obj = paths[i].getLastPathComponent();
662: if (obj instanceof ObjectTreeNode) {
663: list.add((ObjectTreeNode) obj);
664: }
665: }
666: }
667: ObjectTreeNode[] ar = list.toArray(new ObjectTreeNode[list
668: .size()]);
669: Arrays.sort(ar, new NodeComparator());
670: return ar;
671: }
672:
673: /**
674: * Return an array of the currently selected database
675: * objects.
676: *
677: * @return array of <TT>ObjectTreeNode</TT> objects.
678: */
679: IDatabaseObjectInfo[] getSelectedDatabaseObjects() {
680: ObjectTreeNode[] nodes = getSelectedNodes();
681: IDatabaseObjectInfo[] dbObjects = new IDatabaseObjectInfo[nodes.length];
682: for (int i = 0; i < nodes.length; ++i) {
683: dbObjects[i] = nodes[i].getDatabaseObjectInfo();
684: }
685: return dbObjects;
686: }
687:
688: /**
689: * Return a type-safe list of the currently selected database tables
690: *
691: * @return list of <TT>ITableInfo</TT> objects.
692: */
693: List<ITableInfo> getSelectedTables() {
694: ObjectTreeNode[] nodes = getSelectedNodes();
695: ArrayList<ITableInfo> result = new ArrayList<ITableInfo>();
696: for (int i = 0; i < nodes.length; ++i) {
697: if (nodes[i].getDatabaseObjectType() == DatabaseObjectType.TABLE) {
698: result.add((ITableInfo) nodes[i]
699: .getDatabaseObjectInfo());
700: }
701: }
702: return result;
703: }
704:
705: /**
706: * Get the appropriate popup menu for the currently selected nodes
707: * in the object tree and display it.
708: *
709: * @param x X pos to display popup at.
710: * @param y Y pos to display popup at.
711: */
712: private void showPopup(int x, int y) {
713: ObjectTreeNode[] selObj = getSelectedNodes();
714: if (selObj.length > 0) {
715: // See if all selected nodes are of the same type.
716: boolean sameType = true;
717: final DatabaseObjectType dboType = selObj[0]
718: .getDatabaseObjectType();
719: for (int i = 1; i < selObj.length; ++i) {
720: if (selObj[i].getDatabaseObjectType() != dboType) {
721: sameType = false;
722: break;
723: }
724: }
725:
726: JPopupMenu pop = null;
727: if (sameType) {
728: pop = getPopup(dboType, false);
729: }
730: if (pop == null) {
731: pop = _globalPopup;
732: }
733: pop.show(this , x, y);
734: }
735: }
736:
737: /**
738: * Fire a "tree cleared" event to all listeners.
739: */
740: private void fireObjectTreeCleared() {
741: // Guaranteed to be non-null.
742: Object[] listeners = _listenerList.getListenerList();
743: // Process the listeners last to first, notifying
744: // those that are interested in this event.
745: ObjectTreeListenerEvent evt = null;
746: for (int i = listeners.length - 2; i >= 0; i -= 2) {
747: if (listeners[i] == IObjectTreeListener.class) {
748: // Lazily create the event.
749: if (evt == null) {
750: evt = new ObjectTreeListenerEvent(ObjectTree.this );
751: }
752: ((IObjectTreeListener) listeners[i + 1])
753: .objectTreeCleared(evt);
754: }
755: }
756: }
757:
758: /**
759: * Fire a "tree refreshed" event to all listeners.
760: */
761: private void fireObjectTreeRefreshed() {
762: // Guaranteed to be non-null.
763: Object[] listeners = _listenerList.getListenerList();
764: // Process the listeners last to first, notifying
765: // those that are interested in this event.
766: ObjectTreeListenerEvent evt = null;
767: for (int i = listeners.length - 2; i >= 0; i -= 2) {
768: if (listeners[i] == IObjectTreeListener.class) {
769: // Lazily create the event.
770: if (evt == null) {
771: evt = new ObjectTreeListenerEvent(ObjectTree.this );
772: }
773: ((IObjectTreeListener) listeners[i + 1])
774: .objectTreeRefreshed(evt);
775: }
776: }
777: }
778:
779: public void dispose() {
780: // Menues that are also shown in the main window Session menu might
781: // be in this popup. If we don't remove them, the Session won't be Garbage Collected.
782: _globalPopup.removeAll();
783: _globalPopup.setInvoker(null);
784: _globalActions.clear();
785: for (Iterator<JPopupMenu> i = _popups.values().iterator(); i
786: .hasNext();) {
787: JPopupMenu popup = i.next();
788: popup.removeAll();
789: popup.setInvoker(null);
790: }
791: _popups.clear();
792: }
793:
794: private final class NodeExpansionListener implements
795: TreeExpansionListener {
796: public void treeExpanded(TreeExpansionEvent evt) {
797: // Get the node to be expanded.
798: final TreePath path = evt.getPath();
799: final Object parentObj = path.getLastPathComponent();
800: if (parentObj instanceof ObjectTreeNode) {
801: startExpandingTree((ObjectTreeNode) parentObj, false,
802: null, false, _startExpandInThread);
803: _expandedPathNames.put(path.toString(), null);
804: }
805: }
806:
807: public void treeCollapsed(TreeExpansionEvent evt) {
808: _expandedPathNames.remove(evt.getPath().toString());
809: }
810: }
811:
812: /**
813: * This class is used to sort the nodes by their title.
814: */
815: private static class NodeComparator implements
816: Comparator<ObjectTreeNode>, Serializable {
817: private static final long serialVersionUID = 1L;
818:
819: public int compare(ObjectTreeNode obj1, ObjectTreeNode obj2) {
820: return obj1.toString().compareToIgnoreCase(obj2.toString());
821: }
822: }
823:
824: private class ExpansionController implements Runnable {
825: private final ObjectTreeNode _node;
826: private final boolean _selectNode;
827: private final Map<String, Object> _selectedPathNames;
828: private boolean _refreshSchemaInfo;
829:
830: ExpansionController(ObjectTreeNode node, boolean selectNode,
831: Map<String, Object> selectedPathNames,
832: boolean refreshSchemaInfo) {
833: super ();
834: _node = node;
835: _selectNode = selectNode;
836: _selectedPathNames = selectedPathNames;
837: _refreshSchemaInfo = refreshSchemaInfo;
838: }
839:
840: public void run() {
841: synchronized (ObjectTree.this ._syncObject) {
842: CursorChanger cursorChg = new CursorChanger(
843: ObjectTree.this );
844: cursorChg.show();
845: try {
846: if (_refreshSchemaInfo) {
847: _session.getSchemaInfo().reload(
848: _node.getDatabaseObjectInfo());
849: }
850:
851: expandNode(_node, _selectNode);
852: if (_selectedPathNames != null) {
853: final List<TreePath> newlySelectedTreepaths = new ArrayList<TreePath>();
854:
855: GUIUtils
856: .processOnSwingEventThread(new Runnable() {
857: public void run() {
858: restoreExpansionState(_node,
859: _selectedPathNames,
860: newlySelectedTreepaths);
861: setSelectionPaths(newlySelectedTreepaths
862: .toArray(new TreePath[newlySelectedTreepaths
863: .size()]));
864: }
865: });
866: }
867: } finally {
868: cursorChg.restore();
869: }
870: }
871: }
872: }
873:
874: /**
875: * This class actually loads the tree.
876: */
877: private final class TreeLoader {
878: private ObjectTreeNode _parentNode;
879: private INodeExpander[] _expanders;
880: private boolean _selectParentNode;
881:
882: TreeLoader(ObjectTreeNode parentNode,
883: INodeExpander[] expanders, boolean selectParentNode) {
884: super ();
885: _parentNode = parentNode;
886: _expanders = expanders;
887: _selectParentNode = selectParentNode;
888: }
889:
890: void execute() {
891: try {
892: try {
893: ObjectTreeNode loadingNode = showLoadingNode();
894: try {
895: loadChildren();
896: } finally {
897: if (_parentNode.isNodeChild(loadingNode)) {
898: _parentNode.remove(loadingNode);
899: }
900: }
901: } finally {
902: fireStructureChanged(_parentNode);
903: if (_selectParentNode) {
904: clearSelection();
905: setSelectionPath(new TreePath(_parentNode
906: .getPath()));
907: }
908: }
909: } catch (Throwable ex) {
910: final String msg = "Error: " + _parentNode.toString();
911: s_log.error(msg, ex);
912: _session.showErrorMessage(msg + ": " + ex.toString());
913: }
914: }
915:
916: /**
917: * This adds a node to the tree that says "Loading..." in order to give
918: * feedback to the user.
919: */
920: private ObjectTreeNode showLoadingNode() {
921: IDatabaseObjectInfo doi = new DatabaseObjectInfo(null,
922: null, "Loading...", DatabaseObjectType.OTHER,
923: _session.getSQLConnection().getSQLMetaData());
924: ObjectTreeNode loadingNode = new ObjectTreeNode(_session,
925: doi);
926: _parentNode.add(loadingNode);
927: fireStructureChanged(_parentNode);
928: return loadingNode;
929: }
930:
931: /**
932: * This expands the parent node and shows all its children.
933: */
934: private void loadChildren() throws SQLException {
935: for (int i = 0; i < _expanders.length; ++i) {
936: boolean nodeTypeAllowsChildren = false;
937: DatabaseObjectType lastDboType = null;
938: List<ObjectTreeNode> list = _expanders[i]
939: .createChildren(_session, _parentNode);
940: Iterator<ObjectTreeNode> it = list.iterator();
941: while (it.hasNext()) {
942: Object nextObj = it.next();
943: if (nextObj instanceof ObjectTreeNode) {
944: ObjectTreeNode childNode = (ObjectTreeNode) nextObj;
945: if (childNode.getExpanders().length > 0) {
946: childNode.setAllowsChildren(true);
947: } else {
948: DatabaseObjectType childNodeDboType = childNode
949: .getDatabaseObjectType();
950: if (childNodeDboType != lastDboType) {
951: getTypedModel()
952: .addKnownDatabaseObjectType(
953: childNodeDboType);
954: lastDboType = childNodeDboType;
955: if (_model
956: .getExpanders(childNodeDboType).length > 0) {
957: nodeTypeAllowsChildren = true;
958: } else {
959: nodeTypeAllowsChildren = false;
960: }
961: }
962: childNode
963: .setAllowsChildren(nodeTypeAllowsChildren);
964: }
965: _parentNode.add(childNode);
966: }
967: }
968: }
969: }
970:
971: /**
972: * Let the object tree model know that its structure has changed.
973: */
974: private void fireStructureChanged(final ObjectTreeNode node) {
975: GUIUtils.processOnSwingEventThread(new Runnable() {
976: public void run() {
977: ObjectTree.this._model.nodeStructureChanged(node);
978: }
979: });
980: }
981: }
982: }
|