001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005import static org.openstreetmap.josm.tools.I18n.trn;
006
007import java.awt.Component;
008import java.awt.Dimension;
009import java.awt.Font;
010import java.awt.GridBagLayout;
011import java.awt.Insets;
012import java.awt.Point;
013import java.awt.Rectangle;
014import java.awt.event.ActionEvent;
015import java.awt.event.ActionListener;
016import java.awt.event.KeyEvent;
017import java.awt.event.MouseEvent;
018import java.io.BufferedInputStream;
019import java.io.BufferedOutputStream;
020import java.io.BufferedReader;
021import java.io.File;
022import java.io.FileOutputStream;
023import java.io.IOException;
024import java.io.InputStream;
025import java.io.InputStreamReader;
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.JPanel;
037import javax.swing.JPopupMenu;
038import javax.swing.JScrollPane;
039import javax.swing.JTabbedPane;
040import javax.swing.JTable;
041import javax.swing.JViewport;
042import javax.swing.ListSelectionModel;
043import javax.swing.SingleSelectionModel;
044import javax.swing.SwingConstants;
045import javax.swing.SwingUtilities;
046import javax.swing.UIManager;
047import javax.swing.border.EmptyBorder;
048import javax.swing.event.ChangeEvent;
049import javax.swing.event.ChangeListener;
050import javax.swing.event.ListSelectionEvent;
051import javax.swing.event.ListSelectionListener;
052import javax.swing.filechooser.FileFilter;
053import javax.swing.table.AbstractTableModel;
054import javax.swing.table.DefaultTableCellRenderer;
055import javax.swing.table.TableCellRenderer;
056import javax.swing.table.TableModel;
057
058import org.openstreetmap.josm.Main;
059import org.openstreetmap.josm.actions.ExtensionFileFilter;
060import org.openstreetmap.josm.actions.JosmAction;
061import org.openstreetmap.josm.actions.PreferencesAction;
062import org.openstreetmap.josm.gui.ExtendedDialog;
063import org.openstreetmap.josm.gui.PleaseWaitRunnable;
064import org.openstreetmap.josm.gui.SideButton;
065import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
066import org.openstreetmap.josm.gui.mappaint.MapPaintStyles.MapPaintStyleLoader;
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 implements Main.WindowSwitchListener {
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);
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);
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 toOtherApplication() {
192        // nothing
193    }
194
195    @Override
196    public void fromOtherApplication() {
197        // Reload local styles when they have been changed in an external editor.
198        // Checks file modification time.
199        List<StyleSource> toReload = new ArrayList<StyleSource>();
200        for (StyleSource s : MapPaintStyles.getStyles().getStyleSources()) {
201            if (s.isLocal()) {
202                File f = new File(s.url);
203                long mtime = f.lastModified();
204                if (mtime > s.getLastMTime()) {
205                    toReload.add(s);
206                    s.setLastMTime(mtime);
207                }
208            }
209        }
210        if (!toReload.isEmpty()) {
211            Main.info(trn("Reloading {0} map style.", "Reloading {0} map styles.", toReload.size(), toReload.size()));
212            Main.worker.submit(new MapPaintStyleLoader(toReload));
213        }
214    }
215
216    @Override
217    public void showNotify() {
218        MapPaintStyles.addMapPaintSylesUpdateListener(model);
219        Main.main.menu.wireFrameToggleAction.addButtonModel(cbWireframe.getModel());
220        if (Main.pref.getBoolean("mappaint.auto_reload_local_styles", true)) {
221            Main.addWindowSwitchListener(this);
222        }
223    }
224
225    @Override
226    public void hideNotify() {
227        Main.main.menu.wireFrameToggleAction.removeButtonModel(cbWireframe.getModel());
228        MapPaintStyles.removeMapPaintSylesUpdateListener(model);
229        if (Main.pref.getBoolean("mappaint.auto_reload_local_styles", true)) {
230            Main.removeWindowSwitchListener(this);
231        }
232    }
233
234    protected class StylesModel extends AbstractTableModel implements MapPaintSylesUpdateListener {
235
236        List<StyleSource> data = new ArrayList<StyleSource>();
237
238        public StylesModel() {
239            data = new ArrayList<StyleSource>(MapPaintStyles.getStyles().getStyleSources());
240        }
241
242        private StyleSource getRow(int i) {
243            return data.get(i);
244        }
245
246        @Override
247        public int getColumnCount() {
248            return 2;
249        }
250
251        @Override
252        public int getRowCount() {
253            return data.size();
254        }
255
256        @Override
257        public Object getValueAt(int row, int column) {
258            if (column == 0)
259                return getRow(row).active;
260            else
261                return getRow(row);
262        }
263
264        @Override
265        public boolean isCellEditable(int row, int column) {
266            return column == 0;
267        }
268
269        Class<?>[] columnClasses = {Boolean.class, StyleSource.class};
270
271        @Override
272        public Class<?> getColumnClass(int column) {
273            return columnClasses[column];
274        }
275
276        @Override
277        public void setValueAt(Object aValue, int row, int column) {
278            if (row < 0 || row >= getRowCount() || aValue == null)
279                return;
280            if (column == 0) {
281                MapPaintStyles.toggleStyleActive(row);
282            }
283        }
284
285        /**
286         * Make sure the first of the selected entry is visible in the
287         * views of this model.
288         */
289        public void ensureSelectedIsVisible() {
290            int index = selectionModel.getMinSelectionIndex();
291            if (index < 0) return;
292            if (index >= getRowCount()) return;
293            tblStyles.scrollToVisible(index, 0);
294            tblStyles.repaint();
295        }
296
297        /**
298         * MapPaintSylesUpdateListener interface
299         */
300
301        @Override
302        public void mapPaintStylesUpdated() {
303            data = new ArrayList<StyleSource>(MapPaintStyles.getStyles().getStyleSources());
304            fireTableDataChanged();
305            tblStyles.repaint();
306        }
307
308        @Override
309        public void mapPaintStyleEntryUpdated(int idx) {
310            data = new ArrayList<StyleSource>(MapPaintStyles.getStyles().getStyleSources());
311            fireTableRowsUpdated(idx, idx);
312            tblStyles.repaint();
313        }
314    }
315
316    private class MyCheckBoxRenderer extends JCheckBox implements TableCellRenderer {
317
318        public MyCheckBoxRenderer() {
319            setHorizontalAlignment(SwingConstants.CENTER);
320            setVerticalAlignment(SwingConstants.CENTER);
321        }
322
323        @Override
324        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,int row,int column) {
325            if (value == null)
326                return this;
327            boolean b = (Boolean) value;
328            setSelected(b);
329            setEnabled(!cbWireframe.isSelected());
330            return this;
331        }
332    }
333
334    private class StyleSourceRenderer extends DefaultTableCellRenderer {
335        @Override
336        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
337            if (value == null)
338                return this;
339            StyleSource s = (StyleSource) value;
340            JLabel label = (JLabel)super.getTableCellRendererComponent(table,
341                    s.getDisplayString(), isSelected, hasFocus, row, column);
342            label.setIcon(s.getIcon());
343            label.setToolTipText(s.getToolTipText());
344            label.setEnabled(!cbWireframe.isSelected());
345            return label;
346        }
347    }
348
349    protected class OnOffAction extends AbstractAction implements ListSelectionListener {
350        public OnOffAction() {
351            putValue(NAME, tr("On/Off"));
352            putValue(SHORT_DESCRIPTION, tr("Turn selected styles on or off"));
353            putValue(SMALL_ICON, ImageProvider.get("apply"));
354            updateEnabledState();
355        }
356
357        protected void updateEnabledState() {
358            setEnabled(!cbWireframe.isSelected() && tblStyles.getSelectedRowCount() > 0);
359        }
360
361        @Override
362        public void valueChanged(ListSelectionEvent e) {
363            updateEnabledState();
364        }
365
366        @Override
367        public void actionPerformed(ActionEvent e) {
368            int[] pos = tblStyles.getSelectedRows();
369            MapPaintStyles.toggleStyleActive(pos);
370            selectionModel.clearSelection();
371            for (int p: pos) {
372                selectionModel.addSelectionInterval(p, p);
373            }
374        }
375    }
376
377    /**
378     * The action to move down the currently selected entries in the list.
379     */
380    protected class MoveUpDownAction extends AbstractAction implements ListSelectionListener {
381
382        final int increment;
383
384        public MoveUpDownAction(boolean isDown) {
385            increment = isDown ? 1 : -1;
386            putValue(NAME, isDown?tr("Down"):tr("Up"));
387            putValue(SMALL_ICON, isDown ? ImageProvider.get("dialogs", "down") : ImageProvider.get("dialogs", "up"));
388            putValue(SHORT_DESCRIPTION, isDown ? tr("Move the selected entry one row down.") : tr("Move the selected entry one row up."));
389            updateEnabledState();
390        }
391
392        public void updateEnabledState() {
393            int[] sel = tblStyles.getSelectedRows();
394            setEnabled(!cbWireframe.isSelected() && MapPaintStyles.canMoveStyles(sel, increment));
395        }
396
397        @Override
398        public void actionPerformed(ActionEvent e) {
399            int[] sel = tblStyles.getSelectedRows();
400            MapPaintStyles.moveStyles(sel, increment);
401
402            selectionModel.clearSelection();
403            for (int row: sel) {
404                selectionModel.addSelectionInterval(row + increment, row + increment);
405            }
406            model.ensureSelectedIsVisible();
407        }
408
409        @Override
410        public void valueChanged(ListSelectionEvent e) {
411            updateEnabledState();
412        }
413    }
414
415    protected class ReloadAction extends AbstractAction implements ListSelectionListener {
416        public ReloadAction() {
417            putValue(NAME, tr("Reload from file"));
418            putValue(SHORT_DESCRIPTION, tr("reload selected styles from file"));
419            putValue(SMALL_ICON, ImageProvider.get("dialogs", "refresh"));
420            setEnabled(getEnabledState());
421        }
422
423        protected boolean getEnabledState() {
424            if (cbWireframe.isSelected())
425                return false;
426            int[] pos = tblStyles.getSelectedRows();
427            if (pos.length == 0)
428                return false;
429            for (int i : pos) {
430                if (!model.getRow(i).isLocal())
431                    return false;
432            }
433            return true;
434        }
435
436        @Override
437        public void valueChanged(ListSelectionEvent e) {
438            setEnabled(getEnabledState());
439        }
440
441        @Override
442        public void actionPerformed(ActionEvent e) {
443            final int[] rows = tblStyles.getSelectedRows();
444            MapPaintStyles.reloadStyles(rows);
445            Main.worker.submit(new Runnable() {
446                @Override
447                public void run() {
448                    SwingUtilities.invokeLater(new Runnable() {
449                        @Override
450                        public void run() {
451                            selectionModel.clearSelection();
452                            for (int r: rows) {
453                                selectionModel.addSelectionInterval(r, r);
454                            }
455                        }
456                    });
457
458                }
459            });
460        }
461    }
462
463    protected class SaveAsAction extends AbstractAction {
464
465        public SaveAsAction() {
466            putValue(NAME, tr("Save as..."));
467            putValue(SHORT_DESCRIPTION, tr("Save a copy of this Style to file and add it to the list"));
468            putValue(SMALL_ICON, ImageProvider.get("copy"));
469            setEnabled(tblStyles.getSelectedRows().length == 1);
470        }
471
472        @Override
473        public void actionPerformed(ActionEvent e) {
474            int sel = tblStyles.getSelectionModel().getLeadSelectionIndex();
475            if (sel < 0 || sel >= model.getRowCount())
476                return;
477            final StyleSource s = model.getRow(sel);
478
479            JFileChooserManager fcm = new JFileChooserManager(false, "mappaint.clone-style.lastDirectory", System.getProperty("user.home"));
480            String suggestion = fcm.getInitialDirectory() + File.separator + s.getFileNamePart();
481
482            FileFilter ff;
483            if (s instanceof MapCSSStyleSource) {
484                ff = new ExtensionFileFilter("mapcss,css,zip", "mapcss", tr("Map paint style file (*.mapcss, *.zip)"));
485            } else {
486                ff = new ExtensionFileFilter("xml,zip", "xml", tr("Map paint style file (*.xml, *.zip)"));
487            }
488            fcm.createFileChooser(false, null, Arrays.asList(ff, FileFilterAllFiles.getInstance()), ff, JFileChooser.FILES_ONLY)
489                    .getFileChooser().setSelectedFile(new File(suggestion));
490            JFileChooser fc = fcm.openFileChooser();
491            if (fc == null)
492                return;
493            Main.worker.submit(new SaveToFileTask(s, fc.getSelectedFile()));
494        }
495
496        private class SaveToFileTask extends PleaseWaitRunnable {
497            private StyleSource s;
498            private File file;
499
500            private boolean canceled;
501            private boolean error;
502
503            public SaveToFileTask(StyleSource s, File file) {
504                super(tr("Reloading style sources"));
505                this.s = s;
506                this.file = file;
507            }
508
509            @Override
510            protected void cancel() {
511                canceled = true;
512            }
513
514            @Override
515            protected void realRun() {
516                getProgressMonitor().indeterminateSubTask(
517                        tr("Save style ''{0}'' as ''{1}''", s.getDisplayString(), file.getPath()));
518                BufferedInputStream bis = null;
519                BufferedOutputStream bos = null;
520                try {
521                    InputStream in = s.getSourceInputStream();
522                    try {
523                        bis = new BufferedInputStream(in);
524                        bos = new BufferedOutputStream(new FileOutputStream(file));
525                        byte[] buffer = new byte[4096];
526                        int length;
527                        while ((length = bis.read(buffer)) > -1 && !canceled) {
528                            bos.write(buffer, 0, length);
529                        }
530                    } finally {
531                        s.closeSourceInputStream(in);
532                    }
533                } catch (IOException e) {
534                    error = true;
535                } finally {
536                    Utils.close(bis);
537                    Utils.close(bos);
538                }
539            }
540
541            @Override
542            protected void finish() {
543                SwingUtilities.invokeLater(new Runnable() {
544                    @Override
545                    public void run() {
546                        if (!error && !canceled) {
547                            SourceEntry se = new SourceEntry(s);
548                            se.url = file.getPath();
549                            MapPaintStyles.addStyle(se);
550                            tblStyles.getSelectionModel().setSelectionInterval(model.getRowCount() - 1 , model.getRowCount() - 1);
551                            model.ensureSelectedIsVisible();
552                        }
553                    }
554                });
555            }
556        }
557    }
558
559    protected class InfoAction extends AbstractAction {
560
561        boolean errorsTabLoaded;
562        boolean sourceTabLoaded;
563
564        public InfoAction() {
565            putValue(NAME, tr("Info"));
566            putValue(SHORT_DESCRIPTION, tr("view meta information, error log and source definition"));
567            putValue(SMALL_ICON, ImageProvider.get("info"));
568            setEnabled(tblStyles.getSelectedRows().length == 1);
569        }
570
571        @Override
572        public void actionPerformed(ActionEvent e) {
573            int sel = tblStyles.getSelectionModel().getLeadSelectionIndex();
574            if (sel < 0 || sel >= model.getRowCount())
575                return;
576            final StyleSource s = model.getRow(sel);
577            ExtendedDialog info = new ExtendedDialog(Main.parent, tr("Map Style info"), new String[] {tr("Close")});
578            info.setPreferredSize(new Dimension(600, 400));
579            info.setButtonIcons(new String[] {"ok.png"});
580
581            final JTabbedPane tabs = new JTabbedPane();
582
583            tabs.add("Info", buildInfoPanel(s));
584            JLabel lblInfo = new JLabel(tr("Info"));
585            lblInfo.setFont(lblInfo.getFont().deriveFont(Font.PLAIN));
586            tabs.setTabComponentAt(0, lblInfo);
587
588            final JPanel pErrors = new JPanel(new GridBagLayout());
589            tabs.add("Errors", pErrors);
590            JLabel lblErrors;
591            if (s.getErrors().isEmpty()) {
592                lblErrors = new JLabel(tr("Errors"));
593                lblErrors.setFont(lblInfo.getFont().deriveFont(Font.PLAIN));
594                lblErrors.setEnabled(false);
595                tabs.setTabComponentAt(1, lblErrors);
596                tabs.setEnabledAt(1, false);
597            } else {
598                lblErrors = new JLabel(tr("Errors"), ImageProvider.get("misc", "error"), JLabel.HORIZONTAL);
599                tabs.setTabComponentAt(1, lblErrors);
600            }
601
602            final JPanel pSource = new JPanel(new GridBagLayout());
603            tabs.addTab("Source", pSource);
604            JLabel lblSource = new JLabel(tr("Source"));
605            lblSource.setFont(lblSource.getFont().deriveFont(Font.PLAIN));
606            tabs.setTabComponentAt(2, lblSource);
607
608            tabs.getModel().addChangeListener(new ChangeListener() {
609                @Override
610                public void stateChanged(ChangeEvent e) {
611                    if (!errorsTabLoaded && ((SingleSelectionModel) e.getSource()).getSelectedIndex() == 1) {
612                        errorsTabLoaded = true;
613                        buildErrorsPanel(s, pErrors);
614                    }
615                    if (!sourceTabLoaded && ((SingleSelectionModel) e.getSource()).getSelectedIndex() == 2) {
616                        sourceTabLoaded = true;
617                        buildSourcePanel(s, pSource);
618                    }
619                }
620            });
621            info.setContent(tabs, false);
622            info.showDialog();
623        }
624
625        private JPanel buildInfoPanel(StyleSource s) {
626            JPanel p = new JPanel(new GridBagLayout());
627            StringBuilder text = new StringBuilder("<table cellpadding=3>");
628            text.append(tableRow(tr("Title:"), s.getDisplayString()));
629            if (s.url.startsWith("http://")) {
630                text.append(tableRow(tr("URL:"), s.url));
631            } else if (s.url.startsWith("resource://")) {
632                text.append(tableRow(tr("Built-in Style, internal path:"), s.url));
633            } else {
634                text.append(tableRow(tr("Path:"), s.url));
635            }
636            if (s.icon != null) {
637                text.append(tableRow(tr("Icon:"), s.icon));
638            }
639            if (s.getBackgroundColorOverride() != null) {
640                text.append(tableRow(tr("Background:"), Utils.toString(s.getBackgroundColorOverride())));
641            }
642            text.append(tableRow(tr("Style is currently active?"), s.active ? tr("Yes") : tr("No")));
643            text.append("</table>");
644            p.add(new JScrollPane(new HtmlPanel(text.toString())), GBC.eol().fill(GBC.BOTH));
645            return p;
646        }
647
648        private String tableRow(String firstColumn, String secondColumn) {
649            return "<tr><td><b>" + firstColumn + "</b></td><td>" + secondColumn + "</td></tr>";
650        }
651
652        private void buildSourcePanel(StyleSource s, JPanel p) {
653            JosmTextArea txtSource = new JosmTextArea();
654            txtSource.setFont(new Font("Monospaced", txtSource.getFont().getStyle(), txtSource.getFont().getSize()));
655            txtSource.setEditable(false);
656            p.add(new JScrollPane(txtSource), GBC.std().fill());
657
658            try {
659                InputStream is = s.getSourceInputStream();
660                try {
661                    BufferedReader reader = new BufferedReader(new InputStreamReader(is));
662                    try {
663                        String line;
664                        while ((line = reader.readLine()) != null) {
665                            txtSource.append(line + "\n");
666                        }
667                    } finally {
668                        reader.close();
669                    }
670                } finally {
671                    s.closeSourceInputStream(is);
672                }
673            } catch (IOException ex) {
674                txtSource.append("<ERROR: failed to read file!>");
675            }
676        }
677
678        private void buildErrorsPanel(StyleSource s, JPanel p) {
679            JosmTextArea txtErrors = new JosmTextArea();
680            txtErrors.setFont(new Font("Monospaced", txtErrors.getFont().getStyle(), txtErrors.getFont().getSize()));
681            txtErrors.setEditable(false);
682            p.add(new JScrollPane(txtErrors), GBC.std().fill());
683            for (Throwable t : s.getErrors()) {
684                txtErrors.append(t.toString() + "\n");
685            }
686        }
687    }
688
689    class PopupMenuHandler extends PopupMenuLauncher {
690        @Override
691        public void launch(MouseEvent evt) {
692            if (cbWireframe.isSelected())
693                return;
694            super.launch(evt);
695        }
696
697        @Override
698        protected void showMenu(MouseEvent evt) {
699            menu = new MapPaintPopup();
700            super.showMenu(evt);
701        }
702    }
703
704    /**
705     * The popup menu displayed when right-clicking a map paint entry
706     */
707    public class MapPaintPopup extends JPopupMenu {
708        /**
709         * Constructs a new {@code MapPaintPopup}.
710         */
711        public MapPaintPopup() {
712            add(reloadAction);
713            add(new SaveAsAction());
714            addSeparator();
715            add(new InfoAction());
716        }
717    }
718}