/*
* 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.ApplicationNamesInfo;
import com.intellij.openapi.application.ex.ApplicationEx;
import com.intellij.openapi.application.ex.ApplicationManagerEx;
import com.intellij.openapi.diagnostic.Logger;
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.util.text.StringUtil;
import com.intellij.ui.*;
import com.intellij.ui.border.CustomLineBorder;
import com.intellij.ui.components.JBLabel;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.util.concurrency.SwingWorker;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.update.UiNotifyConnector;
import com.intellij.xml.util.XmlStringUtil;
import consulo.annotations.RequiredDispatchThread;
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.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 java.awt.*;
import java.awt.event.MouseEvent;
import java.net.URL;
import java.util.*;
import java.util.List;
import static com.intellij.openapi.util.text.StringUtil.isEmptyOrSpaces;
/**
* @author stathik
* @since Dec 25, 2003
*/
public abstract class PluginManagerMain implements Disposable {
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;
private JBScrollPane myScrollPane;
protected JBLabel myPanelDescription;
private JBScrollPane myPanel;
protected PluginTableModel myPluginsModel;
protected PluginTable myPluginTable;
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;
}
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(JBUI.Borders.empty(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(JBUI.scale(1), JBUI.scale(1), 0, JBUI.scale(1)));
final JLabel mySortLabel = new JLabel();
mySortLabel.setForeground(UIUtil.getLabelDisabledForeground());
mySortLabel.setBorder(JBUI.Borders.empty(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(myPluginTable),
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 (myPluginsModel.isSortByStatus()) {
text += " status,";
}
if (myPluginsModel.isSortByRating()) {
text += " rating,";
}
if (myPluginsModel.isSortByDownloads()) {
text += " downloads,";
}
if (myPluginsModel.isSortByUpdated()) {
text += " updated,";
}
text += " name";
mySortLabel.setText(text);
}
};
myPluginTable.getModel().addTableModelListener(modelListener);
modelListener.tableChanged(null);
Border border = new BorderUIResource.LineBorderUIResource(new JBColor(Gray._220, Gray._55), JBUI.scale(1));
myInfoPanel.setBorder(border);
}
protected abstract JScrollPane createTable();
@Override
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() {
@Override
public void run() {
requireShutdown = false;
TableUtil.ensureSelectionExists(getPluginTable());
}
});
}
public PluginTable getPluginTable() {
return myPluginTable;
}
public PluginTableModel getPluginsModel() {
return myPluginsModel;
}
protected void installTableActions() {
myPluginTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
refresh();
}
});
//PopupHandler.installUnknownPopupHandler(pluginTable, getActionGroup(false), ActionManager.getInstance());
new MySpeedSearchBar(myPluginTable);
}
@RequiredDispatchThread
public void refresh() {
final IdeaPluginDescriptor[] descriptors = myPluginTable.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 myPluginsModel.dependent(pluginDescriptor);
}
protected void modifyPluginsList(List<IdeaPluginDescriptor> list) {
IdeaPluginDescriptor[] selected = myPluginTable.getSelectedObjects();
myPluginsModel.updatePluginsList(list);
myPluginsModel.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;
}
/**
* 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>();
@Override
public Object construct() {
try {
list = RepositoryHelper.loadOnlyPluginsFromRepository(null, consulo.ide.updateSettings.UpdateSettings.getInstance().getChannel());
}
catch (Exception e) {
LOG.info(e);
errorMessages.add(e.getMessage());
}
return list;
}
@Override
public void finished() {
UIUtil.invokeLaterIfNeeded(new Runnable() {
@Override
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) {
myPluginTable.setPaintBusy(status);
myBusy = status;
}
protected void loadAvailablePlugins() {
loadPluginsFromHostInBackground();
}
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;
}
protected DefaultActionGroup createSortersGroup() {
final DefaultActionGroup group = new DefaultActionGroup("Sort by", true);
group.addAction(new SortByStatusAction(myPluginTable, myPluginsModel));
return group;
}
public static class MyHyperlinkListener implements HyperlinkListener {
@Override
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);
}
@Override
public int getSelectedIndex() {
return myComponent.getSelectedRow();
}
@Override
public Object[] getAllElements() {
return myComponent.getElements();
}
@Override
public String getElementText(Object element) {
return ((IdeaPluginDescriptor)element).getName();
}
@Override
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) {
myPluginTable.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(@NotNull Collection<? extends IdeaPluginDescriptor> installed, Project project) {
String pluginName = installed.size() == 1 ? installed.iterator().next().getName() : null;
notifyPluginsWereUpdated(pluginName != null ? "Plugin \'" + pluginName + "\' was successfully installed" : "Plugins were installed", project);
}
public static void notifyPluginsWereUpdated(final String title, 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>";
new NotificationGroup("Plugins Lifecycle Group", NotificationDisplayType.STICKY_BALLOON, true)
.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(true, true);
}
}
}).notify(project);
}
public class MyPluginsFilter extends FilterComponent {
public MyPluginsFilter() {
super("PLUGIN_FILTER", 5);
}
@Override
public void filter() {
myPluginsModel.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);
}
}
}