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