001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.preferences.plugin;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.GridBagConstraints;
007import java.awt.GridBagLayout;
008import java.awt.Insets;
009import java.awt.Rectangle;
010import java.awt.event.MouseAdapter;
011import java.awt.event.MouseEvent;
012import java.util.HashSet;
013import java.util.List;
014import java.util.Set;
015
016import javax.swing.JComponent;
017import javax.swing.JLabel;
018import javax.swing.SwingConstants;
019import javax.swing.SwingUtilities;
020
021import org.openstreetmap.josm.gui.widgets.HtmlPanel;
022import org.openstreetmap.josm.gui.widgets.VerticallyScrollablePanel;
023import org.openstreetmap.josm.plugins.PluginInformation;
024import org.openstreetmap.josm.tools.Utils;
025
026/**
027 * A panel displaying the list of known plugins.
028 */
029public class PluginListPanel extends VerticallyScrollablePanel {
030    static final class PluginCheckBoxMouseAdapter extends MouseAdapter {
031        private final PluginCheckBox cbPlugin;
032
033        PluginCheckBoxMouseAdapter(PluginCheckBox cbPlugin) {
034            this.cbPlugin = cbPlugin;
035        }
036
037        @Override
038        public void mouseClicked(MouseEvent e) {
039            cbPlugin.doClick();
040        }
041    }
042
043    private final transient PluginPreferencesModel model;
044
045    /** Whether the plugin list has been built up already in the UI. */
046    private boolean pluginListInitialized;
047
048    /**
049     * Constructs a new {@code PluginListPanel} with a default model.
050     */
051    public PluginListPanel() {
052        this(new PluginPreferencesModel());
053    }
054
055    /**
056     * Constructs a new {@code PluginListPanel} with a given model.
057     * @param model The plugin model
058     */
059    public PluginListPanel(PluginPreferencesModel model) {
060        this.model = model;
061        setLayout(new GridBagLayout());
062    }
063
064    protected static String formatPluginRemoteVersion(PluginInformation pi) {
065        StringBuilder sb = new StringBuilder();
066        if (Utils.isBlank(pi.version)) {
067            sb.append(tr("unknown"));
068        } else {
069            sb.append(pi.version);
070            if (pi.oldmode) {
071                sb.append('*');
072            }
073        }
074        return sb.toString();
075    }
076
077    protected static String formatPluginLocalVersion(PluginInformation pi) {
078        if (pi == null)
079            return tr("unknown");
080        if (Utils.isBlank(pi.localversion))
081            return tr("unknown");
082        return pi.localversion;
083    }
084
085    protected static String formatCheckboxTooltipText(PluginInformation pi) {
086        if (pi == null)
087            return "";
088        if (pi.downloadlink == null)
089            return tr("Plugin bundled with JOSM");
090        else
091            return pi.downloadlink;
092    }
093
094    /**
095     * Displays a message when the plugin list is empty.
096     */
097    public void displayEmptyPluginListInformation() {
098        GridBagConstraints gbc = new GridBagConstraints();
099        gbc.gridx = 0;
100        gbc.anchor = GridBagConstraints.CENTER;
101        gbc.fill = GridBagConstraints.BOTH;
102        gbc.insets = new Insets(40, 0, 40, 0);
103        gbc.weightx = 1.0;
104        gbc.weighty = 1.0;
105
106        HtmlPanel hint = new HtmlPanel();
107        hint.setText(
108                "<html>"
109                + (model.getAvailablePlugins().isEmpty() ?
110                        tr("Please click on <strong>Download list</strong> to download and display a list of available plugins.") :
111                        tr("The filter returned no results."))
112                + "</html>"
113        );
114        hint.putClientProperty("plugin", "empty");
115        hint.setVisible(false);
116        add(hint, gbc);
117    }
118
119    /**
120     * Displays a list of plugins.
121     * @param displayedPlugins list of plugins
122     * @since 13799
123     */
124    public void displayPluginList(List<PluginInformation> displayedPlugins) {
125        GridBagConstraints gbc = new GridBagConstraints();
126        gbc.gridx = 0;
127        gbc.anchor = GridBagConstraints.NORTHWEST;
128        gbc.fill = GridBagConstraints.HORIZONTAL;
129        gbc.weightx = 1.0;
130
131        int row = -1;
132        for (final PluginInformation pi : displayedPlugins) {
133            boolean selected = model.isSelectedPlugin(pi.getName());
134            String remoteversion = formatPluginRemoteVersion(pi);
135            String localversion = formatPluginLocalVersion(model.getPluginInformation(pi.getName()));
136
137            final PluginCheckBox cbPlugin = new PluginCheckBox(pi, selected, this, model);
138            String pluginText = tr("{0}: Version {1} (local: {2})", pi.getName(), remoteversion, localversion);
139            if (!Utils.isEmpty(pi.requires)) {
140                pluginText += tr(" (requires: {0})", pi.requires);
141            }
142            JLabel lblPlugin = new JLabel(
143                    pluginText,
144                    pi.getScaledIcon(),
145                    SwingConstants.LEADING);
146            lblPlugin.addMouseListener(new PluginCheckBoxMouseAdapter(cbPlugin));
147
148            gbc.gridx = 0;
149            gbc.gridy = ++row;
150            gbc.insets = new Insets(5, 5, 0, 5);
151            gbc.weighty = 0.0;
152            gbc.weightx = 0.0;
153            cbPlugin.putClientProperty("plugin", pi);
154            add(cbPlugin, gbc);
155
156            gbc.gridx = 1;
157            gbc.weightx = 1.0;
158            lblPlugin.putClientProperty("plugin", pi);
159            add(lblPlugin, gbc);
160
161            HtmlPanel description = new HtmlPanel();
162            description.setText(pi.getDescriptionAsHtml());
163            description.enableClickableHyperlinks();
164            lblPlugin.setLabelFor(description);
165
166            gbc.gridx = 1;
167            gbc.gridy = ++row;
168            gbc.insets = new Insets(3, 25, 5, 5);
169            gbc.weighty = 1.0;
170            description.putClientProperty("plugin", pi);
171            add(description, gbc);
172        }
173        pluginListInitialized = true;
174    }
175
176    /**
177     * Refreshes the list.
178     *
179     * If the list has been changed completely (i.e. not just filtered),
180     * call {@link #resetDisplayedComponents()} prior to calling this method.
181     */
182    public void refreshView() {
183        final Rectangle visibleRect = getVisibleRect();
184        if (!pluginListInitialized) {
185            removeAll();
186            displayEmptyPluginListInformation();
187            displayPluginList(model.getAvailablePlugins());
188        } else {
189            hidePluginsNotInList(new HashSet<>(model.getDisplayedPlugins()));
190        }
191        revalidate();
192        repaint();
193        SwingUtilities.invokeLater(() -> scrollRectToVisible(visibleRect));
194    }
195
196    /**
197     * Hides components in the list for plugins that are currently filtered away.
198     *
199     * Since those components are relatively heavyweight rebuilding them every time
200     * when the filter changes is fairly slow, so we build them once and just hide
201     * those that shouldn't be visible.
202     *
203     * @param displayedPlugins A set of plugins that are currently visible.
204     */
205    private void hidePluginsNotInList(Set<PluginInformation> displayedPlugins) {
206        synchronized (getTreeLock()) {
207            for (int i = 0; i < getComponentCount(); i++) {
208                JComponent component = (JComponent) getComponent(i);
209                Object plugin = component.getClientProperty("plugin");
210                if ("empty".equals(plugin)) {
211                    // Hide the empty plugin list warning if it's there
212                    component.setVisible(displayedPlugins.isEmpty());
213                } else {
214                    component.setVisible(displayedPlugins.contains(plugin));
215                }
216            }
217        }
218    }
219
220    /**
221     * Causes the components for the list items to be rebuilt from scratch.
222     *
223     * Should be called before calling {@link #refreshView()} whenever the
224     * underlying list changes to display a completely different set of
225     * plugins instead of merely hiding plugins by a filter.
226     */
227    public void resetDisplayedComponents() {
228        pluginListInitialized = false;
229    }
230
231    @Override
232    public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
233        return visibleRect.height / 4;
234    }
235
236    @Override
237    public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
238        return visibleRect.height;
239    }
240}