package org.netbeans.gradle.project.view; import java.awt.Image; import java.util.ArrayList; import java.util.Collection; import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Logger; import javax.swing.Action; import org.jtrim.concurrent.UpdateTaskExecutor; import org.jtrim.event.CopyOnTriggerListenerManager; import org.jtrim.event.EventDispatcher; import org.jtrim.event.ListenerManager; import org.jtrim.event.ListenerRef; import org.jtrim.swing.concurrent.SwingUpdateTaskExecutor; import org.jtrim.utils.ExceptionHelper; import org.netbeans.gradle.project.NbGradleProject; import org.netbeans.gradle.project.NbIcons; import org.netbeans.gradle.project.NbStrings; import org.netbeans.gradle.project.ProjectDisplayInfo; import org.netbeans.gradle.project.ProjectIssue; import org.netbeans.gradle.project.ProjectIssue.Kind; import org.netbeans.gradle.project.api.nodes.NodeRefresher; import org.netbeans.gradle.project.model.ModelRefreshListener; import org.netbeans.gradle.project.util.ArrayUtils; import org.netbeans.spi.java.project.support.ui.PackageView; import org.netbeans.spi.project.ui.LogicalViewProvider; import org.netbeans.spi.project.ui.PathFinder; import org.openide.filesystems.FileObject; import org.openide.loaders.DataFolder; import org.openide.nodes.Children; import org.openide.nodes.FilterNode; import org.openide.nodes.Node; import org.openide.nodes.NodeAdapter; import org.openide.nodes.NodeEvent; import org.openide.util.ImageUtilities; import org.openide.util.Lookup; import org.openide.util.lookup.Lookups; import org.openide.util.lookup.ProxyLookup; public final class GradleProjectLogicalViewProvider implements LogicalViewProvider, ModelRefreshListener { private static final Logger LOGGER = Logger.getLogger(GradleProjectLogicalViewProvider.class.getName()); private final NbGradleProject project; private final ContextActionProvider actionProvider; private final ListenerManager<ModelRefreshListener> childRefreshListeners; private final AtomicReference<Collection<ModelRefreshListener>> listenersToFinalize; public GradleProjectLogicalViewProvider(NbGradleProject project, ContextActionProvider actionProvider) { ExceptionHelper.checkNotNullArgument(project, "project"); ExceptionHelper.checkNotNullArgument(actionProvider, "actionProvider"); this.project = project; this.actionProvider = actionProvider; this.childRefreshListeners = new CopyOnTriggerListenerManager<>(); this.listenersToFinalize = new AtomicReference<>(null); } public ListenerRef addChildModelRefreshListener(final ModelRefreshListener listener) { ExceptionHelper.checkNotNullArgument(listener, "listener"); return childRefreshListeners.registerListener(listener); } @Override public void startRefresh() { final List<ModelRefreshListener> listeners = new ArrayList<>(); childRefreshListeners.onEvent(new EventDispatcher<ModelRefreshListener, Void>() { @Override public void onEvent(ModelRefreshListener eventListener, Void arg) { eventListener.startRefresh(); listeners.add(eventListener); } }, null); Collection<ModelRefreshListener> prevListeners = listenersToFinalize.getAndSet(listeners); if (prevListeners != null) { LOGGER.warning("startRefresh/endRefresh mismatch."); } } @Override public void endRefresh(boolean extensionsChanged) { Collection<ModelRefreshListener> listeners = listenersToFinalize.getAndSet(null); if (listeners == null) { return; } for (ModelRefreshListener listener: listeners) { listener.endRefresh(extensionsChanged); } } @Override public Node createLogicalView() { DataFolder projectFolder = DataFolder.findFolder(project.getProjectDirectory()); final GradleProjectNode result = new GradleProjectNode(projectFolder, actionProvider); ProjectDisplayInfo displayInfo = project.getDisplayInfo(); final ListenerRef displayNameRef = displayInfo.displayName().addChangeListener(new Runnable() { @Override public void run() { result.fireDisplayNameChange(); } }); final ListenerRef descriptionRef = displayInfo.description().addChangeListener(new Runnable() { @Override public void run() { result.fireShortDescriptionChange(); } }); final ListenerRef modelListenerRef = project.currentModel().addChangeListener(new Runnable() { @Override public void run() { result.fireModelChange(); } }); final ListenerRef infoListenerRef = project.getProjectIssueManager().addChangeListener(new Runnable() { @Override public void run() { result.fireInfoChangeEvent(); } }); result.addNodeListener(new NodeAdapter(){ @Override public void nodeDestroyed(NodeEvent ev) { displayNameRef.unregister(); descriptionRef.unregister(); infoListenerRef.unregister(); modelListenerRef.unregister(); } }); return result; } private Lookup createLookup(GradleProjectChildFactory childFactory, Children children, Object... extraServices) { NodeRefresher nodeRefresher = NodeUtils.defaultNodeRefresher(children, childFactory); Object[] services = ArrayUtils.concatArrays( new Object[]{nodeRefresher, project.getProjectDirectory()}, extraServices); return new ProxyLookup(project.getLookup(), Lookups.fixed(services)); } private final class GradleProjectNode extends FilterNode { @SuppressWarnings("VolatileArrayField") private volatile Action[] actions; private final ContextActionProvider actionProvider; private final UpdateTaskExecutor modelChanges; private final UpdateTaskExecutor displayNameChanges; private final UpdateTaskExecutor descriptionChanges; private final UpdateTaskExecutor iconChanges; public GradleProjectNode(DataFolder projectFolder, ContextActionProvider actionProvider) { this(projectFolder, actionProvider, new GradleProjectChildFactory(project, GradleProjectLogicalViewProvider.this)); } private GradleProjectNode(DataFolder projectFolder, ContextActionProvider actionProvider, GradleProjectChildFactory childFactory) { this(projectFolder, actionProvider, childFactory, Children.create(childFactory, false)); } private GradleProjectNode( DataFolder projectFolder, ContextActionProvider actionProvider, GradleProjectChildFactory childFactory, org.openide.nodes.Children children) { // Do not add lookup of "node" because that might fool NB to believe that multiple projects are selected. super(projectFolder.getNodeDelegate().cloneNode(), children, createLookup(childFactory, children, projectFolder)); this.actionProvider = actionProvider; // TODO: It would be nicer to lazily initialize this list. this.actions = actionProvider.getActions(); this.modelChanges = new SwingUpdateTaskExecutor(true); this.displayNameChanges = new SwingUpdateTaskExecutor(true); this.descriptionChanges = new SwingUpdateTaskExecutor(true); this.iconChanges = new SwingUpdateTaskExecutor(true); } private void updateActionsList() { actions = actionProvider.getActions(); } public void fireDisplayNameChange() { displayNameChanges.execute(new Runnable() { @Override public void run() { fireDisplayNameChange(null, null); } }); } public void fireShortDescriptionChange() { descriptionChanges.execute(new Runnable() { @Override public void run() { fireShortDescriptionChange(null, null); } }); } public void fireModelChange() { modelChanges.execute(new Runnable() { @Override public void run() { updateActionsList(); } }); } public void fireInfoChangeEvent() { iconChanges.execute(new Runnable() { @Override public void run() { fireIconChange(); fireOpenedIconChange(); } }); } @Override public Action[] getActions(boolean context) { return actions.clone(); } private void appendHtmlList(String caption, List<String> toAdd, StringBuilder result) { if (toAdd.isEmpty()) { return; } result.append("<B>"); result.append(caption); result.append("</B>:"); result.append("<ul>\n"); for (String info: toAdd) { result.append("<li>"); // TODO: quote strings, so that they are valid for html result.append(info); result.append("</li>\n"); } result.append("</ul>\n"); } @Override public Image getIcon(int type) { Image icon = NbIcons.getGradleIcon(); Collection<ProjectIssue> infos = project.getProjectIssueManager().getIssues(); if (!infos.isEmpty()) { Map<ProjectIssue.Kind, List<String>> infoMap = new EnumMap<>(ProjectIssue.Kind.class); for (ProjectIssue.Kind kind: ProjectIssue.Kind.values()) { infoMap.put(kind, new ArrayList<String>()); } Kind mostImportantKind = Kind.INFO; for (ProjectIssue info: infos) { for (ProjectIssue.Entry entry: info.getEntries()) { Kind kind = entry.getKind(); if (mostImportantKind.getImportance() < kind.getImportance()) { mostImportantKind = kind; } infoMap.get(kind).add(entry.getSummary()); } } StringBuilder completeText = new StringBuilder(1024); appendHtmlList(NbStrings.getErrorCaption(), infoMap.get(ProjectIssue.Kind.ERROR), completeText); appendHtmlList(NbStrings.getWarningCaption(), infoMap.get(ProjectIssue.Kind.WARNING), completeText); appendHtmlList(NbStrings.getInfoCaption(), infoMap.get(ProjectIssue.Kind.INFO), completeText); icon = ImageUtilities.addToolTipToImage(icon, completeText.toString()); } return icon; } @Override public Image getOpenedIcon(int type) { return getIcon(type); } @Override public String getDisplayName() { return project.getDisplayName(); } @Override public String getShortDescription() { return project.getDisplayInfo().description().getValue(); } } @Override public Node findPath(Node root, Object target) { if (target == null) { return null; } FileObject targetFile = NodeUtils.tryGetFileSearchTarget(target); Node[] children = root.getChildren().getNodes(true); for (Node child: children) { boolean hasNodeFinder = false; for (PathFinder nodeFinder: child.getLookup().lookupAll(PathFinder.class)) { hasNodeFinder = true; Node result = nodeFinder.findPath(child, target); if (result != null) { return result; } } if (hasNodeFinder) { continue; } // This will always return {@code null} because PackageView // asks for PathFinder as well but since it is not in its // specification, we won't rely on this. Node result = PackageView.findPath(child, target); if (result == null && targetFile != null) { result = NodeUtils.findChildFileOfFolderNode(child, targetFile); } if (result != null) { return result; } } return null; } }