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