/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ /* Part of the Processing project - http://processing.org Copyright (c) 2013-15 The Processing Foundation Copyright (c) 2011-12 Ben Fry and Casey Reas This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package processing.app.contrib; import java.awt.*; import java.util.List; import java.util.*; import java.util.Map.Entry; import javax.swing.*; import javax.swing.RowSorter.SortKey; import javax.swing.border.Border; import javax.swing.event.*; import javax.swing.table.*; import processing.app.Base; import processing.app.Platform; import processing.app.ui.Toolkit; // The "Scrollable" implementation and its methods here take care of preventing // the scrolling area from running exceptionally slowly. Not sure why they're // necessary in the first place, however; seems like odd behavior. // It also allows the description text in the panels to wrap properly. public class ListPanel extends JPanel implements Scrollable, ContributionListing.ChangeListener { ContributionTab contributionTab; TreeMap<Contribution, DetailPanel> panelByContribution = new TreeMap<Contribution, DetailPanel>(ContributionListing.COMPARATOR); Set<Contribution> visibleContributions = new TreeSet<Contribution>(ContributionListing.COMPARATOR); private DetailPanel selectedPanel; protected Contribution.Filter filter; protected ContributionListing contribListing = ContributionListing.getInstance(); protected JTable table; DefaultTableModel model; JScrollPane scrollPane; static Icon upToDateIcon; static Icon updateAvailableIcon; static Icon incompatibleIcon; static Icon foundationIcon; static Icon downloadingIcon; // Should this be in theme.txt? Of course! Is it? No. static final Color HEADER_BGCOLOR = new Color(0xffEBEBEB); public ListPanel() { if (upToDateIcon == null) { upToDateIcon = Toolkit.getLibIconX("manager/up-to-date"); updateAvailableIcon = Toolkit.getLibIconX("manager/update-available"); incompatibleIcon = Toolkit.getLibIconX("manager/incompatible"); foundationIcon = Toolkit.getLibIconX("icons/foundation", 16); downloadingIcon = Toolkit.getLibIconX("manager/downloading"); } } public ListPanel(final ContributionTab contributionTab, Contribution.Filter filter) { this.contributionTab = contributionTab; this.filter = filter; setLayout(new GridBagLayout()); setOpaque(true); setBackground(Color.WHITE); model = new ContribTableModel(); table = new JTable(model) { @Override public Component prepareRenderer(TableCellRenderer renderer, int row, int column) { Component c = super.prepareRenderer(renderer, row, column); if (isRowSelected(row)) { c.setBackground(new Color(0xe0fffd)); } else { c.setBackground(Color.white); } return c; } }; // There is a space before Status String[] colName = { " Status", "Name", "Author" }; model.setColumnIdentifiers(colName); scrollPane = new JScrollPane(table); scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); scrollPane.setBorder(BorderFactory.createEmptyBorder()); table.setFillsViewportHeight(true); table.setDefaultRenderer(Contribution.class, new ContribStatusRenderer()); table.setFont(ManagerFrame.NORMAL_PLAIN); table.setRowHeight(Toolkit.zoom(28)); table.setRowMargin(Toolkit.zoom(6)); table.getColumnModel().setColumnMargin(0); table.getColumnModel().getColumn(0).setMaxWidth(ManagerFrame.STATUS_WIDTH); table.getColumnModel().getColumn(2).setMinWidth(ManagerFrame.AUTHOR_WIDTH); table.getColumnModel().getColumn(2).setMaxWidth(ManagerFrame.AUTHOR_WIDTH); table.setShowGrid(false); table.setColumnSelectionAllowed(false); table.setCellSelectionEnabled(false); table.setAutoCreateColumnsFromModel(true); table.setAutoCreateRowSorter(false); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); table.getSelectionModel().addListSelectionListener(new ListSelectionListener() { public void valueChanged(ListSelectionEvent event) { //TODO this executes 2 times when clicked and 1 time when traversed using arrow keys //Ideally this should always be true but while clearing the table something fishy is going on if (table.getSelectedRow() != -1) { setSelectedPanel(panelByContribution.get(table.getValueAt(table .getSelectedRow(), 0))); // Preventing the focus to move out of filterField after typing every character if (!contributionTab.filterField.hasFocus()) { table.requestFocusInWindow(); } } } }); TableRowSorter<TableModel> sorter = new TableRowSorter<TableModel>(table.getModel()); table.setRowSorter(sorter); sorter.setComparator(1, ContributionListing.COMPARATOR); sorter.setComparator(2, new Comparator<Contribution>() { @Override public int compare(Contribution o1, Contribution o2) { return getAuthorNameWithoutMarkup(o1.getAuthorList()) .compareTo(getAuthorNameWithoutMarkup(o2.getAuthorList())); } }); sorter.setComparator(0, new Comparator<Contribution>() { @Override public int compare(Contribution o1, Contribution o2) { int pos1 = 0; if (o1.isInstalled()) { pos1 = 1; if (contribListing.hasUpdates(o1)) { pos1 = 2; } if (!o1.isCompatible(Base.getRevision())) { pos1 = 3; } } else { pos1 = 4; } int pos2 = 0; if (o2.isInstalled()) { pos2 = 1; if (contribListing.hasUpdates(o2)) { pos2 = 2; } if (!o2.isCompatible(Base.getRevision())) { pos2 = 3; } } else { pos2 = 4; } return pos1 - pos2; } }); table.getTableHeader().setDefaultRenderer(new ContribHeaderRenderer()); GroupLayout layout = new GroupLayout(this); layout.setHorizontalGroup(layout.createParallelGroup().addComponent(scrollPane)); layout.setVerticalGroup(layout.createSequentialGroup().addComponent(scrollPane)); this.setLayout(layout); table.setVisible(true); } class ContribHeaderRenderer extends DefaultTableCellRenderer { public ContribHeaderRenderer() { setHorizontalTextPosition(LEFT); setOpaque(true); } /** * Returns the default table header cell renderer. * <P> * If the column is sorted, the appropriate icon is retrieved from the * current Look and Feel, and a border appropriate to a table header cell * is applied. * <P> * Subclasses may override this method to provide custom content or * formatting. * * @param table the <code>JTable</code>. * @param value the value to assign to the header cell * @param isSelected This parameter is ignored. * @param hasFocus This parameter is ignored. * @param row This parameter is ignored. * @param column the column of the header cell to render * @return the default table header cell renderer */ @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); JTableHeader tableHeader = table.getTableHeader(); if (tableHeader != null) { setForeground(tableHeader.getForeground()); } setFont(ManagerFrame.SMALL_PLAIN); setIcon(getSortIcon(table, column)); setBackground(HEADER_BGCOLOR); // if (column % 2 == 0) { // setBackground(new Color(0xdfdfdf)); // } else { // setBackground(new Color(0xebebeb)); // } setBorder(null); return this; } /** * Overloaded to return an icon suitable to the primary sorted column, or null if * the column is not the primary sort key. * * @param table the <code>JTable</code>. * @param column the column index. * @return the sort icon, or null if the column is unsorted. */ protected Icon getSortIcon(JTable table, int column) { SortKey sortKey = getSortKey(table, column); if (sortKey != null && table.convertColumnIndexToView(sortKey.getColumn()) == column) { switch (sortKey.getSortOrder()) { case ASCENDING: return UIManager.getIcon("Table.ascendingSortIcon"); case DESCENDING: return UIManager.getIcon("Table.descendingSortIcon"); } } return null; } /** * Returns the current sort key, or null if the column is unsorted. * * @param table the table * @param column the column index * @return the SortKey, or null if the column is unsorted */ protected SortKey getSortKey(JTable table, int column) { RowSorter rowSorter = table.getRowSorter(); if (rowSorter == null) { return null; } List sortedColumns = rowSorter.getSortKeys(); if (sortedColumns.size() > 0) { return (SortKey) sortedColumns.get(0); } return null; } } private class ContribStatusRenderer extends DefaultTableCellRenderer { @Override public void setVerticalAlignment(int alignment) { super.setVerticalAlignment(SwingConstants.CENTER); } @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { Contribution contribution = (Contribution) value; JLabel label = new JLabel(); if (value == null) { // Working on https://github.com/processing/processing/issues/3667 //System.err.println("null value seen in getTableCellRendererComponent()"); // TODO this is now working, but the underlying issue is not fixed return label; } if (column == 0) { Icon icon = null; label.setFont(ManagerFrame.NORMAL_PLAIN); if (contribution.isInstalled()) { icon = upToDateIcon; if (contribListing.hasUpdates(contribution)) { icon = updateAvailableIcon; } if (!contribution.isCompatible(Base.getRevision())) { icon = incompatibleIcon; } } if ((panelByContribution.get(contribution)).updateInProgress || (panelByContribution.get(contribution)).installInProgress) { // Display "Loading icon" if download/install in progress label.setIcon(downloadingIcon); } else { label.setIcon(icon); } label.setHorizontalAlignment(SwingConstants.CENTER); if (isSelected) { label.setBackground(new Color(0xe0fffd)); } label.setOpaque(true); // return table.getDefaultRenderer(Icon.class).getTableCellRendererComponent(table, icon, isSelected, false, row, column); } else if (column == 1) { // Generating ellipses based on fontMetrics final Font boldFont = ManagerFrame.NORMAL_BOLD; String fontFace = "<font face=\"" + boldFont.getName() + "\">"; FontMetrics fontMetrics = table.getFontMetrics(boldFont); //table.getFont()); int colSize = table.getColumnModel().getColumn(1).getWidth(); String sentence = contribution.getSentence(); //int currentWidth = table.getFontMetrics(table.getFont().deriveFont(Font.BOLD)).stringWidth(contribution.getName() + " | "); int currentWidth = table.getFontMetrics(boldFont).stringWidth(contribution.getName() + " | "); int ellipsesWidth = fontMetrics.stringWidth("..."); //String name = "<html><body><b>" + contribution.getName(); String name = "<html><body>" + fontFace + contribution.getName(); if (sentence == null) { label.setText(name + "</font></body></html>"); } else { sentence = " | </font>" + sentence; currentWidth += ellipsesWidth; int i = 0; for (i = 0; i < sentence.length(); i++) { currentWidth += fontMetrics.charWidth(sentence.charAt(i)); if (currentWidth >= colSize) { break; } } // Adding ellipses only if text doesn't fits into the column if(i != sentence.length()){ label.setText(name + sentence.substring(0, i) + "...</body></html>"); }else { label.setText(name + sentence + "</body></html>"); } } if (!contribution.isCompatible(Base.getRevision())) { label.setForeground(Color.LIGHT_GRAY); } if (table.isRowSelected(row)) { label.setBackground(new Color(0xe0fffd)); } label.setFont(ManagerFrame.NORMAL_PLAIN); label.setOpaque(true); } else { if (contribution.isSpecial()) { label = new JLabel(foundationIcon); } else { label = new JLabel(); } String authorList = contribution.getAuthorList(); String name = getAuthorNameWithoutMarkup(authorList); label.setText(name); label.setHorizontalAlignment(SwingConstants.LEFT); if(!contribution.isCompatible(Base.getRevision())){ label.setForeground(Color.LIGHT_GRAY); }else{ label.setForeground(Color.BLACK); } if (table.isRowSelected(row)) { label.setBackground(new Color(0xe0fffd)); } label.setFont(ManagerFrame.NORMAL_BOLD); label.setOpaque(true); } return label; } } static private class ContribTableModel extends DefaultTableModel { @Override public boolean isCellEditable(int row, int column) { return false; } @Override public Class<?> getColumnClass(int columnIndex) { return Contribution.class; } } String getAuthorNameWithoutMarkup(String authorList) { StringBuilder name = new StringBuilder(""); if (authorList != null) { for (int i = 0; i < authorList.length(); i++) { if (authorList.charAt(i) == '[' || authorList.charAt(i) == ']') { continue; } if (authorList.charAt(i) == '(') { i++; while (authorList.charAt(i) != ')') { i++; } } else { name.append(authorList.charAt(i)); } } } return name.toString(); } // Thread: EDT void updatePanelOrdering(Set<Contribution> contributionsSet) { model.getDataVector().removeAllElements(); int rowCount = 0; for (Contribution entry : contributionsSet) { model.addRow(new Object[]{entry, entry, entry}); if (selectedPanel != null && entry.getName().equals(selectedPanel.getContrib().getName())) { table.setRowSelectionInterval(rowCount, rowCount); } rowCount++; } model.fireTableDataChanged(); } // Thread: EDT public void contributionAdded(final Contribution contribution) { if (filter.matches(contribution)) { if (!panelByContribution.containsKey(contribution)) { DetailPanel newPanel = new DetailPanel(ListPanel.this); panelByContribution.put(contribution, newPanel); visibleContributions.add(contribution); newPanel.setContribution(contribution); add(newPanel); updatePanelOrdering(visibleContributions); updateColors(); // XXX this is the place } } } // Thread: EDT public void contributionRemoved(final Contribution contribution) { if (filter.matches(contribution)) { DetailPanel panel = panelByContribution.get(contribution); if (panel != null) { remove(panel); panelByContribution.remove(contribution); } visibleContributions.remove(contribution); updatePanelOrdering(visibleContributions); updateColors(); updateUI(); } } // Thread: EDT public void contributionChanged(final Contribution oldContrib, final Contribution newContrib) { if (filter.matches(oldContrib) || filter.matches(newContrib)) { DetailPanel panel = panelByContribution.get(oldContrib); if (panel == null) { contributionAdded(newContrib); } else { panelByContribution.remove(oldContrib); panel.setContribution(newContrib); panelByContribution.put(newContrib, panel); } if (visibleContributions.contains(oldContrib)) { visibleContributions.remove(oldContrib); visibleContributions.add(newContrib); } updatePanelOrdering(visibleContributions); } } // Thread: EDT public void filterLibraries(List<Contribution> filteredContributions) { visibleContributions.clear(); for (Contribution contribution : panelByContribution.keySet()) { if (contribution.getType() == contributionTab.contribType) { // contains() uses equals() and there can be multiple instances, // so Contribution.equals() has to be overridden if (filteredContributions.contains(contribution)) { if (panelByContribution.keySet().contains(contribution)) { visibleContributions.add(contribution); } } } } // TODO: Make the following loop work for optimization // for (Contribution contribution : filteredContributions) { // if (contribution.getType() == contributionTab.contribType) { // if(panelByContribution.keySet().contains(contribution)){ // visibleContributions.add(contribution); // } // } // } updatePanelOrdering(visibleContributions); } // Thread: EDT protected void setSelectedPanel(DetailPanel contributionPanel) { contributionTab.updateStatusPanel(contributionPanel); if (selectedPanel == contributionPanel) { selectedPanel.setSelected(true); } else { DetailPanel lastSelected = selectedPanel; selectedPanel = contributionPanel; if (lastSelected != null) { lastSelected.setSelected(false); } contributionPanel.setSelected(true); updateColors(); requestFocusInWindow(); } } protected DetailPanel getSelectedPanel() { return selectedPanel; } // Thread: EDT /** * Updates the colors of all library panels that are visible. */ protected void updateColors() { int count = 0; for (Entry<Contribution, DetailPanel> entry : panelByContribution.entrySet()) { DetailPanel panel = entry.getValue(); if (panel.isVisible() && panel.isSelected()) { panel.setBackground(UIManager.getColor("List.selectionBackground")); panel.setForeground(UIManager.getColor("List.selectionForeground")); panel.setBorder(UIManager.getBorder("List.focusCellHighlightBorder")); count++; } else { Border border = null; if (panel.isVisible()) { if (Platform.isMacOS()) { if (count % 2 == 1) { border = UIManager.getBorder("List.oddRowBackgroundPainter"); } else { border = UIManager.getBorder("List.evenRowBackgroundPainter"); } } else { if (count % 2 == 1) { panel.setBackground(new Color(219, 224, 229)); } else { panel.setBackground(new Color(241, 241, 241)); } } count++; } if (border == null) { border = BorderFactory.createEmptyBorder(1, 1, 1, 1); } panel.setBorder(border); panel.setForeground(UIManager.getColor("List.foreground")); } } } @Override public Dimension getPreferredScrollableViewportSize() { return getPreferredSize(); } /** * Amount to scroll to reveal a new page of items */ @Override public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) { if (orientation == SwingConstants.VERTICAL) { int blockAmount = visibleRect.height; if (direction > 0) { visibleRect.y += blockAmount; } else { visibleRect.y -= blockAmount; } blockAmount += getScrollableUnitIncrement(visibleRect, orientation, direction); return blockAmount; } return 0; } /** * Amount to scroll to reveal the rest of something we are on or a new item */ @Override public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { if (orientation == SwingConstants.VERTICAL) { int lastHeight = 0, height = 0; int bottomOfScrollArea = visibleRect.y + visibleRect.height; for (Component c : getComponents()) { if (c.isVisible()) { if (c instanceof DetailPanel) { Dimension d = c.getPreferredSize(); int nextHeight = height + d.height; if (direction > 0) { // scrolling down if (nextHeight > bottomOfScrollArea) { return nextHeight - bottomOfScrollArea; } } else { // scrolling up if (nextHeight > visibleRect.y) { if (visibleRect.y != height) { return visibleRect.y - height; } else { return visibleRect.y - lastHeight; } } } lastHeight = height; height = nextHeight; } } } } return 0; } @Override public boolean getScrollableTracksViewportHeight() { return false; } @Override public boolean getScrollableTracksViewportWidth() { return true; } public int getRowCount() { return panelByContribution.size(); } }