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