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 transient 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 transient PluginInformation pi; 113 114 JPluginCheckBox(final PluginInformation pi, boolean selected) { 115 this.pi = pi; 116 setSelected(selected); 117 setToolTipText(formatCheckboxTooltipText(pi)); 118 addActionListener(new PluginCbActionListener(this)); 119 } 120 } 121 122 /** 123 * Listener called when the user selects/unselects a plugin checkbox. 124 * 125 */ 126 private class PluginCbActionListener implements ActionListener { 127 private final JPluginCheckBox cb; 128 129 PluginCbActionListener(JPluginCheckBox cb) { 130 this.cb = cb; 131 } 132 133 protected void selectRequiredPlugins(PluginInformation info) { 134 if (info != null && info.requires != null) { 135 for (String s : info.getRequiredPlugins()) { 136 if (!model.isSelectedPlugin(s)) { 137 model.setPluginSelected(s, true); 138 selectRequiredPlugins(model.getPluginInformation(s)); 139 } 140 } 141 } 142 } 143 144 @Override 145 public void actionPerformed(ActionEvent e) { 146 // Select/unselect corresponding plugin in the model 147 model.setPluginSelected(cb.pi.getName(), cb.isSelected()); 148 // Does the newly selected plugin require other plugins ? 149 if (cb.isSelected() && cb.pi.requires != null) { 150 // Select required plugins 151 selectRequiredPlugins(cb.pi); 152 // Alert user if plugin requirements are not met 153 PluginHandler.checkRequiredPluginsPreconditions(PluginListPanel.this, model.getAvailablePlugins(), cb.pi, false); 154 } else if (!cb.isSelected()) { 155 // If the plugin has been unselected, was it required by other plugins still selected ? 156 Set<String> otherPlugins = new HashSet<>(); 157 for (PluginInformation pi : model.getAvailablePlugins()) { 158 if (!pi.equals(cb.pi) && pi.requires != null && model.isSelectedPlugin(pi.getName())) { 159 for (String s : pi.getRequiredPlugins()) { 160 if (s.equals(cb.pi.getName())) { 161 otherPlugins.add(pi.getName()); 162 break; 163 } 164 } 165 } 166 } 167 if (!otherPlugins.isEmpty()) { 168 alertPluginStillRequired(PluginListPanel.this, cb.pi.getName(), otherPlugins); 169 } 170 } 171 } 172 } 173 174 175 /** 176 * Alerts the user if an unselected plugin is still required by another plugins 177 * 178 * @param parent The parent Component used to display error popup 179 * @param plugin the plugin 180 * @param otherPlugins the other plugins 181 */ 182 private static void alertPluginStillRequired(Component parent, String plugin, Set<String> otherPlugins) { 183 StringBuilder sb = new StringBuilder(); 184 sb.append("<html>") 185 .append(trn("Plugin {0} is still required by this plugin:", 186 "Plugin {0} is still required by these {1} plugins:", 187 otherPlugins.size(), 188 plugin, 189 otherPlugins.size())) 190 .append(Utils.joinAsHtmlUnorderedList(otherPlugins)) 191 .append("</html>"); 192 JOptionPane.showMessageDialog( 193 parent, 194 sb.toString(), 195 tr("Warning"), 196 JOptionPane.WARNING_MESSAGE 197 ); 198 } 199 200 /** 201 * Refreshes the list. 202 */ 203 public void refreshView() { 204 final Rectangle visibleRect = getVisibleRect(); 205 List<PluginInformation> displayedPlugins = model.getDisplayedPlugins(); 206 removeAll(); 207 208 GridBagConstraints gbc = new GridBagConstraints(); 209 gbc.gridx = 0; 210 gbc.anchor = GridBagConstraints.NORTHWEST; 211 gbc.fill = GridBagConstraints.HORIZONTAL; 212 gbc.weightx = 1.0; 213 214 if (displayedPlugins.isEmpty()) { 215 displayEmptyPluginListInformation(); 216 return; 217 } 218 219 int row = -1; 220 for (final PluginInformation pi : displayedPlugins) { 221 boolean selected = model.isSelectedPlugin(pi.getName()); 222 String remoteversion = formatPluginRemoteVersion(pi); 223 String localversion = formatPluginLocalVersion(model.getPluginInformation(pi.getName())); 224 225 final JPluginCheckBox cbPlugin = new JPluginCheckBox(pi, selected); 226 String pluginText = tr("{0}: Version {1} (local: {2})", pi.getName(), remoteversion, localversion); 227 if (pi.requires != null && !pi.requires.isEmpty()) { 228 pluginText += tr(" (requires: {0})", pi.requires); 229 } 230 JLabel lblPlugin = new JLabel( 231 pluginText, 232 pi.getScaledIcon(), 233 SwingConstants.LEFT); 234 lblPlugin.addMouseListener(new MouseAdapter() { 235 @Override 236 public void mouseClicked(MouseEvent e) { 237 cbPlugin.doClick(); 238 } 239 }); 240 241 gbc.gridx = 0; 242 gbc.gridy = ++row; 243 gbc.insets = new Insets(5, 5, 0, 5); 244 gbc.weighty = 0.0; 245 gbc.weightx = 0.0; 246 add(cbPlugin, gbc); 247 248 gbc.gridx = 1; 249 gbc.weightx = 1.0; 250 add(lblPlugin, gbc); 251 252 HtmlPanel description = new HtmlPanel(); 253 description.setText(pi.getDescriptionAsHtml()); 254 description.getEditorPane().addHyperlinkListener(new HyperlinkListener() { 255 @Override 256 public void hyperlinkUpdate(HyperlinkEvent e) { 257 if (e.getEventType() == EventType.ACTIVATED) { 258 OpenBrowser.displayUrl(e.getURL().toString()); 259 } 260 } 261 }); 262 lblPlugin.setLabelFor(description); 263 264 gbc.gridx = 1; 265 gbc.gridy = ++row; 266 gbc.insets = new Insets(3, 25, 5, 5); 267 gbc.weighty = 1.0; 268 add(description, gbc); 269 } 270 revalidate(); 271 repaint(); 272 if (visibleRect != null && visibleRect.width > 0 && visibleRect.height > 0) { 273 SwingUtilities.invokeLater(new Runnable() { 274 @Override 275 public void run() { 276 scrollRectToVisible(visibleRect); 277 } 278 }); 279 } 280 } 281}