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
;
49 import javax
.swing
.border
.Border
;
51 import java
.awt
.event
.KeyEvent
;
52 import java
.util
.ArrayList
;
53 import java
.util
.HashMap
;
54 import java
.util
.List
;
58 * @author Eugene Zhuravlev
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
) {
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
);
97 final VirtualFileManagerEx fileManager
= ((VirtualFileManagerEx
)VirtualFileManager
.getInstance());
98 fileManager
.addVirtualFileManagerListener(fileManagerListener
);
99 registerDisposable(new Disposable() {
100 public void dispose() {
101 fileManager
.removeVirtualFileManagerListener(fileManagerListener
);
107 protected ModifiableRootModel
getModel() {
108 return myState
.getRootModel();
111 public String
getHelpTopic() {
112 return "projectStructure.modules.sources";
115 public String
getDisplayName() {
119 public Icon
getIcon() {
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
);
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());
185 protected JPanel
createBottomControl(Module module
) {
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
)) {
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() {
240 scroller
.scrollRectToVisible(component
.getBounds());
243 myRootTreeEditor
.setContentEntryEditor(editor
);
244 myRootTreeEditor
.requestFocus();
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();
260 private String
getNextContentEntry(final String contentEntryUrl
) {
261 return getAdjacentContentEntry(contentEntryUrl
, 1);
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();
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
)) {
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
)) {
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
);
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
);
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
;
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
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
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())) {
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
)) {
425 ProjectBundle
.message("module.paths.add.content.duplicate.error", file
.getPresentableUrl(), module
.getName()));