001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.preferences.plugin; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005import static org.openstreetmap.josm.tools.I18n.trn; 006 007import java.awt.Component; 008import java.awt.GridBagConstraints; 009import java.awt.GridBagLayout; 010import java.awt.Insets; 011import java.awt.Rectangle; 012import java.awt.event.ActionEvent; 013import java.awt.event.ActionListener; 014import java.awt.event.MouseAdapter; 015import java.awt.event.MouseEvent; 016import java.util.HashSet; 017import java.util.List; 018import java.util.Set; 019 020import javax.swing.JCheckBox; 021import javax.swing.JLabel; 022import javax.swing.JOptionPane; 023import javax.swing.SwingConstants; 024import javax.swing.SwingUtilities; 025import javax.swing.event.HyperlinkEvent; 026import javax.swing.event.HyperlinkEvent.EventType; 027import javax.swing.event.HyperlinkListener; 028 029import org.openstreetmap.josm.gui.widgets.HtmlPanel; 030import org.openstreetmap.josm.gui.widgets.VerticallyScrollablePanel; 031import org.openstreetmap.josm.plugins.PluginHandler; 032import org.openstreetmap.josm.plugins.PluginInformation; 033import org.openstreetmap.josm.tools.OpenBrowser; 034import org.openstreetmap.josm.tools.Utils; 035 036/** 037 * A panel displaying the list of known plugins. 038 */ 039public class PluginListPanel extends VerticallyScrollablePanel { 040 private PluginPreferencesModel model; 041 042 /** 043 * Constructs a new {@code PluginListPanel} with a default model. 044 */ 045 public PluginListPanel() { 046 this(new PluginPreferencesModel()); 047 } 048 049 /** 050 * Constructs a new {@code PluginListPanel} with a given model. 051 * @param model The plugin model 052 */ 053 public PluginListPanel(PluginPreferencesModel model) { 054 this.model = model; 055 setLayout(new GridBagLayout()); 056 } 057 058 protected String formatPluginRemoteVersion(PluginInformation pi) { 059 StringBuilder sb = new StringBuilder(); 060 if (pi.version == null || pi.version.trim().isEmpty()) { 061 sb.append(tr("unknown")); 062 } else { 063 sb.append(pi.version); 064 if (pi.oldmode) { 065 sb.append("*"); 066 } 067 } 068 return sb.toString(); 069 } 070 071 protected String formatPluginLocalVersion(PluginInformation pi) { 072 if (pi == null) return tr("unknown"); 073 if (pi.localversion == null || pi.localversion.trim().isEmpty()) 074 return tr("unknown"); 075 return pi.localversion; 076 } 077 078 protected String formatCheckboxTooltipText(PluginInformation pi) { 079 if (pi == null) return ""; 080 if (pi.downloadlink == null) 081 return tr("Plugin bundled with JOSM"); 082 else 083 return pi.downloadlink; 084 } 085 086 /** 087 * Displays a message when the plugin list is empty. 088 */ 089 public void displayEmptyPluginListInformation() { 090 GridBagConstraints gbc = new GridBagConstraints(); 091 gbc.gridx = 0; 092 gbc.anchor = GridBagConstraints.CENTER; 093 gbc.fill = GridBagConstraints.BOTH; 094 gbc.insets = new Insets(40,0,40,0); 095 gbc.weightx = 1.0; 096 gbc.weighty = 1.0; 097 098 HtmlPanel hint = new HtmlPanel(); 099 hint.setText( 100 "<html>" 101 + tr("Please click on <strong>Download list</strong> to download and display a list of available plugins.") 102 + "</html>" 103 ); 104 add(hint, gbc); 105 } 106 107 /** 108 * A plugin checkbox. 109 * 110 */ 111 private class JPluginCheckBox extends JCheckBox { 112 public final PluginInformation pi; 113 public JPluginCheckBox(final PluginInformation pi, boolean selected) { 114 this.pi = pi; 115 setSelected(selected); 116 setToolTipText(formatCheckboxTooltipText(pi)); 117 addActionListener(new PluginCbActionListener(this)); 118 } 119 } 120 121 /** 122 * Listener called when the user selects/unselects a plugin checkbox. 123 * 124 */ 125 private class PluginCbActionListener implements ActionListener { 126 private final JPluginCheckBox cb; 127 public PluginCbActionListener(JPluginCheckBox cb) { 128 this.cb = cb; 129 } 130 protected void selectRequiredPlugins(PluginInformation info) { 131 if (info != null && info.requires != null) { 132 for (String s : info.getRequiredPlugins()) { 133 if (!model.isSelectedPlugin(s)) { 134 model.setPluginSelected(s, true); 135 selectRequiredPlugins(model.getPluginInformation(s)); 136 } 137 } 138 } 139 } 140 @Override 141 public void actionPerformed(ActionEvent e) { 142 // Select/unselect corresponding plugin in the model 143 model.setPluginSelected(cb.pi.getName(), cb.isSelected()); 144 // Does the newly selected plugin require other plugins ? 145 if (cb.isSelected() && cb.pi.requires != null) { 146 // Select required plugins 147 selectRequiredPlugins(cb.pi); 148 // Alert user if plugin requirements are not met 149 PluginHandler.checkRequiredPluginsPreconditions(PluginListPanel.this, model.getAvailablePlugins(), cb.pi, false); 150 } 151 // If the plugin has been unselected, was it required by other plugins still selected ? 152 else if (!cb.isSelected()) { 153 Set<String> otherPlugins = new HashSet<>(); 154 for (PluginInformation pi : model.getAvailablePlugins()) { 155 if (!pi.equals(cb.pi) && pi.requires != null && model.isSelectedPlugin(pi.getName())) { 156 for (String s : pi.getRequiredPlugins()) { 157 if (s.equals(cb.pi.getName())) { 158 otherPlugins.add(pi.getName()); 159 break; 160 } 161 } 162 } 163 } 164 if (!otherPlugins.isEmpty()) { 165 alertPluginStillRequired(PluginListPanel.this, cb.pi.getName(), otherPlugins); 166 } 167 } 168 } 169 } 170 171 172 /** 173 * Alerts the user if an unselected plugin is still required by another plugins 174 * 175 * @param parent The parent Component used to display error popup 176 * @param plugin the plugin 177 * @param otherPlugins the other plugins 178 */ 179 private static void alertPluginStillRequired(Component parent, String plugin, Set<String> otherPlugins) { 180 StringBuilder sb = new StringBuilder(); 181 sb.append("<html>"); 182 sb.append(trn("Plugin {0} is still required by this plugin:", 183 "Plugin {0} is still required by these {1} plugins:", 184 otherPlugins.size(), 185 plugin, 186 otherPlugins.size() 187 )); 188 sb.append(Utils.joinAsHtmlUnorderedList(otherPlugins)); 189 sb.append("</html>"); 190 JOptionPane.showMessageDialog( 191 parent, 192 sb.toString(), 193 tr("Warning"), 194 JOptionPane.WARNING_MESSAGE 195 ); 196 } 197 198 /** 199 * Refreshes the list. 200 */ 201 public void refreshView() { 202 final Rectangle visibleRect = getVisibleRect(); 203 List<PluginInformation> displayedPlugins = model.getDisplayedPlugins(); 204 removeAll(); 205 206 GridBagConstraints gbc = new GridBagConstraints(); 207 gbc.gridx = 0; 208 gbc.anchor = GridBagConstraints.NORTHWEST; 209 gbc.fill = GridBagConstraints.HORIZONTAL; 210 gbc.weightx = 1.0; 211 212 if (displayedPlugins.isEmpty()) { 213 displayEmptyPluginListInformation(); 214 return; 215 } 216 217 int row = -1; 218 for (final PluginInformation pi : displayedPlugins) { 219 boolean selected = model.isSelectedPlugin(pi.getName()); 220 String remoteversion = formatPluginRemoteVersion(pi); 221 String localversion = formatPluginLocalVersion(model.getPluginInformation(pi.getName())); 222 223 final JPluginCheckBox cbPlugin = new JPluginCheckBox(pi, selected); 224 String pluginText = tr("{0}: Version {1} (local: {2})", pi.getName(), remoteversion, localversion); 225 if (pi.requires != null && !pi.requires.isEmpty()) { 226 pluginText += tr(" (requires: {0})", pi.requires); 227 } 228 JLabel lblPlugin = new JLabel( 229 pluginText, 230 pi.getScaledIcon(), 231 SwingConstants.LEFT); 232 lblPlugin.addMouseListener(new MouseAdapter() { 233 @Override 234 public void mouseClicked(MouseEvent e) { 235 cbPlugin.doClick(); 236 } 237 }); 238 239 gbc.gridx = 0; 240 gbc.gridy = ++row; 241 gbc.insets = new Insets(5,5,0,5); 242 gbc.weighty = 0.0; 243 gbc.weightx = 0.0; 244 add(cbPlugin, gbc); 245 246 gbc.gridx = 1; 247 gbc.weightx = 1.0; 248 add(lblPlugin, gbc); 249 250 HtmlPanel description = new HtmlPanel(); 251 description.setText(pi.getDescriptionAsHtml()); 252 description.getEditorPane().addHyperlinkListener(new HyperlinkListener() { 253 @Override 254 public void hyperlinkUpdate(HyperlinkEvent e) { 255 if(e.getEventType() == EventType.ACTIVATED) { 256 OpenBrowser.displayUrl(e.getURL().toString()); 257 } 258 } 259 }); 260 261 gbc.gridx = 1; 262 gbc.gridy = ++row; 263 gbc.insets = new Insets(3,25,5,5); 264 gbc.weighty = 1.0; 265 add(description, gbc); 266 } 267 revalidate(); 268 repaint(); 269 if (visibleRect != null && visibleRect.width > 0 && visibleRect.height > 0) { 270 SwingUtilities.invokeLater(new Runnable() { 271 @Override 272 public void run() { 273 scrollRectToVisible(visibleRect); 274 } 275 }); 276 } 277 } 278}