private void createComponents() {
    categoryLabel = new JLabel(Language.text("contrib.category"));

    categoryChooser = new JComboBox<String>();
    categoryChooser.setMaximumRowCount(20);
    categoryChooser.setFont(Toolkit.getSansFont(14, Font.PLAIN));

    updateCategoryChooser();

    categoryChooser.addItemListener(
        new ItemListener() {
          public void itemStateChanged(ItemEvent e) {
            category = (String) categoryChooser.getSelectedItem();
            if (ContributionManagerDialog.ANY_CATEGORY.equals(category)) {
              category = null;
            }
            filterLibraries(category, filterField.filters);
            contributionListPanel.updateColors();
          }
        });

    filterField = new FilterField();
  }
public class ContributionTab extends JPanel {
  static final String ANY_CATEGORY = Language.text("contrib.all");

  ContributionType contribType;
  ContributionManagerDialog contribDialog;

  Contribution.Filter filter;
  JComboBox<String> categoryChooser;
  JScrollPane scrollPane;
  ListPanel contributionListPanel;
  StatusPanel statusPanel;
  FilterField filterField;
  JButton restartButton;
  JLabel categoryLabel;
  JLabel loaderLabel;

  JPanel errorPanel;
  JTextPane errorMessage;
  JButton tryAgainButton;
  JButton closeButton;

  // the calling editor, so updates can be applied
  Editor editor;
  String category;
  ContributionListing contribListing;

  JProgressBar progressBar;

  public ContributionTab() {}

  public ContributionTab(ContributionType type, ContributionManagerDialog dialog) {
    this.contribType = type;
    this.contribDialog = dialog;

    /*
    if (type == ContributionType.MODE) {
      title = Language.text("contrib.manager_title.mode");
    } else if (type == ContributionType.TOOL) {
      title = Language.text("contrib.manager_title.tool");
    } else if (type == ContributionType.LIBRARY) {
      title = Language.text("contrib.manager_title.library");
    } else if (type == ContributionType.EXAMPLES) {
      title = Language.text("contrib.manager_title.examples");
    }
    */

    filter =
        new Contribution.Filter() {
          public boolean matches(Contribution contrib) {
            return contrib.getType() == contribType;
          }
        };

    contribListing = ContributionListing.getInstance();
    statusPanel = new StatusPanel(650, this);
    contributionListPanel = new ListPanel(this, filter);
    contribListing.addListener(contributionListPanel);
  }

  //  public boolean hasUpdates(Base base) {
  //    return contribListing.hasUpdates(base);
  //  }

  public void showFrame(final Editor editor, boolean activateErrorPanel, final boolean loading) {
    this.editor = editor;

    setLayout(editor, activateErrorPanel, loading);
    contributionListPanel.setVisible(!loading);
    loaderLabel.setVisible(loading);
    errorPanel.setVisible(activateErrorPanel);

    validate();
    repaint();
  }

