563f684639ff5e4c64c7eba259d20afe030deba4
[idea/community.git] / java / idea-ui / src / com / intellij / ide / util / importProject / ProjectLayoutPanel.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.ide.util.importProject;
3
4 import com.intellij.CommonBundle;
5 import com.intellij.icons.AllIcons;
6 import com.intellij.ide.JavaUiBundle;
7 import com.intellij.ide.util.ElementsChooser;
8 import com.intellij.ide.util.projectWizard.importSources.DetectedProjectRoot;
9 import com.intellij.openapi.actionSystem.*;
10 import com.intellij.openapi.ui.DialogWrapper;
11 import com.intellij.openapi.ui.InputValidator;
12 import com.intellij.openapi.ui.Messages;
13 import com.intellij.openapi.ui.Splitter;
14 import com.intellij.openapi.ui.ex.MultiLineLabel;
15 import com.intellij.openapi.util.NlsContexts;
16 import com.intellij.openapi.util.text.StringUtil;
17 import com.intellij.ui.DocumentAdapter;
18 import com.intellij.ui.IdeBorderFactory;
19 import com.intellij.ui.ScrollPaneFactory;
20 import com.intellij.ui.components.JBList;
21 import com.intellij.util.IconUtil;
22 import com.intellij.util.PlatformIcons;
23 import com.intellij.util.containers.ContainerUtil;
24 import com.intellij.util.ui.FormBuilder;
25 import com.intellij.util.ui.JBUI;
26 import org.jetbrains.annotations.NotNull;
27 import org.jetbrains.annotations.Nullable;
28
29 import javax.swing.*;
30 import javax.swing.event.DocumentEvent;
31 import javax.swing.event.ListSelectionEvent;
32 import javax.swing.event.ListSelectionListener;
33 import java.awt.*;
34 import java.awt.event.InputEvent;
35 import java.awt.event.KeyEvent;
36 import java.io.File;
37 import java.util.List;
38 import java.util.*;
39
40 /**
41  * @author Eugene Zhuravlev
42  */
43 abstract class ProjectLayoutPanel<T> extends JPanel {
44
45   private final ElementsChooser<T> myEntriesChooser;
46   private final JList myDependenciesList;
47   private final ModuleInsight myInsight;
48
49   private final Comparator<T> COMPARATOR = (o1, o2) -> {
50     final int w1 = getWeight(o1);
51     final int w2 = getWeight(o2);
52     if (w1 != w2) {
53       return w1 - w2;
54     }
55     return getElementText(o1).compareToIgnoreCase(getElementText(o2));
56   };
57
58   ProjectLayoutPanel(final ModuleInsight insight) {
59     super(new BorderLayout());
60     setBorder(BorderFactory.createEmptyBorder(5, 5, 0, 5));
61     myInsight = insight;
62
63     myEntriesChooser = new ElementsChooser<T>(true) {
64       @Override
65       public String getItemText(@NotNull T element) {
66         return getElementText(element);
67       }
68     };
69     myDependenciesList = createList();
70
71     final Splitter splitter = new Splitter(false);
72
73     final JPanel entriesPanel = new JPanel(new BorderLayout());
74     entriesPanel.add(myEntriesChooser, BorderLayout.CENTER);
75     entriesPanel.setBorder(IdeBorderFactory.createTitledBorder(StringUtil.capitalize(StringUtil.pluralize(getElementTypeName())), false));
76     splitter.setFirstComponent(entriesPanel);
77
78     final JScrollPane depsPane = ScrollPaneFactory.createScrollPane(myDependenciesList);
79     final JPanel depsPanel = new JPanel(new BorderLayout());
80     depsPanel.add(depsPane, BorderLayout.CENTER);
81     depsPanel.setBorder(IdeBorderFactory.createTitledBorder(getDependenciesTitle(), false));
82     splitter.setSecondComponent(depsPanel);
83
84     JPanel groupPanel = new JPanel(new BorderLayout());
85     groupPanel.add(createEntriesActionToolbar().getComponent(), BorderLayout.NORTH);
86     groupPanel.add(splitter, BorderLayout.CENTER);
87     groupPanel.setBorder(BorderFactory.createEmptyBorder(10, 0, 0, 0));
88
89     final MultiLineLabel description = new MultiLineLabel(getStepDescriptionText());
90     add(description, BorderLayout.NORTH);
91     add(groupPanel, BorderLayout.CENTER);
92
93     myEntriesChooser.addListSelectionListener(new ListSelectionListener() {
94       @Override
95       public void valueChanged(final ListSelectionEvent e) {
96         if (e.getValueIsAdjusting()) {
97           return;
98         }
99         final List<T> entries = getSelectedEntries();
100         final Collection deps = getDependencies(entries);
101
102         final DefaultListModel depsModel = (DefaultListModel)myDependenciesList.getModel();
103         depsModel.clear();
104         for (Object dep : alphaSortList(new ArrayList(deps))) {
105           depsModel.addElement(dep);
106         }
107       }
108     });
109   }
110
111   private ActionToolbar createEntriesActionToolbar() {
112     final DefaultActionGroup entriesActions = new DefaultActionGroup();
113
114     final RenameAction rename = new RenameAction();
115     rename.registerCustomShortcutSet(new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_F6, InputEvent.SHIFT_DOWN_MASK)), this);
116     entriesActions.add(rename);
117
118     final MergeAction merge = new MergeAction();
119     merge.registerCustomShortcutSet(new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_F3, 0)), this);
120     entriesActions.add(merge);
121
122     final SplitAction split = new SplitAction();
123     split.registerCustomShortcutSet(new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_F4, 0)), this);
124     entriesActions.add(split);
125
126     return ActionManager.getInstance().createActionToolbar("ProjectLayoutPanel.Entries", entriesActions, true);
127   }
128
129   public final ModuleInsight getInsight() {
130     return myInsight;
131   }
132
133   private JList createList() {
134     final JList list = new JBList(new DefaultListModel());
135     list.getSelectionModel().setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
136     list.setCellRenderer(new MyListCellRenderer());
137     return list;
138   }
139
140   public final Collection getDependencies(final List<? extends T> entries) {
141     final Set deps = new HashSet();
142     for (T et : entries) {
143       deps.addAll(getDependencies(et));
144     }
145     return deps;
146   }
147
148   @NotNull
149   public List<T> getSelectedEntries() {
150     return myEntriesChooser.getSelectedElements();
151   }
152
153   @NotNull
154   public List<T> getChosenEntries() {
155     return myEntriesChooser.getMarkedElements();
156   }
157
158   public void rebuild() {
159     myEntriesChooser.clear();
160     for (final T entry : alphaSortList(getEntries())) {
161       myEntriesChooser.addElement(entry, true, new EntryProperties(entry));
162     }
163     if (myEntriesChooser.getElementCount() > 0) {
164       myEntriesChooser.selectElements(Collections.singleton(myEntriesChooser.getElementAt(0)));
165     }
166   }
167
168   private List<T> alphaSortList(final List<T> entries) {
169     entries.sort(COMPARATOR);
170     return entries;
171   }
172
173   @Nullable
174   protected Icon getElementIcon(Object element) {
175     if (element instanceof ModuleDescriptor) {
176       return ((ModuleDescriptor)element).getModuleType().getIcon();
177     }
178     if (element instanceof LibraryDescriptor) {
179       return PlatformIcons.LIBRARY_ICON;
180     }
181     if (element instanceof File) {
182       final File file = (File)element;
183       return file.isDirectory()? PlatformIcons.FOLDER_ICON : PlatformIcons.JAR_ICON;
184     }
185     return null;
186   }
187
188   protected int getWeight(Object element) {
189     if (element instanceof File) {
190       return 10;
191     }
192     if (element instanceof ModuleDescriptor) {
193       return 20;
194     }
195     if (element instanceof LibraryDescriptor) {
196       return ((LibraryDescriptor)element).getJars().size() > 1? 30 : 40;
197     }
198     return Integer.MAX_VALUE;
199   }
200
201   protected static String getElementText(Object element) {
202     if (element instanceof LibraryDescriptor) {
203       final StringBuilder builder = new StringBuilder();
204       builder.append(((LibraryDescriptor)element).getName());
205       final Collection<File> jars = ((LibraryDescriptor)element).getJars();
206       if (jars.size() == 1) {
207         final File parentFile = jars.iterator().next().getParentFile();
208         if (parentFile != null) {
209           builder.append(" (");
210           builder.append(parentFile.getPath());
211           builder.append(")");
212         }
213       }
214       return builder.toString();
215     }
216
217     if (element instanceof File) {
218       final StringBuilder builder = new StringBuilder();
219       builder.append(((File)element).getName());
220       final File parentFile = ((File)element).getParentFile();
221       if (parentFile != null) {
222         builder.append(" (");
223         builder.append(parentFile.getPath());
224         builder.append(")");
225       }
226       return builder.toString();
227     }
228
229     if (element instanceof ModuleDescriptor) {
230       final ModuleDescriptor moduleDescriptor = (ModuleDescriptor)element;
231       final StringBuilder builder = new StringBuilder();
232       builder.append(moduleDescriptor.getName());
233
234       final Set<File> contents = moduleDescriptor.getContentRoots();
235       final int rootCount = contents.size();
236       if (rootCount > 0) {
237         builder.append(" (");
238         builder.append(contents.iterator().next().getPath());
239         if (rootCount > 1) {
240           builder.append("...");
241         }
242         builder.append(")");
243       }
244
245       final Collection<? extends DetectedProjectRoot> sourceRoots = moduleDescriptor.getSourceRoots();
246       if (sourceRoots.size() > 0) {
247         StringJoiner joiner = new StringJoiner(",", " [", "]");
248         for (DetectedProjectRoot root : sourceRoots) {
249           joiner.add(root.getDirectory().getName());
250         }
251         builder.append(joiner);
252       }
253       return builder.toString();
254     }
255
256     return "";
257   }
258
259   protected abstract List<T> getEntries();
260
261   protected abstract Collection getDependencies(T entry);
262
263   @Nullable
264   protected abstract T merge(List<? extends T> entries);
265
266   @Nullable
267   protected abstract T split(T entry, String newEntryName, Collection<? extends File> extractedData);
268
269   protected abstract Collection<File> getContent(T entry);
270
271   protected abstract String getElementName(T entry);
272
273   protected abstract void setElementName(T entry, String name);
274
275   protected abstract @NlsContexts.Label String getSplitDialogChooseFilesPrompt();
276
277   protected abstract @NlsContexts.DialogMessage String getNameAlreadyUsedMessage(final String name);
278
279   protected abstract @NlsContexts.DialogMessage String getStepDescriptionText();
280
281   protected abstract @NlsContexts.TabTitle String getEntriesChooserTitle();
282
283   protected abstract @NlsContexts.TabTitle String getDependenciesTitle();
284
285   protected abstract String getElementTypeName();
286
287   private boolean isNameAlreadyUsed(String entryName) {
288     final Set<T> itemsToIgnore = new HashSet<>(myEntriesChooser.getSelectedElements());
289     for (T entry : getEntries()) {
290       if (itemsToIgnore.contains(entry)) {
291         continue;
292       }
293       if (entryName.equals(getElementName(entry))) {
294         return true;
295       }
296     }
297     return false;
298   }
299
300   @NotNull
301   private InputValidator getValidator() {
302     return new InputValidator() {
303       @Override
304       public boolean checkInput(final String inputString) {
305         return true;
306       }
307
308       @Override
309       public boolean canClose(final String inputString) {
310         if (isNameAlreadyUsed(inputString.trim())) {
311           Messages.showErrorDialog(getNameAlreadyUsedMessage(inputString), "");
312           return false;
313         }
314         return true;
315       }
316     };
317   }
318
319   private final class MergeAction extends AnAction {
320     private MergeAction() {
321       super(CommonBundle.messagePointer("action.text.merge"), () -> "", AllIcons.Vcs.Merge); // todo
322     }
323
324     @Override
325     public void actionPerformed(@NotNull final AnActionEvent e) {
326       final List<T> elements = myEntriesChooser.getSelectedElements();
327       if (elements.size() > 1) {
328         final String newName = Messages.showInputDialog(
329           ProjectLayoutPanel.this,
330           JavaUiBundle.message("label.enter.new.name.for.merge.result"),
331           JavaUiBundle.message("dialog.title.merge.module.or.library"),
332           Messages.getQuestionIcon(), getElementName(elements.get(0)), getValidator());
333         if (newName != null) {
334           final T merged = merge(elements);
335           setElementName(merged, newName);
336           for (T element : elements) {
337             myEntriesChooser.removeElement(element);
338           }
339           myEntriesChooser.addElement(merged, true, new EntryProperties(merged));
340           myEntriesChooser.sort(COMPARATOR);
341           myEntriesChooser.selectElements(Collections.singleton(merged));
342         }
343       }
344     }
345
346     @Override
347     public void update(@NotNull final AnActionEvent e) {
348       e.getPresentation().setEnabled(myEntriesChooser.getSelectedElements().size() > 1);
349     }
350
351   }
352
353   private final class SplitAction extends AnAction {
354     private SplitAction() {
355       super(CommonBundle.messagePointer("action.text.split"), () -> "", AllIcons.Modules.Split); // todo
356     }
357
358     @Override
359     public void actionPerformed(@NotNull final AnActionEvent e) {
360       final List<T> elements = myEntriesChooser.getSelectedElements();
361
362       if (elements.size() == 1) {
363         final T entry = elements.get(0);
364         final Collection<File> files = getContent(entry);
365
366         final SplitDialog dialog = new SplitDialog(files);
367         if (dialog.showAndGet()) {
368           final String newName = dialog.getName();
369           final Collection<File> chosenFiles = dialog.getChosenFiles();
370
371           final T extracted = split(entry, newName, chosenFiles);
372           if (extracted != null) {
373             if (!getEntries().contains(entry)) {
374               myEntriesChooser.removeElement(entry);
375             }
376             myEntriesChooser.addElement(extracted, true, new EntryProperties(extracted));
377             myEntriesChooser.sort(COMPARATOR);
378             myEntriesChooser.selectElements(Collections.singleton(extracted));
379           }
380         }
381       }
382     }
383     @Override
384     public void update(@NotNull final AnActionEvent e) {
385       final List<T> elements = myEntriesChooser.getSelectedElements();
386       e.getPresentation().setEnabled(elements.size() == 1 && getContent(elements.get(0)).size() > 1);
387     }
388   }
389
390   private final class RenameAction extends AnAction {
391     private RenameAction() {
392       super(CommonBundle.messagePointer("action.text.rename"), () -> "", IconUtil.getEditIcon()); // todo
393     }
394
395     @Override
396     public void actionPerformed(@NotNull final AnActionEvent e) {
397       final List<T> elements = myEntriesChooser.getSelectedElements();
398       if (elements.size() == 1) {
399         final T element = elements.get(0);
400         final String newName = Messages.showInputDialog(ProjectLayoutPanel.this,
401                                                         JavaUiBundle.message("label.new.name.for.0.1", getElementTypeName(), getElementName(element)),
402                                                         JavaUiBundle.message("dialog.title.rename.module.or.library.0", StringUtil.capitalize(getElementTypeName())),
403                                                         Messages.getQuestionIcon(),
404                                                         getElementName(element),
405                                                         getValidator()
406         );
407         if (newName != null) {
408           setElementName(element, newName);
409           myEntriesChooser.sort(COMPARATOR);
410           myEntriesChooser.selectElements(Collections.singleton(element));
411         }
412       }
413     }
414
415     @Override
416     public void update(@NotNull final AnActionEvent e) {
417       e.getPresentation().setEnabled(myEntriesChooser.getSelectedElements().size() == 1);
418     }
419   }
420
421   private class MyListCellRenderer extends DefaultListCellRenderer {
422     @Override
423     public Component getListCellRendererComponent(final JList list, final Object value, final int index, final boolean isSelected, final boolean cellHasFocus) {
424       final Component comp = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
425       setText(getElementText(value));
426       setIcon(getElementIcon(value));
427       return comp;
428     }
429   }
430
431   private final class SplitDialog extends DialogWrapper {
432     final JTextField myNameField;
433     final ElementsChooser<File> myChooser;
434
435     private SplitDialog(final Collection<File> files) {
436       super(myEntriesChooser, true);
437       setTitle(JavaUiBundle.message("dialog.title.split.module.or.library.0", StringUtil.capitalize(getElementTypeName())));
438
439       myNameField = new JTextField();
440       myChooser = new ElementsChooser<File>(true) {
441         @Override
442         protected String getItemText(@NotNull final File value) {
443           return getElementText(value);
444         }
445       };
446       for (final File file : files) {
447         myChooser.addElement(file, false, new ElementsChooser.ElementProperties() {
448           @Override
449           public Icon getIcon() {
450             return getElementIcon(file);
451           }
452         });
453       }
454       myChooser.selectElements(ContainerUtil.createMaybeSingletonList(ContainerUtil.getFirstItem(files)));
455       myChooser.addElementsMarkListener(new ElementsChooser.ElementsMarkListener<File>() {
456         @Override
457         public void elementMarkChanged(File element, boolean isMarked) {
458           updateOkButton();
459         }
460       });
461       myNameField.getDocument().addDocumentListener(new DocumentAdapter() {
462         @Override
463         protected void textChanged(@NotNull DocumentEvent e) {
464           updateOkButton();
465         }
466       });
467
468       init();
469       updateOkButton();
470     }
471
472     private void updateOkButton() {
473       setOKActionEnabled(!getName().isEmpty() && !getChosenFiles().isEmpty());
474     }
475
476     @Override
477     protected void doOKAction() {
478       final String name = getName();
479       if (isNameAlreadyUsed(name)) {
480         Messages.showErrorDialog(getNameAlreadyUsedMessage(name), "");
481         return;
482       }
483       super.doOKAction();
484     }
485
486     @Override
487     @Nullable
488     protected JComponent createCenterPanel() {
489       FormBuilder builder = FormBuilder.createFormBuilder().setVertical(true);
490       builder.addLabeledComponent(JavaUiBundle.message("label.project.layout.panel.name"), myNameField);
491       builder.addLabeledComponent(getSplitDialogChooseFilesPrompt(), myChooser);
492       myChooser.setPreferredSize(JBUI.size(450, 300));
493       return builder.getPanel();
494     }
495
496     @Override
497     public JComponent getPreferredFocusedComponent() {
498       return myNameField;
499     }
500
501     public String getName() {
502       return myNameField.getText().trim();
503     }
504
505     public Collection<File> getChosenFiles() {
506       return myChooser.getMarkedElements();
507     }
508   }
509
510   private class EntryProperties implements ElementsChooser.ElementProperties {
511     private final T myEntry;
512
513     EntryProperties(final T entry) {
514       myEntry = entry;
515     }
516
517     @Override
518     public Icon getIcon() {
519       return getElementIcon(myEntry);
520     }
521   }
522 }