/*
* Copyright 2003-2016 JetBrains s.r.o.
*
* Licensed 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 jetbrains.mps.ide.projectPane;
import com.intellij.ide.CopyProvider;
import com.intellij.ide.CutProvider;
import com.intellij.ide.PasteProvider;
import com.intellij.ide.projectView.ProjectView;
import com.intellij.ide.projectView.impl.AbstractProjectViewPane;
import com.intellij.openapi.actionSystem.ActionPlaces;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.actionSystem.PlatformDataKeys;
import com.intellij.openapi.actionSystem.ToggleAction;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.openapi.vfs.VirtualFileManagerListener;
import jetbrains.mps.classloading.DeployListener;
import jetbrains.mps.ide.MPSCoreComponents;
import jetbrains.mps.ide.actions.CopyNode_Action;
import jetbrains.mps.ide.actions.CutNode_Action;
import jetbrains.mps.ide.actions.PasteNode_Action;
import jetbrains.mps.ide.project.ProjectHelper;
import jetbrains.mps.ide.projectPane.fileSystem.nodes.ProjectTreeNode;
import jetbrains.mps.ide.ui.tree.MPSTreeNode;
import jetbrains.mps.ide.ui.tree.MPSTreeNodeEx;
import jetbrains.mps.ide.ui.tree.module.GeneratorTreeNode;
import jetbrains.mps.ide.ui.tree.module.NamespaceTextNode;
import jetbrains.mps.ide.ui.tree.module.ProjectModuleTreeNode;
import jetbrains.mps.ide.ui.tree.module.TransientModelsTreeNode;
import jetbrains.mps.ide.ui.tree.smodel.PackageNode;
import jetbrains.mps.ide.ui.tree.smodel.SModelTreeNode;
import jetbrains.mps.ide.ui.tree.smodel.SNodeTreeNode;
import jetbrains.mps.ide.vfs.VirtualFileUtils;
import jetbrains.mps.make.IMakeNotificationListener;
import jetbrains.mps.make.IMakeNotificationListener.Stub;
import jetbrains.mps.make.IMakeService;
import jetbrains.mps.make.MakeNotification;
import jetbrains.mps.module.ReloadableModule;
import jetbrains.mps.project.AbstractModule;
import jetbrains.mps.project.DevKit;
import jetbrains.mps.project.ProjectOperationContext;
import jetbrains.mps.project.Solution;
import jetbrains.mps.smodel.IOperationContext;
import jetbrains.mps.smodel.Language;
import jetbrains.mps.smodel.RepoListenerRegistrar;
import jetbrains.mps.util.Pair;
import jetbrains.mps.vfs.IFile;
import jetbrains.mps.workbench.ActionPlace;
import jetbrains.mps.workbench.FileSystemModelHelper;
import jetbrains.mps.workbench.MPSDataKeys;
import jetbrains.mps.workbench.action.ActionUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SRepository;
import org.jetbrains.mps.openapi.module.SRepositoryContentAdapter;
import org.jetbrains.mps.openapi.repository.CommandListener;
import org.jetbrains.mps.openapi.util.ProgressMonitor;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
public abstract class BaseLogicalViewProjectPane extends AbstractProjectViewPane {
private final ProjectView myProjectView;
private VirtualFileManagerListener myRefreshListener = new RefreshListener();
private final MyRepositoryListener myRepositoryListener = new MyRepositoryListener();
protected boolean myDisposed;
private DeployListener myClassesListener = new DeployListener() {
@Override
public void onUnloaded(Set<ReloadableModule> unloadedModules, @NotNull ProgressMonitor monitor) {
}
@Override
public void onLoaded(Set<ReloadableModule> loadedModules, @NotNull ProgressMonitor monitor) {
rebuild();
}
};
private IMakeNotificationListener myMakeNotificationListener = new Stub() {
@Override
public void sessionClosed(MakeNotification notification) {
// rebuild tree in case of 'cancel' too (need to get 'transient models' node rebuilt)
rebuild();
}
};
protected BaseLogicalViewProjectPane(Project project, ProjectView projectView) {
super(project);
myProjectView = projectView;
}
public Project getProject() {
return myProject;
}
public ProjectView getProjectView() {
return myProjectView;
}
public abstract void rebuild();
public abstract void selectNextModel(SModel md);
@Override
public Object getData(String dataId) {
//MPSDK
if (dataId.equals(MPSDataKeys.NODE.getName())) {
return getSelectedSNode();
}
if (dataId.equals(MPSDataKeys.NODES.getName())) {
return getSelectedSNodes();
}
if (dataId.equals(MPSDataKeys.MODEL.getName())) {
return getSelectedModel();
}
if (dataId.equals(MPSDataKeys.CONTEXT_MODEL.getName())) {
return getContextModel();
}
if (dataId.equals(MPSDataKeys.MODELS.getName())) {
return getSelectedModels();
}
if (dataId.equals(MPSDataKeys.MODULE.getName())) {
return getSelectedModule();
}
if (dataId.equals(MPSDataKeys.CONTEXT_MODULE.getName())) {
return getContextModule();
}
if (dataId.equals(MPSDataKeys.MODULES.getName())) {
return getSelectedModules();
}
if (dataId.equals(MPSDataKeys.VIRTUAL_PACKAGES.getName())) {
return getSelectedPackages();
}
if (dataId.equals(MPSDataKeys.NAMESPACE.getName())) {
NamespaceTextNode selectedNamespaceNode = getSelectedTreeNode(NamespaceTextNode.class);
if (selectedNamespaceNode != null) {
return selectedNamespaceNode.getNamespace();
}
return null;
}
if (dataId.equals(MPSDataKeys.OPERATION_CONTEXT.getName())) {
return getContextForSelection();
}
if (dataId.equals(MPSDataKeys.TREE_NODE.getName())) {
return getSelectedTreeNode(TreeNode.class);
}
if (dataId.equals(MPSDataKeys.TREE_NODES.getName())) {
return getSelectedTreeNodes(TreeNode.class);
}
if (dataId.equals(MPSDataKeys.TREE_SELECTION_SIZE.getName())) {
return getSelectionSize();
}
if (dataId.equals(MPSDataKeys.PLACE.getName())) {
return getPlace();
}
//PDK
if (dataId.equals(PlatformDataKeys.COPY_PROVIDER.getName())) {
return new MyCopyProvider();
}
if (dataId.equals(PlatformDataKeys.PASTE_PROVIDER.getName())) {
return new MyPasteProvider();
}
if (dataId.equals(PlatformDataKeys.CUT_PROVIDER.getName())) {
return new MyCutProvider();
}
if (dataId.equals(PlatformDataKeys.VIRTUAL_FILE_ARRAY.getName())) {
return getSelectedFiles();
}
//not found
return null;
}
public boolean isDisposed() {
return myDisposed;
}
@Override
public void dispose() {
if (isComponentCreated()) {
removeListeners();
}
myDisposed = true;
super.dispose();
}
public boolean showNodeStructure() {
return !isDisposed() && getProjectView().isShowMembers(getId());
}
public boolean isSortByType() {
return getProjectView().isSortByType(getId());
}
@Override
public void installComparator() {
// Overrode to avoid NPE
}
@Nullable
public Comparator<Object> getTreeChildrenComparator() {
return new Comparator<Object>() {
@Override
public int compare(final Object o1, final Object o2) {
if (!(o1 instanceof SNode) || !(o2 instanceof SNode)) {
return 0;
}
final SNode node1 = (SNode) o1;
final SNode node2 = (SNode) o2;
if (!isSortByType()) {
return comparePresentations(node1, node2);
}
// (1) node.getConcept() doesn't require model read, nor concept.getQualifiedName
// (2) If we got SNode, we're are already in model read
int result = compareConceptFqNames(node1, node2);
if (result != 0) {
return result;
}
return comparePresentations(node1, node2);
}
private int compareConceptFqNames(SNode node1, SNode node2) {
String concept1 = node1.getConcept().getQualifiedName();
String concept2 = node2.getConcept().getQualifiedName();
return concept1.compareTo(concept2);
}
private int comparePresentations(SNode node1, SNode node2) {
String presentation1 = node1.getPresentation();
String presentation2 = node2.getPresentation();
if (presentation1 == null) {
return presentation2 == null ? 0 : 1;
}
if (presentation2 == null) {
return -1;
}
return presentation1.compareTo(presentation2);
}
};
}
@Override
public void addToolbarActions(final DefaultActionGroup group) {
group.addAction(new SortByTypeToggleAction()).setAsSecondary(true);
}
protected void removeListeners() {
ApplicationManager.getApplication().getComponent(MPSCoreComponents.class).getClassLoaderManager().removeListener(myClassesListener);
jetbrains.mps.project.Project mpsProject = ProjectHelper.fromIdeaProject(getProject());
mpsProject.getModelAccess().removeCommandListener(myRepositoryListener);
new RepoListenerRegistrar(mpsProject.getRepository(), myRepositoryListener).detach();
if (IMakeService.INSTANCE.hasMakeService()) {
IMakeService.INSTANCE.get().removeListener(myMakeNotificationListener);
}
VirtualFileManager.getInstance().removeVirtualFileManagerListener(myRefreshListener);
}
protected void addListeners() {
VirtualFileManager.getInstance().addVirtualFileManagerListener(myRefreshListener);
jetbrains.mps.project.Project mpsProject = ProjectHelper.fromIdeaProject(getProject());
new RepoListenerRegistrar(mpsProject.getRepository(), myRepositoryListener).attach();
mpsProject.getModelAccess().addCommandListener(myRepositoryListener);
if (IMakeService.INSTANCE.hasMakeService()) {
IMakeService.INSTANCE.get().addListener(myMakeNotificationListener);
}
ApplicationManager.getApplication().getComponent(MPSCoreComponents.class).getClassLoaderManager().addListener(myClassesListener);
}
public SNode getSelectedSNode() {
List<SNode> result = getSelectedSNodes();
if (result.size() != 1) {
return null;
}
return result.get(0);
}
public List<SNode> getSelectedSNodes() {
List<SNode> result = new ArrayList<>();
for (MPSTreeNodeEx node : getSelectedTreeNodes(MPSTreeNodeEx.class)) {
SNode snode = node.getSNode();
if (snode == null) {
continue;
}
result.add(snode);
}
return result;
}
public List<SModel> getSelectedModels() {
List<SModel> result = new ArrayList<>();
for (SModelTreeNode node : getSelectedTreeNodes(SModelTreeNode.class)) {
result.add(node.getModel());
}
return result;
}
public SModel getSelectedModel() {
SModelTreeNode selectedTreeNode = getSelectedTreeNode(SModelTreeNode.class);
if (selectedTreeNode == null) {
return null;
}
return selectedTreeNode.getModel();
}
public SModel getContextModel() {
MPSTreeNode treeNode = (MPSTreeNode) getSelectedTreeNode(TreeNode.class);
while (treeNode != null && !(treeNode instanceof SModelTreeNode)) {
treeNode = (MPSTreeNode) treeNode.getParent();
}
if (treeNode == null) {
return null;
}
return ((SModelTreeNode) treeNode).getModel();
}
public SModule getSelectedModule() {
ProjectModuleTreeNode selectedTreeNode = getSelectedTreeNode(ProjectModuleTreeNode.class);
if (selectedTreeNode == null) {
return null;
}
return selectedTreeNode.getModule();
}
public SModule getContextModule() {
MPSTreeNode treeNode = (MPSTreeNode) getSelectedTreeNode(TreeNode.class);
while (treeNode != null && !(treeNode instanceof ProjectModuleTreeNode)) {
treeNode = (MPSTreeNode) treeNode.getParent();
}
if (treeNode == null) {
return null;
}
return ((ProjectModuleTreeNode) treeNode).getModule();
}
public List<SModule> getSelectedModules() {
List<SModule> result = new ArrayList<>();
for (ProjectModuleTreeNode node : getSelectedTreeNodes(ProjectModuleTreeNode.class)) {
result.add(node.getModule());
}
return result;
}
public List<Pair<SModel, String>> getSelectedPackages() {
List<Pair<SModel, String>> result = new ArrayList<>();
TreePath[] paths = getTree().getSelectionPaths();
SRepository projectRepo = ProjectHelper.getProjectRepository(getProject());
if (paths == null || projectRepo == null) {
return result;
}
for (TreePath path : paths) {
MPSTreeNode node = (MPSTreeNode) path.getLastPathComponent();
if (node instanceof PackageNode) {
PackageNode pn = (PackageNode) node;
result.add(new Pair<>(pn.getModelReference().resolve(projectRepo), pn.getFullPackage()));
}
}
return result;
}
public int getSelectionSize() {
TreePath[] selection = getSelectionPaths();
return selection == null ? 0 : selection.length;
}
private IOperationContext getContextForSelection() {
TreePath[] selection = getTree().getSelectionPaths();
if (selection == null) {
return null;
}
if (selection.length > 0) {
return new ProjectOperationContext(ProjectHelper.fromIdeaProject(myProject));
}
return null;
}
private <T extends TreeNode> T getSelectedTreeNode(Class<T> nodeClass) {
TreePath selectionPath = getTree().getSelectionPath();
if (selectionPath == null) {
return null;
}
Object selectedNode = selectionPath.getLastPathComponent();
if (!(nodeClass.isInstance(selectedNode))) {
return null;
}
return (T) selectedNode;
}
private ActionPlace getPlace() {
TreeNode treeNode = getSelectedTreeNode(TreeNode.class);
if (treeNode instanceof SNodeTreeNode) {
return ActionPlace.PROJECT_PANE_SNODE;
} else if (treeNode instanceof SModelTreeNode) {
return ActionPlace.PROJECT_PANE_SMODEL;
} else if ((treeNode instanceof ProjectTreeNode) || (treeNode instanceof jetbrains.mps.ide.ui.tree.module.ProjectTreeNode)) {
return ActionPlace.PROJECT_PANE_PROJECT;
} else if (treeNode instanceof GeneratorTreeNode) {
return ActionPlace.PROJECT_PANE_GENERATOR;
} else if (treeNode instanceof TransientModelsTreeNode) {
return ActionPlace.PROJECT_PANE_TRANSIENT_MODULES;
} else if (treeNode instanceof PackageNode) {
return ActionPlace.PROJECT_PANE_PACKAGE;
} else if (treeNode instanceof NamespaceTextNode) {
return ActionPlace.PROJECT_PANE_NAMESPACE;
} else if (treeNode instanceof ProjectModuleTreeNode) {
SModule module = getSelectedModule();
if (module instanceof Language) {
return ActionPlace.PROJECT_PANE_LANGUAGE;
} else if (module instanceof DevKit) {
return ActionPlace.PROJECT_PANE_DEVKIT;
} else if (module instanceof Solution) {
return ActionPlace.PROJECT_PANE_SOLUTION;
}
}
return ActionPlace.PROJECT_PANE;
}
public <T extends TreeNode> List<T> getSelectedTreeNodes(Class<T> nodeClass) {
TreePath[] selectionPaths = getTree().getSelectionPaths();
if (selectionPaths == null) {
return new ArrayList<>();
}
List<T> selectedTreeNodes = new ArrayList<>(selectionPaths.length);
for (TreePath selectionPath : selectionPaths) {
if (selectionPath == null) {
continue;
}
Object selectedNode = selectionPath.getLastPathComponent();
if (!(nodeClass.isInstance(selectedNode))) {
continue;
}
selectedTreeNodes.add((T) selectedNode);
}
return selectedTreeNodes;
}
private VirtualFile[] getSelectedFiles() {
List<VirtualFile> selectedFilesList = new LinkedList<>();
// add selected model files
List<SModel> descriptors = getSelectedModels();
if (descriptors != null) {
for (SModel descriptor : descriptors) {
selectedFilesList.addAll(new FileSystemModelHelper(descriptor).getVirtualFiles());
}
}
// add selected modules files
List<SModule> modules = getSelectedModules();
if (modules != null) {
for (SModule m : modules) {
if (!(m instanceof AbstractModule)) {
continue;
}
AbstractModule module = (AbstractModule) m;
IFile home = module.getModuleSourceDir();
if (home != null) {
VirtualFile vfile = VirtualFileUtils.getProjectVirtualFile(home);
if (vfile != null) {
selectedFilesList.add(vfile);
}
}
IFile ifile = module.getDescriptorFile();
if (ifile != null) {
VirtualFile vfile = VirtualFileUtils.getProjectVirtualFile(ifile);
if (vfile != null) {
selectedFilesList.add(vfile);
}
}
}
}
if (selectedFilesList.isEmpty()) {
return null;
}
return selectedFilesList.toArray(new VirtualFile[selectedFilesList.size()]);
}
/*package*/
static AnActionEvent createEvent(DataContext context) {
return ActionUtils.createEvent(ActionPlaces.PROJECT_VIEW_POPUP, context);
}
protected abstract boolean isComponentCreated();
private static class MyCopyProvider implements CopyProvider {
private CopyNode_Action myAction = new CopyNode_Action();
@Override
public void performCopy(@NotNull DataContext dataContext) {
AnActionEvent event = createEvent(dataContext);
ActionUtils.updateAndPerformAction(myAction, event);
}
@Override
public boolean isCopyEnabled(@NotNull DataContext dataContext) {
AnActionEvent event = createEvent(dataContext);
myAction.update(event);
return event.getPresentation().isEnabled();
}
@Override
public boolean isCopyVisible(@NotNull DataContext dataContext) {
return true;
}
}
private static class MyPasteProvider implements PasteProvider {
private PasteNode_Action myAction = new PasteNode_Action();
@Override
public void performPaste(@NotNull DataContext dataContext) {
AnActionEvent event = createEvent(dataContext);
ActionUtils.updateAndPerformAction(myAction, event);
}
@Override
public boolean isPastePossible(@NotNull DataContext dataContext) {
return true;
}
@Override
public boolean isPasteEnabled(@NotNull DataContext dataContext) {
AnActionEvent event = createEvent(dataContext);
myAction.update(event);
return event.getPresentation().isEnabled();
}
}
private static class MyCutProvider implements CutProvider {
private CutNode_Action myAction = new CutNode_Action();
@Override
public void performCut(@NotNull DataContext dataContext) {
AnActionEvent event = createEvent(dataContext);
ActionUtils.updateAndPerformAction(myAction, event);
}
@Override
public boolean isCutEnabled(@NotNull DataContext dataContext) {
AnActionEvent event = createEvent(dataContext);
myAction.update(event);
return event.getPresentation().isEnabled();
}
@Override
public boolean isCutVisible(@NotNull DataContext dataContext) {
return true;
}
}
private class MyRepositoryListener extends SRepositoryContentAdapter implements CommandListener {
private boolean myNeedRebuild = false;
/*package*/ void rebuildTreeIfNeeded() {
if (myNeedRebuild) {
rebuild();
myNeedRebuild = false;
}
}
@Override
public void repositoryChanged() {
myNeedRebuild = true;
}
@Override
public void modelRenamed(SModule module, SModel model, SModelReference oldRef) {
myNeedRebuild = true;
}
@Override
protected void startListening(SModel model) {
if (!model.isReadOnly()) {
model.addModelListener(this);
}
}
@Override
protected void stopListening(SModel model) {
model.removeModelListener(this);
}
@Override
public void modelReplaced(SModel model) {
myNeedRebuild = true;
}
@Override
public void commandStarted() {
myNeedRebuild = false;
}
@Override
public void commandFinished() {
rebuildTreeIfNeeded();
}
}
private class RefreshListener implements VirtualFileManagerListener {
@Override
public void beforeRefreshStart(boolean asynchronous) {
}
@Override
public void afterRefreshFinish(boolean asynchronous) {
myRepositoryListener.rebuildTreeIfNeeded();
}
}
private class SortByTypeToggleAction extends ToggleAction {
public SortByTypeToggleAction() {
super("Sort Roots by Concept", "Sort root nodes by concept", null);
}
@Override
public boolean isSelected(@Nullable AnActionEvent e) {
return isSortByType();
}
@Override
public void setSelected(@Nullable AnActionEvent e, boolean state) {
if (state != isSortByType()) {
getProjectView().setSortByType(getId(), state);
rebuild();
}
}
}
}