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