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