/* * 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.editor.tabs; import com.intellij.openapi.actionSystem.ActionManager; import com.intellij.openapi.actionSystem.ActionPlaces; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.DefaultActionGroup; import com.intellij.openapi.actionSystem.IdeActions; import com.intellij.openapi.actionSystem.LangDataKeys; import com.intellij.openapi.actionSystem.Separator; import com.intellij.openapi.actionSystem.impl.ActionButton; import com.intellij.openapi.editor.Document; import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx; import com.intellij.openapi.ui.ShadowAction; import com.intellij.openapi.vcs.FileStatusManager; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.wm.IdeFocusManager; import jetbrains.mps.ide.ModelReadAction; import jetbrains.mps.ide.editor.BaseNodeEditor; import jetbrains.mps.ide.editor.MPSEditorDataKeys; import jetbrains.mps.ide.editorTabs.tabfactory.NodeChangeCallback; import jetbrains.mps.ide.editorTabs.tabfactory.TabComponentFactory; import jetbrains.mps.ide.editorTabs.tabfactory.TabsComponent; import jetbrains.mps.ide.editorTabs.tabfactory.tabs.AddAspectAction; import jetbrains.mps.ide.editorTabs.tabfactory.tabs.CreateGroupsBuilder; import jetbrains.mps.ide.editorTabs.tabfactory.tabs.CreateModeCallback; import jetbrains.mps.ide.project.ProjectHelper; import jetbrains.mps.nodeEditor.EditorSettings; import jetbrains.mps.nodeEditor.EditorSettingsListener; import jetbrains.mps.nodefs.MPSNodeVirtualFile; import jetbrains.mps.nodefs.NodeVirtualFileSystem; import jetbrains.mps.openapi.editor.EditorState; import jetbrains.mps.plugins.relations.RelationDescriptor; import jetbrains.mps.project.Project; import jetbrains.mps.smodel.SNodeUtil; import jetbrains.mps.util.EqualUtil; import org.jdom.Element; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.openapi.event.SPropertyChangeEvent; import org.jetbrains.mps.openapi.model.SModel; import org.jetbrains.mps.openapi.model.SNode; import org.jetbrains.mps.openapi.model.SNodeChangeListenerAdapter; import org.jetbrains.mps.openapi.model.SNodeReference; import org.jetbrains.mps.openapi.module.SModule; import javax.swing.JComponent; import javax.swing.JPanel; import java.awt.BorderLayout; import java.awt.Dimension; import java.util.Collection; import java.util.List; import java.util.Set; public class TabbedEditor extends BaseNodeEditor { private TabsComponent myTabsComponent; private final MyNameListener myNameListener = new MyNameListener(); private final SNodeReference myBaseNode; private final Set<RelationDescriptor> myPossibleTabs; private final ShadowAction myNextTabAction, myPrevTabAction; // UI container to hold tab UI components plus auxiliary controls like 'Add aspect' action and alike. private final JPanel myTabsPanel; private final RepoChangeListener myRepoChangeListener = new RepoChangeListener(); private final FileStatusChangeListener myFileStatusListener = new FileStatusChangeListener(); private final EditorSettingsListener mySettingsListener = new EditorSettingsListener() { @Override public void settingsChanged() { final SNodeReference node = getCurrentlyEditedNode(); JComponent comp = myTabsComponent.getComponent(); if (comp != null) { myTabsPanel.remove(comp); } myProject.getModelAccess().runReadInEDT(new Runnable() { @Override public void run() { if (myDisposed) { return; } installTabsComponent(); if (node != null) { myTabsComponent.updateTabs(); myTabsComponent.editNode(node); } } }); } }; private final MPSNodeVirtualFile myVirtualFile; private boolean myDisposed; public TabbedEditor(SNodeReference baseNode, Set<RelationDescriptor> possibleTabs, @NotNull Project mpsProject) { super(mpsProject); myBaseNode = baseNode; myPossibleTabs = possibleTabs; myVirtualFile = NodeVirtualFileSystem.getInstance().getFileFor(mpsProject.getRepository(), myBaseNode); myTabsPanel = new JPanel(new BorderLayout()); // bloody BaseNodeEditor makes us know about layout used there getComponent().add(myTabsPanel, BorderLayout.SOUTH); installTabsComponent(); showNode(myBaseNode.resolve(myProject.getRepository()), false); myNextTabAction = new ShadowAction(new BaseNavigationAction(() -> myTabsComponent.nextTab()), ActionManager.getInstance().getAction(IdeActions.ACTION_NEXT_EDITOR_TAB), getComponent()); myPrevTabAction = new ShadowAction(new BaseNavigationAction(() -> myTabsComponent.prevTab()), ActionManager.getInstance().getAction(IdeActions.ACTION_PREVIOUS_EDITOR_TAB), getComponent()); final AnAction addAction = new AddAspectAction(mpsProject, myBaseNode, myPossibleTabs, new SetTabsComponentNode()) { @Override protected RelationDescriptor getCurrentAspect() { return myTabsComponent.getCurrentTabAspect(); } }; ActionButton btn = new ActionButton(addAction, addAction.getTemplatePresentation(), ActionPlaces.UNKNOWN, new Dimension(23, 23)); myTabsPanel.add(btn, BorderLayout.WEST); EditorSettings.getInstance().addEditorSettingsListener(mySettingsListener); myRepoChangeListener.subscribeTo(myProject.getRepository()); myFileStatusListener.attach(myProject); } private void installTabsComponent() { if (myTabsComponent != null) { myTabsComponent.dispose(); } final NodeChangeCallback nodeChangeCallback = newNode -> showNodeInternal(newNode); final CreateModeCallback createAspectCallback = new CreateModeCallback() { @Override public void create(RelationDescriptor tab) { // FIXME what if we create two+ aspects in a row, who's responsible to dispose inactive CreatePanel instances? final CreatePanel cp = new CreatePanel(myProject, myBaseNode, new SetTabsComponentNode(), tab); showComponent(cp); final IdeFocusManager fm = IdeFocusManager.getInstance(ProjectHelper.toIdeaProject(myProject)); fm.doWhenFocusSettlesDown(() -> fm.requestFocus(cp, false)); } }; myTabsComponent = TabComponentFactory.createTabsComponent(myBaseNode, myPossibleTabs, getEditorPanel(), nodeChangeCallback, createAspectCallback, ProjectHelper.toIdeaProject(myProject)); myRepoChangeListener.setTabController(myTabsComponent); myFileStatusListener.setTabController(myTabsComponent, myBaseNode); JComponent c = myTabsComponent.getComponent(); if (c != null) { myTabsPanel.add(c, BorderLayout.CENTER); } } @Override public void dispose() { myDisposed = true; myFileStatusListener.detach(); EditorSettings.getInstance().removeEditorSettingsListener(mySettingsListener); myNextTabAction.dispose(); myPrevTabAction.dispose(); myProject.getModelAccess().runReadAction(() -> { myRepoChangeListener.unsubscribeFrom(myProject.getRepository()); myNameListener.detach(); }); myTabsComponent.dispose(); super.dispose(); } @Override public boolean isTabbed() { return true; } @Override public List<Document> getAllEditedDocuments() { return myTabsComponent.getAllEditedDocuments(); } @Override public void showNode(SNode node, boolean select) { SNodeReference currentNodeReference = getCurrentlyEditedNode(); SNodeReference newNodeReference = node.getReference(); if (currentNodeReference != null && currentNodeReference.equals(newNodeReference)) { return; } if (currentNodeReference == null) { showEditor(); } myTabsComponent.updateTabs(); myTabsComponent.editNode(newNodeReference); } private void showNodeInternal(SNodeReference nodeRef) { if (getCurrentEditorComponent() == null) { showEditor(); } myNameListener.detach(); if (nodeRef == null) { // Null means that it is empty tab - just update tab header executeInEDT(new PrioritizedTask(TaskType.UPDATE_PROPERTIES, myType2TaskMap) { @Override public void performTask() { updateProperties(); } }); return; } SNode node = nodeRef.resolve(myProject.getRepository()); if (node == null || node.getModel() == null) { // FIXME suggest create new? Use CreatePanel? return; } SModel md = node.getModel(); SModule module = md.getModule(); assert module != null : md.getReference().toString() + "; node is disposed = " + !org.jetbrains.mps.openapi.model.SNodeUtil.isAccessible(node, myProject.getRepository()); SNodeReference selection = nodeRef; if (myTabsComponent.getCurrentTabAspect() != null) { Collection<SNodeReference> a = myTabsComponent.getSelectionFor(myTabsComponent.getCurrentTabAspect(), nodeRef); selection = a.isEmpty() ? selection : a.iterator().next(); } editNode(nodeRef, selection); myNameListener.attach(md); executeInEDT(new PrioritizedTask(TaskType.UPDATE_PROPERTIES, myType2TaskMap) { @Override public void performTask() { updateProperties(); } }); } /*package*/ void updateProperties() { final com.intellij.openapi.project.Project project = ProjectHelper.toIdeaProject(myProject); FileEditorManagerEx manager = FileEditorManagerEx.getInstanceEx(project); VirtualFile virtualFile = manager.getCurrentFile(); if (virtualFile != null) { FileStatusManager.getInstance(project).fileStatusChanged(virtualFile); manager.updateFilePresentation(virtualFile); } } @Override public Object getData(@NonNls String dataId) { if (MPSEditorDataKeys.EDITOR_CREATE_GROUP.getName().equals(dataId)) { return getCreateGroup(); } if (dataId.equals(LangDataKeys.VIRTUAL_FILE.getName())) { return myVirtualFile; } return null; } private AnAction getCreateGroup() { DefaultActionGroup result = new DefaultActionGroup(); List<DefaultActionGroup> groups = new CreateGroupsBuilder(myProject, myBaseNode, new SetTabsComponentNode()).getCreateGroups(myPossibleTabs, myTabsComponent.getCurrentTabAspect()); for (DefaultActionGroup group : groups) { group.setPopup(false); result.add(group); result.add(new Separator()); } return result; } private class MyNameListener extends SNodeChangeListenerAdapter { private SModel myLastAttachModel; synchronized void attach(@Nullable SModel model) { detach(); myLastAttachModel = model; if (model != null) { model.addChangeListener(this); } } synchronized void detach() { if (myLastAttachModel != null) { myLastAttachModel.removeChangeListener(this); myLastAttachModel = null; } } @Override public void propertyChanged(@NotNull SPropertyChangeEvent event) { if (SNodeUtil.property_INamedConcept_name.equals(event.getProperty()) && event.getNode().getReference().equals(getCurrentlyEditedNode())) { updateProperties(); } } } @Override public EditorState saveState() { TabbedEditorState state = new TabbedEditorState(); saveState(state); return state; } protected void saveState(TabbedEditorState state) { super.saveState(state); state.setNode(getCurrentlyEditedNode()); } @Override public void loadState(@NotNull final EditorState state) { myProject.getModelAccess().runReadAction(() -> { if (state instanceof TabbedEditorState) { SNodeReference nodePointer = ((TabbedEditorState) state).getNode(); SNode node = nodePointer == null ? null : nodePointer.resolve(myProject.getRepository()); if (node != null) { showNode(node, false); } } else { //regular editor was shown for that node last time showNode(myBaseNode.resolve(myProject.getRepository()), false); } }); super.loadState(state); } public final static class TabbedEditorState extends BaseEditorState implements EditorState { private static final String NODE = "node"; private static final String NODE_REF = "node_ref"; private SNodeReference myCurrentNode; /*package*/ void setNode(@Nullable SNodeReference ref) { myCurrentNode = ref; } @Nullable /*package*/ SNodeReference getNode() { return myCurrentNode; } @Override public void save(Element e) { super.save(e); boolean createNewElement = (e.getChild(NODE) == null); Element node = createNewElement ? new Element(NODE) : e.getChild(NODE); if (myCurrentNode != null) { node.setAttribute(NODE_REF, jetbrains.mps.smodel.SNodePointer.serialize(myCurrentNode)); } if (createNewElement) { e.addContent(node); } } @Override public void load(Element e) { super.load(e); Element nodeElem = e.getChild(NODE); String val = nodeElem.getAttributeValue(NODE_REF); if (val != null) { myCurrentNode = jetbrains.mps.smodel.SNodePointer.deserialize(val); } } public int hashCode() { return super.hashCode() * 13 + (myCurrentNode == null ? 0 : myCurrentNode.hashCode()); } public boolean equals(Object obj) { return obj instanceof TabbedEditorState && super.equals(obj) && EqualUtil.equals(myCurrentNode, ((TabbedEditorState) obj).myCurrentNode); } } private static class BaseNavigationAction extends ModelReadAction { public BaseNavigationAction(Runnable delegate) { super(null, delegate); setEnabledInModalContext(true); } } private class SetTabsComponentNode implements NodeChangeCallback { @Override public void changeNode(SNodeReference newNode) { myTabsComponent.updateTabs(); myTabsComponent.editNode(newNode); } } }