  protected void setLayout(final Editor editor, boolean activateErrorPanel, boolean isLoading) {
    if (progressBar == null) {
      progressBar = new JProgressBar();
      progressBar.setVisible(false);

      createComponents();
      buildErrorPanel();

      loaderLabel = new JLabel(Toolkit.getLibIcon("manager/loader.gif"));
      loaderLabel.setOpaque(false);
      loaderLabel.setBackground(Color.WHITE);
    }

    int scrollBarWidth =
        contributionListPanel.scrollPane.getVerticalScrollBar().getPreferredSize().width;

    GroupLayout layout = new GroupLayout(this);
    setLayout(layout);
    //    layout.setAutoCreateContainerGaps(true);
    //    layout.setAutoCreateGaps(true);
    layout.setHorizontalGroup(
        layout
            .createParallelGroup(GroupLayout.Alignment.CENTER)
            .addGroup(
                layout
                    .createSequentialGroup()
                    .addGap(ContributionManagerDialog.STATUS_WIDTH)
                    .addComponent(
                        filterField,
                        ContributionManagerDialog.FILTER_WIDTH,
                        ContributionManagerDialog.FILTER_WIDTH,
                        ContributionManagerDialog.FILTER_WIDTH)
                    //                  .addPreferredGap(LayoutStyle.ComponentPlacement.UNRELATED)
                    .addPreferredGap(
                        LayoutStyle.ComponentPlacement.RELATED,
                        GroupLayout.PREFERRED_SIZE,
                        Short.MAX_VALUE)
                    .addComponent(
                        categoryChooser,
                        ContributionManagerDialog.AUTHOR_WIDTH,
                        ContributionManagerDialog.AUTHOR_WIDTH,
                        ContributionManagerDialog.AUTHOR_WIDTH)
                    .addGap(scrollBarWidth))
            .addComponent(loaderLabel)
            .addComponent(contributionListPanel)
            .addComponent(errorPanel)
            .addComponent(statusPanel));

    layout.setVerticalGroup(
        layout
            .createSequentialGroup()
            .addContainerGap()
            .addGroup(
                layout
                    .createParallelGroup(GroupLayout.Alignment.CENTER)
                    .addComponent(categoryChooser)
                    .addComponent(filterField))
            .addPreferredGap(LayoutStyle.ComponentPlacement.UNRELATED)
            .addGroup(
                layout
                    .createParallelGroup(GroupLayout.Alignment.CENTER)
                    .addComponent(loaderLabel)
                    .addComponent(contributionListPanel))
            .addComponent(errorPanel)
            .addComponent(
                statusPanel,
                GroupLayout.PREFERRED_SIZE,
                GroupLayout.DEFAULT_SIZE,
                GroupLayout.PREFERRED_SIZE));
    layout.linkSize(SwingConstants.VERTICAL, categoryChooser, filterField);

    // these will occupy space even if not visible
    layout.setHonorsVisibility(contributionListPanel, false);
    layout.setHonorsVisibility(categoryChooser, false);

    setBackground(Color.WHITE);
    setBorder(null);
  }

  private void createComponents() {
    categoryLabel = new JLabel(Language.text("contrib.category"));

    categoryChooser = new JComboBox<String>();
    categoryChooser.setMaximumRowCount(20);
    categoryChooser.setFont(Toolkit.getSansFont(14, Font.PLAIN));

    updateCategoryChooser();

    categoryChooser.addItemListener(
        new ItemListener() {
          public void itemStateChanged(ItemEvent e) {
            category = (String) categoryChooser.getSelectedItem();
            if (ContributionManagerDialog.ANY_CATEGORY.equals(category)) {
              category = null;
            }
            filterLibraries(category, filterField.filters);
            contributionListPanel.updateColors();
          }
        });

    filterField = new FilterField();
  }

