6c2687978febbaa4c309708d548f65ef466f75e5
[idea/community.git] / java / idea-ui / src / com / intellij / openapi / projectRoots / ui / SdkEditor.java
1 /*
2  * Copyright 2000-2016 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.openapi.projectRoots.ui;
17
18 import com.google.common.collect.Lists;
19 import com.intellij.openapi.Disposable;
20 import com.intellij.openapi.SdkEditorAdditionalOptionsProvider;
21 import com.intellij.openapi.application.ApplicationManager;
22 import com.intellij.openapi.diagnostic.Logger;
23 import com.intellij.openapi.options.Configurable;
24 import com.intellij.openapi.options.ConfigurationException;
25 import com.intellij.openapi.project.Project;
26 import com.intellij.openapi.project.ProjectBundle;
27 import com.intellij.openapi.projectRoots.*;
28 import com.intellij.openapi.projectRoots.impl.ProjectJdkImpl;
29 import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil;
30 import com.intellij.openapi.roots.OrderRootType;
31 import com.intellij.openapi.roots.ui.OrderRootTypeUIFactory;
32 import com.intellij.openapi.ui.Messages;
33 import com.intellij.openapi.ui.TextFieldWithBrowseButton;
34 import com.intellij.openapi.util.ActionCallback;
35 import com.intellij.openapi.util.Comparing;
36 import com.intellij.openapi.util.Disposer;
37 import com.intellij.openapi.util.io.FileUtil;
38 import com.intellij.openapi.vfs.VirtualFile;
39 import com.intellij.ui.TabbedPaneWrapper;
40 import com.intellij.ui.navigation.History;
41 import com.intellij.ui.navigation.Place;
42 import com.intellij.util.containers.ContainerUtil;
43 import com.intellij.util.ui.JBUI;
44 import com.intellij.util.ui.UIUtil;
45 import org.jetbrains.annotations.NonNls;
46 import org.jetbrains.annotations.NotNull;
47 import org.jetbrains.annotations.Nullable;
48
49 import javax.swing.*;
50 import javax.swing.event.ChangeEvent;
51 import javax.swing.event.ChangeListener;
52 import java.awt.*;
53 import java.awt.event.ActionEvent;
54 import java.awt.event.ActionListener;
55 import java.io.File;
56 import java.util.*;
57 import java.util.List;
58
59 /**
60  * @author: MYakovlev
61  * @since Aug 15, 2002
62  */
63 public class SdkEditor implements Configurable, Place.Navigator {
64   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.projectRoots.ui.SdkEditor");
65   @NonNls private static final String SDK_TAB = "sdkTab";
66
67   private Sdk mySdk;
68   private final Map<OrderRootType, SdkPathEditor> myPathEditors = new HashMap<>();
69
70   private TextFieldWithBrowseButton myHomeComponent;
71   private final Map<SdkType, List<AdditionalDataConfigurable>> myAdditionalDataConfigurables = new HashMap<>();
72   private final Map<AdditionalDataConfigurable, JComponent> myAdditionalDataComponents = new HashMap<>();
73   private JPanel myAdditionalDataPanel;
74   private final SdkModificator myEditedSdkModificator = new EditedSdkModificator();
75
76   // GUI components
77   private JPanel myMainPanel;
78   private TabbedPaneWrapper myTabbedPane;
79   private Project myProject;
80   private final SdkModel mySdkModel;
81   private JLabel myHomeFieldLabel;
82   private String myVersionString;
83
84   private String myInitialName;
85   private String myInitialPath;
86   private final History myHistory;
87
88   private final Disposable myDisposable = Disposer.newDisposable();
89
90   public SdkEditor(Project project, SdkModel sdkModel, History history, final ProjectJdkImpl sdk) {
91     myProject = project;
92     mySdkModel = sdkModel;
93     myHistory = history;
94     mySdk = sdk;
95     createMainPanel();
96     initSdk(sdk);
97   }
98
99   private void initSdk(Sdk sdk) {
100     mySdk = sdk;
101     if (mySdk != null) {
102       myInitialName = mySdk.getName();
103       myInitialPath = mySdk.getHomePath();
104     }
105     else {
106       myInitialName = "";
107       myInitialPath = "";
108     }
109     for (final AdditionalDataConfigurable additionalDataConfigurable : getAdditionalDataConfigurable()) {
110       additionalDataConfigurable.setSdk(sdk);
111     }
112     if (myMainPanel != null) {
113       reset();
114     }
115   }
116
117   @Override
118   public String getDisplayName() {
119     return ProjectBundle.message("sdk.configure.editor.title");
120   }
121
122   @Override
123   public String getHelpTopic() {
124     return null;
125   }
126
127   @Override
128   public JComponent createComponent() {
129     return myMainPanel;
130   }
131
132   private void createMainPanel() {
133     myMainPanel = new JPanel(new GridBagLayout());
134
135     myTabbedPane = new TabbedPaneWrapper(myDisposable);
136     for (OrderRootType type : OrderRootType.getAllTypes()) {
137       if (mySdk == null || showTabForType(type)) {
138         final SdkPathEditor pathEditor = OrderRootTypeUIFactory.FACTORY.getByKey(type).createPathEditor(mySdk);
139         if (pathEditor != null) {
140           pathEditor.setAddBaseDir(mySdk.getHomeDirectory());
141           myTabbedPane.addTab(pathEditor.getDisplayName(), pathEditor.createComponent());
142           myPathEditors.put(type, pathEditor);
143         }
144       }
145     }
146
147     myTabbedPane.addChangeListener(new ChangeListener() {
148       @Override
149       public void stateChanged(final ChangeEvent e) {
150         myHistory.pushQueryPlace();
151       }
152     });
153
154     myHomeComponent = createHomeComponent();
155     myHomeComponent.getTextField().setEditable(false);
156
157     myHomeFieldLabel = new JLabel(getHomeFieldLabelValue());
158     final int leftInset = 10;
159     final int rightInset = 10;
160     myMainPanel.add(myHomeFieldLabel,
161                     new GridBagConstraints(0, GridBagConstraints.RELATIVE, 1, 1, 0.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.NONE,
162                                            JBUI.insets(2, leftInset, 2, 2), 0, 0));
163     myMainPanel.add(myHomeComponent, new GridBagConstraints(1, GridBagConstraints.RELATIVE, 1, 1, 1.0, 0.0, GridBagConstraints.CENTER,
164                                                             GridBagConstraints.HORIZONTAL,
165                                                             JBUI.insets(2, 2, 2, rightInset), 0, 0));
166
167     myAdditionalDataPanel = new JPanel(new BorderLayout());
168     myMainPanel.add(myAdditionalDataPanel, new GridBagConstraints(0, GridBagConstraints.RELATIVE, 2, 1, 1.0, 0.0, GridBagConstraints.CENTER,
169                                                                   GridBagConstraints.BOTH,
170                                                                   JBUI.insets(2, leftInset, 0, rightInset), 0, 0));
171
172     myMainPanel.add(myTabbedPane.getComponent(),
173                     new GridBagConstraints(0, GridBagConstraints.RELATIVE, 2, 1, 1.0, 1.0, GridBagConstraints.CENTER,
174                                            GridBagConstraints.BOTH,
175                                            JBUI.insetsTop(2), 0, 0));
176   }
177
178   protected TextFieldWithBrowseButton createHomeComponent() {
179     return new TextFieldWithBrowseButton(new ActionListener() {
180       @Override
181       public void actionPerformed(ActionEvent e) {
182         doSelectHomePath();
183       }
184     });
185   }
186
187   protected boolean showTabForType(@NotNull OrderRootType type) {
188     return ((SdkType)mySdk.getSdkType()).isRootTypeApplicable(type);
189   }
190
191   private String getHomeFieldLabelValue() {
192     if (mySdk != null) {
193       return ((SdkType)mySdk.getSdkType()).getHomeFieldLabel();
194     }
195     return ProjectBundle.message("sdk.configure.general.home.path");
196   }
197
198   @Override
199   public boolean isModified() {
200     boolean isModified = !Comparing.equal(mySdk == null ? null : mySdk.getName(), myInitialName);
201     isModified =
202       isModified || !Comparing.equal(FileUtil.toSystemIndependentName(getHomeValue()), FileUtil.toSystemIndependentName(myInitialPath));
203     for (PathEditor pathEditor : myPathEditors.values()) {
204       isModified = isModified || pathEditor.isModified();
205     }
206     for (final AdditionalDataConfigurable configurable : getAdditionalDataConfigurable()) {
207       isModified = isModified || configurable.isModified();
208     }
209     return isModified;
210   }
211
212   @Override
213   public void apply() throws ConfigurationException {
214     if (!Comparing.equal(myInitialName, mySdk == null ? "" : mySdk.getName())) {
215       if (mySdk == null || mySdk.getName().isEmpty()) {
216         throw new ConfigurationException(ProjectBundle.message("sdk.list.name.required.error"));
217       }
218     }
219     if (mySdk != null) {
220       myInitialName = mySdk.getName();
221       myInitialPath = mySdk.getHomePath();
222       final SdkModificator sdkModificator = mySdk.getSdkModificator();
223       sdkModificator.setHomePath(getHomeValue().replace(File.separatorChar, '/'));
224       for (SdkPathEditor pathEditor : myPathEditors.values()) {
225         pathEditor.apply(sdkModificator);
226       }
227       ApplicationManager.getApplication().runWriteAction(() -> sdkModificator.commitChanges());
228       for (final AdditionalDataConfigurable configurable : getAdditionalDataConfigurable()) {
229         if (configurable != null) {
230           configurable.apply();
231         }
232       }
233     }
234   }
235
236   @Override
237   public void reset() {
238     if (mySdk == null) {
239       setHomePathValue("");
240       for (SdkPathEditor pathEditor : myPathEditors.values()) {
241         pathEditor.reset(null);
242       }
243     }
244     else {
245       final SdkModificator sdkModificator = mySdk.getSdkModificator();
246       for (OrderRootType type : myPathEditors.keySet()) {
247         myPathEditors.get(type).reset(sdkModificator);
248       }
249       sdkModificator.commitChanges();
250       setHomePathValue(mySdk.getHomePath().replace('/', File.separatorChar));
251     }
252     myVersionString = null;
253     myHomeFieldLabel.setText(getHomeFieldLabelValue());
254     updateAdditionalDataComponent();
255
256     for (final AdditionalDataConfigurable configurable : getAdditionalDataConfigurable()) {
257       configurable.reset();
258     }
259
260     myHomeComponent.setEnabled(mySdk != null);
261
262     for (int i = 0; i < myTabbedPane.getTabCount(); i++) {
263       myTabbedPane.setEnabledAt(i, mySdk != null);
264     }
265   }
266
267   @Override
268   public void disposeUIResources() {
269     for (final SdkType sdkType : myAdditionalDataConfigurables.keySet()) {
270       for (final AdditionalDataConfigurable configurable : myAdditionalDataConfigurables.get(sdkType)) {
271         configurable.disposeUIResources();
272       }
273     }
274     myAdditionalDataConfigurables.clear();
275     myAdditionalDataComponents.clear();
276
277     Disposer.dispose(myDisposable);
278   }
279
280   private String getHomeValue() {
281     return myHomeComponent.getText().trim();
282   }
283
284   private void clearAllPaths() {
285     for (PathEditor editor : myPathEditors.values()) {
286       editor.clearList();
287     }
288   }
289
290   private void setHomePathValue(String absolutePath) {
291     myHomeComponent.setText(absolutePath);
292     final Color fg;
293     if (absolutePath != null && !absolutePath.isEmpty()) {
294       final File homeDir = new File(absolutePath);
295       boolean homeMustBeDirectory = mySdk == null || ((SdkType)mySdk.getSdkType()).getHomeChooserDescriptor().isChooseFolders();
296       fg = homeDir.exists() && homeDir.isDirectory() == homeMustBeDirectory
297            ? UIUtil.getFieldForegroundColor()
298            : PathEditor.INVALID_COLOR;
299     }
300     else {
301       fg = UIUtil.getFieldForegroundColor();
302     }
303     myHomeComponent.getTextField().setForeground(fg);
304   }
305
306   private void doSelectHomePath() {
307     final SdkType sdkType = (SdkType)mySdk.getSdkType();
308     SdkConfigurationUtil.selectSdkHome(sdkType, path -> doSetHomePath(path, sdkType));
309   }
310
311   private void doSetHomePath(final String homePath, final SdkType sdkType) {
312     if (homePath == null) {
313       return;
314     }
315     setHomePathValue(homePath.replace('/', File.separatorChar));
316
317     final String newSdkName = suggestSdkName(homePath);
318     ((ProjectJdkImpl)mySdk).setName(newSdkName);
319
320     try {
321       final Sdk dummySdk = (Sdk)mySdk.clone();
322       SdkModificator sdkModificator = dummySdk.getSdkModificator();
323       sdkModificator.setHomePath(homePath);
324       sdkModificator.removeAllRoots();
325       sdkModificator.commitChanges();
326
327       sdkType.setupSdkPaths(dummySdk, mySdkModel);
328
329       clearAllPaths();
330       myVersionString = dummySdk.getVersionString();
331       if (myVersionString == null) {
332         Messages.showMessageDialog(ProjectBundle.message("sdk.java.corrupt.error", homePath),
333                                    ProjectBundle.message("sdk.java.corrupt.title"), Messages.getErrorIcon());
334       }
335       sdkModificator = dummySdk.getSdkModificator();
336       for (OrderRootType type : myPathEditors.keySet()) {
337         SdkPathEditor pathEditor = myPathEditors.get(type);
338         pathEditor.setAddBaseDir(dummySdk.getHomeDirectory());
339         pathEditor.addPaths(sdkModificator.getRoots(type));
340       }
341       mySdkModel.getMulticaster().sdkHomeSelected(dummySdk, homePath);
342     }
343     catch (CloneNotSupportedException e) {
344       LOG.error(e); // should not happen in normal program
345     }
346   }
347
348   private String suggestSdkName(final String homePath) {
349     final String currentName = mySdk.getName();
350     final String suggestedName = ((SdkType)mySdk.getSdkType()).suggestSdkName(currentName, homePath);
351     if (Comparing.equal(currentName, suggestedName)) return currentName;
352     String newSdkName = suggestedName;
353     final Set<String> allNames = new HashSet<>();
354     Sdk[] sdks = mySdkModel.getSdks();
355     for (Sdk sdk : sdks) {
356       allNames.add(sdk.getName());
357     }
358     int i = 0;
359     while (allNames.contains(newSdkName)) {
360       newSdkName = suggestedName + " (" + ++i + ")";
361     }
362     return newSdkName;
363   }
364
365   private void updateAdditionalDataComponent() {
366     myAdditionalDataPanel.removeAll();
367     for (AdditionalDataConfigurable configurable : getAdditionalDataConfigurable()) {
368       JComponent component = myAdditionalDataComponents.get(configurable);
369       if (component == null) {
370         component = configurable.createComponent();
371         myAdditionalDataComponents.put(configurable, component);
372       }
373       if (component != null) {
374         if (configurable.getTabName() != null) {
375           for (int i = 0; i < myTabbedPane.getTabCount(); i++) {
376             if (configurable.getTabName().equals(myTabbedPane.getTitleAt(i))) {
377               myTabbedPane.removeTabAt(i);
378             }
379           }
380           myTabbedPane.addTab(configurable.getTabName(), component);
381         }
382         else {
383           myAdditionalDataPanel.add(component, BorderLayout.CENTER);
384         }
385       }
386     }
387   }
388
389   @NotNull
390   private List<AdditionalDataConfigurable> getAdditionalDataConfigurable() {
391     if (mySdk == null) {
392       return ContainerUtil.emptyList();
393     }
394     return initAdditionalDataConfigurable(mySdk);
395   }
396
397   @NotNull
398   private List<AdditionalDataConfigurable> initAdditionalDataConfigurable(Sdk sdk) {
399     final SdkType sdkType = (SdkType)sdk.getSdkType();
400     List<AdditionalDataConfigurable> configurables = myAdditionalDataConfigurables.get(sdkType);
401     if (configurables == null) {
402       configurables = Lists.newArrayList();
403       myAdditionalDataConfigurables.put(sdkType, configurables);
404
405
406       AdditionalDataConfigurable sdkConfigurable = sdkType.createAdditionalDataConfigurable(mySdkModel, myEditedSdkModificator);
407       if (sdkConfigurable != null) {
408         configurables.add(sdkConfigurable);
409       }
410
411       for (SdkEditorAdditionalOptionsProvider factory : SdkEditorAdditionalOptionsProvider.getSdkOptionsFactory(mySdk.getSdkType())) {
412         AdditionalDataConfigurable options = factory.createOptions(myProject, mySdk);
413         if (options != null) {
414           configurables.add(options);
415         }
416       }
417     }
418
419     return configurables;
420   }
421
422   private class EditedSdkModificator implements SdkModificator {
423     @Override
424     public String getName() {
425       return mySdk.getName();
426     }
427
428     @Override
429     public void setName(String name) {
430       ((ProjectJdkImpl)mySdk).setName(name);
431     }
432
433     @Override
434     public String getHomePath() {
435       return getHomeValue();
436     }
437
438     @Override
439     public void setHomePath(String path) {
440       doSetHomePath(path, (SdkType)mySdk.getSdkType());
441     }
442
443     @Override
444     public String getVersionString() {
445       return myVersionString != null ? myVersionString : mySdk.getVersionString();
446     }
447
448     @Override
449     public void setVersionString(String versionString) {
450       throw new UnsupportedOperationException(); // not supported for this editor
451     }
452
453     @Override
454     public SdkAdditionalData getSdkAdditionalData() {
455       return mySdk.getSdkAdditionalData();
456     }
457
458     @Override
459     public void setSdkAdditionalData(SdkAdditionalData data) {
460       throw new UnsupportedOperationException(); // not supported for this editor
461     }
462
463     @Override
464     public VirtualFile[] getRoots(OrderRootType rootType) {
465       final PathEditor editor = myPathEditors.get(rootType);
466       if (editor == null) {
467         throw new IllegalStateException("no editor for root type " + rootType);
468       }
469       return editor.getRoots();
470     }
471
472     @Override
473     public void addRoot(VirtualFile root, OrderRootType rootType) {
474       myPathEditors.get(rootType).addPaths(root);
475     }
476
477     @Override
478     public void removeRoot(VirtualFile root, OrderRootType rootType) {
479       myPathEditors.get(rootType).removePaths(root);
480     }
481
482     @Override
483     public void removeRoots(OrderRootType rootType) {
484       myPathEditors.get(rootType).clearList();
485     }
486
487     @Override
488     public void removeAllRoots() {
489       for (PathEditor editor : myPathEditors.values()) {
490         editor.clearList();
491       }
492     }
493
494     @Override
495     public void commitChanges() {
496     }
497
498     @Override
499     public boolean isWritable() {
500       return true;
501     }
502   }
503
504   @Override
505   public ActionCallback navigateTo(@Nullable final Place place, final boolean requestFocus) {
506     if (place == null) return ActionCallback.DONE;
507     myTabbedPane.setSelectedTitle((String)place.getPath(SDK_TAB));
508     return ActionCallback.DONE;
509   }
510
511   @Override
512   public void queryPlace(@NotNull final Place place) {
513     place.putPath(SDK_TAB, myTabbedPane.getSelectedTitle());
514   }
515
516   @Override
517   public void setHistory(final History history) {
518   }
519 }