blob: 04f908a17c00c6e9e0ca909d14894678a0de02a9 [file] [log] [blame]
/*
* Copyright 2000-2014 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 com.intellij.openapi.roots.ui.configuration;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileChooser.FileChooser;
import com.intellij.openapi.fileChooser.FileChooserDescriptor;
import com.intellij.openapi.fileChooser.ex.FileChooserKeys;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectBundle;
import com.intellij.openapi.roots.ContentEntry;
import com.intellij.openapi.roots.ModifiableRootModel;
import com.intellij.openapi.roots.ModuleRootModel;
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.util.registry.Registry;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.openapi.vfs.ex.VirtualFileManagerAdapter;
import com.intellij.ui.JBSplitter;
import com.intellij.ui.OnePixelSplitter;
import com.intellij.ui.ScrollPaneFactory;
import com.intellij.ui.border.CustomLineBorder;
import com.intellij.ui.roots.ToolbarPanel;
import com.intellij.util.Consumer;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jps.model.module.JpsModuleSourceRootType;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author Eugene Zhuravlev
* Date: Oct 4, 2003
* Time: 6:54:57 PM
*/
public class CommonContentEntriesEditor extends ModuleElementsEditor {
private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.roots.ui.configuration.ContentEntriesEditor");
public static final String NAME = ProjectBundle.message("module.paths.title");
private static final Color BACKGROUND_COLOR = UIUtil.getListBackground();
protected ContentEntryTreeEditor myRootTreeEditor;
private MyContentEntryEditorListener myContentEntryEditorListener;
protected JPanel myEditorsPanel;
protected final Map<String, ContentEntryEditor> myEntryToEditorMap = new HashMap<String, ContentEntryEditor>();
private String mySelectedEntryUrl;
private VirtualFile myLastSelectedDir = null;
private final String myModuleName;
private final ModulesProvider myModulesProvider;
private final ModuleConfigurationState myState;
private final List<ModuleSourceRootEditHandler<?>> myEditHandlers = new ArrayList<ModuleSourceRootEditHandler<?>>();
public CommonContentEntriesEditor(String moduleName, final ModuleConfigurationState state, JpsModuleSourceRootType<?>... rootTypes) {
super(state);
myState = state;
myModuleName = moduleName;
myModulesProvider = state.getModulesProvider();
for (JpsModuleSourceRootType<?> type : rootTypes) {
myEditHandlers.add(ModuleSourceRootEditHandler.getEditHandler(type));
}
final VirtualFileManagerAdapter fileManagerListener = new VirtualFileManagerAdapter() {
@Override
public void afterRefreshFinish(boolean asynchronous) {
if (state.getProject().isDisposed()) {
return;
}
final Module module = getModule();
if (module == null || module.isDisposed()) return;
for (final ContentEntryEditor editor : myEntryToEditorMap.values()) {
editor.update();
}
}
};
final VirtualFileManager fileManager = VirtualFileManager.getInstance();
fileManager.addVirtualFileManagerListener(fileManagerListener);
registerDisposable(new Disposable() {
@Override
public void dispose() {
fileManager.removeVirtualFileManagerListener(fileManagerListener);
}
});
}
@Override
protected ModifiableRootModel getModel() {
return myState.getRootModel();
}
@Override
public String getHelpTopic() {
return "projectStructure.modules.sources";
}
@Override
public String getDisplayName() {
return NAME;
}
protected final List<ModuleSourceRootEditHandler<?>> getEditHandlers() {
return myEditHandlers;
}
@Override
public void disposeUIResources() {
if (myRootTreeEditor != null) {
myRootTreeEditor.setContentEntryEditor(null);
}
myEntryToEditorMap.clear();
super.disposeUIResources();
}
@Override
public JPanel createComponentImpl() {
final Module module = getModule();
final Project project = module.getProject();
myContentEntryEditorListener = new MyContentEntryEditorListener();
final JPanel mainPanel = new JPanel(new BorderLayout());
if (!Registry.is("ide.new.project.settings")) {
mainPanel.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
}
addAdditionalSettingsToPanel(mainPanel);
final JPanel entriesPanel = new JPanel(new BorderLayout());
final DefaultActionGroup group = new DefaultActionGroup();
final AddContentEntryAction action = new AddContentEntryAction();
action.registerCustomShortcutSet(KeyEvent.VK_C, InputEvent.ALT_DOWN_MASK, mainPanel);
group.add(action);
myEditorsPanel = new ScrollablePanel(new VerticalStackLayout());
myEditorsPanel.setBackground(BACKGROUND_COLOR);
JScrollPane myScrollPane = ScrollPaneFactory.createScrollPane(myEditorsPanel, Registry.is("ide.new.project.settings"));
final ToolbarPanel toolbarPanel = new ToolbarPanel(myScrollPane, group);
if (Registry.is("ide.new.project.settings")) {
toolbarPanel.setBorder(new CustomLineBorder(1,0,0,0));
}
entriesPanel.add(toolbarPanel, BorderLayout.CENTER);
final JBSplitter splitter = Registry.is("ide.new.project.settings") ? new OnePixelSplitter(false) : new JBSplitter(false);
splitter.setProportion(0.6f);
splitter.setHonorComponentsMinimumSize(true);
myRootTreeEditor = createContentEntryTreeEditor(project);
final JComponent component = myRootTreeEditor.createComponent();
if (Registry.is("ide.new.project.settings")) {
component.setBorder(new CustomLineBorder(1,0,0,0));
}
splitter.setFirstComponent(component);
splitter.setSecondComponent(entriesPanel);
JPanel contentPanel = new JPanel(new GridBagLayout());
if (!Registry.is("ide.new.project.settings")) {
contentPanel.setBorder(BorderFactory.createEtchedBorder());
}
final ActionToolbar actionToolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.UNKNOWN, myRootTreeEditor.getEditingActionsGroup(), true);
contentPanel.add(new JLabel("Mark as:"),
new GridBagConstraints(0, 0, 1, 1, 0, 0, GridBagConstraints.WEST, 0, new Insets(0, 10, 0, 10), 0, 0));
contentPanel.add(actionToolbar.getComponent(),
new GridBagConstraints(1, 0, 1, 1, 1.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL,
new Insets(0, 0, 0, 0), 0, 0));
contentPanel.add(splitter,
new GridBagConstraints(0, GridBagConstraints.RELATIVE, 2, 1, 1.0, 1.0, GridBagConstraints.WEST, GridBagConstraints.BOTH,
new Insets(0, 0, 0, 0), 0, 0));
mainPanel.add(contentPanel, BorderLayout.CENTER);
final JPanel innerPanel = createBottomControl(module);
if (innerPanel != null) {
mainPanel.add(innerPanel, BorderLayout.SOUTH);
}
final ModifiableRootModel model = getModel();
if (model != null) {
final ContentEntry[] contentEntries = model.getContentEntries();
if (contentEntries.length > 0) {
for (final ContentEntry contentEntry : contentEntries) {
addContentEntryPanel(contentEntry.getUrl());
}
selectContentEntry(contentEntries[0].getUrl());
}
}
return mainPanel;
}
@Nullable
protected JPanel createBottomControl(Module module) {
return null;
}
protected ContentEntryTreeEditor createContentEntryTreeEditor(Project project) {
return new ContentEntryTreeEditor(project, myEditHandlers);
}
protected void addAdditionalSettingsToPanel(final JPanel mainPanel) {
}
protected Module getModule() {
return myModulesProvider.getModule(myModuleName);
}
protected void addContentEntryPanel(final String contentEntry) {
final ContentEntryEditor contentEntryEditor = createContentEntryEditor(contentEntry);
contentEntryEditor.initUI();
contentEntryEditor.addContentEntryEditorListener(myContentEntryEditorListener);
registerDisposable(new Disposable() {
@Override
public void dispose() {
contentEntryEditor.removeContentEntryEditorListener(myContentEntryEditorListener);
}
});
myEntryToEditorMap.put(contentEntry, contentEntryEditor);
Border border = BorderFactory.createEmptyBorder(2, 2, 0, 2);
final JComponent component = contentEntryEditor.getComponent();
final Border componentBorder = component.getBorder();
if (componentBorder != null) {
border = BorderFactory.createCompoundBorder(border, componentBorder);
}
if (Registry.is("ide.new.project.settings")) {
component.setBorder(new EmptyBorder(0,0,0,0));
} else {
component.setBorder(border);
}
myEditorsPanel.add(component);
}
protected ContentEntryEditor createContentEntryEditor(String contentEntryUrl) {
return new ContentEntryEditor(contentEntryUrl, myEditHandlers) {
@Override
protected ModifiableRootModel getModel() {
return CommonContentEntriesEditor.this.getModel();
}
};
}
void selectContentEntry(final String contentEntryUrl) {
if (mySelectedEntryUrl != null && mySelectedEntryUrl.equals(contentEntryUrl)) {
return;
}
try {
if (mySelectedEntryUrl != null) {
ContentEntryEditor editor = myEntryToEditorMap.get(mySelectedEntryUrl);
if (editor != null) {
editor.setSelected(false);
}
}
if (contentEntryUrl != null) {
ContentEntryEditor editor = myEntryToEditorMap.get(contentEntryUrl);
if (editor != null) {
editor.setSelected(true);
final JComponent component = editor.getComponent();
final JComponent scroller = (JComponent)component.getParent();
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
scroller.scrollRectToVisible(component.getBounds());
}
});
myRootTreeEditor.setContentEntryEditor(editor);
myRootTreeEditor.requestFocus();
}
}
}
finally {
mySelectedEntryUrl = contentEntryUrl;
}
}
@Override
public void moduleStateChanged() {
if (myRootTreeEditor != null) { //in order to update exclude output root if it is under content root
myRootTreeEditor.update();
}
}
@Nullable
private String getNextContentEntry(final String contentEntryUrl) {
return getAdjacentContentEntry(contentEntryUrl, 1);
}
@Nullable
private String getAdjacentContentEntry(final String contentEntryUrl, int delta) {
final ContentEntry[] contentEntries = getModel().getContentEntries();
for (int idx = 0; idx < contentEntries.length; idx++) {
ContentEntry entry = contentEntries[idx];
if (contentEntryUrl.equals(entry.getUrl())) {
int nextEntryIndex = (idx + delta) % contentEntries.length;
if (nextEntryIndex < 0) {
nextEntryIndex += contentEntries.length;
}
return nextEntryIndex == idx ? null : contentEntries[nextEntryIndex].getUrl();
}
}
return null;
}
protected List<ContentEntry> addContentEntries(final VirtualFile[] files) {
List<ContentEntry> contentEntries = new ArrayList<ContentEntry>();
for (final VirtualFile file : files) {
if (isAlreadyAdded(file)) {
continue;
}
final ContentEntry contentEntry = getModel().addContentEntry(file);
contentEntries.add(contentEntry);
}
return contentEntries;
}
private boolean isAlreadyAdded(VirtualFile file) {
final VirtualFile[] contentRoots = getModel().getContentRoots();
for (VirtualFile contentRoot : contentRoots) {
if (contentRoot.equals(file)) {
return true;
}
}
return false;
}
@Override
public void saveData() {
}
protected void addContentEntryPanels(ContentEntry[] contentEntriesArray) {
for (ContentEntry contentEntry : contentEntriesArray) {
addContentEntryPanel(contentEntry.getUrl());
}
myEditorsPanel.revalidate();
myEditorsPanel.repaint();
selectContentEntry(contentEntriesArray[contentEntriesArray.length - 1].getUrl());
}
private final class MyContentEntryEditorListener extends ContentEntryEditorListenerAdapter {
@Override
public void editingStarted(@NotNull ContentEntryEditor editor) {
selectContentEntry(editor.getContentEntryUrl());
}
@Override
public void beforeEntryDeleted(@NotNull ContentEntryEditor editor) {
final String entryUrl = editor.getContentEntryUrl();
if (mySelectedEntryUrl != null && mySelectedEntryUrl.equals(entryUrl)) {
myRootTreeEditor.setContentEntryEditor(null);
}
final String nextContentEntryUrl = getNextContentEntry(entryUrl);
removeContentEntryPanel(entryUrl);
selectContentEntry(nextContentEntryUrl);
editor.removeContentEntryEditorListener(this);
}
@Override
public void navigationRequested(@NotNull ContentEntryEditor editor, VirtualFile file) {
if (mySelectedEntryUrl != null && mySelectedEntryUrl.equals(editor.getContentEntryUrl())) {
myRootTreeEditor.requestFocus();
myRootTreeEditor.select(file);
}
else {
selectContentEntry(editor.getContentEntryUrl());
myRootTreeEditor.requestFocus();
myRootTreeEditor.select(file);
}
}
private void removeContentEntryPanel(final String contentEntryUrl) {
ContentEntryEditor editor = myEntryToEditorMap.get(contentEntryUrl);
if (editor != null) {
myEditorsPanel.remove(editor.getComponent());
myEntryToEditorMap.remove(contentEntryUrl);
myEditorsPanel.revalidate();
myEditorsPanel.repaint();
}
}
}
private class AddContentEntryAction extends IconWithTextAction implements DumbAware {
private final FileChooserDescriptor myDescriptor;
public AddContentEntryAction() {
super(ProjectBundle.message("module.paths.add.content.action"),
ProjectBundle.message("module.paths.add.content.action.description"), AllIcons.Modules.AddContentEntry);
myDescriptor = new FileChooserDescriptor(false, true, true, false, true, true) {
@Override
public void validateSelectedFiles(VirtualFile[] files) throws Exception {
validateContentEntriesCandidates(files);
}
};
myDescriptor.putUserData(LangDataKeys.MODULE_CONTEXT, getModule());
myDescriptor.setTitle(ProjectBundle.message("module.paths.add.content.title"));
myDescriptor.setDescription(ProjectBundle.message("module.paths.add.content.prompt"));
myDescriptor.putUserData(FileChooserKeys.DELETE_ACTION_AVAILABLE, false);
}
@Override
public void actionPerformed(AnActionEvent e) {
FileChooser.chooseFiles(myDescriptor, myProject, myLastSelectedDir, new Consumer<List<VirtualFile>>() {
@Override
public void consume(List<VirtualFile> files) {
myLastSelectedDir = files.get(0);
addContentEntries(VfsUtilCore.toVirtualFileArray(files));
}
});
}
@Nullable
private ContentEntry getContentEntry(final String url) {
final ContentEntry[] entries = getModel().getContentEntries();
for (final ContentEntry entry : entries) {
if (entry.getUrl().equals(url)) return entry;
}
return null;
}
private void validateContentEntriesCandidates(VirtualFile[] files) throws Exception {
for (final VirtualFile file : files) {
// check for collisions with already existing entries
for (final String contentEntryUrl : myEntryToEditorMap.keySet()) {
final ContentEntry contentEntry = getContentEntry(contentEntryUrl);
if (contentEntry == null) continue;
final VirtualFile contentEntryFile = contentEntry.getFile();
if (contentEntryFile == null) {
continue; // skip invalid entry
}
if (contentEntryFile.equals(file)) {
throw new Exception(ProjectBundle.message("module.paths.add.content.already.exists.error", file.getPresentableUrl()));
}
if (VfsUtilCore.isAncestor(contentEntryFile, file, true)) {
// intersection not allowed
throw new Exception(
ProjectBundle.message("module.paths.add.content.intersect.error", file.getPresentableUrl(),
contentEntryFile.getPresentableUrl()));
}
if (VfsUtilCore.isAncestor(file, contentEntryFile, true)) {
// intersection not allowed
throw new Exception(
ProjectBundle.message("module.paths.add.content.dominate.error", file.getPresentableUrl(),
contentEntryFile.getPresentableUrl()));
}
}
// check if the same root is configured for another module
final Module[] modules = myModulesProvider.getModules();
for (final Module module : modules) {
if (myModuleName.equals(module.getName())) {
continue;
}
ModuleRootModel rootModel = myModulesProvider.getRootModel(module);
LOG.assertTrue(rootModel != null);
final VirtualFile[] moduleContentRoots = rootModel.getContentRoots();
for (VirtualFile moduleContentRoot : moduleContentRoots) {
if (file.equals(moduleContentRoot)) {
throw new Exception(
ProjectBundle.message("module.paths.add.content.duplicate.error", file.getPresentableUrl(), module.getName()));
}
}
}
}
}
}
}