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}