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}