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