many template folders per module in django (PY-595)
[idea/community.git] / platform / lang-impl / src / com / intellij / openapi / projectRoots / ui / PathEditor.java
1 /*
2  * Copyright 2000-2009 JetBrains s.r.o.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16 package com.intellij.openapi.projectRoots.ui;
17
18 import com.intellij.openapi.application.ApplicationManager;
19 import com.intellij.openapi.diagnostic.Logger;
20 import com.intellij.openapi.fileChooser.FileChooser;
21 import com.intellij.openapi.fileChooser.FileChooserDescriptor;
22 import com.intellij.openapi.fileTypes.FileTypeManager;
23 import com.intellij.openapi.fileTypes.FileTypes;
24 import com.intellij.openapi.project.ProjectBundle;
25 import com.intellij.openapi.projectRoots.SdkModificator;
26 import com.intellij.openapi.roots.OrderRootType;
27 import com.intellij.openapi.util.Computable;
28 import com.intellij.openapi.util.IconLoader;
29 import com.intellij.openapi.vfs.JarFileSystem;
30 import com.intellij.openapi.vfs.LocalFileSystem;
31 import com.intellij.openapi.vfs.VfsUtil;
32 import com.intellij.openapi.vfs.VirtualFile;
33 import com.intellij.openapi.vfs.ex.http.HttpFileSystem;
34 import com.intellij.ui.ListUtil;
35 import com.intellij.ui.ScrollPaneFactory;
36 import com.intellij.ui.components.JBList;
37 import com.intellij.util.Icons;
38 import com.intellij.util.containers.HashSet;
39 import com.intellij.util.ui.UIUtil;
40 import gnu.trove.TIntArrayList;
41 import org.jetbrains.annotations.NotNull;
42 import org.jetbrains.annotations.Nullable;
43
44 import javax.swing.*;
45 import javax.swing.event.ListSelectionEvent;
46 import javax.swing.event.ListSelectionListener;
47 import java.awt.*;
48 import java.awt.event.ActionEvent;
49 import java.awt.event.ActionListener;
50 import java.util.ArrayList;
51 import java.util.Arrays;
52 import java.util.List;
53 import java.util.Set;
54
55 /**
56  * @author MYakovlev
57  */
58 public class PathEditor {
59   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.projectRoots.ui.PathEditor");
60   public static final Color INVALID_COLOR = new Color(210, 0, 0);
61
62   protected JPanel myPanel;
63   private JButton myRemoveButton;
64   private JButton myAddButton;
65   private JButton mySpecifyUrlButton;
66   private JList myList;
67   private DefaultListModel myModel;
68   private final Set<VirtualFile> myAllFiles = new HashSet<VirtualFile>();
69   private boolean myModified = false;
70   private boolean myEnabled = false;
71   private static final Icon ICON_INVALID = IconLoader.getIcon("/nodes/ppInvalid.png");
72   private static final Icon ICON_EMPTY = IconLoader.getIcon("/nodes/emptyNode.png");
73   private final String myDisplayName;
74   private final OrderRootType myOrderRootType;
75   private final FileChooserDescriptor myDescriptor;
76
77   public PathEditor(final String displayName,
78                     final OrderRootType orderRootType,
79                     final FileChooserDescriptor descriptor) {
80     myDisplayName = displayName;
81     myOrderRootType = orderRootType;
82     myDescriptor = descriptor;
83   }
84
85   protected boolean isShowUrlButton() {
86     return false;
87   }
88
89   protected void onSpecifyUrlButtonClicked() {
90   }
91
92   public String getDisplayName() {
93     return myDisplayName;
94   }
95
96   protected void setModified(boolean modified) {
97     myModified = modified;
98   }
99
100   public boolean isModified() {
101     return myModified;
102   }
103
104   public void apply(SdkModificator sdkModificator) {
105     sdkModificator.removeRoots(myOrderRootType);
106     // add all items
107     for (int i = 0; i < getRowCount(); i++) {
108       sdkModificator.addRoot(getValueAt(i), myOrderRootType);
109     }
110     setModified(false);
111     updateButtons();
112   }
113
114   public VirtualFile[] getRoots() {
115     final int count = getRowCount();
116     if (count == 0) {
117       return VirtualFile.EMPTY_ARRAY;
118     }
119     final VirtualFile[] roots = new VirtualFile[count];
120     for (int i = 0; i < count; i++) {
121       roots[i] = getValueAt(i);
122     }
123     return roots;
124   }
125
126   public void setPaths(@NotNull List<VirtualFile> paths) {
127     keepSelectionState();
128     clearList();
129     myEnabled = true;
130     if (myEnabled) {
131       for (VirtualFile file : paths) {
132         addElement(file);
133       }
134     }
135     setModified(false);
136     updateButtons();
137   }
138
139   public void reset(@Nullable SdkModificator modificator) {
140     keepSelectionState();
141     clearList();
142     myEnabled = modificator != null;
143     if (myEnabled) {
144       VirtualFile[] files = modificator.getRoots(myOrderRootType);
145       for (VirtualFile file : files) {
146         addElement(file);
147       }
148     }
149     setModified(false);
150     updateButtons();
151   }
152
153   public JComponent createComponent() {
154     myPanel = new JPanel(new GridBagLayout());
155     Insets anInsets = new Insets(2, 2, 2, 2);
156
157     myModel = new DefaultListModel();
158     myList = new JBList(myModel);
159     myList.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
160       public void valueChanged(ListSelectionEvent e) {
161         updateButtons();
162       }
163     });
164     myList.setCellRenderer(new MyCellRenderer());
165
166     myRemoveButton = new JButton(ProjectBundle.message("button.remove"));
167     myAddButton = new JButton(ProjectBundle.message("button.add"));
168     mySpecifyUrlButton = new JButton(ProjectBundle.message("sdk.paths.specify.url.button"));
169
170     mySpecifyUrlButton.setVisible(isShowUrlButton());
171
172     myAddButton.addActionListener(new ActionListener() {
173       public void actionPerformed(ActionEvent e) {
174         final VirtualFile[] added = doAdd();
175         if (added.length > 0) {
176           setModified(true);
177         }
178         updateButtons();
179         requestDefaultFocus();
180         setSelectedRoots(added);
181       }
182     });
183     myRemoveButton.addActionListener(new ActionListener() {
184       public void actionPerformed(ActionEvent e) {
185         List removedItems = ListUtil.removeSelectedItems(myList);
186         itemsRemoved(removedItems);
187       }
188     });
189     mySpecifyUrlButton.addActionListener(new ActionListener() {
190       public void actionPerformed(ActionEvent e) {
191         onSpecifyUrlButtonClicked();
192       }
193     });
194
195     JScrollPane scrollPane = ScrollPaneFactory.createScrollPane(myList);
196     scrollPane.setPreferredSize(new Dimension(500, 500));
197     myPanel
198       .add(scrollPane, new GridBagConstraints(0, 0, 1, 8, 1.0, 1.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, anInsets, 0, 0));
199     myPanel.add(myAddButton,
200                 new GridBagConstraints(1, 0, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, anInsets, 0, 0));
201     myPanel.add(myRemoveButton,
202                 new GridBagConstraints(1, 1, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, anInsets, 0, 0));
203     myPanel.add(mySpecifyUrlButton,
204                 new GridBagConstraints(1, 4, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, anInsets, 0, 0));
205     myPanel.add(Box.createRigidArea(new Dimension(mySpecifyUrlButton.getPreferredSize().width, 4)),
206                 new GridBagConstraints(1, 5, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.NONE, anInsets, 0, 0));
207     return myPanel;
208   }
209
210   private void itemsRemoved(List removedItems) {
211     myAllFiles.removeAll(removedItems);
212     if (removedItems.size() > 0) {
213       setModified(true);
214     }
215     updateButtons();
216     requestDefaultFocus();
217   }
218
219   private VirtualFile[] doAdd() {
220     VirtualFile[] files = FileChooser.chooseFiles(myPanel, myDescriptor);
221     files = adjustAddedFileSet(myPanel, files);
222     List<VirtualFile> added = new ArrayList<VirtualFile>(files.length);
223     for (VirtualFile vFile : files) {
224       if (addElement(vFile)) {
225         added.add(vFile);
226       }
227     }
228     return VfsUtil.toVirtualFileArray(added);
229   }
230
231   /**
232    * Implement this method to adjust adding behavior, this method is called right after the files
233    * or directories are selected for added. This method allows adding UI that modify file set.
234    * <p/>
235    * The default implementation returns a value passed the parameter files and does nothing.
236    *
237    * @param component a component that could be used as a parent.
238    * @param files     a selected file set
239    * @return adjusted file set
240    */
241   protected VirtualFile[] adjustAddedFileSet(final Component component, final VirtualFile[] files) {
242     return files;
243   }
244
245   protected void updateButtons() {
246     Object[] values = getSelectedRoots();
247     myRemoveButton.setEnabled((values.length > 0) && myEnabled);
248     myAddButton.setEnabled(myEnabled);
249     mySpecifyUrlButton.setEnabled(myEnabled && !isUrlInserted());
250     mySpecifyUrlButton.setVisible(isShowUrlButton());
251   }
252
253   private boolean isUrlInserted() {
254     if (getRowCount() > 0) {
255       return ((VirtualFile)myModel.lastElement()).getFileSystem() instanceof HttpFileSystem;
256     }
257     return false;
258   }
259
260   protected void requestDefaultFocus() {
261     if (myList != null) {
262       myList.requestFocus();
263     }
264   }
265
266   public void addPaths(VirtualFile... paths) {
267     boolean added = false;
268     keepSelectionState();
269     for (final VirtualFile path : paths) {
270       if (addElement(path)) {
271         added = true;
272       }
273     }
274     if (added) {
275       setModified(true);
276       updateButtons();
277     }
278   }
279
280   public void removePaths(VirtualFile... paths) {
281     final Set<VirtualFile> pathsSet = new java.util.HashSet<VirtualFile>(Arrays.asList(paths));
282     int size = getRowCount();
283     final TIntArrayList indicesToRemove = new TIntArrayList(paths.length);
284     for (int idx = 0; idx < size; idx++) {
285       VirtualFile path = getValueAt(idx);
286       if (pathsSet.contains(path)) {
287         indicesToRemove.add(idx);
288       }
289     }
290     final List list = ListUtil.removeIndices(myList, indicesToRemove.toNativeArray());
291     itemsRemoved(list);
292   }
293
294   /**
295    * Method adds element only if it is not added yet.
296    */
297   protected boolean addElement(VirtualFile item) {
298     if (item == null) {
299       return false;
300     }
301     if (myAllFiles.contains(item)) {
302       return false;
303     }
304     if (isUrlInserted()) {
305       myModel.insertElementAt(item, myModel.size() - 1);
306     }
307     else {
308       myModel.addElement(item);
309     }
310     myAllFiles.add(item);
311     return true;
312   }
313
314   protected void setSelectedRoots(Object[] roots) {
315     ArrayList<Object> rootsList = new ArrayList<Object>(roots.length);
316     for (Object root : roots) {
317       if (root != null) {
318         rootsList.add(root);
319       }
320     }
321     myList.getSelectionModel().clearSelection();
322     int rowCount = myModel.getSize();
323     for (int i = 0; i < rowCount; i++) {
324       Object currObject = myModel.get(i);
325       LOG.assertTrue(currObject != null);
326       if (rootsList.contains(currObject)) {
327         myList.getSelectionModel().addSelectionInterval(i, i);
328       }
329     }
330   }
331
332   private void keepSelectionState() {
333     final Object[] selectedItems = getSelectedRoots();
334
335     SwingUtilities.invokeLater(new Runnable() {
336       public void run() {
337         if (selectedItems != null) {
338           setSelectedRoots(selectedItems);
339         }
340       }
341     });
342   }
343
344   protected Object[] getSelectedRoots() {
345     return myList.getSelectedValues();
346   }
347
348   private int getRowCount() {
349     return myModel.getSize();
350   }
351
352   private VirtualFile getValueAt(int row) {
353     return (VirtualFile)myModel.get(row);
354   }
355
356   public void clearList() {
357     myModel.clear();
358     myAllFiles.clear();
359     setModified(true);
360   }
361
362   private static boolean isJarFile(final VirtualFile file) {
363     return ApplicationManager.getApplication().runReadAction(new Computable<Boolean>() {
364       public Boolean compute() {
365         VirtualFile tempFile = file;
366         if ((file.getFileSystem() instanceof JarFileSystem) && file.getParent() == null) {
367           //[myakovlev] It was bug - directories with *.jar extensions was saved as files of JarFileSystem.
368           //    so we can not just return true, we should filter such directories.
369           String path = file.getPath().substring(0, file.getPath().length() - JarFileSystem.JAR_SEPARATOR.length());
370           tempFile = LocalFileSystem.getInstance().findFileByPath(path);
371         }
372         if (tempFile != null && !tempFile.isDirectory()) {
373           return Boolean.valueOf(FileTypeManager.getInstance().getFileTypeByFile(tempFile).equals(FileTypes.ARCHIVE));
374         }
375         return Boolean.FALSE;
376
377       }
378     }).booleanValue();
379   }
380
381   /**
382    * @return icon for displaying parameter (ProjectRoot or VirtualFile)
383    *         If parameter is not ProjectRoot or VirtualFile, returns empty icon "/nodes/emptyNode.png"
384    */
385   private static Icon getIconForRoot(Object projectRoot) {
386     if (projectRoot instanceof VirtualFile) {
387       final VirtualFile file = (VirtualFile)projectRoot;
388       if (!file.isValid()) {
389         return ICON_INVALID;
390       }
391       else if (isHttpRoot(file)) {
392         return Icons.WEB_ICON;
393       }
394       else {
395         return isJarFile(file) ? Icons.JAR_ICON : Icons.FILE_ICON;
396       }
397     }
398     return ICON_EMPTY;
399   }
400
401   private static boolean isHttpRoot(VirtualFile virtualFileOrProjectRoot) {
402     if (virtualFileOrProjectRoot != null) {
403       return (virtualFileOrProjectRoot.getFileSystem() instanceof HttpFileSystem);
404     }
405     return false;
406   }
407
408   private final class MyCellRenderer extends DefaultListCellRenderer {
409     private String getPresentableString(final Object value) {
410       return ApplicationManager.getApplication().runReadAction(new Computable<String>() {
411         public String compute() {
412           //noinspection HardCodedStringLiteral
413           return (value instanceof VirtualFile) ? ((VirtualFile)value).getPresentableUrl() : "UNKNOWN OBJECT";
414         }
415       });
416     }
417
418     public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
419       super.getListCellRendererComponent(list, getPresentableString(value), index, isSelected, cellHasFocus);
420       if (isSelected) {
421         setForeground(UIUtil.getListSelectionForeground());
422       }
423       else {
424         if (value instanceof VirtualFile) {
425           VirtualFile file = (VirtualFile)value;
426           if (!file.isValid()) {
427             setForeground(INVALID_COLOR);
428           }
429         }
430       }
431       setIcon(getIconForRoot(value));
432       return this;
433     }
434   }
435 }