  protected void buildErrorPanel() {
    errorPanel = new JPanel();
    GroupLayout layout = new GroupLayout(errorPanel);
    layout.setAutoCreateGaps(true);
    layout.setAutoCreateContainerGaps(true);
    errorPanel.setLayout(layout);
    //    errorPanel.setBorder(BorderFactory.createMatteBorder(2, 0, 0, 0, Color.BLACK));
    errorMessage = new JTextPane();
    errorMessage.setEditable(false);
    errorMessage.setContentType("text/html");
    errorMessage.setText(
        "<html><body>Could not connect to the Processing server.<br>"
            + "Contributions cannot be installed or updated without an Internet connection.<br>"
            + "Please verify your network connection again, then try connecting again.</body></html>");
    errorMessage.setFont(Toolkit.getSansFont(14, Font.PLAIN));
    errorMessage.setMaximumSize(new Dimension(550, 50));
    errorMessage.setOpaque(false);

    StyledDocument doc = errorMessage.getStyledDocument();
    SimpleAttributeSet center = new SimpleAttributeSet();
    StyleConstants.setAlignment(center, StyleConstants.ALIGN_CENTER);
    doc.setParagraphAttributes(0, doc.getLength(), center, false);

    closeButton = new JButton("X");
    closeButton.setContentAreaFilled(false);
    closeButton.addActionListener(
        new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            contribDialog.makeAndShowTab(false, false);
          }
        });
    tryAgainButton = new JButton("Try Again");
    tryAgainButton.setFont(Toolkit.getSansFont(14, Font.PLAIN));
    tryAgainButton.addActionListener(
        new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            contribDialog.makeAndShowTab(false, true);
            contribDialog.downloadAndUpdateContributionListing(editor.getBase());
          }
        });
    layout.setHorizontalGroup(
        layout
            .createSequentialGroup()
            .addPreferredGap(
                LayoutStyle.ComponentPlacement.RELATED, GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE)
            .addGroup(
                layout
                    .createParallelGroup(GroupLayout.Alignment.CENTER)
                    .addComponent(errorMessage)
                    .addComponent(
                        tryAgainButton,
                        StatusPanel.BUTTON_WIDTH,
                        StatusPanel.BUTTON_WIDTH,
                        StatusPanel.BUTTON_WIDTH))
            .addPreferredGap(
                LayoutStyle.ComponentPlacement.RELATED, GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE)
            .addComponent(closeButton));
    layout.setVerticalGroup(
        layout
            .createSequentialGroup()
            .addGroup(
                layout.createParallelGroup().addComponent(errorMessage).addComponent(closeButton))
            .addPreferredGap(LayoutStyle.ComponentPlacement.UNRELATED)
            .addComponent(tryAgainButton));
    errorPanel.setBackground(Color.PINK);
    errorPanel.validate();
  }

  @Override
  protected void paintComponent(Graphics g) {
    super.paintComponent(g);
    g.setColor(new Color(0xe0fffd));
    g.fillRect(getX(), getY() - ContributionManagerDialog.TAB_HEIGHT - 2, getWidth(), 2);
  }

  protected void updateCategoryChooser() {
    if (categoryChooser != null) {
      ArrayList<String> categories;
      categoryChooser.removeAllItems();
      categories = new ArrayList<String>(contribListing.getCategories(filter));
      //      for (int i = 0; i < categories.size(); i++) {
      //        System.out.println(i + " category: " + categories.get(i));
      //      }
      Collections.sort(categories);
      //    categories.add(0, ContributionManagerDialog.ANY_CATEGORY);
      boolean categoriesFound = false;
      categoryChooser.addItem(ContributionManagerDialog.ANY_CATEGORY);
      for (String s : categories) {
        categoryChooser.addItem(s);
        if (!s.equals(Contribution.UNKNOWN_CATEGORY)) {
          categoriesFound = true;
        }
      }
      categoryChooser.setVisible(categoriesFound);
    }
  }

  protected void filterLibraries(String category, List<String> filters) {
    List<Contribution> filteredLibraries = contribListing.getFilteredLibraryList(category, filters);
    contributionListPanel.filterLibraries(filteredLibraries);
  }

  protected void updateContributionListing() {
    if (editor != null) {
      List<Contribution> contributions = new ArrayList<Contribution>();

      List<Library> libraries = new ArrayList<Library>(editor.getMode().contribLibraries);

      // Only add core libraries that are installed in the sketchbook
      // https://github.com/processing/processing/issues/3688
      // libraries.addAll(editor.getMode().coreLibraries);
      final String sketchbookPath = Base.getSketchbookLibrariesFolder().getAbsolutePath();
      for (Library lib : editor.getMode().coreLibraries) {
        if (lib.getLibraryPath().startsWith(sketchbookPath)) {
          libraries.add(lib);
        }
      }

      contributions.addAll(libraries);

      Base base = editor.getBase();

      List<ToolContribution> tools = base.getToolContribs();
      contributions.addAll(tools);

      List<ModeContribution> modes = base.getModeContribs();
      contributions.addAll(modes);

      List<ExamplesContribution> examples = base.getExampleContribs();
      contributions.addAll(examples);

      //    ArrayList<LibraryCompilation> compilations = LibraryCompilation.list(libraries);
      //
      //    // Remove libraries from the list that are part of a compilations
      //    for (LibraryCompilation compilation : compilations) {
      //      Iterator<Library> it = libraries.iterator();
      //      while (it.hasNext()) {
      //        Library current = it.next();
      //        if (compilation.getFolder().equals(current.getFolder().getParentFile())) {
      //          it.remove();
      //        }
      //      }
      //    }

      contribListing.updateInstalledList(contributions);
    }
  }

  protected void setFilterText(String filter) {
    if (filter == null || filter.isEmpty()) {
      filterField.setText("");
    } else {
      filterField.setText(filter);
    }
    filterField.applyFilter();
  }

  // TODO: this is causing a lot of bugs as the hint is wrongly firing applyFilter()
  class FilterField extends JTextField {
    Icon searchIcon;
    List<String> filters;
    JLabel filterLabel;

    public FilterField() {
      super("");

      filterLabel = new JLabel("Filter");
      filterLabel.setFont(Toolkit.getSansFont(14, Font.PLAIN));
      filterLabel.setOpaque(false);

      setFont(Toolkit.getSansFont(14, Font.PLAIN));
      searchIcon = Toolkit.getLibIconX("manager/search");
      filterLabel.setIcon(searchIcon);
      // searchIcon = new
      // ImageIcon(java.awt.Toolkit.getDefaultToolkit().getImage("NSImage://NSComputerTemplate"));
      setOpaque(false);
      // setBorder(BorderFactory.createMatteBorder(0, 33, 0, 0, searchIcon));

      GroupLayout fl = new GroupLayout(this);
      setLayout(fl);
      fl.setHorizontalGroup(fl.createSequentialGroup().addComponent(filterLabel));
      fl.setVerticalGroup(
          fl.createSequentialGroup()
              .addPreferredGap(
                  LayoutStyle.ComponentPlacement.RELATED,
                  GroupLayout.PREFERRED_SIZE,
                  Short.MAX_VALUE)
              .addComponent(filterLabel)
              .addPreferredGap(
                  LayoutStyle.ComponentPlacement.RELATED,
                  GroupLayout.PREFERRED_SIZE,
                  Short.MAX_VALUE));

      filters = new ArrayList<String>();

      addFocusListener(
          new FocusListener() {
            public void focusLost(FocusEvent focusEvent) {
              if (getText().isEmpty()) {
                //            setBorder(BorderFactory.createMatteBorder(0, 33, 0, 0, searchIcon));
                filterLabel.setVisible(true);
              }
            }

            public void focusGained(FocusEvent focusEvent) {
              //          setBorder(BorderFactory.createEmptyBorder(0, 3, 0, 0));
              filterLabel.setVisible(false);
            }
          });

      getDocument()
          .addDocumentListener(
              new DocumentListener() {
                public void removeUpdate(DocumentEvent e) {
                  applyFilter();
                }

                public void insertUpdate(DocumentEvent e) {
                  applyFilter();
                }

                public void changedUpdate(DocumentEvent e) {
                  applyFilter();
                }
              });
    }

    public void applyFilter() {
      String filter = getText();
      filter = filter.toLowerCase();

      // Replace anything but 0-9, a-z, or : with a space
      filter = filter.replaceAll("[^\\x30-\\x39^\\x61-\\x7a^\\x3a]", " ");
      filters = Arrays.asList(filter.split(" "));
      filterLibraries(category, filters);

      contributionListPanel.updateColors();
    }
  }

  //  public boolean hasAlreadyBeenOpened() {
  //    return panel != null;
  //  }

  public void updateStatusPanel(DetailPanel contributionPanel) {
    statusPanel.update(contributionPanel);
  }
}