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: package org.netbeans.modules.project.ui;
043:
044: import java.beans.PropertyChangeEvent;
045: import java.beans.PropertyChangeListener;
046: import java.io.CharConversionException;
047: import java.lang.ref.Reference;
048: import java.lang.ref.WeakReference;
049: import java.text.MessageFormat;
050: import java.util.ArrayList;
051: import java.util.Arrays;
052: import java.util.Collection;
053: import java.util.Collections;
054: import java.util.Iterator;
055: import java.util.List;
056: import java.util.ResourceBundle;
057: import java.util.Set;
058: import java.util.WeakHashMap;
059: import javax.swing.Action;
060: import javax.swing.JSeparator;
061: import javax.swing.SwingUtilities;
062: import javax.swing.event.ChangeEvent;
063: import javax.swing.event.ChangeListener;
064: import org.netbeans.api.project.FileOwnerQuery;
065: import org.netbeans.api.project.Project;
066: import org.netbeans.api.project.ProjectManager;
067: import org.netbeans.api.project.ProjectUtils;
068: import org.netbeans.api.project.SourceGroup;
069: import org.netbeans.api.project.Sources;
070: import org.netbeans.spi.project.ui.LogicalViewProvider;
071: import org.openide.ErrorManager;
072: import org.openide.filesystems.FileObject;
073: import org.openide.filesystems.FileStateInvalidException;
074: import org.openide.filesystems.FileStatusEvent;
075: import org.openide.filesystems.FileStatusListener;
076: import org.openide.filesystems.FileSystem;
077: import org.openide.filesystems.FileUtil;
078: import org.openide.loaders.DataObject;
079: import org.openide.nodes.AbstractNode;
080: import org.openide.nodes.Children;
081: import org.openide.nodes.FilterNode;
082: import org.openide.nodes.Node;
083: import org.openide.util.Lookup;
084: import org.openide.util.NbBundle;
085: import org.openide.util.RequestProcessor;
086: import org.openide.util.WeakListeners;
087: import org.openide.util.lookup.Lookups;
088: import org.openide.util.lookup.ProxyLookup;
089: import org.openide.xml.XMLUtil;
090: import org.openidex.search.FileObjectFilter;
091: import org.openidex.search.SearchInfo;
092: import org.openidex.search.SearchInfoFactory;
093:
094: /** Root node for list of open projects
095: * @author Petr Hrebejk
096: */
097: public class ProjectsRootNode extends AbstractNode {
098:
099: static final int PHYSICAL_VIEW = 0;
100: static final int LOGICAL_VIEW = 1;
101:
102: private static final String ICON_BASE = "org/netbeans/modules/project/ui/resources/projectsRootNode.gif"; //NOI18N
103: private static final String ACTIONS_FOLDER = "ProjectsTabActions"; // NOI18N
104:
105: private ResourceBundle bundle;
106: private final int type;
107:
108: public ProjectsRootNode(int type) {
109: super (new ProjectChildren(type));
110: setIconBaseWithExtension(ICON_BASE);
111: this .type = type;
112: }
113:
114: public String getName() {
115: return ("OpenProjects"); // NOI18N
116: }
117:
118: public String getDisplayName() {
119: if (this .bundle == null) {
120: this .bundle = NbBundle.getBundle(ProjectsRootNode.class);
121: }
122: return bundle.getString("LBL_OpenProjectsNode_Name"); // NOI18N
123: }
124:
125: public boolean canRename() {
126: return false;
127: }
128:
129: public Node.Handle getHandle() {
130: return new Handle(type);
131: }
132:
133: public Action[] getActions(boolean context) {
134: if (context || type == PHYSICAL_VIEW) {
135: return new Action[0];
136: } else {
137: List<Action> actions = new ArrayList<Action>();
138: for (Object o : Lookups.forPath(ACTIONS_FOLDER).lookupAll(
139: Object.class)) {
140: if (o instanceof Action) {
141: actions.add((Action) o);
142: } else if (o instanceof JSeparator) {
143: actions.add(null);
144: }
145: }
146: return actions.toArray(new Action[actions.size()]);
147: }
148: }
149:
150: /** Finds node for given object in the view
151: * @return the node or null if the node was not found
152: */
153: Node findNode(FileObject target) {
154:
155: ProjectChildren ch = (ProjectChildren) getChildren();
156:
157: if (ch.type == LOGICAL_VIEW) {
158: // Speed up search in case we have an owner project - look in its node first.
159: Project ownerProject = FileOwnerQuery.getOwner(target);
160: for (int lookOnlyInOwnerProject = (ownerProject != null) ? 0
161: : 1; lookOnlyInOwnerProject < 2; lookOnlyInOwnerProject++) {
162: for (Node node : ch.getNodes(true)) {
163: Project p = node.getLookup().lookup(Project.class);
164: assert p != null : "Should have had a Project in lookup of "
165: + node;
166: if (lookOnlyInOwnerProject == 0
167: && p != ownerProject) {
168: continue; // but try again (in next outer loop) as a fallback
169: }
170: LogicalViewProvider lvp = p.getLookup().lookup(
171: LogicalViewProvider.class);
172: if (lvp != null) {
173: // XXX (cf. #63554): really should be calling this on DataObject usually, since
174: // DataNode does *not* currently have a FileObject in its lookup (should it?)
175: // ...but it is not clear who has implemented findPath to assume FileObject!
176: Node selectedNode = lvp.findPath(node, target);
177: if (selectedNode != null) {
178: return selectedNode;
179: }
180: }
181: }
182: }
183: return null;
184:
185: } else if (ch.type == PHYSICAL_VIEW) {
186: for (Node node : ch.getNodes(true)) {
187: // XXX could do similar optimization as for LOGICAL_VIEW; every nodes[i] must have some Project in its lookup
188: PhysicalView.PathFinder pf = node.getLookup().lookup(
189: PhysicalView.PathFinder.class);
190: if (pf != null) {
191: Node n = pf.findPath(node, target);
192: if (n != null) {
193: return n;
194: }
195: }
196: }
197: return null;
198: } else {
199: return null;
200: }
201: }
202:
203: private static class Handle implements Node.Handle {
204:
205: private static final long serialVersionUID = 78374332058L;
206:
207: private int viewType;
208:
209: public Handle(int viewType) {
210: this .viewType = viewType;
211: }
212:
213: public Node getNode() {
214: return new ProjectsRootNode(viewType);
215: }
216:
217: }
218:
219: // XXX Needs to listen to project rename
220: // However project rename is currently disabled so it is not a big deal
221: static class ProjectChildren extends
222: Children.Keys<ProjectChildren.Pair> implements
223: ChangeListener, PropertyChangeListener {
224:
225: private java.util.Map<Sources, Reference<Project>> sources2projects = new WeakHashMap<Sources, Reference<Project>>();
226:
227: int type;
228:
229: public ProjectChildren(int type) {
230: this .type = type;
231: }
232:
233: // Children.Keys impl --------------------------------------------------
234:
235: @Override
236: public void addNotify() {
237: OpenProjectList.getDefault()
238: .addPropertyChangeListener(this );
239: setKeys(getKeys());
240: }
241:
242: @Override
243: public void removeNotify() {
244: OpenProjectList.getDefault().removePropertyChangeListener(
245: this );
246: for (Sources sources : sources2projects.keySet()) {
247: sources.removeChangeListener(this );
248: }
249: sources2projects.clear();
250: setKeys(Collections.<Pair> emptySet());
251: }
252:
253: protected Node[] createNodes(Pair p) {
254: Project project = p.project;
255:
256: Node origNodes[] = null;
257: boolean[] projectInLookup = new boolean[1];
258: projectInLookup[0] = true;
259:
260: if (type == PHYSICAL_VIEW) {
261: Sources sources = ProjectUtils.getSources(project);
262: sources.removeChangeListener(this );
263: sources.addChangeListener(this );
264: sources2projects.put(sources,
265: new WeakReference<Project>(project));
266: origNodes = PhysicalView.createNodesForProject(project);
267: } else {
268: origNodes = new Node[] { logicalViewForProject(project,
269: projectInLookup) };
270: }
271:
272: Node[] badgedNodes = new Node[origNodes.length];
273: for (int i = 0; i < origNodes.length; i++) {
274: if (type == PHYSICAL_VIEW
275: && !PhysicalView.isProjectDirNode(origNodes[i])) {
276: // Don't badge external sources
277: badgedNodes[i] = origNodes[i];
278: } else {
279: badgedNodes[i] = new BadgingNode(p, origNodes[i],
280: type == LOGICAL_VIEW && projectInLookup[0],
281: type == LOGICAL_VIEW);
282: }
283: }
284:
285: return badgedNodes;
286: }
287:
288: final Node logicalViewForProject(Project project,
289: boolean[] projectInLookup) {
290: Node node;
291:
292: LogicalViewProvider lvp = project.getLookup().lookup(
293: LogicalViewProvider.class);
294:
295: if (lvp == null) {
296: ErrorManager
297: .getDefault()
298: .log(
299: ErrorManager.WARNING,
300: "Warning - project "
301: + ProjectUtils.getInformation(
302: project).getName()
303: + " failed to supply a LogicalViewProvider in its lookup"); // NOI18N
304: Sources sources = ProjectUtils.getSources(project);
305: sources.removeChangeListener(this );
306: sources.addChangeListener(this );
307: Node[] physical = PhysicalView
308: .createNodesForProject(project);
309: if (physical.length > 0) {
310: node = physical[0];
311: } else {
312: node = Node.EMPTY;
313: }
314: } else {
315: node = lvp.createLogicalView();
316: if (node.getLookup().lookup(Project.class) != project) {
317: // Various actions, badging, etc. are not going to work.
318: ErrorManager
319: .getDefault()
320: .log(
321: ErrorManager.WARNING,
322: "Warning - project "
323: + ProjectUtils
324: .getInformation(
325: project)
326: .getName()
327: + " failed to supply itself in the lookup of the root node of its own logical view"); // NOI18N
328: //#114664
329: if (projectInLookup != null) {
330: projectInLookup[0] = false;
331: }
332: }
333: }
334:
335: return node;
336: }
337:
338: // PropertyChangeListener impl -----------------------------------------
339:
340: public void propertyChange(PropertyChangeEvent e) {
341: if (OpenProjectList.PROPERTY_OPEN_PROJECTS.equals(e
342: .getPropertyName())) {
343: setKeys(getKeys());
344: }
345: }
346:
347: // Change listener impl ------------------------------------------------
348:
349: public void stateChanged(ChangeEvent e) {
350:
351: Reference<Project> projectRef = sources2projects.get(e
352: .getSource());
353: if (projectRef == null) {
354: return;
355: }
356:
357: final Project project = projectRef.get();
358:
359: if (project == null) {
360: return;
361: }
362:
363: // Fix for 50259, callers sometimes hold locks
364: SwingUtilities.invokeLater(new Runnable() {
365: public void run() {
366: refresh(project);
367: }
368: });
369: }
370:
371: final void refresh(Project p) {
372: refreshKey(new Pair(p, p.getProjectDirectory()));
373: }
374:
375: // Own methods ---------------------------------------------------------
376:
377: public Collection<Pair> getKeys() {
378: List<Project> projects = Arrays.asList(OpenProjectList
379: .getDefault().getOpenProjects());
380: Collections.sort(projects,
381: OpenProjectList.PROJECT_BY_DISPLAYNAME);
382:
383: List<Pair> dirs = Arrays.asList(new Pair[projects.size()]);
384:
385: for (int i = 0; i < projects.size(); i++) {
386: Project project = projects.get(i);
387: dirs.set(i, new Pair(project, project
388: .getProjectDirectory()));
389: }
390:
391: return dirs;
392: }
393:
394: /** Object that comparers two projects just by their directory.
395: * This allows to replace a LazyProject with real one without discarding
396: * the nodes.
397: */
398: private static final class Pair extends Object {
399: public Project project;
400: public final FileObject fo;
401:
402: public Pair(Project project, FileObject fo) {
403: this .project = project;
404: this .fo = fo;
405: }
406:
407: @Override
408: public boolean equals(Object obj) {
409: if (obj == null) {
410: return false;
411: }
412: if (getClass() != obj.getClass()) {
413: return false;
414: }
415: final Pair other = (Pair) obj;
416: if (this .fo != other.fo
417: && (this .fo == null || !this .fo
418: .equals(other.fo))) {
419: return false;
420: }
421: return true;
422: }
423:
424: @Override
425: public int hashCode() {
426: int hash = 7;
427: hash = 53 * hash
428: + (this .fo != null ? this .fo.hashCode() : 0);
429: return hash;
430: }
431: }
432:
433: }
434:
435: private static final class BadgingNode extends FilterNode implements
436: PropertyChangeListener, Runnable, FileStatusListener {
437:
438: private static String badgedNamePattern = NbBundle.getMessage(
439: ProjectsRootNode.class,
440: "LBL_MainProject_BadgedNamePattern");
441: private final FileObject file;
442: private final Set<FileObject> files;
443: private FileStatusListener fileSystemListener;
444: private RequestProcessor.Task task;
445: private volatile boolean nameChange;
446: private final boolean logicalView;
447: private final ProjectChildren.Pair pair;
448:
449: public BadgingNode(ProjectChildren.Pair p, Node n,
450: boolean addSearchInfo, boolean logicalView) {
451: super (n, null, badgingLookup(n, addSearchInfo));
452: this .pair = p;
453: this .logicalView = logicalView;
454: OpenProjectList.getDefault().addPropertyChangeListener(
455: WeakListeners.propertyChange(this , OpenProjectList
456: .getDefault()));
457: Project proj = getOriginal().getLookup().lookup(
458: Project.class);
459: if (proj != null) {
460: file = proj.getProjectDirectory();
461: assert file != null : "Project returns null directory: "
462: + proj;
463: files = Collections.singleton(file);
464: try {
465: FileSystem fs = file.getFileSystem();
466: fileSystemListener = FileUtil
467: .weakFileStatusListener(this , fs);
468: fs.addFileStatusListener(fileSystemListener);
469: } catch (FileStateInvalidException e) {
470: ErrorManager err = ErrorManager.getDefault();
471: err.annotate(e, "Can not get " + file
472: + " filesystem, ignoring..."); // NO18N
473: err.notify(ErrorManager.INFORMATIONAL, e);
474: }
475: } else {
476: file = null;
477: files = null;
478: }
479: }
480:
481: private static Lookup badgingLookup(Node n,
482: boolean addSearchInfo) {
483: if (addSearchInfo) {
484: return new BadgingLookup(n.getLookup(), Lookups
485: .singleton(alwaysSearchableSearchInfo(n
486: .getLookup().lookup(Project.class))));
487: } else {
488: return new BadgingLookup(n.getLookup());
489: }
490: }
491:
492: public void run() {
493: if (nameChange) {
494: fireDisplayNameChange(null, null);
495: nameChange = false;
496: }
497: }
498:
499: public void annotationChanged(FileStatusEvent event) {
500: if (task == null) {
501: task = RequestProcessor.getDefault().create(this );
502: }
503:
504: if (nameChange == false && event.isNameChange()) {
505: if (event.hasChanged(file)) {
506: nameChange |= event.isNameChange();
507: }
508: }
509:
510: task.schedule(50); // batch by 50 ms
511: }
512:
513: public String getDisplayName() {
514: String original = super .getDisplayName();
515: Project proj = getOriginal().getLookup().lookup(
516: Project.class);
517: if (proj != null) {
518: try {
519: original = proj.getProjectDirectory()
520: .getFileSystem().getStatus().annotateName(
521: original,
522: Collections.singleton(proj
523: .getProjectDirectory()));
524: } catch (FileStateInvalidException e) {
525: ErrorManager.getDefault().notify(
526: ErrorManager.INFORMATIONAL, e);
527: }
528: }
529: return isMain() ? MessageFormat.format(badgedNamePattern,
530: new Object[] { original }) : original;
531: }
532:
533: public String getHtmlDisplayName() {
534: String htmlName = getOriginal().getHtmlDisplayName();
535: String dispName = null;
536: if (isMain() && htmlName == null) {
537: dispName = super .getDisplayName();
538: try {
539: dispName = XMLUtil.toElementContent(dispName);
540: } catch (CharConversionException ex) {
541: // ignore
542: }
543: }
544: Project proj = getOriginal().getLookup().lookup(
545: Project.class);
546: if (proj != null) {
547: try {
548: FileSystem.Status stat = proj.getProjectDirectory()
549: .getFileSystem().getStatus();
550: if (stat instanceof FileSystem.HtmlStatus) {
551: FileSystem.HtmlStatus hstat = (FileSystem.HtmlStatus) stat;
552:
553: String result = hstat.annotateNameHtml(super
554: .getDisplayName(), Collections
555: .singleton(proj.getProjectDirectory()));
556: //Make sure the super string was really modified
557: if (result != null
558: && !super .getDisplayName().equals(
559: result)) {
560: return isMain() ? "<b>" + (result) + "</b>"
561: : result; //NOI18N
562: }
563: }
564: } catch (FileStateInvalidException e) {
565: ErrorManager.getDefault().notify(
566: ErrorManager.INFORMATIONAL, e);
567: }
568: }
569: return isMain() ? "<b>"
570: + (htmlName == null ? dispName : htmlName) + "</b>"
571: : htmlName; //NOI18N
572: }
573:
574: public void propertyChange(PropertyChangeEvent e) {
575: if (OpenProjectList.PROPERTY_MAIN_PROJECT.equals(e
576: .getPropertyName())) {
577: fireDisplayNameChange(null, null);
578: }
579: if (OpenProjectList.PROPERTY_REPLACE.equals(e
580: .getPropertyName())) {
581: Project p = getLookup().lookup(Project.class);
582: if (p == null) {
583: return;
584: }
585: FileObject fo = p.getProjectDirectory();
586: Project newProj = (Project) e.getNewValue();
587: assert newProj != null;
588: if (newProj.getProjectDirectory().equals(fo)) {
589: ProjectChildren ch = (ProjectChildren) getParentNode()
590: .getChildren();
591: Node n = null;
592: if (logicalView) {
593: n = ch.logicalViewForProject(newProj, null);
594: } else {
595: Node[] arr = PhysicalView
596: .createNodesForProject(newProj);
597: if (arr.length > 1) {
598: pair.project = newProj;
599: ch.refresh(newProj);
600: return;
601: }
602: for (Node one : arr) {
603: if (PhysicalView.isProjectDirNode(one)) {
604: n = one;
605: break;
606: }
607: }
608: assert n != null;
609: }
610: changeOriginal(n, true);
611:
612: BadgingLookup bl = (BadgingLookup) getLookup();
613: if (bl.isSearchInfo()) {
614: bl
615: .setMyLookups(
616: n.getLookup(),
617: Lookups
618: .singleton(alwaysSearchableSearchInfo(newProj)));
619: } else {
620: bl.setMyLookups(n.getLookup());
621: }
622: }
623: }
624: }
625:
626: private boolean isMain() {
627: Project p = getLookup().lookup(Project.class);
628: return p != null
629: && OpenProjectList.getDefault().isMainProject(p);
630: }
631:
632: } // end of BadgingNode
633:
634: private static final class BadgingLookup extends ProxyLookup {
635: public BadgingLookup(Lookup... lkps) {
636: super (lkps);
637: }
638:
639: public void setMyLookups(Lookup... lkps) {
640: setLookups(lkps);
641: }
642:
643: public boolean isSearchInfo() {
644: return getLookups().length > 1;
645: }
646: } // end of BadgingLookup
647:
648: /**
649: * Produce a {@link SearchInfo} variant that is always searchable, for speed.
650: * @see "#48685"
651: */
652: static SearchInfo alwaysSearchableSearchInfo(Project p) {
653: return new AlwaysSearchableSearchInfo(p);
654: }
655:
656: private static final class AlwaysSearchableSearchInfo implements
657: SearchInfo {
658:
659: private final SearchInfo delegate;
660:
661: public AlwaysSearchableSearchInfo(Project prj) {
662: SearchInfo projectSearchInfo = prj.getLookup().lookup(
663: SearchInfo.class);
664: if (projectSearchInfo != null) {
665: delegate = projectSearchInfo;
666: } else {
667: SourceGroup groups[] = ProjectUtils.getSources(prj)
668: .getSourceGroups(Sources.TYPE_GENERIC);
669: FileObject folders[] = new FileObject[groups.length];
670: for (int i = 0; i < groups.length; i++) {
671: folders[i] = groups[i].getRootFolder();
672: }
673: delegate = SearchInfoFactory
674: .createSearchInfo(
675: folders,
676: true,
677: new FileObjectFilter[] { SearchInfoFactory.VISIBILITY_FILTER });
678: }
679: }
680:
681: public boolean canSearch() {
682: return true;
683: }
684:
685: public Iterator<DataObject> objectsToSearch() {
686: return delegate.objectsToSearch();
687: }
688:
689: }
690:
691: }
|