modifiable root model: stateless content entry editor
[fedora-idea.git] / platform / lang-impl / src / com / intellij / openapi / roots / ui / configuration / CommonContentEntriesEditor.java
blob5048f1910bd88aa8b02a3819ee40acaa7ae1667d
1 /*
2 * Copyright 2000-2009 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.intellij.openapi.roots.ui.configuration;
19 import com.intellij.openapi.Disposable;
20 import com.intellij.openapi.actionSystem.AnActionEvent;
21 import com.intellij.openapi.actionSystem.DefaultActionGroup;
22 import com.intellij.openapi.actionSystem.LangDataKeys;
23 import com.intellij.openapi.diagnostic.Logger;
24 import com.intellij.openapi.fileChooser.FileChooser;
25 import com.intellij.openapi.fileChooser.FileChooserDescriptor;
26 import com.intellij.openapi.module.Module;
27 import com.intellij.openapi.project.DumbAware;
28 import com.intellij.openapi.project.Project;
29 import com.intellij.openapi.project.ProjectBundle;
30 import com.intellij.openapi.roots.ContentEntry;
31 import com.intellij.openapi.roots.ModuleRootModel;
32 import com.intellij.openapi.roots.ModifiableRootModel;
33 import com.intellij.openapi.roots.ui.componentsList.components.ScrollablePanel;
34 import com.intellij.openapi.roots.ui.componentsList.layout.VerticalStackLayout;
35 import com.intellij.openapi.roots.ui.configuration.actions.IconWithTextAction;
36 import com.intellij.openapi.ui.Splitter;
37 import com.intellij.openapi.util.IconLoader;
38 import com.intellij.openapi.vfs.VfsUtil;
39 import com.intellij.openapi.vfs.VirtualFile;
40 import com.intellij.openapi.vfs.VirtualFileManager;
41 import com.intellij.openapi.vfs.ex.VirtualFileManagerAdapter;
42 import com.intellij.openapi.vfs.ex.VirtualFileManagerEx;
43 import com.intellij.ui.ScrollPaneFactory;
44 import com.intellij.ui.roots.ToolbarPanel;
45 import com.intellij.util.ui.UIUtil;
46 import org.jetbrains.annotations.Nullable;
48 import javax.swing.*;
49 import javax.swing.border.Border;
50 import java.awt.*;
51 import java.awt.event.KeyEvent;
52 import java.util.ArrayList;
53 import java.util.HashMap;
54 import java.util.List;
55 import java.util.Map;
57 /**
58 * @author Eugene Zhuravlev
59 * Date: Oct 4, 2003
60 * Time: 6:54:57 PM
62 public abstract class CommonContentEntriesEditor extends ModuleElementsEditor {
63 private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.roots.ui.configuration.ContentEntriesEditor");
64 public static final String NAME = ProjectBundle.message("module.paths.title");
65 public static final Icon ICON = IconLoader.getIcon("/modules/sources.png");
66 private static final Color BACKGROUND_COLOR = UIUtil.getListBackground();
67 private static final Icon ADD_CONTENT_ENTRY_ICON = IconLoader.getIcon("/modules/addContentEntry.png");
69 private ContentEntryTreeEditor myRootTreeEditor;
70 private MyContentEntryEditorListener myContentEntryEditorListener;
71 protected JPanel myEditorsPanel;
72 private final Map<String, ContentEntryEditor> myEntryToEditorMap = new HashMap<String, ContentEntryEditor>();
73 private String mySelectedEntryUrl;
75 private VirtualFile myLastSelectedDir = null;
76 private final String myModuleName;
77 private final ModulesProvider myModulesProvider;
78 private ModuleConfigurationState myState;
80 public CommonContentEntriesEditor(String moduleName, ModuleConfigurationState state) {
81 super(state);
82 myState = state;
83 myModuleName = moduleName;
84 myModulesProvider = state.getModulesProvider();
85 final VirtualFileManagerAdapter fileManagerListener = new VirtualFileManagerAdapter() {
86 public void afterRefreshFinish(boolean asynchronous) {
87 final Module module = getModule();
88 if (module == null || module.isDisposed() || module.getProject().isDisposed()) return;
89 for (final String contentEntry : myEntryToEditorMap.keySet()) {
90 final ContentEntryEditor editor = myEntryToEditorMap.get(contentEntry);
91 if (editor != null) {
92 editor.update();
97 final VirtualFileManagerEx fileManager = ((VirtualFileManagerEx)VirtualFileManager.getInstance());
98 fileManager.addVirtualFileManagerListener(fileManagerListener);
99 registerDisposable(new Disposable() {
100 public void dispose() {
101 fileManager.removeVirtualFileManagerListener(fileManagerListener);
106 @Override
107 protected ModifiableRootModel getModel() {
108 return myState.getRootModel();
111 public String getHelpTopic() {
112 return "projectStructure.modules.sources";
115 public String getDisplayName() {
116 return NAME;
119 public Icon getIcon() {
120 return ICON;
123 public void disposeUIResources() {
124 if (myRootTreeEditor != null) {
125 myRootTreeEditor.setContentEntryEditor(null);
128 myEntryToEditorMap.clear();
129 super.disposeUIResources();
132 public JPanel createComponentImpl() {
133 final Module module = getModule();
134 final Project project = module.getProject();
136 myContentEntryEditorListener = new MyContentEntryEditorListener();
138 final JPanel mainPanel = new JPanel(new BorderLayout());
139 mainPanel.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
141 addAdditionalSettingsToPanel(mainPanel);
143 final JPanel entriesPanel = new JPanel(new BorderLayout());
145 final DefaultActionGroup group = new DefaultActionGroup();
146 final AddContentEntryAction action = new AddContentEntryAction();
147 action.registerCustomShortcutSet(KeyEvent.VK_C, KeyEvent.ALT_DOWN_MASK, mainPanel);
148 group.add(action);
150 myEditorsPanel = new ScrollablePanel(new VerticalStackLayout());
151 myEditorsPanel.setBackground(BACKGROUND_COLOR);
152 JScrollPane myScrollPane = ScrollPaneFactory.createScrollPane(myEditorsPanel);
153 entriesPanel.add(new ToolbarPanel(myScrollPane, group), BorderLayout.CENTER);
155 final Splitter splitter = new Splitter(false);
156 splitter.setHonorComponentsMinimumSize(true);
157 mainPanel.add(splitter, BorderLayout.CENTER);
159 final JPanel editorsPanel = new JPanel(new GridBagLayout());
160 splitter.setFirstComponent(editorsPanel);
161 editorsPanel.add(entriesPanel,
162 new GridBagConstraints(0, 0, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
164 myRootTreeEditor = createContentEntryTreeEditor(project);
165 final JComponent treeEditorComponent = myRootTreeEditor.createComponent();
166 splitter.setSecondComponent(treeEditorComponent);
168 final JPanel innerPanel = createBottomControl(module);
169 if (innerPanel != null) {
170 mainPanel.add(innerPanel, BorderLayout.SOUTH);
173 final ContentEntry[] contentEntries = getModel().getContentEntries();
174 if (contentEntries.length > 0) {
175 for (final ContentEntry contentEntry : contentEntries) {
176 addContentEntryPanel(contentEntry.getUrl());
178 selectContentEntry(contentEntries[0].getUrl());
181 return mainPanel;
184 @Nullable
185 protected JPanel createBottomControl(Module module) {
186 return null;
189 protected abstract ContentEntryTreeEditor createContentEntryTreeEditor(Project project);
191 protected void addAdditionalSettingsToPanel(final JPanel mainPanel) {
194 protected Module getModule() {
195 return myModulesProvider.getModule(myModuleName);
198 protected void addContentEntryPanel(final String contentEntry) {
199 final ContentEntryEditor contentEntryEditor = createContentEntryEditor(contentEntry);
200 contentEntryEditor.initUI();
201 contentEntryEditor.addContentEntryEditorListener(myContentEntryEditorListener);
202 registerDisposable(new Disposable() {
203 public void dispose() {
204 contentEntryEditor.removeContentEntryEditorListener(myContentEntryEditorListener);
207 myEntryToEditorMap.put(contentEntry, contentEntryEditor);
208 Border border = BorderFactory.createEmptyBorder(2, 2, 0, 2);
209 final JComponent component = contentEntryEditor.getComponent();
210 final Border componentBorder = component.getBorder();
211 if (componentBorder != null) {
212 border = BorderFactory.createCompoundBorder(border, componentBorder);
214 component.setBorder(border);
215 myEditorsPanel.add(component);
218 protected abstract ContentEntryEditor createContentEntryEditor(String contentEntryUrl);
220 void selectContentEntry(final String contentEntryUrl) {
221 if (mySelectedEntryUrl != null && mySelectedEntryUrl.equals(contentEntryUrl)) {
222 return;
224 try {
225 if (mySelectedEntryUrl != null) {
226 ContentEntryEditor editor = myEntryToEditorMap.get(mySelectedEntryUrl);
227 if (editor != null) {
228 editor.setSelected(false);
232 if (contentEntryUrl != null) {
233 ContentEntryEditor editor = myEntryToEditorMap.get(contentEntryUrl);
234 if (editor != null) {
235 editor.setSelected(true);
236 final JComponent component = editor.getComponent();
237 final JComponent scroller = (JComponent)component.getParent();
238 SwingUtilities.invokeLater(new Runnable() {
239 public void run() {
240 scroller.scrollRectToVisible(component.getBounds());
243 myRootTreeEditor.setContentEntryEditor(editor);
244 myRootTreeEditor.requestFocus();
248 finally {
249 mySelectedEntryUrl = contentEntryUrl;
253 public void moduleStateChanged() {
254 if (myRootTreeEditor != null) { //in order to update exclude output root if it is under content root
255 myRootTreeEditor.update();
259 @Nullable
260 private String getNextContentEntry(final String contentEntryUrl) {
261 return getAdjacentContentEntry(contentEntryUrl, 1);
264 @Nullable
265 private String getAdjacentContentEntry(final String contentEntryUrl, int delta) {
266 final ContentEntry[] contentEntries = getModel().getContentEntries();
267 for (int idx = 0; idx < contentEntries.length; idx++) {
268 ContentEntry entry = contentEntries[idx];
269 if (contentEntryUrl.equals(entry.getUrl())) {
270 int nextEntryIndex = (idx + delta) % contentEntries.length;
271 if (nextEntryIndex < 0) {
272 nextEntryIndex += contentEntries.length;
274 return nextEntryIndex == idx ? null : contentEntries[nextEntryIndex].getUrl();
277 return null;
280 protected List<ContentEntry> addContentEntries(final VirtualFile[] files) {
281 List<ContentEntry> contentEntries = new ArrayList<ContentEntry>();
282 for (final VirtualFile file : files) {
283 if (isAlreadyAdded(file)) {
284 continue;
286 final ContentEntry contentEntry = getModel().addContentEntry(file);
287 contentEntries.add(contentEntry);
289 return contentEntries;
292 private boolean isAlreadyAdded(VirtualFile file) {
293 final VirtualFile[] contentRoots = getModel().getContentRoots();
294 for (VirtualFile contentRoot : contentRoots) {
295 if (contentRoot.equals(file)) {
296 return true;
299 return false;
302 public void saveData() {
305 protected void addContentEntryPanels(ContentEntry[] contentEntriesArray) {
306 for (ContentEntry contentEntry : contentEntriesArray) {
307 addContentEntryPanel(contentEntry.getUrl());
309 myEditorsPanel.revalidate();
310 myEditorsPanel.repaint();
311 selectContentEntry(contentEntriesArray[contentEntriesArray.length - 1].getUrl());
314 private final class MyContentEntryEditorListener extends ContentEntryEditorListenerAdapter {
315 public void editingStarted(ContentEntryEditor editor) {
316 selectContentEntry(editor.getContentEntryUrl());
319 public void beforeEntryDeleted(ContentEntryEditor editor) {
320 final String entryUrl = editor.getContentEntryUrl();
321 if (mySelectedEntryUrl != null && mySelectedEntryUrl.equals(entryUrl)) {
322 myRootTreeEditor.setContentEntryEditor(null);
324 final String nextContentEntryUrl = getNextContentEntry(entryUrl);
325 removeContentEntryPanel(entryUrl);
326 selectContentEntry(nextContentEntryUrl);
327 editor.removeContentEntryEditorListener(this);
330 public void navigationRequested(ContentEntryEditor editor, VirtualFile file) {
331 if (mySelectedEntryUrl != null && mySelectedEntryUrl.equals(editor.getContentEntryUrl())) {
332 myRootTreeEditor.requestFocus();
333 myRootTreeEditor.select(file);
335 else {
336 selectContentEntry(editor.getContentEntryUrl());
337 myRootTreeEditor.requestFocus();
338 myRootTreeEditor.select(file);
342 private void removeContentEntryPanel(final String contentEntryUrl) {
343 ContentEntryEditor editor = myEntryToEditorMap.get(contentEntryUrl);
344 if (editor != null) {
345 myEditorsPanel.remove(editor.getComponent());
346 myEntryToEditorMap.remove(contentEntryUrl);
347 myEditorsPanel.revalidate();
348 myEditorsPanel.repaint();
353 private class AddContentEntryAction extends IconWithTextAction implements DumbAware {
354 private final FileChooserDescriptor myDescriptor;
356 public AddContentEntryAction() {
357 super(ProjectBundle.message("module.paths.add.content.action"),
358 ProjectBundle.message("module.paths.add.content.action.description"), ADD_CONTENT_ENTRY_ICON);
359 myDescriptor = new FileChooserDescriptor(false, true, true, false, true, true) {
360 public void validateSelectedFiles(VirtualFile[] files) throws Exception {
361 validateContentEntriesCandidates(files);
364 myDescriptor.putUserData(LangDataKeys.MODULE_CONTEXT, getModule());
365 myDescriptor.setTitle(ProjectBundle.message("module.paths.add.content.title"));
366 myDescriptor.setDescription(ProjectBundle.message("module.paths.add.content.prompt"));
369 public void actionPerformed(AnActionEvent e) {
370 VirtualFile[] files = FileChooser.chooseFiles(myProject, myDescriptor, myLastSelectedDir);
371 if (files.length > 0) {
372 myLastSelectedDir = files[0];
373 addContentEntries(files);
377 @Nullable
378 private ContentEntry getContentEntry(final String url) {
379 final ContentEntry[] entries = getModel().getContentEntries();
380 for (final ContentEntry entry : entries) {
381 if (entry.getUrl().equals(url)) return entry;
384 return null;
387 private void validateContentEntriesCandidates(VirtualFile[] files) throws Exception {
388 for (final VirtualFile file : files) {
389 // check for collisions with already existing entries
390 for (final String contentEntryUrl : myEntryToEditorMap.keySet()) {
391 final ContentEntry contentEntry = getContentEntry(contentEntryUrl);
392 if (contentEntry == null) continue;
393 final VirtualFile contentEntryFile = contentEntry.getFile();
394 if (contentEntryFile == null) {
395 continue; // skip invalid entry
397 if (contentEntryFile.equals(file)) {
398 throw new Exception(ProjectBundle.message("module.paths.add.content.already.exists.error", file.getPresentableUrl()));
400 if (VfsUtil.isAncestor(contentEntryFile, file, true)) {
401 // intersection not allowed
402 throw new Exception(
403 ProjectBundle.message("module.paths.add.content.intersect.error", file.getPresentableUrl(),
404 contentEntryFile.getPresentableUrl()));
406 if (VfsUtil.isAncestor(file, contentEntryFile, true)) {
407 // intersection not allowed
408 throw new Exception(
409 ProjectBundle.message("module.paths.add.content.dominate.error", file.getPresentableUrl(),
410 contentEntryFile.getPresentableUrl()));
413 // check if the same root is configured for another module
414 final Module[] modules = myModulesProvider.getModules();
415 for (final Module module : modules) {
416 if (myModuleName.equals(module.getName())) {
417 continue;
419 ModuleRootModel rootModel = myModulesProvider.getRootModel(module);
420 LOG.assertTrue(rootModel != null);
421 final VirtualFile[] moduleContentRoots = rootModel.getContentRoots();
422 for (VirtualFile moduleContentRoot : moduleContentRoots) {
423 if (file.equals(moduleContentRoot)) {
424 throw new Exception(
425 ProjectBundle.message("module.paths.add.content.duplicate.error", file.getPresentableUrl(), module.getName()));