001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs;
003
004import static org.openstreetmap.josm.tools.I18n.marktr;
005import static org.openstreetmap.josm.tools.I18n.tr;
006
007import java.awt.Component;
008import java.awt.Dimension;
009import java.awt.Font;
010import java.awt.GridBagLayout;
011import java.awt.Insets;
012import java.awt.event.ActionEvent;
013import java.awt.event.KeyEvent;
014import java.awt.event.MouseEvent;
015import java.io.BufferedReader;
016import java.io.File;
017import java.io.IOException;
018import java.io.InputStream;
019import java.io.InputStreamReader;
020import java.nio.charset.StandardCharsets;
021import java.nio.file.Files;
022import java.nio.file.StandardCopyOption;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.Collection;
026import java.util.List;
027
028import javax.swing.AbstractAction;
029import javax.swing.DefaultButtonModel;
030import javax.swing.DefaultListSelectionModel;
031import javax.swing.ImageIcon;
032import javax.swing.JCheckBox;
033import javax.swing.JFileChooser;
034import javax.swing.JLabel;
035import javax.swing.JMenu;
036import javax.swing.JPanel;
037import javax.swing.JScrollPane;
038import javax.swing.JTabbedPane;
039import javax.swing.JTable;
040import javax.swing.ListSelectionModel;
041import javax.swing.SingleSelectionModel;
042import javax.swing.SwingConstants;
043import javax.swing.SwingUtilities;
044import javax.swing.UIManager;
045import javax.swing.border.EmptyBorder;
046import javax.swing.event.ListSelectionEvent;
047import javax.swing.event.ListSelectionListener;
048import javax.swing.filechooser.FileFilter;
049import javax.swing.table.AbstractTableModel;
050import javax.swing.table.DefaultTableCellRenderer;
051import javax.swing.table.TableCellRenderer;
052
053import org.openstreetmap.josm.actions.ExtensionFileFilter;
054import org.openstreetmap.josm.actions.JosmAction;
055import org.openstreetmap.josm.actions.PreferencesAction;
056import org.openstreetmap.josm.data.preferences.sources.SourceEntry;
057import org.openstreetmap.josm.gui.ExtendedDialog;
058import org.openstreetmap.josm.gui.MainApplication;
059import org.openstreetmap.josm.gui.PleaseWaitRunnable;
060import org.openstreetmap.josm.gui.SideButton;
061import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
062import org.openstreetmap.josm.gui.mappaint.MapPaintStyles.MapPaintSylesUpdateListener;
063import org.openstreetmap.josm.gui.mappaint.StyleSettingGroupGui;
064import org.openstreetmap.josm.gui.mappaint.StyleSource;
065import org.openstreetmap.josm.gui.mappaint.loader.MapPaintStyleLoader;
066import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
067import org.openstreetmap.josm.gui.preferences.map.MapPaintPreference;
068import org.openstreetmap.josm.gui.util.FileFilterAllFiles;
069import org.openstreetmap.josm.gui.util.GuiHelper;
070import org.openstreetmap.josm.gui.util.StayOpenPopupMenu;
071import org.openstreetmap.josm.gui.widgets.AbstractFileChooser;
072import org.openstreetmap.josm.gui.widgets.FileChooserManager;
073import org.openstreetmap.josm.gui.widgets.HtmlPanel;
074import org.openstreetmap.josm.gui.widgets.JosmTextArea;
075import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
076import org.openstreetmap.josm.gui.widgets.ScrollableTable;
077import org.openstreetmap.josm.tools.GBC;
078import org.openstreetmap.josm.tools.ImageOverlay;
079import org.openstreetmap.josm.tools.ImageProvider;
080import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
081import org.openstreetmap.josm.tools.InputMapUtils;
082import org.openstreetmap.josm.tools.Logging;
083import org.openstreetmap.josm.tools.Shortcut;
084import org.openstreetmap.josm.tools.Utils;
085
086/**
087 * Dialog to configure the map painting style.
088 * @since 3843
089 */
090public class MapPaintDialog extends ToggleDialog {
091
092    protected ScrollableTable tblStyles;
093    protected StylesModel model;
094    protected final DefaultListSelectionModel selectionModel = new DefaultListSelectionModel();
095
096    protected OnOffAction onoffAction;
097    protected ReloadAction reloadAction;
098    protected MoveUpDownAction upAction;
099    protected MoveUpDownAction downAction;
100    protected JCheckBox cbWireframe;
101
102    /**
103     * Action that opens the map paint preferences.
104     */
105    public static final JosmAction PREFERENCE_ACTION = PreferencesAction.forPreferenceSubTab(
106            tr("Map paint preferences"), null, MapPaintPreference.class, /* ICON */ "dialogs/mappaintpreference");
107
108    /**
109     * Constructs a new {@code MapPaintDialog}.
110     */
111    public MapPaintDialog() {
112        super(tr("Map Paint Styles"), "mapstyle", tr("configure the map painting style"),
113                Shortcut.registerShortcut("subwindow:mappaint", tr("Toggle: {0}", tr("MapPaint")),
114                        KeyEvent.VK_M, Shortcut.ALT_SHIFT), 150, false, MapPaintPreference.class);
115        build();
116    }
117
118    protected void build() {
119        model = new StylesModel();
120
121        cbWireframe = new JCheckBox();
122        JLabel wfLabel = new JLabel(tr("Wireframe View"), ImageProvider.get("dialogs/mappaint", "wireframe_small"), JLabel.HORIZONTAL);
123        wfLabel.setFont(wfLabel.getFont().deriveFont(Font.PLAIN));
124        wfLabel.setLabelFor(cbWireframe);
125
126        cbWireframe.setModel(new DefaultButtonModel() {
127            @Override
128            public void setSelected(boolean b) {
129                super.setSelected(b);
130                tblStyles.setEnabled(!b);
131                onoffAction.updateEnabledState();
132                upAction.updateEnabledState();
133                downAction.updateEnabledState();
134            }
135        });
136        cbWireframe.addActionListener(e -> MainApplication.getMenu().wireFrameToggleAction.actionPerformed(null));
137        cbWireframe.setBorder(new EmptyBorder(new Insets(1, 1, 1, 1)));
138
139        tblStyles = new ScrollableTable(model);
140        tblStyles.setSelectionModel(selectionModel);
141        tblStyles.addMouseListener(new PopupMenuHandler());
142        tblStyles.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
143        tblStyles.setBackground(UIManager.getColor("Panel.background"));
144        tblStyles.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
145        tblStyles.setTableHeader(null);
146        tblStyles.getColumnModel().getColumn(0).setMaxWidth(1);
147        tblStyles.getColumnModel().getColumn(0).setResizable(false);
148        tblStyles.getColumnModel().getColumn(0).setCellRenderer(new MyCheckBoxRenderer());
149        tblStyles.getColumnModel().getColumn(1).setCellRenderer(new StyleSourceRenderer());
150        tblStyles.setShowGrid(false);
151        tblStyles.setIntercellSpacing(new Dimension(0, 0));
152
153        JPanel p = new JPanel(new GridBagLayout());
154        p.add(cbWireframe, GBC.std(0, 0));
155        p.add(wfLabel, GBC.std(1, 0).weight(1, 0));
156        p.add(tblStyles, GBC.std(0, 1).span(2).fill());
157
158        reloadAction = new ReloadAction();
159        onoffAction = new OnOffAction();
160        upAction = new MoveUpDownAction(false);
161        downAction = new MoveUpDownAction(true);
162        selectionModel.addListSelectionListener(onoffAction);
163        selectionModel.addListSelectionListener(reloadAction);
164        selectionModel.addListSelectionListener(upAction);
165        selectionModel.addListSelectionListener(downAction);
166
167        // Toggle style on Enter and Spacebar
168        InputMapUtils.addEnterAction(tblStyles, onoffAction);
169        InputMapUtils.addSpacebarAction(tblStyles, onoffAction);
170
171        createLayout(p, true, Arrays.asList(
172                new SideButton(onoffAction, false),
173                new SideButton(upAction, false),
174                new SideButton(downAction, false),
175                new SideButton(PREFERENCE_ACTION, false)
176        ));
177    }
178
179    @Override
180    public void showNotify() {
181        MapPaintStyles.addMapPaintSylesUpdateListener(model);
182        model.mapPaintStylesUpdated();
183        MainApplication.getMenu().wireFrameToggleAction.addButtonModel(cbWireframe.getModel());
184    }
185
186    @Override
187    public void hideNotify() {
188        MainApplication.getMenu().wireFrameToggleAction.removeButtonModel(cbWireframe.getModel());
189        MapPaintStyles.removeMapPaintSylesUpdateListener(model);
190    }
191
192    protected class StylesModel extends AbstractTableModel implements MapPaintSylesUpdateListener {
193
194        private final Class<?>[] columnClasses = {Boolean.class, StyleSource.class};
195
196        private transient List<StyleSource> data = new ArrayList<>();
197
198        /**
199         * Constructs a new {@code StylesModel}.
200         */
201        public StylesModel() {
202            data = new ArrayList<>(MapPaintStyles.getStyles().getStyleSources());
203        }
204
205        private StyleSource getRow(int i) {
206            return data.get(i);
207        }
208
209        @Override
210        public int getColumnCount() {
211            return 2;
212        }
213
214        @Override
215        public int getRowCount() {
216            return data.size();
217        }
218
219        @Override
220        public Object getValueAt(int row, int column) {
221            if (column == 0)
222                return getRow(row).active;
223            else
224                return getRow(row);
225        }
226
227        @Override
228        public boolean isCellEditable(int row, int column) {
229            return column == 0;
230        }
231
232        @Override
233        public Class<?> getColumnClass(int column) {
234            return columnClasses[column];
235        }
236
237        @Override
238        public void setValueAt(Object aValue, int row, int column) {
239            if (row < 0 || row >= getRowCount() || aValue == null)
240                return;
241            if (column == 0) {
242                MapPaintStyles.toggleStyleActive(row);
243            }
244        }
245
246        /**
247         * Make sure the first of the selected entry is visible in the
248         * views of this model.
249         */
250        public void ensureSelectedIsVisible() {
251            int index = selectionModel.getMinSelectionIndex();
252            if (index < 0)
253                return;
254            if (index >= getRowCount())
255                return;
256            tblStyles.scrollToVisible(index, 0);
257            tblStyles.repaint();
258        }
259
260        @Override
261        public void mapPaintStylesUpdated() {
262            data = new ArrayList<>(MapPaintStyles.getStyles().getStyleSources());
263            fireTableDataChanged();
264            tblStyles.repaint();
265        }
266
267        @Override
268        public void mapPaintStyleEntryUpdated(int idx) {
269            data = new ArrayList<>(MapPaintStyles.getStyles().getStyleSources());
270            fireTableRowsUpdated(idx, idx);
271            tblStyles.repaint();
272        }
273    }
274
275    private class MyCheckBoxRenderer extends JCheckBox implements TableCellRenderer {
276
277        /**
278         * Constructs a new {@code MyCheckBoxRenderer}.
279         */
280        MyCheckBoxRenderer() {
281            setHorizontalAlignment(SwingConstants.CENTER);
282            setVerticalAlignment(SwingConstants.CENTER);
283        }
284
285        @Override
286        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
287            if (value == null)
288                return this;
289            boolean b = (Boolean) value;
290            setSelected(b);
291            setEnabled(!cbWireframe.isSelected());
292            return this;
293        }
294    }
295
296    private class StyleSourceRenderer extends DefaultTableCellRenderer {
297        @Override
298        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
299            if (value == null)
300                return this;
301            StyleSource s = (StyleSource) value;
302            JLabel label = (JLabel) super.getTableCellRendererComponent(table,
303                    s.getDisplayString(), isSelected, hasFocus, row, column);
304            label.setIcon(s.getIcon());
305            label.setToolTipText(s.getToolTipText());
306            label.setEnabled(!cbWireframe.isSelected());
307            return label;
308        }
309    }
310
311    protected class OnOffAction extends AbstractAction implements ListSelectionListener {
312        /**
313         * Constructs a new {@code OnOffAction}.
314         */
315        public OnOffAction() {
316            putValue(NAME, tr("On/Off"));
317            putValue(SHORT_DESCRIPTION, tr("Turn selected styles on or off"));
318            new ImageProvider("apply").getResource().attachImageIcon(this, true);
319            updateEnabledState();
320        }
321
322        protected void updateEnabledState() {
323            setEnabled(!cbWireframe.isSelected() && tblStyles.getSelectedRowCount() > 0);
324        }
325
326        @Override
327        public void valueChanged(ListSelectionEvent e) {
328            updateEnabledState();
329        }
330
331        @Override
332        public void actionPerformed(ActionEvent e) {
333            int[] pos = tblStyles.getSelectedRows();
334            MapPaintStyles.toggleStyleActive(pos);
335            selectionModel.setValueIsAdjusting(true);
336            selectionModel.clearSelection();
337            for (int p: pos) {
338                selectionModel.addSelectionInterval(p, p);
339            }
340            selectionModel.setValueIsAdjusting(false);
341        }
342    }
343
344    /**
345     * The action to move down the currently selected entries in the list.
346     */
347    protected class MoveUpDownAction extends AbstractAction implements ListSelectionListener {
348
349        private final int increment;
350
351        /**
352         * Constructs a new {@code MoveUpDownAction}.
353         * @param isDown {@code true} to move the entry down, {@code false} to move it up
354         */
355        public MoveUpDownAction(boolean isDown) {
356            increment = isDown ? 1 : -1;
357            putValue(NAME, isDown ? tr("Down") : tr("Up"));
358            new ImageProvider("dialogs", isDown ? "down" : "up").getResource().attachImageIcon(this, true);
359            putValue(SHORT_DESCRIPTION, isDown ? tr("Move the selected entry one row down.") : tr("Move the selected entry one row up."));
360            updateEnabledState();
361        }
362
363        public void updateEnabledState() {
364            int[] sel = tblStyles.getSelectedRows();
365            setEnabled(!cbWireframe.isSelected() && MapPaintStyles.canMoveStyles(sel, increment));
366        }
367
368        @Override
369        public void actionPerformed(ActionEvent e) {
370            int[] sel = tblStyles.getSelectedRows();
371            MapPaintStyles.moveStyles(sel, increment);
372
373            selectionModel.setValueIsAdjusting(true);
374            selectionModel.clearSelection();
375            for (int row: sel) {
376                selectionModel.addSelectionInterval(row + increment, row + increment);
377            }
378            selectionModel.setValueIsAdjusting(false);
379            model.ensureSelectedIsVisible();
380        }
381
382        @Override
383        public void valueChanged(ListSelectionEvent e) {
384            updateEnabledState();
385        }
386    }
387
388    protected class ReloadAction extends AbstractAction implements ListSelectionListener {
389        /**
390         * Constructs a new {@code ReloadAction}.
391         */
392        public ReloadAction() {
393            putValue(NAME, tr("Reload from file"));
394            putValue(SHORT_DESCRIPTION, tr("reload selected styles from file"));
395            new ImageProvider("dialogs", "refresh").getResource().attachImageIcon(this);
396            setEnabled(getEnabledState());
397        }
398
399        protected boolean getEnabledState() {
400            if (cbWireframe.isSelected())
401                return false;
402            int[] pos = tblStyles.getSelectedRows();
403            if (pos.length == 0)
404                return false;
405            for (int i : pos) {
406                if (!model.getRow(i).isLocal())
407                    return false;
408            }
409            return true;
410        }
411
412        @Override
413        public void valueChanged(ListSelectionEvent e) {
414            setEnabled(getEnabledState());
415        }
416
417        @Override
418        public void actionPerformed(ActionEvent e) {
419            final int[] rows = tblStyles.getSelectedRows();
420            MapPaintStyleLoader.reloadStyles(rows);
421            MainApplication.worker.submit(() -> SwingUtilities.invokeLater(() -> {
422                selectionModel.setValueIsAdjusting(true);
423                selectionModel.clearSelection();
424                for (int r: rows) {
425                    selectionModel.addSelectionInterval(r, r);
426                }
427                selectionModel.setValueIsAdjusting(false);
428            }));
429        }
430    }
431
432    protected class SaveAsAction extends AbstractAction {
433
434        /**
435         * Constructs a new {@code SaveAsAction}.
436         */
437        public SaveAsAction() {
438            putValue(NAME, tr("Save as..."));
439            putValue(SHORT_DESCRIPTION, tr("Save a copy of this Style to file and add it to the list"));
440            new ImageProvider("copy").getResource().attachImageIcon(this);
441            setEnabled(tblStyles.getSelectedRows().length == 1);
442        }
443
444        @Override
445        public void actionPerformed(ActionEvent e) {
446            int sel = tblStyles.getSelectionModel().getLeadSelectionIndex();
447            if (sel < 0 || sel >= model.getRowCount())
448                return;
449            final StyleSource s = model.getRow(sel);
450
451            FileChooserManager fcm = new FileChooserManager(false, "mappaint.clone-style.lastDirectory", Utils.getSystemProperty("user.home"));
452            String suggestion = fcm.getInitialDirectory() + File.separator + s.getFileNamePart();
453
454            FileFilter ff;
455            if (s instanceof MapCSSStyleSource) {
456                ff = new ExtensionFileFilter("mapcss,css,zip", "mapcss", tr("Map paint style file (*.mapcss, *.zip)"));
457            } else {
458                ff = new ExtensionFileFilter("xml,zip", "xml", tr("Map paint style file (*.xml, *.zip)"));
459            }
460            fcm.createFileChooser(false, null, Arrays.asList(ff, FileFilterAllFiles.getInstance()), ff, JFileChooser.FILES_ONLY)
461                    .getFileChooser().setSelectedFile(new File(suggestion));
462            AbstractFileChooser fc = fcm.openFileChooser();
463            if (fc == null)
464                return;
465            MainApplication.worker.submit(new SaveToFileTask(s, fc.getSelectedFile()));
466        }
467
468        private class SaveToFileTask extends PleaseWaitRunnable {
469            private final StyleSource s;
470            private final File file;
471
472            private boolean canceled;
473            private boolean error;
474
475            SaveToFileTask(StyleSource s, File file) {
476                super(tr("Reloading style sources"));
477                this.s = s;
478                this.file = file;
479            }
480
481            @Override
482            protected void cancel() {
483                canceled = true;
484            }
485
486            @Override
487            protected void realRun() {
488                getProgressMonitor().indeterminateSubTask(
489                        tr("Save style ''{0}'' as ''{1}''", s.getDisplayString(), file.getPath()));
490                try {
491                    try (InputStream in = s.getSourceInputStream()) {
492                        Files.copy(in, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
493                    }
494                } catch (IOException e) {
495                    Logging.warn(e);
496                    error = true;
497                }
498            }
499
500            @Override
501            protected void finish() {
502                SwingUtilities.invokeLater(() -> {
503                    if (!error && !canceled) {
504                        SourceEntry se = new SourceEntry(s);
505                        se.url = file.getPath();
506                        MapPaintStyles.addStyle(se);
507                        tblStyles.getSelectionModel().setSelectionInterval(model.getRowCount() - 1, model.getRowCount() - 1);
508                        model.ensureSelectedIsVisible();
509                    }
510                });
511            }
512        }
513    }
514
515    /**
516     * Displays information about selected paint style in a new dialog.
517     */
518    protected class InfoAction extends AbstractAction {
519
520        private boolean errorsTabLoaded;
521        private boolean warningsTabLoaded;
522        private boolean sourceTabLoaded;
523
524        /**
525         * Constructs a new {@code InfoAction}.
526         */
527        public InfoAction() {
528            putValue(NAME, tr("Info"));
529            putValue(SHORT_DESCRIPTION, tr("view meta information, error log and source definition"));
530            new ImageProvider("info").getResource().attachImageIcon(this);
531            setEnabled(tblStyles.getSelectedRows().length == 1);
532        }
533
534        @Override
535        public void actionPerformed(ActionEvent e) {
536            int sel = tblStyles.getSelectionModel().getLeadSelectionIndex();
537            if (sel < 0 || sel >= model.getRowCount())
538                return;
539            final StyleSource s = model.getRow(sel);
540            ExtendedDialog info = new ExtendedDialog(MainApplication.getMainFrame(), tr("Map Style info"), tr("Close"));
541            info.setPreferredSize(new Dimension(600, 400));
542            info.setButtonIcons("ok");
543
544            final JTabbedPane tabs = new JTabbedPane();
545
546            JLabel lblInfo = new JLabel(tr("Info"));
547            lblInfo.setLabelFor(tabs.add("Info", buildInfoPanel(s)));
548            lblInfo.setFont(lblInfo.getFont().deriveFont(Font.PLAIN));
549            tabs.setTabComponentAt(0, lblInfo);
550
551            final JPanel pErrors = addErrorOrWarningTab(tabs, lblInfo,
552                    s.getErrors(), marktr("Errors"), 1, ImageProvider.get("misc", "error"));
553            final JPanel pWarnings = addErrorOrWarningTab(tabs, lblInfo,
554                    s.getWarnings(), marktr("Warnings"), 2, ImageProvider.get("warning-small"));
555
556            final JPanel pSource = new JPanel(new GridBagLayout());
557            JLabel lblSource = new JLabel(tr("Source"));
558            lblSource.setLabelFor(tabs.add("Source", pSource));
559            lblSource.setFont(lblSource.getFont().deriveFont(Font.PLAIN));
560            tabs.setTabComponentAt(3, lblSource);
561
562            tabs.getModel().addChangeListener(e1 -> {
563                if (!errorsTabLoaded && ((SingleSelectionModel) e1.getSource()).getSelectedIndex() == 1) {
564                    errorsTabLoaded = true;
565                    buildErrorsOrWarningPanel(s.getErrors(), pErrors);
566                }
567                if (!warningsTabLoaded && ((SingleSelectionModel) e1.getSource()).getSelectedIndex() == 2) {
568                    warningsTabLoaded = true;
569                    buildErrorsOrWarningPanel(s.getWarnings(), pWarnings);
570                }
571                if (!sourceTabLoaded && ((SingleSelectionModel) e1.getSource()).getSelectedIndex() == 3) {
572                    sourceTabLoaded = true;
573                    buildSourcePanel(s, pSource);
574                }
575            });
576            info.setContent(tabs, false);
577            info.showDialog();
578        }
579
580        private JPanel addErrorOrWarningTab(final JTabbedPane tabs, JLabel lblInfo,
581                Collection<?> items, String title, int pos, ImageIcon icon) {
582            final JPanel pErrors = new JPanel(new GridBagLayout());
583            tabs.add(title, pErrors);
584            if (items.isEmpty()) {
585                JLabel lblErrors = new JLabel(tr(title));
586                lblErrors.setLabelFor(pErrors);
587                lblErrors.setFont(lblInfo.getFont().deriveFont(Font.PLAIN));
588                lblErrors.setEnabled(false);
589                tabs.setTabComponentAt(pos, lblErrors);
590                tabs.setEnabledAt(pos, false);
591            } else {
592                JLabel lblErrors = new JLabel(tr(title), icon, JLabel.HORIZONTAL);
593                lblErrors.setLabelFor(pErrors);
594                tabs.setTabComponentAt(pos, lblErrors);
595            }
596            return pErrors;
597        }
598
599        private JPanel buildInfoPanel(StyleSource s) {
600            JPanel p = new JPanel(new GridBagLayout());
601            StringBuilder text = new StringBuilder("<table cellpadding=3>");
602            text.append(tableRow(tr("Title:"), s.getDisplayString()));
603            if (s.url.startsWith("http://") || s.url.startsWith("https://")) {
604                text.append(tableRow(tr("URL:"), s.url));
605            } else if (s.url.startsWith("resource://")) {
606                text.append(tableRow(tr("Built-in Style, internal path:"), s.url));
607            } else {
608                text.append(tableRow(tr("Path:"), s.url));
609            }
610            if (s.icon != null) {
611                text.append(tableRow(tr("Icon:"), s.icon));
612            }
613            if (s.getBackgroundColorOverride() != null) {
614                text.append(tableRow(tr("Background:"), Utils.toString(s.getBackgroundColorOverride())));
615            }
616            text.append(tableRow(tr("Style is currently active?"), s.active ? tr("Yes") : tr("No")))
617                .append("</table>");
618            p.add(new JScrollPane(new HtmlPanel(text.toString())), GBC.eol().fill(GBC.BOTH));
619            return p;
620        }
621
622        private String tableRow(String firstColumn, String secondColumn) {
623            return "<tr><td><b>" + firstColumn + "</b></td><td>" + secondColumn + "</td></tr>";
624        }
625
626        private void buildSourcePanel(StyleSource s, JPanel p) {
627            JosmTextArea txtSource = new JosmTextArea();
628            txtSource.setFont(GuiHelper.getMonospacedFont(txtSource));
629            txtSource.setEditable(false);
630            p.add(new JScrollPane(txtSource), GBC.std().fill());
631
632            try (BufferedReader reader = new BufferedReader(new InputStreamReader(s.getSourceInputStream(), StandardCharsets.UTF_8))) {
633                reader.lines().forEach(line -> txtSource.append(line + '\n'));
634            } catch (IOException ex) {
635                Logging.error(ex);
636                txtSource.append("<ERROR: failed to read file!>");
637            }
638            txtSource.setCaretPosition(0);
639        }
640
641        private <T> void buildErrorsOrWarningPanel(Collection<T> items, JPanel p) {
642            JosmTextArea txtErrors = new JosmTextArea();
643            txtErrors.setFont(GuiHelper.getMonospacedFont(txtErrors));
644            txtErrors.setEditable(false);
645            p.add(new JScrollPane(txtErrors), GBC.std().fill());
646            for (T t : items) {
647                txtErrors.append(t.toString() + '\n');
648            }
649            txtErrors.setCaretPosition(0);
650        }
651    }
652
653    class PopupMenuHandler extends PopupMenuLauncher {
654        @Override
655        public void launch(MouseEvent evt) {
656            if (cbWireframe.isSelected())
657                return;
658            super.launch(evt);
659        }
660
661        @Override
662        protected void showMenu(MouseEvent evt) {
663            menu = new MapPaintPopup();
664            super.showMenu(evt);
665        }
666    }
667
668    /**
669     * The popup menu displayed when right-clicking a map paint entry
670     */
671    public class MapPaintPopup extends StayOpenPopupMenu {
672        /**
673         * Constructs a new {@code MapPaintPopup}.
674         */
675        public MapPaintPopup() {
676            add(reloadAction);
677            add(new SaveAsAction());
678
679            JMenu setMenu = new JMenu(tr("Style settings"));
680            setMenu.setIcon(new ImageProvider("preference").setMaxSize(ImageSizes.POPUPMENU).addOverlay(
681                new ImageOverlay(new ImageProvider("dialogs/mappaint", "pencil"), 0.5, 0.5, 1.0, 1.0)).get());
682            setMenu.setToolTipText(tr("Customize the style"));
683            add(setMenu);
684
685            final int sel = tblStyles.getSelectionModel().getLeadSelectionIndex();
686            final StyleSource style = sel >= 0 && sel < model.getRowCount() ? model.getRow(sel) : null;
687            if (style == null || style.settings.isEmpty()) {
688                setMenu.setEnabled(false);
689            } else {
690                // Add settings groups
691                style.settingGroups.forEach((group, settings) -> new StyleSettingGroupGui(group, settings).addMenuEntry(setMenu));
692                // Add settings not in groups
693                style.settings.stream()
694                        .filter(s -> style.settingGroups.values().stream().flatMap(List::stream).noneMatch(s::equals))
695                        .forEach(s -> s.getStyleSettingGui().addMenuEntry(setMenu));
696            }
697
698            addSeparator();
699            add(new InfoAction());
700        }
701    }
702}