987d9bfe726fb05ad35be159889f493169468106
[idea/community.git] / platform / dvcs-impl / src / com / intellij / dvcs / ui / CloneDvcsDialog.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.dvcs.ui;
3
4 import com.intellij.codeInsight.hint.HintManager;
5 import com.intellij.dvcs.DvcsRememberedInputs;
6 import com.intellij.dvcs.DvcsUtil;
7 import com.intellij.dvcs.hosting.RepositoryHostingService;
8 import com.intellij.dvcs.hosting.RepositoryListLoader;
9 import com.intellij.dvcs.hosting.RepositoryListLoadingException;
10 import com.intellij.dvcs.repo.ClonePathProvider;
11 import com.intellij.openapi.Disposable;
12 import com.intellij.openapi.actionSystem.ActionManager;
13 import com.intellij.openapi.actionSystem.IdeActions;
14 import com.intellij.openapi.application.ApplicationManager;
15 import com.intellij.openapi.application.ModalityState;
16 import com.intellij.openapi.editor.Editor;
17 import com.intellij.openapi.editor.event.DocumentListener;
18 import com.intellij.openapi.fileChooser.FileChooserDescriptor;
19 import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory;
20 import com.intellij.openapi.keymap.KeymapUtil;
21 import com.intellij.openapi.progress.ProgressIndicator;
22 import com.intellij.openapi.progress.Task;
23 import com.intellij.openapi.project.Project;
24 import com.intellij.openapi.ui.ComboBox;
25 import com.intellij.openapi.ui.DialogWrapper;
26 import com.intellij.openapi.ui.TextFieldWithBrowseButton;
27 import com.intellij.openapi.ui.ValidationInfo;
28 import com.intellij.openapi.ui.popup.Balloon;
29 import com.intellij.openapi.ui.popup.JBPopupFactory;
30 import com.intellij.openapi.util.Disposer;
31 import com.intellij.openapi.util.NlsSafe;
32 import com.intellij.openapi.util.io.FileUtil;
33 import com.intellij.openapi.util.text.StringUtil;
34 import com.intellij.ui.*;
35 import com.intellij.ui.awt.RelativePoint;
36 import com.intellij.ui.components.JBOptionButton;
37 import com.intellij.util.Alarm;
38 import com.intellij.util.containers.ContainerUtil;
39 import com.intellij.util.progress.ComponentVisibilityProgressManager;
40 import com.intellij.util.ui.JBDimension;
41 import com.intellij.util.ui.JBUI;
42 import com.intellij.util.ui.UIUtil;
43 import org.jetbrains.annotations.Nls;
44 import org.jetbrains.annotations.NonNls;
45 import org.jetbrains.annotations.NotNull;
46 import org.jetbrains.annotations.Nullable;
47
48 import javax.swing.*;
49 import javax.swing.event.DocumentEvent;
50 import java.awt.*;
51 import java.awt.event.ActionEvent;
52 import java.awt.event.FocusAdapter;
53 import java.awt.event.FocusEvent;
54 import java.nio.file.InvalidPathException;
55 import java.nio.file.Path;
56 import java.nio.file.Paths;
57 import java.util.List;
58 import java.util.*;
59
60 import static com.intellij.util.ui.UI.PanelFactory;
61
62 /**
63  * @deprecated Migrate to {@link com.intellij.openapi.vcs.ui.cloneDialog.VcsCloneDialogExtension}
64  * or {@link com.intellij.openapi.vcs.ui.VcsCloneComponent}
65  */
66 @Deprecated
67 public abstract class CloneDvcsDialog extends DialogWrapper {
68
69   private ComboBox<String> myRepositoryUrlCombobox;
70   private CollectionComboBoxModel<String> myRepositoryUrlComboboxModel;
71   private TextFieldWithAutoCompletion<String> myRepositoryUrlField;
72   private ComponentVisibilityProgressManager mySpinnerProgressManager;
73   private JButton myTestButton; // test repository
74   private MyTextFieldWithBrowseButton myDirectoryField;
75   private LoginButtonComponent myLoginButtonComponent;
76
77   @NotNull protected final Project myProject;
78   @NotNull protected final String myVcsDirectoryName;
79
80   @Nullable private ValidationInfo myCreateDirectoryValidationInfo;
81   @Nullable private ValidationInfo myRepositoryTestValidationInfo;
82   @Nullable private ProgressIndicator myRepositoryTestProgressIndicator;
83
84   @NotNull private final List<String> myLoadedRepositoryHostingServicesNames;
85   @Nullable private Alarm myRepositoryUrlAutoCompletionTooltipAlarm;
86   @NotNull private final Set<String> myUniqueAvailableRepositories;
87   @NotNull private final List<ValidationInfo> myRepositoryListLoadingErrors = new ArrayList<>();
88
89   public CloneDvcsDialog(@NotNull Project project, @NotNull String displayName, @NotNull String vcsDirectoryName) {
90     this(project, displayName, vcsDirectoryName, null);
91   }
92
93   public CloneDvcsDialog(@NotNull Project project,
94                          @NotNull String displayName,
95                          @NotNull String vcsDirectoryName,
96                          @Nullable String defaultUrl) {
97     super(project, true);
98     myProject = project;
99     myVcsDirectoryName = vcsDirectoryName;
100     myLoadedRepositoryHostingServicesNames = new ArrayList<>();
101     myUniqueAvailableRepositories = new HashSet<>();
102
103     initComponents(defaultUrl);
104     Map<String, RepositoryListLoader> loadersToSchedule = initUrlAutocomplete();
105     setTitle(DvcsBundle.getString("clone.title"));
106     setOKButtonText(DvcsBundle.getString("clone.button"));
107     init();
108     scheduleLater(loadersToSchedule);
109   }
110
111   @Override
112   protected void doOKAction() {
113     String path = myDirectoryField.getText();
114     new Task.Modal(myProject, DvcsBundle.message("progress.title.creating.destination.directory"), true) {
115       private ValidationInfo error = null;
116
117       @Override
118       public void run(@NotNull ProgressIndicator indicator) {
119         error = CloneDvcsValidationUtils.createDestination(path);
120       }
121
122       @Override
123       public void onSuccess() {
124         if (error == null) {
125           CloneDvcsDialog.super.doOKAction();
126         }
127         else {
128           myCreateDirectoryValidationInfo = error;
129           startTrackingValidation();
130         }
131       }
132     }.queue();
133   }
134
135   @NotNull
136   public String getSourceRepositoryURL() {
137     return getCurrentUrlText();
138   }
139
140   @NotNull
141   public String getParentDirectory() {
142     Path parent = Paths.get(myDirectoryField.getText()).toAbsolutePath().getParent();
143     return Objects.requireNonNull(parent).toAbsolutePath().toString();
144   }
145
146   @NotNull
147   public String getDirectoryName() {
148     return Paths.get(myDirectoryField.getText()).getFileName().toString();
149   }
150
151   private void initComponents(@Nullable String defaultUrl) {
152     myRepositoryUrlComboboxModel = new CollectionComboBoxModel<>();
153     myRepositoryUrlField = TextFieldWithAutoCompletion.create(myProject,
154                                                               myRepositoryUrlComboboxModel.getItems(),
155                                                               false,
156                                                               "");
157
158     JLabel repositoryUrlFieldSpinner = new JLabel(new AnimatedIcon.Default());
159     repositoryUrlFieldSpinner.setVisible(false);
160
161     mySpinnerProgressManager = new ComponentVisibilityProgressManager(repositoryUrlFieldSpinner);
162     Disposer.register(getDisposable(), mySpinnerProgressManager);
163
164     myRepositoryUrlCombobox = new ComboBox<>();
165     myRepositoryUrlCombobox.setEditable(true);
166     myRepositoryUrlCombobox.setEditor(ComboBoxCompositeEditor.withComponents(myRepositoryUrlField,
167                                                                              repositoryUrlFieldSpinner));
168     myRepositoryUrlCombobox.setModel(myRepositoryUrlComboboxModel);
169
170     myRepositoryUrlField.addDocumentListener(new DocumentListener() {
171       @Override
172       public void documentChanged(@NotNull com.intellij.openapi.editor.event.DocumentEvent event) {
173         myDirectoryField.trySetChildPath(defaultDirectoryPath(myRepositoryUrlField.getText().trim()));
174       }
175     });
176     myRepositoryUrlField.addDocumentListener(new DocumentListener() {
177       @Override
178       public void documentChanged(@NotNull com.intellij.openapi.editor.event.DocumentEvent event) {
179         myRepositoryTestValidationInfo = null;
180       }
181     });
182
183     myTestButton = new JButton(DvcsBundle.getString("clone.repository.url.test.label"));
184     myTestButton.addActionListener(e -> test());
185
186     FileChooserDescriptor fcd = FileChooserDescriptorFactory.createSingleFolderDescriptor();
187     fcd.setShowFileSystemRoots(true);
188     fcd.setHideIgnored(false);
189     myDirectoryField = new MyTextFieldWithBrowseButton(ClonePathProvider.defaultParentDirectoryPath(myProject, getRememberedInputs()));
190     myDirectoryField.addBrowseFolderListener(DvcsBundle.getString("clone.destination.directory.browser.title"),
191                                              DvcsBundle.getString("clone.destination.directory.browser.description"),
192                                              myProject,
193                                              fcd);
194
195     if (defaultUrl != null) {
196       myRepositoryUrlField.setText(defaultUrl);
197       myRepositoryUrlField.selectAll();
198       myTestButton.setEnabled(true);
199     }
200   }
201
202   /**
203    * Initializes component structure for repository list loading
204    *
205    * @return already enabled loaders for pre-scheduling
206    */
207   private Map<String, RepositoryListLoader> initUrlAutocomplete() {
208     Collection<RepositoryHostingService> repositoryHostingServices = getRepositoryHostingServices();
209     if (repositoryHostingServices.size() > 1) {
210       myRepositoryUrlAutoCompletionTooltipAlarm = new Alarm(getDisposable());
211       myRepositoryUrlAutoCompletionTooltipAlarm.setActivationComponent(myRepositoryUrlCombobox);
212     }
213
214     List<Action> loginActions = new ArrayList<>();
215     Map<String, RepositoryListLoader> enabledLoaders = new HashMap<>();
216     for (RepositoryHostingService service : repositoryHostingServices) {
217       String serviceDisplayName = service.getServiceDisplayName();
218       RepositoryListLoader loader = service.getRepositoryListLoader(myProject);
219       if (loader == null) continue;
220       if (loader.isEnabled()) {
221         enabledLoaders.put(serviceDisplayName, loader);
222       }
223       else {
224         loginActions.add(new AbstractAction(DvcsBundle.message("clone.repository.url.autocomplete.login.text", serviceDisplayName)) {
225           @Override
226           public void actionPerformed(ActionEvent e) {
227             if (loader.enable(myLoginButtonComponent.getPanel())) {
228               myLoginButtonComponent.removeAction(this);
229               schedule(serviceDisplayName, loader);
230             }
231           }
232         });
233       }
234     }
235
236     myRepositoryUrlField.addFocusListener(new FocusAdapter() {
237       @Override
238       public void focusGained(FocusEvent e) {
239         showRepositoryUrlAutoCompletionTooltip();
240       }
241     });
242
243     myLoginButtonComponent = new LoginButtonComponent(loginActions);
244     return enabledLoaders;
245   }
246
247   @NotNull
248   protected Collection<RepositoryHostingService> getRepositoryHostingServices() {
249     return Collections.emptyList();
250   }
251
252   private void scheduleLater(@NotNull Map<String, RepositoryListLoader> loaders) {
253     ApplicationManager.getApplication().invokeLater(() -> loaders.forEach(this::schedule), ModalityState.stateForComponent(getRootPane()));
254   }
255
256   private void schedule(@NotNull String serviceDisplayName, @NotNull RepositoryListLoader loader) {
257     mySpinnerProgressManager.run(new Task.Backgroundable(myProject, DvcsBundle.message("progress.title.visible")) {
258       private final List<String> myNewRepositories = new ArrayList<>();
259       private final List<RepositoryListLoadingException> myErrors = new ArrayList<>();
260
261       @Override
262       public void run(@NotNull ProgressIndicator indicator) {
263         RepositoryListLoader.Result loadingResult =
264           loader.getAvailableRepositoriesFromMultipleSources(indicator);
265         for (String repository : loadingResult.getUrls()) {
266           if (myUniqueAvailableRepositories.add(repository)) {
267             myNewRepositories.add(repository);
268           }
269         }
270         myErrors.addAll(loadingResult.getErrors());
271       }
272
273       @Override
274       public void onSuccess() {
275         if (mySpinnerProgressManager.getDisposed()) return;
276         if (!myNewRepositories.isEmpty()) {
277           // otherwise editor content will be reset
278           @NlsSafe String text = myRepositoryUrlField.getText();
279           myRepositoryUrlCombobox.setSelectedItem(text);
280           myRepositoryUrlComboboxModel.addAll(myRepositoryUrlComboboxModel.getSize(), myNewRepositories);
281           myRepositoryUrlField.setVariants(myRepositoryUrlComboboxModel.getItems());
282         }
283         myLoadedRepositoryHostingServicesNames.add(serviceDisplayName);
284         showRepositoryUrlAutoCompletionTooltip();
285         if (!myErrors.isEmpty()) {
286           for (RepositoryListLoadingException error : myErrors) {
287             @Nls StringBuilder errorMessageBuilder = new StringBuilder();
288             errorMessageBuilder.append(error.getMessage());
289             Throwable cause = error.getCause();
290             if (cause != null) errorMessageBuilder.append(": ").append(cause.getMessage());
291             @Nls String message = errorMessageBuilder.toString();
292             myRepositoryListLoadingErrors.add(new ValidationInfo(message).asWarning().withOKEnabled());
293           }
294           startTrackingValidation();
295         }
296       }
297     });
298   }
299
300   private void showRepositoryUrlAutoCompletionTooltip() {
301     if (myRepositoryUrlAutoCompletionTooltipAlarm == null) {
302       showRepositoryUrlAutoCompletionTooltipNow();
303     }
304     else {
305       myRepositoryUrlAutoCompletionTooltipAlarm.cancelAllRequests();
306       myRepositoryUrlAutoCompletionTooltipAlarm.addComponentRequest(this::showRepositoryUrlAutoCompletionTooltipNow, 1);
307     }
308   }
309
310   private void showRepositoryUrlAutoCompletionTooltipNow() {
311     if (!hasErrors(myRepositoryUrlCombobox) && !myLoadedRepositoryHostingServicesNames.isEmpty()) {
312       Editor editor = myRepositoryUrlField.getEditor();
313       if (editor == null) return;
314       String completionShortcutText =
315         KeymapUtil.getFirstKeyboardShortcutText(ActionManager.getInstance().getAction(IdeActions.ACTION_CODE_COMPLETION));
316       HintManager.getInstance().showInformationHint(editor,
317                                                     DvcsBundle.message("clone.repository.url.autocomplete.hint",
318                                                                        DvcsUtil.joinWithAnd(myLoadedRepositoryHostingServicesNames, 0),
319                                                                        completionShortcutText));
320     }
321   }
322
323   private void test() {
324     String testUrl = getCurrentUrlText();
325     if (myRepositoryTestProgressIndicator != null) {
326       myRepositoryTestProgressIndicator.cancel();
327       myRepositoryTestProgressIndicator = null;
328     }
329     myRepositoryTestProgressIndicator =
330       mySpinnerProgressManager
331         .run(new Task.Backgroundable(myProject, DvcsBundle.message("clone.repository.url.test.title", testUrl), true) {
332           private TestResult myTestResult;
333
334           @Override
335           public void run(@NotNull ProgressIndicator indicator) {
336             myTestResult = test(testUrl);
337           }
338
339           @Override
340           public void onSuccess() {
341             if (myTestResult.isSuccess()) {
342               myRepositoryTestValidationInfo = null;
343               Disposable dialogDisposable = getDisposable();
344               if (Disposer.isDisposed(dialogDisposable)) return;
345               JBPopupFactory.getInstance()
346                 .createBalloonBuilder(new JLabel(DvcsBundle.getString("clone.repository.url.test.success.message")))
347                 .setDisposable(dialogDisposable)
348                 .createBalloon()
349                 .show(new RelativePoint(myTestButton, new Point(myTestButton.getWidth() / 2,
350                                                                 myTestButton.getHeight())),
351                       Balloon.Position.below);
352             }
353             else {
354               myRepositoryTestValidationInfo =
355                 new ValidationInfo(DvcsBundle.message("clone.repository.url.test.failed.message", myTestResult.myErrorMessage),
356                                    myRepositoryUrlCombobox);
357               startTrackingValidation();
358             }
359             myRepositoryTestProgressIndicator = null;
360           }
361         });
362   }
363
364   @NotNull
365   protected abstract TestResult test(@NotNull String url);
366
367   @NotNull
368   protected abstract DvcsRememberedInputs getRememberedInputs();
369
370   @NotNull
371   @Override
372   protected List<ValidationInfo> doValidateAll() {
373     ValidationInfo urlValidation = CloneDvcsValidationUtils.checkRepositoryURL(myRepositoryUrlCombobox, getCurrentUrlText());
374     ValidationInfo directoryValidation = CloneDvcsValidationUtils.checkDirectory(myDirectoryField.getText(),
375                                                                                  myDirectoryField.getTextField());
376
377     myTestButton.setEnabled(urlValidation == null);
378
379     List<ValidationInfo> infoList = new ArrayList<>();
380     ContainerUtil.addIfNotNull(infoList, myRepositoryTestValidationInfo);
381     ContainerUtil.addIfNotNull(infoList, myCreateDirectoryValidationInfo);
382     ContainerUtil.addIfNotNull(infoList, urlValidation);
383     ContainerUtil.addIfNotNull(infoList, directoryValidation);
384     infoList.addAll(myRepositoryListLoadingErrors);
385     return infoList;
386   }
387
388   @NotNull
389   private String getCurrentUrlText() {
390     return FileUtil.expandUserHome(myRepositoryUrlField.getText().trim());
391   }
392
393   /**
394    * @deprecated use {@link #getRepositoryHostingServices()}
395    */
396   @Deprecated
397   public void prependToHistory(@NotNull final String item) {
398     myRepositoryUrlComboboxModel.add(item);
399   }
400
401   public void rememberSettings() {
402     final DvcsRememberedInputs rememberedInputs = getRememberedInputs();
403     rememberedInputs.addUrl(getSourceRepositoryURL());
404     rememberedInputs.setCloneParentDir(getParentDirectory());
405   }
406
407   /**
408    * Get default name for checked out directory
409    *
410    * @param url an URL to checkout
411    * @return a default repository name
412    */
413   @NotNull
414   private String defaultDirectoryPath(@NotNull final String url) {
415     return StringUtil.trimEnd(ClonePathProvider.relativeDirectoryPathForVcsUrl(myProject, url), myVcsDirectoryName);
416   }
417
418   @Nullable
419   @Override
420   public JComponent getPreferredFocusedComponent() {
421     return myRepositoryUrlField;
422   }
423
424   @NotNull
425   @Override
426   protected JPanel createSouthAdditionalPanel() {
427     return myLoginButtonComponent.getPanel();
428   }
429
430   @Override
431   @NotNull
432   protected JComponent createCenterPanel() {
433     JPanel panel = PanelFactory.grid()
434       .add(PanelFactory.panel(JBUI.Panels.simplePanel(UIUtil.DEFAULT_HGAP, UIUtil.DEFAULT_VGAP)
435                                 .addToCenter(myRepositoryUrlCombobox)
436                                 .addToRight(myTestButton))
437              .withLabel(DvcsBundle.getString("clone.repository.url.label")))
438       .add(PanelFactory.panel(myDirectoryField)
439              .withLabel(DvcsBundle.getString("clone.destination.directory.label")))
440       .createPanel();
441     panel.setPreferredSize(new JBDimension(500, 50, true));
442     return panel;
443   }
444
445   protected static class TestResult {
446     @NotNull public static final TestResult SUCCESS = new TestResult(null);
447     @Nullable private final String myErrorMessage;
448
449     public TestResult(@Nullable String errorMessage) {
450       myErrorMessage = errorMessage;
451     }
452
453     public boolean isSuccess() {
454       return myErrorMessage == null;
455     }
456
457     @Nullable
458     public String getError() {
459       return myErrorMessage;
460     }
461   }
462
463   private static final class MyTextFieldWithBrowseButton extends TextFieldWithBrowseButton {
464     @NotNull private final Path myDefaultParentPath;
465     private boolean myModifiedByUser = false;
466
467     private MyTextFieldWithBrowseButton(@NotNull @NonNls String defaultParentPath) {
468       myDefaultParentPath = Paths.get(defaultParentPath).toAbsolutePath();
469       setText(myDefaultParentPath.toString());
470       getTextField().getDocument().addDocumentListener(new DocumentAdapter() {
471         @Override
472         protected void textChanged(@NotNull DocumentEvent e) {
473           myModifiedByUser = true;
474         }
475       });
476     }
477
478     public void trySetChildPath(@NotNull String child) {
479       if (!myModifiedByUser) {
480         try {
481           setText(myDefaultParentPath.resolve(child).toString());
482         }
483         catch (InvalidPathException ignored) {
484         }
485         finally {
486           myModifiedByUser = false;
487         }
488       }
489     }
490   }
491
492   private static class LoginButtonComponent {
493     @NotNull private final JBOptionButton myButton;
494     @NotNull private final JPanel myPanel;
495     @NotNull private final List<Action> myActions;
496
497     LoginButtonComponent(@NotNull List<Action> actions) {
498       myButton = new JBOptionButton(ContainerUtil.getFirstItem(actions), getActionsAfterFirst(actions));
499       myPanel = PanelFactory.panel(myButton)
500         .withTooltip(DvcsBundle.getString("clone.repository.url.autocomplete.login.tooltip"))
501         .createPanel();
502       myPanel.setVisible(!actions.isEmpty());
503       myPanel.setBorder(JBUI.Borders.emptyRight(16));
504       myActions = new ArrayList<>(actions);
505     }
506
507     void removeAction(@NotNull Action action) {
508       if (myActions.remove(action)) {
509         if (!myActions.isEmpty()) {
510           myButton.setAction(ContainerUtil.getFirstItem(myActions));
511           myButton.setOptions(getActionsAfterFirst(myActions));
512         }
513         else {
514           myButton.setAction(null);
515           myButton.setOptions((Action[])null);
516           myPanel.setVisible(false);
517         }
518       }
519     }
520
521     private static Action @NotNull [] getActionsAfterFirst(@NotNull List<Action> actions) {
522       if (actions.size() <= 1) {
523         return new Action[0];
524       }
525       else {
526         return actions.subList(1, actions.size()).toArray(new Action[actions.size() - 1]);
527       }
528     }
529
530     @NotNull
531     public JPanel getPanel() {
532       return myPanel;
533     }
534   }
535 }