PluginManagerConfigurable: do not get custom repository plugins from network for...
[idea/community.git] / platform / platform-impl / src / com / intellij / ide / plugins / newui / MyPluginModel.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.plugins.newui;
3
4 import com.intellij.ide.IdeBundle;
5 import com.intellij.ide.impl.ProjectUtil;
6 import com.intellij.ide.plugins.*;
7 import com.intellij.openapi.application.ApplicationManager;
8 import com.intellij.openapi.application.ApplicationNamesInfo;
9 import com.intellij.openapi.application.ModalityState;
10 import com.intellij.openapi.application.ex.ApplicationInfoEx;
11 import com.intellij.openapi.diagnostic.Logger;
12 import com.intellij.openapi.extensions.PluginDescriptor;
13 import com.intellij.openapi.extensions.PluginId;
14 import com.intellij.openapi.options.Configurable;
15 import com.intellij.openapi.options.ConfigurationException;
16 import com.intellij.openapi.options.newEditor.SettingsDialog;
17 import com.intellij.openapi.progress.ProcessCanceledException;
18 import com.intellij.openapi.ui.DialogWrapper;
19 import com.intellij.openapi.ui.Messages;
20 import com.intellij.openapi.util.Ref;
21 import com.intellij.openapi.util.io.FileUtil;
22 import com.intellij.openapi.util.text.StringUtil;
23 import com.intellij.openapi.wm.IdeFrame;
24 import com.intellij.openapi.wm.ex.ProgressIndicatorEx;
25 import com.intellij.openapi.wm.ex.StatusBarEx;
26 import com.intellij.openapi.wm.impl.welcomeScreen.WelcomeFrame;
27 import com.intellij.util.containers.ContainerUtil;
28 import org.jetbrains.annotations.NotNull;
29 import org.jetbrains.annotations.Nullable;
30
31 import javax.swing.*;
32 import java.awt.*;
33 import java.io.IOException;
34 import java.nio.file.FileVisitResult;
35 import java.util.List;
36 import java.util.*;
37 import java.util.Map.Entry;
38 import java.util.stream.Collectors;
39
40 /**
41  * @author Alexander Lobas
42  */
43 public abstract class MyPluginModel extends InstalledPluginsTableModel implements PluginManagerMain.PluginEnabler {
44   private static final Logger LOG = Logger.getInstance(MyPluginModel.class);
45
46   private final List<ListPluginComponent> myInstalledPluginComponents = new ArrayList<>();
47   private final Map<PluginId, List<ListPluginComponent>> myInstalledPluginComponentMap = new HashMap<>();
48   private final Map<PluginId, List<ListPluginComponent>> myMarketplacePluginComponentMap = new HashMap<>();
49   private final List<PluginsGroup> myEnabledGroups = new ArrayList<>();
50   private PluginsGroupComponent myInstalledPanel;
51   private PluginsGroup myDownloaded;
52   private PluginsGroup myInstalling;
53   private PluginsGroup myUpdates;
54   private Configurable.TopComponentController myTopController;
55   private List<String> myVendorsSorted;
56   private List<String> myTagsSorted;
57
58   private static final Set<IdeaPluginDescriptor> myInstallingPlugins = new HashSet<>();
59   private static final Set<IdeaPluginDescriptor> myInstallingWithUpdatesPlugins = new HashSet<>();
60   static final Map<PluginId, InstallPluginInfo> myInstallingInfos = new HashMap<>();
61
62   public boolean needRestart;
63   public boolean createShutdownCallback = true;
64   private boolean myInstallsRequiringRestart;
65   private final List<PluginDetailsPageComponent> myDetailPanels = new ArrayList<>();
66
67   private StatusBarEx myStatusBar;
68
69   private PluginUpdatesService myPluginUpdatesService;
70
71   private Runnable myInvalidFixCallback;
72
73   private final Map<PluginId, PendingDynamicPluginInstall> myDynamicPluginsToInstall = new LinkedHashMap<>();
74   private final Set<IdeaPluginDescriptor> myDynamicPluginsToUninstall = new HashSet<>();
75   private final Set<IdeaPluginDescriptor> myPluginsToRemoveOnCancel = new HashSet<>();
76
77   private final Set<PluginId> myErrorPluginsToDisable = new HashSet<>();
78
79   protected MyPluginModel() {
80     Window window = ProjectUtil.getActiveFrameOrWelcomeScreen();
81     myStatusBar = getStatusBar(window);
82     if (myStatusBar == null && window != null) {
83       myStatusBar = getStatusBar(window.getOwner());
84     }
85   }
86
87   @Nullable
88   private static StatusBarEx getStatusBar(@Nullable Window frame) {
89     if (frame instanceof IdeFrame && !(frame instanceof WelcomeFrame)) {
90       return (StatusBarEx)((IdeFrame)frame).getStatusBar();
91     }
92     return null;
93   }
94
95   public boolean isModified() {
96     if (needRestart ||
97         !myDynamicPluginsToInstall.isEmpty() ||
98         !myDynamicPluginsToUninstall.isEmpty() ||
99         !myPluginsToRemoveOnCancel.isEmpty()) {
100       return true;
101     }
102
103     for (IdeaPluginDescriptor descriptor : view) {
104       boolean enabledInTable = isEnabled(descriptor);
105
106       if (descriptor.isEnabled() != enabledInTable) {
107         if (enabledInTable && !PluginManagerCore.isDisabled(descriptor.getPluginId())) {
108           continue; // was disabled automatically on startup
109         }
110         return true;
111       }
112     }
113
114     for (Map.Entry<PluginId, Boolean> entry : getEnabledMap().entrySet()) {
115       Boolean enabled = entry.getValue();
116       if (enabled != null && !enabled && !PluginManagerCore.isDisabled(entry.getKey())) {
117         return true;
118       }
119     }
120
121     return false;
122   }
123
124   /**
125    * @return true if changes were applied without restart
126    */
127   public boolean apply(JComponent parent) throws ConfigurationException {
128     Map<PluginId, Boolean> enabledMap = getEnabledMap();
129     List<String> dependencies = new ArrayList<>();
130
131     for (Map.Entry<PluginId, Set<PluginId>> entry : getDependentToRequiredListMap().entrySet()) {
132       PluginId id = entry.getKey();
133
134       if (enabledMap.get(id) == null) {
135         continue;
136       }
137
138       for (PluginId dependId : entry.getValue()) {
139         if (!PluginManagerCore.isModuleDependency(dependId)) {
140           IdeaPluginDescriptor descriptor = PluginManagerCore.getPlugin(id);
141           if (!(descriptor instanceof IdeaPluginDescriptorImpl) ||
142               !((IdeaPluginDescriptorImpl)descriptor).isDeleted() && !descriptor.isImplementationDetail()) {
143             dependencies.add("\"" + (descriptor == null ? id.getIdString() : descriptor.getName()) + "\"");
144           }
145           break;
146         }
147       }
148     }
149
150     if (!dependencies.isEmpty()) {
151       throw new ConfigurationException("<html><body style=\"padding: 5px;\">Unable to apply changes: plugin" +
152                                        (dependencies.size() == 1 ? " " : "s ") +
153                                        StringUtil.join(dependencies, ", ") +
154                                        " won't be able to load.</body></html>");
155     }
156
157     Set<PluginId> uninstallsRequiringRestart = new HashSet<>();
158     for (IdeaPluginDescriptor pluginDescriptor : myDynamicPluginsToUninstall) {
159       if (!PluginInstaller.uninstallDynamicPlugin(parent, pluginDescriptor, false)) {
160         uninstallsRequiringRestart.add(pluginDescriptor.getPluginId());
161       }
162       else {
163         enabledMap.remove(pluginDescriptor.getPluginId());
164       }
165     }
166
167     boolean installsRequiringRestart = myInstallsRequiringRestart;
168
169     for (PendingDynamicPluginInstall pendingPluginInstall : myDynamicPluginsToInstall.values()) {
170       if (!uninstallsRequiringRestart.contains(pendingPluginInstall.getPluginDescriptor().getPluginId())) {
171         InstalledPluginsState.getInstance().trackPluginInstallation(() ->
172           PluginInstaller.installAndLoadDynamicPlugin(pendingPluginInstall.getFile(), parent,
173                                                       pendingPluginInstall.getPluginDescriptor())
174         );
175       }
176       else {
177         try {
178           PluginInstaller.installAfterRestart(pendingPluginInstall.getFile(), true, null, pendingPluginInstall.getPluginDescriptor());
179           installsRequiringRestart = true;
180         }
181         catch (IOException e) {
182           LOG.error(e);
183         }
184       }
185     }
186
187     myDynamicPluginsToInstall.clear();
188     myPluginsToRemoveOnCancel.clear();
189
190     boolean enableDisableAppliedWithoutRestart = applyEnableDisablePlugins(parent, enabledMap);
191     myDynamicPluginsToUninstall.clear();
192     boolean changesAppliedWithoutRestart = enableDisableAppliedWithoutRestart && uninstallsRequiringRestart.isEmpty() && !installsRequiringRestart;
193     if (!changesAppliedWithoutRestart) {
194       InstalledPluginsState.getInstance().setRestartRequired(true);
195     }
196     return changesAppliedWithoutRestart;
197   }
198
199   public void removePluginsOnCancel(@Nullable JComponent parentComponent) {
200     myPluginsToRemoveOnCancel.forEach(pluginDescriptor -> PluginInstaller.uninstallDynamicPlugin(parentComponent, pluginDescriptor, false));
201     myPluginsToRemoveOnCancel.clear();
202   }
203
204   private boolean applyEnableDisablePlugins(JComponent parentComponent, Map<PluginId, Boolean> enabledMap) {
205     List<IdeaPluginDescriptor> pluginDescriptorsToDisable = new ArrayList<>();
206     List<IdeaPluginDescriptor> pluginDescriptorsToEnable = new ArrayList<>();
207
208     for (IdeaPluginDescriptor descriptor : view) {
209       if (myDynamicPluginsToUninstall.contains(descriptor)) {
210         continue;
211       }
212
213       PluginId pluginId = descriptor.getPluginId();
214       if (enabledMap.get(pluginId) == null) { // if enableMap contains null for id => enable/disable checkbox don't touch
215         continue;
216       }
217       boolean shouldEnable = isEnabled(pluginId);
218       if (shouldEnable != descriptor.isEnabled()) {
219         if (shouldEnable) {
220           pluginDescriptorsToEnable.add(descriptor);
221         }
222         else {
223           pluginDescriptorsToDisable.add(descriptor);
224         }
225       }
226       else if (!shouldEnable && myErrorPluginsToDisable.contains(pluginId)) {
227         pluginDescriptorsToDisable.add(descriptor);
228       }
229     }
230
231     return PluginEnabler.updatePluginEnabledState(pluginDescriptorsToEnable, pluginDescriptorsToDisable, parentComponent);
232   }
233
234   public void pluginInstalledFromDisk(@NotNull PluginInstallCallbackData callbackData) {
235     appendOrUpdateDescriptor(callbackData.getPluginDescriptor(), callbackData.getRestartNeeded());
236     if (!callbackData.getRestartNeeded()) {
237       myDynamicPluginsToInstall.put(callbackData.getPluginDescriptor().getPluginId(),
238                                     new PendingDynamicPluginInstall(callbackData.getFile(), callbackData.getPluginDescriptor()));
239     }
240   }
241
242   public void addComponent(@NotNull ListPluginComponent component) {
243     if (!component.isMarketplace()) {
244       if (myInstallingPlugins.contains(component.myPlugin)) {
245         return;
246       }
247
248       myInstalledPluginComponents.add(component);
249
250       List<ListPluginComponent> components =
251         myInstalledPluginComponentMap.computeIfAbsent(component.myPlugin.getPluginId(), __ -> new ArrayList<>());
252       components.add(component);
253     }
254     else {
255       List<ListPluginComponent> components =
256         myMarketplacePluginComponentMap.computeIfAbsent(component.myPlugin.getPluginId(), __ -> new ArrayList<>());
257       components.add(component);
258     }
259   }
260
261   public void removeComponent(@NotNull ListPluginComponent component) {
262     if (!component.isMarketplace()) {
263       myInstalledPluginComponents.remove(component);
264
265       List<ListPluginComponent> components = myInstalledPluginComponentMap.get(component.myPlugin.getPluginId());
266       if (components != null) {
267         components.remove(component);
268         if (components.isEmpty()) {
269           myInstalledPluginComponentMap.remove(component.myPlugin.getPluginId());
270         }
271       }
272     }
273     else {
274       List<ListPluginComponent> components = myMarketplacePluginComponentMap.get(component.myPlugin.getPluginId());
275       if (components != null) {
276         components.remove(component);
277         if (components.isEmpty()) {
278           myMarketplacePluginComponentMap.remove(component.myPlugin.getPluginId());
279         }
280       }
281     }
282   }
283
284   public void setTopController(@NotNull Configurable.TopComponentController topController) {
285     myTopController = topController;
286
287     for (InstallPluginInfo info : myInstallingInfos.values()) {
288       info.fromBackground(this);
289     }
290     if (!myInstallingInfos.isEmpty()) {
291       myTopController.showProgress(true);
292     }
293   }
294
295   public void setPluginUpdatesService(@NotNull PluginUpdatesService service) {
296     myPluginUpdatesService = service;
297   }
298
299   @Nullable
300   public PluginsGroup getDownloadedGroup() {
301     return myDownloaded;
302   }
303
304   @NotNull
305   public static Set<IdeaPluginDescriptor> getInstallingPlugins() {
306     return myInstallingPlugins;
307   }
308
309   static boolean isInstallingOrUpdate(@NotNull IdeaPluginDescriptor descriptor) {
310     return myInstallingWithUpdatesPlugins.contains(descriptor);
311   }
312
313   void installOrUpdatePlugin(@Nullable JComponent parentComponent,
314                              @NotNull IdeaPluginDescriptor descriptor,
315                              @Nullable IdeaPluginDescriptor updateDescriptor,
316                              @NotNull ModalityState modalityState) {
317     IdeaPluginDescriptor actionDescriptor = updateDescriptor == null ? descriptor : updateDescriptor;
318
319     if (!PluginManagerMain.checkThirdPartyPluginsAllowed(Collections.singletonList(actionDescriptor))) {
320       return;
321     }
322
323     boolean allowUninstallWithoutRestart = true;
324     if (updateDescriptor != null) {
325       IdeaPluginDescriptorImpl installedPluginDescriptor = PluginEnabler.tryLoadFullDescriptor((IdeaPluginDescriptorImpl) descriptor);
326       if (installedPluginDescriptor == null || !DynamicPlugins.allowLoadUnloadWithoutRestart(installedPluginDescriptor)) {
327         allowUninstallWithoutRestart = false;
328       }
329       else if (!installedPluginDescriptor.isEnabled()) {
330         FileUtil.delete(installedPluginDescriptor.getPath());
331       }
332       else if (DynamicPlugins.allowLoadUnloadSynchronously(installedPluginDescriptor)) {
333         if (!PluginInstaller.uninstallDynamicPlugin(parentComponent, installedPluginDescriptor, true)) {
334           allowUninstallWithoutRestart = false;
335         }
336       }
337       else {
338         performUninstall(installedPluginDescriptor);
339       }
340     }
341
342     PluginNode pluginNode;
343     if (actionDescriptor instanceof PluginNode) {
344       pluginNode = (PluginNode)actionDescriptor;
345     }
346     else {
347       pluginNode = new PluginNode(actionDescriptor.getPluginId(), actionDescriptor.getName(), "-1");
348       pluginNode.setDepends(Arrays.asList(actionDescriptor.getDependentPluginIds()), actionDescriptor.getOptionalDependentPluginIds());
349       pluginNode.setRepositoryName(PluginInstaller.UNKNOWN_HOST_MARKER);
350     }
351     List<PluginNode> pluginsToInstall = ContainerUtil.newArrayList(pluginNode);
352
353     PluginManagerMain.suggestToEnableInstalledDependantPlugins(this, pluginsToInstall);
354
355     installPlugin(pluginsToInstall, getCustomRepoPlugins(), prepareToInstall(descriptor, updateDescriptor), allowUninstallWithoutRestart,
356                   modalityState);
357   }
358
359   private void installPlugin(@NotNull List<PluginNode> pluginsToInstall,
360                              @NotNull Collection<? extends IdeaPluginDescriptor> customPlugins,
361                              @NotNull InstallPluginInfo info, boolean allowInstallWithoutRestart,
362                              @NotNull ModalityState modalityState) {
363     ApplicationManager.getApplication().executeOnPooledThread(() -> {
364       boolean cancel = false;
365       boolean error = false;
366       boolean showErrors = true;
367       boolean restartRequired = true;
368       List<PendingDynamicPluginInstall> pluginsToInstallSynchronously = new ArrayList<>();
369       try {
370         PluginInstallOperation operation = new PluginInstallOperation(pluginsToInstall, customPlugins, this, info.indicator);
371         operation.setAllowInstallWithoutRestart(allowInstallWithoutRestart);
372         operation.run();
373         for (PendingDynamicPluginInstall install : operation.getPendingDynamicPluginInstalls()) {
374           if (DynamicPlugins.allowLoadUnloadSynchronously(install.getPluginDescriptor())) {
375             pluginsToInstallSynchronously.add(install);
376             myPluginsToRemoveOnCancel.add(install.getPluginDescriptor());
377           }
378           else {
379             myDynamicPluginsToInstall.put(install.getPluginDescriptor().getPluginId(), install);
380           }
381         }
382
383         error = !operation.isSuccess();
384         showErrors = !operation.isShownErrors();
385         restartRequired = operation.isRestartRequired();
386       }
387       catch (ProcessCanceledException e) {
388         cancel = true;
389       }
390       catch (Throwable e) {
391         LOG.error(e);
392         error = true;
393       }
394
395       boolean success = !error;
396       boolean _cancel = cancel;
397       boolean _showErrors = showErrors;
398       boolean finalRestartRequired = restartRequired;
399       ApplicationManager.getApplication()
400         .invokeLater(() -> {
401           for (PendingDynamicPluginInstall install : pluginsToInstallSynchronously) {
402             IdeaPluginDescriptorImpl installedDescriptor =
403               PluginInstaller.installAndLoadDynamicPlugin(install.getFile(), myInstalledPanel, install.getPluginDescriptor());
404             if (installedDescriptor != null && installedDescriptor.getPluginId().equals(info.getDescriptor().getPluginId())) {
405               info.setInstalledDescriptor(installedDescriptor);
406             }
407           }
408           info.finish(success, _cancel, _showErrors, finalRestartRequired);
409         }, modalityState);
410     });
411   }
412
413   public boolean toBackground() {
414     for (InstallPluginInfo info : myInstallingInfos.values()) {
415       info.toBackground(myStatusBar);
416     }
417     return !myInstallingInfos.isEmpty();
418   }
419
420   @NotNull
421   private InstallPluginInfo prepareToInstall(@NotNull IdeaPluginDescriptor descriptor, @Nullable IdeaPluginDescriptor updateDescriptor) {
422     boolean install = updateDescriptor == null;
423     InstallPluginInfo info = new InstallPluginInfo(descriptor, updateDescriptor, this, install);
424     myInstallingInfos.put(descriptor.getPluginId(), info);
425
426     if (myInstallingWithUpdatesPlugins.isEmpty()) {
427       myTopController.showProgress(true);
428     }
429     myInstallingWithUpdatesPlugins.add(descriptor);
430     if (install) {
431       myInstallingPlugins.add(descriptor);
432     }
433
434     if (install && myInstalling != null) {
435       if (myInstalling.ui == null) {
436         myInstalling.descriptors.add(descriptor);
437         myInstalledPanel.addGroup(myInstalling, 0);
438       }
439       else {
440         myInstalledPanel.addToGroup(myInstalling, descriptor);
441       }
442
443       myInstalling.titleWithCount();
444       myInstalledPanel.doLayout();
445     }
446
447     List<ListPluginComponent> gridComponents = myMarketplacePluginComponentMap.get(descriptor.getPluginId());
448     if (gridComponents != null) {
449       for (ListPluginComponent gridComponent : gridComponents) {
450         gridComponent.showProgress();
451       }
452     }
453     List<ListPluginComponent> listComponents = myInstalledPluginComponentMap.get(descriptor.getPluginId());
454     if (listComponents != null) {
455       for (ListPluginComponent listComponent : listComponents) {
456         listComponent.showProgress();
457       }
458     }
459     for (PluginDetailsPageComponent panel : myDetailPanels) {
460       if (panel.myPlugin == descriptor) {
461         panel.showProgress();
462       }
463     }
464
465     return info;
466   }
467
468   /**
469    * @param descriptor          Descriptor on which the installation was requested (can be a PluginNode or an IdeaPluginDescriptorImpl)
470    * @param installedDescriptor If the plugin was loaded synchronously, the descriptor which has actually been installed; otherwise null.
471    */
472   void finishInstall(@NotNull IdeaPluginDescriptor descriptor,
473                      @Nullable IdeaPluginDescriptorImpl installedDescriptor,
474                      boolean success,
475                      boolean showErrors,
476                      boolean restartRequired) {
477     InstallPluginInfo info = finishInstall(descriptor);
478
479     if (myInstallingWithUpdatesPlugins.isEmpty()) {
480       myTopController.showProgress(false);
481     }
482
483     List<ListPluginComponent> marketplaceComponents = myMarketplacePluginComponentMap.get(descriptor.getPluginId());
484     if (marketplaceComponents != null) {
485       for (ListPluginComponent gridComponent : marketplaceComponents) {
486         if (installedDescriptor != null) {
487           gridComponent.myPlugin = installedDescriptor;
488         }
489         gridComponent.hideProgress(success, restartRequired);
490       }
491     }
492     List<ListPluginComponent> installedComponents = myInstalledPluginComponentMap.get(descriptor.getPluginId());
493     if (installedComponents != null) {
494       for (ListPluginComponent listComponent : installedComponents) {
495         if (installedDescriptor != null) {
496           listComponent.myPlugin = installedDescriptor;
497         }
498         listComponent.hideProgress(success, restartRequired);
499       }
500     }
501     for (PluginDetailsPageComponent panel : myDetailPanels) {
502       if (panel.isShowingPlugin(descriptor)) {
503         if (installedDescriptor != null) {
504           panel.myPlugin = installedDescriptor;
505         }
506         panel.hideProgress(success);
507       }
508     }
509
510     if (info.install) {
511       if (myInstalling != null && myInstalling.ui != null) {
512         clearInstallingProgress(descriptor);
513         if (myInstallingPlugins.isEmpty()) {
514           myInstalledPanel.removeGroup(myInstalling);
515         }
516         else {
517           myInstalledPanel.removeFromGroup(myInstalling, descriptor);
518           myInstalling.titleWithCount();
519         }
520         myInstalledPanel.doLayout();
521       }
522       if (success) {
523         appendOrUpdateDescriptor(installedDescriptor != null ? installedDescriptor : descriptor, restartRequired);
524         appendDependsAfterInstall();
525       }
526     }
527     else if (success) {
528       if (myDownloaded != null && myDownloaded.ui != null && restartRequired) {
529         ListPluginComponent component = myDownloaded.ui.findComponent(descriptor);
530         if (component != null) {
531           component.enableRestart();
532         }
533       }
534       if (myUpdates != null) {
535         myUpdates.titleWithCount();
536       }
537       myPluginUpdatesService.finishUpdate(info.updateDescriptor);
538     }
539     else {
540       myPluginUpdatesService.finishUpdate();
541     }
542
543     info.indicator.cancel();
544
545     if (success) {
546       needRestart = true;
547       myInstallsRequiringRestart |= restartRequired;
548     }
549     if (!success && showErrors) {
550       Messages.showErrorDialog(IdeBundle.message("plugins.configurable.plugin.installing.failed", descriptor.getName()),
551                                IdeBundle.message("action.download.and.install.plugin"));
552     }
553   }
554
555   private void clearInstallingProgress(@NotNull IdeaPluginDescriptor descriptor) {
556     if (myInstallingPlugins.isEmpty()) {
557       for (ListPluginComponent listComponent : myInstalling.ui.plugins) {
558         listComponent.clearProgress();
559       }
560     }
561     else {
562       for (ListPluginComponent listComponent : myInstalling.ui.plugins) {
563         if (listComponent.myPlugin == descriptor) {
564           listComponent.clearProgress();
565           return;
566         }
567       }
568     }
569   }
570
571   @NotNull
572   static InstallPluginInfo finishInstall(@NotNull IdeaPluginDescriptor descriptor) {
573     InstallPluginInfo info = myInstallingInfos.remove(descriptor.getPluginId());
574     info.close();
575     myInstallingWithUpdatesPlugins.remove(descriptor);
576     if (info.install) {
577       myInstallingPlugins.remove(descriptor);
578     }
579     return info;
580   }
581
582   static void addProgress(@NotNull IdeaPluginDescriptor descriptor, @NotNull ProgressIndicatorEx indicator) {
583     myInstallingInfos.get(descriptor.getPluginId()).indicator.addStateDelegate(indicator);
584   }
585
586   static void removeProgress(@NotNull IdeaPluginDescriptor descriptor, @NotNull ProgressIndicatorEx indicator) {
587     myInstallingInfos.get(descriptor.getPluginId()).indicator.removeStateDelegate(indicator);
588   }
589
590   public void addEnabledGroup(@NotNull PluginsGroup group) {
591     myEnabledGroups.add(group);
592   }
593
594   public void setDownloadedGroup(@NotNull PluginsGroupComponent panel,
595                                  @NotNull PluginsGroup downloaded,
596                                  @NotNull PluginsGroup installing) {
597     myInstalledPanel = panel;
598     myDownloaded = downloaded;
599     myInstalling = installing;
600   }
601
602   public void setUpdateGroup(@NotNull PluginsGroup group) {
603     myUpdates = group;
604   }
605
606   private void appendDependsAfterInstall() {
607     if (myDownloaded == null || myDownloaded.ui == null) {
608       return;
609     }
610
611     for (IdeaPluginDescriptor descriptor : InstalledPluginsState.getInstance().getInstalledPlugins()) {
612       if (myDownloaded.ui.findComponent(descriptor) != null) {
613         continue;
614       }
615
616       appendOrUpdateDescriptor(descriptor, true);
617
618       String id = descriptor.getPluginId().getIdString();
619
620       for (Entry<PluginId, List<ListPluginComponent>> entry : myMarketplacePluginComponentMap.entrySet()) {
621         if (id.equals(entry.getKey().getIdString())) {
622           for (ListPluginComponent component : entry.getValue()) {
623             component.hideProgress(true, true);
624           }
625           break;
626         }
627       }
628     }
629   }
630
631   public void addDetailPanel(@NotNull PluginDetailsPageComponent detailPanel) {
632     myDetailPanels.add(detailPanel);
633   }
634
635   public void appendOrUpdateDescriptor(@NotNull IdeaPluginDescriptor descriptor, boolean restartNeeded) {
636     PluginId id = descriptor.getPluginId();
637     if (!PluginManagerCore.isPluginInstalled(id)) {
638       int i = view.indexOf(descriptor);
639       if (i < 0) {
640         view.add(descriptor);
641       }
642       else {
643         view.set(i, descriptor);
644       }
645
646       setEnabled(descriptor, true);
647     }
648
649     if (restartNeeded) {
650       needRestart = myInstallsRequiringRestart = true;
651     }
652     if (myDownloaded == null) {
653       return;
654     }
655
656     myVendorsSorted = null;
657     myTagsSorted = null;
658
659     if (myDownloaded.ui == null) {
660       myDownloaded.descriptors.add(descriptor);
661       myDownloaded.titleWithEnabled(this);
662
663       myInstalledPanel.addGroup(myDownloaded, myInstalling == null || myInstalling.ui == null ? 0 : 1);
664       myInstalledPanel.setSelection(myDownloaded.ui.plugins.get(0));
665       myInstalledPanel.doLayout();
666
667       addEnabledGroup(myDownloaded);
668     }
669     else {
670       ListPluginComponent component = myDownloaded.ui.findComponent(descriptor);
671       if (component != null) {
672         myInstalledPanel.setSelection(component);
673         component.enableRestart();
674         return;
675       }
676
677       myInstalledPanel.addToGroup(myDownloaded, descriptor);
678       myDownloaded.titleWithEnabled(this);
679       myInstalledPanel.setSelection(myDownloaded.ui.plugins.get(myDownloaded.descriptors.indexOf(descriptor)));
680       myInstalledPanel.doLayout();
681     }
682   }
683
684   @NotNull
685   public List<String> getVendors() {
686     if (ContainerUtil.isEmpty(myVendorsSorted)) {
687       myVendorsSorted = getVendors(getInstalledDescriptors());
688     }
689     return myVendorsSorted;
690   }
691
692   @NotNull
693   public List<String> getTags() {
694     if (ContainerUtil.isEmpty(myTagsSorted)) {
695       Set<String> allTags = new HashSet<>();
696
697       for (IdeaPluginDescriptor descriptor : getInstalledDescriptors()) {
698         allTags.addAll(PluginManagerConfigurable.getTags(descriptor));
699       }
700
701       myTagsSorted = ContainerUtil.sorted(allTags, String::compareToIgnoreCase);
702     }
703     return myTagsSorted;
704   }
705
706   @NotNull
707   public List<IdeaPluginDescriptor> getInstalledDescriptors() {
708     assert myInstalledPanel != null;
709
710     return myInstalledPanel.getGroups().stream().flatMap(group -> group.plugins.stream()).map(plugin -> plugin.myPlugin)
711       .collect(Collectors.toList());
712   }
713
714   @NotNull
715   public static List<String> getVendors(@NotNull Collection<? extends IdeaPluginDescriptor> descriptors) {
716     Map<String, Integer> vendors = new HashMap<>();
717
718     for (IdeaPluginDescriptor descriptor : descriptors) {
719       String vendor = StringUtil.trim(descriptor.getVendor());
720       if (!StringUtil.isEmptyOrSpaces(vendor)) {
721         Integer count = vendors.get(vendor);
722         if (count == null) {
723           vendors.put(vendor, 1);
724         }
725         else {
726           vendors.put(vendor, count + 1);
727         }
728       }
729     }
730
731     vendors.put("JetBrains", Integer.MAX_VALUE);
732
733     return ContainerUtil.sorted(vendors.keySet(), (v1, v2) -> {
734       int result = vendors.get(v2) - vendors.get(v1);
735       return result == 0 ? v2.compareToIgnoreCase(v1) : result;
736     });
737   }
738
739   public static boolean isVendor(@NotNull IdeaPluginDescriptor descriptor, @NotNull Set<String> vendors) {
740     String vendor = StringUtil.trim(descriptor.getVendor());
741     if (StringUtil.isEmpty(vendor)) {
742       return false;
743     }
744
745     for (String vendorToFind : vendors) {
746       if (vendor.equalsIgnoreCase(vendorToFind) || StringUtil.containsIgnoreCase(vendor, vendorToFind)) {
747         return true;
748       }
749     }
750
751     return false;
752   }
753
754   public boolean isEnabled(@NotNull IdeaPluginDescriptor plugin) {
755     Boolean enabled = getEnabledMap().get(plugin.getPluginId());
756     return enabled == null || enabled;
757   }
758
759   @NotNull
760   String getEnabledTitle(@NotNull IdeaPluginDescriptor plugin) {
761     return isEnabled(plugin)
762            ? IdeBundle.message("plugins.configurable.disable.button")
763            : IdeBundle.message("plugins.configurable.enable.button");
764   }
765
766   void changeEnableDisable(@NotNull IdeaPluginDescriptor plugin) {
767     changeEnableDisable(new IdeaPluginDescriptor[]{plugin}, !isEnabled(plugin));
768   }
769
770   public void changeEnableDisable(IdeaPluginDescriptor @NotNull [] plugins, boolean state) {
771     enableRows(plugins, state);
772     updateAfterEnableDisable();
773     runInvalidFixCallback();
774   }
775
776   @Override
777   public void enablePlugins(Set<? extends IdeaPluginDescriptor> disabled) {
778     changeEnableDisable(disabled.toArray(new IdeaPluginDescriptor[0]), true);
779   }
780
781   @Override
782   public void disablePlugins(Set<? extends IdeaPluginDescriptor> disabled) {
783     changeEnableDisable(disabled.toArray(new IdeaPluginDescriptor[0]), false);
784   }
785
786   void enableRequiredPlugins(@NotNull IdeaPluginDescriptor descriptor) {
787     Set<PluginId> requiredPluginIds = getRequiredPlugins(descriptor.getPluginId());
788     if (ContainerUtil.isEmpty(requiredPluginIds)) {
789       return;
790     }
791
792     List<IdeaPluginDescriptor> allPlugins = getAllPlugins();
793     Set<IdeaPluginDescriptor> requiredPlugins = new HashSet<>();
794
795     for (PluginId pluginId : requiredPluginIds) {
796       IdeaPluginDescriptor result = ContainerUtil.find(allPlugins, d -> pluginId.equals(d.getPluginId()));
797       if (result == null && PluginManagerCore.isModuleDependency(pluginId)) {
798         result = ContainerUtil.find(allPlugins, d -> {
799           if (d instanceof IdeaPluginDescriptorImpl) {
800             return ((IdeaPluginDescriptorImpl)d).getModules().contains(pluginId);
801           }
802           return false;
803         });
804         if (result != null) {
805           getEnabledMap().put(pluginId, Boolean.TRUE);
806         }
807       }
808       if (result != null) {
809         requiredPlugins.add(result);
810       }
811     }
812
813     if (!requiredPlugins.isEmpty()) {
814       enablePlugins(requiredPlugins);
815     }
816   }
817
818   @Override
819   protected void handleBeforeChangeEnableState(@NotNull IdeaPluginDescriptor descriptor, boolean value) {
820     PluginId pluginId = descriptor.getPluginId();
821     myErrorPluginsToDisable.remove(pluginId);
822
823     if (value || descriptor.isEnabled()) {
824       return;
825     }
826
827     if (PluginManagerCore.isIncompatible(descriptor) || hasProblematicDependencies(pluginId)) {
828       myErrorPluginsToDisable.add(pluginId);
829     }
830   }
831
832   private void runInvalidFixCallback() {
833     if (myInvalidFixCallback != null) {
834       ApplicationManager.getApplication().invokeLater(myInvalidFixCallback, ModalityState.any());
835     }
836   }
837
838   public void setInvalidFixCallback(@Nullable Runnable invalidFixCallback) {
839     myInvalidFixCallback = invalidFixCallback;
840   }
841
842   private void updateAfterEnableDisable() {
843     for (ListPluginComponent component : myInstalledPluginComponents) {
844       component.updateEnabledState();
845     }
846     for (PluginDetailsPageComponent detailPanel : myDetailPanels) {
847       detailPanel.updateEnabledState();
848     }
849     for (PluginsGroup group : myEnabledGroups) {
850       group.titleWithEnabled(this);
851     }
852   }
853
854   public void runRestartButton(@NotNull Component component) {
855     if (PluginManagerConfigurable.showRestartDialog() == Messages.YES) {
856       needRestart = true;
857       createShutdownCallback = false;
858
859       DialogWrapper settings = DialogWrapper.findInstance(component);
860       assert settings instanceof SettingsDialog : settings;
861       ((SettingsDialog)settings).applyAndClose(false /* will be saved on app exit */);
862
863       ApplicationManager.getApplication().exit(true, false, true);
864     }
865   }
866
867   static boolean showUninstallDialog(@NotNull Component uiParent, @NotNull List<? extends ListPluginComponent> selection) {
868     int size = selection.size();
869     return showUninstallDialog(uiParent, size == 1 ? selection.get(0).myPlugin.getName() : null, size);
870   }
871
872   static boolean showUninstallDialog(@NotNull Component uiParent, @Nullable String singleName, int count) {
873     String message;
874     if (singleName == null) {
875       message = IdeBundle.message("prompt.uninstall.several.plugins", count);
876     }
877     else {
878       message = IdeBundle.message("prompt.uninstall.plugin", singleName);
879     }
880
881     return Messages.showYesNoDialog(uiParent, message, IdeBundle.message("title.plugin.uninstall"), Messages.getQuestionIcon()) ==
882            Messages.YES;
883   }
884
885   void uninstallAndUpdateUi(@NotNull Component uiParent, @NotNull IdeaPluginDescriptor descriptor) {
886     List<IdeaPluginDescriptor> deps = dependent(descriptor);
887     if (!deps.isEmpty()) {
888       String listOfDeps = StringUtil.join(deps, plugin -> {
889         return "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;" + plugin.getName();
890       }, "<br>");
891       String beginS = deps.size() == 1 ? "" : "s";
892       String middleS = deps.size() == 1 ? "s" : "";
893       String message = "<html>Following plugin" + beginS + " depend" + middleS + " on " + descriptor.getName() +
894                        ". Continue to remove?<br>" + listOfDeps + "</html>";
895       String title = IdeBundle.message("title.plugin.uninstall");
896       if (Messages.showYesNoDialog(uiParent, message, title, Messages.getQuestionIcon()) != Messages.YES) {
897         return;
898       }
899     }
900
901     boolean needRestartForUninstall = performUninstall((IdeaPluginDescriptorImpl)descriptor);
902     needRestart |= descriptor.isEnabled() && needRestartForUninstall;
903     myInstallsRequiringRestart |= needRestartForUninstall;
904
905     List<ListPluginComponent> listComponents = myInstalledPluginComponentMap.get(descriptor.getPluginId());
906     if (listComponents != null) {
907       for (ListPluginComponent listComponent : listComponents) {
908         listComponent.updateAfterUninstall(needRestartForUninstall);
909       }
910     }
911
912     for (ListPluginComponent component : myInstalledPluginComponents) {
913       component.updateErrors();
914     }
915
916     for (PluginDetailsPageComponent panel : myDetailPanels) {
917       if (panel.myPlugin == descriptor) {
918         panel.updateButtons();
919       }
920     }
921   }
922
923   private boolean performUninstall(IdeaPluginDescriptorImpl descriptorImpl) {
924     boolean needRestartForUninstall = true;
925     try {
926       descriptorImpl.setDeleted(true);
927       // Load descriptor to make sure we get back all the data cleared after the descriptor has been loaded
928       IdeaPluginDescriptorImpl fullDescriptor = PluginEnabler.tryLoadFullDescriptor(descriptorImpl);
929       LOG.assertTrue(fullDescriptor != null);
930       needRestartForUninstall = PluginInstaller.prepareToUninstall(fullDescriptor);
931       InstalledPluginsState.getInstance().onPluginUninstall(descriptorImpl, needRestartForUninstall);
932       if (!needRestartForUninstall) {
933         myDynamicPluginsToUninstall.add(fullDescriptor);
934       }
935     }
936     catch (IOException e) {
937       LOG.error(e);
938     }
939     return needRestartForUninstall;
940   }
941
942   @Nullable
943   public static IdeaPluginDescriptor findPlugin(@NotNull PluginId id) {
944     IdeaPluginDescriptor plugin = PluginManagerCore.getPlugin(id);
945     if (plugin == null && PluginManagerCore.isModuleDependency(id)) {
946       IdeaPluginDescriptor descriptor = PluginManagerCore.findPluginByModuleDependency(id);
947       if (descriptor != null) return descriptor;
948     }
949     return plugin;
950   }
951
952   public boolean hasProblematicDependencies(PluginId pluginId) {
953     Set<PluginId> ids = getDependentToRequiredListMap().get(pluginId);
954     if (ContainerUtil.isEmpty(ids)) {
955       return false;
956     }
957
958     for (PluginId id : ids) {
959       IdeaPluginDescriptor plugin = findPlugin(id);
960       if (plugin != null && !isEnabled(plugin)) {
961         return true;
962       }
963     }
964
965     return false;
966   }
967
968   public boolean hasErrors(@NotNull IdeaPluginDescriptor plugin) {
969     PluginId pluginId = plugin.getPluginId();
970     if (PluginManagerCore.isIncompatible(plugin) || hasProblematicDependencies(pluginId)) {
971       return true;
972     }
973     if (!isLoaded(pluginId)) {
974       InstalledPluginsState state = InstalledPluginsState.getInstance();
975       return !state.wasInstalled(pluginId) && !state.wasInstalledWithoutRestart(pluginId);
976     }
977     return false;
978   }
979
980   @NotNull
981   public String getErrorMessage(@NotNull PluginDescriptor pluginDescriptor, @NotNull Ref<? super String> enableAction) {
982     String message;
983
984     Set<PluginId> requiredPlugins = getRequiredPlugins(pluginDescriptor.getPluginId());
985     if (ContainerUtil.isEmpty(requiredPlugins)) {
986       message = "Incompatible with the current " + ApplicationNamesInfo.getInstance().getFullProductName() + " version.";
987     }
988     else if (requiredPlugins.contains(PluginId.getId("com.intellij.modules.ultimate"))) {
989       message = "The plugin requires IntelliJ IDEA Ultimate.";
990     }
991     else {
992       boolean[] enable = {true};
993       String deps = StringUtil.join(requiredPlugins, id -> {
994         IdeaPluginDescriptor plugin = findPlugin(id);
995         if (enable[0] && (plugin == null || PluginManagerCore.isIncompatible(plugin))) {
996           enable[0] = false;
997         }
998         return StringUtil.wrapWithDoubleQuote(plugin != null ? plugin.getName() : id.getIdString());
999       }, ", ");
1000
1001       int size = requiredPlugins.size();
1002       message = IdeBundle.message("new.plugin.manager.incompatible.deps.tooltip", size, deps);
1003       if (enable[0]) {
1004         enableAction.set(IdeBundle.message("new.plugin.manager.incompatible.deps.action", size));
1005       }
1006     }
1007
1008     return message;
1009   }
1010
1011   @NotNull
1012   abstract protected Collection<IdeaPluginDescriptor> getCustomRepoPlugins();
1013
1014   @NotNull
1015   private List<IdeaPluginDescriptor> dependent(@NotNull IdeaPluginDescriptor rootDescriptor) {
1016     ApplicationInfoEx appInfo = ApplicationInfoEx.getInstanceEx();
1017     PluginId rootId = rootDescriptor.getPluginId();
1018
1019     List<IdeaPluginDescriptor> result = new ArrayList<>();
1020     for (IdeaPluginDescriptor plugin : getAllPlugins()) {
1021       PluginId pluginId = plugin.getPluginId();
1022       if (pluginId == rootId || appInfo.isEssentialPlugin(pluginId) || !plugin.isEnabled() || plugin.isImplementationDetail()) {
1023         continue;
1024       }
1025       if (plugin instanceof IdeaPluginDescriptorImpl && ((IdeaPluginDescriptorImpl)plugin).isDeleted()) {
1026         continue;
1027       }
1028
1029       PluginManagerCore.processAllDependencies(plugin, false, PluginManagerCore.buildPluginIdMap(), (id, descriptor) -> {
1030         if (id == rootId) {
1031           result.add(plugin);
1032           return FileVisitResult.TERMINATE;
1033         }
1034         return FileVisitResult.CONTINUE;
1035       });
1036     }
1037     return result;
1038   }
1039
1040   private final Map<String, Icon> myIcons = new HashMap<>(); // local cache for PluginLogo WeakValueMap
1041
1042   @NotNull
1043   public Icon getIcon(@NotNull IdeaPluginDescriptor descriptor, boolean big, boolean jb, boolean error, boolean disabled) {
1044     String key = descriptor.getPluginId().getIdString() + big + jb + error + disabled;
1045     Icon icon = myIcons.get(key);
1046     if (icon == null) {
1047       icon = PluginLogo.getIcon(descriptor, big, jb, error, disabled);
1048       if (icon != PluginLogo.getDefault()) {
1049         myIcons.put(key, icon);
1050       }
1051     }
1052     return icon;
1053   }
1054 }