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