text:
// Copyright 2000-2021 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.
package com.intellij.ide.plugins;

import com.intellij.execution.process.ProcessIOExecutorService;
import com.intellij.featureStatistics.FeatureUsageTracker;
import com.intellij.icons.AllIcons;
import com.intellij.ide.CopyProvider;
import com.intellij.ide.DataManager;
import com.intellij.ide.IdeBundle;
import com.intellij.ide.plugins.certificates.PluginCertificateManager;
import com.intellij.ide.plugins.marketplace.MarketplaceRequests;
import com.intellij.ide.plugins.newui.*;
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.application.ApplicationInfo;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ApplicationNamesInfo;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.application.ex.ApplicationInfoEx;
import com.intellij.openapi.application.ex.ApplicationManagerEx;
import com.intellij.openapi.application.impl.ApplicationInfoImpl;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.extensions.PluginId;
import com.intellij.openapi.ide.CopyPasteManager;
import com.intellij.openapi.options.Configurable;
import com.intellij.openapi.options.ConfigurationException;
import com.intellij.openapi.options.SearchableConfigurable;
import com.intellij.openapi.options.ShowSettingsUtil;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.ui.popup.JBPopup;
import com.intellij.openapi.ui.popup.JBPopupListener;
import com.intellij.openapi.ui.popup.LightweightWindowEvent;
import com.intellij.openapi.updateSettings.impl.UpdateChecker;
import com.intellij.openapi.updateSettings.impl.UpdateSettings;
import com.intellij.openapi.util.NlsContexts;
import com.intellij.openapi.util.NlsSafe;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.ThrowableNotNullFunction;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.util.text.StringUtilRt;
import com.intellij.ui.JBColor;
import com.intellij.ui.LicensingFacade;
import com.intellij.ui.RelativeFont;
import com.intellij.ui.SimpleTextAttributes;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.ui.components.JBTextField;
import com.intellij.ui.components.fields.ExtendableTextComponent;
import com.intellij.ui.components.labels.LinkLabel;
import com.intellij.ui.components.labels.LinkListener;
import com.intellij.ui.popup.PopupFactoryImpl;
import com.intellij.ui.scale.JBUIScale;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.net.HttpConfigurable;
import com.intellij.util.ui.*;
import org.jetbrains.annotations.*;

import javax.swing.*;
import java.awt.*;
import java.io.IOException;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

/**
 * @author Alexander Lobas
 */
