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