blob: c9532c52512e8c4d324d468cbc62bf7f3a7fc103 [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.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.Action;
import javax.swing.Icon;
import javax.swing.SwingUtilities;
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.project.Project;
import org.netbeans.api.project.ProjectInformation;
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.api.queries.VisibilityQuery;
import static org.netbeans.modules.project.ui.Bundle.*;
import org.netbeans.spi.project.ui.LogicalViewProvider;
import org.netbeans.spi.project.ui.support.CommonProjectActions;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileStateInvalidException;
import org.openide.filesystems.FileUIUtils;
import org.openide.filesystems.FileUtil;
import org.openide.loaders.ChangeableDataFilter;
import org.openide.loaders.DataFilter;
import org.openide.loaders.DataFolder;
import org.openide.loaders.DataObject;
import org.openide.loaders.DataObjectNotFoundException;
import org.openide.nodes.FilterNode;
import org.openide.nodes.Node;
import org.openide.nodes.NodeEvent;
import org.openide.nodes.NodeListener;
import org.openide.nodes.NodeMemberEvent;
import org.openide.nodes.NodeNotFoundException;
import org.openide.nodes.NodeOp;
import org.openide.nodes.NodeReorderEvent;
import org.openide.util.ChangeSupport;
import org.openide.util.ImageUtilities;
import org.openide.util.Lookup;
import org.openide.util.NbBundle.Messages;
import org.openide.util.RequestProcessor;
import org.openide.util.WeakListeners;
import org.openide.util.lookup.Lookups;
import org.openide.util.lookup.ProxyLookup;
/**
* Support for creating logical views.
* @author Jesse Glick, Petr Hrebejk
*/
public class PhysicalView {
private PhysicalView() {}
private static final Logger LOG = Logger.getLogger(PhysicalView.class.getName());
private static final RequestProcessor RP = new RequestProcessor(PhysicalView.class);
private static final class GroupNodeInfo {
public final boolean isProjectDir;
public GroupNodeInfo(boolean isProjectDir) {
this.isProjectDir = isProjectDir;
}
}
public static boolean isProjectDirNode( Node n ) {
GroupNodeInfo i = n.getLookup().lookup(GroupNodeInfo.class);
return i != null && i.isProjectDir;
}
public static Node[] createNodesForProject( Project p ) {
Sources s = ProjectUtils.getSources(p);
SourceGroup[] groups = s.getSourceGroups(Sources.TYPE_GENERIC);
final List<Node> nodesList = new ArrayList<>( groups.length );
for (SourceGroup group : groups) {
final Node n = createNodeForSourceGroup(group, p);
if (n != null) {
nodesList.add(n);
}
}
Node nodes[] = new Node[ nodesList.size() ];
nodesList.toArray( nodes );
return nodes;
}
@CheckForNull
static Node createNodeForSourceGroup(
@NonNull final SourceGroup group,
@NonNull final Project project) {
if ("sharedlibraries".equals(group.getName())) { //NOI18N
//HACK - ignore shared libs group in UI, it's only useful for version control commits.
return null;
}
final FileObject rootFolder = group.getRootFolder();
if (!rootFolder.isValid() || !rootFolder.isFolder()) {
return null;
}
final FileObject projectDirectory = project.getProjectDirectory();
return new ProjectIconNode(new GroupNode(
project,
group,
projectDirectory.equals(rootFolder) || FileUtil.isParentOf(rootFolder, projectDirectory),
DataFolder.findFolder(rootFolder)),
true);
}
static final class VisibilityQueryDataFilter implements ChangeListener, ChangeableDataFilter, DataFilter.FileBased {
private final ChangeSupport changeSupport = new ChangeSupport( this );
public VisibilityQueryDataFilter() {
VisibilityQuery.getDefault().addChangeListener( this );
}
public @Override boolean acceptDataObject(DataObject obj) {
return acceptFileObject(obj.getPrimaryFile());
}
public @Override void stateChanged(ChangeEvent e) {
final Runnable r = new Runnable () {
public @Override void run() {
changeSupport.fireChange();
}
};
SwingUtilities.invokeLater(r);
}
public @Override void addChangeListener(ChangeListener listener) {
changeSupport.addChangeListener( listener );
}
public @Override void removeChangeListener(ChangeListener listener) {
changeSupport.removeChangeListener( listener );
}
public @Override boolean acceptFileObject(FileObject fo) {
return VisibilityQuery.getDefault().isVisible(fo);
}
}
static final class GroupNode extends FilterNode implements PropertyChangeListener {
private static final DataFilter VISIBILITY_QUERY_FILTER = new VisibilityQueryDataFilter();
private ProjectInformation pi;
private SourceGroup group;
private boolean isProjectDir;
private Boolean initialized;
private final Node projectDelegateNode;
public GroupNode(Project project, SourceGroup group, boolean isProjectDir, DataFolder dataFolder ) {
super( dataFolder.getNodeDelegate(),
dataFolder.createNodeChildren( VISIBILITY_QUERY_FILTER ),
createLookup(project, group, dataFolder, isProjectDir));
this.pi = ProjectUtils.getInformation( project );
this.group = group;
this.isProjectDir = isProjectDir;
if(isProjectDir) {
LogicalViewProvider lvp = project.getLookup().lookup(LogicalViewProvider.class);
// used to retrieve e.g. actions in case of a folder representing a project,
// so that a projects context menu is the same is in a logical view
this.projectDelegateNode = lvp != null ? lvp.createLogicalView() : null;
} else {
this.projectDelegateNode = null;
}
pi.addPropertyChangeListener(WeakListeners.propertyChange(this, pi));
group.addPropertyChangeListener( WeakListeners.propertyChange( this, group ) );
}
private boolean initialized() {
synchronized (RP) {
if (initialized != null) {
return initialized;
} else {
initialized = false;
RP.post(new Runnable() {
@Override public void run() {
pi.getDisplayName();
synchronized (RP) {
initialized = true;
}
fireNameChange(null, null);
fireDisplayNameChange(null, null);
}
});
return false;
}
}
}
// XXX May need to change icons as well
public @Override String getName() {
if (isProjectDir && initialized()) {
return pi.getName();
}
else {
String n = group.getName();
if (n == null) {
n = "???"; // NOI18N
LOG.log(Level.WARNING, "SourceGroup impl of type {0} specified a null getName(); this is illegal", group.getClass().getName());
}
return n;
}
}
@Messages({"# {0} - display name of the group", "# {1} - display name of the project", "# {2} - original name of the folder", "FMT_PhysicalView_GroupName={1} - {0}"})
public @Override String getDisplayName() {
if ( isProjectDir ) {
return initialized() ? pi.getDisplayName() : group.getDisplayName();
}
else {
return FMT_PhysicalView_GroupName(group.getDisplayName(), pi.getDisplayName(), getOriginal().getDisplayName());
}
}
@Messages({"HINT_project=Project in {0}", "HINT_group=Source folder in {0}"})
public @Override String getShortDescription() {
FileObject gdir = group.getRootFolder();
String dir = FileUtil.getFileDisplayName(gdir);
return isProjectDir ? HINT_project(dir) : HINT_group(dir);
}
public @Override boolean canRename() {
return false;
}
@Override public Node.PropertySet[] getPropertySets() {
return new Node.PropertySet[0];
}
public @Override boolean canCut() {
return false;
}
public @Override boolean canCopy() {
// At least for now.
return false;
}
public @Override boolean canDestroy() {
return false;
}
public @Override Action[] getActions(boolean context) {
if ( context ) {
return super.getActions( true );
}
else {
Action[] folderActions = super.getActions( false );
Action[] projectActions;
if ( isProjectDir ) {
if( projectDelegateNode != null ) {
projectActions = projectDelegateNode.getActions( false );
}
else {
// If this is project dir then the properties action
// has to be replaced to invoke project customizer
projectActions = new Action[ folderActions.length ];
for ( int i = 0; i < folderActions.length; i++ ) {
if ( folderActions[i] instanceof org.openide.actions.PropertiesAction ) {
projectActions[i] = CommonProjectActions.customizeProjectAction();
}
else {
projectActions[i] = folderActions[i];
}
}
}
}
else {
projectActions = folderActions;
}
return projectActions;
}
}
// Private methods -------------------------------------------------
public @Override void propertyChange(final PropertyChangeEvent evt) {
final Runnable r = new Runnable () {
public @Override void run() {
String prop = evt.getPropertyName();
boolean ok = false;
if (prop == null || ProjectInformation.PROP_DISPLAY_NAME.equals(prop)) {
fireDisplayNameChange(null, null);
ok = true;
}
if (prop == null || ProjectInformation.PROP_NAME.equals(prop)) {
fireNameChange(null, null);
ok = true;
}
if (prop == null || ProjectInformation.PROP_ICON.equals(prop)) {
// OK, ignore
ok = true;
}
if (prop == null || "name".equals(prop) ) { // NOI18N
fireNameChange(null, null);
ok = true;
}
if (prop == null || "displayName".equals(prop) ) { // NOI18N
fireDisplayNameChange(null, null);
ok = true;
}
if (prop == null || "icon".equals(prop) ) { // NOI18N
// OK, ignore
ok = true;
}
if (prop == null || "rootFolder".equals(prop) ) { // NOI18N
// XXX Do something to children and lookup
fireNameChange(null, null);
fireDisplayNameChange(null, null);
fireShortDescriptionChange(null, null);
ok = true;
}
if (prop == null || SourceGroup.PROP_CONTAINERSHIP.equals(prop)) {
// OK, ignore
ok = true;
}
if (!ok) {
assert false : "Attempt to fire an unsupported property change event from " + pi.getClass().getName() + ": " + prop;
}
}
};
SwingUtilities.invokeLater(r);
}
private static Lookup createLookup(Project p, SourceGroup group, DataFolder dataFolder, boolean isProjectDir) {
return new ProxyLookup(
dataFolder.getNodeDelegate().getLookup(),
Lookups.fixed(p, new PathFinder(group), new GroupNodeInfo(isProjectDir)),
p.getLookup());
}
}
static final class ProjectIconNode extends FilterNode implements NodeListener { // #194068
private final boolean root;
public ProjectIconNode(Node orig, boolean root) {
super(orig, orig.isLeaf() ? Children.LEAF : new ProjectBadgingChildren(orig));
this.root = root;
setValue("VCS_PHYSICAL", Boolean.TRUE); //#159543
addNodeListener(this);
}
@Override
protected NodeListener createNodeListener() {
return new NodeAdapter(this) {
@Override
protected void propertyChange(FilterNode fn, PropertyChangeEvent ev) {
super.propertyChange(fn, ev);
if (Node.PROP_LEAF.equals(ev.getPropertyName())) {
Node orig = getOriginal();
setChildren(orig.isLeaf() ? Children.LEAF : new ProjectBadgingChildren(orig));
}
}
};
}
public @Override Image getIcon(int type) {
return swap(super.getIcon(type), type);
}
public @Override Image getOpenedIcon(int type) {
return swap(super.getOpenedIcon(type), type);
}
private Image swap(Image base, int type) {
if (!root) { // do not use icon on root node in Files tab
FileObject folder = getOriginal().getLookup().lookup(FileObject.class);
if (folder != null && folder.isFolder()) {
ProjectManager.Result r = ProjectManager.getDefault().isProject2(folder);
if (r != null) {
Icon icon = r.getIcon();
if (icon != null) {
Image img = ImageUtilities.icon2Image(icon);
try {
//#217008
DataFolder df = getOriginal().getLookup().lookup(DataFolder.class);
img = FileUIUtils.getImageDecorator(folder.getFileSystem()).annotateIcon(img, type, df.files());
} catch (FileStateInvalidException e) {
// no fs, do nothing
}
return img;
}
}
}
}
return base;
}
public @Override String getShortDescription() {
FileObject folder = getOriginal().getLookup().lookup(FileObject.class);
if (folder != null && folder.isFolder()) {
try {
Project p = ProjectManager.getDefault().findProject(folder);
if (p != null) {
return ProjectUtils.getInformation(p).getDisplayName();
}
} catch (IOException x) {
LOG.log(Level.FINE, null, x);
}
}
return super.getShortDescription();
}
@Override
public void childrenAdded(NodeMemberEvent ev) {
}
@Override
public void childrenRemoved(NodeMemberEvent ev) {
}
@Override
public void childrenReordered(NodeReorderEvent ev) {
}
@Override
public void nodeDestroyed(NodeEvent ev) {
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
}
}
private static final class ProjectBadgingChildren extends FilterNode.Children {
public ProjectBadgingChildren(Node orig) {
super(orig);
}
protected @Override Node copyNode(Node orig) {
return new ProjectIconNode(orig, false);
}
}
public static class PathFinder {
private SourceGroup group;
public PathFinder( SourceGroup group ) {
this.group = group;
}
public Node findPath( Node root, Object object ) {
if ( !( object instanceof FileObject ) ) {
return null;
}
FileObject fo = (FileObject)object;
FileObject groupRoot = group.getRootFolder();
if ( FileUtil.isParentOf( groupRoot, fo ) /* && group.contains( fo ) */ ) {
// The group contains the object
String relPath = FileUtil.getRelativePath( groupRoot, fo );
ArrayList<String> path = new ArrayList<String>();
StringTokenizer strtok = new StringTokenizer( relPath, "/" );
while( strtok.hasMoreTokens() ) {
path.add( strtok.nextToken() );
}
if (path.size() > 0) {
path.remove(path.size() - 1);
} else {
return null;
}
try {
//#75205
Node parent = NodeOp.findPath( root, Collections.enumeration( path ) );
if (parent != null) {
//not nice but there isn't a findNodes(name) method.
Node[] nds = parent.getChildren().getNodes(true);
for (int i = 0; i < nds.length; i++) {
FileObject dobj = nds[i].getLookup().lookup(FileObject.class);
if (dobj != null && fo.equals(dobj)) {
return nds[i];
}
}
String name = fo.getName();
try {
DataObject dobj = DataObject.find( fo );
name = dobj.getNodeDelegate().getName();
} catch (DataObjectNotFoundException ex) {
}
return parent.getChildren().findChild(name);
}
}
catch ( NodeNotFoundException e ) {
return null;
}
}
else if ( groupRoot.equals( fo ) ) {
return root;
}
return null;
}
}
}