blob: e7601a6b0fc0e727326aaf22a878da8d4d7865d7 [file] [log] [blame]
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.ide.plugins;
import com.intellij.CommonBundle;
import com.intellij.icons.AllIcons;
import com.intellij.ide.BrowserUtil;
import com.intellij.ide.DataManager;
import com.intellij.ide.IdeBundle;
import com.intellij.ide.plugins.sorters.SortByStatusAction;
import com.intellij.ide.ui.search.SearchUtil;
import com.intellij.ide.ui.search.SearchableOptionsRegistrar;
import com.intellij.notification.*;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ApplicationNamesInfo;
import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.application.ex.ApplicationEx;
import com.intellij.openapi.application.ex.ApplicationInfoEx;
import com.intellij.openapi.application.ex.ApplicationManagerEx;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.extensions.PluginId;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.updateSettings.impl.PluginDownloader;
import com.intellij.openapi.updateSettings.impl.UpdateChecker;
import com.intellij.openapi.updateSettings.impl.UpdateSettings;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.ui.*;
import com.intellij.ui.border.CustomLineBorder;
import com.intellij.ui.components.JBLabel;
import com.intellij.ui.speedSearch.SpeedSearchSupply;
import com.intellij.util.concurrency.SwingWorker;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.update.UiNotifyConnector;
import com.intellij.xml.util.XmlStringUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import javax.swing.event.*;
import javax.swing.plaf.BorderUIResource;
import javax.swing.text.html.HTMLDocument;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.HTMLFrameHyperlinkEvent;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.util.*;
import java.util.List;
import static com.intellij.openapi.util.text.StringUtil.isEmptyOrSpaces;
/**
* @author stathik
* @author Konstantin Bulenkov
*/
public abstract class PluginManagerMain implements Disposable {
public static final String JETBRAINS_VENDOR = "JetBrains";
public static final NotificationGroup PLUGIN_LIFECYCLE_NOTIFICATION_GROUP =
new NotificationGroup("Plugins Lifecycle Group", NotificationDisplayType.STICKY_BALLOON, true);
public static Logger LOG = Logger.getInstance("#com.intellij.ide.plugins.PluginManagerMain");
@NonNls private static final String TEXT_PREFIX = "<html><head>" +
" <style type=\"text/css\">" +
" p {" +
" font-family: Arial,serif; font-size: 12pt; margin: 2px 2px" +
" }" +
" </style>" +
"</head><body style=\"font-family: Arial,serif; font-size: 12pt; margin: 5px 5px;\">";
@NonNls private static final String TEXT_SUFFIX = "</body></html>";
@NonNls private static final String HTML_PREFIX = "<a href=\"";
@NonNls private static final String HTML_SUFFIX = "</a>";
private boolean requireShutdown = false;
private JPanel myToolbarPanel;
private JPanel main;
private JEditorPane myDescriptionTextArea;
private JPanel myTablePanel;
protected JPanel myActionsPanel;
private JPanel myHeader;
private PluginHeaderPanel myPluginHeaderPanel;
private JPanel myInfoPanel;
protected JBLabel myPanelDescription;
protected PluginTableModel pluginsModel;
protected PluginTable pluginTable;
private ActionToolbar myActionToolbar;
protected final MyPluginsFilter myFilter = new MyPluginsFilter();
protected PluginManagerUISettings myUISettings;
private boolean myDisposed = false;
private boolean myBusy = false;
public PluginManagerMain(
PluginManagerUISettings uiSettings) {
myUISettings = uiSettings;
}
public static boolean isJetBrainsPlugin(@NotNull IdeaPluginDescriptor plugin) {
return JETBRAINS_VENDOR.equals(plugin.getVendor());
}
protected void init() {
GuiUtils.replaceJSplitPaneWithIDEASplitter(main);
myDescriptionTextArea.setEditorKit(new HTMLEditorKit());
myDescriptionTextArea.setEditable(false);
myDescriptionTextArea.addHyperlinkListener(new MyHyperlinkListener());
JScrollPane installedScrollPane = createTable();
myPluginHeaderPanel = new PluginHeaderPanel(this, getPluginTable());
myHeader.setBackground(UIUtil.getTextFieldBackground());
myPluginHeaderPanel.getPanel().setBackground(UIUtil.getTextFieldBackground());
myPluginHeaderPanel.getPanel().setOpaque(true);
myHeader.add(myPluginHeaderPanel.getPanel(), BorderLayout.CENTER);
installTableActions();
myTablePanel.add(installedScrollPane, BorderLayout.CENTER);
UIUtil.applyStyle(UIUtil.ComponentStyle.SMALL, myPanelDescription);
myPanelDescription.setBorder(new EmptyBorder(0, 7, 0, 0));
final JPanel header = new JPanel(new BorderLayout()) {
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
final Color bg = UIUtil.getTableBackground(false);
((Graphics2D)g).setPaint(new GradientPaint(0, 0, ColorUtil.shift(bg, 1.4), 0, getHeight(), ColorUtil.shift(bg, 0.9)));
g.fillRect(0,0, getWidth(), getHeight());
}
};
header.setBorder(new CustomLineBorder(1, 1, 0, 1));
final JLabel mySortLabel = new JLabel();
mySortLabel.setForeground(UIUtil.getLabelDisabledForeground());
mySortLabel.setBorder(new EmptyBorder(1, 1, 1, 5));
mySortLabel.setIcon(AllIcons.General.SplitDown);
mySortLabel.setHorizontalTextPosition(SwingConstants.LEADING);
header.add(mySortLabel, BorderLayout.EAST);
myTablePanel.add(header, BorderLayout.NORTH);
myToolbarPanel.setLayout(new BorderLayout());
myActionToolbar = ActionManager.getInstance().createActionToolbar("PluginManager", getActionGroup(true), true);
final JComponent component = myActionToolbar.getComponent();
myToolbarPanel.add(component, BorderLayout.CENTER);
myToolbarPanel.add(myFilter, BorderLayout.WEST);
new ClickListener() {
@Override
public boolean onClick(@NotNull MouseEvent event, int clickCount) {
JBPopupFactory.getInstance().createActionGroupPopup("Sort by:", createSortersGroup(), DataManager.getInstance().getDataContext(pluginTable),
JBPopupFactory.ActionSelectionAid.SPEEDSEARCH, true).showUnderneathOf(mySortLabel);
return true;
}
}.installOn(mySortLabel);
final TableModelListener modelListener = new TableModelListener() {
@Override
public void tableChanged(TableModelEvent e) {
String text = "Sort by:";
if (pluginsModel.isSortByStatus()) {
text += " status,";
}
if (pluginsModel.isSortByRating()) {
text += " rating,";
}
if (pluginsModel.isSortByDownloads()) {
text += " downloads,";
}
if (pluginsModel.isSortByUpdated()) {
text += " updated,";
}
text += " name";
mySortLabel.setText(text);
}
};
pluginTable.getModel().addTableModelListener(modelListener);
modelListener.tableChanged(null);
Border border = new BorderUIResource.LineBorderUIResource(new JBColor(Gray._220, Gray._55), 1);
myInfoPanel.setBorder(border);
}
protected abstract JScrollPane createTable();
public void dispose() {
myDisposed = true;
}
public boolean isDisposed() {
return myDisposed;
}
public void filter(String filter) {
myFilter.setSelectedItem(filter);
}
public void reset() {
UiNotifyConnector.doWhenFirstShown(getPluginTable(), new Runnable() {
public void run() {
requireShutdown = false;
TableUtil.ensureSelectionExists(getPluginTable());
}
});
}
public PluginTable getPluginTable() {
return pluginTable;
}
public PluginTableModel getPluginsModel() {
return pluginsModel;
}
protected void installTableActions() {
pluginTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
public void valueChanged(ListSelectionEvent e) {
refresh();
}
});
PopupHandler.installUnknownPopupHandler(pluginTable, getActionGroup(false), ActionManager.getInstance());
new MySpeedSearchBar(pluginTable);
}
public void refresh() {
final IdeaPluginDescriptor[] descriptors = pluginTable.getSelectedObjects();
pluginInfoUpdate(descriptors != null && descriptors.length == 1 ? descriptors[0] : null,
myFilter.getFilter(), myDescriptionTextArea, myPluginHeaderPanel, this);
myActionToolbar.updateActionsImmediately();
final JComponent parent = (JComponent)myHeader.getParent();
parent.revalidate();
parent.repaint();
}
public void setRequireShutdown(boolean val) {
requireShutdown |= val;
}
public ArrayList<IdeaPluginDescriptorImpl> getDependentList(IdeaPluginDescriptorImpl pluginDescriptor) {
return pluginsModel.dependent(pluginDescriptor);
}
protected void modifyPluginsList(List<IdeaPluginDescriptor> list) {
IdeaPluginDescriptor[] selected = pluginTable.getSelectedObjects();
pluginsModel.updatePluginsList(list);
pluginsModel.filter(myFilter.getFilter().toLowerCase());
if (selected != null) {
select(selected);
}
}
protected abstract ActionGroup getActionGroup(boolean inToolbar);
protected abstract PluginManagerMain getAvailable();
protected abstract PluginManagerMain getInstalled();
public JPanel getMainPanel() {
return main;
}
protected boolean acceptHost(String host) {
return true;
}
/**
* Start a new thread which downloads new list of plugins from the site in
* the background and updates a list of plugins in the table.
*/
protected void loadPluginsFromHostInBackground() {
setDownloadStatus(true);
new SwingWorker() {
List<IdeaPluginDescriptor> list = null;
List<String> errorMessages = new ArrayList<String>();
public Object construct() {
try {
list = RepositoryHelper.loadPluginsFromRepository(null);
}
catch (Exception e) {
LOG.info(e);
errorMessages.add(e.getMessage());
}
String builtinPluginsUrl = ApplicationInfoEx.getInstanceEx().getBuiltinPluginsUrl();
if (builtinPluginsUrl != null) {
processPluginHost(builtinPluginsUrl, true);
}
for (String host : UpdateSettings.getInstance().myPluginHosts) {
processPluginHost(host, false);
}
return list;
}
void processPluginHost(@NotNull String host, boolean builtIn) {
if (!acceptHost(host)) return;
final Map<PluginId, PluginDownloader> downloaded = new HashMap<PluginId, PluginDownloader>();
try {
UpdateChecker.checkPluginsHost(host, downloaded, false, null);
for (PluginDownloader downloader : downloaded.values()) {
final PluginNode pluginNode = PluginDownloader.createPluginNode(host, downloader);
if (pluginNode != null) {
if (list == null) list = new ArrayList<IdeaPluginDescriptor>();
list.add(pluginNode);
}
}
}
catch (ProcessCanceledException ignore) {
}
catch (FileNotFoundException e) {
LOG.info(e);
}
catch (Exception e) {
if (builtIn) {
LOG.info("built-in repo failed: " + e.toString());
}
else {
LOG.info(e);
errorMessages.add(e.getMessage());
}
}
}
public void finished() {
UIUtil.invokeLaterIfNeeded(new Runnable() {
public void run() {
setDownloadStatus(false);
if (list != null) {
modifyPluginsList(list);
propagateUpdates(list);
}
if (!errorMessages.isEmpty()) {
if (Messages.OK == Messages.showOkCancelDialog(
IdeBundle.message("error.list.of.plugins.was.not.loaded", StringUtil.join(errorMessages, ", ")),
IdeBundle.message("title.plugins"),
CommonBundle.message("button.retry"), CommonBundle.getCancelButtonText(), Messages.getErrorIcon())) {
loadPluginsFromHostInBackground();
}
}
}
});
}
}.start();
}
protected abstract void propagateUpdates(List<IdeaPluginDescriptor> list);
protected void setDownloadStatus(boolean status) {
pluginTable.setPaintBusy(status);
myBusy = status;
}
protected void loadAvailablePlugins() {
ArrayList<IdeaPluginDescriptor> list;
try {
// If we already have a file with downloaded plugins from the last time,
// then read it, load into the list and start the updating process.
// Otherwise just start the process of loading the list and save it
// into the persistent config file for later reading.
File file = new File(PathManager.getPluginsPath(), RepositoryHelper.PLUGIN_LIST_FILE);
if (file.exists()) {
RepositoryContentHandler handler = new RepositoryContentHandler();
SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
parser.parse(file, handler);
list = handler.getPluginsList();
modifyPluginsList(list);
}
}
catch (Exception ex) {
// Nothing to do, just ignore - if nothing can be read from the local
// file just start downloading of plugins' list from the site.
}
loadPluginsFromHostInBackground();
}
public static boolean downloadPlugins(final List<PluginNode> plugins,
final List<IdeaPluginDescriptor> allPlugins,
final Runnable onSuccess,
@Nullable final Runnable cleanup) throws IOException {
final boolean[] result = new boolean[1];
try {
ProgressManager.getInstance().run(new Task.Backgroundable(null, IdeBundle.message("progress.download.plugins"), true, PluginManagerUISettings.getInstance()) {
@Override
public void run(@NotNull ProgressIndicator indicator) {
try {
if (PluginInstaller.prepareToInstall(plugins, allPlugins)) {
ApplicationManager.getApplication().invokeLater(onSuccess);
result[0] = true;
}
}
finally {
if (cleanup != null) cleanup.run();
}
}
});
}
catch (RuntimeException e) {
if (e.getCause() != null && e.getCause() instanceof IOException) {
throw (IOException)e.getCause();
}
else {
throw e;
}
}
return result[0];
}
public boolean isRequireShutdown() {
return requireShutdown;
}
public void ignoreChanges() {
requireShutdown = false;
}
public static void pluginInfoUpdate(IdeaPluginDescriptor plugin,
@Nullable String filter,
@NotNull JEditorPane descriptionTextArea,
@NotNull PluginHeaderPanel header, PluginManagerMain manager) {
if (plugin == null) {
setTextValue(null, filter, descriptionTextArea);
header.getPanel().setVisible(false);
return;
}
StringBuilder sb = new StringBuilder();
header.setPlugin(plugin);
String description = plugin.getDescription();
if (!isEmptyOrSpaces(description)) {
sb.append(description);
}
String changeNotes = plugin.getChangeNotes();
if (!isEmptyOrSpaces(changeNotes)) {
sb.append("<h4>Change Notes</h4>");
sb.append(changeNotes);
}
if (!plugin.isBundled()) {
String vendor = plugin.getVendor();
String vendorEmail = plugin.getVendorEmail();
String vendorUrl = plugin.getVendorUrl();
if (!isEmptyOrSpaces(vendor) || !isEmptyOrSpaces(vendorEmail) || !isEmptyOrSpaces(vendorUrl)) {
sb.append("<h4>Vendor</h4>");
if (!isEmptyOrSpaces(vendor)) {
sb.append(vendor);
}
if (!isEmptyOrSpaces(vendorUrl)) {
sb.append("<br>").append(composeHref(vendorUrl));
}
if (!isEmptyOrSpaces(vendorEmail)) {
sb.append("<br>")
.append(HTML_PREFIX)
.append("mailto:").append(vendorEmail)
.append("\">").append(vendorEmail).append(HTML_SUFFIX);
}
}
String pluginDescriptorUrl = plugin.getUrl();
if (!isEmptyOrSpaces(pluginDescriptorUrl)) {
sb.append("<h4>Plugin homepage</h4>").append(composeHref(pluginDescriptorUrl));
}
String size = plugin instanceof PluginNode ? ((PluginNode)plugin).getSize() : null;
if (!isEmptyOrSpaces(size)) {
sb.append("<h4>Size</h4>").append(PluginManagerColumnInfo.getFormattedSize(size));
}
}
setTextValue(sb, filter, descriptionTextArea);
}
private static void setTextValue(@Nullable StringBuilder text, @Nullable String filter, JEditorPane pane) {
if (text != null) {
text.insert(0, TEXT_PREFIX);
text.append(TEXT_SUFFIX);
pane.setText(SearchUtil.markup(text.toString(), filter).trim());
pane.setCaretPosition(0);
}
else {
pane.setText(TEXT_PREFIX + TEXT_SUFFIX);
}
}
private static String composeHref(String vendorUrl) {
return HTML_PREFIX + vendorUrl + "\">" + vendorUrl + HTML_SUFFIX;
}
public boolean isModified() {
if (requireShutdown) return true;
return false;
}
public String apply() {
final String applyMessage = canApply();
if (applyMessage != null) return applyMessage;
setRequireShutdown(true);
return null;
}
@Nullable
protected String canApply() {
return null;
}
private void createUIComponents() {
myHeader = new JPanel(new BorderLayout()) {
@Override
public Color getBackground() {
return UIUtil.getTextFieldBackground();
}
};
}
public static class MyHyperlinkListener implements HyperlinkListener {
public void hyperlinkUpdate(HyperlinkEvent e) {
if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
JEditorPane pane = (JEditorPane)e.getSource();
if (e instanceof HTMLFrameHyperlinkEvent) {
HTMLFrameHyperlinkEvent evt = (HTMLFrameHyperlinkEvent)e;
HTMLDocument doc = (HTMLDocument)pane.getDocument();
doc.processHTMLFrameHyperlinkEvent(evt);
}
else {
URL url = e.getURL();
if (url != null) {
BrowserUtil.browse(url);
}
}
}
}
}
private static class MySpeedSearchBar extends SpeedSearchBase<PluginTable> {
public MySpeedSearchBar(PluginTable cmp) {
super(cmp);
}
@Override
protected int convertIndexToModel(int viewIndex) {
return getComponent().convertRowIndexToModel(viewIndex);
}
public int getSelectedIndex() {
return myComponent.getSelectedRow();
}
public Object[] getAllElements() {
return myComponent.getElements();
}
public String getElementText(Object element) {
return ((IdeaPluginDescriptor)element).getName();
}
public void selectElement(Object element, String selectedText) {
for (int i = 0; i < myComponent.getRowCount(); i++) {
if (myComponent.getObjectAt(i).getName().equals(((IdeaPluginDescriptor)element).getName())) {
myComponent.setRowSelectionInterval(i, i);
TableUtil.scrollSelectionToVisible(myComponent);
break;
}
}
}
}
public void select(IdeaPluginDescriptor... descriptors) {
pluginTable.select(descriptors);
}
protected static boolean isAccepted(String filter,
Set<String> search,
IdeaPluginDescriptor descriptor) {
if (StringUtil.isEmpty(filter)) return true;
if (isAccepted(search, filter, descriptor.getName())) {
return true;
}
else {
final String description = descriptor.getDescription();
if (description != null && isAccepted(search, filter, description)) {
return true;
}
final String category = descriptor.getCategory();
if (category != null && isAccepted(search, filter, category)) {
return true;
}
final String changeNotes = descriptor.getChangeNotes();
if (changeNotes != null && isAccepted(search, filter, changeNotes)) {
return true;
}
}
return false;
}
private static boolean isAccepted(final Set<String> search,
@NotNull final String filter,
@NotNull final String description) {
if (StringUtil.containsIgnoreCase(description, filter)) return true;
final SearchableOptionsRegistrar optionsRegistrar = SearchableOptionsRegistrar.getInstance();
final HashSet<String> descriptionSet = new HashSet<String>(search);
descriptionSet.removeAll(optionsRegistrar.getProcessedWords(description));
if (descriptionSet.isEmpty()) {
return true;
}
return false;
}
public static void notifyPluginsWereInstalled(@Nullable String pluginName, final Project project) {
notifyPluginsWereUpdated(pluginName != null
? "Plugin \'" + pluginName + "\' was successfully installed"
: "Plugins were installed", project);
}
public static void notifyPluginsWereUpdated(final String title, @Nullable final Project project) {
final ApplicationEx app = ApplicationManagerEx.getApplicationEx();
final boolean restartCapable = app.isRestartCapable();
String message =
restartCapable ? IdeBundle.message("message.idea.restart.required", ApplicationNamesInfo.getInstance().getFullProductName())
: IdeBundle.message("message.idea.shutdown.required", ApplicationNamesInfo.getInstance().getFullProductName());
message += "<br><a href=";
message += restartCapable ? "\"restart\">Restart now" : "\"shutdown\">Shutdown";
message += "</a>";
PLUGIN_LIFECYCLE_NOTIFICATION_GROUP
.createNotification(title,
XmlStringUtil.wrapInHtml(message), NotificationType.INFORMATION,
new NotificationListener() {
@Override
public void hyperlinkUpdate(@NotNull Notification notification,
@NotNull HyperlinkEvent event) {
notification.expire();
if (restartCapable) {
app.restart(true);
}
else {
app.exit(false, true);
}
}
}).notify(project);
}
public class MyPluginsFilter extends FilterComponent {
public MyPluginsFilter() {
super("PLUGIN_FILTER", 5);
}
public void filter() {
getPluginTable().putClientProperty(SpeedSearchSupply.SEARCH_QUERY_KEY, getFilter());
pluginsModel.filter(getFilter().toLowerCase());
TableUtil.ensureSelectionExists(getPluginTable());
}
}
protected class RefreshAction extends DumbAwareAction {
public RefreshAction() {
super("Reload List of Plugins", "Reload list of plugins", AllIcons.Actions.Refresh);
}
@Override
public void actionPerformed(AnActionEvent e) {
loadAvailablePlugins();
myFilter.setFilter("");
}
@Override
public void update(AnActionEvent e) {
e.getPresentation().setEnabled(!myBusy);
}
}
protected DefaultActionGroup createSortersGroup() {
final DefaultActionGroup group = new DefaultActionGroup("Sort by", true);
group.addAction(new SortByStatusAction(pluginTable, pluginsModel));
return group;
}
}