001 // License: GPL. For details, see LICENSE file. 002 package org.openstreetmap.josm.gui.preferences.plugin; 003 004 import static org.openstreetmap.josm.tools.I18n.tr; 005 import static org.openstreetmap.josm.tools.I18n.trn; 006 007 import java.awt.Component; 008 import java.awt.GridBagConstraints; 009 import java.awt.GridBagLayout; 010 import java.awt.Insets; 011 import java.awt.Rectangle; 012 import java.awt.event.ActionEvent; 013 import java.awt.event.ActionListener; 014 import java.util.HashSet; 015 import java.util.List; 016 import java.util.Set; 017 018 import javax.swing.JCheckBox; 019 import javax.swing.JLabel; 020 import javax.swing.JOptionPane; 021 import javax.swing.SwingConstants; 022 import javax.swing.SwingUtilities; 023 import javax.swing.event.HyperlinkEvent; 024 import javax.swing.event.HyperlinkEvent.EventType; 025 import javax.swing.event.HyperlinkListener; 026 027 import org.openstreetmap.josm.gui.widgets.HtmlPanel; 028 import org.openstreetmap.josm.gui.widgets.VerticallyScrollablePanel; 029 import org.openstreetmap.josm.plugins.PluginHandler; 030 import org.openstreetmap.josm.plugins.PluginInformation; 031 import org.openstreetmap.josm.tools.OpenBrowser; 032 033 public 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().equals("")) { 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().equals("")) 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 public void actionPerformed(ActionEvent e) { 115 // Select/unselect corresponding plugin in the model 116 model.setPluginSelected(cb.pi.getName(), cb.isSelected()); 117 // Does the newly selected plugin require other plugins ? 118 if (cb.isSelected() && cb.pi.requires != null) { 119 // Select required plugins 120 for (String s : cb.pi.requires.split(";")) { 121 model.setPluginSelected(s.trim(), true); 122 } 123 // Alert user if plugin requirements are not met 124 PluginHandler.checkRequiredPluginsPreconditions(PluginListPanel.this, model.getAvailablePlugins(), cb.pi); 125 } 126 // If the plugin has been unselected, was it required by other plugins still selected ? 127 else if (!cb.isSelected()) { 128 Set<String> otherPlugins = new HashSet<String>(); 129 for (PluginInformation pi : model.getAvailablePlugins()) { 130 if (!pi.equals(cb.pi) && pi.requires != null && model.isSelectedPlugin(pi.getName())) { 131 for (String s : pi.requires.split(";")) { 132 if (s.trim().equals(cb.pi.getName())) { 133 otherPlugins.add(pi.getName()); 134 break; 135 } 136 } 137 } 138 } 139 if (!otherPlugins.isEmpty()) { 140 alertPluginStillRequired(PluginListPanel.this, cb.pi.getName(), otherPlugins); 141 } 142 } 143 } 144 }; 145 146 147 /** 148 * Alerts the user if an unselected plugin is still required by another plugins 149 * 150 * @param parent The parent Component used to display error popup 151 * @param plugin the plugin 152 * @param otherPlugins the other plugins 153 */ 154 private static void alertPluginStillRequired(Component parent, String plugin, Set<String> otherPlugins) { 155 StringBuilder sb = new StringBuilder(); 156 sb.append("<html>"); 157 sb.append(trn("Plugin {0} is still required by this plugin:", 158 "Plugin {0} is still required by these {1} plugins:", 159 otherPlugins.size(), 160 plugin, 161 otherPlugins.size() 162 )); 163 sb.append("<ul>"); 164 for (String p: otherPlugins) { 165 sb.append("<li>").append(p).append("</li>"); 166 } 167 sb.append("</ul>").append("</html>"); 168 JOptionPane.showMessageDialog( 169 parent, 170 sb.toString(), 171 tr("Warning"), 172 JOptionPane.WARNING_MESSAGE 173 ); 174 } 175 176 public void refreshView() { 177 final Rectangle visibleRect = getVisibleRect(); 178 List<PluginInformation> displayedPlugins = model.getDisplayedPlugins(); 179 removeAll(); 180 181 GridBagConstraints gbc = new GridBagConstraints(); 182 gbc.gridx = 0; 183 gbc.anchor = GridBagConstraints.NORTHWEST; 184 gbc.fill = GridBagConstraints.HORIZONTAL; 185 gbc.weightx = 1.0; 186 187 if (displayedPlugins.isEmpty()) { 188 displayEmptyPluginListInformation(); 189 return; 190 } 191 192 int row = -1; 193 for (final PluginInformation pi : displayedPlugins) { 194 boolean selected = model.isSelectedPlugin(pi.getName()); 195 String remoteversion = formatPluginRemoteVersion(pi); 196 String localversion = formatPluginLocalVersion(model.getPluginInformation(pi.getName())); 197 198 JPluginCheckBox cbPlugin = new JPluginCheckBox(pi, selected); 199 String pluginText = tr("{0}: Version {1} (local: {2})", pi.getName(), remoteversion, localversion); 200 if (pi.requires != null && !pi.requires.isEmpty()) { 201 pluginText += tr(" (requires: {0})", pi.requires); 202 } 203 JLabel lblPlugin = new JLabel( 204 pluginText, 205 pi.getScaledIcon(), 206 SwingConstants.LEFT); 207 208 gbc.gridx = 0; 209 gbc.gridy = ++row; 210 gbc.insets = new Insets(5,5,0,5); 211 gbc.weighty = 0.0; 212 gbc.weightx = 0.0; 213 add(cbPlugin, gbc); 214 215 gbc.gridx = 1; 216 gbc.weightx = 1.0; 217 add(lblPlugin, gbc); 218 219 HtmlPanel description = new HtmlPanel(); 220 description.setText(pi.getDescriptionAsHtml()); 221 description.getEditorPane().addHyperlinkListener(new HyperlinkListener() { 222 public void hyperlinkUpdate(HyperlinkEvent e) { 223 if(e.getEventType() == EventType.ACTIVATED) { 224 OpenBrowser.displayUrl(e.getURL().toString()); 225 } 226 } 227 }); 228 229 gbc.gridx = 1; 230 gbc.gridy = ++row; 231 gbc.insets = new Insets(3,25,5,5); 232 gbc.weighty = 1.0; 233 add(description, gbc); 234 } 235 revalidate(); 236 repaint(); 237 if (visibleRect != null && visibleRect.width > 0 && visibleRect.height > 0) { 238 SwingUtilities.invokeLater(new Runnable() { 239 @Override 240 public void run() { 241 scrollRectToVisible(visibleRect); 242 } 243 }); 244 } 245 } 246 }