59695873a77aa3108d6899c6c5c5af8338a45ea4
[idea/community.git] / platform / lang-impl / src / com / intellij / webcore / packaging / ManagePackagesDialog.java
1 /*
2  * Copyright 2000-2014 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.webcore.packaging;
17
18 import com.intellij.icons.AllIcons;
19 import com.intellij.ide.plugins.PluginManagerMain;
20 import com.intellij.openapi.actionSystem.AnActionEvent;
21 import com.intellij.openapi.application.Application;
22 import com.intellij.openapi.application.ApplicationManager;
23 import com.intellij.openapi.application.ModalityState;
24 import com.intellij.openapi.diagnostic.Logger;
25 import com.intellij.openapi.project.Project;
26 import com.intellij.openapi.ui.DialogWrapper;
27 import com.intellij.openapi.ui.Messages;
28 import com.intellij.openapi.util.text.StringUtil;
29 import com.intellij.ui.*;
30 import com.intellij.ui.components.JBList;
31 import com.intellij.util.CatchingConsumer;
32 import com.intellij.util.Function;
33 import com.intellij.util.ObjectUtils;
34 import com.intellij.util.ui.JBUI;
35 import com.intellij.util.ui.PlatformColors;
36 import com.intellij.util.ui.UIUtil;
37 import com.intellij.util.ui.update.UiNotifyConnector;
38 import org.jetbrains.annotations.NotNull;
39 import org.jetbrains.annotations.Nullable;
40
41 import javax.swing.*;
42 import javax.swing.event.ListSelectionEvent;
43 import javax.swing.event.ListSelectionListener;
44 import java.awt.*;
45 import java.awt.event.ActionEvent;
46 import java.awt.event.ActionListener;
47 import java.awt.event.KeyAdapter;
48 import java.awt.event.KeyEvent;
49 import java.io.IOException;
50 import java.util.*;
51 import java.util.List;
52
53 /**
54  * User: catherine
55  * <p/>
56  * UI for installing python packages
57  */
58 public class ManagePackagesDialog extends DialogWrapper {
59   private static final Logger LOG = Logger.getInstance(ManagePackagesDialog.class);
60
61   @NotNull private final Project myProject;
62   private final PackageManagementService myController;
63
64   private JPanel myFilter;
65   private JPanel myMainPanel;
66   private JEditorPane myDescriptionTextArea;
67   private JBList myPackages;
68   private JButton myInstallButton;
69   private JCheckBox myOptionsCheckBox;
70   private JTextField myOptionsField;
71   private JCheckBox myInstallToUser;
72   private JComboBox myVersionComboBox;
73   private JCheckBox myVersionCheckBox;
74   private JButton myManageButton;
75   private final PackagesNotificationPanel myNotificationArea;
76   private JSplitPane mySplitPane;
77   private JPanel myNotificationsAreaPlaceholder;
78   private PackagesModel myPackagesModel;
79   private String mySelectedPackageName;
80   private final Set<String> myInstalledPackages;
81   @Nullable private final PackageManagementService.Listener myPackageListener;
82
83   private Set<String> myCurrentlyInstalling = new HashSet<>();
84   protected final ListSpeedSearch myListSpeedSearch;
85
86   public ManagePackagesDialog(@NotNull Project project, final PackageManagementService packageManagementService,
87                               @Nullable final PackageManagementService.Listener packageListener) {
88     super(project, true);
89     myProject = project;
90     myController = packageManagementService;
91
92     myPackageListener = packageListener;
93     init();
94     setTitle("Available Packages");
95     myPackages = new JBList();
96     myNotificationArea = new PackagesNotificationPanel();
97     myNotificationsAreaPlaceholder.add(myNotificationArea.getComponent(), BorderLayout.CENTER);
98
99     final AnActionButton reloadButton = new AnActionButton("Reload List of Packages", AllIcons.Actions.Refresh) {
100       @Override
101       public void actionPerformed(AnActionEvent e) {
102         myPackages.setPaintBusy(true);
103         final Application application = ApplicationManager.getApplication();
104         application.executeOnPooledThread(() -> {
105           try {
106             myController.reloadAllPackages();
107             initModel();
108             myPackages.setPaintBusy(false);
109           }
110           catch (final IOException e1) {
111             application.invokeLater(() -> {
112               //noinspection DialogTitleCapitalization
113               Messages.showErrorDialog(myMainPanel, "Error updating package list: " + e1.getMessage(), "Reload List of Packages");
114               myPackages.setPaintBusy(false);
115             }, ModalityState.any());
116           }
117         });
118       }
119     };
120     myListSpeedSearch = new ListSpeedSearch(myPackages, new Function<Object, String>() {
121       @Override
122       public String fun(Object o) {
123         if (o instanceof RepoPackage)
124           return ((RepoPackage)o).getName();
125         return "";
126       }
127     });
128     JPanel packagesPanel = ToolbarDecorator.createDecorator(myPackages)
129       .disableAddAction()
130       .disableUpDownActions()
131       .disableRemoveAction()
132       .addExtraAction(reloadButton)
133       .createPanel();
134     packagesPanel.setPreferredSize(new Dimension(JBUI.scale(400), -1));
135     packagesPanel.setMinimumSize(new Dimension(JBUI.scale(100), -1));
136     myPackages.setFixedCellWidth(0);
137     myPackages.setFixedCellHeight(JBUI.scale(22));
138     myPackages.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
139     mySplitPane.setLeftComponent(packagesPanel);
140
141     myPackages.addListSelectionListener(new MyPackageSelectionListener());
142     myInstallToUser.addActionListener(new ActionListener() {
143       @Override
144       public void actionPerformed(ActionEvent event) {
145         myController.installToUserChanged(myInstallToUser.isSelected());
146       }
147     });
148     myOptionsCheckBox.setEnabled(false);
149     myVersionCheckBox.setEnabled(false);
150     myVersionCheckBox.addActionListener(new ActionListener() {
151       @Override
152       public void actionPerformed(ActionEvent event) {
153         myVersionComboBox.setEnabled(myVersionCheckBox.isSelected());
154       }
155     });
156
157     UiNotifyConnector.doWhenFirstShown(myPackages, () -> initModel());
158     myOptionsCheckBox.addActionListener(new ActionListener() {
159       @Override
160       public void actionPerformed(ActionEvent event) {
161         myOptionsField.setEnabled(myOptionsCheckBox.isSelected());
162       }
163     });
164     myInstallButton.setEnabled(false);
165     myDescriptionTextArea.addHyperlinkListener(new PluginManagerMain.MyHyperlinkListener());
166     addInstallAction();
167     myInstalledPackages = new HashSet<>();
168     updateInstalledPackages();
169     addManageAction();
170     myPackages.setCellRenderer(new MyTableRenderer());
171
172     if (myController.canInstallToUser()) {
173       myInstallToUser.setVisible(true);
174       myInstallToUser.setSelected(myController.isInstallToUserSelected());
175       myInstallToUser.setText(myController.getInstallToUserText());
176     }
177     else {
178       myInstallToUser.setVisible(false);
179     }
180     myMainPanel.setPreferredSize(new Dimension(JBUI.scale(900), JBUI.scale(700)));
181   }
182
183   public void selectPackage(@NotNull InstalledPackage pkg) {
184     mySelectedPackageName = pkg.getName();
185     doSelectPackage(mySelectedPackageName);
186   }
187
188   private void addManageAction() {
189     if (myController.getAllRepositories() != null) {
190       myManageButton.addActionListener(new ActionListener() {
191         @Override
192         public void actionPerformed(ActionEvent event) {
193           ManageRepoDialog dialog = new ManageRepoDialog(myProject, myController);
194           dialog.show();
195         }
196       });
197     }
198     else {
199       myManageButton.setVisible(false);
200     }
201   }
202
203   private void addInstallAction() {
204     myInstallButton.addActionListener(new ActionListener() {
205       @Override
206       public void actionPerformed(ActionEvent event) {
207         final Object pyPackage = myPackages.getSelectedValue();
208         if (pyPackage instanceof RepoPackage) {
209           RepoPackage repoPackage = (RepoPackage)pyPackage;
210
211           String extraOptions = null;
212           if (myOptionsCheckBox.isEnabled() && myOptionsCheckBox.isSelected()) {
213             extraOptions = myOptionsField.getText();
214           }
215
216           String version = null;
217           if (myVersionCheckBox.isEnabled() && myVersionCheckBox.isSelected()) {
218             version = (String) myVersionComboBox.getSelectedItem();
219           }
220
221           final PackageManagementService.Listener listener = new PackageManagementService.Listener() {
222             @Override
223             public void operationStarted(final String packageName) {
224               if (!ApplicationManager.getApplication().isDispatchThread()) {
225                 ApplicationManager.getApplication().invokeLater(() -> handleInstallationStarted(packageName), ModalityState.stateForComponent(myMainPanel));
226               }
227               else {
228                 handleInstallationStarted(packageName);
229               }
230             }
231
232             @Override
233             public void operationFinished(final String packageName,
234                                           @Nullable final PackageManagementService.ErrorDescription errorDescription) {
235               if (!ApplicationManager.getApplication().isDispatchThread()) {
236                 ApplicationManager.getApplication().invokeLater(() -> handleInstallationFinished(packageName, errorDescription), ModalityState.stateForComponent(myMainPanel));
237               }
238               else {
239                 handleInstallationFinished(packageName, errorDescription);
240               }
241             }
242           };
243           myController.installPackage(repoPackage, version, false, extraOptions, listener, myInstallToUser.isSelected());
244           myInstallButton.setEnabled(false);
245         }
246       }
247     });
248   }
249
250   private void handleInstallationStarted(String packageName) {
251     setDownloadStatus(true);
252     myCurrentlyInstalling.add(packageName);
253     if (myPackageListener != null) {
254       myPackageListener.operationStarted(packageName);
255     }
256     myPackages.repaint();
257   }
258
259   private void handleInstallationFinished(String packageName, PackageManagementService.ErrorDescription errorDescription) {
260     if (myPackageListener != null) {
261       myPackageListener.operationFinished(packageName, errorDescription);
262     }
263     setDownloadStatus(false);
264     myNotificationArea.showResult(packageName, errorDescription);
265
266     updateInstalledPackages();
267
268     myCurrentlyInstalling.remove(packageName);
269     myPackages.repaint();
270   }
271
272   private void updateInstalledPackages() {
273     ApplicationManager.getApplication().executeOnPooledThread(() -> {
274       try {
275         final Collection<InstalledPackage> installedPackages = myController.getInstalledPackages();
276         UIUtil.invokeLaterIfNeeded(() -> {
277           myInstalledPackages.clear();
278           for (InstalledPackage pkg : installedPackages) {
279             myInstalledPackages.add(pkg.getName());
280           }
281         });
282       }
283       catch(IOException e) {
284         LOG.info("Error updating list of installed packages:" + e);
285       }
286     });
287   }
288
289   public void initModel() {
290     setDownloadStatus(true);
291     final Application application = ApplicationManager.getApplication();
292     application.executeOnPooledThread(() -> {
293       try {
294         myPackagesModel = new PackagesModel(myController.getAllPackages());
295
296         application.invokeLater(() -> {
297           myPackages.setModel(myPackagesModel);
298           ((MyPackageFilter)myFilter).filter();
299           doSelectPackage(mySelectedPackageName);
300           setDownloadStatus(false);
301         }, ModalityState.any());
302       }
303       catch (final IOException e) {
304         application.invokeLater(() -> {
305           if (myMainPanel.isShowing()) {
306             Messages.showErrorDialog(myMainPanel, "Error loading package list:" + e.getMessage(), "Packages");
307           }
308           setDownloadStatus(false);
309         }, ModalityState.any());
310       }
311     });
312   }
313
314   private void doSelectPackage(@Nullable String packageName) {
315     PackagesModel packagesModel = ObjectUtils.tryCast(myPackages.getModel(), PackagesModel.class);
316     if (packageName == null || packagesModel == null) {
317       return;
318     }
319     for (int i = 0; i < packagesModel.getSize(); i++) {
320       RepoPackage repoPackage = packagesModel.getElementAt(i);
321       if (packageName.equals(repoPackage.getName())) {
322         myPackages.setSelectedIndex(i);
323         myPackages.ensureIndexIsVisible(i);
324         break;
325       }
326     }
327   }
328
329   protected void setDownloadStatus(boolean status) {
330     myPackages.setPaintBusy(status);
331   }
332
333   @Override
334   protected JComponent createCenterPanel() {
335     return myMainPanel;
336   }
337
338   private void createUIComponents() {
339     myFilter = new MyPackageFilter();
340   }
341
342   public void setOptionsText(@NotNull String optionsText) {
343     myOptionsField.setText(optionsText);
344   }
345
346   private class MyPackageFilter extends FilterComponent {
347     public MyPackageFilter() {
348       super("PACKAGE_FILTER", 5);
349       getTextEditor().addKeyListener(new KeyAdapter() {
350         public void keyPressed(final KeyEvent e) {
351           if (e.getKeyCode() == KeyEvent.VK_ENTER) {
352             e.consume();
353             filter();
354             myPackages.requestFocus();
355           } else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
356             onEscape(e);
357           }
358         }
359       });
360     }
361
362     public void filter() {
363       if (myPackagesModel != null)
364         myPackagesModel.filter(getFilter());
365     }
366   }
367
368   private class PackagesModel extends CollectionListModel<RepoPackage> {
369     protected final List<RepoPackage> myFilteredOut = new ArrayList<>();
370     protected List<RepoPackage> myView = new ArrayList<>();
371
372     public PackagesModel(List<RepoPackage> packages) {
373       super(packages);
374       myView = packages;
375     }
376
377     public void add(String urlResource, String element) {
378       super.add(new RepoPackage(element, urlResource));
379     }
380
381     protected void filter(final String filter) {
382       final Collection<RepoPackage> toProcess = toProcess();
383
384       toProcess.addAll(myFilteredOut);
385       myFilteredOut.clear();
386
387       final ArrayList<RepoPackage> filtered = new ArrayList<>();
388
389       RepoPackage toSelect = null;
390       for (RepoPackage repoPackage : toProcess) {
391         final String packageName = repoPackage.getName();
392         if (StringUtil.containsIgnoreCase(packageName, filter)) {
393           filtered.add(repoPackage);
394         }
395         else {
396           myFilteredOut.add(repoPackage);
397         }
398         if (StringUtil.equalsIgnoreCase(packageName, filter)) toSelect = repoPackage;
399       }
400       filter(filtered, toSelect);
401     }
402
403     public void filter(List<RepoPackage> filtered, @Nullable final RepoPackage toSelect){
404       myView.clear();
405       myPackages.clearSelection();
406       for (RepoPackage repoPackage : filtered) {
407         myView.add(repoPackage);
408       }
409       if (toSelect != null)
410         myPackages.setSelectedValue(toSelect, true);
411       Collections.sort(myView);
412       fireContentsChanged(this, 0, myView.size());
413     }
414
415     @Override
416     public RepoPackage getElementAt(int index) {
417       return myView.get(index);
418     }
419
420     protected ArrayList<RepoPackage> toProcess() {
421       return new ArrayList<>(myView);
422     }
423
424     @Override
425     public int getSize() {
426       return myView.size();
427     }
428   }
429
430   @Nullable
431   public JComponent getPreferredFocusedComponent() {
432     return myFilter;
433   }
434
435   private class MyPackageSelectionListener implements ListSelectionListener {
436     @Override
437     public void valueChanged(ListSelectionEvent event) {
438       myOptionsCheckBox.setEnabled(myPackages.getSelectedIndex() >= 0);
439       myVersionCheckBox.setEnabled(myPackages.getSelectedIndex() >= 0);
440       myOptionsCheckBox.setSelected(false);
441       myVersionCheckBox.setSelected(false);
442       myVersionComboBox.setEnabled(false);
443       myOptionsField.setEnabled(false);
444       myDescriptionTextArea.setText("<html><body style='text-align: center;padding-top:20px;'>Loading...</body></html>");
445       final Object pyPackage = myPackages.getSelectedValue();
446       if (pyPackage instanceof RepoPackage) {
447         final String packageName = ((RepoPackage)pyPackage).getName();
448         mySelectedPackageName = packageName;
449         myVersionComboBox.removeAllItems();
450         if (myVersionCheckBox.isEnabled()) {
451           myController.fetchPackageVersions(packageName, new CatchingConsumer<List<String>, Exception>() {
452             @Override
453             public void consume(final List<String> releases) {
454               ApplicationManager.getApplication().invokeLater(() -> {
455                 if (myPackages.getSelectedValue() == pyPackage) {
456                   myVersionComboBox.removeAllItems();
457                   for (String release : releases) {
458                     myVersionComboBox.addItem(release);
459                   }
460                 }
461               }, ModalityState.any());
462             }
463
464             @Override
465             public void consume(Exception e) {
466               LOG.info("Error retrieving releases", e);
467             }
468           });
469         }
470         myInstallButton.setEnabled(!myCurrentlyInstalling.contains(packageName));
471
472         myController.fetchPackageDetails(packageName, new CatchingConsumer<String, Exception>() {
473           @Override
474           public void consume(final String details) {
475             UIUtil.invokeLaterIfNeeded(() -> {
476               if (myPackages.getSelectedValue() == pyPackage) {
477                 myDescriptionTextArea.setText(details);
478                 myDescriptionTextArea.setCaretPosition(0);
479               }/* else {
480                  do nothing, because other package gets selected
481               }*/
482             });
483           }
484
485           @Override
486           public void consume(Exception exception) {
487             UIUtil.invokeLaterIfNeeded(() -> myDescriptionTextArea.setText("No information available"));
488             LOG.info("Error retrieving package details", exception);
489           }
490         });
491       }
492       else {
493         myInstallButton.setEnabled(false);
494         myDescriptionTextArea.setText("");
495       }
496     }
497   }
498
499   @NotNull
500   protected Action[] createActions() {
501     return new Action[0];
502   }
503
504   private class MyTableRenderer extends DefaultListCellRenderer {
505     private JLabel myNameLabel = new JLabel();
506     private JLabel myRepositoryLabel = new JLabel();
507     private JPanel myPanel = new JPanel(new BorderLayout());
508
509     private MyTableRenderer() {
510       myPanel.setBorder(BorderFactory.createEmptyBorder(1, 0, 1, 1));
511       // setting border.left on myPanel doesn't prevent from myRepository being painted on left empty area
512       myNameLabel.setBorder(BorderFactory.createEmptyBorder(0, 2, 0, 0));
513
514       myRepositoryLabel.setFont(UIUtil.getLabelFont(UIUtil.FontSize.SMALL));
515       myPanel.add(myNameLabel, BorderLayout.WEST);
516       myPanel.add(myRepositoryLabel, BorderLayout.EAST);
517       myNameLabel.setOpaque(true);
518     }
519
520     @Override
521     public Component getListCellRendererComponent(JList list,
522                                                   Object value,
523                                                   int index,
524                                                   boolean isSelected,
525                                                   boolean cellHasFocus) {
526       if (value instanceof RepoPackage) {
527         RepoPackage repoPackage = (RepoPackage) value;
528         String name = repoPackage.getName();
529         if (myCurrentlyInstalling.contains(name)) {
530           final String colorCode = UIUtil.isUnderDarcula() ? "589df6" : "0000FF";
531           name = "<html><body>" + repoPackage.getName() + " <font color=\"#" + colorCode + "\">(installing)</font></body></html>";
532         }
533         myNameLabel.setText(name);
534         myRepositoryLabel.setText(repoPackage.getRepoUrl());
535         Component orig = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
536         final Color fg = orig.getForeground();
537         myNameLabel.setForeground(myInstalledPackages.contains(name) ? PlatformColors.BLUE : fg);
538       }
539       myRepositoryLabel.setForeground(JBColor.GRAY);
540
541       final Color bg;
542       if (isSelected) {
543         bg = UIUtil.getListSelectionBackground();
544       }
545       else {
546         bg = index % 2 == 1 ? UIUtil.getListBackground() : UIUtil.getDecoratedRowColor();
547       }
548       myPanel.setBackground(bg);
549       myNameLabel.setBackground(bg);
550       myRepositoryLabel.setBackground(bg);
551       return myPanel;
552     }
553   }
554 }