001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2008 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-2008 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.ruby.railsprojects.ui;
043:
044: import java.awt.Image;
045: import java.beans.PropertyChangeEvent;
046: import java.beans.PropertyChangeListener;
047: import java.util.ArrayList;
048: import java.util.Arrays;
049: import java.util.Collection;
050: import java.util.HashMap;
051: import java.util.HashSet;
052: import java.util.Iterator;
053: import java.util.List;
054: import java.util.Map;
055: import java.util.ResourceBundle;
056: import java.util.Set;
057: import javax.swing.Action;
058: import javax.swing.JSeparator;
059: import javax.swing.event.ChangeEvent;
060: import javax.swing.event.ChangeListener;
061: import org.netbeans.modules.ruby.railsprojects.GenerateAction;
062: import org.netbeans.modules.ruby.railsprojects.RailsActionProvider;
063: import org.netbeans.api.project.FileOwnerQuery;
064: import org.netbeans.api.project.Project;
065: import org.netbeans.api.project.ProjectUtils;
066: import org.netbeans.api.project.SourceGroup;
067: import org.netbeans.api.project.Sources;
068: import org.netbeans.api.ruby.platform.RubyPlatform;
069: import org.netbeans.modules.ruby.railsprojects.MigrateAction;
070: import org.netbeans.modules.ruby.railsprojects.ui.customizer.RailsProjectProperties;
071: import org.netbeans.modules.ruby.railsprojects.RailsProject;
072: import org.netbeans.modules.ruby.railsprojects.UpdateHelper;
073: import org.netbeans.modules.ruby.railsprojects.plugins.PluginAction;
074: import org.netbeans.modules.ruby.rubyproject.AutoTestSupport;
075: import org.netbeans.modules.ruby.rubyproject.RakeTargetsAction;
076: import org.netbeans.modules.ruby.rubyproject.RakeTargetsDebugAction;
077: import org.netbeans.modules.ruby.spi.project.support.rake.RakeProjectEvent;
078: import org.netbeans.spi.project.ActionProvider;
079: import org.netbeans.spi.project.SubprojectProvider;
080: import org.netbeans.modules.ruby.spi.project.support.rake.PropertyEvaluator;
081: import org.netbeans.modules.ruby.spi.project.support.rake.RakeProjectListener;
082: import org.netbeans.modules.ruby.spi.project.support.rake.ReferenceHelper;
083: import org.netbeans.spi.project.ui.LogicalViewProvider;
084: import org.netbeans.spi.project.ui.support.CommonProjectActions;
085: import org.netbeans.spi.project.ui.support.NodeFactorySupport;
086: import org.netbeans.spi.project.ui.support.DefaultProjectOperations;
087: import org.netbeans.spi.project.ui.support.ProjectSensitiveActions;
088: import org.openide.ErrorManager;
089: import org.openide.actions.FindAction;
090: import org.openide.filesystems.FileObject;
091: import org.openide.filesystems.FileStateInvalidException;
092: import org.openide.filesystems.FileStatusEvent;
093: import org.openide.filesystems.FileStatusListener;
094: import org.openide.filesystems.FileSystem;
095: import org.openide.filesystems.FileUtil;
096: import org.openide.loaders.DataObject;
097: import org.openide.nodes.AbstractNode;
098: import org.openide.nodes.Node;
099: import org.openide.util.HelpCtx;
100: import org.openide.util.NbBundle;
101: import org.openide.util.RequestProcessor;
102: import org.openide.util.WeakListeners;
103: import org.openide.util.actions.SystemAction;
104: import org.openide.util.lookup.Lookups;
105:
106: /**
107: * Support for creating logical views.
108: * @author Petr Hrebejk
109: */
110: public class RailsLogicalViewProvider implements LogicalViewProvider {
111:
112: //private static final RequestProcessor BROKEN_LINKS_RP = new RequestProcessor("RubyPhysicalViewProvider.BROKEN_LINKS_RP"); // NOI18N
113:
114: private final RailsProject project;
115: private final UpdateHelper helper;
116: private final PropertyEvaluator evaluator;
117: private final SubprojectProvider spp;
118: private final ReferenceHelper resolver;
119: private List<ChangeListener> changeListeners;
120:
121: public RailsLogicalViewProvider(RailsProject project,
122: UpdateHelper helper, PropertyEvaluator evaluator,
123: SubprojectProvider spp, ReferenceHelper resolver) {
124: this .project = project;
125: assert project != null;
126: this .helper = helper;
127: assert helper != null;
128: this .evaluator = evaluator;
129: assert evaluator != null;
130: this .spp = spp;
131: assert spp != null;
132: this .resolver = resolver;
133: }
134:
135: public Node createLogicalView() {
136: return new RailsLogicalViewRootNode();
137: }
138:
139: public PropertyEvaluator getEvaluator() {
140: return evaluator;
141: }
142:
143: public ReferenceHelper getRefHelper() {
144: return resolver;
145: }
146:
147: public UpdateHelper getUpdateHelper() {
148: return helper;
149: }
150:
151: public Node findPath(Node root, Object target) {
152: Project project = root.getLookup().lookup(Project.class);
153: if (project == null) {
154: return null;
155: }
156:
157: if (target instanceof FileObject) {
158: FileObject targetFO = (FileObject) target;
159: Project owner = FileOwnerQuery.getOwner(targetFO);
160: if (!project.equals(owner)) {
161: return null; // Don't waste time if project does not own the fo
162: }
163:
164: Node[] rootChildren = root.getChildren().getNodes(true);
165: for (int i = 0; i < rootChildren.length; i++) {
166: TreeRootNode.PathFinder pf2 = rootChildren[i]
167: .getLookup().lookup(
168: TreeRootNode.PathFinder.class);
169: if (pf2 != null) {
170: Node n = pf2.findPath(rootChildren[i], target);
171: if (n != null) {
172: return n;
173: }
174: }
175: FileObject childFO = rootChildren[i].getLookup()
176: .lookup(DataObject.class).getPrimaryFile();
177: if (targetFO.equals(childFO)) {
178: return rootChildren[i];
179: }
180: }
181: }
182:
183: return null;
184: }
185:
186: public synchronized void addChangeListener(ChangeListener l) {
187: if (this .changeListeners == null) {
188: this .changeListeners = new ArrayList<ChangeListener>();
189: }
190: this .changeListeners.add(l);
191: }
192:
193: public synchronized void removeChangeListener(ChangeListener l) {
194: if (this .changeListeners == null) {
195: return;
196: }
197: this .changeListeners.remove(l);
198: }
199:
200: /**
201: * Used by RailsProjectCustomizer to mark the project as broken when it warns user
202: * about project's broken references and advices him to use BrokenLinksAction to correct it.
203: *
204: */
205: public void testBroken() {
206: ChangeListener[] _listeners;
207: synchronized (this ) {
208: if (this .changeListeners == null) {
209: return;
210: }
211: _listeners = this .changeListeners
212: .toArray(new ChangeListener[this .changeListeners
213: .size()]);
214: }
215: ChangeEvent event = new ChangeEvent(this );
216: for (int i = 0; i < _listeners.length; i++) {
217: _listeners[i].stateChanged(event);
218: }
219: }
220:
221: // private static Lookup createLookup( Project project ) {
222: // DataFolder rootFolder = DataFolder.findFolder(project.getProjectDirectory());
223: // // XXX Remove root folder after FindAction rewrite
224: // return Lookups.fixed(new Object[] {project, rootFolder});
225: // }
226:
227: // Private innerclasses ----------------------------------------------------------------
228:
229: private static final String[] BREAKABLE_PROPERTIES = new String[] {
230: RailsProjectProperties.JAVAC_CLASSPATH,
231: RailsProjectProperties.RUN_CLASSPATH,
232: RailsProjectProperties.DEBUG_CLASSPATH,
233: RailsProjectProperties.RUN_TEST_CLASSPATH,
234: RailsProjectProperties.DEBUG_TEST_CLASSPATH,
235: RailsProjectProperties.JAVAC_TEST_CLASSPATH, };
236:
237: // private static Image brokenProjectBadge = Utilities.loadImage("org/netbeans/modules/ruby/railsprojects/ui/resources/brokenProjectBadge.gif", true);
238:
239: /** Filter node containin additional features for the Ruby physical
240: */
241: private final class RailsLogicalViewRootNode extends AbstractNode
242: implements Runnable, FileStatusListener, ChangeListener,
243: PropertyChangeListener {
244:
245: private Set<FileObject> files;
246: private Map<FileSystem, FileStatusListener> fileSystemListeners;
247: private RequestProcessor.Task task;
248: private final Object privateLock = new Object();
249: private boolean iconChange;
250: private boolean nameChange;
251: private ChangeListener sourcesListener;
252: private Map<SourceGroup, PropertyChangeListener> groupsListeners;
253:
254: public RailsLogicalViewRootNode() {
255: super (
256: NodeFactorySupport
257: .createCompositeChildren(project,
258: "Projects/org-netbeans-modules-ruby-railsprojects/Nodes"), // NOI18N
259: Lookups.singleton(project));
260: setIconBaseWithExtension("org/netbeans/modules/ruby/railsprojects/ui/resources/rails.png"); // NOI18N
261: super .setName(ProjectUtils.getInformation(project)
262: .getDisplayName());
263: setProjectFiles(project);
264: helper.getRakeProjectHelper().addRakeProjectListener(
265: new RakeProjectListener() {
266: public void configurationXmlChanged(
267: RakeProjectEvent ev) {
268: fireShortDescriptionChange(null, null);
269: }
270:
271: public void propertiesChanged(
272: RakeProjectEvent ev) {
273: fireShortDescriptionChange(null, null);
274: }
275: });
276: }
277:
278: public @Override
279: String getShortDescription() {
280: String platformDesc = RubyPlatform
281: .platformDescriptionFor(project);
282: if (platformDesc == null) {
283: platformDesc = NbBundle.getMessage(
284: RailsLogicalViewProvider.class,
285: "RailsLogicalViewProvider.PlatformNotFound");
286: }
287: String dirName = FileUtil.getFileDisplayName(project
288: .getProjectDirectory());
289: return NbBundle
290: .getMessage(
291: RailsLogicalViewProvider.class,
292: "RailsLogicalViewProvider.ProjectTooltipDescription",
293: dirName, platformDesc);
294: }
295:
296: protected final void setProjectFiles(Project project) {
297: Sources sources = ProjectUtils.getSources(project); // returns singleton
298: if (sourcesListener == null) {
299: sourcesListener = WeakListeners.change(this , sources);
300: sources.addChangeListener(sourcesListener);
301: }
302: setGroups(Arrays.asList(sources
303: .getSourceGroups(Sources.TYPE_GENERIC)));
304: }
305:
306: private final void setGroups(Collection<SourceGroup> groups) {
307: if (groupsListeners != null) {
308: Iterator it = groupsListeners.keySet().iterator();
309: while (it.hasNext()) {
310: SourceGroup group = (SourceGroup) it.next();
311: PropertyChangeListener pcl = groupsListeners
312: .get(group);
313: group.removePropertyChangeListener(pcl);
314: }
315: }
316: groupsListeners = new HashMap<SourceGroup, PropertyChangeListener>();
317: Set<FileObject> roots = new HashSet<FileObject>();
318: Iterator it = groups.iterator();
319: for (SourceGroup group : groups) {
320: PropertyChangeListener pcl = WeakListeners
321: .propertyChange(this , group);
322: groupsListeners.put(group, pcl);
323: group.addPropertyChangeListener(pcl);
324: FileObject fo = group.getRootFolder();
325: roots.add(fo);
326: }
327: setFiles(roots);
328: }
329:
330: protected final void setFiles(Set<FileObject> files) {
331: if (fileSystemListeners != null) {
332: for (FileSystem fs : fileSystemListeners.keySet()) {
333: FileStatusListener fsl = fileSystemListeners
334: .get(fs);
335: fs.removeFileStatusListener(fsl);
336: }
337: }
338:
339: fileSystemListeners = new HashMap<FileSystem, FileStatusListener>();
340: this .files = files;
341: if (files == null) {
342: return;
343: }
344:
345: Iterator it = files.iterator();
346: Set<FileSystem> hookedFileSystems = new HashSet<FileSystem>();
347: while (it.hasNext()) {
348: FileObject fo = (FileObject) it.next();
349: try {
350: FileSystem fs = fo.getFileSystem();
351: if (hookedFileSystems.contains(fs)) {
352: continue;
353: }
354: hookedFileSystems.add(fs);
355: FileStatusListener fsl = FileUtil
356: .weakFileStatusListener(this , fs);
357: fs.addFileStatusListener(fsl);
358: fileSystemListeners.put(fs, fsl);
359: } catch (FileStateInvalidException e) {
360: ErrorManager err = ErrorManager.getDefault();
361: err.annotate(e, ErrorManager.UNKNOWN, "Cannot get "
362: + fo + " filesystem, ignoring...", null,
363: null, null); // NOI18N
364: err.notify(ErrorManager.INFORMATIONAL, e);
365: }
366: }
367: }
368:
369: // public String getHtmlDisplayName() {
370: // String dispName = super.getDisplayName();
371: // try {
372: // dispName = XMLUtil.toElementContent(dispName);
373: // } catch (CharConversionException ex) {
374: // return dispName;
375: // }
376: // // XXX text colors should be taken from UIManager, not hard-coded!
377: // //return broken || illegalState ? "<font color=\"#A40000\">" + dispName + "</font>" : null; //NOI18N
378: // return null;
379: // }
380:
381: public @Override
382: Image getIcon(int type) {
383: Image img = getMyIcon(type);
384:
385: if (files != null && files.iterator().hasNext()) {
386: try {
387: FileObject fo = files.iterator().next();
388: img = fo.getFileSystem().getStatus().annotateIcon(
389: img, type, files);
390: } catch (FileStateInvalidException e) {
391: ErrorManager.getDefault().notify(
392: ErrorManager.INFORMATIONAL, e);
393: }
394: }
395:
396: return img;
397: }
398:
399: private Image getMyIcon(int type) {
400: Image original = super .getIcon(type);
401: // return broken || illegalState ? Utilities.mergeImages(original, brokenProjectBadge, 8, 0) : original;
402: return original;
403: }
404:
405: public @Override
406: Image getOpenedIcon(int type) {
407: Image img = getMyOpenedIcon(type);
408:
409: if (files != null && files.iterator().hasNext()) {
410: try {
411: FileObject fo = files.iterator().next();
412: img = fo.getFileSystem().getStatus().annotateIcon(
413: img, type, files);
414: } catch (FileStateInvalidException e) {
415: ErrorManager.getDefault().notify(
416: ErrorManager.INFORMATIONAL, e);
417: }
418: }
419:
420: return img;
421: }
422:
423: private Image getMyOpenedIcon(int type) {
424: Image original = super .getOpenedIcon(type);
425: //return broken || illegalState ? Utilities.mergeImages(original, brokenProjectBadge, 8, 0) : original;
426: return original;
427: }
428:
429: public void run() {
430: boolean fireIcon;
431: boolean fireName;
432: synchronized (privateLock) {
433: fireIcon = iconChange;
434: fireName = nameChange;
435: iconChange = false;
436: nameChange = false;
437: }
438: if (fireIcon) {
439: fireIconChange();
440: fireOpenedIconChange();
441: }
442: if (fireName) {
443: fireDisplayNameChange(null, null);
444: }
445: }
446:
447: public void annotationChanged(FileStatusEvent event) {
448: if (task == null) {
449: task = RequestProcessor.getDefault().create(this );
450: }
451:
452: synchronized (privateLock) {
453: if ((iconChange == false && event.isIconChange())
454: || (nameChange == false && event.isNameChange())) {
455: Iterator it = files.iterator();
456: while (it.hasNext()) {
457: FileObject fo = (FileObject) it.next();
458: if (event.hasChanged(fo)) {
459: iconChange |= event.isIconChange();
460: nameChange |= event.isNameChange();
461: }
462: }
463: }
464: }
465:
466: task.schedule(50); // batch by 50 ms
467: }
468:
469: // sources change
470: public void stateChanged(ChangeEvent e) {
471: setProjectFiles(project);
472: fireShortDescriptionChange(null, null);
473: }
474:
475: // group change
476: public void propertyChange(PropertyChangeEvent evt) {
477: setProjectFiles(project);
478: }
479:
480: public @Override
481: Action[] getActions(boolean context) {
482: return getAdditionalActions();
483: }
484:
485: public @Override
486: boolean canRename() {
487: return true;
488: }
489:
490: public @Override
491: void setName(String s) {
492: DefaultProjectOperations.performDefaultRenameOperation(
493: project, s);
494: }
495:
496: public @Override
497: HelpCtx getHelpCtx() {
498: return new HelpCtx(RailsLogicalViewRootNode.class);
499: }
500:
501: /*
502: public boolean canDestroy() {
503: return true;
504: }
505:
506: public void destroy() throws IOException {
507: System.out.println("Destroy " + project.getProjectDirectory());
508: LogicalViews.closeProjectAction().actionPerformed(new ActionEvent(this, 0, ""));
509: project.getProjectDirectory().delete();
510: }
511: */
512:
513: // Private methods -------------------------------------------------------------
514: @SuppressWarnings("unchecked")
515: private Action[] getAdditionalActions() {
516:
517: ResourceBundle bundle = NbBundle
518: .getBundle(RailsLogicalViewProvider.class);
519:
520: List actions = new ArrayList();
521:
522: actions.add(SystemAction.get(GenerateAction.class));
523: actions.add(null);
524: actions.add(CommonProjectActions.newFileAction());
525: actions.add(null);
526: //actions.add(ProjectSensitiveActions.projectCommandAction(ActionProvider.COMMAND_BUILD, bundle.getString("LBL_BuildAction_Name"), null)); // NOI18N
527: //actions.add(ProjectSensitiveActions.projectCommandAction(ActionProvider.COMMAND_REBUILD, bundle.getString("LBL_RebuildAction_Name"), null)); // NOI18N
528: //actions.add(ProjectSensitiveActions.projectCommandAction(ActionProvider.COMMAND_CLEAN, bundle.getString("LBL_CleanAction_Name"), null)); // NOI18N
529: //actions.add(null);
530: actions.add(SystemAction.get(RakeTargetsAction.class));
531: actions.add(SystemAction.get(RakeTargetsDebugAction.class));
532: actions.add(SystemAction.get(MigrateAction.class));
533: actions.add(null);
534: actions
535: .add(ProjectSensitiveActions.projectCommandAction(
536: RailsActionProvider.COMMAND_RAILS_CONSOLE,
537: bundle.getString("LBL_ConsoleAction_Name"),
538: null)); // NOI18N
539: actions.add(SystemAction.get(PluginAction.class));
540: //actions.add(ProjectSensitiveActions.projectCommandAction(RailsActionProvider.COMMAND_RDOC, bundle.getString("LBL_RDocAction_Name"), null)); // NOI18N
541: actions.add(null);
542: actions.add(ProjectSensitiveActions.projectCommandAction(
543: ActionProvider.COMMAND_RUN, bundle
544: .getString("LBL_RunAction_Name"), null)); // NOI18N
545: if (AutoTestSupport.isInstalled(project)) {
546: actions
547: .add(ProjectSensitiveActions
548: .projectCommandAction(
549: RailsActionProvider.COMMAND_AUTOTEST,
550: bundle
551: .getString("LBL_AutoTest"),
552: null)); // NOI18N
553: }
554: actions.add(ProjectSensitiveActions.projectCommandAction(
555: ActionProvider.COMMAND_DEBUG, bundle
556: .getString("LBL_DebugAction_Name"), null)); // NOI18N
557: actions.add(ProjectSensitiveActions.projectCommandAction(
558: ActionProvider.COMMAND_TEST, bundle
559: .getString("LBL_TestAction_Name"), null)); // NOI18N
560: actions.add(CommonProjectActions
561: .setProjectConfigurationAction());
562: actions.add(null);
563: actions.add(CommonProjectActions.setAsMainProjectAction());
564: actions.add(CommonProjectActions.openSubprojectsAction());
565: actions.add(CommonProjectActions.closeProjectAction());
566: actions.add(null);
567: actions.add(CommonProjectActions.renameProjectAction());
568: actions.add(CommonProjectActions.moveProjectAction());
569: actions.add(CommonProjectActions.copyProjectAction());
570: actions.add(CommonProjectActions.deleteProjectAction());
571: actions.add(null);
572: actions.add(SystemAction.get(FindAction.class));
573:
574: // honor 57874 contact
575:
576: Collection<? extends Object> res = Lookups.forPath(
577: "Projects/Actions").lookupAll(Object.class); // NOI18N
578: if (!res.isEmpty()) {
579: actions.add(null);
580: for (Object next : res) {
581: if (next instanceof Action) {
582: actions.add((Action) next);
583: } else if (next instanceof JSeparator) {
584: actions.add(null);
585: }
586: }
587: }
588:
589: actions.add(null);
590: actions.add(CommonProjectActions.customizeProjectAction());
591:
592: return (Action[]) actions
593: .toArray(new Action[actions.size()]);
594: }
595:
596: public @Override
597: String toString() {
598: return super .toString() + "[project=" + project + "]"; // NOI18N
599: }
600: }
601: }
|