blob: d4512dbd963f3683ed8a259dfa0ab932b1e122d9 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.project.ui;
import java.awt.Image;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.CharConversionException;
import java.io.IOException;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.Action;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.annotations.common.NullAllowed;
import org.netbeans.api.annotations.common.StaticResource;
import org.netbeans.api.project.FileOwnerQuery;
import org.netbeans.api.project.Project;
import org.netbeans.spi.project.ProjectIconAnnotator;
import org.netbeans.api.project.ProjectManager;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.api.project.SourceGroup;
import org.netbeans.api.project.Sources;
import org.netbeans.spi.project.ActionProvider;
import org.netbeans.spi.project.FileOwnerQueryImplementation;
import org.netbeans.spi.project.ui.LogicalViewProvider;
import org.netbeans.spi.project.ui.support.ProjectConvertors;
import org.openide.filesystems.FileChangeAdapter;
import org.openide.filesystems.FileChangeListener;
import org.openide.filesystems.FileEvent;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileStateInvalidException;
import org.openide.filesystems.FileStatusEvent;
import org.openide.filesystems.FileStatusListener;
import org.openide.filesystems.FileSystem;
import org.openide.filesystems.FileUIUtils;
import org.openide.filesystems.FileUtil;
import org.openide.nodes.AbstractNode;
import org.openide.nodes.Children;
import org.openide.nodes.FilterNode;
import org.openide.nodes.Node;
import static org.openide.nodes.Node.PROP_DISPLAY_NAME;
import org.openide.nodes.NodeEvent;
import org.openide.nodes.NodeListener;
import org.openide.nodes.NodeMemberEvent;
import org.openide.nodes.NodeReorderEvent;
import org.openide.util.Lookup;
import org.openide.util.LookupEvent;
import org.openide.util.LookupListener;
import org.openide.util.Mutex;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
import org.openide.util.Union2;
import org.openide.util.Utilities;
import org.openide.util.WeakListeners;
import org.openide.util.WeakSet;
import org.openide.util.lookup.Lookups;
import org.openide.util.lookup.ProxyLookup;
import org.openide.util.lookup.ServiceProvider;
import org.openide.xml.XMLUtil;
/** Root node for list of open projects
*/
public class ProjectsRootNode extends AbstractNode {
private static final Logger LOG = Logger.getLogger(ProjectsRootNode.class.getName());
private static final Set<ProjectsRootNode> all = new WeakSet<ProjectsRootNode>();
private static final RequestProcessor RP = new RequestProcessor(ProjectsRootNode.class);
static final int PHYSICAL_VIEW = 0;
static final int LOGICAL_VIEW = 1;
private static final @StaticResource String ICON_BASE = "org/netbeans/modules/project/ui/resources/projectsRootNode.gif"; //NOI18N
public static final String ACTIONS_FOLDER = "ProjectsTabActions"; // NOI18N
public static final String ACTIONS_FOLDER_PHYSICAL = "FilesTabActions";
private ResourceBundle bundle;
private final int type;
public ProjectsRootNode( int type ) {
super(new ProjectChildren(type), /* for CollapseAll */Lookups.singleton(type == LOGICAL_VIEW ? ProjectTab.ID_LOGICAL : ProjectTab.ID_PHYSICAL));
setIconBaseWithExtension( ICON_BASE );
this.type = type;
synchronized(all){
all.add(this);
}
}
@Override
public String getName() {
return ( "OpenProjects" ); // NOI18N
}
@Override
public String getDisplayName() {
if ( this.bundle == null ) {
this.bundle = NbBundle.getBundle( ProjectsRootNode.class );
}
return bundle.getString( "LBL_OpenProjectsNode_Name" ); // NOI18N
}
@Override
public boolean canRename() {
return false;
}
@Override
public Node.Handle getHandle() {
return new Handle(type);
}
@Override
public Action[] getActions( boolean context ) {
if (context) { // XXX why?
return new Action[0];
} else {
List<? extends Action> actions = Utilities.actionsForPath(type == PHYSICAL_VIEW ? ACTIONS_FOLDER_PHYSICAL : ACTIONS_FOLDER);
return actions.toArray(new Action[actions.size()]);
}
}
/** Finds node for given object in the view
* @return the node or null if the node was not found
*/
Node findNode(FileObject target) {
ProjectChildren ch = (ProjectChildren)getChildren();
assert ((ch.type == LOGICAL_VIEW) || (ch.type == PHYSICAL_VIEW));
// Speed up search in case we have an owner project - look in its node first.
Project ownerProject = findProject(target);
final SelectInProjectFileOwnerQueryImpl foq = SelectInProjectFileOwnerQueryImpl.getInstance();
if (foq != null) {
foq.setCurrentProject(target, ownerProject);
}
try {
for (int lookOnlyInOwnerProject = (ownerProject != null) ? 0 : 1; lookOnlyInOwnerProject < 2; lookOnlyInOwnerProject++) {
for (Node node : ch.getNodes(true)) {
Project p = node.getLookup().lookup(Project.class);
assert p != null : "Should have had a Project in lookup of " + node;
if (lookOnlyInOwnerProject == 0 && p != ownerProject) {
continue; // but try again (in next outer loop) as a fallback
}
Node n = null;
LogicalViewProvider lvp = p.getLookup().lookup(LogicalViewProvider.class);
if (lvp != null) {
// XXX (cf. #63554): really should be calling this on DataObject usually, since
// DataNode does *not* currently have a FileObject in its lookup (should it?)
// ...but it is not clear who has implemented findPath to assume FileObject!
n = lvp.findPath(node, target);
}
if (n == null && ch.type == PHYSICAL_VIEW) {
PhysicalView.PathFinder pf = node.getLookup().lookup(PhysicalView.PathFinder.class);
if ( pf != null ) {
n = pf.findPath(node, target);
}
}
if ( n != null ) {
return n;
}
}
}
} finally {
if (foq != null) {
foq.clearCurrentProject();
}
}
return null;
}
static void checkNoLazyNode() {
synchronized(all){
for (ProjectsRootNode root : all) {
checkNoLazyNode(root.getChildren());
}
}
}
static void checkNoLazyNode(Children children) {
for (Node n : children.getNodes()) {
if (n instanceof BadgingNode) {
((BadgingNode)n).replaceProject(null);
}
if (n.getLookup().lookup(LazyProject.class) != null) {
OpenProjectList.LOGGER.warning("LazyProjects remain visible");
}
}
}
@CheckForNull
private static Project findProject(@NonNull final FileObject target) {
Project owner = FileOwnerQuery.getOwner(target);
if (owner != null && ProjectConvertors.isConvertorProject(owner)) {
FileObject dir = owner.getProjectDirectory().getParent();
while (dir != null) {
Project p = FileOwnerQuery.getOwner(dir);
if (p != null && !ProjectConvertors.isConvertorProject(p)) {
owner = p;
break;
}
dir = dir.getParent();
}
}
return owner;
}
private static class Handle implements Node.Handle {
private static final long serialVersionUID = 78374332058L;
private final int viewType;
public Handle( int viewType ) {
this.viewType = viewType;
}
@Override
public Node getNode() {
return new ProjectsRootNode( viewType );
}
}
// However project rename is currently disabled so it is not a big deal
static class ProjectChildren extends Children.Keys<ProjectChildren.Pair> implements ChangeListener, PropertyChangeListener, NodeListener {
static final RequestProcessor RP = new RequestProcessor(ProjectChildren.class);
private final java.util.Map <Sources,Reference<Project>> sources2projects = new WeakHashMap<Sources,Reference<Project>>();
//@GuardedBy("projects2Pairs")
private final java.util.Map <Project,Reference<Pair>> projects2Pairs = Collections.synchronizedMap(new WeakHashMap<>());
final int type;
public ProjectChildren( int type ) {
this.type = type;
}
// Children.Keys impl --------------------------------------------------
@Override
public void addNotify() {
OpenProjectList.getDefault().addPropertyChangeListener(this);
if (Boolean.getBoolean("test.projectnode.sync")) {
setKeys( getKeys());
} else {
RP.post(new Runnable() {
@Override
public void run() {
setKeys( getKeys() );
}
});
}
}
@Override
public void removeNotify() {
OpenProjectList.getDefault().removePropertyChangeListener(this);
for (Sources sources : sources2projects.keySet()) {
sources.removeChangeListener( this );
}
sources2projects.clear();
projects2Pairs.clear();
setKeys(Collections.<Pair>emptySet());
}
@Override
public int getNodesCount(boolean optimalResult) {
if (optimalResult) {
setKeys(getKeys());
}
return super.getNodesCount(optimalResult);
}
@Override
protected Node[] createNodes(Pair p) {
Project project = p.project;
Node origNodes[] = null;
boolean[] projectInLookup = new boolean[1];
projectInLookup[0] = true;
if (type == PHYSICAL_VIEW) {
final Sources sources = p.data.second().first();
final SourceGroup[] groups = p.data.second().second();
sources.removeChangeListener( this );
sources.addChangeListener( this );
sources2projects.put( sources, new WeakReference<Project>( project ) );
final List<Node> nodes = new ArrayList<>(groups.length);
for (SourceGroup group : groups) {
final Node n = PhysicalView.createNodeForSourceGroup(group, project);
if (n != null) {
nodes.add(n);
}
}
origNodes = nodes.toArray(new Node[nodes.size()]);
} else {
assert type == LOGICAL_VIEW;
origNodes = new Node[] {
logicalViewForProject(
project,
p.data,
projectInLookup)
};
}
Node[] badgedNodes = new Node[ origNodes.length ];
for( int i = 0; i < origNodes.length; i++ ) {
if ( type == PHYSICAL_VIEW && !PhysicalView.isProjectDirNode( origNodes[i] ) ) {
// Don't badge external sources
badgedNodes[i] = origNodes[i];
}
else {
badgedNodes[i] = new BadgingNode(
this,
p,
origNodes[i],
type == LOGICAL_VIEW
);
}
}
return badgedNodes;
}
@NonNull
final Node logicalViewForProject(
@NonNull final Project project,
final Union2<LogicalViewProvider,org.openide.util.Pair<Sources,SourceGroup[]>> data,
final boolean[] projectInLookup) {
Node node;
if (!data.hasFirst()) {
LOG.log(
Level.WARNING,
"Warning - project of {0} in {1} doesn't supply a LogicalViewProvider in its lookup", // NOI18N
new Object[]{
project.getClass(),
FileUtil.getFileDisplayName(project.getProjectDirectory())
});
final Sources sources = data.second().first();
final SourceGroup[] groups = data.second().second();
sources.removeChangeListener(this);
sources.addChangeListener(this);
if (groups.length > 0) {
node = PhysicalView.createNodeForSourceGroup(groups[0], project);
} else {
node = Node.EMPTY;
}
} else {
final LogicalViewProvider lvp = data.first();
node = lvp.createLogicalView();
if (!project.equals(node.getLookup().lookup(Project.class))) {
// Various actions, badging, etc. are not going to work.
LOG.log(
Level.WARNING,
"Warning - project {0} failed to supply itself in the lookup of the root node of its own logical view", // NOI18N
ProjectUtils.getInformation(project).getName());
//#114664
if (projectInLookup != null) {
projectInLookup[0] = false;
}
}
}
node.addNodeListener(WeakListeners.create(NodeListener.class, this, node));
return node;
}
// NodeListener impl -----------------------------------------
@Override public void childrenAdded(NodeMemberEvent ev) { }
@Override public void childrenRemoved(NodeMemberEvent ev) { }
@Override public void childrenReordered(NodeReorderEvent ev) { }
@Override public void nodeDestroyed(NodeEvent ev) { }
// PropertyChangeListener & NodeListener impl -----------------------------------------
@Override
public void propertyChange( PropertyChangeEvent e ) {
if ( OpenProjectList.PROPERTY_OPEN_PROJECTS.equals( e.getPropertyName() ) ) {
RP.post(new Runnable() {
public @Override void run() {
setKeys(getKeys());
}
});
} else if( PROP_DISPLAY_NAME.equals(e.getPropertyName()) ) {
RP.schedule(new Runnable() {
public @Override void run() {
setKeys( getKeys() );
}
}, 500, TimeUnit.MILLISECONDS);
}
}
// Change listener impl ------------------------------------------------
@Override
public void stateChanged( ChangeEvent e ) {
Reference<Project> projectRef = sources2projects.get(e.getSource());
if ( projectRef == null ) {
return;
}
final Project project = projectRef.get();
if ( project == null ) {
return;
}
// Fix for 50259, callers sometimes hold locks
RP.post(new Runnable() {
public @Override void run() {
Optional.ofNullable(projects2Pairs.get(project))
.map((ref) -> ref.get())
.ifPresent((p) -> p.update(project));
refresh(project);
}
} );
}
final void refresh(Project p) {
refreshKey(new Pair(p, type));
}
// Own methods ---------------------------------------------------------
public Collection<Pair> getKeys() {
List<Project> projects = Arrays.asList( OpenProjectList.getDefault().getOpenProjects() );
Collections.sort(projects, OpenProjectList.projectByDisplayName());
final List<Pair> dirs = new ArrayList<>(projects.size());
final java.util.Map<Project,Pair> snapshot = new HashMap<>();
for (Project project : projects) {
final Pair p = new Pair(project, type);
dirs.add(p);
snapshot.put(project, p);
}
synchronized (projects2Pairs) {
projects2Pairs.clear();
snapshot.entrySet()
.forEach((e) -> projects2Pairs.put(
e.getKey(),
new WeakReference<>(e.getValue())));
}
return dirs;
}
/** Object that comparers two projects just by their directory.
* This allows to replace a LazyProject with real one without discarding
* the nodes.
*/
static final class Pair extends Object {
Project project;
final FileObject fo;
private final int type;
private Union2<LogicalViewProvider,org.openide.util.Pair<Sources,SourceGroup[]>> data;
public Pair(
final Project project,
final int type) {
this.project = project;
this.fo = project.getProjectDirectory();
this.type = type;
this.data = createData(project, type);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Pair other = (Pair) obj;
if (this.fo != other.fo && (this.fo == null || !this.fo.equals(other.fo))) {
return false;
}
return true;
}
@Override
public int hashCode() {
int hash = 7;
hash = 53 * hash + (this.fo != null ? this.fo.hashCode() : 0);
return hash;
}
private void update(@NonNull final Project project) {
assert project != null;
this.project = project;
this.data = createData(project, type);
}
private static Union2<LogicalViewProvider,org.openide.util.Pair<Sources,SourceGroup[]>> createData(
final Project p,
final int type) {
switch (type) {
case LOGICAL_VIEW:
final LogicalViewProvider lvp = p.getLookup().lookup(LogicalViewProvider.class);
if (lvp != null) {
return Union2.createFirst(lvp);
}
case PHYSICAL_VIEW:
final Sources s = ProjectUtils.getSources(p);
final SourceGroup[] groups = s.getSourceGroups(Sources.TYPE_GENERIC);
return Union2.createSecond(org.openide.util.Pair.of(s, groups));
default:
throw new IllegalArgumentException(Integer.toString(type));
}
}
}
}
static final class BadgingNode extends FilterNode implements ChangeListener, PropertyChangeListener, Runnable, FileStatusListener {
private static final String MAGIC = "BadgingNode.μαγικ"; // #199591
private final Object privateLock = new Object();
private Set<FileObject> files;
private Map<FileSystem,FileStatusListener> fileSystemListeners;
private ChangeListener sourcesListener;
private Map<SourceGroup,PropertyChangeListener> groupsListeners;
RequestProcessor.Task task;
private boolean nameChange;
private boolean iconChange;
private volatile Boolean mainCache;
private final ProjectChildren ch;
private final boolean logicalView;
private final ProjectChildren.Pair pair;
private final Set<FileObject> projectDirsListenedTo = new WeakSet<FileObject>();
private static final int DELAY = 50;
private final FileChangeListener newSubDirListener = new FileChangeAdapter() {
public @Override void fileDataCreated(FileEvent fe) {
setProjectFilesAsynch();
}
public @Override void fileFolderCreated(FileEvent fe) {
setProjectFilesAsynch();
}
};
private void setProjectFilesAsynch() {
if (Boolean.getBoolean("test.nodelay")) { //for tests only
setProjectFiles();
return;
}
fsRefreshTask.schedule(DELAY);
}
private final RequestProcessor.Task fsRefreshTask = Hacks.RP.create(new Runnable() {
@Override
public void run() {
setProjectFiles();
}
});
private final Lookup.Result<ProjectIconAnnotator> result = Lookup.getDefault().lookupResult(ProjectIconAnnotator.class);
static class AnnotationListener implements LookupListener, ChangeListener {
private final Set<ProjectIconAnnotator> annotators = new WeakSet<ProjectIconAnnotator>();
private final Reference<BadgingNode> node;
public AnnotationListener(BadgingNode node) {
this.node = new WeakReference<BadgingNode>(node);
}
void init() {
BadgingNode n = node.get();
if (n == null) {
return;
}
for (ProjectIconAnnotator annotator : n.result.allInstances()) {
if (annotators.add(annotator)) {
annotator.addChangeListener(WeakListeners.change(this, annotator));
}
}
}
public @Override void resultChanged(LookupEvent ev) {
init();
stateChanged(null);
}
public @Override void stateChanged(ChangeEvent e) {
BadgingNode n = node.get();
if (n == null) {
return;
}
n.fireIconChange();
n.fireOpenedIconChange();
}
}
public BadgingNode(ProjectChildren ch, ProjectChildren.Pair p, Node n, boolean logicalView) {
super(n, null, badgingLookup(n));
this.ch = ch;
this.pair = p;
this.logicalView = logicalView;
OpenProjectList.log(Level.FINER, "BadgingNode init {0}", toStringForLog()); // NOI18N
OpenProjectList.getDefault().addPropertyChangeListener(WeakListeners.propertyChange(this, OpenProjectList.getDefault()));
setProjectFilesAsynch();
OpenProjectList.log(Level.FINER, "BadgingNode finished {0}", toStringForLog()); // NOI18N
AnnotationListener annotationListener = new AnnotationListener(this);
annotationListener.init();
result.addLookupListener(annotationListener);
}
private static Lookup badgingLookup(Node n) {
return new BadgingLookup(n.getLookup());
}
protected final void setProjectFiles() {
Project prj = getLookup().lookup(Project.class);
if (prj != null && /* #145682 */ !(prj instanceof LazyProject)) {
setProjectFiles(prj);
}
}
private void replaceProject(Project newProj) {
if (newProj == null) {
try {
newProj = ProjectManager.getDefault().findProject(pair.fo);
if (newProj == pair.project) {
return;
}
} catch (IOException ex) {
OpenProjectList.log(Level.INFO, "No project for " + pair.fo, ex); // NOI18N
} catch (IllegalArgumentException ex) {
OpenProjectList.log(Level.INFO, "No project for " + pair.fo, ex); // NOI18N
}
}
OpenProjectList.log(Level.FINER, "replacing for {0}", toStringForLog());
Project p = getLookup().lookup(Project.class);
if (p == null) {
OpenProjectList.log(Level.FINE, "no project in lookup {0}", toStringForLog());
return;
}
FileObject fo = p.getProjectDirectory();
if (newProj != null && newProj.getProjectDirectory().equals(fo)) {
Node n = null;
if (logicalView) {
n = ch.logicalViewForProject(
newProj,
ProjectChildren.Pair.createData(
newProj,
logicalView ? LOGICAL_VIEW : PHYSICAL_VIEW),
null);
OpenProjectList.log(Level.FINER, "logical view {0}", n);
} else {
Node[] arr = PhysicalView.createNodesForProject(newProj);
OpenProjectList.log(Level.FINER, "physical view {0}", Arrays.asList(arr));
if (arr.length > 1) {
pair.update(newProj);
OpenProjectList.log(Level.FINER, "refreshing for {0}", newProj);
ch.refresh(newProj);
OpenProjectList.log(Level.FINER, "refreshed for {0}", newProj);
return;
} else if (arr.length == 1) {
n = arr[0];
} else {
OpenProjectList.log(Level.WARNING, "newProject yields null node: " + newProj);
n = Node.EMPTY;
}
}
OpenProjectList.log(Level.FINER, "change original: {0}", n);
OpenProjectList.log(Level.FINER, "children before change original: {0}", getChildren());
OpenProjectList.log(Level.FINER, "delegate children before change original: {0}", getOriginal().getChildren());
changeOriginal(n, true);
OpenProjectList.log(Level.FINER, "delegate after change original: {0}", getOriginal());
OpenProjectList.log(Level.FINER, "name after change original: {0}", getName());
OpenProjectList.log(Level.FINER, "children after change original: {0}", getChildren());
OpenProjectList.log(Level.FINER, "delegate children after change original: {0}", getOriginal().getChildren());
BadgingLookup bl = (BadgingLookup) getLookup();
bl.setMyLookups(n.getLookup());
OpenProjectList.log(Level.FINER, "done {0}", toStringForLog());
setProjectFilesAsynch();
} else {
FileObject newDir;
if (newProj != null) {
newDir = newProj.getProjectDirectory();
} else {
newDir = null;
//#228790 use RP instead of EventQueue.invokeLater, job can block on project write mutex
RP.post(new Runnable() {
@Override
public void run() {
OpenProjectList.getDefault().close(new Project[] { pair.project }, false);
}
});
}
OpenProjectList.log(Level.FINER, "wrong directories. current: " + fo + " new " + newDir);
}
}
private void setProjectFiles(Project project) {
Sources sources = ProjectUtils.getSources(project); // returns singleton
if (sourcesListener == null) {
sourcesListener = WeakListeners.change(this, sources);
sources.addChangeListener(sourcesListener);
}
setGroups(Arrays.asList(sources.getSourceGroups(Sources.TYPE_GENERIC)), project.getProjectDirectory());
}
private void setGroups(Collection<SourceGroup> groups, FileObject projectDirectory) {
if (groupsListeners != null) {
for (Map.Entry<SourceGroup, PropertyChangeListener> entry : groupsListeners.entrySet()) {
entry.getKey().removePropertyChangeListener(entry.getValue());
}
}
Map<SourceGroup,PropertyChangeListener> _groupsListeners = new HashMap<SourceGroup, PropertyChangeListener>();
Set<FileObject> roots = new HashSet<FileObject>();
for (SourceGroup group : groups) {
PropertyChangeListener pcl = WeakListeners.propertyChange(this, group);
_groupsListeners.put(group, pcl);
group.addPropertyChangeListener(pcl);
FileObject fo = group.getRootFolder();
if (fo.equals(projectDirectory)) {
// #78994: do not listen to project root folder since changes in a nested project will mark it as modified.
// Instead, listen to direct subdirs which are owned by this project. Not very precise but the best we can do.
// (Would ideally obtain a complete but minimal list of dirs which cover this project but no subprojects.
// Unfortunately the current APIs provide no efficient way of doing this in general.)
for (FileObject kid : fo.getChildren()) {
Project owner = FileOwnerQuery.getOwner(kid);
// Not sufficient to check owner == project, because at startup owner will be a LazyProject.
if (owner != null && owner.getProjectDirectory() == projectDirectory) {
roots.add(kid);
}
}
if (projectDirsListenedTo.add(fo)) {
fo.addFileChangeListener(FileUtil.weakFileChangeListener(newSubDirListener, fo));
}
} else {
roots.add(fo);
}
}
groupsListeners = _groupsListeners;
setFiles(roots);
}
protected final void setFiles(Set<FileObject> files) {
if (fileSystemListeners != null) {
for (Map.Entry<FileSystem, FileStatusListener> entry : fileSystemListeners.entrySet()) {
entry.getKey().removeFileStatusListener(entry.getValue());
}
}
fileSystemListeners = new HashMap<FileSystem, FileStatusListener>();
this.files = files;
Set<FileSystem> hookedFileSystems = new HashSet<FileSystem>();
for (FileObject fo : files) {
try {
FileSystem fs = fo.getFileSystem();
if (hookedFileSystems.contains(fs)) {
continue;
}
hookedFileSystems.add(fs);
FileStatusListener fsl = FileUtil.weakFileStatusListener(this, fs);
fs.addFileStatusListener(fsl);
fileSystemListeners.put(fs, fsl);
} catch (FileStateInvalidException e) {
LOG.log(Level.INFO, "Cannot get " + fo + " filesystem, ignoring...", e); // NOI18N
}
}
}
@Override
public void run() {
boolean fireIcon;
boolean fireName;
synchronized (privateLock) {
fireIcon = iconChange;
fireName = nameChange;
iconChange = false;
nameChange = false;
}
if (fireIcon) {
fireIconChange();
fireOpenedIconChange();
}
if (fireName) {
fireDisplayNameChange(null, null);
}
}
@Override
public void annotationChanged(FileStatusEvent event) {
if (task == null) {
task = Hacks.RP.create(this);
}
synchronized (privateLock) {
if ((iconChange == false && event.isIconChange()) || (nameChange == false && event.isNameChange())) {
for (FileObject fo : files) {
if (event.hasChanged(fo)) {
iconChange |= event.isIconChange();
nameChange |= event.isNameChange();
}
}
}
}
task.schedule(50); // batch by 50 ms
}
public @Override String getDisplayName() {
String original = super.getDisplayName();
if (files != null && files.iterator().hasNext()) {
try {
original = files.iterator().next().getFileSystem().getDecorator().annotateName(original, files);
} catch (FileStateInvalidException e) {
LOG.log(Level.INFO, null, e);
}
}
return original;
}
/** Get display name used for logging as original display name can cause deadlock issue #160512 */
private String getLogName() {
String original = super.getDisplayName();
if (files != null && files.iterator().hasNext()) {
try {
original = files.iterator().next().getFileSystem().getDecorator().annotateName(original, files);
} catch (FileStateInvalidException e) {
LOG.log(Level.INFO, null, e);
}
}
return original;
}
/** Special version of to Strign used for logging as original toString uses display name
* => can cause deadlock issue #160512 */
private String toStringForLog() {
return getClass().getName() + "@" + Integer.toHexString(hashCode()) //NOI18N
+ "[Name=" + getName() + ", displayName=" + getLogName() + "]"; //NOI18N
}
public @Override String getHtmlDisplayName() {
String htmlName = getOriginal().getHtmlDisplayName();
if (htmlName == null) {
try {
htmlName = XMLUtil.toElementContent(getOriginal().getDisplayName());
} catch (CharConversionException ex) {
// ignore
}
}
if (htmlName == null) {
return null;
}
if (files != null && files.iterator().hasNext()) {
try {
String annotatedMagic = files.iterator().next().getFileSystem().
getDecorator().annotateNameHtml(MAGIC, files);
if (annotatedMagic != null) {
htmlName = annotatedMagic.replace(MAGIC, htmlName);
}
} catch (FileStateInvalidException e) {
LOG.log(Level.INFO, null, e);
}
}
return isMainAsync()? "<b>" + htmlName + "</b>" : htmlName;
}
public @Override Image getIcon(int type) {
return getIcon(type, false);
}
public @Override Image getOpenedIcon(int type) {
return getIcon(type, true);
}
private Image getIcon(int type, boolean opened) {
Image img = opened ? super.getOpenedIcon(type) : super.getIcon(type);
if (logicalView) {
if (files != null && files.iterator().hasNext()) {
try {
FileObject fo = files.iterator().next();
img = FileUIUtils.getImageDecorator(fo.getFileSystem()).annotateIcon(img, type, files);
} catch (FileStateInvalidException e) {
LOG.log(Level.INFO, null, e);
}
}
Project p = getLookup().lookup(Project.class);
if (p != null) {
for (ProjectIconAnnotator pa : result.allInstances()) {
img = pa.annotateIcon(p, img, opened);
}
}
}
return img;
}
@Override
public void propertyChange( PropertyChangeEvent e ) {
if ( OpenProjectList.PROPERTY_MAIN_PROJECT.equals( e.getPropertyName() ) ) {
mainCache = null;
fireDisplayNameChange( null, null );
}
if ( OpenProjectList.PROPERTY_REPLACE.equals(e.getPropertyName())) {
replaceProject((Project)e.getNewValue());
}
if (SourceGroup.PROP_CONTAINERSHIP.equals(e.getPropertyName())) {
setProjectFilesAsynch();
}
}
private boolean isMainAsync() {
final Boolean res = mainCache;
if (res != null) {
return res;
}
RP.execute(new Runnable() {
@Override
public void run() {
mainCache = isMain();
fireDisplayNameChange( null, null );
}
});
return false;
}
private boolean isMain() {
Project p = getLookup().lookup(Project.class);
return p != null && OpenProjectList.getDefault().isMainProject( p );
}
// sources change
@Override
public void stateChanged(ChangeEvent e) {
fsRefreshTask.schedule(DELAY);
}
@Override
public Object getValue(String attributeName) {
if ("customDelete".equals(attributeName)) {
return true;
}
return super.getValue(attributeName);
}
@Override
public boolean canDestroy() {
Project p = getLookup().lookup(Project.class);
if (p == null) {
return false;
}
ActionProvider ap = p.getLookup().lookup(ActionProvider.class);
String[] sa = ap != null ? ap.getSupportedActions() : new String[0];
int k = sa.length;
for (int i = 0; i < k; i++) {
if (ActionProvider.COMMAND_DELETE.equals(sa[i])) {
return ap.isActionEnabled(ActionProvider.COMMAND_DELETE, getLookup());
}
}
return false;
}
@Override
public void destroy() throws IOException {
Project p = getLookup().lookup(Project.class);
if (p == null) {
return;
}
final ActionProvider ap = p.getLookup().lookup(ActionProvider.class);
Mutex.EVENT.writeAccess(new Runnable() {
@Override
public void run() {
ap.invokeAction(ActionProvider.COMMAND_DELETE, getLookup());
}
});
}
} // end of BadgingNode
private static final class BadgingLookup extends ProxyLookup {
public BadgingLookup(Lookup... lkps) {
super(lkps);
}
public void setMyLookups(Lookup... lkps) {
setLookups(lkps);
}
public boolean isSearchInfo() {
return getLookups().length > 1;
}
} // end of BadgingLookup
/**
* The {@link FileOwnerQueryImplementation} returning the first non artificial project owner
* for {@link LogicalViewProvider#findPath}.
* This {@link FileOwnerQueryImplementation} removes a need for {@link Project}'s {@link LogicalViewProvider}s
* to ignore the artificial projects as done in {@link ProjectsRootNode#findProject}.
* Needs to have the higher priority than the SimpleFileOwnerQueryImplementation.
*/
@ServiceProvider(service = FileOwnerQueryImplementation.class, position = 10)
public static final class SelectInProjectFileOwnerQueryImpl implements FileOwnerQueryImplementation {
private final ThreadLocal<Object[]> current = new ThreadLocal<Object[]>();
@Override
@CheckForNull
public Project getOwner(final URI file) {
final Object[] currentTuple = current.get();
if (currentTuple != null) {
Object currentUri = currentTuple[1];
if (currentUri == null) {
currentTuple[1] = currentUri = ((FileObject)currentTuple[0]).toURI();
}
if (currentUri.equals(file)) {
return (Project) currentTuple[2];
}
}
return null;
}
@Override
public Project getOwner(final FileObject file) {
final Object[] currentTuple = current.get();
if (currentTuple != null && currentTuple[0].equals(file)) {
return (Project) currentTuple[2];
}
return null;
}
private void setCurrentProject(
@NonNull final FileObject fo,
@NullAllowed final Project prj) {
assert fo != null;
current.set(new Object[]{fo, null, prj});
}
private void clearCurrentProject() {
current.remove();
}
@CheckForNull
private static SelectInProjectFileOwnerQueryImpl getInstance() {
for (FileOwnerQueryImplementation impl : Lookup.getDefault().lookupAll(FileOwnerQueryImplementation.class)) {
if (SelectInProjectFileOwnerQueryImpl.class == impl.getClass()) {
return SelectInProjectFileOwnerQueryImpl.class.cast(impl);
}
}
return null;
}
}
}