/*
* Copyright 2003-2015 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.ui.dialogs.properties.roots.editors;
import com.intellij.icons.AllIcons.Modules;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.fileChooser.FileChooser;
import com.intellij.openapi.fileChooser.FileChooserDescriptor;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.roots.ui.componentsList.components.ScrollablePanel;
import com.intellij.openapi.roots.ui.componentsList.layout.VerticalStackLayout;
import com.intellij.openapi.roots.ui.configuration.actions.IconWithTextAction;
import com.intellij.openapi.ui.Splitter;
import com.intellij.openapi.ui.popup.JBPopup;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.ui.popup.PopupStep;
import com.intellij.openapi.ui.popup.util.BaseListPopupStep;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.ScrollPaneFactory;
import com.intellij.ui.awt.RelativePoint;
import com.intellij.ui.components.JBPanel;
import com.intellij.ui.roots.ToolbarPanel;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.UIUtil;
import jetbrains.mps.extapi.persistence.FileBasedModelRoot;
import jetbrains.mps.ide.ui.dialogs.properties.PropertiesBundle;
import jetbrains.mps.ide.ui.dialogs.properties.roots.editors.ModelRootEntryContainer.ContentEntryEditorListener;
import jetbrains.mps.ide.vfs.VirtualFileUtils;
import jetbrains.mps.persistence.MementoImpl;
import jetbrains.mps.persistence.PersistenceRegistry;
import jetbrains.mps.project.AbstractModule;
import jetbrains.mps.project.structure.model.ModelRootDescriptor;
import jetbrains.mps.project.structure.modules.ModuleDescriptor;
import jetbrains.mps.smodel.ModelAccessHelper;
import jetbrains.mps.vfs.FileSystemExtPoint;
import jetbrains.mps.vfs.IFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SRepository;
import org.jetbrains.mps.openapi.persistence.Memento;
import org.jetbrains.mps.openapi.persistence.ModelRoot;
import org.jetbrains.mps.openapi.ui.persistence.ModelRootEntry;
import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Point;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import static com.intellij.openapi.vfs.VfsUtilCore.isAncestor;
/**
* UIComponent which contains all the module roots.
* It is located in the module properties dialog.
*/
public class ModelRootContentEntriesEditor implements Disposable {
private static final Color BACKGROUND_COLOR = UIUtil.getListBackground();
private final ModuleDescriptor myModuleDescriptor;
private final SRepository myRepository;
private final ModelRootEntryPersistence myRootEntryPersistence;
private final List<ModelRootEntryContainer> myModelRootEntries = new ArrayList<>();
private ModelRootEntryContainer myFocusedModelRootEntryContainer;
private final MyContentEntryEditorListener myEditorListener = new MyContentEntryEditorListener();
private JPanel myEditorsListPanel;
private JBPanel myEditorPanel;
private JBPanel myMainPanel;
private IFile myDefaultFolder;
public ModelRootContentEntriesEditor(ModuleDescriptor moduleDescriptor, SRepository repository) {
myModuleDescriptor = moduleDescriptor;
myRepository = repository;
myRootEntryPersistence = new ModelRootEntryPersistence().initFromEP();
for (ModelRootDescriptor descriptor : myModuleDescriptor.getModelRootDescriptors()) {
ModelRootEntry entry = myRootEntryPersistence.getModelRootEntry(descriptor);
Disposer.register(this, entry);
ModelRootEntryContainer container = new ModelRootEntryContainer(entry);
container.addContentEntryEditorListener(myEditorListener);
myModelRootEntries.add(container);
}
initUI();
}
private AnAction getContentEntryActions() {
final List<AddContentEntryAction> list = new ArrayList<>();
for (String type : myRootEntryPersistence.getModelRootTypes()) {
list.add(new AddContentEntryAction(type));
}
return new IconWithTextAction(
PropertiesBundle.message("module.common.roots.add.title"),
PropertiesBundle.message("module.common.roots.add.tip"),
Modules.AddContentEntry) {
@Override
public void actionPerformed(final AnActionEvent e) {
if (list.size() == 1) {
myRepository.getModelAccess().runReadAction(() -> list.get(0).actionPerformed(e));
return;
}
final JBPopup popup = JBPopupFactory.getInstance().createListPopup(
new BaseListPopupStep<AddContentEntryAction>(null, list) {
@Override
public Icon getIconFor(AddContentEntryAction aValue) {
return aValue.getTemplatePresentation().getIcon();
}
@Override
public boolean hasSubstep(AddContentEntryAction selectedValue) {
return false;
}
@Override
public boolean isMnemonicsNavigationEnabled() {
return true;
}
@Override
public PopupStep onChosen(final AddContentEntryAction selectedValue, final boolean finalChoice) {
return doFinalStep(() -> selectedValue.actionPerformed(e));
}
@Override
@NotNull
public String getTextFor(AddContentEntryAction value) {
return value.getTemplatePresentation().getText();
}
});
popup.show(new RelativePoint(myEditorsListPanel, new Point(0, 0)));
}
};
}
public void initUI() {
myMainPanel = new JBPanel(new BorderLayout());
myMainPanel.setPreferredSize(new Dimension(300, 300));
final JBPanel entriesPanel = new JBPanel(new BorderLayout());
final DefaultActionGroup group = new DefaultActionGroup();
group.add(getContentEntryActions());
myEditorsListPanel = new ScrollablePanel(new VerticalStackLayout());
myEditorsListPanel.setBackground(BACKGROUND_COLOR);
JScrollPane scrollPane = ScrollPaneFactory.createScrollPane(myEditorsListPanel);
scrollPane.setPreferredSize(new Dimension(250, 300));
entriesPanel.add(new ToolbarPanel(scrollPane, group), BorderLayout.CENTER);
Splitter splitter = new Splitter(false);
splitter.setHonorComponentsMinimumSize(true);
myMainPanel.add(splitter, BorderLayout.CENTER);
final JBPanel editorsPanel = new JBPanel(new GridBagLayout());
splitter.setFirstComponent(editorsPanel);
editorsPanel.add(entriesPanel,
new GridBagConstraints(0, 0, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, JBUI.emptyInsets(), 0, 0));
final JBPanel editorPanel = new JBPanel(new BorderLayout());
editorPanel.setBorder(BorderFactory.createEtchedBorder());
myEditorPanel = new JBPanel(new BorderLayout());
editorPanel.add(myEditorPanel, BorderLayout.CENTER);
splitter.setSecondComponent(editorPanel);
for (ModelRootEntryContainer entry : myModelRootEntries) {
myEditorsListPanel.add(entry.getComponent());
}
selectEntry(myModelRootEntries.size() > 0 ? myModelRootEntries.get(0) : null);
}
private void selectEntry(ModelRootEntryContainer entry) {
try {
if (entry != null && entry.equals(myFocusedModelRootEntryContainer)) {
return;
}
if (myFocusedModelRootEntryContainer != null) {
myFocusedModelRootEntryContainer.setFocused(false);
}
if (entry == null) {
myFocusedModelRootEntryContainer = null;
myEditorPanel.removeAll();
return;
}
entry.setFocused(true);
myEditorPanel.removeAll();
myEditorPanel.add(entry.getEditor().createComponent(), BorderLayout.CENTER);
myFocusedModelRootEntryContainer = entry;
} finally {
myMainPanel.updateUI();
}
}
private void deleteEntry(ModelRootEntryContainer entry) {
if (myModelRootEntries.contains(entry)) {
myEditorsListPanel.remove(entry.getComponent());
int idx = myModelRootEntries.indexOf(entry);
myModelRootEntries.remove(entry);
if (myFocusedModelRootEntryContainer.equals(entry)) {
selectEntry(myModelRootEntries.size() > 0 ?
myModelRootEntries.get(Math.max(idx - 1, 0))
: null);
} else {
myMainPanel.updateUI();
}
}
}
public boolean isModified() {
List<ModelRootDescriptor> newSet = getDescriptors();
Collection<ModelRootDescriptor> modelRootDescriptors = myModuleDescriptor.getModelRootDescriptors();
return !(modelRootDescriptors.containsAll(newSet) && newSet.containsAll(modelRootDescriptors));
}
public void apply() {
myModuleDescriptor.getModelRootDescriptors().clear();
myModuleDescriptor.getModelRootDescriptors().addAll(getDescriptors());
}
private List<ModelRootDescriptor> getDescriptors() {
List<ModelRootDescriptor> descriptors = new LinkedList<>();
for (ModelRootEntryContainer container : myModelRootEntries) {
Memento memento = new MementoImpl();
container.getModelRoot().save(memento);
descriptors.add(new ModelRootDescriptor(container.getModelRoot().getType(), memento));
}
return descriptors;
}
public Collection<ModelRoot> getModelRoots() {
List<ModelRoot> modelRoots = new LinkedList<>();
for (ModelRootEntryContainer container : myModelRootEntries) {
modelRoots.add(container.getModelRoot());
}
return modelRoots;
}
public Collection<ModelRootEntryContainer> getModelRootsEntries() {
return myModelRootEntries;
}
public JComponent getComponent() {
return myMainPanel;
}
@Override
public void dispose() {
}
/** Set default folder for FileBasedModel root content dir if module is not in repository yet */
public final void setDefaultFolder(IFile defaultFolder) {
myDefaultFolder = defaultFolder;
}
private class AddContentEntryAction extends IconWithTextAction implements DumbAware {
private String myType;
AddContentEntryAction(@NotNull String type) {
super(type);
myType = type;
}
@Override
public void actionPerformed(AnActionEvent e) {
ModelRoot modelRoot = PersistenceRegistry.getInstance().getModelRootFactory(myType).create();
ModelRootEntry entry = myRootEntryPersistence.getModelRootEntry(modelRoot);
Disposer.register(ModelRootContentEntriesEditor.this, entry);
if (entry.getModelRoot() instanceof FileBasedModelRoot) {
if (!checkAndAddFBModelRoot(entry)) {
return;
}
}
ModelRootEntryContainer container = new ModelRootEntryContainer(entry);
container.addContentEntryEditorListener(myEditorListener);
myModelRootEntries.add(container);
myEditorsListPanel.add(container.getComponent());
selectEntry(container);
myEditorsListPanel.revalidate();
myEditorsListPanel.repaint();
}
private boolean checkAndAddFBModelRoot(ModelRootEntry entry) {
IFile contentRoot = myDefaultFolder != null ? myDefaultFolder : FileSystemExtPoint.getFS().getFile("");
final SModule module = new ModelAccessHelper(myRepository).runReadAction(() -> myRepository.getModule(myModuleDescriptor.getId()));
if (module instanceof AbstractModule) {
contentRoot = ((AbstractModule) module).getModuleSourceDir() == null
? ((AbstractModule) module).getDescriptorFile().getParent()
: ((AbstractModule) module).getModuleSourceDir();
}
Set<VirtualFile> candidatesForIntersection = new HashSet<>();
for (ModelRootEntryContainer existingEntryContainer : myModelRootEntries) {
if (entry.getClass().equals(existingEntryContainer.getModelRootEntry().getClass())) {
FileBasedModelRoot existingModelRoot = (FileBasedModelRoot) existingEntryContainer.getModelRootEntry().getModelRoot();
candidatesForIntersection.add(VirtualFileUtils.getVirtualFile(existingModelRoot.getContentRoot()));
}
}
FileChooserDescriptor fileChooserDescriptor = new FileChooserDescriptor(false, true, true, false, true, false);
fileChooserDescriptor.setTitle("Choose root folder for new model root");
VirtualFile chosen = null;
while (chosen == null) {
VirtualFile contentRootVFile = VirtualFileUtils.getProjectVirtualFile(contentRoot);
VirtualFile[] files = FileChooser.chooseFiles(fileChooserDescriptor, null, null, contentRootVFile);
if (files.length == 0) {
return false;
} else if (files.length > 0) {
assert files.length == 1; // internal contract of the <code>FileChooser</code>
chosen = files[0];
for (VirtualFile candidate : candidatesForIntersection) {
if (doIntersect(chosen, candidate)) {
JOptionPane.showMessageDialog(myMainPanel,
MessageFormat.format("Can''t create new model root, it intersects with the existing model root: '{1}'! \nChoose another folder", candidate));
chosen = null;
break;
}
}
}
}
contentRoot = VirtualFileUtils.toIFile(chosen);
assert contentRoot != null; // : #toIFile method contract
((FileBasedModelRoot) entry.getModelRoot()).setContentRoot(contentRoot.getPath());
return true;
}
private boolean doIntersect(VirtualFile chosen, VirtualFile candidate) {
return isAncestor(candidate, chosen, true) || isAncestor(candidate, chosen, false);
}
}
private final class MyContentEntryEditorListener implements ContentEntryEditorListener {
@Override
public void focused(ModelRootEntryContainer entry) {
selectEntry(entry);
}
@Override
public void delete(ModelRootEntryContainer entry) {
deleteEntry(entry);
}
@Override
public void dataChanged(ModelRootEntryContainer entry) {
}
}
}