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