001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.preferences.plugin; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.GridBagConstraints; 007import java.awt.GridBagLayout; 008import java.awt.Insets; 009import java.awt.Rectangle; 010import java.awt.event.MouseAdapter; 011import java.awt.event.MouseEvent; 012import java.util.HashSet; 013import java.util.List; 014import java.util.Set; 015 016import javax.swing.JComponent; 017import javax.swing.JLabel; 018import javax.swing.SwingConstants; 019import javax.swing.SwingUtilities; 020 021import org.openstreetmap.josm.gui.widgets.HtmlPanel; 022import org.openstreetmap.josm.gui.widgets.VerticallyScrollablePanel; 023import org.openstreetmap.josm.plugins.PluginInformation; 024import org.openstreetmap.josm.tools.Utils; 025 026/** 027 * A panel displaying the list of known plugins. 028 */ 029public class PluginListPanel extends VerticallyScrollablePanel { 030 static final class PluginCheckBoxMouseAdapter extends MouseAdapter { 031 private final PluginCheckBox cbPlugin; 032 033 PluginCheckBoxMouseAdapter(PluginCheckBox cbPlugin) { 034 this.cbPlugin = cbPlugin; 035 } 036 037 @Override 038 public void mouseClicked(MouseEvent e) { 039 cbPlugin.doClick(); 040 } 041 } 042 043 private final transient PluginPreferencesModel model; 044 045 /** Whether the plugin list has been built up already in the UI. */ 046 private boolean pluginListInitialized; 047 048 /** 049 * Constructs a new {@code PluginListPanel} with a default model. 050 */ 051 public PluginListPanel() { 052 this(new PluginPreferencesModel()); 053 } 054 055 /** 056 * Constructs a new {@code PluginListPanel} with a given model. 057 * @param model The plugin model 058 */ 059 public PluginListPanel(PluginPreferencesModel model) { 060 this.model = model; 061 setLayout(new GridBagLayout()); 062 } 063 064 protected static String formatPluginRemoteVersion(PluginInformation pi) { 065 StringBuilder sb = new StringBuilder(); 066 if (Utils.isBlank(pi.version)) { 067 sb.append(tr("unknown")); 068 } else { 069 sb.append(pi.version); 070 if (pi.oldmode) { 071 sb.append('*'); 072 } 073 } 074 return sb.toString(); 075 } 076 077 protected static String formatPluginLocalVersion(PluginInformation pi) { 078 if (pi == null) 079 return tr("unknown"); 080 if (Utils.isBlank(pi.localversion)) 081 return tr("unknown"); 082 return pi.localversion; 083 } 084 085 protected static String formatCheckboxTooltipText(PluginInformation pi) { 086 if (pi == null) 087 return ""; 088 if (pi.downloadlink == null) 089 return tr("Plugin bundled with JOSM"); 090 else 091 return pi.downloadlink; 092 } 093 094 /** 095 * Displays a message when the plugin list is empty. 096 */ 097 public void displayEmptyPluginListInformation() { 098 GridBagConstraints gbc = new GridBagConstraints(); 099 gbc.gridx = 0; 100 gbc.anchor = GridBagConstraints.CENTER; 101 gbc.fill = GridBagConstraints.BOTH; 102 gbc.insets = new Insets(40, 0, 40, 0); 103 gbc.weightx = 1.0; 104 gbc.weighty = 1.0; 105 106 HtmlPanel hint = new HtmlPanel(); 107 hint.setText( 108 "<html>" 109 + (model.getAvailablePlugins().isEmpty() ? 110 tr("Please click on <strong>Download list</strong> to download and display a list of available plugins.") : 111 tr("The filter returned no results.")) 112 + "</html>" 113 ); 114 hint.putClientProperty("plugin", "empty"); 115 hint.setVisible(false); 116 add(hint, gbc); 117 } 118 119 /** 120 * Displays a list of plugins. 121 * @param displayedPlugins list of plugins 122 * @since 13799 123 */ 124 public void displayPluginList(List<PluginInformation> displayedPlugins) { 125 GridBagConstraints gbc = new GridBagConstraints(); 126 gbc.gridx = 0; 127 gbc.anchor = GridBagConstraints.NORTHWEST; 128 gbc.fill = GridBagConstraints.HORIZONTAL; 129 gbc.weightx = 1.0; 130 131 int row = -1; 132 for (final PluginInformation pi : displayedPlugins) { 133 boolean selected = model.isSelectedPlugin(pi.getName()); 134 String remoteversion = formatPluginRemoteVersion(pi); 135 String localversion = formatPluginLocalVersion(model.getPluginInformation(pi.getName())); 136 137 final PluginCheckBox cbPlugin = new PluginCheckBox(pi, selected, this, model); 138 String pluginText = tr("{0}: Version {1} (local: {2})", pi.getName(), remoteversion, localversion); 139 if (!Utils.isEmpty(pi.requires)) { 140 pluginText += tr(" (requires: {0})", pi.requires); 141 } 142 JLabel lblPlugin = new JLabel( 143 pluginText, 144 pi.getScaledIcon(), 145 SwingConstants.LEADING); 146 lblPlugin.addMouseListener(new PluginCheckBoxMouseAdapter(cbPlugin)); 147 148 gbc.gridx = 0; 149 gbc.gridy = ++row; 150 gbc.insets = new Insets(5, 5, 0, 5); 151 gbc.weighty = 0.0; 152 gbc.weightx = 0.0; 153 cbPlugin.putClientProperty("plugin", pi); 154 add(cbPlugin, gbc); 155 156 gbc.gridx = 1; 157 gbc.weightx = 1.0; 158 lblPlugin.putClientProperty("plugin", pi); 159 add(lblPlugin, gbc); 160 161 HtmlPanel description = new HtmlPanel(); 162 description.setText(pi.getDescriptionAsHtml()); 163 description.enableClickableHyperlinks(); 164 lblPlugin.setLabelFor(description); 165 166 gbc.gridx = 1; 167 gbc.gridy = ++row; 168 gbc.insets = new Insets(3, 25, 5, 5); 169 gbc.weighty = 1.0; 170 description.putClientProperty("plugin", pi); 171 add(description, gbc); 172 } 173 pluginListInitialized = true; 174 } 175 176 /** 177 * Refreshes the list. 178 * 179 * If the list has been changed completely (i.e. not just filtered), 180 * call {@link #resetDisplayedComponents()} prior to calling this method. 181 */ 182 public void refreshView() { 183 final Rectangle visibleRect = getVisibleRect(); 184 if (!pluginListInitialized) { 185 removeAll(); 186 displayEmptyPluginListInformation(); 187 displayPluginList(model.getAvailablePlugins()); 188 } else { 189 hidePluginsNotInList(new HashSet<>(model.getDisplayedPlugins())); 190 } 191 revalidate(); 192 repaint(); 193 SwingUtilities.invokeLater(() -> scrollRectToVisible(visibleRect)); 194 } 195 196 /** 197 * Hides components in the list for plugins that are currently filtered away. 198 * 199 * Since those components are relatively heavyweight rebuilding them every time 200 * when the filter changes is fairly slow, so we build them once and just hide 201 * those that shouldn't be visible. 202 * 203 * @param displayedPlugins A set of plugins that are currently visible. 204 */ 205 private void hidePluginsNotInList(Set<PluginInformation> displayedPlugins) { 206 synchronized (getTreeLock()) { 207 for (int i = 0; i < getComponentCount(); i++) { 208 JComponent component = (JComponent) getComponent(i); 209 Object plugin = component.getClientProperty("plugin"); 210 if ("empty".equals(plugin)) { 211 // Hide the empty plugin list warning if it's there 212 component.setVisible(displayedPlugins.isEmpty()); 213 } else { 214 component.setVisible(displayedPlugins.contains(plugin)); 215 } 216 } 217 } 218 } 219 220 /** 221 * Causes the components for the list items to be rebuilt from scratch. 222 * 223 * Should be called before calling {@link #refreshView()} whenever the 224 * underlying list changes to display a completely different set of 225 * plugins instead of merely hiding plugins by a filter. 226 */ 227 public void resetDisplayedComponents() { 228 pluginListInitialized = false; 229 } 230 231 @Override 232 public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { 233 return visibleRect.height / 4; 234 } 235 236 @Override 237 public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) { 238 return visibleRect.height; 239 } 240}