/*
* 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.fileSystem;
import com.intellij.icons.AllIcons.Actions;
import com.intellij.ide.SelectInContext;
import com.intellij.ide.SelectInTarget;
import com.intellij.ide.projectView.ProjectView;
import com.intellij.ide.projectView.impl.AbstractProjectViewPane;
import com.intellij.openapi.actionSystem.ActionGroup;
import com.intellij.openapi.actionSystem.ActionPlaces;
import com.intellij.openapi.actionSystem.DataProvider;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.actionSystem.PlatformDataKeys;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.FileEditorManagerAdapter;
import com.intellij.openapi.fileEditor.FileEditorManagerEvent;
import com.intellij.openapi.fileEditor.FileEditorManagerListener;
import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.ActionCallback;
import com.intellij.openapi.vcs.FileStatusListener;
import com.intellij.openapi.vcs.FileStatusManager;
import com.intellij.openapi.vcs.ProjectLevelVcsManager;
import com.intellij.openapi.vcs.VcsListener;
import com.intellij.openapi.vcs.changes.ChangeListAdapter;
import com.intellij.openapi.vcs.changes.ChangeListListener;
import com.intellij.openapi.vcs.changes.ChangeListManager;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileAdapter;
import com.intellij.openapi.vfs.VirtualFileCopyEvent;
import com.intellij.openapi.vfs.VirtualFileEvent;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.openapi.vfs.VirtualFileManagerListener;
import com.intellij.openapi.vfs.VirtualFileMoveEvent;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowId;
import com.intellij.openapi.wm.ToolWindowManager;
import com.intellij.ui.ScrollPaneFactory;
import com.intellij.util.messages.MessageBus;
import com.intellij.util.messages.MessageBusConnection;
import jetbrains.mps.extapi.persistence.FileSystemBasedDataSource;
import jetbrains.mps.ide.projectPane.AbstractProjectViewSelectInTarget;
import jetbrains.mps.ide.projectPane.ProjectPaneActionGroups;
import jetbrains.mps.ide.projectPane.fileSystem.actions.providers.FilePaneCopyProvider;
import jetbrains.mps.ide.projectPane.fileSystem.actions.providers.FilePanePasteProvider;
import jetbrains.mps.ide.projectPane.fileSystem.nodes.AbstractFileTreeNode;
import jetbrains.mps.ide.projectPane.fileSystem.nodes.FileTreeNode;
import jetbrains.mps.ide.projectPane.fileSystem.nodes.FolderTreeNode;
import jetbrains.mps.ide.projectPane.fileSystem.nodes.ModuleTreeNode;
import jetbrains.mps.ide.projectPane.fileSystem.nodes.ProjectTreeNode;
import jetbrains.mps.ide.ui.tree.MPSTree;
import jetbrains.mps.ide.ui.tree.MPSTreeNode;
import jetbrains.mps.ide.ui.tree.TextTreeNode;
import jetbrains.mps.ide.vfs.VirtualFileUtils;
import jetbrains.mps.smodel.ModelAccess;
import jetbrains.mps.util.Computable;
import jetbrains.mps.vfs.IFile;
import jetbrains.mps.workbench.ActionPlace;
import jetbrains.mps.workbench.MPSDataKeys;
import jetbrains.mps.nodefs.MPSNodeVirtualFile;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
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.persistence.DataSource;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.JScrollPane;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.LinkedList;
import java.util.List;
public class FileViewProjectPane extends AbstractProjectViewPane implements DataProvider {
private static final Logger LOG = LogManager.getLogger(FileViewProjectPane.class);
@NonNls
public static final String ID = "FileSystem";
public static final String TITLE = "File System";
private final Project myProject;
private final MessageBus myBus;
private final IdeDocumentHistory myIdeDocumentHistory;
private final ProjectView myProjectView;
private final FileEditorManager myEditorManager;
private ChangeListListener myChangeListListener;
private MessageBusConnection myMessageBusConnection;
private FileStatusListener myFileStatusListener;
private VirtualFileAdapter myFileListener;
private VirtualFileManagerListener myVirtualFileManagerListener;
private JScrollPane myScrollPane;
@Override
public void addToolbarActions(DefaultActionGroup actionGroup) {
super.addToolbarActions(actionGroup);
}
protected FileViewProjectPane(final Project project, final ProjectView projectView, final MessageBus bus, @NotNull IdeDocumentHistory ideDocumentHistory,
@NotNull FileEditorManager fileEditorManager) {
super(project);
myProject = project;
myProjectView = projectView;
myBus = bus;
myIdeDocumentHistory = ideDocumentHistory;
myEditorManager = fileEditorManager;
}
private MPSTreeNode createRoot(Project project) {
VirtualFile folder = project.getBaseDir();
assert folder != null;
return new ProjectTreeNode(project);
}
@Override
public void dispose() {
if (isInitialized()) {
disposeListeners();
}
myScrollPane = null;
}
@Override
public MPSTree getTree() {
return (MPSTree) myTree;
}
public Project getProject() {
return myProject;
}
public void rebuildTreeLater() {
getTree().rebuildLater();
}
@Override
public Icon getIcon() {
return Actions.ShowAsTree;
}
@Override
public JComponent createComponent() {
if (isInitialized()) {
return myScrollPane;
}
installListeners();
myTree = new MPSTree() {
@Override
protected ActionGroup createPopupActionGroup(final MPSTreeNode node) {
return ProjectPaneActionGroups.getActionGroup(node);
}
@Override
protected String getPopupMenuPlace() {
return ActionPlaces.FILEVIEW_POPUP;
}
@Override
protected MPSTreeNode rebuild() {
MPSTreeNode node;
if (myProject != null && !myProject.isDisposed() && (myProject.getBaseDir() != null)) {
node = createRoot(myProject);
} else {
node = new TextTreeNode("No Project");
}
return node;
}
};
myTree.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ENTER && e.getModifiers() == 0) {
openEditor();
e.consume();
}
}
});
myTree.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() >= 2) {
openEditor();
e.consume();
}
}
});
rebuildTreeLater();
myScrollPane = ScrollPaneFactory.createScrollPane(myTree);
return myScrollPane;
}
private void installListeners() {
FileStatusManager.getInstance(myProject).addFileStatusListener(myFileStatusListener = new FileStatusChangeListener());
VirtualFileManager.getInstance().addVirtualFileListener(myFileListener = new FileChangesListener());
VirtualFileManager.getInstance().addVirtualFileManagerListener(myVirtualFileManagerListener = new RefreshListener());
ChangeListManager.getInstance(myProject).addChangeListListener(myChangeListListener = new ChangeListUpdateListener());
myMessageBusConnection = myBus.connect(this);
myMessageBusConnection.subscribe(ProjectLevelVcsManager.VCS_CONFIGURATION_CHANGED, new VcsListener() {
@Override
public void directoryMappingChanged() {
rebuildTreeLater();
}
});
myMessageBusConnection.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, new FileEditorManagerAdapter() {
@Override
public void fileOpened(@NotNull FileEditorManager source, @NotNull VirtualFile file) {
if (myProjectView.isAutoscrollFromSource(getId())) {
selectNode(file, false);
}
}
@Override
public void selectionChanged(@NotNull FileEditorManagerEvent event) {
if (myProjectView.isAutoscrollFromSource(getId())) {
VirtualFile newFile = event.getNewFile();
if (newFile != null) {
selectNode(newFile, false);
}
}
}
});
}
private void disposeListeners() {
FileStatusManager.getInstance(myProject).removeFileStatusListener(myFileStatusListener);
VirtualFileManager.getInstance().removeVirtualFileListener(myFileListener);
VirtualFileManager.getInstance().removeVirtualFileManagerListener(myVirtualFileManagerListener);
ChangeListManager.getInstance(myProject).removeChangeListListener(myChangeListListener);
myMessageBusConnection.disconnect();
}
private boolean isInitialized() {
return myScrollPane != null;
}
@NotNull
@Override
public ActionCallback updateFromRoot(boolean restoreExpandedPaths) {
rebuildTreeLater();
return new ActionCallback();
}
@Override
public void select(Object element, VirtualFile file, boolean requestFocus) {
selectNode(file, false);
}
@Override
public Object getData(String dataId) {
if (PlatformDataKeys.VIRTUAL_FILE_ARRAY.getName().equals(dataId)) {
List<VirtualFile> files = new LinkedList<VirtualFile>();
TreePath[] treePaths = getSelectionPaths();
if (treePaths != null) {
for (TreePath tp : treePaths) {
Object lastPathComponent = tp.getLastPathComponent();
if (lastPathComponent instanceof AbstractFileTreeNode) {
AbstractFileTreeNode node = (AbstractFileTreeNode) lastPathComponent;
VirtualFile file = node.getFile();
if (file.isValid()) {
files.add(file);
}
}
}
}
return files.toArray(new VirtualFile[files.size()]);
} else if (PlatformDataKeys.VIRTUAL_FILE.getName().equals(dataId)) {
TreePath tp = getSelectedPath();
if (tp == null) {
return super.getData(dataId);
}
Object lastPathComponent = tp.getLastPathComponent();
if (lastPathComponent instanceof AbstractFileTreeNode) {
AbstractFileTreeNode node = (AbstractFileTreeNode) lastPathComponent;
VirtualFile file = node.getFile();
if (file.isValid()) {
return file;
}
}
} else if (PlatformDataKeys.COPY_PROVIDER.getName().equals(dataId)) {
return new FilePaneCopyProvider();
} else if (PlatformDataKeys.PASTE_PROVIDER.getName().equals(dataId)) {
return new FilePanePasteProvider();
} else if (PlatformDataKeys.CUT_PROVIDER.getName().equals(dataId)) {
return new FilePaneCopyProvider();
} else if (dataId.equals(MPSDataKeys.PLACE.getName())) {
TreeNode treeNode = getSelectedTreeNode(TreeNode.class);
if (treeNode instanceof ModuleTreeNode) {
return ActionPlace.PROJECT_PANE_MODULE;
} else if (treeNode instanceof FileTreeNode) {
return ActionPlace.PROJECT_PANE_FILE;
} else if (treeNode instanceof FolderTreeNode) {
return ActionPlace.PROJECT_PANE_FOLDER;
}
}
return super.getData(dataId);
}
//todo eliminate code duplication in BaseLogicalViewProjectPane
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 void openEditor() {
TreePath selectionPath = getTree().getSelectionPath();
if (selectionPath == null) return;
if (!(selectionPath.getLastPathComponent() instanceof FileTreeNode)) return;
final FileTreeNode fileTreeNode = (FileTreeNode) selectionPath.getLastPathComponent();
// assertion was added for http://youtrack.jetbrains.net/issue/MPS-7762
assert fileTreeNode.getFile().isValid() : "Underlying file is not valid";
com.intellij.openapi.command.CommandProcessor.getInstance().executeCommand(myProject, new Runnable() {
@Override
public void run() {
myIdeDocumentHistory.includeCurrentCommandAsNavigation();
myEditorManager.openFile(fileTreeNode.getFile(), true, true);
}
}, "navigate", "");
}
public void selectNode(@NotNull final VirtualFile file, final boolean changeView) {
ToolWindowManager windowManager = ToolWindowManager.getInstance(getProject());
ToolWindow projectViewToolWindow = windowManager.getToolWindow(ToolWindowId.PROJECT_VIEW);
projectViewToolWindow.activate(new Runnable() {
@Override
public void run() {
myProjectView.changeView(getId());
MPSTreeNode nodeToSelect = getNode(file);
if (nodeToSelect != null) {
TreePath treePath = new TreePath(nodeToSelect.getPath());
getTree().setSelectionPath(treePath);
getTree().scrollPathToVisible(treePath);
getTree().selectNode(nodeToSelect);
if (changeView) {
myProjectView.changeView(getId());
}
} else {
LOG.info("Can not find file " + file + " in tree.");
}
}
}, false);
}
@Nullable
protected MPSTreeNode getNode(VirtualFile file) {
DefaultTreeModel treeModel = (DefaultTreeModel) getTree().getModel();
MPSTreeNode rootTreeNode = (MPSTreeNode) treeModel.getRoot();
return getNode(rootTreeNode, file);
}
@Nullable
private MPSTreeNode getNode(MPSTreeNode rootTreeNode, VirtualFile file) {
if (rootTreeNode instanceof AbstractFileTreeNode) {
VirtualFile nodeFile = ((AbstractFileTreeNode) rootTreeNode).getFile();
if (nodeFile != null) {
if (nodeFile.getUrl().equals(file.getUrl())) {
return rootTreeNode;
}
if (!VfsUtil.isAncestor(nodeFile, file, false)) {
return null;
}
} else {
return null;
}
}
for (MPSTreeNode node : rootTreeNode) {
node.init();
MPSTreeNode result = getNode(node, file);
if (result != null) {
return result;
}
}
return null;
}
@Override
public SelectInTarget createSelectInTarget() {
return new AbstractProjectViewSelectInTarget(myProject, getId(), getWeight(), getTitle()) {
public VirtualFile myFile;
@Override
public boolean canSelect(SelectInContext context) {
VirtualFile virtualFile = context.getVirtualFile();
if (!(virtualFile instanceof MPSNodeVirtualFile)) {
myFile = virtualFile;
if (isInitialized() && getNode(virtualFile) == null) return false;
return true;
}
final MPSNodeVirtualFile nodeVirtualFile = (MPSNodeVirtualFile) virtualFile;
SModel smodel = ModelAccess.instance().runReadAction(new Computable<SModel>() {
@Override
public SModel compute() {
return nodeVirtualFile.getNode().getModel();
}
});
if (smodel == null) return false;
DataSource source = smodel.getSource();
if (!(source instanceof FileSystemBasedDataSource)) return false;
IFile modelFile = ((FileSystemBasedDataSource) source).getAffectedFiles().iterator().next();
VirtualFile realFile = null;
if (modelFile != null) {
realFile = VirtualFileUtils.getProjectVirtualFile(modelFile);
}
myFile = realFile;
if ((realFile == null) || (isInitialized() && getNode(realFile) == null)) return false;
return true;
}
@Override
protected void doSelectIn(SelectInContext context, boolean requestFocus) {
ModelAccess.instance().runReadAction(new Runnable() {
@Override
public void run() {
selectNode(myFile, true);
}
});
}
};
}
@Override
public String getTitle() {
return TITLE;
}
@Override
@NotNull
public String getId() {
return ID;
}
// used for sorting tabs in the tabbed pane
@Override
public int getWeight() {
return 5;
}
private class RefreshListener implements VirtualFileManagerListener {
@Override
public void beforeRefreshStart(boolean asynchonous) {
}
@Override
public void afterRefreshFinish(boolean asynchonous) {
rebuildTreeLater();
}
}
private class FileChangesListener extends VirtualFileAdapter {
@Override
public void fileCreated(VirtualFileEvent event) {
rebuildTreeLater();
}
@Override
public void fileDeleted(VirtualFileEvent event) {
rebuildTreeLater();
}
@Override
public void fileMoved(VirtualFileMoveEvent event) {
rebuildTreeLater();
}
@Override
public void fileCopied(VirtualFileCopyEvent event) {
rebuildTreeLater();
}
}
private class ChangeListUpdateListener extends ChangeListAdapter {
@Override
public void changeListUpdateDone() {
rebuildTreeLater();
}
}
private class FileStatusChangeListener implements FileStatusListener {
@Override
public void fileStatusesChanged() {
rebuildTreeLater();
}
@Override
public void fileStatusChanged(@NotNull VirtualFile virtualFile) {
rebuildTreeLater();
}
}
}