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