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}