CPP-13710 - commit only current document if there are no processors that can access...
[idea/community.git] / java / idea-ui / src / com / intellij / openapi / roots / ui / configuration / ModuleEditor.java
1 // Copyright 2000-2019 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;
3
4 import com.intellij.facet.impl.ProjectFacetsConfigurator;
5 import com.intellij.ide.JavaUiBundle;
6 import com.intellij.openapi.Disposable;
7 import com.intellij.openapi.actionSystem.DataProvider;
8 import com.intellij.openapi.actionSystem.LangDataKeys;
9 import com.intellij.openapi.diagnostic.Logger;
10 import com.intellij.openapi.extensions.ExtensionPointName;
11 import com.intellij.openapi.module.Module;
12 import com.intellij.openapi.module.ModuleConfigurationEditor;
13 import com.intellij.openapi.module.impl.ModuleConfigurationStateImpl;
14 import com.intellij.openapi.options.Configurable;
15 import com.intellij.openapi.options.ConfigurationException;
16 import com.intellij.openapi.options.ModuleConfigurableEP;
17 import com.intellij.openapi.project.Project;
18 import com.intellij.openapi.roots.*;
19 import com.intellij.openapi.roots.impl.libraries.LibraryEx;
20 import com.intellij.openapi.roots.libraries.Library;
21 import com.intellij.openapi.roots.libraries.LibraryTable;
22 import com.intellij.ui.navigation.History;
23 import com.intellij.ui.navigation.Place;
24 import com.intellij.util.EventDispatcher;
25 import com.intellij.util.containers.ContainerUtil;
26 import gnu.trove.THashSet;
27 import org.jetbrains.annotations.NonNls;
28 import org.jetbrains.annotations.NotNull;
29 import org.jetbrains.annotations.Nullable;
30
31 import javax.swing.*;
32 import java.awt.*;
33 import java.lang.reflect.InvocationHandler;
34 import java.lang.reflect.InvocationTargetException;
35 import java.lang.reflect.Method;
36 import java.lang.reflect.Proxy;
37 import java.util.List;
38 import java.util.*;
39
40 /**
41  * @author Eugene Zhuravlev
42  */
43 public abstract class ModuleEditor implements Place.Navigator, Disposable {
44   private static final Logger LOG = Logger.getInstance(ModuleEditor.class);
45   private static final ExtensionPointName<ModuleConfigurableEP> MODULE_CONFIGURABLES = ExtensionPointName.create("com.intellij.moduleConfigurable");
46   public static final String SELECTED_EDITOR_NAME = "selectedEditor";
47
48   private final Project myProject;
49   private JPanel myGenericSettingsPanel;
50   private ModificationOfImportedModelWarningComponent myModificationOfImportedModelWarningComponent;
51   private ModifiableRootModel myModifiableRootModel; // important: in order to correctly update OrderEntries UI use corresponding proxy for the model
52
53   private final ModulesProvider myModulesProvider;
54   private String myName;
55   private final Module myModule;
56
57   protected final List<ModuleConfigurationEditor> myEditors = new ArrayList<>();
58   private ModifiableRootModel myModifiableRootModelProxy;
59
60   private final EventDispatcher<ChangeListener> myEventDispatcher = EventDispatcher.create(ChangeListener.class);
61   @NonNls private static final String METHOD_COMMIT = "commit";
62   private boolean myEditorsInitialized;
63
64   protected History myHistory;
65
66   public ModuleEditor(Project project, ModulesProvider modulesProvider,
67                       @NotNull Module module) {
68     myProject = project;
69     myModulesProvider = modulesProvider;
70     myModule = module;
71     myName = module.getName();
72   }
73
74   public void init(History history) {
75     myHistory = history;
76
77     for (ModuleConfigurationEditor each : myEditors) {
78       if (each instanceof ModuleElementsEditor) {
79         ((ModuleElementsEditor)each).setHistory(myHistory);
80       }
81     }
82
83     restoreSelectedEditor();
84   }
85
86   public abstract ProjectFacetsConfigurator getFacetsConfigurator();
87
88   protected abstract JComponent createCenterPanel();
89
90   @Nullable
91   public abstract ModuleConfigurationEditor getSelectedEditor();
92
93   public abstract void selectEditor(String displayName);
94
95   protected abstract void restoreSelectedEditor();
96
97   @Nullable
98   public abstract ModuleConfigurationEditor getEditor(@NotNull String displayName);
99
100   protected abstract void disposeCenterPanel();
101
102   public interface ChangeListener extends EventListener {
103     void moduleStateChanged(ModifiableRootModel moduleRootModel);
104   }
105
106   public void addChangeListener(ChangeListener listener) {
107     myEventDispatcher.addListener(listener);
108   }
109
110   public void removeChangeListener(ChangeListener listener) {
111     myEventDispatcher.removeListener(listener);
112   }
113
114   @Nullable
115   public Module getModule() {
116     final Module[] all = myModulesProvider.getModules();
117     for (Module each : all) {
118       if (each == myModule) return myModule;
119     }
120
121     return myModulesProvider.getModule(myName);
122   }
123
124   public ModifiableRootModel getModifiableRootModel() {
125     if (myModifiableRootModel == null) {
126       final Module module = getModule();
127       if (module != null) {
128         myModifiableRootModel = ModuleRootManagerEx.getInstanceEx(module).getModifiableModelForMultiCommit(new UIRootConfigurationAccessor(myProject));
129       }
130     }
131     return myModifiableRootModel;
132   }
133
134   public OrderEntry @NotNull [] getOrderEntries() {
135     if (myModifiableRootModel == null) { // do not clone all model if not necessary
136       return ModuleRootManager.getInstance(getModule()).getOrderEntries();
137     }
138     return myModifiableRootModel.getOrderEntries();
139   }
140
141   public ModifiableRootModel getModifiableRootModelProxy() {
142     if (myModifiableRootModelProxy == null) {
143       final ModifiableRootModel rootModel = getModifiableRootModel();
144       if (rootModel != null) {
145         myModifiableRootModelProxy = (ModifiableRootModel)Proxy.newProxyInstance(
146           getClass().getClassLoader(), new Class[]{ModifiableRootModel.class}, new ModifiableRootModelInvocationHandler(rootModel)
147         );
148       }
149     }
150     return myModifiableRootModelProxy;
151   }
152
153   public ModuleRootModel getRootModel() {
154     if (myModifiableRootModel != null) {
155       return getModifiableRootModelProxy();
156     }
157     return ModuleRootManager.getInstance(myModule);
158   }
159
160   public boolean isModified() {
161     for (ModuleConfigurationEditor moduleElementsEditor : myEditors) {
162       if (moduleElementsEditor.isModified()) {
163         return true;
164       }
165     }
166     return false;
167   }
168
169   private void createEditors(@Nullable Module module) {
170     if (module == null) return;
171
172     ModuleConfigurationState state = createModuleConfigurationState();
173     for (ModuleConfigurationEditorProvider provider : collectProviders(module)) {
174       ModuleConfigurationEditor[] editors = provider.createEditors(state);
175       if (editors.length > 0 && provider instanceof ModuleConfigurationEditorProviderEx &&
176           ((ModuleConfigurationEditorProviderEx)provider).isCompleteEditorSet()) {
177         myEditors.clear();
178         ContainerUtil.addAll(myEditors, editors);
179         break;
180       }
181       else {
182         ContainerUtil.addAll(myEditors, editors);
183       }
184     }
185
186     for (Configurable moduleConfigurable : module.getComponentInstancesOfType(Configurable.class)) {
187       reportDeprecatedModuleEditor(moduleConfigurable.getClass());
188       myEditors.add(new ModuleConfigurableWrapper(moduleConfigurable));
189     }
190     for(ModuleConfigurableEP extension : MODULE_CONFIGURABLES.getExtensionList(module)) {
191       if (extension.canCreateConfigurable()) {
192         Configurable configurable = extension.createConfigurable();
193         if (configurable != null) {
194           reportDeprecatedModuleEditor(configurable.getClass());
195           myEditors.add(new ModuleConfigurableWrapper(configurable));
196         }
197       }
198     }
199     for (ModuleConfigurationEditor editor : myEditors) {
200       if (editor instanceof ModuleElementsEditor) {
201         ((ModuleElementsEditor)editor).addListener(this::updateImportedModelWarning);
202       }
203     }
204   }
205
206   private static final Set<Class<?>> ourReportedDeprecatedClasses = new HashSet<>();
207   private static void reportDeprecatedModuleEditor(@NotNull Class<?> aClass) {
208     if (ourReportedDeprecatedClasses.add(aClass)) {
209       LOG.warn(aClass.getName() + " uses deprecated way to register itself as a module editor. " + ModuleConfigurationEditorProvider.class.getName() + " extension point should be used instead");
210     }
211   }
212
213   private static ModuleConfigurationEditorProvider @NotNull [] collectProviders(@NotNull Module module) {
214     List<ModuleConfigurationEditorProvider> result = new ArrayList<>(module.getComponentInstancesOfType(ModuleConfigurationEditorProvider.class));
215     for (ModuleConfigurationEditorProvider component : result) {
216       reportDeprecatedModuleEditor(component.getClass());
217     }
218     ContainerUtil.addAll(result, ModuleConfigurationEditorProvider.EP_NAME.getExtensions(module));
219     return result.toArray(new ModuleConfigurationEditorProvider[0]);
220   }
221
222   @NotNull
223   public ModuleConfigurationState createModuleConfigurationState() {
224     return new ModuleConfigurationStateImpl(myProject, myModulesProvider) {
225       @Override
226       public ModifiableRootModel getRootModel() {
227         return getModifiableRootModelProxy();
228       }
229
230       @Override
231       public FacetsProvider getFacetsProvider() {
232         return getFacetsConfigurator();
233       }
234     };
235   }
236
237   @NotNull
238   private JPanel createPanel() {
239     getModifiableRootModel(); //initialize model if needed
240     getModifiableRootModelProxy();
241
242     myGenericSettingsPanel = new ModuleEditorPanel();
243
244     createEditors(getModule());
245
246     final JComponent component = createCenterPanel();
247     myGenericSettingsPanel.add(component, BorderLayout.CENTER);
248     myModificationOfImportedModelWarningComponent = new ModificationOfImportedModelWarningComponent();
249     myGenericSettingsPanel.add(myModificationOfImportedModelWarningComponent.getLabel(), BorderLayout.SOUTH);
250     updateImportedModelWarning();
251     myEditorsInitialized = true;
252     return myGenericSettingsPanel;
253   }
254
255   @NotNull
256   public JPanel getPanel() {
257     if (myGenericSettingsPanel == null) {
258       myGenericSettingsPanel = createPanel();
259     }
260
261     return myGenericSettingsPanel;
262   }
263
264   public void moduleCountChanged() {
265     updateOrderEntriesInEditors(false);
266   }
267
268   private void updateOrderEntriesInEditors(boolean forceInitEditors) {
269     if (getModule() != null) { //module with attached module libraries was deleted
270       if (myEditorsInitialized || forceInitEditors) {
271         getPanel();  //init editor if needed
272         for (final ModuleConfigurationEditor myEditor : myEditors) {
273           myEditor.moduleStateChanged();
274         }
275         updateImportedModelWarning();
276       }
277       myEventDispatcher.getMulticaster().moduleStateChanged(getModifiableRootModelProxy());
278     }
279   }
280
281   private void updateImportedModelWarning() {
282     if (!myEditorsInitialized) return;
283
284     ProjectModelExternalSource externalSource = ModuleRootManager.getInstance(myModule).getExternalSource();
285     if (externalSource != null && isModified()) {
286       myModificationOfImportedModelWarningComponent.showWarning(
287         JavaUiBundle.message("project.roots.module.banner.text", myModule.getName()), externalSource);
288     }
289     else {
290       myModificationOfImportedModelWarningComponent.hideWarning();
291     }
292   }
293
294   public void updateCompilerOutputPathChanged(String baseUrl, String moduleName){
295     if (myGenericSettingsPanel == null) return; //wasn't initialized yet
296     for (final ModuleConfigurationEditor myEditor : myEditors) {
297       if (myEditor instanceof ModuleElementsEditor) {
298         ((ModuleElementsEditor)myEditor).moduleCompileOutputChanged(baseUrl, moduleName);
299       }
300     }
301   }
302
303   @Override
304   public void dispose() {
305     try {
306       for (final ModuleConfigurationEditor myEditor : myEditors) {
307         myEditor.disposeUIResources();
308       }
309
310       myEditors.clear();
311
312       disposeCenterPanel();
313
314       if (myModifiableRootModel != null) {
315         myModifiableRootModel.dispose();
316       }
317
318       myGenericSettingsPanel = null;
319     }
320     finally {
321       resetModifiableModel();
322     }
323   }
324
325   public ModifiableRootModel apply() throws ConfigurationException {
326     for (ModuleConfigurationEditor editor : myEditors) {
327       editor.saveData();
328       editor.apply();
329     }
330     return myModifiableRootModel;
331   }
332
333   void resetModifiableModel() {
334     myModifiableRootModel = null;
335     myModifiableRootModelProxy = null;
336   }
337
338   public void canApply() throws ConfigurationException {
339     for (ModuleConfigurationEditor editor : myEditors) {
340       if (editor instanceof ModuleElementsEditor) {
341         ((ModuleElementsEditor)editor).canApply();
342       }
343     }
344   }
345
346   @NotNull
347   public String getName() {
348     return myName;
349   }
350
351   private class ModifiableRootModelInvocationHandler implements InvocationHandler, ProxyDelegateAccessor {
352     private final ModifiableRootModel myDelegateModel;
353     @NonNls private final Set<String> myCheckedNames = ContainerUtil
354       .set("addOrderEntry", "addLibraryEntry", "addInvalidLibrary", "addModuleOrderEntry", "addInvalidModuleEntry", "removeOrderEntry",
355            "setSdk", "inheritSdk", "inheritCompilerOutputPath", "setExcludeOutput", "replaceEntryOfType", "rearrangeOrderEntries");
356
357     ModifiableRootModelInvocationHandler(@NotNull ModifiableRootModel model) {
358       myDelegateModel = model;
359     }
360
361     @Override
362     public Object invoke(Object object, Method method, Object[] params) throws Throwable {
363       final boolean needUpdate = myCheckedNames.contains(method.getName());
364       try {
365         final Object result = method.invoke(myDelegateModel, unwrapParams(params));
366         if (result instanceof LibraryTable) {
367           return Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{LibraryTable.class},
368                                         new LibraryTableInvocationHandler((LibraryTable)result));
369         }
370         return result;
371       }
372       catch (InvocationTargetException e) {
373         throw e.getCause();
374       }
375       finally {
376         if (needUpdate) {
377           updateOrderEntriesInEditors(true);
378         }
379       }
380     }
381
382     @Override
383     public Object getDelegate() {
384       return myDelegateModel;
385     }
386   }
387
388   private class LibraryTableInvocationHandler implements InvocationHandler, ProxyDelegateAccessor {
389     private final LibraryTable myDelegateTable;
390     @NonNls private final Set<String> myCheckedNames = new THashSet<>(Collections.singletonList("removeLibrary" /*,"createLibrary"*/));
391
392     LibraryTableInvocationHandler(@NotNull LibraryTable table) {
393       myDelegateTable = table;
394     }
395
396     @Override
397     public Object invoke(Object object, Method method, Object[] params) throws Throwable {
398       final boolean needUpdate = myCheckedNames.contains(method.getName());
399       try {
400         final Object result = method.invoke(myDelegateTable, unwrapParams(params));
401         if (result instanceof Library) {
402           return Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{result instanceof LibraryEx ? LibraryEx.class : Library.class},
403                                         new LibraryInvocationHandler((Library)result));
404         }
405         if (result instanceof LibraryTable.ModifiableModel) {
406           return Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{LibraryTable.ModifiableModel.class},
407                                         new LibraryTableModelInvocationHandler((LibraryTable.ModifiableModel)result));
408         }
409         if (result instanceof Library[]) {
410           Library[] libraries = (Library[])result;
411           for (int idx = 0; idx < libraries.length; idx++) {
412             Library library = libraries[idx];
413             libraries[idx] =
414             (Library)Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{library instanceof LibraryEx ? LibraryEx.class : Library.class},
415                                             new LibraryInvocationHandler(library));
416           }
417         }
418         return result;
419       }
420       catch (InvocationTargetException e) {
421         throw e.getCause();
422       }
423       finally {
424         if (needUpdate) {
425           updateOrderEntriesInEditors(true);
426         }
427       }
428     }
429
430     @Override
431     public Object getDelegate() {
432       return myDelegateTable;
433     }
434   }
435
436   private class LibraryInvocationHandler implements InvocationHandler, ProxyDelegateAccessor {
437     private final Library myDelegateLibrary;
438
439     LibraryInvocationHandler(@NotNull Library delegateLibrary) {
440       myDelegateLibrary = delegateLibrary;
441     }
442
443     @Override
444     public Object invoke(Object object, Method method, Object[] params) throws Throwable {
445       try {
446         final Object result = method.invoke(myDelegateLibrary, unwrapParams(params));
447         if (result instanceof LibraryEx.ModifiableModelEx) {
448           return Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{LibraryEx.ModifiableModelEx.class},
449                                         new LibraryModifiableModelInvocationHandler((LibraryEx.ModifiableModelEx)result));
450         }
451         return result;
452       }
453       catch (InvocationTargetException e) {
454         throw e.getCause();
455       }
456     }
457
458     @Override
459     public Object getDelegate() {
460       return myDelegateLibrary;
461     }
462   }
463
464   private class LibraryModifiableModelInvocationHandler implements InvocationHandler, ProxyDelegateAccessor {
465     private final Library.ModifiableModel myDelegateModel;
466
467     LibraryModifiableModelInvocationHandler(@NotNull Library.ModifiableModel delegateModel) {
468       myDelegateModel = delegateModel;
469     }
470
471     @Override
472     public Object invoke(Object object, Method method, Object[] params) throws Throwable {
473       final boolean needUpdate = METHOD_COMMIT.equals(method.getName());
474       try {
475         return method.invoke(myDelegateModel, unwrapParams(params));
476       }
477       catch (InvocationTargetException e) {
478         throw e.getCause();
479       }
480       finally {
481         if (needUpdate) {
482           updateOrderEntriesInEditors(true);
483         }
484       }
485     }
486
487     @Override
488     public Object getDelegate() {
489       return myDelegateModel;
490     }
491   }
492
493   private class LibraryTableModelInvocationHandler implements InvocationHandler, ProxyDelegateAccessor {
494     private final LibraryTable.ModifiableModel myDelegateModel;
495
496     LibraryTableModelInvocationHandler(@NotNull LibraryTable.ModifiableModel delegateModel) {
497       myDelegateModel = delegateModel;
498     }
499
500     @Override
501     public Object invoke(Object object, Method method, Object[] params) throws Throwable {
502       final boolean needUpdate = METHOD_COMMIT.equals(method.getName());
503       try {
504         Object result = method.invoke(myDelegateModel, unwrapParams(params));
505         if (result instanceof Library[]) {
506           Library[] libraries = (Library[])result;
507           for (int idx = 0; idx < libraries.length; idx++) {
508             Library library = libraries[idx];
509             libraries[idx] =
510             (Library)Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{LibraryEx.class},
511                                             new LibraryInvocationHandler(library));
512           }
513         }
514         if (result instanceof Library) {
515           result =
516           Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{LibraryEx.class},
517                                  new LibraryInvocationHandler((Library)result));
518         }
519         return result;
520       }
521       catch (InvocationTargetException e) {
522         throw e.getCause();
523       }
524       finally {
525         if (needUpdate) {
526           updateOrderEntriesInEditors(true);
527         }
528       }
529     }
530
531     @Override
532     public Object getDelegate() {
533       return myDelegateModel;
534     }
535   }
536
537   public interface ProxyDelegateAccessor {
538     Object getDelegate();
539   }
540
541   private static Object[] unwrapParams(Object[] params) {
542     if (params == null || params.length == 0) {
543       return params;
544     }
545     final Object[] unwrappedParams = new Object[params.length];
546     for (int idx = 0; idx < params.length; idx++) {
547       Object param = params[idx];
548       if (param != null && Proxy.isProxyClass(param.getClass())) {
549         final InvocationHandler invocationHandler = Proxy.getInvocationHandler(param);
550         if (invocationHandler instanceof ProxyDelegateAccessor) {
551           param = ((ProxyDelegateAccessor)invocationHandler).getDelegate();
552         }
553       }
554       unwrappedParams[idx] = param;
555     }
556     return unwrappedParams;
557   }
558
559   @Nullable
560   public String getHelpTopic() {
561     if (myEditors.isEmpty()) {
562       return null;
563     }
564     final ModuleConfigurationEditor selectedEditor = getSelectedEditor();
565     return selectedEditor != null ? selectedEditor.getHelpTopic() : null;
566   }
567
568   public void setModuleName(@NotNull String name) {
569     myName = name;
570   }
571
572   private class ModuleEditorPanel extends JPanel implements DataProvider{
573     ModuleEditorPanel() {
574       super(new BorderLayout());
575     }
576
577     @Override
578     public Object getData(@NotNull String dataId) {
579       if (LangDataKeys.MODULE_CONTEXT.is(dataId)) {
580         return getModule();
581       }
582       return null;
583     }
584   }
585 }