public final class PluginManagerConfigurable
  implements SearchableConfigurable, Configurable.NoScroll, Configurable.NoMargin, Configurable.TopComponentProvider {

  private static final Logger LOG = Logger.getInstance(PluginManagerConfigurable.class);

  public static final String ID = "preferences.pluginManager";
  public static final String SELECTION_TAB_KEY = "PluginConfigurable.selectionTab";

  @SuppressWarnings("UseJBColor") public static final Color MAIN_BG_COLOR =
    JBColor.namedColor("Plugins.background", new JBColor(() -> JBColor.isBright() ? UIUtil.getListBackground() : new Color(0x313335)));
  public static final Color SEARCH_BG_COLOR = JBColor.namedColor("Plugins.SearchField.background", MAIN_BG_COLOR);
  public static final Color SEARCH_FIELD_BORDER_COLOR =
    JBColor.namedColor("Plugins.SearchField.borderColor", new JBColor(0xC5C5C5, 0x515151));

  private static final int MARKETPLACE_TAB = 0;
  private static final int INSTALLED_TAB = 1;

  public static final int ITEMS_PER_GROUP = 9;

  public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("MMM dd, yyyy");
  private static final DecimalFormat K_FORMAT = new DecimalFormat("###.#K");
  private static final DecimalFormat M_FORMAT = new DecimalFormat("###.#M");

  private TabbedPaneHeaderComponent myTabHeaderComponent;
  private MultiPanel myCardPanel;

  private PluginsTab myMarketplaceTab;
  private PluginsTab myInstalledTab;

  private PluginsGroupComponentWithProgress myMarketplacePanel;
  private PluginsGroupComponent myInstalledPanel;

  private Runnable myMarketplaceRunnable;

  private SearchResultPanel myMarketplaceSearchPanel;
  private SearchResultPanel myInstalledSearchPanel;

  private final LinkLabel<Object> myUpdateAll = new LinkLabel<>(IdeBundle.message("plugin.manager.update.all"), null);
  private final JLabel myUpdateCounter = new CountComponent();
  private final CountIcon myCountIcon = new CountIcon();

  private final MyPluginModel myPluginModel;

  private PluginUpdatesService myPluginUpdatesService;

  private List<String> myTagsSorted;
  private List<String> myVendorsSorted;

  private DefaultActionGroup myMarketplaceSortByGroup;
  private Consumer<MarketplaceSortByAction> myMarketplaceSortByCallback;
  private LinkComponent myMarketplaceSortByAction;

  private DefaultActionGroup myInstalledSearchGroup;
  private Consumer<InstalledSearchOptionAction> myInstalledSearchCallback;
  private boolean myInstalledSearchSetState = true;

  private String myLaterSearchQuery;

  public PluginManagerConfigurable(@Nullable Project project) {
    myPluginModel = new MyPluginModel(project);
  }

  public PluginManagerConfigurable() {
    this((Project)null);
  }

  /**
   * @deprecated use {@link PluginManagerConfigurable}
   */
  @Deprecated
  @ApiStatus.ScheduledForRemoval(inVersion = "2021.3")
  public PluginManagerConfigurable(PluginManagerUISettings uiSettings) {
    this();
  }

  @NotNull
  @Override
  public String getId() {
    return ID;
  }

  @Override
  public String getDisplayName() {
    return IdeBundle.message("title.plugins");
  }

  @Override
  public @NotNull JComponent getCenterComponent(@NotNull TopComponentController controller) {
    myPluginModel.setTopController(controller);
    return myTabHeaderComponent;
  }

  public @NotNull JComponent getTopComponent() {
    return getCenterComponent(TopComponentController.EMPTY);
  }

  @Nullable
  @Override
  public JComponent createComponent() {
    myTabHeaderComponent = new TabbedPaneHeaderComponent(createGearActions(), index -> {
      myCardPanel.select(index, true);
      storeSelectionTab(index);

      String query = (index == MARKETPLACE_TAB ? myInstalledTab : myMarketplaceTab).getSearchQuery();
      (index == MARKETPLACE_TAB ? myMarketplaceTab : myInstalledTab).setSearchQuery(query);
    });

    myUpdateAll.setVisible(false);
    myUpdateCounter.setVisible(false);

    myTabHeaderComponent.addTab(IdeBundle.message("plugin.manager.tab.marketplace"), null);
    myTabHeaderComponent.addTab(IdeBundle.message("plugin.manager.tab.installed"), myCountIcon);

    myPluginUpdatesService = PluginUpdatesService.connectWithCounter(countValue -> {
      int count = countValue == null ? 0 : countValue;
      String text = Integer.toString(count);
      boolean visible = count > 0;

      myUpdateAll.setEnabled(true);
      myUpdateAll.setVisible(visible);

      myUpdateCounter.setText(text);
      myUpdateCounter.setVisible(visible);

      myCountIcon.setText(text);
      myTabHeaderComponent.update();
    });
    myPluginModel.setPluginUpdatesService(myPluginUpdatesService);

    createMarketplaceTab();
    createInstalledTab();

    myCardPanel = new MultiPanel() {
      @Override
      protected JComponent create(Integer key) {
        if (key == MARKETPLACE_TAB) {
          return myMarketplaceTab.createPanel();
        }
        if (key == INSTALLED_TAB) {
          return myInstalledTab.createPanel();
        }
        return super.create(key);
      }
    };
    myCardPanel.setMinimumSize(new JBDimension(580, 380));
    myCardPanel.setPreferredSize(new JBDimension(800, 600));

    myTabHeaderComponent.setListener();

    int selectionTab = getStoredSelectionTab();
    myTabHeaderComponent.setSelection(selectionTab);
    myCardPanel.select(selectionTab, true);

    if (myLaterSearchQuery != null) {
      Runnable search = enableSearch(myLaterSearchQuery);
      if (search != null) {
        ApplicationManager.getApplication().invokeLater(search, ModalityState.any());
      }
      myLaterSearchQuery = null;
    }

    return myCardPanel;
  }

  @NotNull
  private DefaultActionGroup createGearActions() {
    DefaultActionGroup actions = new DefaultActionGroup();
    actions.add(new DumbAwareAction(IdeBundle.message("plugin.manager.repositories")) {
      @Override
      public void actionPerformed(@NotNull AnActionEvent e) {
        if (ShowSettingsUtil.getInstance().editConfigurable(myCardPanel, new PluginHostsConfigurable())) {
          resetPanels();
        }
      }
    });
    actions.add(new DumbAwareAction(IdeBundle.message("button.http.proxy.settings")) {
      @Override
      public void actionPerformed(@NotNull AnActionEvent e) {
        if (HttpConfigurable.editConfigurable(myCardPanel)) {
          resetPanels();
        }
      }
    });
    actions.addSeparator();
    actions.add(new DumbAwareAction(IdeBundle.message("plugin.manager.custom.certificates")) {
      @Override
      public void actionPerformed(@NotNull AnActionEvent e) {
        if (ShowSettingsUtil.getInstance().editConfigurable(myCardPanel, new PluginCertificateManager())) {
          resetPanels();
        }
      }
    });
    actions.add(new InstallFromDiskAction());
    actions.addSeparator();
    actions.add(new ChangePluginStateAction(false));
    actions.add(new ChangePluginStateAction(true));

    return actions;
  }

  private static void showRightBottomPopup(@NotNull Component component, @NotNull @Nls String title, @NotNull ActionGroup group) {
    DefaultActionGroup actions = new GroupByActionGroup();
    actions.addSeparator("  " + title);
    actions.addAll(group);

    DataContext context = DataManager.getInstance().getDataContext(component);

    JBPopup popup = new PopupFactoryImpl.ActionGroupPopup(null, actions, context, false, false, false, true, null, -1, null, null);
    popup.addListener(new JBPopupListener() {
      @Override
      public void beforeShown(@NotNull LightweightWindowEvent event) {
        Point location = component.getLocationOnScreen();
        Dimension size = popup.getSize();
        popup.setLocation(new Point(location.x + component.getWidth() - size.width, location.y + component.getHeight()));
      }
    });
    popup.show(component);
  }

  private void resetPanels() {
    CustomPluginRepositoryService.getInstance().clearCache();

    myTagsSorted = null;
    myVendorsSorted = null;

    myPluginUpdatesService.recalculateUpdates();

    if (myMarketplacePanel == null) {
      return;
    }

    int selectionTab = myTabHeaderComponent.getSelectionTab();
    if (selectionTab == MARKETPLACE_TAB) {
      myMarketplaceRunnable.run();
    }
    else {
      myMarketplacePanel.setVisibleRunnable(myMarketplaceRunnable);
    }
  }

  private static int getStoredSelectionTab() {
    int value = PropertiesComponent.getInstance().getInt(SELECTION_TAB_KEY, MARKETPLACE_TAB);
    return value < MARKETPLACE_TAB || value > INSTALLED_TAB ? MARKETPLACE_TAB : value;
  }

  private static void storeSelectionTab(int value) {
    PropertiesComponent.getInstance().setValue(SELECTION_TAB_KEY, value, MARKETPLACE_TAB);
  }

  private void createMarketplaceTab() {
    myMarketplaceTab = new PluginsTab() {
      @Override
      protected void createSearchTextField(int flyDelay) {
        super.createSearchTextField(250);
        mySearchTextField.setHistoryPropertyName("MarketplacePluginsSearchHistory");
      }

      @NotNull
      @Override
      protected PluginDetailsPageComponent createDetailsPanel(@NotNull LinkListener<Object> searchListener) {
        PluginDetailsPageComponent detailPanel = new PluginDetailsPageComponent(myPluginModel, searchListener, true);
        myPluginModel.addDetailPanel(detailPanel);
        return detailPanel;
      }

      @NotNull
      @Override
      protected JComponent createPluginsPanel(@NotNull Consumer<? super PluginsGroupComponent> selectionListener) {
        MultiSelectionEventHandler eventHandler = new MultiSelectionEventHandler();
        myMarketplacePanel =
          new PluginsGroupComponentWithProgress(new PluginListLayout(), eventHandler,
                                                d -> new ListPluginComponent(myPluginModel, d, mySearchListener, true));

        myMarketplacePanel.setSelectionListener(selectionListener);
        registerCopyProvider(myMarketplacePanel);

        //noinspection ConstantConditions
        ((SearchUpDownPopupController)myMarketplaceSearchPanel.controller).setEventHandler(eventHandler);

        Runnable runnable = () -> {
          List<PluginsGroup> groups = new ArrayList<>();

          try {
            Map<String, List<IdeaPluginDescriptor>> customRepositoriesMap = CustomPluginRepositoryService.getInstance().getCustomRepositoryPluginMap();
            try {
              addGroupViaLightDescriptor(groups, IdeBundle.message("plugins.configurable.featured"), "is_featured_search=true",
                                         "/sortBy:featured");
              addGroupViaLightDescriptor(groups, IdeBundle.message("plugins.configurable.new.and.updated"), "orderBy=update+date",
                                         "/sortBy:updated");
              addGroupViaLightDescriptor(groups, IdeBundle.message("plugins.configurable.top.downloads"), "orderBy=downloads",
                                         "/sortBy:downloads");
              addGroupViaLightDescriptor(groups, IdeBundle.message("plugins.configurable.top.rated"), "orderBy=rating", "/sortBy:rating");
            }
            catch (IOException e) {
              LOG.info("Main plugin repository is not available ('" + e.getMessage() + "'). Please check your network settings.");
            }

            for (String host : UpdateSettings.getInstance().getPluginHosts()) {
              List<IdeaPluginDescriptor> allDescriptors = customRepositoriesMap.get(host);
              if (allDescriptors != null) {
                addGroup(groups, IdeBundle.message("plugins.configurable.repository.0", host), "/repository:\"" + host + "\"",
                         descriptors -> {
                           int allSize = allDescriptors.size();
                           descriptors.addAll(ContainerUtil.getFirstItems(allDescriptors, ITEMS_PER_GROUP));
                           PluginsGroup.sortByName(descriptors);
                           return allSize > ITEMS_PER_GROUP;
                         });
              }
            }
          }
          catch (IOException e) {
            LOG.info(e);
          }
          finally {
            ApplicationManager.getApplication().invokeLater(() -> {
              myMarketplacePanel.stopLoading();
              try {
                PluginLogo.startBatchMode();

                for (PluginsGroup group : groups) {
                  myMarketplacePanel.addGroup(group);
                }
              }
              finally {
                PluginLogo.endBatchMode();
              }
              myMarketplacePanel.doLayout();
              myMarketplacePanel.initialSelection();
            }, ModalityState.any());
          }
        };

        myMarketplaceRunnable = () -> {
          myMarketplacePanel.clear();
          myMarketplacePanel.startLoading();
          ApplicationManager.getApplication().executeOnPooledThread(runnable);
        };

        myMarketplacePanel.getEmptyText().setText(IdeBundle.message("plugins.configurable.marketplace.plugins.not.loaded"))
          .appendSecondaryText(IdeBundle.message("message.check.the.internet.connection.and") + " ", StatusText.DEFAULT_ATTRIBUTES, null)
          .appendSecondaryText(IdeBundle.message("message.link.refresh"), SimpleTextAttributes.LINK_PLAIN_ATTRIBUTES,
                               e -> myMarketplaceRunnable.run());

        ApplicationManager.getApplication().executeOnPooledThread(runnable);
        return createScrollPane(myMarketplacePanel, false);
      }

      @Override
      protected void updateMainSelection(@NotNull Consumer<? super PluginsGroupComponent> selectionListener) {
        selectionListener.accept(myMarketplacePanel);
      }

      @NotNull
      @Override
      protected SearchResultPanel createSearchPanel(@NotNull Consumer<? super PluginsGroupComponent> selectionListener) {
        SearchUpDownPopupController marketplaceController = new SearchUpDownPopupController(mySearchTextField) {
          @NotNull
          @Override
          protected List<String> getAttributes() {
            List<String> attributes = new ArrayList<>();
            attributes.add(SearchWords.TAG.getValue());
            attributes.add(SearchWords.SORT_BY.getValue());
            attributes.add(SearchWords.ORGANIZATION.getValue());
            if (!UpdateSettings.getInstance().getPluginHosts().isEmpty()) {
              attributes.add(SearchWords.REPOSITORY.getValue());
            }
            return attributes;
          }

          @Nullable
          @Override
          protected List<String> getValues(@NotNull String attribute) {
            SearchWords word = SearchWords.find(attribute);
            if (word == null) return null;
            switch (word) {
              case TAG:
                if (myTagsSorted == null || myTagsSorted.isEmpty()) {
                  Set<String> allTags = new HashSet<>();
                  for (IdeaPluginDescriptor descriptor : CustomPluginRepositoryService.getInstance().getCustomRepositoryPlugins()) {
                    if (descriptor instanceof PluginNode) {
                      List<String> tags = ((PluginNode)descriptor).getTags();
                      if (tags != null && !tags.isEmpty()) {
                        allTags.addAll(tags);
                      }
                    }
                  }
                  try {
                    ProcessIOExecutorService.INSTANCE.submit(() -> {
                      allTags.addAll(MarketplaceRequests.getInstance().getAllPluginsTags());
                    }).get();
                  }
                  catch (InterruptedException | ExecutionException e) {
                    LOG.error("Error while getting tags from marketplace", e);
                  }
                  myTagsSorted = ContainerUtil.sorted(allTags, String::compareToIgnoreCase);
                }
                return myTagsSorted;
              case SORT_BY:
                return Arrays.asList("downloads", "name", "rating", "updated");
              case ORGANIZATION:
                if (myVendorsSorted == null || myVendorsSorted.isEmpty()) {
                  LinkedHashSet<String> vendors = new LinkedHashSet<>();
                  try {
                    ProcessIOExecutorService.INSTANCE.submit(() -> {
                      vendors.addAll(MarketplaceRequests.getInstance().getAllPluginsVendors());
                    }).get();
                  }
                  catch (InterruptedException | ExecutionException e) {
                    LOG.error("Error while getting vendors from marketplace", e);
                  }
                  myVendorsSorted = new ArrayList<>(vendors);
                }
                return myVendorsSorted;
              case REPOSITORY:
                return UpdateSettings.getInstance().getPluginHosts();
            }
            return null;
          }

          @Override
          protected void showPopupForQuery() {
            showSearchPanel(mySearchTextField.getText());
          }

          @Override
          protected void handleEnter() {
            if (!mySearchTextField.getText().isEmpty()) {
              handleTrigger("marketplace.suggest.popup.enter");
            }
          }

          @Override
          protected void handlePopupListFirstSelection() {
            handleTrigger("marketplace.suggest.popup.select");
          }

          private void handleTrigger(@NonNls String key) {
            if (myPopup != null && myPopup.type == SearchPopup.Type.SearchQuery) {
              FeatureUsageTracker.getInstance().triggerFeatureUsed(key);
            }
          }
        };

        myMarketplaceSortByGroup = new DefaultActionGroup();

        for (SortBySearchOption option : SortBySearchOption.values()) {
          myMarketplaceSortByGroup.addAction(new MarketplaceSortByAction(option));
        }

        myMarketplaceSortByAction = new LinkComponent() {
          @Override
          protected boolean isInClickableArea(Point pt) {
            return true;
          }
        };
        myMarketplaceSortByAction.setIcon(new Icon() {
          @Override
          public void paintIcon(Component c, Graphics g, int x, int y) {
            getIcon().paintIcon(c, g, x, y + 1);
          }

          @Override
          public int getIconWidth() {
            return getIcon().getIconWidth();
          }

          @Override
          public int getIconHeight() {
            return getIcon().getIconHeight();
          }

          @NotNull
          private Icon getIcon() {
            return AllIcons.General.ButtonDropTriangle;
          }
        }); // TODO: icon
        myMarketplaceSortByAction.setPaintUnderline(false);
        myMarketplaceSortByAction.setIconTextGap(JBUIScale.scale(4));
        myMarketplaceSortByAction.setHorizontalTextPosition(SwingConstants.LEFT);
        myMarketplaceSortByAction.setForeground(PluginsGroupComponent.SECTION_HEADER_FOREGROUND);

        //noinspection unchecked
        myMarketplaceSortByAction.setListener(
          (component, __) -> showRightBottomPopup(component.getParent().getParent(), IdeBundle.message("plugins.configurable.sort.by"),
                                                  myMarketplaceSortByGroup), null);

        myMarketplaceSortByCallback = updateAction -> {
          MarketplaceSortByAction removeAction = null;
          MarketplaceSortByAction addAction = null;

          if (updateAction.myState) {
            for (AnAction action : myMarketplaceSortByGroup.getChildren(null)) {
              MarketplaceSortByAction sortByAction = (MarketplaceSortByAction)action;
              if (sortByAction != updateAction && sortByAction.myState) {
                sortByAction.myState = false;
                removeAction = sortByAction;
                break;
              }
            }
            addAction = updateAction;
          }
          else {
            if (updateAction.myOption == SortBySearchOption.Relevance) {
              updateAction.myState = true;
              return;
            }

            for (AnAction action : myMarketplaceSortByGroup.getChildren(null)) {
              MarketplaceSortByAction sortByAction = (MarketplaceSortByAction)action;
              if (sortByAction.myOption == SortBySearchOption.Relevance) {
                sortByAction.myState = true;
                break;
              }
            }

            removeAction = updateAction;
          }

          List<String> queries = new ArrayList<>();
          new SearchQueryParser.Marketplace(mySearchTextField.getText()) {
            @Override
            protected void addToSearchQuery(@NotNull String query) {
              queries.add(query);
            }

            @Override
            protected void handleAttribute(@NotNull String name, @NotNull String value) {
              queries.add(name + SearchQueryParser.wrapAttribute(value));
            }
          };
          if (removeAction != null) {
            String query = removeAction.getQuery();
            if (query != null) {
              queries.remove(query);
            }
          }
          if (addAction != null) {
            String query = addAction.getQuery();
            if (query != null) {
              queries.add(query);
            }
          }

          String query = StringUtil.join(queries, " ");
          mySearchTextField.setTextIgnoreEvents(query);
          if (query.isEmpty()) {
            myMarketplaceTab.hideSearchPanel();
          }
          else {
            myMarketplaceTab.showSearchPanel(query);
          }
        };

        MultiSelectionEventHandler eventHandler = new MultiSelectionEventHandler();
        marketplaceController.setSearchResultEventHandler(eventHandler);

        PluginsGroupComponentWithProgress panel =
          new PluginsGroupComponentWithProgress(new PluginListLayout(), eventHandler,
                                                descriptor -> new ListPluginComponent(myPluginModel, descriptor, mySearchListener, true));

        panel.setSelectionListener(selectionListener);
        registerCopyProvider(panel);

        myMarketplaceSearchPanel =
          new SearchResultPanel(marketplaceController, panel, 0, 0) {
            @Override
            protected void handleQuery(@NotNull String query, @NotNull PluginsGroup result) {
              try {
                Map<String, List<IdeaPluginDescriptor>> customRepositoriesMap = CustomPluginRepositoryService.getInstance().getCustomRepositoryPluginMap();

                SearchQueryParser.Marketplace parser = new SearchQueryParser.Marketplace(query);

                if (!parser.repositories.isEmpty()) {
                  for (String repository : parser.repositories) {
                    List<IdeaPluginDescriptor> descriptors = customRepositoriesMap.get(repository);
                    if (descriptors == null) {
                      continue;
                    }
                    if (parser.searchQuery == null) {
                      result.descriptors.addAll(descriptors);
                    }
                    else {
                      for (IdeaPluginDescriptor descriptor : descriptors) {
                        if (StringUtil.containsIgnoreCase(descriptor.getName(), parser.searchQuery)) {
                          result.descriptors.add(descriptor);
                        }
                      }
                    }
                  }
                  ContainerUtil.removeDuplicates(result.descriptors);
                  result.sortByName();
                  return;
                }

                List<PluginNode> pluginsFromMarketplace = MarketplaceRequests.getInstance().searchPlugins(parser.getUrlQuery(), 10000);
                List<IdeaPluginDescriptor> plugins = UpdateChecker.mergePluginsFromRepositories(
                  pluginsFromMarketplace,
                  ContainerUtil.flatten(customRepositoriesMap.values()), false
                ); // compare plugin versions between marketplace & custom repositories
                result.descriptors.addAll(0, plugins);

                if (parser.searchQuery != null) {
                  List<IdeaPluginDescriptor> descriptors = new ArrayList<>();

                  for (Entry<String, List<IdeaPluginDescriptor>> entry : customRepositoriesMap.entrySet()) {
                    for (IdeaPluginDescriptor descriptor : entry.getValue()) {
                      if (StringUtil.containsIgnoreCase(descriptor.getName(), parser.searchQuery)) {
                        descriptors.add(descriptor);
                      }
                    }
                  }

                  result.descriptors.addAll(0, descriptors);
                }

                ContainerUtil.removeDuplicates(result.descriptors);

                if (!result.descriptors.isEmpty()) {
                  String title = IdeBundle.message("plugin.manager.action.label.sort.by.1");

                  for (AnAction action : myMarketplaceSortByGroup.getChildren(null)) {
                    MarketplaceSortByAction sortByAction = (MarketplaceSortByAction)action;
                    sortByAction.setState(parser);
                    if (sortByAction.myState) {
                      title = IdeBundle.message("plugin.manager.action.label.sort.by",
                                                sortByAction.myOption.myPresentableNameSupplier.get());
                    }
                  }

                  myMarketplaceSortByAction.setText(title);
                  result.addRightAction(myMarketplaceSortByAction);
                }
              }
              catch (IOException e) {
                LOG.info(e);
                ApplicationManager.getApplication().invokeLater(
                  () -> myPanel.getEmptyText()
                    .setText(IdeBundle.message("plugins.configurable.search.result.not.loaded"))
                    .appendSecondaryText(
                      IdeBundle.message("plugins.configurable.check.internet"), StatusText.DEFAULT_ATTRIBUTES, null), ModalityState.any()
                );
              }
            }
          };

        return myMarketplaceSearchPanel;
      }
    };
  }

  private void createInstalledTab() {
    myInstalledSearchGroup = new DefaultActionGroup();

    for (InstalledSearchOption option : InstalledSearchOption.values()) {
      myInstalledSearchGroup.add(new InstalledSearchOptionAction(option));
    }

    myInstalledTab = new PluginsTab() {
      @Override
      protected void createSearchTextField(int flyDelay) {
        super.createSearchTextField(flyDelay);

        JBTextField textField = mySearchTextField.getTextEditor();
        textField.putClientProperty("search.extension", ExtendableTextComponent.Extension
          .create(AllIcons.Actions.More, AllIcons.Actions.More, IdeBundle.message("plugins.configurable.search.options"), // TODO: icon
                  () -> showRightBottomPopup(textField, IdeBundle.message("plugins.configurable.show"), myInstalledSearchGroup)));
        textField.putClientProperty("JTextField.variant", null);
        textField.putClientProperty("JTextField.variant", "search");

        mySearchTextField.setHistoryPropertyName("InstalledPluginsSearchHistory");
      }

      @NotNull
      @Override
      protected PluginDetailsPageComponent createDetailsPanel(@NotNull LinkListener<Object> searchListener) {
        PluginDetailsPageComponent detailPanel = new PluginDetailsPageComponent(myPluginModel, searchListener, false);
        myPluginModel.addDetailPanel(detailPanel);
        return detailPanel;
      }

      @NotNull
      @Override
      protected JComponent createPluginsPanel(@NotNull Consumer<? super PluginsGroupComponent> selectionListener) {
        MultiSelectionEventHandler eventHandler = new MultiSelectionEventHandler();
        myInstalledPanel =
          new PluginsGroupComponent(new PluginListLayout(), eventHandler,
                                    descriptor -> new ListPluginComponent(myPluginModel, descriptor, mySearchListener, false));

        myInstalledPanel.setSelectionListener(selectionListener);
        registerCopyProvider(myInstalledPanel);

        //noinspection ConstantConditions
        ((SearchUpDownPopupController)myInstalledSearchPanel.controller).setEventHandler(eventHandler);

        try {
          PluginLogo.startBatchMode();

          PluginsGroup installing = new PluginsGroup(IdeBundle.message("plugins.configurable.installing"));
          installing.descriptors.addAll(MyPluginModel.getInstallingPlugins());
          if (!installing.descriptors.isEmpty()) {
            installing.sortByName();
            installing.titleWithCount();
            myInstalledPanel.addGroup(installing);
          }

          PluginsGroup downloaded = new PluginsGroup(IdeBundle.message("plugins.configurable.downloaded"));
          downloaded.descriptors.addAll(InstalledPluginsState.getInstance().getInstalledPlugins());

          Map<@NlsSafe String, List<IdeaPluginDescriptor>> bundledGroups = new HashMap<>();
          ApplicationInfoEx appInfo = ApplicationInfoEx.getInstanceEx();
          int downloadedEnabled = 0;
          boolean hideImplDetails = PluginManager.getInstance().hideImplementationDetails();
          String otherCategoryTitle = IdeBundle.message("plugins.configurable.other.bundled");

          for (IdeaPluginDescriptor descriptor : PluginManagerCore.getPlugins()) {
            if (!appInfo.isEssentialPlugin(descriptor.getPluginId())) {
              if (descriptor.isBundled()) {
                if (hideImplDetails && descriptor.isImplementationDetail()) {
                  continue;
                }
                String category = StringUtil.defaultIfEmpty(descriptor.getCategory(), otherCategoryTitle);
                List<IdeaPluginDescriptor> groupDescriptors = bundledGroups.get(category);
                if (groupDescriptors == null) {
                  bundledGroups.put(category, groupDescriptors = new ArrayList<>());
                }
                groupDescriptors.add(descriptor);
              }
              else {
                downloaded.descriptors.add(descriptor);
                if (descriptor.isEnabled()) {
                  downloadedEnabled++;
                }
              }
            }
          }

          if (!downloaded.descriptors.isEmpty()) {
            myUpdateAll.setListener(new LinkListener<>() {
              @Override
              public void linkSelected(LinkLabel<Object> aSource, Object aLinkData) {
                myUpdateAll.setEnabled(false);

                for (UIPluginGroup group : getInstalledGroups()) {
                  for (ListPluginComponent plugin : group.plugins) {
                    plugin.updatePlugin();
                  }
                }
              }
            }, null);
            downloaded.addRightAction(myUpdateAll);

            downloaded.addRightAction(myUpdateCounter);

            downloaded.sortByName();
            downloaded.titleWithCount(downloadedEnabled);
            myInstalledPanel.addGroup(downloaded);
            myPluginModel.addEnabledGroup(downloaded);
          }

          myPluginModel.setDownloadedGroup(myInstalledPanel, downloaded, installing);

          List<PluginsGroup> groups = new ArrayList<>();

          for (Entry<@NlsSafe String, List<IdeaPluginDescriptor>> entry : bundledGroups.entrySet()) {
            PluginsGroup group = new PluginsGroup(entry.getKey()) {
              @Override
              public void titleWithCount(int enabled) {
                rightAction.setText(enabled == 0 ? IdeBundle.message("plugins.configurable.enable.all")
                                                 : IdeBundle.message("plugins.configurable.disable.all"));
              }
            };
            group.descriptors.addAll(entry.getValue());
            group.sortByName();
            group.rightAction = new LinkLabel<>(
              "",
              null,
              (__, ___) -> myPluginModel.changeEnableDisable(
                Set.copyOf(group.descriptors),
                PluginEnableDisableAction.globally(group.rightAction.getText().startsWith("Enable"))
              )
            );
            group.titleWithEnabled(myPluginModel);
            groups.add(group);
          }

          ContainerUtil.sort(groups, (o1, o2) -> StringUtil.compare(o1.title, o2.title, true));
          PluginsGroup otherGroup = ContainerUtil.find(groups, group -> group.title.equals(otherCategoryTitle));
          if (otherGroup != null) {
            groups.remove(otherGroup);
            groups.add(otherGroup);
          }

          for (PluginsGroup group : groups) {
            myInstalledPanel.addGroup(group);
            myPluginModel.addEnabledGroup(group);
          }

          myPluginUpdatesService.calculateUpdates(updates -> {
            if (ContainerUtil.isEmpty(updates)) {
              clearUpdates(myInstalledPanel);
              clearUpdates(myInstalledSearchPanel.getPanel());
            }
            else {
              applyUpdates(myInstalledPanel, updates);
              applyUpdates(myInstalledSearchPanel.getPanel(), updates);
            }
            selectionListener.accept(myInstalledPanel);
          });
        }
        finally {
          PluginLogo.endBatchMode();
        }

        return createScrollPane(myInstalledPanel, true);
      }

      @Override
      protected void updateMainSelection(@NotNull Consumer<? super PluginsGroupComponent> selectionListener) {
        selectionListener.accept(myInstalledPanel);
      }

      @Override
      public void hideSearchPanel() {
        super.hideSearchPanel();
        if (myInstalledSearchSetState) {
          for (AnAction action : myInstalledSearchGroup.getChildren(null)) {
            ((InstalledSearchOptionAction)action).setState(null);
          }
        }
        myPluginModel.setInvalidFixCallback(null);
      }

      @NotNull
      @Override
      protected SearchResultPanel createSearchPanel(@NotNull Consumer<? super PluginsGroupComponent> selectionListener) {
        SearchUpDownPopupController installedController = new SearchUpDownPopupController(mySearchTextField) {
          @NotNull
          @Override
          @NonNls
          protected List<String> getAttributes() {
            return Arrays
              .asList(
                "/downloaded",
                "/outdated",
                "/enabled",
                "/disabled",
                "/invalid",
                "/bundled",
                SearchWords.ORGANIZATION.getValue(),
                SearchWords.TAG.getValue()
              );
          }

          @Nullable
          @Override
          protected List<String> getValues(@NotNull String attribute) {
            if (SearchWords.ORGANIZATION.getValue().equals(attribute)) {
              return myPluginModel.getVendors();
            }
            if (SearchWords.TAG.getValue().equals(attribute)) {
              return myPluginModel.getTags();
            }
            return null;
          }

          @Override
          protected void showPopupForQuery() {
            showSearchPanel(mySearchTextField.getText());
          }
        };

        MultiSelectionEventHandler eventHandler = new MultiSelectionEventHandler();
        installedController.setSearchResultEventHandler(eventHandler);

        PluginsGroupComponent panel =
          new PluginsGroupComponent(new PluginListLayout(), eventHandler,
                                    descriptor -> new ListPluginComponent(myPluginModel, descriptor, mySearchListener, false));

        panel.setSelectionListener(selectionListener);
        registerCopyProvider(panel);

        myInstalledSearchCallback = updateAction -> {
          List<String> queries = new ArrayList<>();
          new SearchQueryParser.Installed(mySearchTextField.getText()) {
            @Override
            protected void addToSearchQuery(@NotNull String query) {
              queries.add(query);
            }

            @Override
            protected void handleAttribute(@NotNull String name, @NotNull String value) {
              if (!updateAction.myState) {
                queries.add(name + (value.isEmpty() ? "" : SearchQueryParser.wrapAttribute(value)));
              }
            }
          };

          if (updateAction.myState) {
            for (AnAction action : myInstalledSearchGroup.getChildren(null)) {
              if (action != updateAction) {
                ((InstalledSearchOptionAction)action).myState = false;
              }
            }

            queries.add(updateAction.getQuery());
          }
          else {
            queries.remove(updateAction.getQuery());
          }

          try {
            myInstalledSearchSetState = false;

            String query = StringUtil.join(queries, " ");
            mySearchTextField.setTextIgnoreEvents(query);
            if (query.isEmpty()) {
              myInstalledTab.hideSearchPanel();
            }
            else {
              myInstalledTab.showSearchPanel(query);
            }
          }
          finally {
            myInstalledSearchSetState = true;
          }
        };

        myInstalledSearchPanel = new SearchResultPanel(installedController, panel, 0, 0) {
          @Override
          protected void setEmptyText(@NotNull String query) {
            myPanel.getEmptyText().setText(IdeBundle.message("plugins.configurable.nothing.found"));
            if (query.contains("/downloaded") || query.contains("/outdated") ||
                query.contains("/enabled") || query.contains("/disabled") ||
                query.contains("/invalid") || query.contains("/bundled")) {
              return;
            }
            myPanel.getEmptyText().appendSecondaryText(IdeBundle.message("plugins.configurable.search.in.marketplace"),
                                                       SimpleTextAttributes.LINK_PLAIN_ATTRIBUTES,
                                                       e -> myTabHeaderComponent.setSelectionWithEvents(MARKETPLACE_TAB));
          }

          @Override
          protected void handleQuery(@NotNull String query, @NotNull PluginsGroup result) {
            myPluginModel.setInvalidFixCallback(null);

            SearchQueryParser.Installed parser = new SearchQueryParser.Installed(query);

            if (myInstalledSearchSetState) {
              for (AnAction action : myInstalledSearchGroup.getChildren(null)) {
                ((InstalledSearchOptionAction)action).setState(parser);
              }
            }

            List<IdeaPluginDescriptor> descriptors = myPluginModel.getInstalledDescriptors();

            if (!parser.vendors.isEmpty()) {
              for (Iterator<IdeaPluginDescriptor> I = descriptors.iterator(); I.hasNext(); ) {
                if (!MyPluginModel.isVendor(I.next(), parser.vendors)) {
                  I.remove();
                }
              }
            }
            if (!parser.tags.isEmpty()) {
              for (Iterator<IdeaPluginDescriptor> I = descriptors.iterator(); I.hasNext(); ) {
                if (!ContainerUtil.intersects(getTags(I.next()), parser.tags)) {
                  I.remove();
                }
              }
            }
            for (Iterator<IdeaPluginDescriptor> I = descriptors.iterator(); I.hasNext(); ) {
              IdeaPluginDescriptor descriptor = I.next();
              if (parser.attributes) {
                if (parser.enabled && (!myPluginModel.isEnabled(descriptor) || myPluginModel.hasErrors(descriptor))) {
                  I.remove();
                  continue;
                }
                if (parser.disabled && (myPluginModel.isEnabled(descriptor) || myPluginModel.hasErrors(descriptor))) {
                  I.remove();
                  continue;
                }
                if (parser.bundled && !descriptor.isBundled()) {
                  I.remove();
                  continue;
                }
                if (parser.downloaded && descriptor.isBundled()) {
                  I.remove();
                  continue;
                }
                if (parser.invalid && !myPluginModel.hasErrors(descriptor)) {
                  I.remove();
                  continue;
                }
                if (parser.needUpdate && !PluginUpdatesService.isNeedUpdate(descriptor)) {
                  I.remove();
                  continue;
                }
              }
              if (parser.searchQuery != null && !containsQuery(descriptor, parser.searchQuery)) {
                I.remove();
              }
            }

            result.descriptors.addAll(descriptors);

            if (!result.descriptors.isEmpty()) {
              if (parser.invalid) {
                myPluginModel.setInvalidFixCallback(() -> {
                  PluginsGroup group = myInstalledSearchPanel.getGroup();
                  if (group.ui == null) {
                    myPluginModel.setInvalidFixCallback(null);
                    return;
                  }

                  PluginsGroupComponent resultPanel = myInstalledSearchPanel.getPanel();

                  for (IdeaPluginDescriptor descriptor : new ArrayList<>(group.descriptors)) {
                    if (!myPluginModel.hasErrors(descriptor)) {
                      resultPanel.removeFromGroup(group, descriptor);
                    }
                  }

                  group.titleWithCount();
                  myInstalledSearchPanel.fullRepaint();

                  if (group.descriptors.isEmpty()) {
                    myPluginModel.setInvalidFixCallback(null);
                    myInstalledSearchPanel.removeGroup();
                  }
                });
              }
              else if (parser.needUpdate) {
                result.rightAction = new LinkLabel<>(IdeBundle.message("plugin.manager.update.all"), null, (__, ___) -> {
                  result.rightAction.setEnabled(false);

                  for (ListPluginComponent plugin : result.ui.plugins) {
                    plugin.updatePlugin();
                  }
                });
              }

              Collection<IdeaPluginDescriptor> updates = PluginUpdatesService.getUpdates();
              if (!ContainerUtil.isEmpty(updates)) {
                myPostFillGroupCallback = () -> {
                  applyUpdates(myPanel, updates);
                  selectionListener.accept(myInstalledPanel);
                };
              }
            }
          }
        };

        return myInstalledSearchPanel;
      }
    };

    myPluginModel.setCancelInstallCallback(descriptor -> {
      if (myInstalledSearchPanel == null) {
        return;
      }

      PluginsGroup group = myInstalledSearchPanel.getGroup();

      if (group.ui != null && group.ui.findComponent(descriptor) != null) {
        myInstalledSearchPanel.getPanel().removeFromGroup(group, descriptor);
        group.titleWithCount();
        myInstalledSearchPanel.fullRepaint();

        if (group.descriptors.isEmpty()) {
          myInstalledSearchPanel.removeGroup();
        }
      }
    });
  }

  private static boolean containsQuery(IdeaPluginDescriptor descriptor, String searchQuery) {
    if (StringUtil.containsIgnoreCase(descriptor.getName(), searchQuery)) return true;

    String description = descriptor.getDescription();
    return description != null && StringUtil.containsIgnoreCase(description, searchQuery);
  }

  private static void clearUpdates(@NotNull PluginsGroupComponent panel) {
    for (UIPluginGroup group : panel.getGroups()) {
      for (ListPluginComponent plugin : group.plugins) {
        plugin.setUpdateDescriptor(null);
      }
    }
  }

  private static void applyUpdates(@NotNull PluginsGroupComponent panel, @NotNull Collection<? extends IdeaPluginDescriptor> updates) {
    for (IdeaPluginDescriptor descriptor : updates) {
      for (UIPluginGroup group : panel.getGroups()) {
        ListPluginComponent component = group.findComponent(descriptor);
        if (component != null) {
          component.setUpdateDescriptor(descriptor);
          break;
        }
      }
    }
  }

  public static void registerCopyProvider(@NotNull PluginsGroupComponent component) {
    CopyProvider copyProvider = new CopyProvider() {
      @Override
      public void performCopy(@NotNull DataContext dataContext) {
        StringBuilder result = new StringBuilder();
        for (ListPluginComponent pluginComponent : component.getSelection()) {
          IdeaPluginDescriptor descriptor = pluginComponent.getPluginDescriptor();
          result.append(descriptor.getName()).append(" (").append(descriptor.getVersion()).append(")\n");
        }
        CopyPasteManager.getInstance().setContents(new TextTransferable(result.substring(0, result.length() - 1)));
      }

      @Override
      public boolean isCopyEnabled(@NotNull DataContext dataContext) {
        return !component.getSelection().isEmpty();
      }

      @Override
      public boolean isCopyVisible(@NotNull DataContext dataContext) {
        return true;
      }
    };

    DataManager.registerDataProvider(component, dataId -> PlatformDataKeys.COPY_PROVIDER.is(dataId) ? copyProvider : null);
  }

  @NotNull
  public static List<String> getTags(@NotNull IdeaPluginDescriptor plugin) {
    List<String> tags = null;
    String productCode = plugin.getProductCode();

    if (plugin instanceof PluginNode) {
      tags = ((PluginNode)plugin).getTags();

      if (productCode != null) {
        if (LicensePanel.isEA2Product(productCode)) {
          if (tags != null && tags.contains(Tags.Paid.name())) {
            tags = new ArrayList<>(tags);
            tags.remove(Tags.Paid.name());
          }
        }
        else if (tags == null) {
          return Collections.singletonList(Tags.Paid.name());
        }
        else if (!tags.contains(Tags.Paid.name())) {
          tags = new ArrayList<>(tags);
          tags.add(Tags.Paid.name());
        }
      }
    }
    else if (productCode != null && !plugin.isBundled() && !LicensePanel.isEA2Product(productCode)) {
      LicensingFacade instance = LicensingFacade.getInstance();
      if (instance != null) {
        String stamp = instance.getConfirmationStamp(productCode);
        if (stamp != null) {
          return Collections.singletonList(stamp.startsWith("eval:") ? Tags.Trial.name() : Tags.Purchased.name());
        }
      }
      return Collections.singletonList(Tags.Paid.name());
    }
    if (ContainerUtil.isEmpty(tags)) {
      return Collections.emptyList();
    }

    if (tags.size() > 1) {
      tags = new ArrayList<>(tags);
      if (tags.remove(Tags.EAP.name())) {
        tags.add(0, Tags.EAP.name());
      }
      if (tags.remove(Tags.Paid.name())) {
        tags.add(0, Tags.Paid.name());
      }
    }

    return tags;
  }

  @NotNull
  public static <T extends Component> T setTinyFont(@NotNull T component) {
    return SystemInfo.isMac ? RelativeFont.TINY.install(component) : component;
  }

  public static int offset5() {
    return JBUIScale.scale(5);
  }

  @Nullable
  public static synchronized @NlsSafe String getDownloads(@NotNull IdeaPluginDescriptor plugin) {
    String downloads = null;
    if (plugin instanceof PluginNode) {
      downloads = ((PluginNode)plugin).getDownloads();
    }
    return getFormatLength(downloads);
  }

  @Nullable
  static synchronized String getFormatLength(@Nullable String len) {
    if (!StringUtil.isEmptyOrSpaces(len)) {
      try {
        long value = Long.parseLong(len);
        if (value > 1000) {
          return value < 1000000 ? K_FORMAT.format(value / 1000D) : M_FORMAT.format(value / 1000000D);
        }
        return Long.toString(value);
      }
      catch (NumberFormatException ignore) {
      }
    }

    return null;
  }

  @Nullable
  public static synchronized @NlsSafe String getLastUpdatedDate(@NotNull IdeaPluginDescriptor plugin) {
    long date = 0;
    if (plugin instanceof PluginNode) {
      date = ((PluginNode)plugin).getDate();
    }
    return date > 0 && date != Long.MAX_VALUE ? DATE_FORMAT.format(new Date(date)) : null;
  }

  @Nullable
  public static @NlsSafe String getRating(@NotNull IdeaPluginDescriptor plugin) {
    String rating = null;
    if (plugin instanceof PluginNode) {
      rating = ((PluginNode)plugin).getRating();
    }
    if (rating != null) {
      try {
        if (Double.valueOf(rating) > 0) {
          return StringUtil.trimEnd(rating, ".0");
        }
      }
      catch (NumberFormatException ignore) {
      }
    }
    return null;
  }

  @Nullable
  public static synchronized @NlsSafe String getSize(@NotNull IdeaPluginDescriptor plugin) {
    String size = null;
    if (plugin instanceof PluginNode) {
      size = ((PluginNode)plugin).getSize();
    }
    if (!StringUtil.isEmptyOrSpaces(size)) {
      try {
        return StringUtilRt.formatFileSize(Long.parseLong(size)).toUpperCase(Locale.ENGLISH);
      }
      catch (NumberFormatException ignore) {
      }
    }
    return null;
  }

  @NotNull
  public static @NlsSafe String getVersion(@NotNull IdeaPluginDescriptor oldPlugin, @NotNull IdeaPluginDescriptor newPlugin) {
    return StringUtil.defaultIfEmpty(oldPlugin.getVersion(), "unknown") +
           " " + UIUtil.rightArrow() + " " +
           StringUtil.defaultIfEmpty(newPlugin.getVersion(), "unknown");
  }

  @Messages.YesNoResult
  public static int showRestartDialog() {
    return showRestartDialog(IdeBundle.message("updates.dialog.title", ApplicationNamesInfo.getInstance().getFullProductName()));
  }

  @Messages.YesNoResult
  public static int showRestartDialog(@NotNull @NlsContexts.DialogTitle String title) {
    return showRestartDialog(title, action -> IdeBundle
      .message("ide.restart.required.message", action, ApplicationNamesInfo.getInstance().getFullProductName()));
  }

  @Messages.YesNoResult
  public static int showRestartDialog(@NotNull @NlsContexts.DialogTitle String title, @NotNull Function<? super String, @Nls String> message) {
    String action =
      IdeBundle.message(ApplicationManager.getApplication().isRestartCapable() ? "ide.restart.action" : "ide.shutdown.action");
    return Messages
      .showYesNoDialog(message.apply(action), title, action, IdeBundle.message("ide.notnow.action"), Messages.getQuestionIcon());
  }

  public static void shutdownOrRestartApp() {
    shutdownOrRestartApp(IdeBundle.message("updates.dialog.title", ApplicationNamesInfo.getInstance().getFullProductName()));
  }

  public static void shutdownOrRestartApp(@NotNull @NlsContexts.DialogTitle String title) {
    if (showRestartDialog(title) == Messages.YES) {
      ApplicationManagerEx.getApplicationEx().restart(true);
    }
  }

  public static void shutdownOrRestartAppAfterInstall(@NotNull String plugin) {
    String title = IdeBundle.message("updates.dialog.title", ApplicationNamesInfo.getInstance().getFullProductName());
    Function<String, String> message = action -> IdeBundle
      .message("plugin.installed.ide.restart.required.message", plugin, action, ApplicationNamesInfo.getInstance().getFullProductName());

    if (showRestartDialog(title, message) == Messages.YES) {
      ApplicationManagerEx.getApplicationEx().restart(true);
    }
  }

  /**
   * @deprecated Please use {@link #showPluginConfigurable(Project, Collection)}.
   */
  @Deprecated(since = "2020.2", forRemoval = true)
  public static void showPluginConfigurable(@Nullable Project project, IdeaPluginDescriptor @NotNull ... descriptors) {
    showPluginConfigurable(project,
                           ContainerUtil.map(descriptors, IdeaPluginDescriptor::getPluginId));
  }

  public static void showPluginConfigurable(@Nullable Project project,
                                            @NotNull Collection<PluginId> pluginIds) {
    PluginManagerConfigurable configurable = new PluginManagerConfigurable(project);
    ShowSettingsUtil.getInstance().editConfigurable(project,
                                                    configurable,
                                                    () -> configurable.select(pluginIds));
  }

  public static void showPluginConfigurable(@Nullable Component parent,
                                            @Nullable Project project,
                                            @NotNull Collection<PluginId> pluginIds) {
    if (parent != null) {
      PluginManagerConfigurable configurable = new PluginManagerConfigurable(project);
      ShowSettingsUtil.getInstance().editConfigurable(parent,
                                                      configurable,
                                                      () -> configurable.select(pluginIds));
    }
    else {
      showPluginConfigurable(project, pluginIds);
    }
  }

  public static void showPluginConfigurableAndEnable(@Nullable Project project,
                                                     @NotNull Set<? extends IdeaPluginDescriptor> descriptors) {
    PluginManagerConfigurable configurable = new PluginManagerConfigurable(project);
    ShowSettingsUtil.getInstance().editConfigurable(project,
                                                    configurable,
                                                    () -> {
                                                      configurable.myPluginModel.enablePlugins(descriptors);
                                                      configurable.select(descriptors);
                                                    });
  }

  private enum SortBySearchOption {
    Downloads(IdeBundle.messagePointer("plugins.configurable.SortBySearchOption.Downloads")),
    Name(IdeBundle.messagePointer("plugins.configurable.SortBySearchOption.Name")),
    Rating(IdeBundle.messagePointer("plugins.configurable.SortBySearchOption.Rating")),
    Relevance(IdeBundle.messagePointer("plugins.configurable.SortBySearchOption.Relevance")),
    Updated(IdeBundle.messagePointer("plugins.configurable.SortBySearchOption.Updated"));

    private final Supplier<@Nls String> myPresentableNameSupplier;

    SortBySearchOption(Supplier<@Nls String> supplier) {myPresentableNameSupplier = supplier;}
  }

  private final class MarketplaceSortByAction extends ToggleAction implements DumbAware {
    private final SortBySearchOption myOption;
    private boolean myState;

    private MarketplaceSortByAction(@NotNull SortBySearchOption option) {
      super(option.myPresentableNameSupplier);
      myOption = option;
    }

    @Override
    public boolean isSelected(@NotNull AnActionEvent e) {
      return myState;
    }

    @Override
    public void setSelected(@NotNull AnActionEvent e, boolean state) {
      myState = state;
      myMarketplaceSortByCallback.accept(this);
    }

    public void setState(@NotNull SearchQueryParser.Marketplace parser) {
      if (myOption == SortBySearchOption.Relevance) {
        myState = parser.sortBy == null;
        getTemplatePresentation().setVisible(
          parser.sortBy == null || !parser.tags.isEmpty() || !parser.vendors.isEmpty() || parser.searchQuery != null
        );
      }
      else {
        myState = parser.sortBy != null && myOption.name().equalsIgnoreCase(parser.sortBy);
      }
    }

    @Nullable
    public String getQuery() {
      switch (myOption) {
        case Downloads:
          return "/sortBy:downloads";
        case Name:
          return "/sortBy:name";
        case Rating:
          return "/sortBy:rating";
        case Updated:
          return "/sortBy:updated";
        case Relevance:
        default:
          return null;
      }
    }
  }

  private enum InstalledSearchOption {
    Downloaded(IdeBundle.messagePointer("plugins.configurable.InstalledSearchOption.Downloaded")),
    NeedUpdate(IdeBundle.messagePointer("plugins.configurable.InstalledSearchOption.NeedUpdate")),
    Enabled(IdeBundle.messagePointer("plugins.configurable.InstalledSearchOption.Enabled")),
    Disabled(IdeBundle.messagePointer("plugins.configurable.InstalledSearchOption.Disabled")),
    Invalid(IdeBundle.messagePointer("plugins.configurable.InstalledSearchOption.Invalid")),
    Bundled(IdeBundle.messagePointer("plugins.configurable.InstalledSearchOption.Bundled"));

    private final Supplier<@Nls String> myPresentableNameSupplier;

    InstalledSearchOption(Supplier<@Nls String> name) {myPresentableNameSupplier = name;}
  }

  private final class InstalledSearchOptionAction extends ToggleAction implements DumbAware {
    private final InstalledSearchOption myOption;
    private boolean myState;

    private InstalledSearchOptionAction(@NotNull InstalledSearchOption option) {
      super(option.myPresentableNameSupplier);
      myOption = option;
    }

    @Override
    public boolean isSelected(@NotNull AnActionEvent e) {
      return myState;
    }

    @Override
    public void setSelected(@NotNull AnActionEvent e, boolean state) {
      myState = state;
      myInstalledSearchCallback.accept(this);
    }

    public void setState(@Nullable SearchQueryParser.Installed parser) {
      if (parser == null) {
        myState = false;
        return;
      }

      switch (myOption) {
        case Enabled:
          myState = parser.enabled;
          break;
        case Disabled:
          myState = parser.disabled;
          break;
        case Downloaded:
          myState = parser.downloaded;
          break;
        case Bundled:
          myState = parser.bundled;
          break;
        case Invalid:
          myState = parser.invalid;
          break;
        case NeedUpdate:
          myState = parser.needUpdate;
          break;
      }
    }

    @NotNull
    public String getQuery() {
      return myOption == InstalledSearchOption.NeedUpdate ? "/outdated" : "/" + StringUtil.decapitalize(myOption.name());
    }
  }

  private static class GroupByActionGroup extends DefaultActionGroup implements CheckedActionGroup {
  }

  private final class ChangePluginStateAction extends DumbAwareAction {
    private final boolean myEnable;

    private ChangePluginStateAction(boolean enable) {
      super(enable ? IdeBundle.message("plugins.configurable.enable.all.downloaded")
                   : IdeBundle.message("plugins.configurable.disable.all.downloaded"));
      myEnable = enable;
    }

    @Override
    public void actionPerformed(@NotNull AnActionEvent e) {
      Set<IdeaPluginDescriptor> descriptors = new HashSet<>();
      PluginsGroup group = myPluginModel.getDownloadedGroup();

      if (group == null || group.ui == null) {
        ApplicationInfoImpl appInfo = (ApplicationInfoImpl)ApplicationInfo.getInstance();

        for (IdeaPluginDescriptor descriptor : PluginManagerCore.getPlugins()) {
          if (!appInfo.isEssentialPlugin(descriptor.getPluginId()) &&
              !descriptor.isBundled() && descriptor.isEnabled() != myEnable) {
            descriptors.add(descriptor);
          }
        }
      }
      else {
        for (ListPluginComponent component : group.ui.plugins) {
          IdeaPluginDescriptor plugin = component.getPluginDescriptor();
          if (myPluginModel.isEnabled(plugin) != myEnable) {
            descriptors.add(plugin);
          }
        }
      }

      if (!descriptors.isEmpty()) {
        myPluginModel.changeEnableDisable(
          descriptors,
          PluginEnableDisableAction.globally(myEnable)
        );
      }
    }
  }

  @NotNull
  public static JComponent createScrollPane(@NotNull PluginsGroupComponent panel, boolean initSelection) {
    JBScrollPane pane =
      new JBScrollPane(panel, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
    pane.setBorder(JBUI.Borders.empty());
    if (initSelection) {
      panel.initialSelection();
    }
    return pane;
  }

  private void addGroup(
    @NotNull List<? super PluginsGroup> groups,
    @NotNull @Nls String name,
    @NotNull String showAllQuery,
    @NotNull ThrowableNotNullFunction<? super List<IdeaPluginDescriptor>, Boolean, ? extends IOException> function
  )
    throws IOException {
    PluginsGroup group = new PluginsGroup(name);

    if (Boolean.TRUE.equals(function.fun(group.descriptors))) {
      group.rightAction =
        new LinkLabel<>(IdeBundle.message("plugins.configurable.show.all"), null, myMarketplaceTab.mySearchListener, showAllQuery);
      group.rightAction.setBorder(JBUI.Borders.emptyRight(5));
    }

    if (!group.descriptors.isEmpty()) {
      groups.add(group);
    }
  }

  private void addGroupViaLightDescriptor(
    @NotNull List<? super PluginsGroup> groups,
    @NotNull @Nls String name,
    @NotNull @NonNls String query,
    @NotNull @NonNls String showAllQuery
  ) throws IOException {
    addGroup(groups, name, showAllQuery, descriptors -> {
      List<PluginNode> pluginNodes = MarketplaceRequests.getInstance().searchPlugins(query, ITEMS_PER_GROUP * 2);
      descriptors.addAll(ContainerUtil.getFirstItems(pluginNodes, ITEMS_PER_GROUP));
      return pluginNodes.size() >= ITEMS_PER_GROUP;
    });
  }

  @Override
  @NotNull
  public String getHelpTopic() {
    return ID;
  }

  @Override
  public void disposeUIResources() {
    if (myPluginModel.toBackground()) {
      InstallPluginInfo.showRestart();
      InstalledPluginsState.getInstance().clearShutdownCallback();
    }

    myMarketplaceTab.dispose();
    myInstalledTab.dispose();

    if (myMarketplacePanel != null) {
      myMarketplacePanel.dispose();
    }
    if (myMarketplaceSearchPanel != null) {
      myMarketplaceSearchPanel.dispose();
    }

    myPluginUpdatesService.dispose();
    PluginPriceService.cancel();

    InstalledPluginsState.getInstance().runShutdownCallback();

    InstalledPluginsState.getInstance().resetChangesAppliedWithoutRestart();
  }

  @Override
  public void cancel() {
    myPluginModel.cancel(myCardPanel);
  }

  @Override
  public boolean isModified() {
    return myPluginModel.isModified();
  }

  @Override
  public void apply() throws ConfigurationException {
    if (myPluginModel.apply(myCardPanel)) return;

    if (myPluginModel.createShutdownCallback) {
      InstalledPluginsState.getInstance()
        .setShutdownCallback(() -> ApplicationManager.getApplication().invokeLater(() -> shutdownOrRestartApp()));
    }
  }

  @Override
  public void reset() {
    myPluginModel.clear(myCardPanel);
  }

  /**
   * @deprecated Please use {@link #select(Collection)}.
   */
  @Deprecated(since = "2020.2", forRemoval = true)
  public void select(@NotNull IdeaPluginDescriptor @NotNull ... descriptors) {
    select(ContainerUtil.newHashSet(descriptors));
  }

  private void select(@NotNull Set<? extends IdeaPluginDescriptor> descriptors) {
    select(ContainerUtil.map(descriptors, IdeaPluginDescriptor::getPluginId));
  }

  private void select(@NotNull Collection<PluginId> pluginIds) {
    updateSelectionTab(INSTALLED_TAB);

    List<ListPluginComponent> components = new ArrayList<>();

    for (PluginId pluginId : pluginIds) {
      ListPluginComponent component = findInstalledPluginById(pluginId);
      if (component != null) {
        components.add(component);
      }
    }

    if (!components.isEmpty()) {
      myInstalledPanel.setSelection(components);
    }
  }

  @Nullable
  @Override
  public Runnable enableSearch(String option) {
    if (myTabHeaderComponent == null) {
      myLaterSearchQuery = option;
      return () -> {};
    }
    if (StringUtil.isEmpty(option) && (myTabHeaderComponent.getSelectionTab() == MARKETPLACE_TAB || myInstalledSearchPanel.isEmpty())) {
      return null;
    }

    return () -> {
      boolean marketplace = option != null && option.startsWith(SearchWords.TAG.getValue());
      updateSelectionTab(marketplace ? MARKETPLACE_TAB : INSTALLED_TAB);

      PluginsTab tab = marketplace ? myMarketplaceTab : myInstalledTab;
      tab.clearSearchPanel(option);

      if (!StringUtil.isEmpty(option)) {
        tab.showSearchPanel(option);
      }
    };
  }

  private final class InstallFromDiskAction extends DumbAwareAction {
    private InstallFromDiskAction() {super(IdeBundle.messagePointer("action.InstallFromDiskAction.text"));}

    @Override
    public void actionPerformed(@NotNull AnActionEvent e) {
      PluginInstaller.chooseAndInstall(myPluginModel, myCardPanel, callbackData -> {
        myPluginModel.pluginInstalledFromDisk(callbackData);

        boolean select = myInstalledPanel == null;

        updateSelectionTab(INSTALLED_TAB);

        myInstalledTab.clearSearchPanel("");

        if (select) {
          ListPluginComponent component = findInstalledPluginById(callbackData.getPluginDescriptor().getPluginId());
          if (component != null) {
            myInstalledPanel.setSelection(component);
          }
        }
      });
    }
  }

  private void updateSelectionTab(int tab) {
    if (myTabHeaderComponent.getSelectionTab() != tab) {
      myTabHeaderComponent.setSelectionWithEvents(tab);
    }
  }

  private @NotNull List<UIPluginGroup> getInstalledGroups() {
    return myInstalledPanel.getGroups();
  }

  private @Nullable ListPluginComponent findInstalledPluginById(@NotNull PluginId pluginId) {
    for (UIPluginGroup group : getInstalledGroups()) {
      ListPluginComponent component = group.findComponent(pluginId);
      if (component != null) {
        return component;
      }
    }
    return null;
  }
}


from iOS and Android Devices ?
iSkysoft Data Eraser is the World's NO.1 Data Eraser for iOS devices (iPhone, iPad, iPod) and Android devices.
from your device without zero recovery possibility.
iSkysoft Data Eraser is the World's NO.1 Phone Eraser for iOS devices (iPhone, iPad, iPod) and Android devices.
It provides 100% promise to securely erase all the data (SMS, browser history, account information, photos, music, video, cache data and more files) from your device without zero recovery possibility. You don't need to worry about personal information exposure after you donate or sell your phone. iSkysoft Phone Eraser guards your private information and protects your personal data from data criminals.
Afraid of your account information, business secrets, emails, browser history or other private information saved on your mobile devices exposing to others? iSkysoft Phone Eraser can safeguard your private information by permanently erase all your personal data from your iOS devices (iPhone, iPad and iPod) and Android devices.
iSkysoft Data Eraser offers you the convenient 1-click solution to clearing useless and junk files on your devices. Hence your iPhone, iPad, iPod and Android devices can be released for more free storage and the speed of your device will be improved a lot.
The file compression feature of iSkysoft Phone Eraser can make up more free space for your devices by quality lossless file compression.
The data on your phone cannot be deleted by simple deletion or factory reset. With the data recovery software, the deleted data can be recovered. iSkysoft Data Eraser helps you permanently erase your deleted files from your iOS devices and Android devices to safeguard your personal data.
iSkysoft Phone Eraser do does me a big favor for deleting the business records and personal accounts from my phone to secure my personal information a lot. I'd like to highly recommend it to you.
iSkysoft Phone Eraser offers you 5 data erasing modes to help you clear your iOS and Android devices. "Erase All Data" mode can clear all the data on your devices. "Erase Deleted Data" mode can help you permanently wipe out all previous deleted files without recovery possibility. Also there are "Erase Private Data", "Photo Compressor" and "Express Cleanup" modes for you.


text:
Advance Doubles the Longevity of High-Energy Electric Car Batteries
Researchers solve key challenges with energy-dense lithium-air batteries.
Kevin Bullis archive page
Lithium-air batteries, which could give electric cars the same range as gasoline ones, are a step closer to becoming practical. Researchers at Yale and MIT have found a way to alleviate two of the batteries' biggest problems—their inefficiency and inability to be recharged many times.
The researchers developed a nanostructured membrane that reduces the energy needed to recharge the battery, making it more efficient. The advance also allowed an experimental version of the battery to be recharged 60 times without losing storage capacity— roughly double the number of times as previous versions of the battery. (Electric car batteries should last roughly 1,000 recharge cycles.)
Lithium-air batteries are attractive because of their huge theoretical energy storage capacity, which is, by weight, roughly 10 times higher than the conventional lithium-ion batteries used in electric vehicles today. That means an electric car using such batteries could easily travel the 350-plus miles people expect from a tank of gasoline, and the battery could be much smaller and cheaper than conventional batteries. Some research groups have recently abandoned research on lithium-air batteries because they've had trouble getting the batteries to meet their potential. The advance from Yale and MIT shows that some key problems are being solved, although much work still remains before lithium-air batteries can be used commercially in electric cars.
Lithium-air batteries generate electrical current when lithium ions react with oxygen, forming lithium oxide. Recharging them involves reversing this reaction, breaking the bonds between lithium and oxygen atoms and freeing the oxygen. The problem is that lithium oxide forms a coating on one of the battery electrodes, covering up the catalysts needed to free the oxygen efficiently.
The researchers' solution was to change the structure of lithium-air batteries, adding a membrane made of catalyst-coated polymer nanofibers. They showed that lithium oxide doesn't form on the nanofibers, so the catalysts remain relatively exposed and effective.
The experimental battery uses pure oxygen. To realize the theoretical potential of lithium-air batteries would require developing a system that can work in air, which poses several challenges. For example, lithium ions tend to react with carbon dioxide in air, producing lithium carbonates that make the battery difficult to recharge.
Kevin Bullis

redpajama_set_name: RedPajamaCommonCrawl


text:
Hold On Daddy's Coming!
Stories, rants and reflections by a clueless father of three
Archive for the tag "cancer"
A Journey Through Shawn Kuykendall's Awesomely Random Facebook Page: Part 4
This is the fourth and final installment of a series on Shawn Kuykendall's epic Facebook page. In case you missed it, Parts One, Two, and Three gave us a glimpse into the life of a man who could make us laugh with the best of them. But what follows is Shawn at his very best. Brace for the story that redefines what it means to be strong.
2013 (July – December)
Encouraging messages start pouring in for no apparent reason, while throwbacks of Shawn with his friends suddenly flood his Newsfeed. Clearly something's wrong. Then, the dagger: a friend requests prayer as Shawn begins "his first round of chemotherapy."
Wait, what? For Shawn?
On July 16th, Shawn would announce that he had a rare form of cancer in his chest, and that it was spreading. Stage Four. The body of a professional athlete was now failing him. Shawn was dying.
"My biggest ray of hope is that God has a purpose for me…" he explained. "I love you all and hope to connect soon. Shawn Kuykendall. #Kuykenstrong."
Days later, after a second round of chemotheraphy, Shawn shaved his rapidly-thinning hair. For those who couldn't believe it, this was tangible, undeniable proof that Shawn was in the fight of his life. Nothing would ever be the same.
Meanwhile, this "Kuykenstrong" thing caught fire and spread fast. Almost immediately, a Kuykenstrong blog and Facebook page began tracking Shawn's treatment, while "#Kuykenstrong" shirts and bracelets started selling like hotcakes. Had someone embroidered "Kuykenstrong" on a leather couch it would have gone for thousands. Indeed, with one quick hashtag, Shawn created a moniker of hope for everybody who knew him… and countless others who didn't.
In time, hundreds of people would post pictures of themselves wearing their Kuykenstrong swag. After all, this was one fashion statement upon which Shawn and the rest of the world could agree. Friends, former teammates, and current professional soccer players all posed in their new gear. Heck even this thoughtful DC United intern offered his support…
Just kidding. I know he's a famous snowboarder.
Of course, Shawn didn't ask for the fanfare – all he requested was prayer. And on August 16th, he got lots of it. From Georgetown University Hospital, Shawn announces he is preparing for round three of chemotherapy.
Afterward, his doctors reported the mass in his chest had shrunk, and that his body was responding well to treatment. Obviously great news, but tempered by the reality that the disease was still taking its toll. Shawn then thanked his family for making life feel "normal" despite the "abnormal" circumstances – all of which represented their new normal.
September 5: Shawn posts a video of "red stuff" going into his body and smiles like a lunatic as he gives a big thumbs-up. That red substance, of course, is chemo-related poison. No big deal. Days later, Shawn would be honored at RFK Stadium by the DC United Soccer Team. For someone as humble as Shawn, it's easy to forget he once played soccer for this storied franchise. You know, no big deal…
September 26: Shawn confesses he's "drained" and requests prayer to get through his fifth round of chemotherapy. "It's not easy to fight and I can certainly put on a brave front," declares Shawn. "Today I want to say how difficult it is and I need God more than ever." Scared, weak, but never quite so strong, Shawn would soon announce that the mass in his chest had shrunk yet again, rendering the torturous rounds of chemotherapy worthwhile. God bless him, he was fighting back.
Cancer will force a man to make some difficult decisions, but on October 7th, Shawn faced an easy one. Indeed, Shawn reports that 90s hit Wannabe by the Spice Girls is playing on his iPod, forcing him to decide whether to "hit next" or "let it ride." Of course, Shawn being Shawn, we know exactly how this ends: "Just kidding. I let it plaayyyyyyy." And rightfully so.
October 17: Shawn notes that his rare disease makes him the first person ever at Georgetown Hospital to receive the formula that encompasses his chemotherapy. He also observes that he is usually the "first one in and last one out" of the infusion center, and that bald is "still not his best look." His conclusion?
"Whatever you face today…be bold. Be brave."
Shawn declares "God is good," which is how most of his posts now begin. He then reveals the disheartening news: his cancer is spreading. Aggressively. So much so that he was in the emergency room to control the pain. Despite this gut-wrenching post, Shawn insists: "Don't give up, and smile today." Well, I didn't smile that day.
Shawn did make me smile on November 20 when, paying homage to some of the best music ever made, he notes: "when someone yells 'STOP' I am not sure if it's in the name of love, it's Hammer-time, or if I should collaborate and listen." Well well well. Looks like Shawnsington's back with a brand new edition!
Days later, the Washington Post publishes a front page article on Shawn, which ignites a wildfire of shares, posts and praises. "I Need God More Than Ever" was the title, and those who regularly read the Post know that faith-centric articles are a departure from the norm. But then again, there was nothing normal about Shawn, and now there was no denying that he was legitimately famous. Indeed, followers from around the world wanted to know more about what it meant to be Kuykenstrong.
Shawn spends nearly the entire month at the Georgetown University Hospital in high spirits and in excruciating pain. The disease would keep Shawn from his sister Sami's wedding – perhaps cancer's biggest blow yet.
Photo: Nichole Haun Photography
Shawn didn't post much of anything this month, and the silence was deafening. Then, on January 27th, Shawn announces: "This bachelor is sexy for sure. But he is boring me fast." Now chalk this up to things I never thought I'd say: Shawn's insider commentary on ABC's the Bachelor was a legit breath of fresh air. For those praying for Shawn, this post was sexy for sure.
Shawn shares a post from his "most smart friend" Sergio Del Valle, who drops wisdom on an on-going debate about the existence of God. Knowing thousands of people were tuning in, Shawn figured they should understand why he believed God was real. Fittingly, this was the last thing Shawn would ever post on Facebook.
February 21: Shawn turns 32 and spends the day fighting for his life in the hospital. Says Shawn's mom Sherry, one of nearly 500 people who would post on his wall that day, "Happy birthday to the strongest man I know." The Kuykendalls, who had been by Shawn's side for countless selfies, surgeries and soccer games through the years, were determined to be at his side through the finish.
And then, on March 12th, it was finished. Shawn's fight was over. He was now at someone else's side…
Shawn could have cursed his disease, but chose instead to raise awareness for the cure we have in Jesus Christ – the man who walked this earth without sin, and then died on a cross so that we could have a relationship with God. Jesus isn't something Shawn believed; He's someone Shawn knew. And boy do they have a lot to talk about…
Indeed, both had thousands of followers, both faced death sentences in their early thirties, both endured seemingly endless suffering, and both brought life to a dying world. One went bald, the other wore a crown of thorns – the parallels are impossible to ignore. Of course, Shawn wasn't without sin and that's precisely why he needed Jesus each and every day. I'll let him explain this better than I ever could:
By God's endless mercy, Shawn is now free. His Hot Body Campaign is complete, and the results, I'm sure, are SILLAAAYYY. Which brings me back to why we started this journey in the first place…
Shawn's Facebook page is epic because it highlights so beautifully the life of a friend who left nothing on the field of life. It tells the story of a man who provided so much light in an oft dark and dreary world. That's why I leave you with this:
Yep, what you watched was a music video featuring Shawn, his sister Sami, and actor Robby Stone dancing, fist-pumping and deer hunting their way through Justin Bieber's "Eenie Meenie," with sweet cameos by Audrey Hepburn and Jason Kuykendall. It's an eenie meenie glimpse into a man of incredible character who had the heart of a child. It's classic Shawn.
Shawn got cancer, but there's victory in knowing cancer never really got Shawn. He was a fighter all the way, and through his story, scripture has become real to people around the world. It's a narrative of triumph over tragedy – a legendary account of how Shawn was made weak, and in his weakness, he was made strong.
Kuykenstrong.
Posted in Uncategorized and tagged #kuykenstrong, cancer, christianity, faces of cancer, Shawn Kuykendall, thymic cancer
This is Part Three of a four-part series on Shawn Kuykendall's epic Facebook page. In case you missed it, Part One featured #throwbacks and YouTube throw downs, and Part Two brought us numerous surgeries and selfies. We now pick up where we left off, with yet another treasure chest of Shawn-given amazingness. Let's go!
What's notable about 2012 is that Shawn kept the fire that is his Facebook page burning brighter than ever. Here are a few snapshots of brilliance:
"I just had a Baconato combo. I am about to throw up," and
"Sometimes I get nervous when I see an open door," and
"Recent poll (conducted by me) found that girls don't like guys with a dumpy butt. #screwed."
Ahh, yes. He's still got it!
2012 is the year Shawn proves he may be a thirteen-year-old girl trapped inside the body of a grown man. As evidence, I note that he takes his use of the word "jeally" – a tween-friendly substitute for "jealous" – to extreme levels. Further, he makes no mistake about his fondness for pumpkin spice lattes, The Bachelor, and Hansen's hit song MMMBop. Throw in status updates like "Omg Biebs and Selena broke up!!! #devastated" and it's a no brainer.
And then there's "SIKE!" See, "Sike!" is a great way to note that the previous statement was totally false, and made with intent to deceive. Ex: "Ankle socks look great with sandals! SIKE!" Shawn used this expression back when he traded Pogs and knew all the lines to Free Willy; but then, like the rest of his generation, he stopped using it shortly after the sixth grade…
SIKE! Shawn will turn 30 this year, and he's still going strong.
See? He works out…
Our last new word is "cised," which appears to be some kind of synonym for "excited." Ex: "100% chance I wake up at 3am to preorder the iPhone 4s. I'm cised!" As with most Shawn-isms, "cised" can stand alone as an entire sentence and be spelled with seemingly unnecessary letters, like this: "Banana Republic is introducing a new line of European-cut suits! Ciiiiiised!"
So to recap: "jeally" means jealous (if you're still in middle school), "Sike!" means just kidding (if you're still living in the 90s), "cised" means excited (at least I think), and "ciiiiiiised" means super-duper excited (with a cherry on top). Got it?
Now let's get "cised" about the highlights from this rather fantastic year:
February: Shawn turns 30. To mark the occasion, hundreds of people post on Shawn's wall, and each of them has a different nickname. Here's a few: Shawny, Donald, Uncle Donnie, Prawny, Brodo Baggins, Sine, Dino, America, and Americaaaaaa. And the notion that people actually call him "America" makes me so ridiculously jeally.
March: Shawn takes everything I know to be true about fashion and throws it out the window. Indeed, first he announces that cargo shorts "couldn't be more" out of style. Then he claims he's "over" plaid in all forms (including Target brand pajama pants), leaving me with practically nothing left to wear. What's next? Is he gonna tell me I shouldn't be wearing workout sneakers to social functions?!?!
April 21: Shawn posts a picture and asks everyone to "name what's wrong with it." Guesses include (a) that it's a close-up of "some dude's crotch," (b) the fact that the man "doesn't have arms," and (c) the notion that "he's physically disabled and you just don't like that about him."
All of these answers had promise, but as it turns out Shawn took issue with this man going to a social function in… you guessed it… his workout sneakers. I should have seen this coming.
May: Shawn asks Facebook if anyone "wants to cuddle?" He gets one response from a female, but unfortunately it's just a recommendation to "get a dog." Just as it started to look like all hope was lost, friend Jason McGraw saves the day with this rather adorable response:
May also marks the month Shawn drops this bomb of amazingness:
Yep. What you just watched was a video of Shawn wearing questionably snug white pants to compliment some well-choreographed dance moves (with sister Sami), all to the tune of some song called "I Don't Feel Like Dancing," and all in front of the sheets that Shawn slept on when he turned seven years old. By the way, did anyone else catch Shawn's amazing intro slide about five seconds in? Yea, the only reason I don't feel like dancing is because I feel like slow-clapping instead. It's glorious!
June: After years of keeping it in the dark, Shawn finally admits: "I think I like blue." Wow. Good of Shawn to finally get that off his chest…
August: Shawn posts one of his patented while-driving selfies. To think: this is one of those valuable skills that driving schools simply aren't teaching their students. It's an outrage!
The Inventor of Driver's Seat Photography
September: Shawn's friend Melanie posts a close up of Shawn's torso, and if you look closely it appears to contain feint traces of definition. Now, I'm not sure if this is the result of (a) a successful Hot Body Campaign, or (b) Shawn wearing smaller shirts. Option (c) of course is man boobs. But I seriously doubt that's the case…
October: American University's finest soccer coach reminds us why he isn't cut out for a desk job…
Election Day: Shawn contrasts political candidates with a man named Jesus Christ, and lets just say things don't go so well for the politicians. Say, this is legit wisdom! Kuykendall for Congress!
December: Shawn's friend Kimberly takes him to the White House Christmas Party, where he apparently meets President Obama and then just kind of strolls around the Executive Mansion for a few hours with seemingly unfettered access. Here he is stealing fruit belonging to the most powerful man in the world…
Who let this guy in?
Days later, Shawn finishes the year off right by demonstrating how just one misplaced letter can account for the difference between having a sick mind, and being legitimately homesick:
2013 (January through June):
2013 marks the year Shawn assigned a fun little name to each day of the week. From what I gather, they are as follows: Cabernet Sauvignon Sunday, Tickle Me Tuesday, Way Back Wednesday, Throwback Thursday, Man Up Friday, and Selfie Saturday. And apparently Monday no longer exists.
February: Shawn earns something called an "A" Coaching License. This is a really big deal to Shawn's mom, who's going nuts about it. But that's not saying much because as his former homeschool teacher it's likely she goes nuts every time Shawn earns an "A." Heyoooo!
March: Shawn posts the following picture of a cute little puppy taking a "legit" selfie while lying down for an afternoon nap:
Concedes Shawn: "I have to respect it."
March 28: Shawn is publicly admonished for that night's dreadful goaltending performance. The ensuing conversation sends Shawn spiraling through the four stages of grief:
1. Denial: "Sometimes you save it. Sometimes you don't. What can I say!"
2. Anger: "I didn't have gloves! Blame Billy Knudsen!"
3. Depression: "KuykenBotch in goal today. Disaster."
4. Acceptance: "All I can say is sorry…"
April: This happened…
Yep, leave it to Shawn to post a scandalous mid-bath selfie while eating an ice cream cone. Even better are the comments, which range from "porno!" to "weeeeiiiird" to "I just threw up" to "I approve of this." And for the record I too approve of this…
June 17: Speaking of weeeiiiird, Shawn posts a picture with his parents and notes that they "made" him – both "figuratively" and "literally." And I don't know about you, but Shawn's unsolicited crash course on human reproduction really makes my day – figuratively, and literally.
Well folks, that's the end of this leg of our journey through Shawn's amazing Facebook page. As a parting gift, I leave you with this picture of Shawn getting SILLAAAYY on the dance floor while other men judge him in the background. So nice of the DJ to finally play MMMBop just for Shawn…
Posted in Uncategorized and tagged #kuykenstrong, cancer, christianity, Facebook, faces of cancer, friendship, humor, Jesus, Shawn Kuykendall
This is Part Two of a four-part series on Shawn Kuykendall's epically peculiar Facebook page. In case you missed it, Part One featured a classic Christmas video, a larger-than-life pimple, and a late night run-in with Norman the Zebra. With that in mind, it's hard to believe things could get any more spectacular. Oh but they do…
On March 27, after a two year Facebook hiatus, Shawn announces that he is "BAAAACK" and "better than ever." And indeed he is. But what truly makes 2010 such a special year is that Shawn linked his Twitter and Facebook accounts, opening up a floodgate of profound must-reads. Like these:
"Soooo I burned my mouth on hot cookies haha" or
"Golf at Oak Marr. By myself haha again" or
"Aaaaand someone farted during prayer at the morning devotional."
Note: can't you just feel yourself becoming more intelligent?
2010 is also when Shawn regularly begins using several variations of the word "donk," which I confess is a new one on me. The context in which Shawn uses this word leads me to believe it's an abridged version of "donkey," used to mock someone as both a noun ("haha you're such a donk ") as well as an adjective ("those cargo shorts are "ri-DONK-ulous"). Which could explain this:
"DOOOOONK! It was so cold. I had to!"
"Botch" becomes another regularly employed Shawn-ism, especially when spelled with about seven extra "O's" and used in the following context: "Mess up of night by me… I said, "down to your nutties" and "down to your uhohs!" BOOOOOOOOOTCH haha"
Oh, and speaking of things botched, here are a few statements I'm sure Shawn would like to take back:
– August 14: "Redskins…Super Bowl. There I said it." (Finished dead last in the NFC East…).
– September 14: "I could not be happier about having Donovan McNabb as our quarterback." (Replaced by backup Rex Grossman four months later…).
– November 30: "The Miami Heat will NEVER win a championship." (NBA Champions 2012, 2013).
And since we're talking sports, lets discuss the 2010 World Cup, during which Shawn's 24/7, non-stop commentary created a flood of soccer analysis that nearly drowned my Newsfeed. Think: "Wow Netherlands, WOOOOOOOOOWWW," then "Come on Netherland," then "Holland is not sharp right now," then "Dumb foul by van bommel," then "Violent tackle from de jong"… all in the course of, like, fifteen minutes.
Other highlights from this great year:
May: Shawn portrays a main character in "For The Glory" – a movie about how Kurt Kuykendall found Jesus and became really good at – shocker here – the sport of soccer. Shawn insists his dad is a living legend, but does so by merely referring him as a "ledge." To recap: Kurt Kuykendall = "ledge" = living legend. Got it?
May also marks the launch of Shawn's "Hot Body Campaign," inspired by the realization that he is both "fat" and "soft." And while normal humans would characterize this as "getting in shape," Shawn insists it's all about "getting his body RIIIIIIIIIIIGHT." Right.
June: Shawn gets stranded on the side of the road with a "double flat tire" and praises God for it "in all seriousness." Funny how Shawn sees God's wisdom in something as crappy and bizarre as a car breakdown. In the future, Shawn will face circumstances that are infinitely more crappy and bizarre, and his response will be no different. In all seriousness.
July: Shawn publicly eats Captain Crunch cereal and collects silly bands because, you see, he's actually seven years old.
"Seriously, if you wear silly bands you are nuuuuubs"
August: Shawn has the most highly documented ACL surgery in the history of mankind. And I'm not joking:
That's the knee…
And here it is again.
Same knee…
October 2: Shawn announces that he is "In a relationship" with a girl that he clearly met that night. Here's her classic take: "hahaa you're a creep. I never accepted you as being in a relationship…and I just met you. You're ridiculous."
Shawn's response? "Were in a fight…our first fight."
It's for the best that things didn't work out, because if Shawn had a lady friend by his side then the following "Selfie of the Year" may very well never have been taken. Props for an amazing caption:
"Owls of Ga'Hoole 3D IMAX! By myself! Siiiick!"
Fashion Statement of the Year: "men can wear cardigans." Hooray! Wait. What about everything else Mr. Rodgers wore? Any restrictions? HELP!!!!
Yep, what you just watched was Shawn (and sister Sami) dancing, leaping, and crypt walking around a miniature Christmas tree, with cameo appearances from a fake swordfish and just about every power tool in the Kuykendall shed – all to the tune of Mariah Carey's "All I Want For Christmas Is You." It's literally all I want for Christmas.
This year ushers in a tidal wave of profound status updates. Brace for the brilliance:
– "My butt just went numb from sitting on the floor next to the wall charger so my iSilly could charge…" and
– "At an orientation for new staff… This guy across me has a money coffee stain on his collar…. Yiiiiikes" and
– "Ryan Reynolds is a boat of dreams;" and
– "Thursday? yup… looks like a great night for a bath."
2011 also marks the year Shawn becomes the king of "liking" his own comments, links, pictures and statuses. There were times when Shawn was the only one. He was never deterred…
Highlights from the year that was:
January: Shawn gets stuck in an epic traffic jam (oft referred to in the DC area as the "Traffic-pocalypse") and posts a video of himself singing "Hakuna Matata." What a wonderful phrase!
May: Shawn's team makes it to the finals of the American University staff volleyball tournament. Says Shawn: "we live for opportunities like this." And indeed we do.
Shawn also launches a soccer training camp called the Kuykendall Academy. My guess is that this is just an excuse to play soccer 100% of the time he is not otherwise sleeping, eating Popeye's, or attending weddings.
July: Shawn finally figures out why he's "fat." His explanation was lengthy, but the culprit was, of all things, his "extra volume" shampoo. Oh Shawn…
July was also the height of his recreational soccer league. Having played against his Kuykendall-heavy team on numerous occasions, our strategy was always to "keep the ball away from Shawn." And if that failed he was to be tripped. And if that failed we were to make fun of his mom. It was cutthroat…
August 1: Posting from his vehicle, Shawn is not at all surprised that the incompetent driver in front of him is a female. According to Shawn, this entire interaction is dictated by "science."
October: Shawn publishes the "Selfie of the Year." And I ask: what could be better than a picture of Shawn driving eastbound on Route 66 while wearing an oversized Count von Count costume?
Nothing. Absolutely nothing.
November: Shawn has yet another surgery on his "#kuykenknee." The bad news? Shawn's mom drove him to the hospital while "slamming" some McDonald's with full knowledge of the fact that Shawn couldn't eat. The good news? The procedure was followed by some "siiiiiiiick" chicken from Popeyes. So all is well…
Fashion Statement of the Year: "Girls with Jorts are not money." In case you're confused, this is Shawn's unique way of conveying his aversion to the way jean shorts look when worn by women. Honorable mention: "no one looks sexy eating wings." Of course…
And speaking of fashion statements, here we go with a deluxe new profile pic:
Shawn looking "SILLAAAYY" with his "iSilly"
And that about wraps up the epic years that were 2010 and 2011. But worry not, because the next segment of this series contains still more hilarious songs, selfies, and statuses – all of which are nothing short of ri-DONK-ulous.
Posted in Uncategorized and tagged #kuykenstrong, cancer, christianity, Facebook, faces of cancer, friends, humor, Jesus, Shawn Kuykendall
Family Trip to Thailand? Bring Patience, An Open Mind, and So Much Deodorant
I Tried Single Parenting For A Week And Holy Cow It Was Awful
"I Do Not Have Gas!" And Other Highlights From Our Epic Trip to Korea
On Helping the Kids Say Goodbye to their Favorite Dog in the World
Mario-Karting Through Tokyo Is A Thing You Can Do and It's Glorious
Our 'Classic Guam' Dolphin-Watching Experience
Hiking Mount Lamlam: A Survivor's Story
Our Drama-Filled Adventure to Tarzan Falls
A Step by Step Guide on How to Move Your Family to Guam Without Any Drama Whatsoever
Our Wild and Crazy "Family Vacation" in Paradise
Here's Why Alec Baldwin and I Owe Our Lives to a Flimsy Piece of Rope
17 House Rules Our Toddlers Are Destined To Break
28 Totally Random Thoughts From Myrtle Beach
Butter, Butt Cream & Other Things Our Toddlers Eat (A Poem)
16 Survival Tips From A Fellow Father of Twins
My Golf Outing with President Obama
A Toast to My First Real Month as a Father of Three
How I Darn Near Missed the Birth of our Newborn Son
You Can't Handle the Truth: Life at Naval Justice School
The Old "Remember? We Talked About This…" Response
A Naughty and Nice List of Popular Christmas Songs