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;
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;
49 import javax.swing.event.DocumentEvent;
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;
60 import static com.intellij.util.ui.UI.PanelFactory;
63 * @deprecated Migrate to {@link com.intellij.openapi.vcs.ui.cloneDialog.VcsCloneDialogExtension}
64 * or {@link com.intellij.openapi.vcs.ui.VcsCloneComponent}
67 public abstract class CloneDvcsDialog extends DialogWrapper {
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;
77 @NotNull protected final Project myProject;
78 @NotNull protected final String myVcsDirectoryName;
80 @Nullable private ValidationInfo myCreateDirectoryValidationInfo;
81 @Nullable private ValidationInfo myRepositoryTestValidationInfo;
82 @Nullable private ProgressIndicator myRepositoryTestProgressIndicator;
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<>();
89 public CloneDvcsDialog(@NotNull Project project, @NotNull String displayName, @NotNull String vcsDirectoryName) {
90 this(project, displayName, vcsDirectoryName, null);
93 public CloneDvcsDialog(@NotNull Project project,
94 @NotNull String displayName,
95 @NotNull String vcsDirectoryName,
96 @Nullable String defaultUrl) {
99 myVcsDirectoryName = vcsDirectoryName;
100 myLoadedRepositoryHostingServicesNames = new ArrayList<>();
101 myUniqueAvailableRepositories = new HashSet<>();
103 initComponents(defaultUrl);
104 Map<String, RepositoryListLoader> loadersToSchedule = initUrlAutocomplete();
105 setTitle(DvcsBundle.getString("clone.title"));
106 setOKButtonText(DvcsBundle.getString("clone.button"));
108 scheduleLater(loadersToSchedule);
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;
118 public void run(@NotNull ProgressIndicator indicator) {
119 error = CloneDvcsValidationUtils.createDestination(path);
123 public void onSuccess() {
125 CloneDvcsDialog.super.doOKAction();
128 myCreateDirectoryValidationInfo = error;
129 startTrackingValidation();
136 public String getSourceRepositoryURL() {
137 return getCurrentUrlText();
141 public String getParentDirectory() {
142 Path parent = Paths.get(myDirectoryField.getText()).toAbsolutePath().getParent();
143 return Objects.requireNonNull(parent).toAbsolutePath().toString();
147 public String getDirectoryName() {
148 return Paths.get(myDirectoryField.getText()).getFileName().toString();
151 private void initComponents(@Nullable String defaultUrl) {
152 myRepositoryUrlComboboxModel = new CollectionComboBoxModel<>();
153 myRepositoryUrlField = TextFieldWithAutoCompletion.create(myProject,
154 myRepositoryUrlComboboxModel.getItems(),
158 JLabel repositoryUrlFieldSpinner = new JLabel(new AnimatedIcon.Default());
159 repositoryUrlFieldSpinner.setVisible(false);
161 mySpinnerProgressManager = new ComponentVisibilityProgressManager(repositoryUrlFieldSpinner);
162 Disposer.register(getDisposable(), mySpinnerProgressManager);
164 myRepositoryUrlCombobox = new ComboBox<>();
165 myRepositoryUrlCombobox.setEditable(true);
166 myRepositoryUrlCombobox.setEditor(ComboBoxCompositeEditor.withComponents(myRepositoryUrlField,
167 repositoryUrlFieldSpinner));
168 myRepositoryUrlCombobox.setModel(myRepositoryUrlComboboxModel);
170 myRepositoryUrlField.addDocumentListener(new DocumentListener() {
172 public void documentChanged(@NotNull com.intellij.openapi.editor.event.DocumentEvent event) {
173 myDirectoryField.trySetChildPath(defaultDirectoryPath(myRepositoryUrlField.getText().trim()));
176 myRepositoryUrlField.addDocumentListener(new DocumentListener() {
178 public void documentChanged(@NotNull com.intellij.openapi.editor.event.DocumentEvent event) {
179 myRepositoryTestValidationInfo = null;
183 myTestButton = new JButton(DvcsBundle.getString("clone.repository.url.test.label"));
184 myTestButton.addActionListener(e -> test());
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"),
195 if (defaultUrl != null) {
196 myRepositoryUrlField.setText(defaultUrl);
197 myRepositoryUrlField.selectAll();
198 myTestButton.setEnabled(true);
203 * Initializes component structure for repository list loading
205 * @return already enabled loaders for pre-scheduling
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);
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);
224 loginActions.add(new AbstractAction(DvcsBundle.message("clone.repository.url.autocomplete.login.text", serviceDisplayName)) {
226 public void actionPerformed(ActionEvent e) {
227 if (loader.enable(myLoginButtonComponent.getPanel())) {
228 myLoginButtonComponent.removeAction(this);
229 schedule(serviceDisplayName, loader);
236 myRepositoryUrlField.addFocusListener(new FocusAdapter() {
238 public void focusGained(FocusEvent e) {
239 showRepositoryUrlAutoCompletionTooltip();
243 myLoginButtonComponent = new LoginButtonComponent(loginActions);
244 return enabledLoaders;
248 protected Collection<RepositoryHostingService> getRepositoryHostingServices() {
249 return Collections.emptyList();
252 private void scheduleLater(@NotNull Map<String, RepositoryListLoader> loaders) {
253 ApplicationManager.getApplication().invokeLater(() -> loaders.forEach(this::schedule), ModalityState.stateForComponent(getRootPane()));
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<>();
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);
270 myErrors.addAll(loadingResult.getErrors());
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());
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());
294 startTrackingValidation();
300 private void showRepositoryUrlAutoCompletionTooltip() {
301 if (myRepositoryUrlAutoCompletionTooltipAlarm == null) {
302 showRepositoryUrlAutoCompletionTooltipNow();
305 myRepositoryUrlAutoCompletionTooltipAlarm.cancelAllRequests();
306 myRepositoryUrlAutoCompletionTooltipAlarm.addComponentRequest(this::showRepositoryUrlAutoCompletionTooltipNow, 1);
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));
323 private void test() {
324 String testUrl = getCurrentUrlText();
325 if (myRepositoryTestProgressIndicator != null) {
326 myRepositoryTestProgressIndicator.cancel();
327 myRepositoryTestProgressIndicator = null;
329 myRepositoryTestProgressIndicator =
330 mySpinnerProgressManager
331 .run(new Task.Backgroundable(myProject, DvcsBundle.message("clone.repository.url.test.title", testUrl), true) {
332 private TestResult myTestResult;
335 public void run(@NotNull ProgressIndicator indicator) {
336 myTestResult = test(testUrl);
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)
349 .show(new RelativePoint(myTestButton, new Point(myTestButton.getWidth() / 2,
350 myTestButton.getHeight())),
351 Balloon.Position.below);
354 myRepositoryTestValidationInfo =
355 new ValidationInfo(DvcsBundle.message("clone.repository.url.test.failed.message", myTestResult.myErrorMessage),
356 myRepositoryUrlCombobox);
357 startTrackingValidation();
359 myRepositoryTestProgressIndicator = null;
365 protected abstract TestResult test(@NotNull String url);
368 protected abstract DvcsRememberedInputs getRememberedInputs();
372 protected List<ValidationInfo> doValidateAll() {
373 ValidationInfo urlValidation = CloneDvcsValidationUtils.checkRepositoryURL(myRepositoryUrlCombobox, getCurrentUrlText());
374 ValidationInfo directoryValidation = CloneDvcsValidationUtils.checkDirectory(myDirectoryField.getText(),
375 myDirectoryField.getTextField());
377 myTestButton.setEnabled(urlValidation == null);
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);
389 private String getCurrentUrlText() {
390 return FileUtil.expandUserHome(myRepositoryUrlField.getText().trim());
394 * @deprecated use {@link #getRepositoryHostingServices()}
397 public void prependToHistory(@NotNull final String item) {
398 myRepositoryUrlComboboxModel.add(item);
401 public void rememberSettings() {
402 final DvcsRememberedInputs rememberedInputs = getRememberedInputs();
403 rememberedInputs.addUrl(getSourceRepositoryURL());
404 rememberedInputs.setCloneParentDir(getParentDirectory());
408 * Get default name for checked out directory
410 * @param url an URL to checkout
411 * @return a default repository name
414 private String defaultDirectoryPath(@NotNull final String url) {
415 return StringUtil.trimEnd(ClonePathProvider.relativeDirectoryPathForVcsUrl(myProject, url), myVcsDirectoryName);
420 public JComponent getPreferredFocusedComponent() {
421 return myRepositoryUrlField;
426 protected JPanel createSouthAdditionalPanel() {
427 return myLoginButtonComponent.getPanel();
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")))
441 panel.setPreferredSize(new JBDimension(500, 50, true));
445 protected static class TestResult {
446 @NotNull public static final TestResult SUCCESS = new TestResult(null);
447 @Nullable private final String myErrorMessage;
449 public TestResult(@Nullable String errorMessage) {
450 myErrorMessage = errorMessage;
453 public boolean isSuccess() {
454 return myErrorMessage == null;
458 public String getError() {
459 return myErrorMessage;
463 private static final class MyTextFieldWithBrowseButton extends TextFieldWithBrowseButton {
464 @NotNull private final Path myDefaultParentPath;
465 private boolean myModifiedByUser = false;
467 private MyTextFieldWithBrowseButton(@NotNull @NonNls String defaultParentPath) {
468 myDefaultParentPath = Paths.get(defaultParentPath).toAbsolutePath();
469 setText(myDefaultParentPath.toString());
470 getTextField().getDocument().addDocumentListener(new DocumentAdapter() {
472 protected void textChanged(@NotNull DocumentEvent e) {
473 myModifiedByUser = true;
478 public void trySetChildPath(@NotNull String child) {
479 if (!myModifiedByUser) {
481 setText(myDefaultParentPath.resolve(child).toString());
483 catch (InvalidPathException ignored) {
486 myModifiedByUser = false;
492 private static class LoginButtonComponent {
493 @NotNull private final JBOptionButton myButton;
494 @NotNull private final JPanel myPanel;
495 @NotNull private final List<Action> myActions;
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"))
502 myPanel.setVisible(!actions.isEmpty());
503 myPanel.setBorder(JBUI.Borders.emptyRight(16));
504 myActions = new ArrayList<>(actions);
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));
514 myButton.setAction(null);
515 myButton.setOptions((Action[])null);
516 myPanel.setVisible(false);
521 private static Action @NotNull [] getActionsAfterFirst(@NotNull List<Action> actions) {
522 if (actions.size() <= 1) {
523 return new Action[0];
526 return actions.subList(1, actions.size()).toArray(new Action[actions.size() - 1]);
531 public JPanel getPanel() {