be56b7fb0ab9398c48a2e44f2d5d2fc8f9379b73
[idea/community.git] / java / idea-ui / src / com / intellij / openapi / roots / ui / configuration / libraryEditor / LibraryRootsComponent.java
1 // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.openapi.roots.ui.configuration.libraryEditor;
3
4 import com.intellij.CommonBundle;
5 import com.intellij.icons.AllIcons;
6 import com.intellij.ide.DataManager;
7 import com.intellij.ide.JavaUiBundle;
8 import com.intellij.ide.util.treeView.AbstractTreeStructure;
9 import com.intellij.ide.util.treeView.NodeDescriptor;
10 import com.intellij.openapi.Disposable;
11 import com.intellij.openapi.actionSystem.*;
12 import com.intellij.openapi.application.ApplicationManager;
13 import com.intellij.openapi.fileChooser.FileChooser;
14 import com.intellij.openapi.fileChooser.FileChooserDescriptor;
15 import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory;
16 import com.intellij.openapi.module.Module;
17 import com.intellij.openapi.project.DumbAwareAction;
18 import com.intellij.openapi.project.Project;
19 import com.intellij.openapi.roots.OrderRootType;
20 import com.intellij.openapi.roots.PersistentOrderRootType;
21 import com.intellij.openapi.roots.ProjectModelExternalSource;
22 import com.intellij.openapi.roots.libraries.LibraryKind;
23 import com.intellij.openapi.roots.libraries.LibraryProperties;
24 import com.intellij.openapi.roots.libraries.LibraryType;
25 import com.intellij.openapi.roots.libraries.ui.*;
26 import com.intellij.openapi.roots.libraries.ui.impl.RootDetectionUtil;
27 import com.intellij.openapi.roots.ui.configuration.ModificationOfImportedModelWarningComponent;
28 import com.intellij.openapi.roots.ui.configuration.libraries.LibraryPresentationManager;
29 import com.intellij.openapi.ui.ex.MultiLineLabel;
30 import com.intellij.openapi.ui.popup.JBPopupFactory;
31 import com.intellij.openapi.util.Computable;
32 import com.intellij.openapi.vfs.*;
33 import com.intellij.openapi.wm.IdeFocusManager;
34 import com.intellij.ui.AnActionButton;
35 import com.intellij.ui.AnActionButtonRunnable;
36 import com.intellij.ui.ToolbarDecorator;
37 import com.intellij.ui.tree.AsyncTreeModel;
38 import com.intellij.ui.tree.StructureTreeModel;
39 import com.intellij.ui.treeStructure.Tree;
40 import com.intellij.util.ArrayUtil;
41 import com.intellij.util.IconUtil;
42 import com.intellij.util.containers.ContainerUtil;
43 import com.intellij.util.containers.FilteringIterator;
44 import com.intellij.util.ui.JBUI;
45 import com.intellij.util.ui.tree.TreeModelAdapter;
46 import com.intellij.util.ui.tree.TreeUtil;
47 import org.jetbrains.annotations.NotNull;
48 import org.jetbrains.annotations.Nullable;
49
50 import javax.swing.*;
51 import javax.swing.event.TreeModelEvent;
52 import javax.swing.tree.DefaultMutableTreeNode;
53 import javax.swing.tree.TreePath;
54 import java.awt.*;
55 import java.util.List;
56 import java.util.*;
57
58 /**
59  * @author Eugene Zhuravlev
60  */
61 public class LibraryRootsComponent implements Disposable, LibraryEditorComponent {
62   static final UrlComparator ourUrlComparator = new UrlComparator();
63
64   private JPanel myPanel;
65   private JPanel myTreePanel;
66   private MultiLineLabel myPropertiesLabel;
67   private JPanel myPropertiesPanel;
68   private JPanel myBottomPanel;
69   private LibraryPropertiesEditor myPropertiesEditor;
70   private Tree myTree;
71   private final ModificationOfImportedModelWarningComponent myModificationOfImportedModelWarningComponent;
72   private VirtualFile myLastChosen;
73
74   private final Collection<Runnable> myListeners = ContainerUtil.createLockFreeCopyOnWriteList();
75   @Nullable private final Project myProject;
76
77   private final Computable<? extends LibraryEditor> myLibraryEditorComputable;
78   private LibraryRootsComponentDescriptor myDescriptor;
79   private Module myContextModule;
80   private LibraryRootsComponent.AddExcludedRootActionButton myAddExcludedRootActionButton;
81   private StructureTreeModel<AbstractTreeStructure> myTreeModel;
82   private LibraryRootsComponentDescriptor.RootRemovalHandler myRootRemovalHandler;
83
84   public LibraryRootsComponent(@Nullable Project project, @NotNull LibraryEditor libraryEditor) {
85     this(project, new Computable.PredefinedValueComputable<>(libraryEditor));
86   }
87
88   public LibraryRootsComponent(@Nullable Project project, @NotNull Computable<? extends LibraryEditor> libraryEditorComputable) {
89     myProject = project;
90     myLibraryEditorComputable = libraryEditorComputable;
91     final LibraryEditor editor = getLibraryEditor();
92     final LibraryType type = editor.getType();
93     if (type != null) {
94       myDescriptor = type.createLibraryRootsComponentDescriptor();
95       //noinspection unchecked
96       myPropertiesEditor = type.createPropertiesEditor(this);
97       if (myPropertiesEditor != null) {
98         myPropertiesPanel.add(myPropertiesEditor.createComponent(), BorderLayout.CENTER);
99       }
100     }
101     if (myDescriptor == null) {
102       myDescriptor = new DefaultLibraryRootsComponentDescriptor();
103     }
104     myRootRemovalHandler = myDescriptor.createRootRemovalHandler();
105     myModificationOfImportedModelWarningComponent = new ModificationOfImportedModelWarningComponent();
106     myBottomPanel.add(BorderLayout.CENTER, myModificationOfImportedModelWarningComponent.getLabel());
107     init(new LibraryTreeStructure(this, myDescriptor));
108     updatePropertiesLabel();
109     onRootsChanged();
110   }
111
112   private void onRootsChanged() {
113     myAddExcludedRootActionButton.setEnabled(!getNotExcludedRoots().isEmpty());
114   }
115
116   @NotNull
117   @Override
118   public LibraryProperties getProperties() {
119     return getLibraryEditor().getProperties();
120   }
121
122   @Override
123   public boolean isNewLibrary() {
124     return getLibraryEditor() instanceof NewLibraryEditor;
125   }
126
127   public void updatePropertiesLabel() {
128     StringBuilder text = new StringBuilder();
129     final LibraryType<?> type = getLibraryEditor().getType();
130     final Set<LibraryKind> excluded =
131       type != null ? Collections.singleton(type.getKind()) : Collections.emptySet();
132     for (String description : LibraryPresentationManager.getInstance().getDescriptions(getLibraryEditor().getFiles(OrderRootType.CLASSES),
133                                                                                        excluded)) {
134       if (text.length() > 0) {
135         text.append("\n");
136       }
137       text.append(description);
138     }
139     myPropertiesLabel.setText(text.toString());
140   }
141
142   private void init(AbstractTreeStructure treeStructure) {
143     myPropertiesLabel.setBorder(JBUI.Borders.empty(0, 10));
144     myTreeModel = new StructureTreeModel<>(treeStructure, this);
145     AsyncTreeModel asyncTreeModel = new AsyncTreeModel(myTreeModel, this);
146     asyncTreeModel.addTreeModelListener(new TreeModelAdapter() {
147       @Override
148       public void treeNodesInserted(TreeModelEvent event) {
149         Object[] childNodes = event.getChildren();
150         if (childNodes != null) {
151           for (Object childNode : childNodes) {
152             LibraryTableTreeContentElement element = TreeUtil.getUserObject(LibraryTableTreeContentElement.class, childNode);
153             if (element != null && myTree != null) {
154               myTreeModel.expand(element, myTree, path -> { });
155             }
156           }
157         }
158       }
159     });
160     myTree = new Tree(asyncTreeModel);
161     myTree.setRootVisible(false);
162     myTree.setShowsRootHandles(true);
163     new LibraryRootsTreeSpeedSearch(myTree);
164     myTree.setCellRenderer(new LibraryTreeRenderer());
165     myTreePanel.setLayout(new BorderLayout());
166
167     ToolbarDecorator toolbarDecorator = ToolbarDecorator.createDecorator(myTree).disableUpDownActions()
168       .setPanelBorder(JBUI.Borders.empty())
169       .setRemoveActionName(JavaUiBundle.message("library.remove.action"))
170       .disableRemoveAction();
171     final List<AttachRootButtonDescriptor> popupItems = new ArrayList<>();
172     for (AttachRootButtonDescriptor descriptor : myDescriptor.createAttachButtons()) {
173       Icon icon = descriptor.getToolbarIcon();
174       if (icon != null) {
175         AttachItemAction action = new AttachItemAction(descriptor, descriptor.getButtonText(), icon);
176         toolbarDecorator.addExtraAction(AnActionButton.fromAction(action));
177       }
178       else {
179         popupItems.add(descriptor);
180       }
181     }
182     myAddExcludedRootActionButton = new AddExcludedRootActionButton();
183     toolbarDecorator.addExtraAction(myAddExcludedRootActionButton);
184     toolbarDecorator.addExtraAction(new AnActionButton(JavaUiBundle.messagePointer("action.AnActionButton.text.remove"), IconUtil.getRemoveIcon()) {
185       @Override
186       public void actionPerformed(@NotNull AnActionEvent e) {
187         final List<Object> selectedElements = getSelectedElements();
188         if (selectedElements.isEmpty()) {
189           return;
190         }
191
192         ApplicationManager.getApplication().runWriteAction(() -> {
193           for (Object selectedElement : selectedElements) {
194             LibraryEditor libraryEditor = getLibraryEditor();
195             if (selectedElement instanceof ItemElement) {
196               final ItemElement itemElement = (ItemElement)selectedElement;
197               libraryEditor.removeRoot(itemElement.getUrl(), itemElement.getRootType());
198               myRootRemovalHandler.onRootRemoved(itemElement.getUrl(), itemElement.getRootType(), libraryEditor);
199             }
200             else if (selectedElement instanceof OrderRootTypeElement) {
201               final OrderRootType rootType = ((OrderRootTypeElement)selectedElement).getOrderRootType();
202               final String[] urls = libraryEditor.getUrls(rootType);
203               for (String url : urls) {
204                 libraryEditor.removeRoot(url, rootType);
205               }
206             }
207             else if (selectedElement instanceof ExcludedRootElement) {
208               libraryEditor.removeExcludedRoot(((ExcludedRootElement)selectedElement).getUrl());
209             }
210           }
211         });
212         libraryChanged(true);
213       }
214
215       @Override
216       public void updateButton(@NotNull AnActionEvent e) {
217         super.updateButton(e);
218         Presentation presentation = e.getPresentation();
219         if (ContainerUtil.and(getSelectedElements(), new FilteringIterator.InstanceOf<>(ExcludedRootElement.class))) {
220           presentation.setText(JavaUiBundle.message("action.text.cancel.exclusion"));
221         }
222         else {
223           presentation.setText(getTemplatePresentation().getText());
224         }
225       }
226
227       @Override
228       public ShortcutSet getShortcut() {
229         return CommonShortcuts.getDelete();
230       }
231     });
232     toolbarDecorator.setAddAction(new AnActionButtonRunnable() {
233       @Override
234       public void run(AnActionButton button) {
235         AttachFilesAction attachFilesAction = new AttachFilesAction(myDescriptor.getAttachFilesActionName());
236         if (popupItems.isEmpty()) {
237           attachFilesAction.perform();
238           return;
239         }
240
241         List<AnAction> actions = new ArrayList<>();
242         actions.add(attachFilesAction);
243         for (AttachRootButtonDescriptor descriptor : popupItems) {
244           actions.add(new AttachItemAction(descriptor, descriptor.getButtonText(), null));
245         }
246         final DefaultActionGroup group = new DefaultActionGroup(actions);
247         JBPopupFactory.getInstance().createActionGroupPopup(null, group,
248                                                             DataManager.getInstance().getDataContext(button.getContextComponent()),
249                                                             JBPopupFactory.ActionSelectionAid.SPEEDSEARCH, true)
250           .show(button.getPreferredPopupPoint());
251       }
252     });
253
254     myTreePanel.add(toolbarDecorator.createPanel(), BorderLayout.CENTER);
255   }
256
257   public JComponent getComponent() {
258     return myPanel;
259   }
260
261   @Override
262   @Nullable
263   public Project getProject() {
264     return myProject;
265   }
266
267   public void setContextModule(Module module) {
268     myContextModule = module;
269   }
270
271   @Override
272   @Nullable
273   public VirtualFile getExistingRootDirectory() {
274     for (OrderRootType orderRootType : OrderRootType.getAllPersistentTypes()) {
275       final VirtualFile[] existingRoots = getLibraryEditor().getFiles(orderRootType);
276       if (existingRoots.length > 0) {
277         VirtualFile existingRoot = existingRoots[0];
278         if (existingRoot.getFileSystem() instanceof JarFileSystem) {
279           existingRoot = JarFileSystem.getInstance().getVirtualFileForJar(existingRoot);
280         }
281         if (existingRoot != null) {
282           if (existingRoot.isDirectory()) {
283             return existingRoot;
284           }
285           else {
286             return existingRoot.getParent();
287           }
288         }
289       }
290     }
291     return null;
292   }
293
294   @Override
295   @Nullable
296   public VirtualFile getBaseDirectory() {
297     if (myProject != null) {
298       //todo[nik] perhaps we shouldn't select project base dir if global library is edited
299       return myProject.getBaseDir();
300     }
301     return null;
302   }
303
304   @Override
305   public LibraryEditor getLibraryEditor() {
306     return myLibraryEditorComputable.compute();
307   }
308
309   public boolean hasChanges() {
310     if (myPropertiesEditor != null && myPropertiesEditor.isModified()) {
311       return true;
312     }
313     return getLibraryEditor().hasChanges();
314   }
315
316   @NotNull
317   private List<Object> getSelectedElements() {
318     final TreePath[] selectionPaths = myTree.getSelectionPaths();
319     if (selectionPaths == null) {
320       return Collections.emptyList();
321     }
322
323     List<Object> elements = new ArrayList<>();
324     for (TreePath selectionPath : selectionPaths) {
325       final Object pathElement = getPathElement(selectionPath);
326       if (pathElement != null) {
327         elements.add(pathElement);
328       }
329     }
330     return elements;
331   }
332
333   public void onLibraryRenamed() {
334     updateModificationOfImportedModelWarning();
335   }
336
337   @Nullable
338   private static Object getPathElement(final TreePath selectionPath) {
339     if (selectionPath == null) {
340       return null;
341     }
342     final DefaultMutableTreeNode lastPathComponent = (DefaultMutableTreeNode)selectionPath.getLastPathComponent();
343     if (lastPathComponent == null) {
344       return null;
345     }
346     final Object userObject = lastPathComponent.getUserObject();
347     if (!(userObject instanceof NodeDescriptor)) {
348       return null;
349     }
350     final Object element = ((NodeDescriptor)userObject).getElement();
351     if (!(element instanceof LibraryTableTreeContentElement)) {
352       return null;
353     }
354     return element;
355   }
356
357   @Override
358   public void renameLibrary(String newName) {
359     final LibraryEditor libraryEditor = getLibraryEditor();
360     libraryEditor.setName(newName);
361     libraryChanged(false);
362   }
363
364   @Override
365   public void dispose() {
366     if (myPropertiesEditor != null) {
367       myPropertiesEditor.disposeUIResources();
368     }
369   }
370
371   public void resetProperties() {
372     if (myPropertiesEditor != null) {
373       myPropertiesEditor.reset();
374     }
375   }
376
377   public void applyProperties() {
378     if (myPropertiesEditor != null && myPropertiesEditor.isModified()) {
379       myPropertiesEditor.apply();
380     }
381   }
382
383   @Override
384   public void updateRootsTree() {
385     myTreeModel.invalidate();
386   }
387
388   @Nullable
389   private VirtualFile getFileToSelect() {
390     if (myLastChosen != null) {
391       return myLastChosen;
392     }
393
394     final VirtualFile directory = getExistingRootDirectory();
395     if (directory != null) {
396       return directory;
397     }
398     return getBaseDirectory();
399   }
400
401   private class AttachFilesAction extends AttachItemActionBase {
402     AttachFilesAction(String title) {
403       super(title);
404     }
405
406     @Override
407     protected List<OrderRoot> selectRoots(@Nullable VirtualFile initialSelection) {
408       final String name = getLibraryEditor().getName();
409       final FileChooserDescriptor chooserDescriptor = myDescriptor.createAttachFilesChooserDescriptor(name);
410       if (myContextModule != null) {
411         chooserDescriptor.putUserData(LangDataKeys.MODULE_CONTEXT, myContextModule);
412       }
413       final VirtualFile[] files = FileChooser.chooseFiles(chooserDescriptor, myPanel, myProject, initialSelection);
414       if (files.length == 0) return Collections.emptyList();
415
416       return RootDetectionUtil.detectRoots(Arrays.asList(files), myPanel, myProject, myDescriptor);
417     }
418   }
419
420   public abstract class AttachItemActionBase extends DumbAwareAction {
421     protected AttachItemActionBase(String text) {
422       super(text);
423     }
424
425     @Override
426     public void actionPerformed(@NotNull AnActionEvent e) {
427       perform();
428     }
429
430     void perform() {
431       VirtualFile toSelect = getFileToSelect();
432       List<OrderRoot> roots = selectRoots(toSelect);
433       if (roots.isEmpty()) return;
434
435       final List<OrderRoot> attachedRoots = attachFiles(roots);
436       final OrderRoot first = ContainerUtil.getFirstItem(attachedRoots);
437       if (first != null) {
438         myLastChosen = first.getFile();
439       }
440       fireLibraryChanged();
441       IdeFocusManager.getGlobalInstance().doWhenFocusSettlesDown(() -> IdeFocusManager.getGlobalInstance().requestFocus(myTree, true));
442     }
443
444     protected abstract List<OrderRoot> selectRoots(@Nullable VirtualFile initialSelection);
445   }
446
447   private class AttachItemAction extends AttachItemActionBase {
448     private final AttachRootButtonDescriptor myDescriptor;
449
450     protected AttachItemAction(AttachRootButtonDescriptor descriptor, String title, final Icon icon) {
451       super(title);
452       getTemplatePresentation().setIcon(icon);
453       myDescriptor = descriptor;
454     }
455
456     @Override
457     protected List<OrderRoot> selectRoots(@Nullable VirtualFile initialSelection) {
458       final VirtualFile[] files = myDescriptor.selectFiles(myPanel, initialSelection, myContextModule, getLibraryEditor());
459       if (files.length == 0) return Collections.emptyList();
460
461       List<OrderRoot> roots = new ArrayList<>();
462       for (VirtualFile file : myDescriptor.scanForActualRoots(files, myPanel)) {
463         roots.add(new OrderRoot(file, myDescriptor.getRootType(), myDescriptor.addAsJarDirectories()));
464       }
465       return roots;
466     }
467   }
468
469   private List<OrderRoot> attachFiles(List<? extends OrderRoot> roots) {
470     final List<OrderRoot> rootsToAttach = filterAlreadyAdded(roots);
471     if (!rootsToAttach.isEmpty()) {
472       ApplicationManager.getApplication().runWriteAction(() -> getLibraryEditor().addRoots(rootsToAttach));
473       updatePropertiesLabel();
474       onRootsChanged();
475       myTreeModel.invalidate();
476     }
477     return rootsToAttach;
478   }
479
480   private List<OrderRoot> filterAlreadyAdded(@NotNull List<? extends OrderRoot> roots) {
481     List<OrderRoot> result = new ArrayList<>();
482     for (OrderRoot root : roots) {
483       final VirtualFile[] libraryFiles = getLibraryEditor().getFiles(root.getType());
484       if (!ArrayUtil.contains(root.getFile(), libraryFiles)) {
485         result.add(root);
486       }
487     }
488     return result;
489   }
490
491   private void libraryChanged(boolean putFocusIntoTree) {
492     onRootsChanged();
493     updatePropertiesLabel();
494     myTreeModel.invalidate();
495     if (putFocusIntoTree) {
496       IdeFocusManager.getGlobalInstance().doWhenFocusSettlesDown(() -> IdeFocusManager.getGlobalInstance().requestFocus(myTree, true));
497     }
498     fireLibraryChanged();
499   }
500
501   private void fireLibraryChanged() {
502     for (Runnable listener : myListeners) {
503       listener.run();
504     }
505     updateModificationOfImportedModelWarning();
506   }
507
508   private void updateModificationOfImportedModelWarning() {
509     LibraryEditor libraryEditor = getLibraryEditor();
510     ProjectModelExternalSource externalSource = libraryEditor.getExternalSource();
511     if (externalSource != null && hasChanges()) {
512       String name = libraryEditor instanceof ExistingLibraryEditor ? ((ExistingLibraryEditor)libraryEditor).getLibrary().getName() : libraryEditor.getName();
513       myModificationOfImportedModelWarningComponent.showWarning(name != null ? "Library '" + name + "'" : "Library", externalSource);
514     }
515     else {
516       myModificationOfImportedModelWarningComponent.hideWarning();
517     }
518   }
519
520   public void addListener(Runnable listener) {
521     myListeners.add(listener);
522   }
523
524   public void removeListener(Runnable listener) {
525     myListeners.remove(listener);
526   }
527
528   private Set<VirtualFile> getNotExcludedRoots() {
529     Set<VirtualFile> roots = new LinkedHashSet<>();
530     String[] excludedRootUrls = getLibraryEditor().getExcludedRootUrls();
531     Set<VirtualFile> excludedRoots = new HashSet<>();
532     for (String url : excludedRootUrls) {
533       ContainerUtil.addIfNotNull(excludedRoots, VirtualFileManager.getInstance().findFileByUrl(url));
534     }
535     for (PersistentOrderRootType type : OrderRootType.getAllPersistentTypes()) {
536       VirtualFile[] files = getLibraryEditor().getFiles(type);
537       for (VirtualFile file : files) {
538         if (!VfsUtilCore.isUnder(file, excludedRoots)) {
539           roots.add(VfsUtil.getLocalFile(file));
540         }
541       }
542     }
543     return roots;
544   }
545
546   private class AddExcludedRootActionButton extends AnActionButton {
547     AddExcludedRootActionButton() {
548       super(CommonBundle.messagePointer("button.exclude"), Presentation.NULL_STRING, AllIcons.Modules.AddExcludedRoot);
549     }
550
551     @Override
552     public void actionPerformed(@NotNull AnActionEvent e) {
553       FileChooserDescriptor descriptor = FileChooserDescriptorFactory.createMultipleJavaPathDescriptor();
554       descriptor.setTitle(JavaUiBundle.message("chooser.title.exclude.from.library"));
555       descriptor.setDescription(JavaUiBundle.message(
556         "chooser.description.select.directories.which.should.be.excluded.from.the.library.content"));
557       Set<VirtualFile> roots = getNotExcludedRoots();
558       descriptor.setRoots(roots.toArray(VirtualFile.EMPTY_ARRAY));
559       if (roots.size() < 2) {
560         descriptor.withTreeRootVisible(true);
561       }
562       VirtualFile toSelect = null;
563       for (Object o : getSelectedElements()) {
564         Object itemElement = o instanceof ExcludedRootElement ? ((ExcludedRootElement)o).getParentDescriptor() : o;
565         if (itemElement instanceof ItemElement) {
566           toSelect = VirtualFileManager.getInstance().findFileByUrl(((ItemElement)itemElement).getUrl());
567           break;
568         }
569       }
570       final VirtualFile[] files = FileChooser.chooseFiles(descriptor, myPanel, myProject, toSelect);
571       if (files.length > 0) {
572         ApplicationManager.getApplication().runWriteAction(() -> {
573           for (VirtualFile file : files) {
574             getLibraryEditor().addExcludedRoot(file.getUrl());
575           }
576         });
577         myLastChosen = files[0];
578         libraryChanged(true);
579       }
580     }
581   }
582 }