/*
* 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.SelectInTarget;
import com.intellij.ide.projectView.ProjectView;
import com.intellij.ide.projectView.impl.ProjectViewPane;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DataProvider;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.actionSystem.ToggleAction;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
import com.intellij.openapi.components.StoragePathMacros;
import com.intellij.openapi.fileEditor.FileEditor;
import com.intellij.openapi.fileEditor.FileEditorManagerEvent;
import com.intellij.openapi.fileEditor.FileEditorManagerListener;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.ActionCallback;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.InvalidDataException;
import com.intellij.openapi.util.WriteExternalException;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowId;
import com.intellij.openapi.wm.ToolWindowManager;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.util.messages.MessageBusConnection;
import com.intellij.util.ui.update.MergingUpdateQueue;
import com.intellij.util.ui.update.Update;
import jetbrains.mps.RuntimeFlags;
import jetbrains.mps.icons.MPSIcons;
import jetbrains.mps.ide.ThreadUtils;
import jetbrains.mps.ide.editor.MPSFileNodeEditor;
import jetbrains.mps.ide.platform.watching.ReloadListener;
import jetbrains.mps.ide.platform.watching.ReloadManager;
import jetbrains.mps.ide.projectPane.logicalview.ProjectPaneTree;
import jetbrains.mps.ide.projectPane.logicalview.ProjectTree;
import jetbrains.mps.ide.projectPane.logicalview.ProjectTreeFindHelper;
import jetbrains.mps.ide.projectView.ProjectViewPaneOverride;
import jetbrains.mps.ide.ui.tree.MPSTree;
import jetbrains.mps.ide.ui.tree.MPSTreeNode;
import jetbrains.mps.ide.ui.tree.TreeHighlighterExtension;
import jetbrains.mps.openapi.editor.EditorComponent;
import jetbrains.mps.project.MPSProject;
import jetbrains.mps.smodel.ModelReadRunnable;
import jetbrains.mps.util.annotation.Hack;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jdom.Element;
import org.jetbrains.annotations.NonNls;
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.model.SNodeReference;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SModuleReference;
import org.jetbrains.mps.openapi.module.SRepositoryListenerBase;
import javax.swing.Icon;
import javax.swing.JComponent;
import java.awt.Component;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@State(
name = "MPSProjectPane",
storages = @Storage(StoragePathMacros.WORKSPACE_FILE)
)
public class ProjectPane extends BaseLogicalViewProjectPane implements ProjectViewPaneOverride {
private static final Logger LOG = LogManager.getLogger(ProjectPane.class);
private final SRepositoryListenerBase myRepositoryListener = new SRepositoryListenerBase() {
@Override
public void moduleAdded(@NotNull SModule module) {
ProjectPane.this.updateFromRoot(true);
}
@Override
public void moduleRemoved(@NotNull SModuleReference module) {
ProjectPane.this.updateFromRoot(true);
}
};
private final ReloadListener myReloadListener;
private MyScrollPane myScrollPane;
// FIXME there's update queue in MPSTree, do really we need both?
private final MergingUpdateQueue myUpdateQueue = new MergingUpdateQueue("Project Pane Updates Queue", 500, true, myScrollPane, null, null, true);
public static final String ID = ProjectViewPane.ID;
private final FileEditorManagerListener myEditorListener = new FileEditorManagerListener() {
@Override
public void selectionChanged(@NotNull FileEditorManagerEvent event) {
FileEditor fileEditor = event.getNewEditor();
if (fileEditor instanceof MPSFileNodeEditor) {
final MPSFileNodeEditor editor = (MPSFileNodeEditor) fileEditor;
if (getProjectView().isAutoscrollFromSource(ID)) {
EditorComponent editorComponent = editor.getNodeEditor().getCurrentEditorComponent();
if (editorComponent == null) {
return;
}
final SNode sNode = editorComponent.getEditedNode();
selectNodeWithoutExpansion(sNode.getReference());
}
}
}
};
private List<List<String>> myExpandedPathsRaw = Collections.emptyList();
private List<List<String>> mySelectedPathsRaw = Collections.emptyList();
private MessageBusConnection myConnection;
private final ShowDescriptorModelsAction myShowDescriptorModelsAction;
public ProjectPane(final Project project, ProjectView projectView) {
super(project, projectView);
myUpdateQueue.setRestartTimerOnAdd(true);
myReloadListener = new ReloadListener() {
@Override
public void reloadStarted() {
}
@Override
public void reloadFinished() {
rebuild();
}
};
ApplicationManager.getApplication().getComponent(ReloadManager.class).addReloadListener(myReloadListener);
myShowDescriptorModelsAction = new ShowDescriptorModelsAction(this);
}
@Override
public void dispose() {
myUpdateQueue.dispose();
ApplicationManager.getApplication().getComponent(ReloadManager.class).removeReloadListener(myReloadListener);
super.dispose();
}
@Override
protected void removeListeners() {
super.removeListeners();
myConnection.disconnect();
myConnection = null;
getMPSProject().getRepository().removeRepositoryListener(myRepositoryListener);
}
@Override
protected void addListeners() {
super.addListeners();
assert myConnection == null; // double initialization
myConnection = getProject().getMessageBus().connect();
myConnection.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, myEditorListener);
getMPSProject().getRepository().addRepositoryListener(myRepositoryListener);
}
@Hack
public static ProjectPane getInstance(Project project) {
final ProjectView projectView = ProjectView.getInstance(project);
if (ApplicationManager.getApplication().isUnitTestMode()) {
//to ensure panes are initialized
// despite http://jetbrains.net/tracker/issue/IDEA-24732 is fixed, ProjectViewImpl doesn't load extensions in unit test model
// Perhaps, shall fix ProjectCreationTest (not to rely on != null result), instead?
projectView.getSelectInTargets();
}
return (ProjectPane) projectView.getProjectViewPaneById(ID);
}
// FIXME perhaps, shall be explicit about parameter type, seems that it's always invoked with MPSProject anyway
// and there's hardly need to access ProjectPane without knowledge about IDE.
public static ProjectPane getInstance(jetbrains.mps.project.Project mpsProject) {
if (mpsProject instanceof MPSProject) {
return getInstance(((MPSProject) mpsProject).getProject());
}
return null;
}
@Override
public ProjectTree getTree() {
return (jetbrains.mps.ide.projectPane.logicalview.ProjectTree) myTree;
}
/*package*/ MPSProject getMPSProject() {
// Shall I use getTree().getProject() instead?
return getProject().getComponent(MPSProject.class);
}
@Override
public String getTitle() {
return "Logical View";
}
@Override
@NotNull
public String getId() {
return ID;
}
@Override
public int getWeight() {
return 0;
}
@Override
public SelectInTarget createSelectInTarget() {
return new ProjectPaneSelectInTarget(getMPSProject(), true);
}
@Override
public Icon getIcon() {
return MPSIcons.ProjectPane.LogicalView;
}
@NotNull
@Override
public ActionCallback updateFromRoot(boolean restoreExpandedPaths) {
// XXX why not MPSTree.rebuildLater?
// FIXME what's the difference with #rebuildTree?
myUpdateQueue.queue(new AbstractUpdate(UpdateID.REBUILD) {
@Override
public void run() {
if (getTree() == null) {
return;
}
getTree().rebuildNow();
}
});
return new ActionCallback(); // todo
}
@Override
public void select(Object element, final VirtualFile file, final boolean requestFocus) {
}
@Override
public JComponent createComponent() {
if (isComponentCreated()) {
return myScrollPane;
}
ProjectPaneTree tree = new ProjectPaneTree(this, myProject);
Disposer.register(this, tree);
tree.setShowStructureCondition(this::showNodeStructure);
myTree = tree;
myScrollPane = new MyScrollPane(getTree());
addListeners();
if (!RuntimeFlags.isTestMode()) {
rebuild();
}
TreeHighlighterExtension.attachHighlighters(tree, myProject);
return myScrollPane;
}
@Override
protected boolean isComponentCreated() {
return myScrollPane != null;
}
public void rebuildTree() {
// @see #updateFromRoot
myUpdateQueue.queue(new AbstractUpdate(UpdateID.REBUILD) {
@Override
public void run() {
if (getTree() == null || getProject().isDisposed()) {
return;
}
getTree().rebuildNow();
getTree().expandProjectNode();
}
});
}
public void activate() {
ThreadUtils.assertEDT();
activatePane(null, true);
}
@Override
public void rebuild() {
// This method can be called from different threads, however rebuildTree()
// merely adds an update to the update queue, and thus it's safe to invoke it
// without runReadInEDT or runInUIThreadNoWait as it used to be.
rebuildTree();
}
@Override
public void addToolbarActions(DefaultActionGroup group) {
super.addToolbarActions(group);
group.addAction(myShowDescriptorModelsAction).setAsSecondary(true);
}
@Override
protected void saveExpandedPaths() {
// this gets called from the IDEA's implementation of ProjectViewImpl
// thankfully, the method is declared protected
if (myTree != null) {
myExpandedPathsRaw = ((MPSTree) myTree).getExpandedPathsRaw();
mySelectedPathsRaw = ((MPSTree) myTree).getSelectedPathsRaw();
} else {
myExpandedPathsRaw = Collections.emptyList();
mySelectedPathsRaw = Collections.emptyList();
}
}
@Override
public void restoreExpandedPathsOverride() {
// this gets called from the MPS's implementation of ProjectViewImpl
// we must resort to this hack because the method in the superclass is declared private
if (myTree != null) {
myUpdateQueue.queue(new AbstractUpdate(UpdateID.RESTORE_EXPAND) {
@Override
public void run() {
((MPSTree) myTree).loadState(myExpandedPathsRaw, mySelectedPathsRaw);
}
});
}
}
@Override
public void writeExternal(Element element) throws WriteExternalException {
saveExpandedPaths();
// keep the binary format in sync with what IDEA writes
Element subPane = new Element("subPane");
// we probably don't need this...
if (getSubId() != null) {
subPane.setAttribute("subId", getSubId());
}
writePaths(subPane, myExpandedPathsRaw, "PATH");
writePaths(subPane, mySelectedPathsRaw, "SELECTED");
if (!myShowDescriptorModelsAction.isDefaultState()) {
Element option1 = new Element(ShowDescriptorModelsAction.KEY);
option1.setAttribute("value", Boolean.toString(myShowDescriptorModelsAction.isSelected()));
subPane.addContent(option1);
}
element.addContent(subPane);
}
private void writePaths(Element parentElement, List<List<String>> pathsRaw, String elementName) {
for (List<String> path : pathsRaw) {
Element pathElement = new Element(elementName);
writePath(path, pathElement);
parentElement.addContent(pathElement);
}
}
private void writePath(List<String> path, Element pathElement) {
for (String treeNodeId : path) {
Element elm = new Element("PATH_ELEMENT");
writeNodeId(treeNodeId, elm);
pathElement.addContent(elm);
}
}
private void writeNodeId(String treeNodeId, Element elm) {
Element option1 = new Element("option");
option1.setAttribute("name", "myItemId");
option1.setAttribute("value", treeNodeId);
elm.addContent(option1);
Element option2 = new Element("option");
option2.setAttribute("name", "myItemType");
option2.setAttribute("value", "");
elm.addContent(option2);
}
@Override
public void readExternal(Element element) throws InvalidDataException {
// emulate the superclass's readExternal using the same binary format
List<Element> subPanes = element.getChildren("subPane");
for (Element subPane : subPanes) {
myExpandedPathsRaw = readPaths(subPane, "PATH");
mySelectedPathsRaw = readPaths(subPane, "SELECTED");
Element option1 = subPane.getChild(ShowDescriptorModelsAction.KEY);
if (option1 != null) {
myShowDescriptorModelsAction.setState(Boolean.parseBoolean(option1.getAttributeValue("value")));
}
}
}
private List<List<String>> readPaths(Element parentElement, String name) {
List<List<String>> result = new ArrayList<>();
for (Element pathElement : parentElement.getChildren(name)) {
List<String> path = readPath(pathElement);
result.add(path);
}
return result;
}
@NotNull
private List<String> readPath(Element pathElement) {
List<String> path = new ArrayList<>();
for (Element elm : pathElement.getChildren("PATH_ELEMENT")) {
String treeNodeId = readNodeId(elm);
if (treeNodeId != null) {
path.add(treeNodeId);
}
}
return path;
}
@Nullable
private String readNodeId(Element elm) {
List<Element> options = elm.getChildren("option");
String treeNodeId = null;
for (Element option : options) {
if ("myItemId".equals(option.getAttributeValue("name"))) {
treeNodeId = option.getAttributeValue("value");
break;
}
}
return treeNodeId;
}
//----selection----
public void selectModule(@NotNull final SModule module, final boolean autofocus) {
ThreadUtils.assertEDT();
Runnable lookupAndSelect = new LookupAndSelect(module.getModuleReference());
activatePane(new ScheduleUpdateRunnable(myUpdateQueue, createModelReadUpdate(UpdateID.SELECT, lookupAndSelect)), autofocus);
}
public void selectModel(@NotNull final SModel model, boolean autofocus) {
ThreadUtils.assertEDT();
Runnable lookupAndSelect = new LookupAndSelect(model.getReference());
activatePane(new ScheduleUpdateRunnable(myUpdateQueue, createModelReadUpdate(UpdateID.SELECT, lookupAndSelect)), autofocus);
}
private void activatePane(@Nullable final Runnable postActivate, boolean autoFocusContents) {
// This method may be executed asynchronously, so checking for isDisposed first.
if (isDisposed()) {
return;
}
ToolWindowManager windowManager = ToolWindowManager.getInstance(getProject());
ToolWindow projectViewToolWindow = windowManager.getToolWindow(ToolWindowId.PROJECT_VIEW);
//In unit test mode projectViewToolWindow == null
// besides, https://youtrack.jetbrains.com/issue/MPS-24516 suggests tool window may be missing even in non-test mode (in plugin?)
if (!ApplicationManager.getApplication().isUnitTestMode() && projectViewToolWindow != null) {
projectViewToolWindow.activate(() -> {
// I'm not quite sure next changeView is essential (what does toolWindow.activate() does then?),
// but since there's no documentation what to expect, leave it the way it used to be in PaneActivator.
getProjectView().changeView(getId());
if (postActivate != null) {
postActivate.run();
}
}, autoFocusContents);
}
}
public void selectNode(@NotNull final SNode node, boolean autofocus) {
ThreadUtils.assertEDT();
final Runnable lookupAndSelect = new LookupAndSelect(node.getReference());
activatePane(new ScheduleUpdateRunnable(myUpdateQueue, createModelReadUpdate(UpdateID.SELECT, lookupAndSelect)), autofocus);
}
private void selectNodeWithoutExpansion(@NotNull SNodeReference nodeRef) {
final Runnable lookupAndSelect = new LookupAndSelect(nodeRef);
myUpdateQueue.queue(createModelReadUpdate(UpdateID.SELECT, () -> getTree().runWithoutExpansion(lookupAndSelect)));
}
/**
* @return update code block with the given id, that runs supplied delegate with read access to project repository
*/
private Update createModelReadUpdate(@NotNull UpdateID id, @NotNull Runnable delegate) {
return new UpdateAdapter(id, new ModelReadRunnable(getMPSProject().getModelAccess(), delegate));
}
//----select next queries----
@Override
public void selectNextModel(SModel modelDescriptor) {
final MPSTreeNode mpsTreeNode = createFindHelper().findNextTreeNode(modelDescriptor);
// FIXME selectNextNode does the same, refactor. Check callers if need ThreadUtils at all
ThreadUtils.runInUIThreadNoWait(() -> {
ProjectTree tree = getTree();
if (tree != null) {
tree.selectNode(mpsTreeNode);
}
});
}
public void selectNextNode(SNode node) {
final MPSTreeNode mpsTreeNode = createFindHelper().findNextTreeNode(node);
ThreadUtils.runInUIThreadNoWait(() -> getTree().selectNode(mpsTreeNode));
}
//----tree node selection queries---
public MPSTreeNode findNextTreeNode(SNode node) {
return createFindHelper().findNextTreeNode(node);
}
public boolean isDescriptorModelInGeneratorVisible() {
return myShowDescriptorModelsAction.isSelected();
}
@NotNull
/*package*/ ProjectTreeFindHelper createFindHelper() {
return new ProjectTreeFindHelper(getTree());
}
//----UI----
private class MyScrollPane extends JBScrollPane implements DataProvider {
private MyScrollPane(Component view) {
super(view);
}
@Override
@Nullable
public Object getData(@NonNls String dataId) {
return ProjectPane.this.getData(dataId);
}
}
private enum UpdateID {
REBUILD(20),
SELECT(30),
RESTORE_EXPAND(40);
private int myPriority;
UpdateID(int priority) {
myPriority = priority;
}
public int getPriority() {
return myPriority;
}
}
private abstract static class AbstractUpdate extends Update {
/*package*/ AbstractUpdate(UpdateID id) {
super(id, id.getPriority());
}
}
private static class UpdateAdapter extends Update {
private final Runnable myDelegate;
/*package*/ UpdateAdapter(@NotNull UpdateID id, @NotNull Runnable delegate) {
super(id, id.getPriority());
myDelegate = delegate;
}
@Override
public void run() {
myDelegate.run();
}
}
// handy runnable that places an update into given queue
private static class ScheduleUpdateRunnable implements Runnable {
private final MergingUpdateQueue myQueue;
private final Update myUpdate;
/*package*/ ScheduleUpdateRunnable(@NotNull MergingUpdateQueue queue, @NotNull Update update) {
myQueue = queue;
myUpdate = update;
}
@Override
public void run() {
myQueue.queue(myUpdate);
}
}
// resolve a reference, look up a corresponding tree node, and select it if found
// XXX split to Computable<TreeNode> and runnable that takes it?
private class LookupAndSelect implements Runnable {
private SNodeReference myNode;
private SModelReference myModel;
private SModuleReference myModule;
public LookupAndSelect(SModuleReference module) {
myModule = module;
}
public LookupAndSelect(SModelReference model) {
myModel = model;
}
public LookupAndSelect(SNodeReference node) {
myNode = node;
}
@Override
public void run() {
MPSTreeNode toSelect = null;
if (myModule != null) {
SModule module = myModule.resolve(getMPSProject().getRepository());
if (module == null) {
// likely, by the time we got to this point (selection update), the reference is no longer valid, exit gracefully
return;
}
toSelect = createFindHelper().findMostSuitableModuleTreeNode(module);
if (toSelect == null) {
LOG.warn("Couldn't select module \"" + myModule.getModuleName() + "\" : tree node not found.");
return;
}
} else if (myModel != null) {
SModel model = myModel.resolve(getMPSProject().getRepository());
if (model == null) {
return;
}
toSelect = createFindHelper().findMostSuitableModelTreeNode(model);
if (toSelect == null) {
LOG.warn("Couldn't select model \"" + myModel.getModelName() + "\" : tree node not found.");
return;
}
} else if (myNode != null) {
SNode node = myNode.resolve(getMPSProject().getRepository());
if (node == null) {
return;
}
toSelect = createFindHelper().findMostSuitableSNodeTreeNode(node);
if (toSelect == null) {
LOG.warn("Couldn't select node \"" + myNode.toString() + "\" : tree node not found.");
return;
}
}
if (toSelect != null) {
getTree().selectNode(toSelect);
}
}
}
private static class ShowDescriptorModelsAction extends ToggleAction {
private final ProjectPane myProjectPane;
private boolean myState = false;
/*package*/ static final String KEY = "showGeneratorDescriptor";
ShowDescriptorModelsAction(ProjectPane projectPane) {
super("Show @descriptor models in Generators");
myProjectPane = projectPane;
}
public boolean isSelected() {
return myState;
}
/*package*/ boolean isDefaultState() {
return !isSelected();
}
/*package*/ void setState(boolean selected) {
myState = selected;
}
@Override
public boolean isSelected(AnActionEvent e) {
return isSelected();
}
@Override
public void setSelected(AnActionEvent e, boolean state) {
myState = state;
myProjectPane.rebuild();
}
}
}